Compare commits
6 Commits
af541f1b7e
...
56e5db6898
Author | SHA1 | Date | |
---|---|---|---|
|
56e5db6898 | ||
|
702650e04e | ||
|
309c94bbfc | ||
|
ac12fafa16 | ||
|
694cd90645 | ||
|
220acccc08 |
|
@ -1,13 +1,43 @@
|
|||
import {isSpreadElement} from '@/astride/types.js';
|
||||
|
||||
export default function(node, {evaluate, scope}) {
|
||||
const elements = [];
|
||||
const asyncSpread = Object.create(null);
|
||||
let isAsync = false;
|
||||
for (const element of node.elements) {
|
||||
const {async, value} = evaluate(element, {scope});
|
||||
isAsync = isAsync || async;
|
||||
elements.push(value);
|
||||
for (const index in node.elements) {
|
||||
const element = node.elements[index];
|
||||
if (isSpreadElement(element)) {
|
||||
const {async, value} = evaluate(element.argument, {scope});
|
||||
isAsync = isAsync || async;
|
||||
if (async) {
|
||||
elements.push(value);
|
||||
asyncSpread[elements.length - 1] = true;
|
||||
}
|
||||
else {
|
||||
elements.push(...value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const {async, value} = evaluate(element, {scope});
|
||||
isAsync = isAsync || async;
|
||||
elements.push(value);
|
||||
}
|
||||
}
|
||||
return {
|
||||
async: !!isAsync,
|
||||
value: isAsync ? Promise.all(elements) : elements,
|
||||
value: !isAsync
|
||||
? elements
|
||||
: Promise.all(elements).then((elementsAndOrSpreads) => {
|
||||
const elements = [];
|
||||
for (let i = 0; i < elementsAndOrSpreads.length; ++i) {
|
||||
if (asyncSpread[i]) {
|
||||
elements.push(...elementsAndOrSpreads[i]);
|
||||
}
|
||||
else {
|
||||
elements.push(elementsAndOrSpreads[i]);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,12 +6,19 @@ import expression from '@/astride/test/expression.js';
|
|||
test('evaluates array of literals', async () => {
|
||||
expect(evaluate(await expression('[1.5, 2, "three"]')))
|
||||
.to.deep.include({value: [1.5, 2, 'three']});
|
||||
});
|
||||
|
||||
test('evaluates array containing promises', async () => {
|
||||
const evaluated = evaluate(await expression('[1.5, 2, await "three"]'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.equal([1.5, 2, 'three']);
|
||||
});
|
||||
|
||||
test('evaluates array spread', async () => {
|
||||
expect(evaluate(await expression('[...[4, 5, 6], 1.5, 2, "three"]')))
|
||||
.to.deep.include({value: [4, 5, 6, 1.5, 2, 'three']});
|
||||
const evaluated = evaluate(await expression('[...(await [4, 5, 6]), 1.5, 2, await "three"]'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.equal([4, 5, 6, 1.5, 2, 'three']);
|
||||
});
|
||||
|
|
31
app/ecs-components/emitter.js
Normal file
31
app/ecs-components/emitter.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default function(Component) {
|
||||
return class Emitter extends Component {
|
||||
mergeDiff(original, update) {
|
||||
const merged = {};
|
||||
if (update.emit) {
|
||||
merged.emit = {
|
||||
...original.emit,
|
||||
...update.emit,
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const Instance = super.instanceFromSchema();
|
||||
return class EmitterInstance extends Instance {
|
||||
emitting = [];
|
||||
id = 0;
|
||||
emit(specification) {
|
||||
Component.markChange(this.entity, 'emit', {[this.id++]: specification});
|
||||
}
|
||||
};
|
||||
}
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
});
|
||||
}
|
||||
}
|
29
app/ecs-components/sound.js
Normal file
29
app/ecs-components/sound.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default function(Component) {
|
||||
return class Sound extends Component {
|
||||
mergeDiff(original, update) {
|
||||
const merged = {};
|
||||
if (update.play) {
|
||||
merged.play = [
|
||||
...(original.play ?? []),
|
||||
...update.play,
|
||||
];
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const Instance = super.instanceFromSchema();
|
||||
return class SoundInstance extends Instance {
|
||||
play(source) {
|
||||
Component.markChange(this.entity, 'play', [source]);
|
||||
}
|
||||
};
|
||||
}
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -48,6 +48,12 @@ export default function(Component) {
|
|||
constructor(layer) {
|
||||
this.layer = layer;
|
||||
}
|
||||
get area() {
|
||||
return this.layer.area;
|
||||
}
|
||||
get source() {
|
||||
return this.layer.source;
|
||||
}
|
||||
stamp(at, data) {
|
||||
const changes = {};
|
||||
for (const row in data) {
|
||||
|
@ -72,6 +78,9 @@ export default function(Component) {
|
|||
}
|
||||
return this.layer.data[y * this.layer.area.x + x];
|
||||
}
|
||||
get tileSize() {
|
||||
return this.layer.tileSize;
|
||||
}
|
||||
}
|
||||
return new LayerProxy(layers[index]);
|
||||
};
|
||||
|
|
120
app/engine.js
120
app/engine.js
|
@ -45,6 +45,61 @@ export default class Engine {
|
|||
});
|
||||
}
|
||||
|
||||
acceptActions() {
|
||||
for (const [
|
||||
entity,
|
||||
payload,
|
||||
] of this.incomingActions) {
|
||||
const {Controlled, Inventory, Ticking, Wielder} = entity;
|
||||
switch (payload.type) {
|
||||
case 'changeSlot': {
|
||||
if (!Controlled.locked) {
|
||||
Wielder.activeSlot = payload.value - 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'moveUp':
|
||||
case 'moveRight':
|
||||
case 'moveDown':
|
||||
case 'moveLeft': {
|
||||
Controlled[payload.type] = payload.value;
|
||||
break;
|
||||
}
|
||||
case 'swapSlots': {
|
||||
if (!Controlled.locked) {
|
||||
Inventory.swapSlots(...payload.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'use': {
|
||||
if (!Controlled.locked) {
|
||||
Inventory.item(Wielder.activeSlot + 1).then(async (item) => {
|
||||
if (item) {
|
||||
const code = await(
|
||||
this.server.readAsset([
|
||||
item.source,
|
||||
payload.value ? 'start.js' : 'stop.js',
|
||||
].join('/'))
|
||||
.then((script) => (script.ok ? script.text() : ''))
|
||||
);
|
||||
if (code) {
|
||||
const context = {
|
||||
ecs: this.ecses[entity.Ecs.path],
|
||||
item,
|
||||
wielder: entity,
|
||||
};
|
||||
Ticking.addTickingPromise(Script.tickingPromise(code, context));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.incomingActions = [];
|
||||
}
|
||||
|
||||
async connectPlayer(connection, id) {
|
||||
const entityJson = await this.loadPlayer(id);
|
||||
if (!this.ecses[entityJson.Ecs.path]) {
|
||||
|
@ -112,6 +167,7 @@ export default class Engine {
|
|||
Controlled: {},
|
||||
Direction: {direction: 2},
|
||||
Ecs: {path: join('homesteads', `${id}`)},
|
||||
Emitter: {},
|
||||
Forces: {},
|
||||
Inventory: {
|
||||
slots: {
|
||||
|
@ -133,6 +189,7 @@ export default class Engine {
|
|||
Position: {x: 368, y: 368},
|
||||
VisibleAabb: {},
|
||||
Speed: {speed: 100},
|
||||
Sound: {},
|
||||
Sprite: {
|
||||
anchor: {x: 0.5, y: 0.8},
|
||||
animation: 'moving:down',
|
||||
|
@ -208,12 +265,20 @@ export default class Engine {
|
|||
await this.server.writeData(['players', `${id}`].join('/'), buffer);
|
||||
}
|
||||
|
||||
setClean() {
|
||||
for (const i in this.ecses) {
|
||||
this.ecses[i].setClean();
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.handle = setInterval(() => {
|
||||
const elapsed = (Date.now() - this.last) / 1000;
|
||||
this.last = Date.now();
|
||||
this.acceptActions();
|
||||
this.tick(elapsed);
|
||||
this.update(elapsed);
|
||||
this.setClean();
|
||||
this.frame += 1;
|
||||
}, 1000 / TPS);
|
||||
}
|
||||
|
@ -224,61 +289,6 @@ export default class Engine {
|
|||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const i in this.ecses) {
|
||||
this.ecses[i].setClean();
|
||||
}
|
||||
for (const [
|
||||
entity,
|
||||
payload,
|
||||
] of this.incomingActions) {
|
||||
const {Controlled, Inventory, Ticking, Wielder} = entity;
|
||||
switch (payload.type) {
|
||||
case 'changeSlot': {
|
||||
if (!Controlled.locked) {
|
||||
Wielder.activeSlot = payload.value - 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'moveUp':
|
||||
case 'moveRight':
|
||||
case 'moveDown':
|
||||
case 'moveLeft': {
|
||||
Controlled[payload.type] = payload.value;
|
||||
break;
|
||||
}
|
||||
case 'swapSlots': {
|
||||
if (!Controlled.locked) {
|
||||
Inventory.swapSlots(...payload.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'use': {
|
||||
if (!Controlled.locked) {
|
||||
Inventory.item(Wielder.activeSlot + 1).then(async (item) => {
|
||||
if (item) {
|
||||
const code = await(
|
||||
this.server.readAsset([
|
||||
item.source,
|
||||
payload.value ? 'start.js' : 'stop.js',
|
||||
].join('/'))
|
||||
.then((script) => (script.ok ? script.text() : ''))
|
||||
);
|
||||
if (code) {
|
||||
const context = {
|
||||
ecs: this.ecses[entity.Ecs.path],
|
||||
item,
|
||||
wielder: entity,
|
||||
};
|
||||
Ticking.addTickingPromise(Script.tickingPromise(code, context));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.incomingActions = [];
|
||||
for (const i in this.ecses) {
|
||||
this.ecses[i].tick(elapsed);
|
||||
}
|
||||
|
|
|
@ -42,11 +42,18 @@ export default function EcsComponent() {
|
|||
}
|
||||
const updatedEntities = {...entities};
|
||||
for (const id in payload.ecs) {
|
||||
if (false === payload.ecs[id]) {
|
||||
const update = payload.ecs[id];
|
||||
if (false === update) {
|
||||
delete updatedEntities[id];
|
||||
}
|
||||
else {
|
||||
updatedEntities[id] = ecs.get(id);
|
||||
if (update.Emitter?.emit) {
|
||||
updatedEntities[id].Emitter.emitting = {
|
||||
...updatedEntities[id].Emitter.emitting,
|
||||
...update.Emitter.emit,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
setEntities(updatedEntities);
|
||||
|
|
52
app/react-components/emitter.jsx
Normal file
52
app/react-components/emitter.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {Container} from '@pixi/display';
|
||||
import {PixiComponent} from '@pixi/react';
|
||||
import * as particles from '@pixi/particle-emitter';
|
||||
|
||||
const EmitterInternal = PixiComponent('Emitter', {
|
||||
$$emitter: undefined,
|
||||
$$raf: undefined,
|
||||
create() {
|
||||
return new Container();
|
||||
},
|
||||
applyProps(container, oldProps, newProps) {
|
||||
if (!this.$$emitter) {
|
||||
const {onComplete, particle} = newProps;
|
||||
this.$$emitter = new particles.Emitter(container, particle);
|
||||
this.$$emitter._completeCallback = onComplete;
|
||||
let last = Date.now();
|
||||
const render = () => {
|
||||
this.$$raf = requestAnimationFrame(render);
|
||||
const now = Date.now();
|
||||
this.$$emitter.update((now - last) / 1000);
|
||||
last = now;
|
||||
};
|
||||
this.$$emitter.emit = true;
|
||||
render();
|
||||
}
|
||||
},
|
||||
willUnmount() {
|
||||
if (this.$$emitter) {
|
||||
this.$$emitter.emit = false;
|
||||
cancelAnimationFrame(this.$$raf);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default function Emitter({entity}) {
|
||||
const {Emitter} = entity;
|
||||
const emitters = [];
|
||||
for (const id in Emitter.emitting) {
|
||||
const particle = Emitter.emitting[id];
|
||||
emitters.push(
|
||||
<EmitterInternal
|
||||
key={id}
|
||||
onComplete={() => {
|
||||
delete Emitter.emitting[id];
|
||||
}}
|
||||
particle={particle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <>{emitters}</>;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import {useCallback} from 'react';
|
|||
|
||||
import {useDebug} from '@/context/debug.js';
|
||||
|
||||
import Emitter from './emitter.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
|
||||
function Crosshair({x, y}) {
|
||||
|
@ -40,9 +41,16 @@ export default function Entities({entities}) {
|
|||
<Container
|
||||
key={id}
|
||||
>
|
||||
<Sprite
|
||||
entity={entity}
|
||||
/>
|
||||
{entity.Sprite && (
|
||||
<Sprite
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{entity.Emitter && (
|
||||
<Emitter
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{debug && (
|
||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
||||
)}
|
||||
|
|
|
@ -159,6 +159,11 @@ export default function Ui({disconnected}) {
|
|||
for (const id in payload.ecs) {
|
||||
const entity = ecs.get(id);
|
||||
const update = payload.ecs[id];
|
||||
if (update.Sound?.play) {
|
||||
for (const sound of update.Sound.play) {
|
||||
(new Audio(sound)).play();
|
||||
}
|
||||
}
|
||||
if (update?.MainEntity) {
|
||||
setMainEntity(localMainEntity = id);
|
||||
}
|
||||
|
|
21
package-lock.json
generated
21
package-lock.json
generated
|
@ -7,6 +7,7 @@
|
|||
"name": "silphius-next",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/particle-emitter": "^5.0.8",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"@pixi/spritesheet": "^7.4.2",
|
||||
"@pixi/tilemap": "^4.1.0",
|
||||
|
@ -3419,6 +3420,26 @@
|
|||
"@pixi/sprite": "7.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/particle-emitter": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/particle-emitter/-/particle-emitter-5.0.8.tgz",
|
||||
"integrity": "sha512-OzuZ4+esQo+zJ0u3htuNHHMAE8Ixmr3nz3tEfrTGZHje1vnGyie3ANQj9F0V4OM47oi9jd70njVCmeb7bTkS9A==",
|
||||
"peerDependencies": {
|
||||
"@pixi/constants": ">=6.0.4 <8.0.0",
|
||||
"@pixi/core": ">=6.0.4 <8.0.0",
|
||||
"@pixi/display": ">=6.0.4 <8.0.0",
|
||||
"@pixi/math": ">=6.0.4 <8.0.0",
|
||||
"@pixi/sprite": ">=6.0.4 <8.0.0",
|
||||
"@pixi/ticker": ">=6.0.4 <8.0.0"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"./",
|
||||
"test/pixi-v6-iife",
|
||||
"test/pixi-v6-module"
|
||||
]
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/prepare": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-7.4.2.tgz",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/particle-emitter": "^5.0.8",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"@pixi/spritesheet": "^7.4.2",
|
||||
"@pixi/tilemap": "^4.1.0",
|
||||
|
|
BIN
public/assets/hoe/dig.wav
Normal file
BIN
public/assets/hoe/dig.wav
Normal file
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
const {Controlled, Position, Sprite, Wielder} = wielder
|
||||
const {Controlled, Emitter, Position, Sound, Sprite, Wielder} = wielder
|
||||
const {TileLayers} = ecs.get(1)
|
||||
const layer = TileLayers.layer(0)
|
||||
const projected = Wielder.project(Position.tile, item.tool.projection)
|
||||
|
@ -6,7 +6,105 @@ const projected = Wielder.project(Position.tile, item.tool.projection)
|
|||
Controlled.locked = 1;
|
||||
const [, direction] = Sprite.animation.split(':');
|
||||
|
||||
const dirtParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: 200,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 60
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'rotation',
|
||||
config: {
|
||||
accel: 0,
|
||||
minSpeed: 0,
|
||||
maxSpeed: 0,
|
||||
minStart: 225,
|
||||
maxStart: 320
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.25,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.125,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: 'tileset/7',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.25,
|
||||
max: 0.25,
|
||||
},
|
||||
frequency: 0.01,
|
||||
emitterLifetime: 0.25,
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/assets/hoe/dig.wav');
|
||||
for (let i = 0; i < projected.length; ++i) {
|
||||
Emitter.emit({
|
||||
...dirtParticles,
|
||||
behaviors: [
|
||||
...dirtParticles.behaviors,
|
||||
{
|
||||
type: 'spawnShape',
|
||||
config: {
|
||||
type: 'rect',
|
||||
data: {
|
||||
x: projected[i].x * layer.tileSize.x,
|
||||
y: projected[i].y * layer.tileSize.y,
|
||||
w: layer.tileSize.x,
|
||||
h: layer.tileSize.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
await wait(300)
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
|
|
Loading…
Reference in New Issue
Block a user