Compare commits
16 Commits
73082aad94
...
d88135c85f
Author | SHA1 | Date | |
---|---|---|---|
|
d88135c85f | ||
|
4bf9b8d891 | ||
|
d63f835ebd | ||
|
b12183a6ee | ||
|
c339491590 | ||
|
f89c94b619 | ||
|
15674fb1d7 | ||
|
c6557bee39 | ||
|
2b4c5f5a8e | ||
|
c8622c6814 | ||
|
d8528ad7a5 | ||
|
0cb1624cd8 | ||
|
74ec36dfa8 | ||
|
76f18e09c7 | ||
|
438a0c3be5 | ||
|
95b666e844 |
|
@ -41,7 +41,7 @@ module.exports = {
|
|||
|
||||
// React
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
files: ['**/*.{jsx,tsx}'],
|
||||
plugins: ['react', 'jsx-a11y'],
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
|
|
|
@ -248,6 +248,11 @@ export default class Sandbox {
|
|||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.generator = undefined;
|
||||
this.compile();
|
||||
}
|
||||
|
||||
run(ops = 1000) {
|
||||
let result;
|
||||
for (let i = 0; i < ops; ++i) {
|
||||
|
@ -265,8 +270,7 @@ export default class Sandbox {
|
|||
}
|
||||
const result = this.generator.next();
|
||||
if (result.done) {
|
||||
this.generator = undefined;
|
||||
this.compile();
|
||||
this.reset();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
25
app/context/assets.js
Normal file
25
app/context/assets.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import {Assets} from '@pixi/assets';
|
||||
import {createContext, useContext, useEffect} from 'react';
|
||||
|
||||
const context = createContext();
|
||||
|
||||
export default context;
|
||||
|
||||
const loading = {};
|
||||
|
||||
export function useAsset(source) {
|
||||
const [assets, setAssets] = useContext(context);
|
||||
useEffect(() => {
|
||||
if (!assets[source]) {
|
||||
if (!loading[source]) {
|
||||
(loading[source] = Assets.load(source)).then((asset) => {
|
||||
setAssets((assets) => ({
|
||||
...assets,
|
||||
[source]: asset,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [assets, setAssets, source]);
|
||||
return assets[source];
|
||||
}
|
|
@ -1,3 +1,26 @@
|
|||
import {createContext} from 'react';
|
||||
import {createContext, useContext, useEffect} from 'react';
|
||||
|
||||
export default createContext();
|
||||
const context = createContext();
|
||||
|
||||
export default context;
|
||||
|
||||
export function useClient() {
|
||||
return useContext(context);
|
||||
}
|
||||
|
||||
export function usePacket(type, fn, dependencies) {
|
||||
const client = useClient();
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
function listener(payload) {
|
||||
fn(payload, client);
|
||||
}
|
||||
client.addPacketListener(type, listener);
|
||||
return () => {
|
||||
client.removePacketListener(type, listener);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [client, ...dependencies]);
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import {createContext, useContext} from 'react';
|
||||
|
||||
import {usePacket} from './client.js';
|
||||
|
||||
const context = createContext();
|
||||
|
||||
export default context;
|
||||
|
@ -7,3 +9,8 @@ export default context;
|
|||
export function useEcs() {
|
||||
return useContext(context);
|
||||
}
|
||||
|
||||
export function useEcsTick(fn, dependencies) {
|
||||
const ecs = useEcs();
|
||||
usePacket(':Ecs', fn, [ecs, ...dependencies]);
|
||||
}
|
|
@ -43,15 +43,19 @@ export default class Inventory extends Component {
|
|||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
Instance.prototype.item = async function (slot) {
|
||||
return class InventoryInstance extends Instance {
|
||||
async item(slot) {
|
||||
const {slots} = this;
|
||||
if (!(slot in slots)) {
|
||||
return undefined;
|
||||
}
|
||||
const json = await (
|
||||
fetch([slots[slot].source, 'item.json'].join('/'))
|
||||
.then((response) => (response.ok ? response.json() : {}))
|
||||
);
|
||||
const {readAsset} = Component.ecs;
|
||||
const chars = await readAsset([slots[slot].source, 'item.json'].join('/'))
|
||||
const json = chars.byteLength > 0
|
||||
? JSON.parse(
|
||||
(new TextDecoder()).decode(chars),
|
||||
)
|
||||
: {};
|
||||
const item = {
|
||||
...slots[slot],
|
||||
...json,
|
||||
|
@ -71,8 +75,8 @@ export default class Inventory extends Component {
|
|||
},
|
||||
});
|
||||
return proxy;
|
||||
};
|
||||
Instance.prototype.swapSlots = function(l, r) {
|
||||
}
|
||||
swapSlots(l, r) {
|
||||
const {slots} = this;
|
||||
const tmp = slots[l];
|
||||
const change = {};
|
||||
|
@ -91,8 +95,8 @@ export default class Inventory extends Component {
|
|||
delete slots[r];
|
||||
}
|
||||
Component.markChange(this.entity, 'slotChange', change);
|
||||
};
|
||||
return Instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
slots: {
|
||||
|
|
58
app/ecs-components/plant.js
Normal file
58
app/ecs-components/plant.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Script from '@/util/script.js';
|
||||
|
||||
export default class Plant extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
const Instance = super.instanceFromSchema();
|
||||
return class PlantInstance extends Instance {
|
||||
mayGrow() {
|
||||
return this.mayGrowScriptInstance.evaluateSync();
|
||||
}
|
||||
grow() {
|
||||
const {Ticking} = ecs.get(this.entity);
|
||||
Ticking.addTickingPromise(this.growScriptInstance.tickingPromise());
|
||||
}
|
||||
};
|
||||
}
|
||||
async load(instance) {
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
const {readAsset} = this.ecs;
|
||||
await readAsset(instance.growScript)
|
||||
.then(async (code) => {
|
||||
if (code.byteLength > 0) {
|
||||
const context = {
|
||||
ecs: this.ecs,
|
||||
plant: instance,
|
||||
};
|
||||
instance.growScriptInstance = await Script.fromCode((new TextDecoder()).decode(code), context);
|
||||
}
|
||||
});
|
||||
|
||||
await readAsset(instance.mayGrowScript)
|
||||
.then(async (code) => {
|
||||
if (code.byteLength > 0) {
|
||||
const context = {
|
||||
ecs: this.ecs,
|
||||
plant: instance,
|
||||
};
|
||||
instance.mayGrowScriptInstance = await Script.fromCode((new TextDecoder()).decode(code), context);
|
||||
}
|
||||
});
|
||||
}
|
||||
// heavy handed...
|
||||
markChange() {}
|
||||
static properties = {
|
||||
growScript: {type: 'string'},
|
||||
growth: {type: 'uint16'},
|
||||
mayGrowScript: {type: 'string'},
|
||||
stage: {type: 'uint8'},
|
||||
stages: {
|
||||
type: 'array',
|
||||
subtype: {type: 'uint16'},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -4,15 +4,19 @@ export default class Ticking extends Component {
|
|||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
|
||||
Instance.prototype.$$finished = [];
|
||||
Instance.prototype.$$tickingPromises = [];
|
||||
Instance.prototype.addTickingPromise = function(tickingPromise) {
|
||||
return class TickingInstance extends Instance {
|
||||
|
||||
$$finished = [];
|
||||
$$tickingPromises = [];
|
||||
|
||||
addTickingPromise(tickingPromise) {
|
||||
this.$$tickingPromises.push(tickingPromise);
|
||||
tickingPromise.then(() => {
|
||||
this.$$finished.push(tickingPromise);
|
||||
});
|
||||
}
|
||||
Instance.prototype.tick = function(elapsed) {
|
||||
|
||||
tick(elapsed) {
|
||||
for (const tickingPromise of this.$$finished) {
|
||||
this.$$tickingPromises.splice(
|
||||
this.$$tickingPromises.indexOf(tickingPromise),
|
||||
|
@ -23,15 +27,9 @@ export default class Ticking extends Component {
|
|||
for (const tickingPromise of this.$$tickingPromises) {
|
||||
tickingPromise.tick(elapsed);
|
||||
}
|
||||
for (const tickingPromise of this.$$finished) {
|
||||
this.$$tickingPromises.splice(
|
||||
this.$$tickingPromises.indexOf(tickingPromise),
|
||||
1,
|
||||
);
|
||||
}
|
||||
this.$$finished = [];
|
||||
|
||||
}
|
||||
return Instance;
|
||||
}
|
||||
static properties = {
|
||||
isTicking: {defaultValue: 1, type: 'uint8'},
|
||||
|
|
|
@ -37,7 +37,8 @@ export default class TileLayers extends Component {
|
|||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
Instance.prototype.layer = function (index) {
|
||||
return class TileLayersInstance extends Instance {
|
||||
layer(index) {
|
||||
const {layers} = this;
|
||||
if (!(index in layers)) {
|
||||
return undefined;
|
||||
|
@ -82,8 +83,8 @@ export default class TileLayers extends Component {
|
|||
}
|
||||
}
|
||||
return new LayerProxy(layers[index]);
|
||||
};
|
||||
return Instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
layers: {
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Script from '@/util/script.js';
|
||||
|
||||
export default class Wielder extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
Instance.prototype.activeItem = async function () {
|
||||
const {Inventory, Wielder} = Component.ecs.get(this.entity);
|
||||
return class WielderInstance extends Instance {
|
||||
async activeItem() {
|
||||
const {Inventory, Wielder} = ecs.get(this.entity);
|
||||
return Inventory.item(Wielder.activeSlot + 1);
|
||||
};
|
||||
Instance.prototype.project = function(position, projection) {
|
||||
const {TileLayers: {layers: [layer]}} = Component.ecs.get(1);
|
||||
const {Direction: {direction}} = Component.ecs.get(this.entity);
|
||||
}
|
||||
project(position, projection) {
|
||||
const {TileLayers: {layers: [layer]}} = ecs.get(1);
|
||||
const {Direction: {direction}} = ecs.get(this.entity);
|
||||
let startX = position.x;
|
||||
let startY = position.y;
|
||||
switch (direction) {
|
||||
|
@ -63,7 +65,27 @@ export default class Wielder extends Component {
|
|||
}
|
||||
return projected;
|
||||
}
|
||||
return Instance;
|
||||
async useActiveItem(state) {
|
||||
const entity = ecs.get(this.entity);
|
||||
const {Ticking} = entity;
|
||||
const activeItem = await this.activeItem();
|
||||
if (activeItem) {
|
||||
ecs.readAsset([activeItem.source, state ? 'start.js' : 'stop.js'].join('/'))
|
||||
.then((code) => {
|
||||
if (code.byteLength > 0) {
|
||||
const context = {
|
||||
ecs,
|
||||
item: activeItem,
|
||||
wielder: entity,
|
||||
};
|
||||
Ticking.addTickingPromise(Script.tickingPromise((new TextDecoder()).decode(code), context));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
activeSlot: {type: 'uint16'},
|
||||
|
|
27
app/ecs-systems/plant-growth.js
Normal file
27
app/ecs-systems/plant-growth.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
|
||||
export default class PlantGrowth extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Plant'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const {Plant} of this.select('default')) {
|
||||
if (65535 === Plant.growth || !Plant.mayGrow()) {
|
||||
continue;
|
||||
}
|
||||
const stage = Math.floor(Plant.stage);
|
||||
Plant.growth = Math.min(
|
||||
Plant.growth + (((Math.random() + 0.5) * elapsed) / Plant.stages[stage]) * 65535,
|
||||
65535,
|
||||
);
|
||||
if (65535 === Plant.growth) {
|
||||
Plant.grow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -10,6 +10,9 @@ export default class ControlMovement extends System {
|
|||
|
||||
tick(elapsed) {
|
||||
for (const {Sprite} of this.select('default')) {
|
||||
if (0 === Sprite.speed) {
|
||||
continue;
|
||||
}
|
||||
Sprite.elapsed += elapsed / Sprite.speed;
|
||||
while (Sprite.elapsed > 1) {
|
||||
Sprite.elapsed -= 1;
|
||||
|
|
|
@ -28,22 +28,20 @@ export default class Component {
|
|||
return results;
|
||||
}
|
||||
|
||||
create(entityId, values) {
|
||||
async create(entityId, values) {
|
||||
this.createMany([[entityId, values]]);
|
||||
}
|
||||
|
||||
createMany(entries) {
|
||||
async createMany(entries) {
|
||||
if (entries.length > 0) {
|
||||
const allocated = this.allocateMany(entries.length);
|
||||
const {properties} = this.constructor.schema.specification;
|
||||
const keys = Object.keys(properties);
|
||||
const promises = [];
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [entityId, values = {}] = entries[i];
|
||||
this.map[entityId] = allocated[i];
|
||||
this.data[allocated[i]].entity = entityId;
|
||||
if (false === values) {
|
||||
continue;
|
||||
}
|
||||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
const {defaultValue} = properties[j];
|
||||
|
@ -54,7 +52,9 @@ export default class Component {
|
|||
this.data[allocated[i]][j] = defaultValue;
|
||||
}
|
||||
}
|
||||
promises.push(this.load(this.data[allocated[i]]));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ export default class Component {
|
|||
return this.data[this.map[entityId]];
|
||||
}
|
||||
|
||||
insertMany(entities) {
|
||||
async insertMany(entities) {
|
||||
const creating = [];
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const [entityId, values] = entities[i];
|
||||
|
@ -122,7 +122,7 @@ export default class Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
this.createMany(creating);
|
||||
await this.createMany(creating);
|
||||
}
|
||||
|
||||
instanceFromSchema() {
|
||||
|
@ -170,6 +170,10 @@ export default class Component {
|
|||
return Instance;
|
||||
}
|
||||
|
||||
async load(instance) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
markChange(entityId, key, value) {
|
||||
this.ecs.markChange(entityId, {[this.constructor.componentName]: {[key]: value}})
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class Ecs {
|
|||
}
|
||||
}
|
||||
|
||||
apply(patch) {
|
||||
async apply(patch) {
|
||||
const creating = [];
|
||||
const destroying = [];
|
||||
const removing = [];
|
||||
|
@ -63,7 +63,7 @@ export default class Ecs {
|
|||
this.destroyMany(destroying);
|
||||
this.insertMany(updating);
|
||||
this.removeMany(removing);
|
||||
this.createManySpecific(creating);
|
||||
await this.createManySpecific(creating);
|
||||
}
|
||||
|
||||
changed(criteria) {
|
||||
|
@ -75,6 +75,10 @@ export default class Ecs {
|
|||
next: () => {
|
||||
let result = it.next();
|
||||
hasResult: while (!result.done) {
|
||||
if (false === result.value[1]) {
|
||||
result = it.next();
|
||||
continue;
|
||||
}
|
||||
for (const componentName of criteria) {
|
||||
if (!(componentName in result.value[1])) {
|
||||
result = it.next();
|
||||
|
@ -91,12 +95,12 @@ export default class Ecs {
|
|||
};
|
||||
}
|
||||
|
||||
create(components = {}) {
|
||||
const [entityId] = this.createMany([components]);
|
||||
async create(components = {}) {
|
||||
const [entityId] = await this.createMany([components]);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
createMany(componentsList) {
|
||||
async createMany(componentsList) {
|
||||
const specificsList = [];
|
||||
for (const components of componentsList) {
|
||||
specificsList.push([this.$$caret++, components]);
|
||||
|
@ -104,7 +108,7 @@ export default class Ecs {
|
|||
return this.createManySpecific(specificsList);
|
||||
}
|
||||
|
||||
createManySpecific(specificsList) {
|
||||
async createManySpecific(specificsList) {
|
||||
const entityIds = [];
|
||||
const creating = {};
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
|
@ -125,14 +129,16 @@ export default class Ecs {
|
|||
}
|
||||
this.markChange(entityId, components);
|
||||
}
|
||||
const promises = [];
|
||||
for (const i in creating) {
|
||||
this.Components[i].createMany(creating[i]);
|
||||
promises.push(this.Components[i].createMany(creating[i]));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
this.reindex(entityIds);
|
||||
return entityIds;
|
||||
}
|
||||
|
||||
createSpecific(entityId, components) {
|
||||
async createSpecific(entityId, components) {
|
||||
return this.createManySpecific([[entityId, components]]);
|
||||
}
|
||||
|
||||
|
@ -142,7 +148,7 @@ export default class Ecs {
|
|||
}
|
||||
}
|
||||
|
||||
static deserialize(ecs, view) {
|
||||
static async deserialize(ecs, view) {
|
||||
const componentNames = Object.keys(ecs.Components);
|
||||
const {entities, systems} = decoder.decode(view.buffer);
|
||||
for (const system of systems) {
|
||||
|
@ -164,7 +170,7 @@ export default class Ecs {
|
|||
]);
|
||||
}
|
||||
ecs.$$caret = max + 1;
|
||||
ecs.createManySpecific(specifics);
|
||||
await ecs.createManySpecific(specifics);
|
||||
return ecs;
|
||||
}
|
||||
|
||||
|
@ -220,11 +226,11 @@ export default class Ecs {
|
|||
return this.$$entities[entityId];
|
||||
}
|
||||
|
||||
insert(entityId, components) {
|
||||
this.insertMany([[entityId, components]]);
|
||||
async insert(entityId, components) {
|
||||
return this.insertMany([[entityId, components]]);
|
||||
}
|
||||
|
||||
insertMany(entities) {
|
||||
async insertMany(entities) {
|
||||
const inserting = {};
|
||||
const unique = new Set();
|
||||
for (const [entityId, components] of entities) {
|
||||
|
@ -240,9 +246,11 @@ export default class Ecs {
|
|||
unique.add(entityId);
|
||||
this.markChange(entityId, diff);
|
||||
}
|
||||
const promises = [];
|
||||
for (const componentName in inserting) {
|
||||
this.Components[componentName].insertMany(inserting[componentName]);
|
||||
promises.push(this.Components[componentName].insertMany(inserting[componentName]));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
this.reindex(unique.values());
|
||||
}
|
||||
|
||||
|
@ -315,7 +323,7 @@ export default class Ecs {
|
|||
for (const componentName in removing) {
|
||||
this.Components[componentName].destroyMany(removing[componentName]);
|
||||
}
|
||||
this.reindex(unique.values());
|
||||
this.reindex(unique);
|
||||
}
|
||||
|
||||
static serialize(ecs, view) {
|
||||
|
|
|
@ -23,6 +23,24 @@ const Position = wrapProperties('Position', {
|
|||
z: {type: 'int32'},
|
||||
});
|
||||
|
||||
function asyncTimesTwo(x) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(x * 2)
|
||||
}, 5);
|
||||
});
|
||||
}
|
||||
|
||||
class Async extends Component {
|
||||
static componentName = 'Async';
|
||||
static properties = {
|
||||
foo: {type: 'uint8'},
|
||||
};
|
||||
async load(instance) {
|
||||
instance.foo = await asyncTimesTwo(instance.foo);
|
||||
}
|
||||
}
|
||||
|
||||
test('activates and deactivates systems at runtime', () => {
|
||||
let oneCount = 0;
|
||||
let twoCount = 0;
|
||||
|
@ -63,31 +81,31 @@ test('activates and deactivates systems at runtime', () => {
|
|||
.to.equal(2);
|
||||
});
|
||||
|
||||
test('creates entities with components', () => {
|
||||
test('creates entities with components', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
});
|
||||
|
||||
test("removes entities' components", () => {
|
||||
test("removes entities' components", async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
ecs.remove(entity, ['Position']);
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}}));
|
||||
});
|
||||
|
||||
test('gets entities', () => {
|
||||
test('gets entities', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
});
|
||||
|
||||
test('destroys entities', () => {
|
||||
test('destroys entities', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
expect(ecs.get(entity))
|
||||
|
@ -101,9 +119,9 @@ test('destroys entities', () => {
|
|||
.to.throw();
|
||||
});
|
||||
|
||||
test('inserts components into entities', () => {
|
||||
test('inserts components into entities', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}});
|
||||
const entity = await ecs.create({Empty: {}});
|
||||
ecs.insert(entity, {Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
|
@ -112,7 +130,7 @@ test('inserts components into entities', () => {
|
|||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
||||
});
|
||||
|
||||
test('ticks systems', () => {
|
||||
test('ticks systems', async () => {
|
||||
const Momentum = wrapProperties('Momentum', {
|
||||
x: {type: 'int32'},
|
||||
y: {type: 'int32'},
|
||||
|
@ -141,7 +159,7 @@ test('ticks systems', () => {
|
|||
},
|
||||
});
|
||||
ecs.system('Physics').active = true;
|
||||
const entity = ecs.create({Momentum: {}, Position: {y: 128}});
|
||||
const entity = await ecs.create({Momentum: {}, Position: {y: 128}});
|
||||
const position = JSON.stringify(ecs.get(entity).Position);
|
||||
ecs.tick(1);
|
||||
expect(JSON.stringify(ecs.get(entity).Position))
|
||||
|
@ -213,8 +231,8 @@ test('schedules entities to be deleted when ticking systems', () => {
|
|||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('adds components to and remove components from entities when ticking systems', () => {
|
||||
let addLength, removeLength;
|
||||
test('adds components to and remove components from entities when ticking systems', async () => {
|
||||
let promise;
|
||||
const ecs = new Ecs({
|
||||
Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
|
||||
Systems: {
|
||||
|
@ -225,10 +243,7 @@ test('adds components to and remove components from entities when ticking system
|
|||
};
|
||||
}
|
||||
tick() {
|
||||
this.insertComponents(1, {Foo: {}});
|
||||
}
|
||||
finalize() {
|
||||
addLength = Array.from(this.select('default')).length;
|
||||
promise = this.insertComponents(1, {Foo: {}});
|
||||
}
|
||||
},
|
||||
RemoveComponent: class extends System {
|
||||
|
@ -240,40 +255,38 @@ test('adds components to and remove components from entities when ticking system
|
|||
tick() {
|
||||
this.removeComponents(1, ['Foo']);
|
||||
}
|
||||
finalize() {
|
||||
removeLength = Array.from(this.select('default')).length;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
ecs.system('AddComponent').active = true;
|
||||
ecs.create();
|
||||
ecs.tick(1);
|
||||
expect(addLength)
|
||||
await promise;
|
||||
expect(Array.from(ecs.system('AddComponent').select('default')).length)
|
||||
.to.equal(1);
|
||||
expect(ecs.get(1).Foo)
|
||||
.to.not.be.undefined;
|
||||
ecs.system('AddComponent').active = false;
|
||||
ecs.system('RemoveComponent').active = true;
|
||||
ecs.tick(1);
|
||||
expect(removeLength)
|
||||
expect(Array.from(ecs.system('RemoveComponent').select('default')).length)
|
||||
.to.equal(0);
|
||||
expect(ecs.get(1).Foo)
|
||||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('generates coalesced diffs for entity creation', () => {
|
||||
test('generates diffs for entity creation', async () => {
|
||||
const ecs = new Ecs();
|
||||
let entity;
|
||||
entity = ecs.create();
|
||||
entity = await ecs.create();
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {}});
|
||||
});
|
||||
|
||||
test('generates diffs for adding and removing components', () => {
|
||||
test('generates diffs for adding and removing components', async () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create();
|
||||
entity = await ecs.create();
|
||||
ecs.setClean();
|
||||
ecs.insert(entity, {Position: {x: 64}});
|
||||
expect(ecs.diff)
|
||||
|
@ -286,10 +299,10 @@ test('generates diffs for adding and removing components', () => {
|
|||
.to.deep.equal({[entity]: {Position: false}});
|
||||
});
|
||||
|
||||
test('generates diffs for empty components', () => {
|
||||
test('generates diffs for empty components', async () => {
|
||||
const ecs = new Ecs({Components: {Empty}});
|
||||
let entity;
|
||||
entity = ecs.create({Empty: {}});
|
||||
entity = await ecs.create({Empty: {}});
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Empty: {}}});
|
||||
ecs.setClean();
|
||||
|
@ -298,10 +311,10 @@ test('generates diffs for empty components', () => {
|
|||
.to.deep.equal({[entity]: {Empty: false}});
|
||||
});
|
||||
|
||||
test('generates diffs for entity mutations', () => {
|
||||
test('generates diffs for entity mutations', async () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create({Position: {}});
|
||||
entity = await ecs.create({Position: {}});
|
||||
ecs.setClean();
|
||||
ecs.get(entity).Position.x = 128;
|
||||
expect(ecs.diff)
|
||||
|
@ -311,10 +324,10 @@ test('generates diffs for entity mutations', () => {
|
|||
.to.deep.equal({});
|
||||
});
|
||||
|
||||
test('generates coalesced diffs for components', () => {
|
||||
test('generates coalesced diffs for components', async () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create({Position});
|
||||
entity = await ecs.create({Position});
|
||||
ecs.remove(entity, ['Position']);
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Position: false}});
|
||||
|
@ -323,10 +336,10 @@ test('generates coalesced diffs for components', () => {
|
|||
.to.deep.equal({[entity]: {Position: {}}});
|
||||
});
|
||||
|
||||
test('generates coalesced diffs for mutations', () => {
|
||||
test('generates coalesced diffs for mutations', async () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create({Position});
|
||||
entity = await ecs.create({Position});
|
||||
ecs.setClean();
|
||||
ecs.get(entity).Position.x = 128;
|
||||
ecs.get(entity).Position.x = 256;
|
||||
|
@ -335,10 +348,10 @@ test('generates coalesced diffs for mutations', () => {
|
|||
.to.deep.equal({[entity]: {Position: {x: 512}}});
|
||||
});
|
||||
|
||||
test('generates diffs for deletions', () => {
|
||||
test('generates diffs for deletions', async () => {
|
||||
const ecs = new Ecs();
|
||||
let entity;
|
||||
entity = ecs.create();
|
||||
entity = await ecs.create();
|
||||
ecs.setClean();
|
||||
ecs.destroy(entity);
|
||||
expect(ecs.diff)
|
||||
|
@ -391,7 +404,7 @@ test('calculates entity size', () => {
|
|||
.to.equal(30);
|
||||
});
|
||||
|
||||
test('serializes and deserializes', () => {
|
||||
test('serializes and deserializes', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
|
@ -404,7 +417,7 @@ test('serializes and deserializes', () => {
|
|||
systems: [],
|
||||
});
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = Ecs.deserialize(
|
||||
const deserialized = await Ecs.deserialize(
|
||||
new Ecs({Components: {Empty, Name, Position}}),
|
||||
view,
|
||||
);
|
||||
|
@ -424,12 +437,12 @@ test('serializes and deserializes', () => {
|
|||
.to.equal('foobar');
|
||||
});
|
||||
|
||||
test('deserializes from compatible ECS', () => {
|
||||
test('deserializes from compatible ECS', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = Ecs.deserialize(
|
||||
const deserialized = await Ecs.deserialize(
|
||||
new Ecs({Components: {Empty, Name}}),
|
||||
view,
|
||||
);
|
||||
|
@ -438,3 +451,30 @@ test('deserializes from compatible ECS', () => {
|
|||
expect(deserialized.get(16).toJSON())
|
||||
.to.deep.equal({Name: {name: 'foobar'}});
|
||||
});
|
||||
|
||||
test('creates entities asynchronously', async () => {
|
||||
const ecs = new Ecs({Components: {Async}});
|
||||
const entity = await ecs.create({Async: {foo: 64}});
|
||||
expect(ecs.get(entity).toJSON())
|
||||
.to.deep.equal({Async: {foo: 128}});
|
||||
});
|
||||
|
||||
test('inserts components asynchronously', async () => {
|
||||
const ecs = new Ecs({Components: {Async}});
|
||||
const entity = await ecs.create();
|
||||
await ecs.insert(entity, {Async: {foo: 64}});
|
||||
expect(ecs.get(entity).toJSON())
|
||||
.to.deep.equal({Async: {foo: 128}});
|
||||
});
|
||||
|
||||
test('deserializes asynchronously', async () => {
|
||||
const ecs = new Ecs({Components: {Async}});
|
||||
await ecs.createSpecific(16, {Async: {foo: 16}});
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = await Ecs.deserialize(
|
||||
new Ecs({Components: {Async}}),
|
||||
view,
|
||||
);
|
||||
expect(deserialized.get(16).toJSON())
|
||||
.to.deep.equal({Async: {foo: 64}});
|
||||
});
|
||||
|
|
|
@ -47,12 +47,12 @@ export default class System {
|
|||
|
||||
finalize() {}
|
||||
|
||||
insertComponents(entityId, components) {
|
||||
this.ecs.insert(entityId, components);
|
||||
async insertComponents(entityId, components) {
|
||||
return this.ecs.insert(entityId, components);
|
||||
}
|
||||
|
||||
insertManyComponents(components) {
|
||||
this.ecs.insertMany(components);
|
||||
async insertManyComponents(components) {
|
||||
return this.ecs.insertMany(components);
|
||||
}
|
||||
|
||||
static get priority() {
|
||||
|
|
|
@ -5,7 +5,6 @@ import Ecs from '@/ecs/ecs.js';
|
|||
import Components from '@/ecs-components/index.js';
|
||||
import Systems from '@/ecs-systems/index.js';
|
||||
import {decode, encode} from '@/packets/index.js';
|
||||
import Script from '@/util/script.js';
|
||||
|
||||
function join(...parts) {
|
||||
return parts.join('/');
|
||||
|
@ -32,7 +31,12 @@ export default class Engine {
|
|||
super.transmit(connection, encode(packet));
|
||||
}
|
||||
}
|
||||
this.server = new SilphiusServer();
|
||||
const server = this.server = new SilphiusServer();
|
||||
this.Ecs = class EngineEcs extends Ecs {
|
||||
readAsset(uri) {
|
||||
return server.readAsset(uri);
|
||||
}
|
||||
}
|
||||
this.server.addPacketListener('Action', (connection, payload) => {
|
||||
this.incomingActions.push([this.connectedPlayers.get(connection).entity, payload]);
|
||||
});
|
||||
|
@ -43,7 +47,7 @@ export default class Engine {
|
|||
entity,
|
||||
payload,
|
||||
] of this.incomingActions) {
|
||||
const {Controlled, Inventory, Ticking, Wielder} = entity;
|
||||
const {Controlled, Inventory, Wielder} = entity;
|
||||
switch (payload.type) {
|
||||
case 'changeSlot': {
|
||||
if (!Controlled.locked) {
|
||||
|
@ -66,25 +70,7 @@ export default class Engine {
|
|||
}
|
||||
case 'use': {
|
||||
if (!Controlled.locked) {
|
||||
Inventory.item(Wielder.activeSlot + 1).then(async (item) => {
|
||||
if (item) {
|
||||
const code = await(
|
||||
this.server.readAsset([
|
||||
item.source,
|
||||
payload.value ? 'start.js' : 'stop.js',
|
||||
].join('/'))
|
||||
.then((script) => (script.ok ? script.text() : ''))
|
||||
);
|
||||
if (code) {
|
||||
const context = {
|
||||
ecs: this.ecses[entity.Ecs.path],
|
||||
item,
|
||||
wielder: entity,
|
||||
};
|
||||
Ticking.addTickingPromise(Script.tickingPromise(code, context));
|
||||
}
|
||||
}
|
||||
});
|
||||
Wielder.useActiveItem(payload.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -99,7 +85,7 @@ export default class Engine {
|
|||
await this.loadEcs(entityJson.Ecs.path);
|
||||
}
|
||||
const ecs = this.ecses[entityJson.Ecs.path];
|
||||
const entity = ecs.create(entityJson);
|
||||
const entity = await ecs.create(entityJson);
|
||||
this.connections.push(connection);
|
||||
this.connectedPlayers.set(
|
||||
connection,
|
||||
|
@ -112,13 +98,13 @@ export default class Engine {
|
|||
}
|
||||
|
||||
createEcs() {
|
||||
return new Ecs({Components, Systems});
|
||||
return new this.Ecs({Components, Systems});
|
||||
}
|
||||
|
||||
async createHomestead(id) {
|
||||
const ecs = this.createEcs();
|
||||
const area = {x: 100, y: 60};
|
||||
ecs.create({
|
||||
await ecs.create({
|
||||
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||
Engine: {},
|
||||
TileLayers: {
|
||||
|
@ -132,11 +118,39 @@ export default class Engine {
|
|||
],
|
||||
},
|
||||
});
|
||||
const plant = {
|
||||
Plant: {
|
||||
growScript: '/assets/tomato-plant/grow.js',
|
||||
mayGrowScript: '/assets/tomato-plant/may-grow.js',
|
||||
stages: [300, 300, 300, 300, 300],
|
||||
},
|
||||
Sprite: {
|
||||
anchor: {x: 0.5, y: 0.75},
|
||||
animation: 'stage/0',
|
||||
frame: 0,
|
||||
frames: 1,
|
||||
source: '/assets/tomato-plant/tomato-plant.json',
|
||||
speed: 0,
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
};
|
||||
const promises = [];
|
||||
for (let y = 0; y < 10; ++y) {
|
||||
for (let x = 0; x < 10; ++x) {
|
||||
promises.push(ecs.create({
|
||||
...plant,
|
||||
Position: {x: 8 + x * 16, y: 8 + y * 16},
|
||||
}));
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
const defaultSystems = [
|
||||
'ResetForces',
|
||||
'ApplyControlMovement',
|
||||
'ApplyForces',
|
||||
'ClampPositions',
|
||||
'PlantGrowth',
|
||||
'FollowCamera',
|
||||
'CalculateAabbs',
|
||||
'UpdateSpatialHash',
|
||||
|
@ -217,11 +231,10 @@ export default class Engine {
|
|||
}
|
||||
|
||||
async loadEcs(path) {
|
||||
this.ecses[path] = Ecs.deserialize(
|
||||
this.ecses[path] = await this.Ecs.deserialize(
|
||||
this.createEcs(),
|
||||
await this.server.readData(path),
|
||||
);
|
||||
this.ecses[path].get(1).Engine.engine = this;
|
||||
}
|
||||
|
||||
async loadPlayer(id) {
|
||||
|
@ -240,7 +253,7 @@ export default class Engine {
|
|||
}
|
||||
|
||||
async saveEcs(path, ecs) {
|
||||
const view = Ecs.serialize(ecs);
|
||||
const view = this.Ecs.serialize(ecs);
|
||||
await this.server.writeData(path, view);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ test('visibility-based updates', async () => {
|
|||
await engine.connectPlayer(0, 0);
|
||||
const ecs = engine.ecses['homesteads/0'];
|
||||
// Create an entity.
|
||||
const entity = ecs.get(ecs.create({
|
||||
const entity = ecs.get(await ecs.create({
|
||||
Forces: {forceX: 1},
|
||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
||||
VisibleAabb: {},
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import {Assets} from '@pixi/assets';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function useAsset(source) {
|
||||
const [asset, setAsset] = useState();
|
||||
useEffect(() => {
|
||||
if (Assets.cache.has(source)) {
|
||||
setAsset(Assets.get(source));
|
||||
}
|
||||
else {
|
||||
Assets.load(source).then(setAsset);
|
||||
}
|
||||
}, [setAsset, source]);
|
||||
return asset;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import {useContext, useEffect} from 'react';
|
||||
|
||||
import ClientContext from '@/context/client.js';
|
||||
|
||||
export default function usePacket(type, fn, dependencies) {
|
||||
const client = useContext(ClientContext);
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
client.addPacketListener(type, fn);
|
||||
return () => {
|
||||
client.removePacketListener(type, fn);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [client, ...dependencies]);
|
||||
}
|
|
@ -14,7 +14,9 @@ class WorkerServer extends Server {
|
|||
return ['UNIVERSE', path].join('/');
|
||||
}
|
||||
async readAsset(path) {
|
||||
return fetch(path);
|
||||
return fetch(path).then((response) => (
|
||||
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
|
||||
));
|
||||
}
|
||||
async readData(path) {
|
||||
const data = await get(this.constructor.qualify(path));
|
||||
|
|
|
@ -2,9 +2,8 @@ import {Container} from '@pixi/react';
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import {useEcs} from '@/context/ecs.js';
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import usePacket from '@/hooks/use-packet.js';
|
||||
|
||||
import Entities from './entities.jsx';
|
||||
import TargetingGhost from './targeting-ghost.jsx';
|
||||
|
@ -25,14 +24,11 @@ export default function EcsComponent() {
|
|||
});
|
||||
}
|
||||
}, [ecs, mainEntity]);
|
||||
usePacket('Tick', (payload) => {
|
||||
if (0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
}
|
||||
useEcsTick((payload) => {
|
||||
if (
|
||||
mainEntity
|
||||
&& payload.ecs[mainEntity]
|
||||
&& (payload.ecs[mainEntity].Inventory || payload.ecs[mainEntity].Wielder)
|
||||
&& payload[mainEntity]
|
||||
&& (payload[mainEntity].Inventory || payload[mainEntity].Wielder)
|
||||
) {
|
||||
ecs.get(mainEntity)
|
||||
.Wielder.activeItem()
|
||||
|
@ -41,8 +37,8 @@ export default function EcsComponent() {
|
|||
});
|
||||
}
|
||||
const updatedEntities = {...entities};
|
||||
for (const id in payload.ecs) {
|
||||
const update = payload.ecs[id];
|
||||
for (const id in payload) {
|
||||
const update = payload[id];
|
||||
if (false === update) {
|
||||
delete updatedEntities[id];
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {Container} from '@pixi/react';
|
||||
|
||||
import Entity from './entity.jsx';
|
||||
|
||||
export default function Entities({entities}) {
|
||||
|
@ -10,5 +12,11 @@ export default function Entities({entities}) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
return <>{renderables}</>;
|
||||
return (
|
||||
<Container
|
||||
sortableChildren
|
||||
>
|
||||
{renderables}
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -35,7 +35,9 @@ function Entities({entity}) {
|
|||
return false;
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Container
|
||||
zIndex={entity.Position?.y}
|
||||
>
|
||||
{entity.Sprite && (
|
||||
<Sprite
|
||||
entity={entity}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {BaseTexture} from '@pixi/core';
|
|||
import {createElement, useContext} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import AssetsContext from '@/context/assets.js';
|
||||
import ClientContext from '@/context/client.js';
|
||||
import DebugContext from '@/context/debug.js';
|
||||
import EcsContext from '@/context/ecs.js';
|
||||
|
@ -16,7 +17,7 @@ import styles from './pixi.module.css';
|
|||
|
||||
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST;
|
||||
|
||||
const Contexts = [ClientContext, DebugContext, EcsContext, MainEntityContext];
|
||||
const Contexts = [AssetsContext, ClientContext, DebugContext, EcsContext, MainEntityContext];
|
||||
|
||||
const ContextBridge = ({children, render}) => {
|
||||
const contexts = Contexts.map(useContext);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Sprite as PixiSprite} from '@pixi/react';
|
||||
|
||||
import useAsset from '@/hooks/use-asset.js';
|
||||
import {useAsset} from '@/context/assets.js';
|
||||
|
||||
export default function Sprite({entity}) {
|
||||
const asset = useAsset(entity.Sprite.source);
|
||||
|
|
|
@ -2,7 +2,7 @@ import {PixiComponent} from '@pixi/react';
|
|||
import '@pixi/spritesheet'; // NECESSARY!
|
||||
import {CompositeTilemap} from '@pixi/tilemap';
|
||||
|
||||
import useAsset from '@/hooks/use-asset.js';
|
||||
import {useAsset} from '@/context/assets.js';
|
||||
|
||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||
create: () => new CompositeTilemap(),
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import {useContext, useEffect, useState} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import addKeyListener from '@/add-key-listener.js';
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import ClientContext from '@/context/client.js';
|
||||
import {useClient, usePacket} from '@/context/client.js';
|
||||
import {useDebug} from '@/context/debug.js';
|
||||
import {useEcs} from '@/context/ecs.js';
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import usePacket from '@/hooks/use-packet.js';
|
||||
|
||||
import Disconnected from './disconnected.jsx';
|
||||
import Dom from './dom.jsx';
|
||||
|
@ -22,7 +21,7 @@ function emptySlots() {
|
|||
|
||||
export default function Ui({disconnected}) {
|
||||
// Key input.
|
||||
const client = useContext(ClientContext);
|
||||
const client = useClient();
|
||||
const [mainEntity, setMainEntity] = useMainEntity();
|
||||
const [debug, setDebug] = useDebug();
|
||||
const [ecs] = useEcs();
|
||||
|
@ -150,15 +149,20 @@ export default function Ui({disconnected}) {
|
|||
}
|
||||
});
|
||||
}, [client, debug, setDebug]);
|
||||
usePacket('Tick', (payload) => {
|
||||
usePacket('Tick', async (payload, client) => {
|
||||
if (0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
}
|
||||
ecs.apply(payload.ecs);
|
||||
await ecs.apply(payload.ecs);
|
||||
for (const listener of client.listeners[':Ecs'] ?? []) {
|
||||
listener(payload.ecs);
|
||||
}
|
||||
}, [hotbarSlots, mainEntity, setMainEntity]);
|
||||
useEcsTick((payload) => {
|
||||
let localMainEntity = mainEntity;
|
||||
for (const id in payload.ecs) {
|
||||
for (const id in payload) {
|
||||
const entity = ecs.get(id);
|
||||
const update = payload.ecs[id];
|
||||
const update = payload[id];
|
||||
if (update.Sound?.play) {
|
||||
for (const sound of update.Sound.play) {
|
||||
(new Audio(sound)).play();
|
||||
|
|
|
@ -2,6 +2,7 @@ import {json} from "@remix-run/node";
|
|||
import {useEffect, useState} from 'react';
|
||||
import {useOutletContext, useParams} from 'react-router-dom';
|
||||
|
||||
import AssetsContext from '@/context/assets.js';
|
||||
import ClientContext from '@/context/client.js';
|
||||
import DebugContext from '@/context/debug.js';
|
||||
import EcsContext from '@/context/ecs.js';
|
||||
|
@ -12,6 +13,31 @@ import Systems from '@/ecs-systems/index.js';
|
|||
import Ui from '@/react-components/ui.jsx';
|
||||
import {juggleSession} from '@/session.server';
|
||||
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
export const cache = new LRUCache({
|
||||
max: 128,
|
||||
});
|
||||
|
||||
class ClientEcs extends Ecs {
|
||||
readAsset(uri) {
|
||||
if (!cache.has(uri)) {
|
||||
let promise, resolve, reject;
|
||||
promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
cache.set(uri, promise);
|
||||
fetch(new URL(uri, window.location.origin))
|
||||
.then(async (response) => {
|
||||
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
return cache.get(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loader({request}) {
|
||||
await juggleSession(request);
|
||||
return json({});
|
||||
|
@ -19,10 +45,11 @@ export async function loader({request}) {
|
|||
|
||||
export default function PlaySpecific() {
|
||||
const Client = useOutletContext();
|
||||
const assetsTuple = useState({});
|
||||
const [client, setClient] = useState();
|
||||
const mainEntityTuple = useState();
|
||||
const debugTuple = useState(false);
|
||||
const ecsTuple = useState(new Ecs({Components, Systems}));
|
||||
const ecsTuple = useState(new ClientEcs({Components, Systems}));
|
||||
const [disconnected, setDisconnected] = useState(false);
|
||||
const params = useParams();
|
||||
const [type, url] = params['*'].split('/');
|
||||
|
@ -96,7 +123,9 @@ export default function PlaySpecific() {
|
|||
<MainEntityContext.Provider value={mainEntityTuple}>
|
||||
<EcsContext.Provider value={ecsTuple}>
|
||||
<DebugContext.Provider value={debugTuple}>
|
||||
<AssetsContext.Provider value={assetsTuple}>
|
||||
<Ui disconnected={disconnected} />
|
||||
</AssetsContext.Provider>
|
||||
</DebugContext.Provider>
|
||||
</EcsContext.Provider>
|
||||
</MainEntityContext.Provider>
|
||||
|
|
|
@ -69,16 +69,18 @@ export default class Script {
|
|||
// }
|
||||
// }
|
||||
|
||||
static async fromCode(code, context = {}) {
|
||||
let ast;
|
||||
if (cache.has(code)) {
|
||||
ast = cache.get(code);
|
||||
evaluateSync() {
|
||||
this.sandbox.reset();
|
||||
const {value: {value}} = this.sandbox.step();
|
||||
return value;
|
||||
}
|
||||
else {
|
||||
cache.set(code, ast = await this.parse(code));
|
||||
|
||||
static async fromCode(code, context = {}) {
|
||||
if (!cache.has(code)) {
|
||||
cache.set(code, this.parse(code));
|
||||
}
|
||||
return new this(
|
||||
new Sandbox(ast, this.createContext(context)),
|
||||
new Sandbox(await cache.get(code), this.createContext(context)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,15 @@ class SocketServer extends Server {
|
|||
static qualify(path) {
|
||||
return join(import.meta.dirname, 'data', 'remote', 'UNIVERSE', path);
|
||||
}
|
||||
async readAsset(path) {
|
||||
const url = new URL(path, 'https://localhost:3000')
|
||||
if ('production' === process.env.NODE_ENV) {
|
||||
url.protocol = 'http:';
|
||||
}
|
||||
return fetch(url.href).then((response) => (
|
||||
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
|
||||
));
|
||||
}
|
||||
async readData(path) {
|
||||
const qualified = this.constructor.qualify(path);
|
||||
await this.ensurePath(dirname(qualified));
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "remix vite:build",
|
||||
"dev": "node ./server.js",
|
||||
"dev": "NODE_OPTIONS=--use-openssl-ca node ./server.js",
|
||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"start": "cross-env NODE_ENV=production node ./server.js",
|
||||
"start": "cross-env NODE_ENV=production NODE_OPTIONS=--use-openssl-ca npm run dev",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"test": "vitest app"
|
||||
|
|
13
public/assets/tomato-plant/grow.js
Normal file
13
public/assets/tomato-plant/grow.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const {Sprite} = ecs.get(plant.entity);
|
||||
|
||||
if (plant.stage < 3) {
|
||||
plant.stage += 1
|
||||
}
|
||||
if (4 === plant.stage) {
|
||||
plant.stage = 3
|
||||
}
|
||||
if (3 !== plant.stage) {
|
||||
plant.growth = 0
|
||||
}
|
||||
|
||||
Sprite.animation = ['stage', plant.stage].join('/')
|
1
public/assets/tomato-plant/may-grow.js
Normal file
1
public/assets/tomato-plant/may-grow.js
Normal file
|
@ -0,0 +1 @@
|
|||
3 !== plant.stage
|
1
public/assets/tomato-plant/tomato-plant.json
Normal file
1
public/assets/tomato-plant/tomato-plant.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"animations":{"stage/0":["tomato-plant/tomato-plant/0"],"stage/1":["tomato-plant/tomato-plant/1"],"stage/2":["tomato-plant/tomato-plant/2"],"stage/3":["tomato-plant/tomato-plant/3"],"stage/4":["tomato-plant/tomato-plant/4"]},"frames":{"tomato-plant/tomato-plant/0":{"frame":{"x":0,"y":0,"w":16,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":16,"h":32},"sourceSize":{"w":16,"h":32}},"tomato-plant/tomato-plant/1":{"frame":{"x":16,"y":0,"w":16,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":16,"h":32},"sourceSize":{"w":16,"h":32}},"tomato-plant/tomato-plant/2":{"frame":{"x":32,"y":0,"w":16,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":16,"h":32},"sourceSize":{"w":16,"h":32}},"tomato-plant/tomato-plant/3":{"frame":{"x":48,"y":0,"w":16,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":16,"h":32},"sourceSize":{"w":16,"h":32}},"tomato-plant/tomato-plant/4":{"frame":{"x":64,"y":0,"w":16,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":16,"h":32},"sourceSize":{"w":16,"h":32}}},"meta":{"format":"RGBA8888","image":"./tomato-plant.png","scale":1,"size":{"w":80,"h":32}}}
|
BIN
public/assets/tomato-plant/tomato-plant.png
Normal file
BIN
public/assets/tomato-plant/tomato-plant.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Loading…
Reference in New Issue
Block a user