refactor!: scrips n tickers

This commit is contained in:
cha0s 2024-10-17 23:35:34 -05:00
parent bea551fa19
commit 2041b38678
60 changed files with 3447 additions and 6499 deletions

View File

@ -28,6 +28,7 @@ module.exports = {
extends: ['eslint:recommended'],
rules: {
'no-constant-condition': ['error', {checkLoops: false}],
'require-yield': 0,
},
overrides: [
@ -60,10 +61,10 @@ module.exports = {
],
},
rules: {
'react/prop-types': 'off',
'jsx-a11y/label-has-associated-control': [2, {
controlComponents: ['SliderText'],
}],
'react/prop-types': 'off',
},
},
@ -80,14 +81,5 @@ module.exports = {
},
},
// Assets
{
files: [
'resources/**/*.js',
],
rules: {
'no-undef': 0,
},
}
],
};

View File

@ -19,10 +19,9 @@ export default class Alive extends Component {
this.$$dead = true;
const {Ticking} = ecs.get(this.entity);
if (Ticking) {
this.$$death.context.entity = ecs.get(this.entity);
this.$$death.locals.entity = ecs.get(this.entity);
const ticker = this.$$death.ticker();
ecs.addDestructionDependency(this.entity.id, ticker);
Ticking.add(ticker);
ecs.addDestructionDependency(this.entity.id, Ticking.add(ticker));
}
}
};

View File

