Compare commits

...

12 Commits

Author SHA1 Message Date
cha0s
6eac298671 refactor: promise and tickers 2024-07-22 01:31:52 -05:00
cha0s
6997e108c0 fix: bounds 2024-07-22 01:25:05 -05:00
cha0s
fa817e3072 fix: query deindexing 2024-07-22 01:12:17 -05:00
cha0s
c224445345 refactor: withResolvers 2024-07-22 00:13:03 -05:00
cha0s
eb6e23f0e1 refactor: tidy 2024-07-21 19:44:37 -05:00
cha0s
86b7b74616 fix: bounds 2024-07-21 19:24:29 -05:00
cha0s
3809bf98e6 refactor: immediate player destroy 2024-07-21 19:24:22 -05:00
cha0s
4278ee7675 fix: create player API 2024-07-21 19:24:05 -05:00
cha0s
a1d1c35565 fun: kittehs 2024-07-21 11:20:16 -05:00
cha0s
ed8c07a88f fix: production build 2024-07-21 11:04:18 -05:00
cha0s
d0252bc9ff perf: json 2024-07-21 07:28:22 -05:00
cha0s
14713797f1 perf: no elapsed 2024-07-21 07:22:48 -05:00
38 changed files with 637 additions and 212 deletions

View File

@ -1,4 +1,5 @@
import {encode} from '@/net/packets/index.js'; import {encode} from '@/net/packets/index.js';
import {withResolvers} from '@/util/promise.js';
let connected = false; let connected = false;
let socket; let socket;
@ -12,7 +13,7 @@ onmessage = async (event) => {
const url = new URL(`wss://${event.data.host}/ws`) const url = new URL(`wss://${event.data.host}/ws`)
socket = new WebSocket(url.href); socket = new WebSocket(url.href);
socket.binaryType = 'arraybuffer'; socket.binaryType = 'arraybuffer';
const {promise, resolve} = Promise.withResolvers(); const {promise, resolve} = withResolvers();
socket.addEventListener('open', resolve); socket.addEventListener('open', resolve);
socket.addEventListener('error', () => { socket.addEventListener('error', () => {
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'})); postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));

View File

@ -1,6 +1,7 @@
import Client from '@/net/client.js'; import Client from '@/net/client.js';
import {encode} from '@/net/packets/index.js'; import {encode} from '@/net/packets/index.js';
import {CLIENT_PREDICTION} from '@/util/constants.js'; import {CLIENT_PREDICTION} from '@/util/constants.js';
import {withResolvers} from '@/util/promise.js';
export default class RemoteClient extends Client { export default class RemoteClient extends Client {
constructor() { constructor() {
@ -30,7 +31,7 @@ export default class RemoteClient extends Client {
const onMessage = (event) => { const onMessage = (event) => {
this.accept(event.data); this.accept(event.data);
} }
const {promise, resolve} = Promise.withResolvers(); const {promise, resolve} = withResolvers();
this.socket.addEventListener('open', resolve); this.socket.addEventListener('open', resolve);
this.socket.addEventListener('error', () => { this.socket.addEventListener('error', () => {
this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'})); this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'}));

View File

@ -0,0 +1,46 @@
import Component from '@/ecs/component.js';
export default class Behaving extends Component {
instanceFromSchema() {
return class BehavingInstance extends super.instanceFromSchema() {
$$routineInstances = {};
tick(elapsed) {
const routine = this.$$routineInstances[this.currentRoutine];
if (routine) {
routine.tick(elapsed);
}
}
};
}
async load(instance) {
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
const promises = [];
for (const key in instance.routines) {
promises.push(
this.ecs.readScript(
instance.routines[key],
{
entity: this.ecs.get(instance.entity),
},
)
.then((script) => {
instance.$$routineInstances[key] = script;
}),
);
}
await Promise.all(promises);
}
static properties = {
currentRoutine: {defaultValue: 'initial', type: 'string'},
isBehaving: {defaultValue: 1, type: 'uint8'},
routines: {
type: 'map',
value: {
type: 'string',
},
},
};
}

View File

@ -8,7 +8,7 @@ export default class Interactive extends Component {
const script = this.interactScriptInstance.clone(); const script = this.interactScriptInstance.clone();
script.context.initiator = initiator; script.context.initiator = initiator;
const {Ticking} = ecs.get(this.entity); const {Ticking} = ecs.get(this.entity);
Ticking.addTickingPromise(script.tickingPromise()); Ticking.add(script.ticker());
} }
get interacting() { get interacting() {
return !!this.$$interacting; return !!this.$$interacting;

View File

@ -11,7 +11,7 @@ export default class Plant extends Component {
} }
grow() { grow() {
const {Ticking} = ecs.get(this.entity); const {Ticking} = ecs.get(this.entity);
Ticking.addTickingPromise(this.growScriptInstance.tickingPromise()); Ticking.add(this.growScriptInstance.ticker());
} }
}; };
} }

View File

@ -3,24 +3,60 @@ import Component from '@/ecs/component.js';
export default class Sprite extends Component { export default class Sprite extends Component {
instanceFromSchema() { instanceFromSchema() {
return class SpriteInstance extends super.instanceFromSchema() { return class SpriteInstance extends super.instanceFromSchema() {
$$sourceJson = {};
get anchor() { get anchor() {
return {x: this.anchorX, y: this.anchorY}; return {x: this.anchorX, y: this.anchorY};
} }
get animation() {
return super.animation;
}
set animation(animation) {
super.animation = animation;
// eslint-disable-next-line no-self-assign
this.frame = this.frame;
}
get frame() {
return super.frame;
}
set frame(frame) {
super.frame = this.frames ? frame % this.frames : 0;
}
get frames() {
if (
!this.animation
|| !this.$$sourceJson.animations
|| !(this.animation in this.$$sourceJson.animations)
) {
return 0;
}
return this.$$sourceJson.animations[this.animation].length;
}
get scale() { get scale() {
return {x: this.scaleX, y: this.scaleY}; return {x: this.scaleX, y: this.scaleY};
} }
toNet() {
// eslint-disable-next-line no-unused-vars
const {elapsed, ...rest} = super.toNet();
return rest;
}
}; };
} }
async load(instance) { async load(instance) {
instance.$$sourceJson = await this.ecs.readJson(instance.source); instance.$$sourceJson = await this.ecs.readJson(instance.source);
} }
markChange(entityId, key, value) {
if ('elapsed' === key) {
return;
}
super.markChange(entityId, key, value);
}
static properties = { static properties = {
anchorX: {defaultValue: 0.5, type: 'float32'}, anchorX: {defaultValue: 0.5, type: 'float32'},
anchorY: {defaultValue: 0.5, type: 'float32'}, anchorY: {defaultValue: 0.5, type: 'float32'},
animation: {type: 'string'}, animation: {type: 'string'},
elapsed: {type: 'float32'}, elapsed: {type: 'float32'},
frame: {type: 'uint16'}, frame: {type: 'uint16'},
frames: {type: 'uint16'}, isAnimating: {defaultValue: 1, type: 'uint8'},
scaleX: {defaultValue: 1, type: 'float32'}, scaleX: {defaultValue: 1, type: 'float32'},
scaleY: {defaultValue: 1, type: 'float32'}, scaleY: {defaultValue: 1, type: 'float32'},
source: {type: 'string'}, source: {type: 'string'},

View File

@ -5,30 +5,30 @@ export default class Ticking extends Component {
return class TickingInstance extends super.instanceFromSchema() { return class TickingInstance extends super.instanceFromSchema() {
$$finished = []; $$finished = [];
$$tickingPromises = []; $$tickers = [];
addTickingPromise(tickingPromise) { add(ticker) {
this.$$tickingPromises.push(tickingPromise); this.$$tickers.push(ticker);
tickingPromise.then(() => { ticker.then(() => {
this.$$finished.push(tickingPromise); this.$$finished.push(ticker);
}); });
} }
reset() { reset() {
this.$$finished = []; this.$$finished = [];
this.$$tickingPromises = []; this.$$tickers = [];
} }
tick(elapsed) { tick(elapsed) {
for (const tickingPromise of this.$$finished) { for (const ticker of this.$$finished) {
this.$$tickingPromises.splice( this.$$tickers.splice(
this.$$tickingPromises.indexOf(tickingPromise), this.$$tickers.indexOf(ticker),
1, 1,
); );
} }
this.$$finished = []; this.$$finished = [];
for (const tickingPromise of this.$$tickingPromises) { for (const ticker of this.$$tickers) {
tickingPromise.tick(elapsed); ticker.tick(elapsed);
} }
} }

View File

@ -20,7 +20,7 @@ export default class Wielder extends Component {
script.context.ecs = ecs; script.context.ecs = ecs;
script.context.item = activeItem; script.context.item = activeItem;
script.context.wielder = entity; script.context.wielder = entity;
Ticking.addTickingPromise(script.tickingPromise()); Ticking.add(script.ticker());
} }
} }
} }

View File

@ -1,6 +1,7 @@
import {Encoder, Decoder} from '@msgpack/msgpack'; import {Encoder, Decoder} from '@msgpack/msgpack';
import {LRUCache} from 'lru-cache'; import {LRUCache} from 'lru-cache';
import {withResolvers} from '@/util/promise.js';
import Script from '@/util/script.js'; import Script from '@/util/script.js';
import EntityFactory from './entity-factory.js'; import EntityFactory from './entity-factory.js';
@ -326,11 +327,7 @@ export default class Ecs {
async readJson(uri) { async readJson(uri) {
const key = ['$$json', uri].join(':'); const key = ['$$json', uri].join(':');
if (!cache.has(key)) { if (!cache.has(key)) {
let promise, resolve, reject; const {promise, resolve, reject} = withResolvers();
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(key, promise); cache.set(key, promise);
this.readAsset(uri) this.readAsset(uri)
.then((chars) => { .then((chars) => {

View File

@ -170,10 +170,16 @@ test('ticks systems', async () => {
.to.deep.equal(JSON.stringify({y: 128 + 30})); .to.deep.equal(JSON.stringify({y: 128 + 30}));
}); });
test('schedules entities to be deleted when ticking systems', () => { test('schedules entities to be deleted when ticking systems', async () => {
const ecs = new Ecs({ const ecs = new Ecs({
Components: {Empty},
Systems: { Systems: {
Despawn: class extends System { Despawn: class extends System {
static queries() {
return {
default: ['Empty'],
};
}
tick() { tick() {
this.ecs.destroy(1); this.ecs.destroy(1);
expect(ecs.get(1)) expect(ecs.get(1))
@ -183,8 +189,10 @@ test('schedules entities to be deleted when ticking systems', () => {
}, },
}); });
ecs.system('Despawn').active = true; ecs.system('Despawn').active = true;
ecs.create(); await ecs.create({Empty: {}});
ecs.tick(1); ecs.tick(1);
expect(Array.from(ecs.system('Despawn').select('default')))
.to.have.lengthOf(0);
expect(ecs.get(1)) expect(ecs.get(1))
.to.be.undefined; .to.be.undefined;
}); });

View File

@ -24,8 +24,8 @@ export default class Query {
} }
deindex(entityIds) { deindex(entityIds) {
for (let i = 0; i < entityIds.length; ++i) { for (const entityId of entityIds) {
this.$$index.delete(entityIds[i]); this.$$index.delete(entityId);
} }
} }

17
app/ecs/systems/behave.js Normal file
View File

@ -0,0 +1,17 @@
import {System} from '@/ecs/index.js';
export default class Behave extends System {
static queries() {
return {
default: ['Behaving'],
};
}
tick(elapsed) {
for (const {Behaving} of this.select('default')) {
Behaving.tick(elapsed);
}
}
}

View File

@ -69,14 +69,14 @@ export default class Colliders extends System {
const script = entity.Collider.collisionStartScriptInstance.clone(); const script = entity.Collider.collisionStartScriptInstance.clone();
script.context.intersections = intersections; script.context.intersections = intersections;
script.context.other = other; script.context.other = other;
entity.Ticking.addTickingPromise(script.tickingPromise()); entity.Ticking.add(script.ticker());
} }
if (other.Collider.collisionStartScriptInstance) { if (other.Collider.collisionStartScriptInstance) {
const script = other.Collider.collisionStartScriptInstance.clone(); const script = other.Collider.collisionStartScriptInstance.clone();
script.context.intersections = intersections script.context.intersections = intersections
.map(([l, r]) => [r, l]); .map(([l, r]) => [r, l]);
script.context.other = entity; script.context.other = entity;
other.Ticking.addTickingPromise(script.tickingPromise()); other.Ticking.add(script.ticker());
} }
} }
for (const i in intersections) { for (const i in intersections) {
@ -127,12 +127,12 @@ export default class Colliders extends System {
if (entity.Collider.collisionEndScriptInstance) { if (entity.Collider.collisionEndScriptInstance) {
const script = entity.Collider.collisionEndScriptInstance.clone(); const script = entity.Collider.collisionEndScriptInstance.clone();
script.context.other = other; script.context.other = other;
entity.Ticking.addTickingPromise(script.tickingPromise()); entity.Ticking.add(script.ticker());
} }
if (other.Collider.collisionEndScriptInstance) { if (other.Collider.collisionEndScriptInstance) {
const script = other.Collider.collisionEndScriptInstance.clone(); const script = other.Collider.collisionEndScriptInstance.clone();
script.context.other = entity; script.context.other = entity;
other.Ticking.addTickingPromise(script.tickingPromise()); other.Ticking.add(script.ticker());
} }
} }
} }

View File

@ -1,6 +1,6 @@
import {System} from '@/ecs/index.js'; import {System} from '@/ecs/index.js';
export default class ControlMovement extends System { export default class RunAnimations extends System {
static queries() { static queries() {
return { return {
@ -10,13 +10,13 @@ export default class ControlMovement extends System {
tick(elapsed) { tick(elapsed) {
for (const {Sprite} of this.select('default')) { for (const {Sprite} of this.select('default')) {
if (0 === Sprite.speed) { if (0 === Sprite.speed || !Sprite.isAnimating) {
continue; continue;
} }
Sprite.elapsed += elapsed / Sprite.speed; Sprite.elapsed += elapsed / Sprite.speed;
while (Sprite.elapsed > 1) { while (Sprite.elapsed > 1) {
Sprite.elapsed -= 1; Sprite.elapsed -= 1;
Sprite.frame = (Sprite.frame + 1) % Sprite.frames; Sprite.frame += 1;
} }
} }
} }

View File

@ -1,6 +1,7 @@
import {LRUCache} from 'lru-cache'; import {LRUCache} from 'lru-cache';
import Ecs from '@/ecs/ecs.js'; import Ecs from '@/ecs/ecs.js';
import {withResolvers} from '@/util/promise.js';
const cache = new LRUCache({ const cache = new LRUCache({
max: 128, max: 128,
@ -9,11 +10,7 @@ const cache = new LRUCache({
export default class ClientEcs extends Ecs { export default class ClientEcs extends Ecs {
async readAsset(uri) { async readAsset(uri) {
if (!cache.has(uri)) { if (!cache.has(uri)) {
let promise, resolve, reject; const {promise, resolve, reject} = withResolvers();
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(uri, promise); cache.set(uri, promise);
fetch(new URL(uri, window.location.origin)) fetch(new URL(uri, window.location.origin))
.then(async (response) => { .then(async (response) => {

View File

@ -16,12 +16,12 @@ export default function Devtools({
}) { }) {
const [ecs] = useEcs(); const [ecs] = useEcs();
const [mainEntity] = useMainEntity(); const [mainEntity] = useMainEntity();
const [mainEntityJson, setMainEntityJson] = useState({}); const [mainEntityJson, setMainEntityJson] = useState('');
useEcsTick(() => { useEcsTick(() => {
if (!ecs || !mainEntity) { if (!ecs || !mainEntity) {
return; return;
} }
setMainEntityJson(ecs.get(mainEntity).toJSON()); setMainEntityJson(JSON.stringify(ecs.get(mainEntity), null, 2));
}, [ecs, mainEntity]); }, [ecs, mainEntity]);
return ( return (
<div className={styles.devtools}> <div className={styles.devtools}>
@ -46,7 +46,7 @@ export default function Devtools({
</label> </label>
</div> </div>
</form> </form>
<pre><code><small>{JSON.stringify(mainEntityJson, null, 2)}</small></code></pre> <pre><code><small>{mainEntityJson}</small></code></pre>
</div> </div>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>

View File

@ -5,7 +5,7 @@ import {useDebug} from '@/react/context/debug.js';
import {useMainEntity} from '@/react/context/main-entity.js'; import {useMainEntity} from '@/react/context/main-entity.js';
import Emitter from './emitter.jsx'; import Emitter from './emitter.jsx';
import Light from './light.jsx'; // import Light from './light.jsx';
import Sprite from './sprite.jsx'; import Sprite from './sprite.jsx';
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) { function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {

View File

@ -1,4 +1,5 @@
import {Group, Layer} from '@pixi/layers'; import {Group, Layer} from '@pixi/layers';
import * as Lights from '@pixi/lights';
class LightLayer extends Layer { class LightLayer extends Layer {
$$diffuseTexture; $$diffuseTexture;
@ -42,22 +43,16 @@ class DeferredLighting {
export const deferredLighting = new DeferredLighting(); export const deferredLighting = new DeferredLighting();
let AmbientLight, PointLight; export class AmbientLight extends Lights.AmbientLight {
constructor(...args) {
if ('undefined' !== typeof window) { super(...args);
const Lights = await import('@pixi/lights'); this.parentGroup = deferredLighting.lightGroup;
AmbientLight = class AmbientLight extends Lights.AmbientLight {
constructor(...args) {
super(...args);
this.parentGroup = deferredLighting.lightGroup;
}
}
PointLight = class PointLight extends Lights.PointLight {
constructor(...args) {
super(...args);
this.parentGroup = deferredLighting.lightGroup;
}
} }
} }
export {AmbientLight, PointLight}; export class PointLight extends Lights.PointLight {
constructor(...args) {
super(...args);
this.parentGroup = deferredLighting.lightGroup;
}
}

View File

@ -4,6 +4,7 @@ import Systems from '@/ecs/systems/index.js';
export default function createEcs(Ecs) { export default function createEcs(Ecs) {
const ecs = new Ecs({Components, Systems}); const ecs = new Ecs({Components, Systems});
const defaultSystems = [ const defaultSystems = [
'Behave',
'PassTime', 'PassTime',
'Attract', 'Attract',
'ResetForces', 'ResetForces',

View File

@ -94,5 +94,40 @@ export default async function createHomestead(id) {
Ticking: {}, Ticking: {},
VisibleAabb: {}, VisibleAabb: {},
}); });
const kitty = {
Behaving: {
routines: {
initial: '/assets/kitty/initial.js',
},
},
Collider: {
bodies: [
{
points: [
{x: -4, y: -4},
{x: 3, y: -4},
{x: 3, y: 3},
{x: -4, y: 3},
],
},
],
},
Controlled: {},
Direction: {direction: 2},
Forces: {},
Position: {x: 250, y: 250},
Speed: {speed: 20},
Sprite: {
anchorX: 0.5,
anchorY: 0.7,
source: '/assets/kitty/kitty.json',
speed: 0.115,
},
Ticking: {},
VisibleAabb: {},
};
for (let i = 0; i < 10; ++i) {
entities.push(kitty);
}
return entities; return entities;
} }

View File

@ -5,10 +5,10 @@ export default async function createPlayer(id) {
bodies: [ bodies: [
{ {
points: [ points: [
{x: -8, y: -8}, {x: -4, y: -4},
{x: 7, y: -8}, {x: 3, y: -4},
{x: 7, y: 7}, {x: 3, y: 3},
{x: -8, y: 7}, {x: -4, y: 3},
], ],
}, },
], ],
@ -49,10 +49,9 @@ export default async function createPlayer(id) {
Sound: {}, Sound: {},
Sprite: { Sprite: {
anchorX: 0.5, anchorX: 0.5,
anchorY: 0.8, anchorY: 0.9,
animation: 'moving:down', animation: 'moving:down',
frame: 0, frame: 0,
frames: 8,
source: '/assets/dude/dude.json', source: '/assets/dude/dude.json',
speed: 0.115, speed: 0.115,
}, },

View File

@ -7,6 +7,7 @@ import {
RESOLUTION, RESOLUTION,
TPS, TPS,
} from '@/util/constants.js'; } from '@/util/constants.js';
import {withResolvers} from '@/util/promise.js';
import createEcs from './create/ecs.js'; import createEcs from './create/ecs.js';
import createForest from './create/forest.js'; import createForest from './create/forest.js';
@ -48,11 +49,7 @@ export default class Engine {
} }
async readAsset(uri) { async readAsset(uri) {
if (!cache.has(uri)) { if (!cache.has(uri)) {
let promise, resolve, reject; const {promise, resolve, reject} = withResolvers();
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(uri, promise); cache.set(uri, promise);
server.readAsset(uri) server.readAsset(uri)
.then(resolve) .then(resolve)
@ -88,7 +85,7 @@ export default class Engine {
...updates, ...updates,
}; };
// remove from old ECS // remove from old ECS
this.destroy(entity.id); this.destroyImmediately(entity.id);
// load if necessary // load if necessary
if (!engine.ecses[path]) { if (!engine.ecses[path]) {
await engine.loadEcs(path); await engine.loadEcs(path);
@ -230,7 +227,7 @@ export default class Engine {
const {entity, id} = connectedPlayer; const {entity, id} = connectedPlayer;
const ecs = this.ecses[entity.Ecs.path]; const ecs = this.ecses[entity.Ecs.path];
await this.savePlayer(id, entity); await this.savePlayer(id, entity);
ecs.destroy(entity.id); ecs.destroyImmediately(entity.id);
this.connectedPlayers.delete(connection); this.connectedPlayers.delete(connection);
this.incomingActions.delete(connection); this.incomingActions.delete(connection);
} }
@ -322,7 +319,6 @@ export default class Engine {
stop() { stop() {
clearTimeout(this.handle); clearTimeout(this.handle);
this.handle = undefined; this.handle = undefined;
this.tick(0);
} }
tick(elapsed) { tick(elapsed) {

View File

@ -0,0 +1,8 @@
import {settings} from '@pixi/core';
settings.ADAPTER = {
...settings.ADAPTER,
createCanvas: () => ({
getContext: () => undefined,
}),
};

View File

@ -2,6 +2,7 @@ import {del, get, set} from 'idb-keyval';
import {encode} from '@/net/packets/index.js'; import {encode} from '@/net/packets/index.js';
import Server from '@/net/server.js'; import Server from '@/net/server.js';
import {withResolvers} from '@/util/promise.js';
import createEcs from './create/ecs.js'; import createEcs from './create/ecs.js';
import './create/forest.js'; import './create/forest.js';
@ -62,41 +63,32 @@ onmessage = async (event) => {
})(); })();
if (import.meta.hot) { if (import.meta.hot) {
const createResolver = () => { const before = withResolvers();
let r; const promises = [before.promise];
const promise = new Promise((resolve) => {
r = resolve;
});
promise.resolve = r;
return promise;
};
const beforeResolver = createResolver();
const resolvers = [beforeResolver];
import.meta.hot.on('vite:beforeUpdate', async () => { import.meta.hot.on('vite:beforeUpdate', async () => {
engine.stop(); engine.stop();
await engine.disconnectPlayer(0); await engine.disconnectPlayer(0);
await engine.saveEcses(); await engine.saveEcses();
beforeResolver.resolve(); before.resolve();
}); });
import.meta.hot.accept('./engine.js'); import.meta.hot.accept('./engine.js');
import.meta.hot.accept('./create/player.js', async ({default: createPlayer}) => { import.meta.hot.accept('./create/player.js', async ({default: createPlayer}) => {
const resolver = createResolver(); const {promise, resolve} = withResolvers();
resolvers.push(resolver); promises.push(promise);
await beforeResolver; await before.promise;
const oldPlayer = await engine.server.readJson('players/0'); const oldPlayer = await engine.loadPlayer(0);
const buffer = await createPlayer(0); const player = await createPlayer(0);
const player = JSON.parse((new TextDecoder()).decode(buffer));
// Less jarring // Less jarring
player.Ecs = oldPlayer.Ecs; player.Ecs = oldPlayer.Ecs;
player.Direction = oldPlayer.Direction; player.Direction = oldPlayer.Direction;
player.Position = oldPlayer.Position; player.Position = oldPlayer.Position;
await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player))); await engine.savePlayer(0, player);
resolver.resolve(); resolve();
}); });
import.meta.hot.accept('./create/forest.js', async ({default: createForest}) => { import.meta.hot.accept('./create/forest.js', async ({default: createForest}) => {
const resolver = createResolver(); const {promise, resolve} = withResolvers();
resolvers.push(resolver); promises.push(promise);
await beforeResolver; await before.promise;
delete engine.ecses['forests/0']; delete engine.ecses['forests/0'];
await engine.server.removeData('forests/0'); await engine.server.removeData('forests/0');
const forest = createEcs(engine.Ecs); const forest = createEcs(engine.Ecs);
@ -104,12 +96,12 @@ if (import.meta.hot) {
await forest.create(entity); await forest.create(entity);
} }
await engine.saveEcs('forests/0', forest); await engine.saveEcs('forests/0', forest);
resolver.resolve(); resolve();
}); });
import.meta.hot.accept('./create/homestead.js', async ({default: createHomestead}) => { import.meta.hot.accept('./create/homestead.js', async ({default: createHomestead}) => {
const resolver = createResolver(); const {promise, resolve} = withResolvers();
resolvers.push(resolver); promises.push(promise);
await beforeResolver; await before.promise;
delete engine.ecses['homesteads/0']; delete engine.ecses['homesteads/0'];
await engine.server.removeData('homesteads/0'); await engine.server.removeData('homesteads/0');
const homestead = createEcs(engine.Ecs); const homestead = createEcs(engine.Ecs);
@ -117,10 +109,10 @@ if (import.meta.hot) {
await homestead.create(entity); await homestead.create(entity);
} }
await engine.saveEcs('homesteads/0', homestead); await engine.saveEcs('homesteads/0', homestead);
resolver.resolve(); resolve();
}); });
import.meta.hot.on('vite:afterUpdate', async () => { import.meta.hot.on('vite:afterUpdate', async () => {
await Promise.all(resolvers); await Promise.all(promises);
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'})); postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
close(); close();
}); });

View File

@ -1,6 +1,6 @@
export const CHUNK_SIZE = 32; export const CHUNK_SIZE = 32;
export const CLIENT_LATENCY = 0; export const CLIENT_LATENCY = 100;
export const CLIENT_PREDICTION = true; export const CLIENT_PREDICTION = true;
@ -11,6 +11,6 @@ export const RESOLUTION = {
y: 450, y: 450,
}; };
export const SERVER_LATENCY = 0; export const SERVER_LATENCY = 100;
export const TPS = 60; export const TPS = 60;

View File

@ -1,4 +1,4 @@
import TickingPromise from '@/util/ticking-promise.js'; import {Ticker} from '@/util/promise.js';
export default function delta(object, properties) { export default function delta(object, properties) {
const deltas = {}; const deltas = {};
@ -12,7 +12,7 @@ export default function delta(object, properties) {
deltas[key] = delta; deltas[key] = delta;
} }
let stop; let stop;
const promise = new TickingPromise( const promise = new Ticker(
(resolve) => { (resolve) => {
stop = resolve; stop = resolve;
}, },

View File

@ -1,4 +1,4 @@
import TickingPromise from '@/util/ticking-promise.js'; import {Ticker, withResolvers} from '@/util/promise.js';
const Modulators = { const Modulators = {
flat: () => 0.5, flat: () => 0.5,
@ -35,9 +35,7 @@ export default function lfo(object, properties) {
} }
} }
oscillator.low = oscillator.median - oscillator.magnitude / 2; oscillator.low = oscillator.median - oscillator.magnitude / 2;
oscillator.promise = new Promise((resolve) => { ({promise: oscillator.promise, resolve: oscillator.stop} = withResolvers());
oscillator.stop = resolve;
});
oscillator.promise.then(() => { oscillator.promise.then(() => {
delete oscillators[key]; delete oscillators[key];
}); });
@ -45,7 +43,7 @@ export default function lfo(object, properties) {
oscillators[key] = oscillator; oscillators[key] = oscillator;
} }
let stop; let stop;
const promise = new TickingPromise( const promise = new Ticker(
(resolve) => { (resolve) => {
stop = resolve; stop = resolve;
Promise.all(promises).then(resolve); Promise.all(promises).then(resolve);

View File

@ -121,6 +121,13 @@ export function createRandom(seed = 0) {
return alea(seed); return alea(seed);
} }
export const directionToVector = [
{x: 0, y: -1},
{x: 1, y: 0},
{x: 0, y: 1},
{x: -1, y: 0},
];
export function distance({x: lx, y: ly}, {x: rx, y: ry}) { export function distance({x: lx, y: ly}, {x: rx, y: ry}) {
const xd = lx - rx; const xd = lx - rx;
const yd = ly - ry; const yd = ly - ry;
@ -246,13 +253,6 @@ export function isCollinear({x: ax, y: ay}, {x: bx, y: by}, {x: cx, y: cy}) {
return (ay - by) * (ax - cx) === (ay - cy) * (ax - bx); return (ay - by) * (ax - cx) === (ay - cy) * (ax - bx);
} }
export const directionToVector = [
{x: 0, y: -1},
{x: 1, y: 0},
{x: 0, y: 1},
{x: -1, y: 0},
];
export function ortho(points, k = {x: 1, y: 1}) { export function ortho(points, k = {x: 1, y: 1}) {
if (points.length < 4) { if (points.length < 4) {
throw new TypeError('Math.ortho(): points.length < 4'); throw new TypeError('Math.ortho(): points.length < 4');

View File

@ -1,4 +1,19 @@
export default class TickingPromise extends Promise { export const all = Promise.all.bind(Promise);
export const allSettled = Promise.allSettled.bind(Promise);
export const any = Promise.any.bind(Promise);
export const race = Promise.race.bind(Promise);
export const reject = Promise.reject.bind(Promise);
export const resolve = Promise.resolve.bind(Promise);
export function withResolvers() {
let resolve, reject, promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return {promise, reject, resolve};
}
export class Ticker extends Promise {
constructor(executor, ticker) { constructor(executor, ticker) {
let _reject; let _reject;
@ -16,22 +31,22 @@ export default class TickingPromise extends Promise {
} }
static all(promises) { static all(promises) {
const tickingPromises = []; const tickers = [];
for (let i = 0; i < promises.length; i++) { for (let i = 0; i < promises.length; i++) {
const promise = promises[i]; const promise = promises[i];
if (promise instanceof TickingPromise) { if (promise instanceof Ticker) {
tickingPromises.push(promise); tickers.push(promise);
// After resolution, stop ticking the promise. // After resolution, stop ticking the promise.
promise.then(() => { promise.then(() => {
tickingPromises.splice(tickingPromises.indexOf(promise), 1); tickers.splice(tickers.indexOf(promise), 1);
}); });
} }
} }
/* v8 ignore next 3 */ /* v8 ignore next 3 */
if (0 === tickingPromises.length) { if (0 === tickers.length) {
return super.all(promises); return super.all(promises);
} }
return new TickingPromise( return new Ticker(
(resolve, reject) => { (resolve, reject) => {
super.all(promises) super.all(promises)
.then(resolve) .then(resolve)
@ -39,8 +54,8 @@ export default class TickingPromise extends Promise {
.catch(reject); .catch(reject);
}, },
(elapsed) => { (elapsed) => {
for (let i = 0; i < tickingPromises.length; i++) { for (let i = 0; i < tickers.length; i++) {
tickingPromises[i].tick(elapsed); tickers[i].tick(elapsed);
} }
}, },
); );

View File

@ -1,17 +1,17 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import TickingPromise from './ticking-promise.js'; import {Ticker} from './promise.js';
test('runs executor', async () => { test('runs executor', async () => {
expect( expect(
await new TickingPromise((resolve) => { await new Ticker((resolve) => {
resolve(32); resolve(32);
}), }),
) )
.to.equal(32); .to.equal(32);
expect( expect(
async () => { async () => {
await new TickingPromise((resolve, reject) => { await new Ticker((resolve, reject) => {
reject(new Error('')); reject(new Error(''));
}) })
} }
@ -22,7 +22,7 @@ test('runs executor', async () => {
test('ticks and resolves', async () => { test('ticks and resolves', async () => {
let done = false; let done = false;
let e = 0; let e = 0;
const tp = new TickingPromise(undefined, (elapsed, resolve) => { const tp = new Ticker(undefined, (elapsed, resolve) => {
e += elapsed; e += elapsed;
if (1 === e) { if (1 === e) {
done = true; done = true;
@ -49,7 +49,7 @@ test('ticks and resolves', async () => {
test('ticks and rejects', async () => { test('ticks and rejects', async () => {
let caught = false; let caught = false;
const tp = new TickingPromise(undefined, (elapsed, resolve, reject) => { const tp = new Ticker(undefined, (elapsed, resolve, reject) => {
reject(new Error()); reject(new Error());
}); });
tp.catch(() => { tp.catch(() => {
@ -66,21 +66,21 @@ test('ticks and rejects', async () => {
test('handles all', async () => { test('handles all', async () => {
let done = 0; let done = 0;
let e1 = 0, e2 = 0; let e1 = 0, e2 = 0;
const tp1 = new TickingPromise(undefined, (elapsed, resolve) => { const tp1 = new Ticker(undefined, (elapsed, resolve) => {
e1 += elapsed; e1 += elapsed;
if (1 === e1) { if (1 === e1) {
done += 1; done += 1;
resolve(16); resolve(16);
} }
}); });
const tp2 = new TickingPromise(undefined, (elapsed, resolve) => { const tp2 = new Ticker(undefined, (elapsed, resolve) => {
e2 += elapsed; e2 += elapsed;
if (2 === e2) { if (2 === e2) {
done += 1; done += 1;
resolve(32); resolve(32);
} }
}); });
const tpa = TickingPromise.all([ const tpa = Ticker.all([
Promise.resolve(8), Promise.resolve(8),
tp1, tp1,
tp2, tp2,

View File

@ -2,10 +2,10 @@ import {parse as acornParse} from 'acorn';
import {LRUCache} from 'lru-cache'; import {LRUCache} from 'lru-cache';
import Sandbox from '@/astride/sandbox.js'; import Sandbox from '@/astride/sandbox.js';
import TickingPromise from '@/util/ticking-promise.js';
import delta from '@/util/delta.js'; import delta from '@/util/delta.js';
import lfo from '@/util/lfo.js'; import lfo from '@/util/lfo.js';
import * as MathUtil from '@/util/math.js'; import * as MathUtil from '@/util/math.js';
import * as PromiseUtil from '@/util/promise.js';
import transition from '@/util/transition.js'; import transition from '@/util/transition.js';
function parse(code, options = {}) { function parse(code, options = {}) {
@ -43,7 +43,7 @@ export default class Script {
delta, delta,
lfo, lfo,
Math: MathUtil, Math: MathUtil,
Promise, Promise: PromiseUtil,
transition, transition,
wait: (seconds) => ( wait: (seconds) => (
new Promise((resolve) => { new Promise((resolve) => {
@ -95,7 +95,7 @@ export default class Script {
tick(elapsed, resolve, reject) { tick(elapsed, resolve, reject) {
if (this.promise) { if (this.promise) {
if (this.promise instanceof TickingPromise) { if (this.promise instanceof PromiseUtil.Ticker) {
this.promise.tick(elapsed); this.promise.tick(elapsed);
} }
return; return;
@ -106,10 +106,12 @@ export default class Script {
if (async) { if (async) {
this.promise = value; this.promise = value;
value value
.catch(reject) .catch(reject ? reject : () => {})
.then(() => { .then(() => {
if (done) { if (done) {
resolve(); if (resolve) {
resolve();
}
} }
}) })
.finally(() => { .finally(() => {
@ -118,14 +120,16 @@ export default class Script {
break; break;
} }
if (done) { if (done) {
resolve(); if (resolve) {
resolve();
}
break; break;
} }
} }
} }
tickingPromise() { ticker() {
return new TickingPromise( return new PromiseUtil.Ticker(
() => {}, () => {},
(elapsed, resolve, reject) => { (elapsed, resolve, reject) => {
this.tick(elapsed, resolve, reject); this.tick(elapsed, resolve, reject);
@ -133,18 +137,18 @@ export default class Script {
); );
} }
static tickingPromise(code, context = {}) { static ticker(code, context = {}) {
let tickingPromise; let ticker;
return new TickingPromise( return new PromiseUtil.Ticker(
(resolve) => { (resolve) => {
this.fromCode(code, context) this.fromCode(code, context)
.then((script) => { .then((script) => {
tickingPromise = script.tickingPromise(); ticker = script.ticker();
resolve(tickingPromise); resolve(ticker);
}) })
}, },
(elapsed) => { (elapsed) => {
tickingPromise?.tick?.(elapsed); ticker?.tick?.(elapsed);
}, },
); );
} }

View File

@ -1,4 +1,4 @@
import TickingPromise from '@/util/ticking-promise.js'; import {Ticker} from '@/util/promise.js';
/* eslint-disable */ /* eslint-disable */
const Easing = { const Easing = {
@ -152,7 +152,7 @@ export default function transition(object, properties) {
transitions[key] = transition; transitions[key] = transition;
} }
let stop; let stop;
const promise = new TickingPromise( const promise = new Ticker(
(resolve) => { (resolve) => {
stop = resolve; stop = resolve;
}, },

View File

@ -0,0 +1,16 @@
entity.Direction.direction = Math.floor(Math.random() * 4);
const map = {0: 'moveUp', 1: 'moveRight', 2: 'moveDown', 3: 'moveLeft'};
entity.Controlled[map[entity.Direction.direction]] = 1;
await wait(0.25 + Math.random() * 2.25);
entity.Controlled[map[entity.Direction.direction]] = 0;
entity.Sprite.isAnimating = 0;
await wait(1 + Math.random() * 3);
entity.Direction.direction = Math.floor(Math.random() * 4);
await wait(0.5 + Math.random() * 2.5);
entity.Sprite.isAnimating = 1;

View File

@ -0,0 +1,263 @@
{
"animations": {
"idle:up": [
"kitty/0"
],
"idle:right": [
"kitty/3"
],
"idle:down": [
"kitty/6"
],
"idle:left": [
"kitty/9"
],
"moving:up": [
"kitty/0",
"kitty/1",
"kitty/2"
],
"moving:right": [
"kitty/3",
"kitty/4",
"kitty/5"
],
"moving:down": [
"kitty/6",
"kitty/7",
"kitty/8"
],
"moving:left": [
"kitty/9",
"kitty/10",
"kitty/11"
]
},
"frames": {
"kitty/0": {
"frame": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/1": {
"frame": {
"x": 16,
"y": 0,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/2": {
"frame": {
"x": 32,
"y": 0,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/3": {
"frame": {
"x": 0,
"y": 16,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/4": {
"frame": {
"x": 16,
"y": 16,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/5": {
"frame": {
"x": 32,
"y": 16,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/6": {
"frame": {
"x": 0,
"y": 32,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/7": {
"frame": {
"x": 16,
"y": 32,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/8": {
"frame": {
"x": 32,
"y": 32,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/9": {
"frame": {
"x": 0,
"y": 48,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/10": {
"frame": {
"x": 16,
"y": 48,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
},
"kitty/11": {
"frame": {
"x": 32,
"y": 48,
"w": 16,
"h": 16
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 16,
"h": 16
},
"sourceSize": {
"w": 16,
"h": 16
}
}
},
"meta": {
"format": "RGBA8888",
"image": "./kitty.png",
"scale": 1,
"size": {
"w": 48,
"h": 64
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

View File

@ -48,71 +48,61 @@ for (let i = 0; i < 10; ++i) {
}, },
) )
const l = lfo( tomato.Ticking.add(
d.deltas.y, Promise.Ticker.all([
{ d.promise,
delta: { lfo(
count: 1, d.deltas.y,
frequency: 0.5, {
magnitude: 64, delta: {
median: 0, count: 1,
offset: -0.5, frequency: 0.5,
}, magnitude: 64,
}, median: 0,
) offset: -0.5,
const ls = 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,
},
},
)
tomato.Ticking.addTickingPromise(
d.promise,
);
tomato.Ticking.addTickingPromise(
l.promise,
);
tomato.Ticking.addTickingPromise(
ls.promise,
);
tomato.Ticking.addTickingPromise(
delta(
tomato.Position,
{
x: {
duration: 0.5,
delta: (12 * x) + Math.random() * 8,
}, },
}, ).promise,
).promise, lfo(
); tomato.Sprite,
tomato.Ticking.addTickingPromise( {
delta( scaleX: {
tomato.Position, count: 1,
{ frequency: 0.5,
y: { magnitude: 0.333,
duration: 0.5, median: 0.333,
delta: (12 * y) + Math.random() * 8, 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,
).promise, 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,
},
},
).promise,
]),
); );
} }

View File

@ -44,6 +44,8 @@ let websocketBuilt = false;
const remixHandler = createRequestHandler({ const remixHandler = createRequestHandler({
build: async () => { build: async () => {
// patch pixi server context
import('./app/server/pixi-context.js');
const ssr = await ( const ssr = await (
viteDevServer viteDevServer
? viteDevServer.ssrLoadModule('virtual:remix/server-build') ? viteDevServer.ssrLoadModule('virtual:remix/server-build')

View File

@ -25,6 +25,11 @@ else {
} }
export default defineConfig({ export default defineConfig({
esbuild: {
supported: {
'top-level-await': true,
},
},
plugins, plugins,
resolve: { resolve: {
alias: [ alias: [
@ -41,4 +46,7 @@ export default defineConfig({
cert: readFileSync(`${cacheDirectory}/localhost.pem`), cert: readFileSync(`${cacheDirectory}/localhost.pem`),
}, },
}, },
worker: {
format: 'es',
},
}); });