Compare commits

..

6 Commits

Author SHA1 Message Date
cha0s
56e5db6898 fun: dirt 2024-06-26 04:18:54 -05:00
cha0s
702650e04e feat: emitter 2024-06-26 04:18:46 -05:00
cha0s
309c94bbfc chore: LayerProxy API 2024-06-26 04:16:14 -05:00
cha0s
ac12fafa16 feat: array spread 2024-06-26 04:15:52 -05:00
cha0s
694cd90645 feat: sound 2024-06-25 12:29:09 -05:00
cha0s
220acccc08 refactor: engine loop 2024-06-25 12:05:14 -05:00
14 changed files with 376 additions and 68 deletions

View File

@ -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) {
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;
}),
};
}

View File

@ -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']);
});

View 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: {},
});
}
}

View 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: {},
});
}
}

View File

@ -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]);
};

View File

@ -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);
}

View File

@ -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);

View 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}</>;
}

View File

@ -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}
>
{entity.Sprite && (
<Sprite
entity={entity}
/>
)}
{entity.Emitter && (
<Emitter
entity={entity}
/>
)}
{debug && (
<Crosshair x={entity.Position.x} y={entity.Position.y} />
)}

View File

@ -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
View File

@ -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",

View File

@ -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

Binary file not shown.

View File

@ -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(':');