refactor!: scrips n tickers
This commit is contained in:
parent
bea551fa19
commit
2041b38678
|
@ -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,
|
||||
},
|
||||
}
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
master.Ticking.add(
|
||||
new Ticker(function* () {
|
||||
let emitting = true;
|
||||
emitter.emit().onEnd(() => {
|
||||
emitting = false;
|
||||
});
|
||||
Ticker.add(
|
||||
new TickerPromise(
|
||||
(resolve) => {
|
||||
promise.then(resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
while (emitting) {
|
||||
const elapsed = yield;
|
||||
this.emitter.tick(elapsed);
|
||||
}
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
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) {
|
||||
|
|
|
@ -28,6 +28,8 @@ addEventListener('message', (particle) => {
|
|||
.onEnd(() => {});
|
||||
});
|
||||
|
||||
postMessage(null);
|
||||
|
||||
let last = performance.now();
|
||||
function tick(now) {
|
||||
const elapsed = (now - last) / 1000;
|
||||
|
|
|
@ -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.addEventListener('message', () => {
|
||||
localParticleWorker.postMessage(ecs.get(1).toJSON());
|
||||
setParticleWorker((particleWorker) => {
|
||||
if (particleWorker) {
|
||||
particleWorker.terminate();
|
||||
}
|
||||
return localParticleWorker;
|
||||
setParticleWorker(localParticleWorker);
|
||||
});
|
||||
}, [particleWorker]);
|
||||
useEcsTick(onEcsTickParticles);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
51
app/util/delta.test.js
Normal 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);
|
||||
});
|
|
@ -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);
|
||||
oscillators[key] = oscillator;
|
||||
}
|
||||
let stop;
|
||||
const promise = new Ticker(
|
||||
(resolve) => {
|
||||
stop = resolve;
|
||||
Promise.all(promises).then(resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
for (const key in oscillators) {
|
||||
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 x = (oscillator.offset + (oscillator.elapsed / oscillator.frequency)) % 1;
|
||||
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);
|
||||
}
|
||||
object[key] = oscillator.low + oscillator.magnitude * (y / oscillator.modulators.length);
|
||||
return oscillator.low + oscillator.magnitude * (y / oscillator.modulators.length);
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, oscillators, promise};
|
||||
oscillators[key] = oscillator;
|
||||
}
|
||||
const ticker = new Ticker(function* () {
|
||||
while (keys.size > 0) {
|
||||
const elapsed = yield;
|
||||
for (const key of keys) {
|
||||
const oscillator = oscillators[key];
|
||||
oscillator.elapsed += elapsed;
|
||||
const rollover = oscillator.elapsed >= oscillator.frequency;
|
||||
if (rollover) {
|
||||
oscillator.count -= 1;
|
||||
oscillator.elapsed = 0 === oscillator.count
|
||||
? oscillator.frequency
|
||||
: oscillator.elapsed % oscillator.frequency;
|
||||
}
|
||||
object[key] = oscillator.compute(oscillator.elapsed);
|
||||
if (rollover && 0 === oscillator.count) {
|
||||
oscillator.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return {oscillators, stop, ticker};
|
||||
}
|
||||
|
|
103
app/util/lfo.test.js
Normal file
103
app/util/lfo.test.js
Normal 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));
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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]);
|
||||
});
|
|
@ -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();
|
||||
return this.fn(this.locals).next().value;
|
||||
}
|
||||
|
||||
static load(pathOrFunction, locals) {
|
||||
let fn;
|
||||
if (this.registered[pathOrFunction]) {
|
||||
fn = this.registered[pathOrFunction];
|
||||
}
|
||||
else if (pathOrFunction) {
|
||||
try {
|
||||
const {value} = this.sandbox.step();
|
||||
return value;
|
||||
fn = eval(pathOrFunction);
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(this.sandbox.$$stack);
|
||||
console.warn(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();
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.promise = null;
|
||||
});
|
||||
break;
|
||||
if (!this.iterator) {
|
||||
this.iterator = this.fn(this.locals);
|
||||
}
|
||||
if (done) {
|
||||
if (resolve) {
|
||||
resolve();
|
||||
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);
|
||||
},
|
||||
);
|
||||
const self = this;
|
||||
return new Ticker(function* () {
|
||||
while (true) {
|
||||
const elapsed = yield;
|
||||
if (self.tick(elapsed)) {
|
||||
break;
|
||||
}
|
||||
|
||||
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 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
52
app/util/ticker.js
Normal 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
35
app/util/ticker.test.js
Normal 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);
|
||||
});
|
|
@ -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,22 +23,22 @@ 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,
|
||||
|
@ -42,7 +46,8 @@ export default function transition(object, properties) {
|
|||
transition.duration,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, transitions, promise};
|
||||
}
|
||||
}
|
||||
});
|
||||
return {stop, ticker, transitions};
|
||||
}
|
||||
|
|
91
app/util/transition.test.js
Normal file
91
app/util/transition.test.js
Normal 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
7217
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
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) {
|
||||
|
@ -8,9 +11,9 @@ for (const entity of entities) {
|
|||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/resources/brush/brush.wav');
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
await wait(0.3)
|
||||
yield Ticker.wait(0.3)
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
await wait(0.1)
|
||||
yield Ticker.wait(0.1)
|
||||
}
|
||||
Inventory.give({
|
||||
qty: 1,
|
||||
|
@ -108,3 +111,4 @@ for (const entity of entities) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
resources/chest/interact.js
Normal file
10
resources/chest/interact.js
Normal 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',
|
||||
// })
|
||||
}
|
|
@ -1,17 +1,22 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*({entity}) {
|
||||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
|
||||
entity.Controlled.directionMove(entity.Direction.direction);
|
||||
|
||||
await wait(0.25 + Math.random() * 2.25);
|
||||
yield Ticker.wait(0.25 + Math.random() * 2.25);
|
||||
|
||||
entity.Controlled.stop();
|
||||
|
||||
entity.Sprite.isAnimating = 0;
|
||||
|
||||
await wait(1 + Math.random() * 3);
|
||||
yield Ticker.wait(1 + Math.random() * 3);
|
||||
|
||||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
|
||||
await wait(0.5 + Math.random() * 2.5);
|
||||
yield Ticker.wait(0.5 + Math.random() * 2.5);
|
||||
|
||||
entity.Sprite.isAnimating = 1;
|
||||
}
|
||||
|
|
16
resources/farm/animals/cow-adult/interact.js
Normal file
16
resources/farm/animals/cow-adult/interact.js
Normal 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',
|
||||
})
|
||||
}
|
|
@ -1,17 +1,22 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*({entity}) {
|
||||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
|
||||
entity.Controlled.directionMove(entity.Direction.direction);
|
||||
|
||||
await wait(0.25 + Math.random() * 2.25);
|
||||
yield Ticker.wait(0.25 + Math.random() * 2.25);
|
||||
|
||||
entity.Controlled.stop();
|
||||
|
||||
entity.Sprite.isAnimating = 0;
|
||||
|
||||
await wait(1 + Math.random() * 3);
|
||||
yield Ticker.wait(1 + Math.random() * 3);
|
||||
|
||||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
|
||||
await wait(0.5 + Math.random() * 2.5);
|
||||
yield Ticker.wait(0.5 + Math.random() * 2.5);
|
||||
|
||||
entity.Sprite.isAnimating = 1;
|
||||
}
|
||||
|
|
15
resources/farm/animals/goat-white/interact.js
Normal file
15
resources/farm/animals/goat-white/interact.js
Normal 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',
|
||||
})
|
||||
}
|
5
resources/hoe/dirt-particle.js
Normal file
5
resources/hoe/dirt-particle.js
Normal 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;
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
const layer0 = ecs.get(1).TileLayers.layer(0)
|
||||
const layer1 = ecs.get(1).TileLayers.layer(1)
|
||||
|
||||
const filtered = []
|
||||
|
||||
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)
|
||||
filtered.push(position);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
return filtered;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const {Direction, Position, Wielder} = wielder
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*({ecs, wielder}) {
|
||||
const {Direction, Position, Wielder} = wielder;
|
||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
|
||||
if (projected?.length > 0) {
|
||||
|
||||
|
@ -16,7 +19,7 @@ if (projected?.length > 0) {
|
|||
entity: {
|
||||
Behaving: {
|
||||
routines: {
|
||||
initial: 'await delta(entity.Forces, {forceY: {delta: 640, duration: 0.125}}).promise',
|
||||
initial: '/resources/hoe/dirt-particle.js',
|
||||
},
|
||||
},
|
||||
Forces: {forceY: -80},
|
||||
|
@ -53,9 +56,9 @@ if (projected?.length > 0) {
|
|||
});
|
||||
}
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
await wait(0.3)
|
||||
yield Ticker.wait(0.3)
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
await wait(0.1)
|
||||
yield Ticker.wait(0.1)
|
||||
}
|
||||
|
||||
for (const position of projected) {
|
||||
|
@ -65,3 +68,4 @@ if (projected?.length > 0) {
|
|||
Controlled.locked = 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
14
resources/homestead/house-teleport/collision-start.js
Normal file
14
resources/homestead/house-teleport/collision-start.js
Normal 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,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
14
resources/homestead/town-teleport/collision-start.js
Normal file
14
resources/homestead/town-teleport/collision-start.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default function* ({ecs, other}) {
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
'town',
|
||||
{
|
||||
Position: {
|
||||
x: 940,
|
||||
y: 480,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
14
resources/house/homestead-teleport/collision-start.js
Normal file
14
resources/house/homestead-teleport/collision-start.js
Normal 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,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
19
resources/kitty/interact.js
Normal file
19
resources/kitty/interact.js
Normal 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',
|
||||
});
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
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)
|
||||
|
@ -10,3 +13,4 @@ if (playerEntity !== other && other.Vulnerable) {
|
|||
type: other.Vulnerable.Types.PAIN,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*(locals) {
|
||||
const {ecs, where, wielder} = locals;
|
||||
const {Player, Position} = wielder;
|
||||
|
||||
const EVERY = 0.03;
|
||||
const N = 14;
|
||||
const SPREAD = 1;
|
||||
|
||||
const creating = [];
|
||||
|
||||
const offset = Math.random() * Math.TAU;
|
||||
|
||||
const specs = [];
|
||||
|
||||
for (let i = 0; i < N; ++i) {
|
||||
creating.push(ecs.get(ecs.create({
|
||||
specs.push({
|
||||
Collider: {
|
||||
bodies: [
|
||||
{
|
||||
|
@ -38,9 +43,11 @@ for (let i = 0; i < N; ++i) {
|
|||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
const creating = Array.from(ecs.createMany(specs)).map((entityId) => ecs.get(entityId));
|
||||
|
||||
const shot = creating.shift();
|
||||
shot.Sprite.alpha = 1;
|
||||
const shots = [
|
||||
|
@ -52,7 +59,7 @@ const shots = [
|
|||
|
||||
let spawner = 0;
|
||||
while (shots.length > 0) {
|
||||
spawner += elapsed;
|
||||
spawner += locals.elapsed;
|
||||
if (creating.length > 0 && spawner >= EVERY) {
|
||||
const entity = creating.shift();
|
||||
entity.Sprite.alpha = 1;
|
||||
|
@ -61,7 +68,7 @@ while (shots.length > 0) {
|
|||
}
|
||||
const destroying = [];
|
||||
for (const shot of shots) {
|
||||
shot.accumulated += elapsed;
|
||||
shot.accumulated += locals.elapsed;
|
||||
if (shot.accumulated <= SPREAD) {
|
||||
shot.entity.Speed.speed = 100 * (1 - (shot.accumulated / SPREAD))
|
||||
}
|
||||
|
@ -86,5 +93,6 @@ while (shots.length > 0) {
|
|||
for (let i = 0; i < destroying.length; ++i) {
|
||||
shots.splice(shots.indexOf(destroying[i]), 1);
|
||||
}
|
||||
await wait();
|
||||
yield Ticker.wait();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
const {Sprite, Ticking, Vulnerable} = entity;
|
||||
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 {promise} = transition(
|
||||
const {ticker} = transition(
|
||||
entity.Sprite,
|
||||
{
|
||||
scaleX: {
|
||||
|
@ -15,8 +21,8 @@ if (Sprite) {
|
|||
magnitude: entity.Sprite.scaleY * 2,
|
||||
},
|
||||
},
|
||||
)
|
||||
Ticking.add(promise);
|
||||
await promise;
|
||||
);
|
||||
yield ticker;
|
||||
ecs.destroy(entity.id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
export default function*({item, wielder}) {
|
||||
const amount = 50;
|
||||
wielder.Health.health += amount
|
||||
wielder.Health.health += amount;
|
||||
wielder.Vulnerable.damage({
|
||||
amount,
|
||||
position: wielder.Position.toJSON(),
|
||||
type: wielder.Vulnerable.Types.HEALING,
|
||||
})
|
||||
|
||||
item.qty -= 1
|
||||
});
|
||||
item.qty -= 1;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
|
@ -1,5 +1,8 @@
|
|||
export default function*({ecs, plant}) {
|
||||
const {Interactive, Sprite} = ecs.get(plant.entity);
|
||||
|
||||
Sprite.zIndex = 65535;
|
||||
|
||||
plant.growth = 0
|
||||
|
||||
if (plant.stage < 3) {
|
||||
|
@ -13,3 +16,4 @@ if (3 === plant.stage) {
|
|||
}
|
||||
|
||||
Sprite.animation = ['stage', plant.stage].join('/')
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
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';
|
||||
|
||||
export default function*({ecs, subject}) {
|
||||
const {Interactive, Position, Plant, Sprite} = subject;
|
||||
Interactive.interacting = false;
|
||||
|
||||
const ids = [];
|
||||
|
||||
const specs = [];
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
|
||||
ids.push(ecs.create({
|
||||
specs.push({
|
||||
Collider: {
|
||||
bodies: [
|
||||
{
|
||||
|
@ -17,16 +21,7 @@ for (let i = 0; i < 10; ++i) {
|
|||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: [
|
||||
'if (other.Inventory) {',
|
||||
' other.Inventory.give({',
|
||||
' qty: 1,',
|
||||
" source: '/resources/tomato/tomato.json',",
|
||||
' })',
|
||||
' ecs.destroy(entity.id)',
|
||||
' return undefined;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
collisionStartScript: '/resources/tomato/collision-start.js',
|
||||
},
|
||||
Forces: {},
|
||||
Magnetic: {},
|
||||
|
@ -40,18 +35,16 @@ for (let i = 0; i < 10; ++i) {
|
|||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
}));
|
||||
|
||||
});
|
||||
}
|
||||
const tomatoes = Array.from(ecs.createMany(specs)).map((entityId) => ecs.get(entityId));
|
||||
|
||||
for (const id of ids) {
|
||||
const tomato = ecs.get(id);
|
||||
|
||||
// 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,
|
||||
{
|
||||
|
@ -61,10 +54,9 @@ for (const id of ids) {
|
|||
},
|
||||
},
|
||||
)
|
||||
|
||||
tomato.Ticking.add(
|
||||
Promise.Ticker.all([
|
||||
d.promise,
|
||||
Ticker.all([
|
||||
d.ticker,
|
||||
lfo(
|
||||
d.deltas.y,
|
||||
{
|
||||
|
@ -76,7 +68,7 @@ for (const id of ids) {
|
|||
offset: -0.5,
|
||||
},
|
||||
},
|
||||
).promise,
|
||||
).ticker,
|
||||
lfo(
|
||||
tomato.Sprite,
|
||||
{
|
||||
|
@ -97,7 +89,7 @@ for (const id of ids) {
|
|||
offset: -0.5,
|
||||
},
|
||||
},
|
||||
).promise,
|
||||
).ticker,
|
||||
delta(
|
||||
tomato.Position,
|
||||
{
|
||||
|
@ -106,7 +98,7 @@ for (const id of ids) {
|
|||
delta: (12 * x) + Math.random() * 8,
|
||||
},
|
||||
},
|
||||
).promise,
|
||||
).ticker,
|
||||
delta(
|
||||
tomato.Position,
|
||||
{
|
||||
|
@ -115,10 +107,12 @@ for (const id of ids) {
|
|||
delta: (12 * y) + Math.random() * 8,
|
||||
},
|
||||
},
|
||||
).promise,
|
||||
).ticker,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Plant.stage = 4;
|
||||
Sprite.animation = ['stage', Plant.stage].join('/')
|
||||
|
||||
}
|
|
@ -1,20 +1,21 @@
|
|||
export default function*({ecs, plant}) {
|
||||
if (3 === plant.stage) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
const {TileLayers, Water} = ecs.get(1);
|
||||
const layer = TileLayers.layer(0)
|
||||
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
|
||||
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
|
||||
return false;
|
||||
}
|
||||
if (Water.water[tileIndex] < 32) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
if (Water.water[tileIndex] > 224) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export default function*({ecs, projected}) {
|
||||
const layer = ecs.get(1).TileLayers.layer(1)
|
||||
const {tileSize} = layer;
|
||||
|
||||
|
@ -25,5 +26,5 @@ for (const position of projected) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
filtered
|
||||
return filtered;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*({ecs, wielder}) {
|
||||
const {Direction, Position, Wielder} = wielder
|
||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
|
||||
if (projected?.length > 0) {
|
||||
|
@ -33,6 +37,7 @@ if (projected?.length > 0) {
|
|||
anchorY: 0.75,
|
||||
animation: 'stage/0',
|
||||
source: '/resources/tomato-plant/tomato-plant.json',
|
||||
zIndex: 0,
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
|
@ -67,7 +72,7 @@ if (projected?.length > 0) {
|
|||
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);
|
||||
yield Ticker.wait(0.125);
|
||||
}
|
||||
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
|
@ -89,3 +94,4 @@ if (projected?.length > 0) {
|
|||
Controlled.locked = 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
9
resources/tomato/collision-start.js
Normal file
9
resources/tomato/collision-start.js
Normal 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);
|
||||
}
|
||||
}
|
14
resources/town/homestead-teleport/collision-start.js
Normal file
14
resources/town/homestead-teleport/collision-start.js
Normal 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,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export default function*({ecs, projected}) {
|
||||
const layer = ecs.get(1).TileLayers.layer(1)
|
||||
|
||||
const filtered = []
|
||||
|
@ -8,4 +9,5 @@ for (const position of projected) {
|
|||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
return filtered;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*({ecs, wielder}) {
|
||||
const {Direction, Position, Wielder} = wielder
|
||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
|
||||
if (projected?.length > 0) {
|
||||
|
@ -41,7 +45,7 @@ if (projected?.length > 0) {
|
|||
ttl: 0.5,
|
||||
});
|
||||
}
|
||||
await wait(0.5);
|
||||
yield Ticker.wait(0.5);
|
||||
|
||||
for (const {x, y} of projected) {
|
||||
const tileIndex = layer.area.x * y + x
|
||||
|
@ -60,3 +64,4 @@ if (projected?.length > 0) {
|
|||
Controlled.locked = 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user