@ -8,7 +8,7 @@ export default class Behaving extends Component {
tick(elapsed) {
const routine = this.$$routineInstances[this.currentRoutine];
if (routine) {
routine.context.entity = ecs.get(this.entity);
routine.locals.entity = ecs.get(this.entity);
routine.tick(elapsed);
}
}

View File

@ -76,23 +76,23 @@ export default class Collider extends Component {
if (!hasMatchingIntersection) {
if (this.$$collisionStart) {
const script = this.$$collisionStart.clone();
script.context.entity = thisEntity;
script.context.other = otherEntity;
script.context.pair = [body, otherBody];
script.locals.entity = thisEntity;
script.locals.other = otherEntity;
script.locals.pair = [body, otherBody];
const ticker = script.ticker();
ecs.addDestructionDependency(otherEntity.id, ticker);
ecs.addDestructionDependency(thisEntity.id, ticker);
thisEntity.Ticking.add(ticker);
const promise = thisEntity.Ticking.add(ticker);
ecs.addDestructionDependency(otherEntity.id, promise);
ecs.addDestructionDependency(thisEntity.id, promise);
}
if (other.$$collisionStart) {
const script = other.$$collisionStart.clone();
script.context.entity = otherEntity;
script.context.other = thisEntity;
script.context.pair = [otherBody, body];
script.locals.entity = otherEntity;
script.locals.other = thisEntity;
script.locals.pair = [otherBody, body];
const ticker = script.ticker();
ecs.addDestructionDependency(otherEntity.id, ticker);
ecs.addDestructionDependency(thisEntity.id, ticker);
otherEntity.Ticking.add(ticker);
const promise = otherEntity.Ticking.add(ticker);
ecs.addDestructionDependency(otherEntity.id, promise);
ecs.addDestructionDependency(thisEntity.id, promise);
}
activeIntersections.add(intersection);
}
@ -161,21 +161,21 @@ export default class Collider extends Component {
];
if (this.$$collisionEnd) {
const script = this.$$collisionEnd.clone();
script.context.other = otherEntity;
script.context.pair = [body, otherBody];
script.locals.other = otherEntity;
script.locals.pair = [body, otherBody];
const ticker = script.ticker();
ecs.addDestructionDependency(thisEntity.id, ticker);
ecs.addDestructionDependency(otherEntity.id, ticker);
thisEntity.Ticking.add(ticker);
const promise = thisEntity.Ticking.add(ticker);
ecs.addDestructionDependency(thisEntity.id, promise);
ecs.addDestructionDependency(otherEntity.id, promise);
}
if (other.$$collisionEnd) {
const script = other.$$collisionEnd.clone();
script.context.other = thisEntity;
script.context.pair = [otherBody, body];
script.locals.other = thisEntity;
script.locals.pair = [otherBody, body];
const ticker = script.ticker();
ecs.addDestructionDependency(thisEntity.id, ticker);
ecs.addDestructionDependency(otherEntity.id, ticker);
otherEntity.Ticking.add(ticker);
const promise = otherEntity.Ticking.add(ticker);
ecs.addDestructionDependency(thisEntity.id, promise);
ecs.addDestructionDependency(otherEntity.id, promise);
}
}
this.$$intersections.delete(other);

View File

@ -1,7 +1,7 @@
import Component from '@/ecs/component.js';
import Emitter from '@/particles/emitter.js';
import {Ticker as TickerPromise} from '@/util/promise.js';
import Ticker from '@/util/ticker.js';
export default class EmitterComponent extends Component {
instanceFromSchema() {
@ -12,21 +12,20 @@ export default class EmitterComponent extends Component {
id = 0;
emit(specification) {
if (specification.server) {
const {Ticker} = ecs.get(1);
if (Ticker) {
const master = ecs.get(1);
if (master.Ticking) {
const emitter = new Emitter(ecs);
const promise = new Promise((resolve) => {
emitter.emit().onEnd(resolve);
});
Ticker.add(
new TickerPromise(
(resolve) => {
promise.then(resolve);
},
(elapsed) => {
master.Ticking.add(
new Ticker(function* () {
let emitting = true;
emitter.emit().onEnd(() => {
emitting = false;
});
while (emitting) {
const elapsed = yield;
this.emitter.tick(elapsed);
}
)
}),
);
}
}

View File

@ -7,9 +7,9 @@ export default class Interactive extends Component {
$$interact;
interact(initiator) {
const script = this.$$interact.clone();
script.context.initiator = initiator;
script.context.subject = ecs.get(this.entity);
const {Ticking} = script.context.subject;
script.locals.initiator = initiator;
script.locals.subject = ecs.get(this.entity);
const {Ticking} = script.locals.subject;
Ticking.add(script.ticker());
}
get interacting() {

View File

@ -87,8 +87,8 @@ class ItemProxy {
}
}
if (this.scripts.projectionCheckInstance) {
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
this.scripts.projectionCheckInstance.context.projected = projected;
this.scripts.projectionCheckInstance.locals.ecs = this.Component.ecs;
this.scripts.projectionCheckInstance.locals.projected = projected;
return this.scripts.projectionCheckInstance.evaluate();
}
else {

View File

@ -1,4 +1,5 @@
import Component from '@/ecs/component.js';
import {withResolvers} from '@/util/promise.js';
export default class Ticking extends Component {
instanceFromSchema() {
@ -7,13 +8,9 @@ export default class Ticking extends Component {
$$tickers = [];
add(ticker) {
this.$$tickers.push(ticker);
ticker.then(() => {
this.$$tickers.splice(
this.$$tickers.indexOf(ticker),
1,
);
});
const resolvers = withResolvers();
this.$$tickers.push({resolvers, ticker});
return resolvers.promise;
}
destroy() {
@ -21,8 +18,14 @@ export default class Ticking extends Component {
}
tick(elapsed) {
for (const ticker of this.$$tickers) {
ticker.tick(elapsed);
for (let i = 0; i < this.$$tickers.length; ++i) {
const {resolvers, ticker} = this.$$tickers[i];
const result = ticker.tick(elapsed);
if (result.done) {
resolvers.resolve();
this.$$tickers.splice(i, 1);
i -= 1;
}
}
}

View File

@ -17,10 +17,10 @@ export default class Wielder extends Component {
let script = state ? startInstance : stopInstance;
if (script) {
script = script.clone();
script.context.ecs = ecs;
script.context.item = activeItem;
script.context.where = where;
script.context.wielder = entity;
script.locals.ecs = ecs;
script.locals.item = activeItem;
script.locals.where = where;
script.locals.wielder = entity;
Ticking.add(script.ticker());
}
}

View File

@ -1,15 +1,10 @@
import {Encoder, Decoder} from '@msgpack/msgpack';
import {LRUCache} from 'lru-cache';
import {withResolvers} from '@/util/promise.js';
import Script from '@/util/script.js';
import EntityFactory from './entity-factory.js';
const cache = new LRUCache({
max: 128,
});
const decoder = new Decoder();
const encoder = new Encoder();
@ -204,10 +199,10 @@ export default class Ecs {
}
createManySpecific(specificsList) {
if (0 === specificsList.length) {
return;
}
const entityIds = new Set();
if (0 === specificsList.length) {
return entityIds;
}
const creating = {};
for (let i = 0; i < specificsList.length; i++) {
const [entityId, components] = specificsList[i];
@ -417,35 +412,12 @@ export default class Ecs {
}
readJson(uri) {
const key = ['$$json', uri].join(':');
if (!cache.has(key)) {
const buffer = this.readAsset(uri);
const json = buffer.byteLength > 0
? JSON.parse(textDecoder.decode(buffer))
: {};
cache.set(key, json);
}
return cache.get(key);
const buffer = this.readAsset(uri);
return buffer.byteLength > 0 ? JSON.parse(textDecoder.decode(buffer)) : {};
}
readScript(uriOrCode, context = {}) {
if (!uriOrCode) {
return undefined;
}
let code = '';
if (!uriOrCode.startsWith('/')) {
code = uriOrCode;
}
else {
const buffer = this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = textDecoder.decode(buffer);
}
}
if (!code) {
return undefined;
}
return Script.fromCode(code, context);
readScript(path, locals = {}) {
return Script.load(path, locals);
}
rebuild(entityId, componentNames) {

View File

@ -28,6 +28,8 @@ addEventListener('message', (particle) => {
.onEnd(() => {});
});
postMessage(null);
let last = performance.now();
function tick(now) {
const elapsed = (now - last) / 1000;

View File

@ -317,19 +317,19 @@ function Ui({disconnected}) {
}, [mainEntityRef]);
useEcsTick(onEcsTick);
const onEcsTickParticles = useCallback((payload, ecs) => {
if (!('1' in payload) || particleWorker) {
return
if (!payload[1]?.AreaSize) {
return;
}
if (particleWorker) {
particleWorker.terminate();
}
const localParticleWorker = new Worker(
new URL('./particle-worker.js', import.meta.url),
{type: 'module'},
);
localParticleWorker.postMessage(ecs.get(1).toJSON());
setParticleWorker((particleWorker) => {
if (particleWorker) {
particleWorker.terminate();
}
return localParticleWorker;
localParticleWorker.addEventListener('message', () => {
localParticleWorker.postMessage(ecs.get(1).toJSON());
setParticleWorker(localParticleWorker);
});
}, [particleWorker]);
useEcsTick(onEcsTickParticles);

View File

@ -48,7 +48,7 @@ function createMaster() {
};
}
function createShitShack(id) {
function createShitShack() {
return {
Collider: {
bodies: [
@ -63,9 +63,6 @@ function createShitShack(id) {
},
],
},
Ecs: {
path: ['houses', `${id}`].join('/'),
},
Position: {x: 100, y: 100},
Sprite: {
anchorX: 0.5,
@ -77,7 +74,7 @@ function createShitShack(id) {
};
}
function createHouseTeleport(id) {
function createHouseTeleport() {
return {
Collider: {
bodies: [
@ -90,20 +87,7 @@ function createHouseTeleport(id) {
],
},
],
collisionStartScript: `
if (other.Player) {
ecs.switchEcs(
other,
'houses/${id}',
{
Position: {
x: 72,
y: 304,
},
},
);
}
`,
collisionStartScript: '/resources/homestead/house-teleport/collision-start.js',
},
Position: {x: 71, y: 113},
Ticking: {},
@ -127,16 +111,7 @@ function createChest() {
},
Interactive: {
interacting: 1,
interactScript: `
initiator.Player.openInventory = subject.Inventory;
// subject.Interlocutor.dialogue({
// body: "Sure, I'm a treasure chest. Probably. Do you really think that means you're about to get some treasure? Hah!",
// monopolizer: true,
// offset: {x: 0, y: -48},
// origin: 'track',
// position: 'track',
// })
`,
interactScript: '/resources/chest/interact.js',
},
Interlocutor: {},
Inventory: {
@ -184,16 +159,7 @@ function createTomato() {
],
},
],
collisionStartScript: [
'if (other.Inventory) {',
' other.Inventory.give({',
' qty: 1,',
" source: '/resources/tomato/tomato.json',",
' })',
' ecs.destroy(entity.id)',
' undefined;',
'}',
].join('\n'),
collisionStartScript: '/resources/tomato/collisiion-start.js',
},
Forces: {},
Magnetic: {},
@ -236,23 +202,7 @@ function createTestKitten() {
},
Interactive: {
interacting: 1,
interactScript: `
const lines = [
'mrowwr',
'p<shake>rrr</shake>o<wave>wwwww</wave>',
'mew<rate frequency={0.5}> </rate>mew!',
'me<wave>wwwww</wave>',
'\\\\*pu<shake>rrrrr</shake>\\\\*',
];
const line = lines[Math.floor(Math.random() * lines.length)];
subject.Interlocutor.dialogue({
body: line,
linger: 2,
offset: {x: 0, y: -16},
origin: 'track',
position: 'track',
})
`,
interactScript: '/resources/kitty/interact.js',
},
Position: {
x: 250 + (Math.random() - 0.5) * 300,
@ -292,20 +242,7 @@ function createTestCow() {
},
Interactive: {
interacting: 1,
interactScript: `
const lines = [
'sno<shake>rr</shake>t',
'm<wave>ooooooooooo</wave>',
];
const line = lines[Math.floor(Math.random() * lines.length)];
subject.Interlocutor.dialogue({
body: line,
linger: 2,
offset: {x: 0, y: -16},
origin: 'track',
position: 'track',
})
`,
interactScript: '/resources/farm/animals/cow-adult/interact.js',
},
Position: {
x: 350 + (Math.random() - 0.5) * 300,
@ -344,19 +281,7 @@ function createTestGoat() {
},
Interactive: {
interacting: 1,
interactScript: `
const lines = [
'Mind your own business, buddy.\\n\\ner, I mean, <shake>MEEHHHHHH</shake>',
];
const line = lines[Math.floor(Math.random() * lines.length)];
subject.Interlocutor.dialogue({
body: line,
linger: 2,
offset: {x: 0, y: -16},
origin: 'track',
position: 'track',
})
`,
interactScript: '/resources/farm/animals/goat-white/interact.js',
},
Position: {
x: 350 + (Math.random() - 0.5) * 300,
@ -385,20 +310,7 @@ function createTownTeleport() {
],
},
],
collisionStartScript: `
if (other.Player) {
ecs.switchEcs(
other,
'town',
{
Position: {
x: 940,
y: 480,
},
},
);
}
`,
collisionStartScript: '/resources/homestead/town-teleport/collision-start.js',
},
Position: {x: 8, y: 432},
@ -437,17 +349,18 @@ function createTomatoPlant() {
anchorY: 0.75,
animation: 'stage/0',
source: '/resources/tomato-plant/tomato-plant.json',
zIndex: 0,
},
Ticking: {},
VisibleAabb: {},
};
}
export default function createHomestead(id) {
export default function createHomestead() {
const entities = [];
entities.push(createMaster());
// entities.push(createShitShack(id));
// entities.push(createHouseTeleport(id));
// entities.push(createShitShack());
// entities.push(createHouseTeleport());
// entities.push(createTownTeleport());
// entities.push(createChest());
// for (let i = 0; i < 200; ++i) {

View File

@ -29,20 +29,7 @@ export default function createHouse(Ecs, id) {
],
},
],
collisionStartScript: `
if (other.Player) {
ecs.switchEcs(
other,
'homesteads/${id}',
{
Position: {
x: 74,
y: 128,
},
},
);
}
`,
collisionStartScript: '/resources/house/homestead-teleport/collision-start.js',
},
Position: {
x: 72,

View File

@ -37,20 +37,7 @@ export default function createTown() {
],
},
],
collisionStartScript: `
if (other.Player) {
ecs.switchEcs(
other,
['homesteads', other.Player.id].join('/'),
{
Position: {
x: 20,
y: 438,
},
},
);
}
`,
collisionStartScript: '/resources/town/homestead-teleport/collision-start.js',
},
Position: {x: 952, y: 480},
Ticking: {},

View File

@ -397,7 +397,7 @@ export default class Engine {
throw error;
}
const homestead = this.createEcs();
for (const entity of await createHomestead(id)) {
for (const entity of await createHomestead()) {
await homestead.create(entity);
}
await this.saveEcs(

View File

@ -122,7 +122,7 @@ if (import.meta.hot) {
delete engine.ecses['homesteads/0'];
await engine.server.removeData('homesteads/0');
const homestead = createEcs(engine.Ecs);
for (const entity of await createHomestead('0')) {
for (const entity of await createHomestead()) {
await homestead.create(entity);
}
await engine.saveEcs('homesteads/0', homestead);

View File

@ -1,32 +1,36 @@
import {Ticker} from '@/util/promise.js';
import Ticker from '@/util/ticker.js';
export default function delta(object, properties) {
const deltas = {};
for (const key in properties) {
const keys = new Set(Object.keys(properties));
function stop() {
keys.clear();
}
for (const key of keys) {
const property = properties[key];
const delta = {
duration: Infinity,
elapsed: 0,
...property,
stop: () => {
keys.delete(key);
},
};
deltas[key] = delta;
}
let stop;
const promise = new Ticker(
(resolve) => {
stop = resolve;
},
(elapsed, resolve) => {
for (const key in deltas) {
const ticker = new Ticker(function* () {
while (keys.size > 0) {
const elapsed = yield;
for (const key of keys) {
deltas[key].elapsed += elapsed;
object[key] += deltas[key].delta * elapsed;
if (deltas[key].elapsed >= deltas[key].duration) {
object[key] += deltas[key].delta * (deltas[key].duration - deltas[key].elapsed);
resolve();
return;
deltas[key].stop();
break;
}
object[key] += deltas[key].delta * elapsed;
}
},
);
return {stop, deltas, promise};
}
});
return {deltas, stop, ticker};
}

51
app/util/delta.test.js Normal file
View File

@ -0,0 +1,51 @@
import {expect, test} from 'vitest';
import delta from './delta.js';
test('mutates', () => {
const O = {x: 10};
const {ticker} = delta(O, {x: {delta: 20}});
expect(O.x).to.equal(10);
ticker.tick(0.5);
expect(O.x).to.equal(20);
ticker.tick(0.5);
expect(O.x).to.equal(30);
ticker.tick(0.5);
expect(O.x).to.equal(40);
ticker.tick(0.5);
expect(O.x).to.equal(50);
});
test('does not overshoot', () => {
const O = {x: 10};
const {ticker} = delta(O, {x: {delta: 20, duration: 0.75}});
ticker.tick(0.5);
expect(O.x).to.equal(20);
ticker.tick(0.5);
expect(O.x).to.equal(25);
ticker.tick(0.5);
expect(O.x).to.equal(25);
});
test('stops mutating', () => {
const O = {x: 10, y: 20};
const {deltas, stop, ticker} = delta(O, {x: {delta: 20}, y: {delta: 10}});
ticker.tick(0.5);
expect(O.x).to.equal(20);
expect(O.y).to.equal(25);
deltas.x.stop();
ticker.tick(0.5);
expect(O.x).to.equal(20);
expect(O.y).to.equal(30);
stop();
expect(O.x).to.equal(20);
expect(O.y).to.equal(30);
});
test('exposes deltas', () => {
const O = {x: 10};
const {deltas, ticker} = delta(O, {x: {delta: 20}});
deltas.x.delta = 50;
ticker.tick(0.5);
expect(O.x).to.equal(35);
});

View File

@ -1,4 +1,4 @@
import {Ticker, withResolvers} from '@/util/promise.js';
import Ticker from '@/util/ticker.js';
const Modulators = {
flat: () => 0.5,
@ -11,8 +11,11 @@ const Modulators = {
export default function lfo(object, properties) {
const oscillators = {};
const promises = [];
for (const key in properties) {
const keys = new Set(Object.keys(properties));
function stop() {
keys.clear();
}
for (const key of keys) {
const property = properties[key];
const oscillator = {
count: Infinity,
@ -35,38 +38,38 @@ export default function lfo(object, properties) {
}
}
oscillator.low = oscillator.median - oscillator.magnitude / 2;
({promise: oscillator.promise, resolve: oscillator.stop} = withResolvers());
oscillator.promise.then(() => {
delete oscillators[key];
});
promises.push(oscillator.promise);
oscillator.stop = () => {
keys.delete(key);
};
oscillator.compute = (elapsed) => {
const x = (oscillator.offset + (elapsed / oscillator.frequency)) % 1;
let y = 0;
for (const modulator of oscillator.modulators) {
y += modulator(x);
}
return oscillator.low + oscillator.magnitude * (y / oscillator.modulators.length);
}
oscillators[key] = oscillator;
}
let stop;
const promise = new Ticker(
(resolve) => {
stop = resolve;
Promise.all(promises).then(resolve);
},
(elapsed) => {
for (const key in oscillators) {
const ticker = new Ticker(function* () {
while (keys.size > 0) {
const elapsed = yield;
for (const key of keys) {
const oscillator = oscillators[key];
oscillator.elapsed += elapsed;
if (oscillator.elapsed >= oscillator.frequency) {
if (0 === --oscillator.count) {
oscillator.stop();
return;
}
oscillator.elapsed = oscillator.elapsed % oscillator.frequency;
const rollover = oscillator.elapsed >= oscillator.frequency;
if (rollover) {
oscillator.count -= 1;
oscillator.elapsed = 0 === oscillator.count
? oscillator.frequency
: oscillator.elapsed % oscillator.frequency;
}
const x = (oscillator.offset + (oscillator.elapsed / oscillator.frequency)) % 1;
let y = 0;
for (const modulator of oscillator.modulators) {
y += modulator(x);
object[key] = oscillator.compute(oscillator.elapsed);
if (rollover && 0 === oscillator.count) {
oscillator.stop();
}
object[key] = oscillator.low + oscillator.magnitude * (y / oscillator.modulators.length);
}
},
);
return {stop, oscillators, promise};
}
});
return {oscillators, stop, ticker};
}

103
app/util/lfo.test.js Normal file
View File

@ -0,0 +1,103 @@
import {expect, test} from 'vitest';
import lfo from './lfo.js';
test('mutates', () => {
const O = {x: 0.25};
const {ticker} = lfo(
O,
{
x: {
count: 1,
frequency: 0.5,
magnitude: 0.5,
modulators: ['sine'],
},
},
);
expect(O.x).to.equal(0.25);
ticker.tick(0.125);
expect(O.x).to.equal(0.5);
ticker.tick(0.125);
expect(O.x).to.be.closeTo(0.25, 0.0001);
ticker.tick(0.125);
expect(O.x).to.equal(0);
ticker.tick(0.125);
expect(O.x).to.equal(0.25);
ticker.tick(0.125);
expect(O.x).to.equal(0.25);
});
test('does not overshoot', () => {
const O = {x: 0.25};
const {oscillators, ticker} = lfo(
O,
{
x: {
count: 1,
frequency: 0.5,
magnitude: 0.5,
modulators: ['sine'],
},
},
);
ticker.tick(0.3);
expect(O.x).to.equal(oscillators.x.compute(0.3));
ticker.tick(0.3);
expect(O.x).to.equal(oscillators.x.compute(0.5));
});
test('stops mutating', () => {
const O = {x: 0.25, y: 0.5};
const {oscillators, stop, ticker} = lfo(
O,
{
x: {
count: 1,
frequency: 0.5,
magnitude: 0.5,
modulators: ['sine'],
},
y: {
count: 1,
frequency: 0.5,
magnitude: 0.5,
modulators: ['sine'],
offset: 0.25,
},
},
);
ticker.tick(0.2);
expect(O.x).to.equal(oscillators.x.compute(0.2));
expect(O.y).to.equal(oscillators.y.compute(0.2));
oscillators.x.stop();
ticker.tick(0.2);
expect(O.x).to.equal(oscillators.x.compute(0.2));
expect(O.y).to.equal(oscillators.y.compute(0.4));
stop();
ticker.tick(0.2);
expect(O.x).to.equal(oscillators.x.compute(0.2));
expect(O.y).to.equal(oscillators.y.compute(0.4));
});
test('exposes oscillators', () => {
const O = {x: 0};
const {oscillators, ticker} = lfo(
O,
{
x: {
count: 1,
frequency: 0.5,
magnitude: 0.5,
modulators: ['sine'],
},
},
);
ticker.tick(0.2);
expect(O.x).to.equal(oscillators.x.compute(0.2));
oscillators.x.count = 2;
ticker.tick(0.3);
expect(O.x).to.equal(oscillators.x.compute(0));
ticker.tick(0.5);
expect(O.x).to.equal(oscillators.x.compute(0.5));
});

View File

@ -12,63 +12,3 @@ export function withResolvers() {
});
return {promise, reject, resolve};
}
export class Ticker extends Promise {
constructor(executor, ticker) {
let _reject;
let _resolve;
super((resolve, reject) => {
_reject = reject;
_resolve = resolve;
if (executor) {
executor(resolve, reject);
}
});
this.reject = _reject;
this.resolve = _resolve;
this.ticker = ticker;
}
static all(promises) {
const tickers = [];
for (let i = 0; i < promises.length; i++) {
const promise = promises[i];
if (promise instanceof Ticker) {
tickers.push(promise);
// After resolution, stop ticking the promise.
promise.then(() => {
tickers.splice(tickers.indexOf(promise), 1);
});
}
}
/* v8 ignore next 3 */
if (0 === tickers.length) {
return super.all(promises);
}
return new Ticker(
(resolve, reject) => {
super.all(promises)
.then(resolve)
/* v8 ignore next */
.catch(reject);
},
(elapsed) => {
for (let i = 0; i < tickers.length; i++) {
tickers[i].tick(elapsed);
}
},
);
}
tick(elapsed) {
this.ticker(elapsed, this.resolve, this.reject);
}
then(...args) {
const promise = super.then(...args);
promise.ticker = this.ticker;
return promise;
}
}

View File

@ -1,100 +0,0 @@
import {expect, test} from 'vitest';
import {Ticker} from './promise.js';
test('runs executor', async () => {
expect(
await new Ticker((resolve) => {
resolve(32);
}),
)
.to.equal(32);
expect(
async () => {
await new Ticker((resolve, reject) => {
reject(new Error(''));
})
}
)
.rejects.toThrowError('');
});
test('ticks and resolves', async () => {
let done = false;
let e = 0;
const tp = new Ticker(undefined, (elapsed, resolve) => {
e += elapsed;
if (1 === e) {
done = true;
resolve(16);
}
});
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.true;
expect(await tp)
.to.equal(16);
});
test('ticks and rejects', async () => {
let caught = false;
const tp = new Ticker(undefined, (elapsed, resolve, reject) => {
reject(new Error());
});
tp.catch(() => {
caught = true;
});
expect(caught)
.to.be.false;
tp.tick(0.25);
await Promise.resolve();
expect(caught)
.to.be.true;
});
test('handles all', async () => {
let done = 0;
let e1 = 0, e2 = 0;
const tp1 = new Ticker(undefined, (elapsed, resolve) => {
e1 += elapsed;
if (1 === e1) {
done += 1;
resolve(16);
}
});
const tp2 = new Ticker(undefined, (elapsed, resolve) => {
e2 += elapsed;
if (2 === e2) {
done += 1;
resolve(32);
}
});
const tpa = Ticker.all([
Promise.resolve(8),
tp1,
tp2,
]);
expect(done)
.to.equal(0);
while (2 !== done) {
tpa.tick(0.25);
await Promise.resolve();
}
expect(e1)
.to.equal(1);
expect(e2)
.to.equal(2);
expect(await tpa)
.to.deep.equal([8, 16, 32]);
});

View File

@ -1,188 +1,111 @@
import {parse as acornParse} from 'acorn';
import {Runner} from 'astride';
import {LRUCache} from 'lru-cache';
import * as color from '@/util/color.js';
import delta from '@/util/delta.js';
import lfo from '@/util/lfo.js';
import * as MathUtil from '@/util/math.js';
import * as PromiseUtil from '@/util/promise.js';
import transition from '@/util/transition.js';
function parse(code, options = {}) {
return acornParse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
...options,
})
}
const Populated = Symbol.for('sandbox.populated');
export const cache = new LRUCache({
max: 128,
});
import Ticker from '@/util/ticker.js';
export default class Script {
constructor(sandbox, code) {
this.code = code;
this.sandbox = sandbox;
this.promise = null;
static registered = {};
constructor(fn, locals) {
if (!fn) {
throw new TypeError('Script needs a function');
}
this.fn = fn;
this.iterator = null;
this.locals = locals;
this.$$ticker = null;
}
clone() {
return new this.constructor(this.sandbox.clone(), this.code);
}
get context() {
return this.sandbox.locals;
}
static contextDefaults() {
return {
color,
console,
delta,
lfo,
Math: MathUtil,
Promise: PromiseUtil,
transition,
wait: (seconds = 0) => (
new PromiseUtil.Ticker(
(resolve) => {
if (0 === seconds) {
resolve();
}
},
(elapsed, resolve) => {
seconds -= elapsed;
if (seconds <= 0) {
resolve();
}
}
)
),
};
}
static createContext(locals = {}) {
if (locals[Populated]) {
return locals;
}
return {
[Populated]: true,
...this.contextDefaults(),
...locals,
};
return new this.constructor(this.fn, this.locals);
}
evaluate() {
this.sandbox.reset();
try {
const {value} = this.sandbox.step();
return value;
return this.fn(this.locals).next().value;
}
static load(pathOrFunction, locals) {
let fn;
if (this.registered[pathOrFunction]) {
fn = this.registered[pathOrFunction];
}
catch (error) {
console.warn(this.sandbox.$$stack);
console.warn(error);
else if (pathOrFunction) {
try {
fn = eval(pathOrFunction);
}
catch (error) {
console.error("Couldn't eval script", pathOrFunction);
console.error(error);
}
}
if (!fn) {
return undefined;
}
return new this(fn, locals);
}
static fromCode(code, context = {}) {
if (!cache.has(code)) {
cache.set(code, this.parse(code));
}
return new this(
new Runner(cache.get(code), this.createContext(context)),
code,
);
}
static parse(code) {
return parse(
code,
{
allowReturnOutsideFunction: true,
},
);
static register(path, fn) {
this.registered[path] = fn;
}
reset() {
this.promise = null;
this.sandbox.reset();
this.iterator = null;
this.$$ticker = null;
}
tick(elapsed, resolve, reject) {
if (this.promise) {
if (this.promise instanceof PromiseUtil.Ticker) {
this.promise.tick(elapsed);
tick(elapsed) {
this.locals.elapsed = elapsed;
if (this.$$ticker) {
const result = this.$$ticker.tick(elapsed);
this.locals.elapsed -= result.value;
if (result.done) {
this.$$ticker = null;
}
return;
}
while (true) {
this.sandbox.locals.elapsed = elapsed;
let async, done, value;
try {
({async, done, value} = this.sandbox.step());
}
catch (error) {
console.warn(this.sandbox.$$stack);
console.warn(error);
if (resolve) {
resolve();
}
return;
}
if (async || value instanceof Promise) {
this.promise = value;
value
.catch(reject ? reject : () => {})
.then(() => {
if (done) {
if (resolve) {
resolve();
}
}
})
.finally(() => {
this.promise = null;
});
break;
}
if (done) {
if (resolve) {
resolve();
}
break;
else {
return false;
}
}
if (!this.iterator) {
this.iterator = this.fn(this.locals);
}
let result;
do {
result = this.iterator.next();
if (result.value instanceof Ticker) {
this.$$ticker = result.value;
const tickerResult = this.$$ticker.tick(elapsed);
this.locals.elapsed -= tickerResult.value;
if (tickerResult.done) {
this.$$ticker = null;
}
else {
break;
}
}
} while (this.locals.elapsed > 0 && !result.done);
if (result.done) {
this.reset();
}
return result.done;
}
ticker() {
return new PromiseUtil.Ticker(
() => {},
(elapsed, resolve, reject) => {
this.tick(elapsed, resolve, reject);
},
);
}
static ticker(code, context = {}) {
let ticker;
return new PromiseUtil.Ticker(
(resolve) => {
this.fromCode(code, context)
.then((script) => {
ticker = script.ticker();
resolve(ticker);
})
},
(elapsed) => {
ticker?.tick?.(elapsed);
},
);
const self = this;
return new Ticker(function* () {
while (true) {
const elapsed = yield;
if (self.tick(elapsed)) {
break;
}
}
});
}
}
const imports = import.meta.glob(
'../../resources/**/*.js',
{eager: true, import: 'default'},
);
for (const path in imports) {
Script.register(path.slice('../..'.length), imports[path]);
}

52
app/util/ticker.js Normal file
View File

@ -0,0 +1,52 @@
export default class Ticker {
constructor(fn) {
this.fn = fn;
this.reset();
}
static all(tickers) {
tickers = [...tickers];
return new this(function* all() {
let consumed = 0;
while (tickers.length > 0) {
const elapsed = yield consumed;
consumed = 0;
for (let i = 0; i < tickers.length; ++i) {
const result = tickers[i].tick(elapsed);
if (result.value > consumed) {
consumed = result.value;
}
if (result.done) {
tickers.splice(i, 1);
i -= 1;
}
}
}
return consumed;
});
}
reset() {
this.iterator = null;
}
tick(elapsed) {
let result;
if (!this.iterator) {
this.iterator = this.fn();
result = this.iterator.next();
}
if (!result || !result.done) {
result = this.iterator.next(elapsed);
}
if (result.done) {
this.reset();
}
return result;
}
static wait(seconds = 0) {
return new this(function* wait() {
while (seconds > 0) {
const elapsed = yield;
seconds -= elapsed;
}
});
}
}

35
app/util/ticker.test.js Normal file
View File

@ -0,0 +1,35 @@
import {expect, test} from 'vitest';
import Ticker from './ticker.js';
function wait(seconds = 0) {
return new Ticker(function* wait() {
let elapsed = 0;
while (seconds > 0) {
elapsed = yield elapsed;
seconds -= elapsed;
}
return seconds + elapsed;
});
}
test('runs ticker', async () => {
const ticker = wait(1);
expect(ticker.tick(0.4)).to.deep.equal({done: false, value: 0.4});
expect(ticker.tick(0.4)).to.deep.equal({done: false, value: 0.4});
const result = ticker.tick(0.4);
expect(result.done).to.be.true;
expect(result.value).to.be.closeTo(0.2, 0.0001);
});
test('runs all tickers', async () => {
const ticker = Ticker.all([
wait(0.5),
wait(0.3),
]);
expect(ticker.tick(0.2)).to.deep.equal({done: false, value: 0.2});
expect(ticker.tick(0.2)).to.deep.equal({done: false, value: 0.2});
const result = ticker.tick(0.2);
expect(result.done).to.be.true;
expect(result.value).to.be.closeTo(0.1, 0.0001);
});

View File

@ -1,10 +1,14 @@
import {Ticker} from '@/util/promise.js';
import Ticker from '@/util/ticker.js';
import * as Easing from './easing';
export default function transition(object, properties) {
const transitions = {};
for (const key in properties) {
const keys = new Set(Object.keys(properties));
function stop() {
keys.clear();
}
for (const key of keys) {
const property = properties[key];
const transition = {
elapsed: 0,
@ -19,30 +23,31 @@ export default function transition(object, properties) {
transition.easing = Easing[transition.easing];
}
}
transition.stop = () => {
keys.delete(key);
},
transitions[key] = transition;
}
let stop;
const promise = new Ticker(
(resolve) => {
stop = resolve;
},
(elapsed, resolve) => {
for (const key in transitions) {
const ticker = new Ticker(function* () {
while (keys.size > 0) {
const elapsed = yield;
for (const key of keys) {
const transition = transitions[key];
transition.elapsed += elapsed;
if (transition.elapsed >= transition.duration) {
object[key] = transition.start + transition.magnitude;
resolve();
return;
transition.stop();
}
else {
object[key] = transition.easing(
transition.elapsed,
transition.start,
transition.magnitude,
transition.duration,
);
}
object[key] = transition.easing(
transition.elapsed,
transition.start,
transition.magnitude,
transition.duration,
);
}
},
);
return {stop, transitions, promise};
}
});
return {stop, ticker, transitions};
}

View File

@ -0,0 +1,91 @@
import {expect, test} from 'vitest';
import transition from './transition.js';
test('mutates', () => {
const O = {x: 0};
const {ticker} = transition(
O,
{
x: {
duration: 1,
easing: 'linear',
magnitude: 0.5,
},
},
);
expect(O.x).to.equal(0);
ticker.tick(0.5);
expect(O.x).to.equal(0.25);
ticker.tick(0.5);
expect(O.x).to.equal(0.5);
ticker.tick(0.5);
expect(O.x).to.equal(0.5);
});
test('does not overshoot', () => {
const O = {x: 0};
const {ticker} = transition(
O,
{
x: {
duration: 1,
easing: 'linear',
magnitude: 0.5,
},
},
);
ticker.tick(0.6);
expect(O.x).to.equal(0.3);
ticker.tick(0.6);
expect(O.x).to.equal(0.5);
});
test('stops mutating', () => {
const O = {x: 0, y: 0.25};
const {stop, ticker, transitions} = transition(
O,
{
x: {
duration: 1,
easing: 'linear',
magnitude: 0.5,
},
y: {
duration: 1,
easing: 'linear',
magnitude: 0.5,
},
},
);
ticker.tick(0.3);
expect(O.x).to.equal(0.15);
expect(O.y).to.equal(0.4);
transitions.x.stop();
ticker.tick(0.3);
expect(O.x).to.equal(0.15);
expect(O.y).to.equal(0.55);
stop();
ticker.tick(0.3);
expect(O.x).to.equal(0.15);
expect(O.y).to.equal(0.55);
});
test('exposes transitions', () => {
const O = {x: 0};
const {ticker, transitions} = transition(
O,
{
x: {
duration: 1,
easing: 'linear',
magnitude: 0.5,
},
},
);
ticker.tick(0.6);
expect(O.x).to.equal(0.3);
transitions.x.duration = 1.8;
ticker.tick(0.6);
expect(O.x).to.be.closeTo(0.3333, 0.0001);
});

7217
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,16 +27,13 @@
"@remix-run/express": "^2.9.2",
"@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.2",
"acorn": "^8.12.0",
"alea": "^1.0.1",
"astride": "file:../astride",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.18.2",
"idb-keyval": "^6.2.1",
"isbot": "^4.1.0",
"kefir": "^3.8.8",
"lru-cache": "^10.2.2",
"morgan": "^1.10.0",
"pixi.js": "^7.4.2",
"react": "^18.2.0",

View File

@ -1,110 +1,114 @@
const {Collider, Controlled, Interacts, Inventory, Sound, Sprite} = wielder
const entities = Collider.closest(Interacts.aabb());
for (const entity of entities) {
const {Emitter, Position, Tags} = entity;
if (Tags && Tags.has('kittan')) {
Controlled.locked = 1
const [, direction] = Sprite.animation.split(':')
for (let i = 0; i < 2; ++i) {
Sound.play('/resources/brush/brush.wav');
Sprite.animation = ['moving', direction].join(':');
await wait(0.3)
Sprite.animation = ['idle', direction].join(':');
await wait(0.1)
import Ticker from '@/util/ticker.js';
export default function*({wielder}) {
const {Collider, Controlled, Interacts, Inventory, Sound, Sprite} = wielder
const entities = Collider.closest(Interacts.aabb());
for (const entity of entities) {
const {Emitter, Position, Tags} = entity;
if (Tags && Tags.has('kittan')) {
Controlled.locked = 1
const [, direction] = Sprite.animation.split(':')
for (let i = 0; i < 2; ++i) {
Sound.play('/resources/brush/brush.wav');
Sprite.animation = ['moving', direction].join(':');
yield Ticker.wait(0.3)
Sprite.animation = ['idle', direction].join(':');
yield Ticker.wait(0.1)
}
Inventory.give({
qty: 1,
source: '/resources/furball/furball.json',
});
Controlled.locked = 0;
const heartParticles = {
behaviors: [
{
type: 'moveAcceleration',
config: {
accel: {
x: 0,
y: -100,
},
minStart: 0,
maxStart: 0,
rotate: false,
}
},
{
type: 'moveSpeed',
config: {
speed: {
list: [
{
time: 0,
value: 30
},
{
time: 1,
value: 0
}
]
}
}
},
{
type: 'scale',
config: {
scale: {
list: [
{
value: 0.5,
time: 0,
},
{
value: 0.125,
time: 1,
},
]
}
}
},
{
type: 'textureSingle',
config: {
texture: '/resources/heart/heart.png',
}
},
],
lifetime: {
min: 0.5,
max: 0.5,
},
frequency: 0.1,
emitterLifetime: 0.25,
pos: {
x: 0,
y: 0
},
rotation: 180,
};
Emitter.emit({
...heartParticles,
behaviors: [
...heartParticles.behaviors,
{
type: 'spawnShape',
config: {
type: 'rect',
data: {
x: Position.x - 8,
y: Position.y,
w: 16,
h: 1,
}
}
}
]
})
break;
}
Inventory.give({
qty: 1,
source: '/resources/furball/furball.json',
});
Controlled.locked = 0;
const heartParticles = {
behaviors: [
{
type: 'moveAcceleration',
config: {
accel: {
x: 0,
y: -100,
},
minStart: 0,
maxStart: 0,
rotate: false,
}
},
{
type: 'moveSpeed',
config: {
speed: {
list: [
{
time: 0,
value: 30
},
{
time: 1,
value: 0
}
]
}
}
},
{
type: 'scale',
config: {
scale: {
list: [
{
value: 0.5,
time: 0,
},
{
value: 0.125,
time: 1,
},
]
}
}
},
{
type: 'textureSingle',
config: {
texture: '/resources/heart/heart.png',
}
},
],
lifetime: {
min: 0.5,
max: 0.5,
},
frequency: 0.1,
emitterLifetime: 0.25,
pos: {
x: 0,
y: 0
},
rotation: 180,
};
Emitter.emit({
...heartParticles,
behaviors: [
...heartParticles.behaviors,
{
type: 'spawnShape',
config: {
type: 'rect',
data: {
x: Position.x - 8,
y: Position.y,
w: 16,
h: 1,
}
}
}
]
})
break;
}
}

View File

@ -0,0 +1,10 @@
export default function* ({initiator, subject}) {
initiator.Player.openInventory = subject.Inventory;
// subject.Interlocutor.dialogue({
// body: "Sure, I'm a treasure chest. Probably. Do you really think that means you're about to get some treasure? Hah!",
// monopolizer: true,
// offset: {x: 0, y: -48},
// origin: 'track',
// position: 'track',
// })
}

View File

@ -1,17 +1,22 @@
entity.Direction.direction = Math.random() * Math.TAU;
import * as Math from '@/util/math.js';
import Ticker from '@/util/ticker.js';
entity.Controlled.directionMove(entity.Direction.direction);
export default function*({entity}) {
entity.Direction.direction = Math.random() * Math.TAU;
await wait(0.25 + Math.random() * 2.25);
entity.Controlled.directionMove(entity.Direction.direction);
entity.Controlled.stop();
yield Ticker.wait(0.25 + Math.random() * 2.25);
entity.Sprite.isAnimating = 0;
entity.Controlled.stop();
await wait(1 + Math.random() * 3);
entity.Sprite.isAnimating = 0;
entity.Direction.direction = Math.random() * Math.TAU;
yield Ticker.wait(1 + Math.random() * 3);
await wait(0.5 + Math.random() * 2.5);
entity.Direction.direction = Math.random() * Math.TAU;
entity.Sprite.isAnimating = 1;
yield Ticker.wait(0.5 + Math.random() * 2.5);
entity.Sprite.isAnimating = 1;
}

View File

@ -0,0 +1,16 @@
import * as Math from '@/util/math.js';
export default function* ({subject}) {
const lines = [
'sno<shake>rr</shake>t',
'm<wave>ooooooooooo</wave>',
];
const line = lines[Math.floor(Math.random() * lines.length)];
subject.Interlocutor.dialogue({
body: line,
linger: 2,
offset: {x: 0, y: -16},
origin: 'track',
position: 'track',
})
}

View File

@ -1,17 +1,22 @@
entity.Direction.direction = Math.random() * Math.TAU;
import * as Math from '@/util/math.js';
import Ticker from '@/util/ticker.js';
entity.Controlled.directionMove(entity.Direction.direction);
export default function*({entity}) {
entity.Direction.direction = Math.random() * Math.TAU;
await wait(0.25 + Math.random() * 2.25);
entity.Controlled.directionMove(entity.Direction.direction);
entity.Controlled.stop();
yield Ticker.wait(0.25 + Math.random() * 2.25);
entity.Sprite.isAnimating = 0;
entity.Controlled.stop();
await wait(1 + Math.random() * 3);
entity.Sprite.isAnimating = 0;
entity.Direction.direction = Math.random() * Math.TAU;
yield Ticker.wait(1 + Math.random() * 3);
await wait(0.5 + Math.random() * 2.5);
entity.Direction.direction = Math.random() * Math.TAU;
entity.Sprite.isAnimating = 1;
yield Ticker.wait(0.5 + Math.random() * 2.5);
entity.Sprite.isAnimating = 1;
}

View File

@ -0,0 +1,15 @@
import * as Math from '@/util/math.js';
export default function* ({subject}) {
const lines = [
'Mind your own business, buddy.\n\ner, I mean, <shake>MEEHHHHHH</shake>',
];
const line = lines[Math.floor(Math.random() * lines.length)];
subject.Interlocutor.dialogue({
body: line,
linger: 2,
offset: {x: 0, y: -16},
origin: 'track',
position: 'track',
})
}

View File

@ -0,0 +1,5 @@
import delta from '@/util/delta.js';
export default function*({entity}) {
yield delta(entity.Forces, {forceY: {delta: 480, duration: 0.125}}).ticker;
}

View File

@ -4,11 +4,9 @@
"price": 100,
"projectionCheck": "/resources/hoe/projection-check.js",
"projection": {
"distance": [3, -1],
"distance": [1, 0],
"grid": [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
[1]
]
},
"start": "/resources/hoe/start.js"

View File

@ -1,15 +1,14 @@
const layer0 = ecs.get(1).TileLayers.layer(0)
const layer1 = ecs.get(1).TileLayers.layer(1)
const filtered = []
for (const position of projected) {
if (
[1, 2, 3, 4, 6].includes(layer0.tile(position))
&& ![7].includes(layer1.tile(position))
) {
filtered.push(position)
export default function*({ecs, projected}) {
const layer0 = ecs.get(1).TileLayers.layer(0);
const layer1 = ecs.get(1).TileLayers.layer(1);
const filtered = [];
for (const position of projected) {
if (
[1, 2, 3, 4, 6].includes(layer0.tile(position))
&& ![7].includes(layer1.tile(position))
) {
filtered.push(position);
}
}
return filtered;
}
return filtered

View File

@ -1,67 +1,71 @@
const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) {
import Ticker from '@/util/ticker.js';
const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers} = ecs.get(1)
const layer = TileLayers.layer(0)
export default function*({ecs, wielder}) {
const {Direction, Position, Wielder} = wielder;
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) {
Controlled.locked = 1
const [, direction] = Sprite.animation.split(':')
const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers} = ecs.get(1)
const layer = TileLayers.layer(0)
for (let i = 0; i < 2; ++i) {
Sound.play('/resources/hoe/dig.wav');
for (const {x, y} of projected) {
Emitter.emit({
entity: {
Behaving: {
routines: {
initial: 'await delta(entity.Forces, {forceY: {delta: 640, duration: 0.125}}).promise',
Controlled.locked = 1
const [, direction] = Sprite.animation.split(':')
for (let i = 0; i < 2; ++i) {
Sound.play('/resources/hoe/dig.wav');
for (const {x, y} of projected) {
Emitter.emit({
entity: {
Behaving: {
routines: {
initial: '/resources/hoe/dirt-particle.js',
},
},
Forces: {forceY: -80},
Position: {
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
},
Sprite: {
tint: 0x552200,
},
Ttl: {ttl: 0.35},
},
Forces: {forceY: -80},
Position: {
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
fields: [
{
path: ['Sprite', 'lightness'],
value: [0.05, 0.25],
},
{
path: ['Sprite', 'alpha'],
value: [0.5, 1],
},
{
path: ['Sprite', 'scale'],
value: [0.05, 0.1],
},
],
frequency: 0.05,
shape: {
type: 'filledRect',
payload: {width: 12, height: 12},
},
Sprite: {
tint: 0x552200,
},
Ttl: {ttl: 0.35},
},
fields: [
{
path: ['Sprite', 'lightness'],
value: [0.05, 0.25],
},
{
path: ['Sprite', 'alpha'],
value: [0.5, 1],
},
{
path: ['Sprite', 'scale'],
value: [0.05, 0.1],
},
],
frequency: 0.05,
shape: {
type: 'filledRect',
payload: {width: 12, height: 12},
},
spurt: 5,
ttl: 0.4,
});
spurt: 5,
ttl: 0.4,
});
}
Sprite.animation = ['moving', direction].join(':');
yield Ticker.wait(0.3)
Sprite.animation = ['idle', direction].join(':');
yield Ticker.wait(0.1)
}
Sprite.animation = ['moving', direction].join(':');
await wait(0.3)
Sprite.animation = ['idle', direction].join(':');
await wait(0.1)
for (const position of projected) {
TileLayers.layer(1).stamp(position, [[7]])
}
Controlled.locked = 0;
}
for (const position of projected) {
TileLayers.layer(1).stamp(position, [[7]])
}
Controlled.locked = 0;
}

View File

@ -0,0 +1,14 @@
export default function* ({ecs, other}) {
if (other.Player) {
ecs.switchEcs(
other,
['houses', other.Player.id].join('/'),
{
Position: {
x: 72,
y: 304,
},
},
);
}
}

View File

@ -0,0 +1,14 @@
export default function* ({ecs, other}) {
if (other.Player) {
ecs.switchEcs(
other,
'town',
{
Position: {
x: 940,
y: 480,
},
},
);
}
}

View File

@ -0,0 +1,14 @@
export default function* ({ecs, other}) {
if (other.Player) {
ecs.switchEcs(
other,
['homesteads', other.Player.id].join('/'),
{
Position: {
x: 74,
y: 128,
},
},
);
}
}

View File

@ -1,17 +1,14 @@
entity.Direction.direction = Math.random() * Math.TAU;
import * as Math from '@/util/math.js';
import Ticker from '@/util/ticker.js';
entity.Controlled.directionMove(entity.Direction.direction);
await wait(0.25 + Math.random() * 2.25);
entity.Controlled.stop();
entity.Sprite.isAnimating = 0;
await wait(1 + Math.random() * 3);
entity.Direction.direction = Math.random() * Math.TAU;
await wait(0.5 + Math.random() * 2.5);
entity.Sprite.isAnimating = 1;
export default function*({entity: {Controlled, Direction, Sprite}}) {
Direction.direction = Math.random() * Math.TAU;
Controlled.directionMove(Direction.direction);
yield Ticker.wait(0.25 + Math.random() * 2.25);
Controlled.stop();
Sprite.isAnimating = 0;
yield Ticker.wait(1 + Math.random() * 3);
Direction.direction = Math.random() * Math.TAU;
yield Ticker.wait(0.5 + Math.random() * 2.5);
Sprite.isAnimating = 1;
}

View File

@ -0,0 +1,19 @@
import * as Math from '@/util/math.js';
export default function* ({subject}) {
const lines = [
'mrowwr',
'p<shake>rrr</shake>o<wave>wwwww</wave>',
'mew<rate frequency={0.5}> </rate>mew!',
'me<wave>wwwww</wave>',
'\\\\*pu<shake>rrrrr</shake>\\\\*',
];
const line = lines[Math.floor(Math.random() * lines.length)];
subject.Interlocutor.dialogue({
body: line,
linger: 2,
offset: {x: 0, y: -16},
origin: 'track',
position: 'track',
});
}

View File

@ -1,12 +1,16 @@
const playerEntity = ecs.lookupPlayerEntity(entity.Owned.owner);
if (playerEntity !== other && other.Vulnerable) {
const magnitude = Math.floor(Math.random() * 2)
other.Vulnerable.damage({
amount: -Math.floor(
Math.pow(10, magnitude)
+ Math.random() * (Math.pow(10, magnitude + 1) - Math.pow(10, magnitude)),
),
position: other.Position.toJSON(),
type: other.Vulnerable.Types.PAIN,
})
import * as Math from '@/util/math.js';
export default function*({ecs, entity, other}) {
const playerEntity = ecs.lookupPlayerEntity(entity.Owned.owner);
if (playerEntity !== other && other.Vulnerable) {
const magnitude = Math.floor(Math.random() * 2)
other.Vulnerable.damage({
amount: -Math.floor(
Math.pow(10, magnitude)
+ Math.random() * (Math.pow(10, magnitude + 1) - Math.pow(10, magnitude)),
),
position: other.Position.toJSON(),
type: other.Vulnerable.Types.PAIN,
})
}
}

View File

@ -1,90 +1,98 @@
const {Player, Position} = wielder;
import * as Math from '@/util/math.js';
import Ticker from '@/util/ticker.js';
const EVERY = 0.03;
const N = 14;
const SPREAD = 1;
export default function*(locals) {
const {ecs, where, wielder} = locals;
const {Player, Position} = wielder;
const creating = [];
const EVERY = 0.03;
const N = 14;
const SPREAD = 1;
const offset = Math.random() * Math.TAU;
const offset = Math.random() * Math.TAU;
for (let i = 0; i < N; ++i) {
creating.push(ecs.get(ecs.create({
Collider: {
bodies: [
{
group: -1,
points: [
{x: -2.5, y: -2.5},
{x: 14, y: -2.5},
{x: 14, y: 2.5},
{x: -2.5, y: 2.5},
],
unstoppable: 1,
},
],
collisionStartScript: '/resources/magic-swords/collision-start.js',
const specs = [];
for (let i = 0; i < N; ++i) {
specs.push({
Collider: {
bodies: [
{
group: -1,
points: [
{x: -2.5, y: -2.5},
{x: 14, y: -2.5},
{x: 14, y: 2.5},
{x: -2.5, y: 2.5},
],
unstoppable: 1,
},
],
collisionStartScript: '/resources/magic-swords/collision-start.js',
},
Controlled: {},
Direction: {direction: offset + Math.TAU * (i / N)},
Forces: {},
Light: {brightness: 0},
Owned: {owner: Player ? Player.id : 0},
Position: {x: Position.x, y: Position.y},
Speed: {},
Sprite: {
alpha: 0,
source: '/resources/magic-swords/magic-sword-shot.json',
},
Ticking: {},
VisibleAabb: {},
});
}
const creating = Array.from(ecs.createMany(specs)).map((entityId) => ecs.get(entityId));
const shot = creating.shift();
shot.Sprite.alpha = 1;
const shots = [
{
accumulated: 0,
entity: shot,
},
Controlled: {},
Direction: {direction: offset + Math.TAU * (i / N)},
Forces: {},
Light: {brightness: 0},
Owned: {owner: Player ? Player.id : 0},
Position: {x: Position.x, y: Position.y},
Speed: {},
Sprite: {
alpha: 0,
source: '/resources/magic-swords/magic-sword-shot.json',
},
Ticking: {},
VisibleAabb: {},
})));
}
];
const shot = creating.shift();
shot.Sprite.alpha = 1;
const shots = [
{
accumulated: 0,
entity: shot,
},
];
let spawner = 0;
while (shots.length > 0) {
spawner += elapsed;
if (creating.length > 0 && spawner >= EVERY) {
const entity = creating.shift();
entity.Sprite.alpha = 1;
shots.push({accumulated: 0, entity})
spawner -= EVERY;
}
const destroying = [];
for (const shot of shots) {
shot.accumulated += elapsed;
if (shot.accumulated <= SPREAD) {
shot.entity.Speed.speed = 100 * (1 - (shot.accumulated / SPREAD))
let spawner = 0;
while (shots.length > 0) {
spawner += locals.elapsed;
if (creating.length > 0 && spawner >= EVERY) {
const entity = creating.shift();
entity.Sprite.alpha = 1;
shots.push({accumulated: 0, entity})
spawner -= EVERY;
}
else {
if (!shot.oriented) {
const toward = Math.atan2(
where.y - shot.entity.Position.y,
where.x - shot.entity.Position.x,
)
shot.entity.Speed.speed = 400;
shot.entity.Direction.direction = (Math.TAU + toward) % Math.TAU;
shot.oriented = true;
const destroying = [];
for (const shot of shots) {
shot.accumulated += locals.elapsed;
if (shot.accumulated <= SPREAD) {
shot.entity.Speed.speed = 100 * (1 - (shot.accumulated / SPREAD))
}
if (shot.accumulated > 1.5) {
shot.entity.Sprite.alpha = 0;
ecs.destroy(shot.entity.id);
destroying.push(shot);
else {
if (!shot.oriented) {
const toward = Math.atan2(
where.y - shot.entity.Position.y,
where.x - shot.entity.Position.x,
)
shot.entity.Speed.speed = 400;
shot.entity.Direction.direction = (Math.TAU + toward) % Math.TAU;
shot.oriented = true;
}
if (shot.accumulated > 1.5) {
shot.entity.Sprite.alpha = 0;
ecs.destroy(shot.entity.id);
destroying.push(shot);
}
}
shot.entity.Controlled.directionMove(shot.entity.Direction.direction);
}
shot.entity.Controlled.directionMove(shot.entity.Direction.direction);
for (let i = 0; i < destroying.length; ++i) {
shots.splice(shots.indexOf(destroying[i]), 1);
}
yield Ticker.wait();
}
for (let i = 0; i < destroying.length; ++i) {
shots.splice(shots.indexOf(destroying[i]), 1);
}
await wait();
}

View File

@ -1,22 +1,28 @@
const {Sprite, Ticking, Vulnerable} = entity;
if (Vulnerable) {
Vulnerable.isInvulnerable = 1;
}
if (Sprite) {
const {promise} = transition(
entity.Sprite,
{
scaleX: {
duration: 0.25,
magnitude: -entity.Sprite.scaleX,
import transition from '@/util/transition.js';
export default function*({ecs, entity}) {
const {Controlled, Sprite, Vulnerable} = entity;
if (Controlled) {
Controlled.locked = 1;
}
if (Vulnerable) {
Vulnerable.isInvulnerable = 1;
}
if (Sprite) {
const {ticker} = transition(
entity.Sprite,
{
scaleX: {
duration: 0.25,
magnitude: -entity.Sprite.scaleX,
},
scaleY: {
duration: 0.25,
magnitude: entity.Sprite.scaleY * 2,
},
},
scaleY: {
duration: 0.25,
magnitude: entity.Sprite.scaleY * 2,
},
},
)
Ticking.add(promise);
await promise;
ecs.destroy(entity.id);
);
yield ticker;
ecs.destroy(entity.id);
}
}

View File

@ -1,9 +1,10 @@
const amount = 50;
wielder.Health.health += amount
wielder.Vulnerable.damage({
amount,
position: wielder.Position.toJSON(),
type: wielder.Vulnerable.Types.HEALING,
})
item.qty -= 1
export default function*({item, wielder}) {
const amount = 50;
wielder.Health.health += amount;
wielder.Vulnerable.damage({
amount,
position: wielder.Position.toJSON(),
type: wielder.Vulnerable.Types.HEALING,
});
item.qty -= 1;
}

View File

@ -1,49 +0,0 @@
#!/usr/bin/env node
import {writeFileSync} from 'node:fs';
import {basename, dirname, extname, join} from 'node:path';
import imageSize from 'image-size';
const tileset = process.argv[2];
let w = parseInt(process.argv[3] || '0');
let h = parseInt(process.argv[4] || '0');
const {width, height} = imageSize(tileset);
if (0 === w) {
w = width;
}
if (0 === h) {
h = height;
}
const total = (width / w) * (height / h);
const json = {
frames: {},
meta: {
format: 'RGBA8888',
image: ['.', basename(tileset)].join('/'),
scale: 1,
size: {w: width, h: height},
},
};
const extlessPath = join(dirname(tileset), basename(tileset, extname(tileset)));
let i = 0;
for (let y = 0; y < height; y += h) {
for (let x = 0; x < width; x += w) {
json.frames[1 === total ? '' : join(extlessPath, `${i++}`)] = {
frame: {x, y, w, h},
spriteSourceSize: {x: 0, y: 0, w, h},
sourceSize: {w, h},
};
}
}
writeFileSync(
`${extlessPath}.json`,
JSON.stringify(json),
);

View File

@ -1,15 +1,19 @@
const {Interactive, Sprite} = ecs.get(plant.entity);
export default function*({ecs, plant}) {
const {Interactive, Sprite} = ecs.get(plant.entity);
plant.growth = 0
Sprite.zIndex = 65535;
if (plant.stage < 3) {
plant.stage += 1
}
if (4 === plant.stage) {
plant.stage = 3
}
if (3 === plant.stage) {
Interactive.interacting = true;
}
plant.growth = 0
Sprite.animation = ['stage', plant.stage].join('/')
if (plant.stage < 3) {
plant.stage += 1
}
if (4 === plant.stage) {
plant.stage = 3
}
if (3 === plant.stage) {
Interactive.interacting = true;
}
Sprite.animation = ['stage', plant.stage].join('/')
}

View File

@ -1,124 +1,118 @@
const {Interactive, Position, Plant, Sprite} = subject;
Interactive.interacting = false;
import delta from '@/util/delta.js';
import lfo from '@/util/lfo.js';
import * as Math from '@/util/math.js';
import Ticker from '@/util/ticker.js';
const ids = [];
export default function*({ecs, subject}) {
const {Interactive, Position, Plant, Sprite} = subject;
Interactive.interacting = false;
for (let i = 0; i < 10; ++i) {
ids.push(ecs.create({
Collider: {
bodies: [
{
points: [
{x: -4, y: -4},
{x: 3, y: -4},
{x: 3, y: 3},
{x: -4, y: 3},
],
},
],
collisionStartScript: [
'if (other.Inventory) {',
' other.Inventory.give({',
' qty: 1,',
" source: '/resources/tomato/tomato.json',",
' })',
' ecs.destroy(entity.id)',
' return undefined;',
'}',
].join('\n'),
},
Forces: {},
Magnetic: {},
Position: {x: Position.x, y: Position.y},
Sprite: {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.333,
scaleY: 0.333,
source: '/resources/tomato/tomato-sprite.json',
},
Ticking: {},
VisibleAabb: {},
}));
}
for (const id of ids) {
const tomato = ecs.get(id);
const {x, y} = Math.normalizeVector({
x: (Math.random() * 2) - 1,
y: (Math.random() * 2) - 1,
});
const d = delta(
tomato.Position,
{
y: {
duration: 0.5,
delta: 0,
const specs = [];
for (let i = 0; i < 10; ++i) {
specs.push({
Collider: {
bodies: [
{
points: [
{x: -4, y: -4},
{x: 3, y: -4},
{x: 3, y: 3},
{x: -4, y: 3},
],
},
],
collisionStartScript: '/resources/tomato/collision-start.js',
},
},
)
Forces: {},
Magnetic: {},
Position: {x: Position.x, y: Position.y},
Sprite: {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.333,
scaleY: 0.333,
source: '/resources/tomato/tomato-sprite.json',
},
Ticking: {},
VisibleAabb: {},
});
}
const tomatoes = Array.from(ecs.createMany(specs)).map((entityId) => ecs.get(entityId));
tomato.Ticking.add(
Promise.Ticker.all([
d.promise,
lfo(
d.deltas.y,
{
delta: {
count: 1,
frequency: 0.5,
magnitude: 64,
median: 0,
offset: -0.5,
},
// const tickers = [];
for (const tomato of tomatoes) {
const {x, y} = Math.normalizeVector({
x: (Math.random() * 2) - 1,
y: (Math.random() * 2) - 1,
});
const d = delta(
tomato.Position,
{
y: {
duration: 0.5,
delta: 0,
},
).promise,
lfo(
tomato.Sprite,
{
scaleX: {
count: 1,
frequency: 0.5,
magnitude: 0.333,
median: 0.333,
elapsed: 0.25,
offset: -0.5,
},
)
tomato.Ticking.add(
Ticker.all([
d.ticker,
lfo(
d.deltas.y,
{
delta: {
count: 1,
frequency: 0.5,
magnitude: 64,
median: 0,
offset: -0.5,
},
},
scaleY: {
count: 1,
frequency: 0.5,
magnitude: 0.333,
median: 0.333,
elapsed: 0.25,
offset: -0.5,
).ticker,
lfo(
tomato.Sprite,
{
scaleX: {
count: 1,
frequency: 0.5,
magnitude: 0.333,
median: 0.333,
elapsed: 0.25,
offset: -0.5,
},
scaleY: {
count: 1,
frequency: 0.5,
magnitude: 0.333,
median: 0.333,
elapsed: 0.25,
offset: -0.5,
},
},
},
).promise,
delta(
tomato.Position,
{
x: {
duration: 0.5,
delta: (12 * x) + Math.random() * 8,
).ticker,
delta(
tomato.Position,
{
x: {
duration: 0.5,
delta: (12 * x) + Math.random() * 8,
},
},
},
).promise,
delta(
tomato.Position,
{
y: {
duration: 0.5,
delta: (12 * y) + Math.random() * 8,
).ticker,
delta(
tomato.Position,
{
y: {
duration: 0.5,
delta: (12 * y) + Math.random() * 8,
},
},
},
).promise,
]),
);
).ticker,
]),
);
}
Plant.stage = 4;
Sprite.animation = ['stage', Plant.stage].join('/')
}
Plant.stage = 4;
Sprite.animation = ['stage', Plant.stage].join('/')

View File

@ -1,20 +1,21 @@
if (3 === plant.stage) {
return false
export default function*({ecs, plant}) {
if (3 === plant.stage) {
return false;
}
const {TileLayers, Water} = ecs.get(1);
const layer = TileLayers.layer(0);
const {Position} = ecs.get(plant.entity);
const x = (Position.x - layer.tileSize.x * 0.5) / layer.tileSize.x;
const y = (Position.y - layer.tileSize.y * 0.5) / layer.tileSize.y;
const tileIndex = layer.area.x * y + x;
if (!Water.water[tileIndex]) {
return false;
}
if (Water.water[tileIndex] < 32) {
return false;
}
if (Water.water[tileIndex] > 224) {
return false;
}
return true;
}
const {TileLayers, Water} = ecs.get(1);
const layer = TileLayers.layer(0)
const {Position} = ecs.get(plant.entity);
const x = (Position.x - layer.tileSize.x * 0.5) / layer.tileSize.x
const y = (Position.y - layer.tileSize.y * 0.5) / layer.tileSize.y
const tileIndex = layer.area.x * y + x
if (!Water.water[tileIndex]) {
return false
}
if (Water.water[tileIndex] < 32) {
return false
}
if (Water.water[tileIndex] > 224) {
return false
}
return true

View File

@ -1,29 +1,30 @@
const layer = ecs.get(1).TileLayers.layer(1)
const {tileSize} = layer;
export default function*({ecs, projected}) {
const layer = ecs.get(1).TileLayers.layer(1)
const {tileSize} = layer;
const filtered = []
const filtered = []
for (const position of projected) {
const x0 = position.x * tileSize.x;
const y0 = position.y * tileSize.y;
const entities = ecs.system('MaintainColliderHash').within({
x0,
x1: x0 + tileSize.x - 1,
y0,
y1: y0 + tileSize.y - 1,
});
let hasPlant = false;
for (const {Plant} of entities) {
if (Plant) {
hasPlant = true
break;
}
}
if (!hasPlant) {
if ([7].includes(layer.tile(position))) {
filtered.push(position)
for (const position of projected) {
const x0 = position.x * tileSize.x;
const y0 = position.y * tileSize.y;
const entities = ecs.system('MaintainColliderHash').within({
x0,
x1: x0 + tileSize.x - 1,
y0,
y1: y0 + tileSize.y - 1,
});
let hasPlant = false;
for (const {Plant} of entities) {
if (Plant) {
hasPlant = true
break;
}
}
if (!hasPlant) {
if ([7].includes(layer.tile(position))) {
filtered.push(position)
}
}
}
return filtered;
}
filtered

View File

@ -1,91 +1,97 @@
const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) {
const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers} = ecs.get(1)
const layer = TileLayers.layer(0)
import * as Math from '@/util/math.js';
import Ticker from '@/util/ticker.js';
Controlled.locked = 1;
const [, direction] = Sprite.animation.split(':')
export default function*({ecs, wielder}) {
const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) {
const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers} = ecs.get(1)
const layer = TileLayers.layer(0)
const plant = {
Collider: {
bodies: [
{
points: [
{x: -8, y: -8},
{x: 7, y: -8},
{x: -8, y: 7},
{x: 7, y: 7},
],
},
],
},
Interactive: {
interactScript: '/resources/tomato-plant/interact.js',
},
Plant: {
growScript: '/resources/tomato-plant/grow.js',
mayGrowScript: '/resources/tomato-plant/may-grow.js',
stages: [0.5, 0.5, 0.5, 0.5, 0.5],
},
Sprite: {
anchorY: 0.75,
animation: 'stage/0',
source: '/resources/tomato-plant/tomato-plant.json',
},
Ticking: {},
VisibleAabb: {},
};
Controlled.locked = 1;
const [, direction] = Sprite.animation.split(':')
Emitter.emit({
count: 25,
frequency: 0.01,
shape: {
type: 'filledRect',
payload: {width: 16, height: 16},
},
entity: {
Forces: {forceY: -100},
Position: {
x: Position.x,
y: Position.y,
const plant = {
Collider: {
bodies: [
{
points: [
{x: -8, y: -8},
{x: 7, y: -8},
{x: -8, y: 7},
{x: 7, y: 7},
],
},
],
},
Interactive: {
interactScript: '/resources/tomato-plant/interact.js',
},
Plant: {
growScript: '/resources/tomato-plant/grow.js',
mayGrowScript: '/resources/tomato-plant/may-grow.js',
stages: [0.5, 0.5, 0.5, 0.5, 0.5],
},
Sprite: {
scaleX: 0.125,
scaleY: 0.125,
tint: 0x221100,
anchorY: 0.75,
animation: 'stage/0',
source: '/resources/tomato-plant/tomato-plant.json',
zIndex: 0,
},
Ttl: {ttl: 0.25},
}
});
Ticking: {},
VisibleAabb: {},
};
Sound.play('/resources/sow.wav');
Sprite.animation = ['moving', direction].join(':');
const directionMap = {0: 'right', 1: 'down', 2: 'left', 3: 'up'};
for (let i = 0; i < 6; ++i) {
Direction.direction = Math.HALF_PI * Math.floor(Math.random() * 4);
Sprite.animation = ['moving', directionMap[Direction.quantize(4)]].join(':');
await wait(0.125);
}
Sprite.animation = ['idle', direction].join(':');
for (const {x, y} of projected) {
ecs.create({
...plant,
Plant: {
...plant.Plant,
growthFactor: Math.floor(Math.random() * 256),
},
Position: {
x: x * layer.tileSize.x + (0.5 * layer.tileSize.x),
y: y * layer.tileSize.y + (0.5 * layer.tileSize.y),
Emitter.emit({
count: 25,
frequency: 0.01,
shape: {
type: 'filledRect',
payload: {width: 16, height: 16},
},
entity: {
Forces: {forceY: -100},
Position: {
x: Position.x,
y: Position.y,
},
Sprite: {
scaleX: 0.125,
scaleY: 0.125,
tint: 0x221100,
},
Ttl: {ttl: 0.25},
}
});
Sound.play('/resources/sow.wav');
Sprite.animation = ['moving', direction].join(':');
const directionMap = {0: 'right', 1: 'down', 2: 'left', 3: 'up'};
for (let i = 0; i < 6; ++i) {
Direction.direction = Math.HALF_PI * Math.floor(Math.random() * 4);
Sprite.animation = ['moving', directionMap[Direction.quantize(4)]].join(':');
yield Ticker.wait(0.125);
}
Sprite.animation = ['idle', direction].join(':');
for (const {x, y} of projected) {
ecs.create({
...plant,
Plant: {
...plant.Plant,
growthFactor: Math.floor(Math.random() * 256),
},
Position: {
x: x * layer.tileSize.x + (0.5 * layer.tileSize.x),
y: y * layer.tileSize.y + (0.5 * layer.tileSize.y),
},
});
}
Controlled.locked = 0;
}
Controlled.locked = 0;
}

View File

@ -0,0 +1,9 @@
export default function*({ecs, entity, other}) {
if (other.Inventory) {
other.Inventory.give({
qty: 1,
source: '/resources/tomato/tomato.json',
})
ecs.destroy(entity.id);
}
}

View File

@ -0,0 +1,14 @@
export default function* ({ecs, other}) {
if (other.Player) {
ecs.switchEcs(
other,
['homesteads', other.Player.id].join('/'),
{
Position: {
x: 20,
y: 438,
},
},
);
}
}

View File

@ -1,11 +1,13 @@
const layer = ecs.get(1).TileLayers.layer(1)
export default function*({ecs, projected}) {
const layer = ecs.get(1).TileLayers.layer(1)
const filtered = []
const filtered = []
for (const position of projected) {
if ([7].includes(layer.tile(position))) {
filtered.push(position)
for (const position of projected) {
if ([7].includes(layer.tile(position))) {
filtered.push(position)
}
}
}
return filtered
return filtered;
}

View File

@ -1,62 +1,67 @@
const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) {
import * as Math from '@/util/math.js';
import Ticker from '@/util/ticker.js';
const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers, Water} = ecs.get(1)
const layer = TileLayers.layer(0)
export default function*({ecs, wielder}) {
const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) {
Controlled.locked = 1
const [, direction] = Sprite.animation.split(':')
Sprite.animation = ['idle', direction].join(':');
const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers, Water} = ecs.get(1)
const layer = TileLayers.layer(0)
Sound.play('/resources/watering-can/water.wav');
Controlled.locked = 1
const [, direction] = Sprite.animation.split(':')
Sprite.animation = ['idle', direction].join(':');
for (const {x, y} of projected) {
Emitter.emit({
entity: {
Forces: {forceY: 100},
Position: {
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
y: y * layer.tileSize.y - (layer.tileSize.y / 4),
Sound.play('/resources/watering-can/water.wav');
for (const {x, y} of projected) {
Emitter.emit({
entity: {
Forces: {forceY: 100},
Position: {
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
y: y * layer.tileSize.y - (layer.tileSize.y / 4),
},
Sprite: {
scaleX: 0.05,
scaleY: 0.15,
tint: 0x0022aa,
},
Ttl: {ttl: 0.1},
},
Sprite: {
scaleX: 0.05,
scaleY: 0.15,
tint: 0x0022aa,
fields: [
{
path: ['Sprite', 'lightness'],
value: [0.111, 0.666],
},
],
frequency: 0.01,
shape: {
type: 'circle',
payload: {radius: 4},
},
Ttl: {ttl: 0.1},
},
fields: [
{
path: ['Sprite', 'lightness'],
value: [0.111, 0.666],
},
],
frequency: 0.01,
shape: {
type: 'circle',
payload: {radius: 4},
},
ttl: 0.5,
});
}
await wait(0.5);
for (const {x, y} of projected) {
const tileIndex = layer.area.x * y + x
let w;
if (Water.water[tileIndex]) {
w = Water.water[tileIndex]
ttl: 0.5,
});
}
else {
w = 0
yield Ticker.wait(0.5);
for (const {x, y} of projected) {
const tileIndex = layer.area.x * y + x
let w;
if (Water.water[tileIndex]) {
w = Water.water[tileIndex]
}
else {
w = 0
}
Water.water[tileIndex] = Math.min(255, 64 + w);
}
Water.water[tileIndex] = Math.min(255, 64 + w);
ecs.system('Water').schedule();
Controlled.locked = 0;
}
ecs.system('Water').schedule();
Controlled.locked = 0;
}

View File

@ -4,11 +4,9 @@
"price": 100,
"projectionCheck": "/resources/watering-can/projection-check.js",
"projection": {
"distance": [3, -1],
"distance": [1, 0],
"grid": [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
[1]
]
},
"start": "/resources/watering-can/start.js"