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
|
// React
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
files: ['**/*.{jsx,tsx}'],
|
||||||
plugins: ['react', 'jsx-a11y'],
|
plugins: ['react', 'jsx-a11y'],
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
|
|
|
@ -248,6 +248,11 @@ export default class Sandbox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.generator = undefined;
|
||||||
|
this.compile();
|
||||||
|
}
|
||||||
|
|
||||||
run(ops = 1000) {
|
run(ops = 1000) {
|
||||||
let result;
|
let result;
|
||||||
for (let i = 0; i < ops; ++i) {
|
for (let i = 0; i < ops; ++i) {
|
||||||
|
@ -265,8 +270,7 @@ export default class Sandbox {
|
||||||
}
|
}
|
||||||
const result = this.generator.next();
|
const result = this.generator.next();
|
||||||
if (result.done) {
|
if (result.done) {
|
||||||
this.generator = undefined;
|
this.reset();
|
||||||
this.compile();
|
|
||||||
}
|
}
|
||||||
return result;
|
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 {createContext, useContext} from 'react';
|
||||||
|
|
||||||
|
import {usePacket} from './client.js';
|
||||||
|
|
||||||
const context = createContext();
|
const context = createContext();
|
||||||
|
|
||||||
export default context;
|
export default context;
|
||||||
|
@ -7,3 +9,8 @@ export default context;
|
||||||
export function useEcs() {
|
export function useEcs() {
|
||||||
return useContext(context);
|
return useContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useEcsTick(fn, dependencies) {
|
||||||
|
const ecs = useEcs();
|
||||||
|
usePacket(':Ecs', fn, [ecs, ...dependencies]);
|
||||||
|
}
|
|
@ -43,56 +43,60 @@ export default class Inventory extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const Instance = super.instanceFromSchema();
|
const Instance = super.instanceFromSchema();
|
||||||
const Component = this;
|
const Component = this;
|
||||||
Instance.prototype.item = async function (slot) {
|
return class InventoryInstance extends Instance {
|
||||||
const {slots} = this;
|
async item(slot) {
|
||||||
if (!(slot in slots)) {
|
const {slots} = this;
|
||||||
return undefined;
|
if (!(slot in slots)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
const instance = this;
|
||||||
|
const proxy = new Proxy(item, {
|
||||||
|
set(target, property, value) {
|
||||||
|
slots[slot][property] = value;
|
||||||
|
if ('qty' === property && value <= 0) {
|
||||||
|
Component.markChange(instance.entity, 'slotChange', {[slot]: false});
|
||||||
|
delete slots[slot];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Component.markChange(instance.entity, 'slotChange', {[slot]: {[property]: value}});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return proxy;
|
||||||
}
|
}
|
||||||
const json = await (
|
swapSlots(l, r) {
|
||||||
fetch([slots[slot].source, 'item.json'].join('/'))
|
const {slots} = this;
|
||||||
.then((response) => (response.ok ? response.json() : {}))
|
const tmp = slots[l];
|
||||||
);
|
const change = {};
|
||||||
const item = {
|
if (slots[r]) {
|
||||||
...slots[slot],
|
change[l] = slots[l] = slots[r];
|
||||||
...json,
|
}
|
||||||
};
|
else {
|
||||||
const instance = this;
|
change[l] = false;
|
||||||
const proxy = new Proxy(item, {
|
delete slots[l];
|
||||||
set(target, property, value) {
|
}
|
||||||
slots[slot][property] = value;
|
if (tmp) {
|
||||||
if ('qty' === property && value <= 0) {
|
change[r] = slots[r] = tmp;
|
||||||
Component.markChange(instance.entity, 'slotChange', {[slot]: false});
|
}
|
||||||
delete slots[slot];
|
else {
|
||||||
}
|
change[r] = false;
|
||||||
else {
|
delete slots[r];
|
||||||
Component.markChange(instance.entity, 'slotChange', {[slot]: {[property]: value}});
|
}
|
||||||
}
|
Component.markChange(this.entity, 'slotChange', change);
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return proxy;
|
|
||||||
};
|
|
||||||
Instance.prototype.swapSlots = function(l, r) {
|
|
||||||
const {slots} = this;
|
|
||||||
const tmp = slots[l];
|
|
||||||
const change = {};
|
|
||||||
if (slots[r]) {
|
|
||||||
change[l] = slots[l] = slots[r];
|
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
change[l] = false;
|
|
||||||
delete slots[l];
|
|
||||||
}
|
|
||||||
if (tmp) {
|
|
||||||
change[r] = slots[r] = tmp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
change[r] = false;
|
|
||||||
delete slots[r];
|
|
||||||
}
|
|
||||||
Component.markChange(this.entity, 'slotChange', change);
|
|
||||||
};
|
|
||||||
return Instance;
|
|
||||||
}
|
}
|
||||||
static properties = {
|
static properties = {
|
||||||
slots: {
|
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,34 +4,32 @@ export default class Ticking extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const Instance = super.instanceFromSchema();
|
const Instance = super.instanceFromSchema();
|
||||||
|
|
||||||
Instance.prototype.$$finished = [];
|
return class TickingInstance extends Instance {
|
||||||
Instance.prototype.$$tickingPromises = [];
|
|
||||||
Instance.prototype.addTickingPromise = function(tickingPromise) {
|
$$finished = [];
|
||||||
this.$$tickingPromises.push(tickingPromise);
|
$$tickingPromises = [];
|
||||||
tickingPromise.then(() => {
|
|
||||||
this.$$finished.push(tickingPromise);
|
addTickingPromise(tickingPromise) {
|
||||||
});
|
this.$$tickingPromises.push(tickingPromise);
|
||||||
|
tickingPromise.then(() => {
|
||||||
|
this.$$finished.push(tickingPromise);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(elapsed) {
|
||||||
|
for (const tickingPromise of this.$$finished) {
|
||||||
|
this.$$tickingPromises.splice(
|
||||||
|
this.$$tickingPromises.indexOf(tickingPromise),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.$$finished = [];
|
||||||
|
for (const tickingPromise of this.$$tickingPromises) {
|
||||||
|
tickingPromise.tick(elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Instance.prototype.tick = function(elapsed) {
|
|
||||||
for (const tickingPromise of this.$$finished) {
|
|
||||||
this.$$tickingPromises.splice(
|
|
||||||
this.$$tickingPromises.indexOf(tickingPromise),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.$$finished = [];
|
|
||||||
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 = {
|
static properties = {
|
||||||
isTicking: {defaultValue: 1, type: 'uint8'},
|
isTicking: {defaultValue: 1, type: 'uint8'},
|
||||||
|
|
|
@ -37,53 +37,54 @@ export default class TileLayers extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const Instance = super.instanceFromSchema();
|
const Instance = super.instanceFromSchema();
|
||||||
const Component = this;
|
const Component = this;
|
||||||
Instance.prototype.layer = function (index) {
|
return class TileLayersInstance extends Instance {
|
||||||
const {layers} = this;
|
layer(index) {
|
||||||
if (!(index in layers)) {
|
const {layers} = this;
|
||||||
return undefined;
|
if (!(index in layers)) {
|
||||||
}
|
return undefined;
|
||||||
const instance = this;
|
|
||||||
class LayerProxy {
|
|
||||||
constructor(layer) {
|
|
||||||
this.layer = layer;
|
|
||||||
}
|
}
|
||||||
get area() {
|
const instance = this;
|
||||||
return this.layer.area;
|
class LayerProxy {
|
||||||
}
|
constructor(layer) {
|
||||||
get source() {
|
this.layer = layer;
|
||||||
return this.layer.source;
|
}
|
||||||
}
|
get area() {
|
||||||
stamp(at, data) {
|
return this.layer.area;
|
||||||
const changes = {};
|
}
|
||||||
for (const row in data) {
|
get source() {
|
||||||
const columns = data[row];
|
return this.layer.source;
|
||||||
for (const column in columns) {
|
}
|
||||||
const tile = columns[column];
|
stamp(at, data) {
|
||||||
const x = at.x + parseInt(column);
|
const changes = {};
|
||||||
const y = at.y + parseInt(row);
|
for (const row in data) {
|
||||||
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
const columns = data[row];
|
||||||
continue;
|
for (const column in columns) {
|
||||||
|
const tile = columns[column];
|
||||||
|
const x = at.x + parseInt(column);
|
||||||
|
const y = at.y + parseInt(row);
|
||||||
|
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const calculated = y * this.layer.area.x + x;
|
||||||
|
this.layer.data[calculated] = tile;
|
||||||
|
changes[calculated] = tile;
|
||||||
}
|
}
|
||||||
const calculated = y * this.layer.area.x + x;
|
|
||||||
this.layer.data[calculated] = tile;
|
|
||||||
changes[calculated] = tile;
|
|
||||||
}
|
}
|
||||||
|
Component.markChange(instance.entity, 'layerChange', {[index]: changes});
|
||||||
}
|
}
|
||||||
Component.markChange(instance.entity, 'layerChange', {[index]: changes});
|
tile({x, y}) {
|
||||||
}
|
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
||||||
tile({x, y}) {
|
return undefined;
|
||||||
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
}
|
||||||
return undefined;
|
return this.layer.data[y * this.layer.area.x + x];
|
||||||
|
}
|
||||||
|
get tileSize() {
|
||||||
|
return this.layer.tileSize;
|
||||||
}
|
}
|
||||||
return this.layer.data[y * this.layer.area.x + x];
|
|
||||||
}
|
|
||||||
get tileSize() {
|
|
||||||
return this.layer.tileSize;
|
|
||||||
}
|
}
|
||||||
|
return new LayerProxy(layers[index]);
|
||||||
}
|
}
|
||||||
return new LayerProxy(layers[index]);
|
}
|
||||||
};
|
|
||||||
return Instance;
|
|
||||||
}
|
}
|
||||||
static properties = {
|
static properties = {
|
||||||
layers: {
|
layers: {
|
||||||
|
|
|
@ -1,69 +1,91 @@
|
||||||
import Component from '@/ecs/component.js';
|
import Component from '@/ecs/component.js';
|
||||||
|
import Script from '@/util/script.js';
|
||||||
|
|
||||||
export default class Wielder extends Component {
|
export default class Wielder extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
|
const {ecs} = this;
|
||||||
const Instance = super.instanceFromSchema();
|
const Instance = super.instanceFromSchema();
|
||||||
const Component = this;
|
return class WielderInstance extends Instance {
|
||||||
Instance.prototype.activeItem = async function () {
|
async activeItem() {
|
||||||
const {Inventory, Wielder} = Component.ecs.get(this.entity);
|
const {Inventory, Wielder} = ecs.get(this.entity);
|
||||||
return Inventory.item(Wielder.activeSlot + 1);
|
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);
|
|
||||||
let startX = position.x;
|
|
||||||
let startY = position.y;
|
|
||||||
switch (direction) {
|
|
||||||
case 0:
|
|
||||||
startX += projection.distance[1];
|
|
||||||
startY -= projection.distance[0];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
startX += projection.distance[0];
|
|
||||||
startY += projection.distance[1];
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
startX -= projection.distance[1];
|
|
||||||
startY += projection.distance[0];
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
startX -= projection.distance[0];
|
|
||||||
startY -= projection.distance[1];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
const projected = [];
|
project(position, projection) {
|
||||||
for (const row in projection.grid) {
|
const {TileLayers: {layers: [layer]}} = ecs.get(1);
|
||||||
const columns = projection.grid[row];
|
const {Direction: {direction}} = ecs.get(this.entity);
|
||||||
for (const column in columns) {
|
let startX = position.x;
|
||||||
const targeted = projection.grid[row][column];
|
let startY = position.y;
|
||||||
if (targeted) {
|
switch (direction) {
|
||||||
let axe;
|
case 0:
|
||||||
switch (direction) {
|
startX += projection.distance[1];
|
||||||
case 0:
|
startY -= projection.distance[0];
|
||||||
axe = [column, row];
|
break;
|
||||||
break;
|
case 1:
|
||||||
case 1:
|
startX += projection.distance[0];
|
||||||
axe = [-row, column];
|
startY += projection.distance[1];
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
axe = [-column, -row];
|
startX -= projection.distance[1];
|
||||||
break;
|
startY += projection.distance[0];
|
||||||
case 3:
|
break;
|
||||||
axe = [row, -column];
|
case 3:
|
||||||
break;
|
startX -= projection.distance[0];
|
||||||
|
startY -= projection.distance[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const projected = [];
|
||||||
|
for (const row in projection.grid) {
|
||||||
|
const columns = projection.grid[row];
|
||||||
|
for (const column in columns) {
|
||||||
|
const targeted = projection.grid[row][column];
|
||||||
|
if (targeted) {
|
||||||
|
let axe;
|
||||||
|
switch (direction) {
|
||||||
|
case 0:
|
||||||
|
axe = [column, row];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
axe = [-row, column];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
axe = [-column, -row];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
axe = [row, -column];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const x = startX + parseInt(axe[0]);
|
||||||
|
const y = startY + parseInt(axe[1]);
|
||||||
|
if (x < 0 || y < 0 || x >= layer.area.x || y >= layer.area.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
projected.push({x, y});
|
||||||
}
|
}
|
||||||
const x = startX + parseInt(axe[0]);
|
|
||||||
const y = startY + parseInt(axe[1]);
|
|
||||||
if (x < 0 || y < 0 || x >= layer.area.x || y >= layer.area.y) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
projected.push({x, y});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return projected;
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return projected;
|
|
||||||
}
|
}
|
||||||
return Instance;
|
|
||||||
}
|
}
|
||||||
static properties = {
|
static properties = {
|
||||||
activeSlot: {type: 'uint16'},
|
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) {
|
tick(elapsed) {
|
||||||
for (const {Sprite} of this.select('default')) {
|
for (const {Sprite} of this.select('default')) {
|
||||||
|
if (0 === Sprite.speed) {
|
||||||
|
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;
|
||||||
|
|
|
@ -28,22 +28,20 @@ export default class Component {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
create(entityId, values) {
|
async create(entityId, values) {
|
||||||
this.createMany([[entityId, values]]);
|
this.createMany([[entityId, values]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
createMany(entries) {
|
async createMany(entries) {
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
const allocated = this.allocateMany(entries.length);
|
const allocated = this.allocateMany(entries.length);
|
||||||
const {properties} = this.constructor.schema.specification;
|
const {properties} = this.constructor.schema.specification;
|
||||||
const keys = Object.keys(properties);
|
const keys = Object.keys(properties);
|
||||||
|
const promises = [];
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
const [entityId, values = {}] = entries[i];
|
const [entityId, values = {}] = entries[i];
|
||||||
this.map[entityId] = allocated[i];
|
this.map[entityId] = allocated[i];
|
||||||
this.data[allocated[i]].entity = entityId;
|
this.data[allocated[i]].entity = entityId;
|
||||||
if (false === values) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let k = 0; k < keys.length; ++k) {
|
for (let k = 0; k < keys.length; ++k) {
|
||||||
const j = keys[k];
|
const j = keys[k];
|
||||||
const {defaultValue} = properties[j];
|
const {defaultValue} = properties[j];
|
||||||
|
@ -54,7 +52,9 @@ export default class Component {
|
||||||
this.data[allocated[i]][j] = defaultValue;
|
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]];
|
return this.data[this.map[entityId]];
|
||||||
}
|
}
|
||||||
|
|
||||||
insertMany(entities) {
|
async insertMany(entities) {
|
||||||
const creating = [];
|
const creating = [];
|
||||||
for (let i = 0; i < entities.length; i++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
const [entityId, values] = entities[i];
|
const [entityId, values] = entities[i];
|
||||||
|
@ -122,7 +122,7 @@ export default class Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.createMany(creating);
|
await this.createMany(creating);
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
|
@ -170,6 +170,10 @@ export default class Component {
|
||||||
return Instance;
|
return Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async load(instance) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
markChange(entityId, key, value) {
|
markChange(entityId, key, value) {
|
||||||
this.ecs.markChange(entityId, {[this.constructor.componentName]: {[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 creating = [];
|
||||||
const destroying = [];
|
const destroying = [];
|
||||||
const removing = [];
|
const removing = [];
|
||||||
|
@ -63,7 +63,7 @@ export default class Ecs {
|
||||||
this.destroyMany(destroying);
|
this.destroyMany(destroying);
|
||||||
this.insertMany(updating);
|
this.insertMany(updating);
|
||||||
this.removeMany(removing);
|
this.removeMany(removing);
|
||||||
this.createManySpecific(creating);
|
await this.createManySpecific(creating);
|
||||||
}
|
}
|
||||||
|
|
||||||
changed(criteria) {
|
changed(criteria) {
|
||||||
|
@ -75,6 +75,10 @@ export default class Ecs {
|
||||||
next: () => {
|
next: () => {
|
||||||
let result = it.next();
|
let result = it.next();
|
||||||
hasResult: while (!result.done) {
|
hasResult: while (!result.done) {
|
||||||
|
if (false === result.value[1]) {
|
||||||
|
result = it.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (const componentName of criteria) {
|
for (const componentName of criteria) {
|
||||||
if (!(componentName in result.value[1])) {
|
if (!(componentName in result.value[1])) {
|
||||||
result = it.next();
|
result = it.next();
|
||||||
|
@ -91,12 +95,12 @@ export default class Ecs {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
create(components = {}) {
|
async create(components = {}) {
|
||||||
const [entityId] = this.createMany([components]);
|
const [entityId] = await this.createMany([components]);
|
||||||
return entityId;
|
return entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
createMany(componentsList) {
|
async createMany(componentsList) {
|
||||||
const specificsList = [];
|
const specificsList = [];
|
||||||
for (const components of componentsList) {
|
for (const components of componentsList) {
|
||||||
specificsList.push([this.$$caret++, components]);
|
specificsList.push([this.$$caret++, components]);
|
||||||
|
@ -104,7 +108,7 @@ export default class Ecs {
|
||||||
return this.createManySpecific(specificsList);
|
return this.createManySpecific(specificsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
createManySpecific(specificsList) {
|
async createManySpecific(specificsList) {
|
||||||
const entityIds = [];
|
const entityIds = [];
|
||||||
const creating = {};
|
const creating = {};
|
||||||
for (let i = 0; i < specificsList.length; i++) {
|
for (let i = 0; i < specificsList.length; i++) {
|
||||||
|
@ -125,14 +129,16 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
this.markChange(entityId, components);
|
this.markChange(entityId, components);
|
||||||
}
|
}
|
||||||
|
const promises = [];
|
||||||
for (const i in creating) {
|
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);
|
this.reindex(entityIds);
|
||||||
return entityIds;
|
return entityIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
createSpecific(entityId, components) {
|
async createSpecific(entityId, components) {
|
||||||
return this.createManySpecific([[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 componentNames = Object.keys(ecs.Components);
|
||||||
const {entities, systems} = decoder.decode(view.buffer);
|
const {entities, systems} = decoder.decode(view.buffer);
|
||||||
for (const system of systems) {
|
for (const system of systems) {
|
||||||
|
@ -164,7 +170,7 @@ export default class Ecs {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
ecs.$$caret = max + 1;
|
ecs.$$caret = max + 1;
|
||||||
ecs.createManySpecific(specifics);
|
await ecs.createManySpecific(specifics);
|
||||||
return ecs;
|
return ecs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,11 +226,11 @@ export default class Ecs {
|
||||||
return this.$$entities[entityId];
|
return this.$$entities[entityId];
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(entityId, components) {
|
async insert(entityId, components) {
|
||||||
this.insertMany([[entityId, components]]);
|
return this.insertMany([[entityId, components]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
insertMany(entities) {
|
async insertMany(entities) {
|
||||||
const inserting = {};
|
const inserting = {};
|
||||||
const unique = new Set();
|
const unique = new Set();
|
||||||
for (const [entityId, components] of entities) {
|
for (const [entityId, components] of entities) {
|
||||||
|
@ -240,9 +246,11 @@ export default class Ecs {
|
||||||
unique.add(entityId);
|
unique.add(entityId);
|
||||||
this.markChange(entityId, diff);
|
this.markChange(entityId, diff);
|
||||||
}
|
}
|
||||||
|
const promises = [];
|
||||||
for (const componentName in inserting) {
|
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());
|
this.reindex(unique.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,7 +323,7 @@ export default class Ecs {
|
||||||
for (const componentName in removing) {
|
for (const componentName in removing) {
|
||||||
this.Components[componentName].destroyMany(removing[componentName]);
|
this.Components[componentName].destroyMany(removing[componentName]);
|
||||||
}
|
}
|
||||||
this.reindex(unique.values());
|
this.reindex(unique);
|
||||||
}
|
}
|
||||||
|
|
||||||
static serialize(ecs, view) {
|
static serialize(ecs, view) {
|
||||||
|
|
|
@ -23,6 +23,24 @@ const Position = wrapProperties('Position', {
|
||||||
z: {type: 'int32'},
|
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', () => {
|
test('activates and deactivates systems at runtime', () => {
|
||||||
let oneCount = 0;
|
let oneCount = 0;
|
||||||
let twoCount = 0;
|
let twoCount = 0;
|
||||||
|
@ -63,31 +81,31 @@ test('activates and deactivates systems at runtime', () => {
|
||||||
.to.equal(2);
|
.to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creates entities with components', () => {
|
test('creates entities with components', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Position}});
|
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)))
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
.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 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']);
|
ecs.remove(entity, ['Position']);
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}}));
|
.to.deep.equal(JSON.stringify({Empty: {}}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gets entities', () => {
|
test('gets entities', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Position}});
|
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)))
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('destroys entities', () => {
|
test('destroys entities', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Position}});
|
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)))
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||||
expect(ecs.get(entity))
|
expect(ecs.get(entity))
|
||||||
|
@ -101,9 +119,9 @@ test('destroys entities', () => {
|
||||||
.to.throw();
|
.to.throw();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('inserts components into entities', () => {
|
test('inserts components into entities', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Position}});
|
const ecs = new Ecs({Components: {Empty, Position}});
|
||||||
const entity = ecs.create({Empty: {}});
|
const entity = await ecs.create({Empty: {}});
|
||||||
ecs.insert(entity, {Position: {y: 128}});
|
ecs.insert(entity, {Position: {y: 128}});
|
||||||
expect(JSON.stringify(ecs.get(entity)))
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
.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}}));
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ticks systems', () => {
|
test('ticks systems', async () => {
|
||||||
const Momentum = wrapProperties('Momentum', {
|
const Momentum = wrapProperties('Momentum', {
|
||||||
x: {type: 'int32'},
|
x: {type: 'int32'},
|
||||||
y: {type: 'int32'},
|
y: {type: 'int32'},
|
||||||
|
@ -141,7 +159,7 @@ test('ticks systems', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ecs.system('Physics').active = true;
|
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);
|
const position = JSON.stringify(ecs.get(entity).Position);
|
||||||
ecs.tick(1);
|
ecs.tick(1);
|
||||||
expect(JSON.stringify(ecs.get(entity).Position))
|
expect(JSON.stringify(ecs.get(entity).Position))
|
||||||
|
@ -213,8 +231,8 @@ test('schedules entities to be deleted when ticking systems', () => {
|
||||||
.to.be.undefined;
|
.to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('adds components to and remove components from entities when ticking systems', () => {
|
test('adds components to and remove components from entities when ticking systems', async () => {
|
||||||
let addLength, removeLength;
|
let promise;
|
||||||
const ecs = new Ecs({
|
const ecs = new Ecs({
|
||||||
Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
|
Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
|
||||||
Systems: {
|
Systems: {
|
||||||
|
@ -225,10 +243,7 @@ test('adds components to and remove components from entities when ticking system
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
tick() {
|
tick() {
|
||||||
this.insertComponents(1, {Foo: {}});
|
promise = this.insertComponents(1, {Foo: {}});
|
||||||
}
|
|
||||||
finalize() {
|
|
||||||
addLength = Array.from(this.select('default')).length;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RemoveComponent: class extends System {
|
RemoveComponent: class extends System {
|
||||||
|
@ -240,40 +255,38 @@ test('adds components to and remove components from entities when ticking system
|
||||||
tick() {
|
tick() {
|
||||||
this.removeComponents(1, ['Foo']);
|
this.removeComponents(1, ['Foo']);
|
||||||
}
|
}
|
||||||
finalize() {
|
|
||||||
removeLength = Array.from(this.select('default')).length;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ecs.system('AddComponent').active = true;
|
ecs.system('AddComponent').active = true;
|
||||||
ecs.create();
|
ecs.create();
|
||||||
ecs.tick(1);
|
ecs.tick(1);
|
||||||
expect(addLength)
|
await promise;
|
||||||
|
expect(Array.from(ecs.system('AddComponent').select('default')).length)
|
||||||
.to.equal(1);
|
.to.equal(1);
|
||||||
expect(ecs.get(1).Foo)
|
expect(ecs.get(1).Foo)
|
||||||
.to.not.be.undefined;
|
.to.not.be.undefined;
|
||||||
ecs.system('AddComponent').active = false;
|
ecs.system('AddComponent').active = false;
|
||||||
ecs.system('RemoveComponent').active = true;
|
ecs.system('RemoveComponent').active = true;
|
||||||
ecs.tick(1);
|
ecs.tick(1);
|
||||||
expect(removeLength)
|
expect(Array.from(ecs.system('RemoveComponent').select('default')).length)
|
||||||
.to.equal(0);
|
.to.equal(0);
|
||||||
expect(ecs.get(1).Foo)
|
expect(ecs.get(1).Foo)
|
||||||
.to.be.undefined;
|
.to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generates coalesced diffs for entity creation', () => {
|
test('generates diffs for entity creation', async () => {
|
||||||
const ecs = new Ecs();
|
const ecs = new Ecs();
|
||||||
let entity;
|
let entity;
|
||||||
entity = ecs.create();
|
entity = await ecs.create();
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
.to.deep.equal({[entity]: {}});
|
.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}});
|
const ecs = new Ecs({Components: {Position}});
|
||||||
let entity;
|
let entity;
|
||||||
entity = ecs.create();
|
entity = await ecs.create();
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
ecs.insert(entity, {Position: {x: 64}});
|
ecs.insert(entity, {Position: {x: 64}});
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
|
@ -286,10 +299,10 @@ test('generates diffs for adding and removing components', () => {
|
||||||
.to.deep.equal({[entity]: {Position: false}});
|
.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}});
|
const ecs = new Ecs({Components: {Empty}});
|
||||||
let entity;
|
let entity;
|
||||||
entity = ecs.create({Empty: {}});
|
entity = await ecs.create({Empty: {}});
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
.to.deep.equal({[entity]: {Empty: {}}});
|
.to.deep.equal({[entity]: {Empty: {}}});
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
|
@ -298,10 +311,10 @@ test('generates diffs for empty components', () => {
|
||||||
.to.deep.equal({[entity]: {Empty: false}});
|
.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}});
|
const ecs = new Ecs({Components: {Position}});
|
||||||
let entity;
|
let entity;
|
||||||
entity = ecs.create({Position: {}});
|
entity = await ecs.create({Position: {}});
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
ecs.get(entity).Position.x = 128;
|
ecs.get(entity).Position.x = 128;
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
|
@ -311,10 +324,10 @@ test('generates diffs for entity mutations', () => {
|
||||||
.to.deep.equal({});
|
.to.deep.equal({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generates coalesced diffs for components', () => {
|
test('generates coalesced diffs for components', async () => {
|
||||||
const ecs = new Ecs({Components: {Position}});
|
const ecs = new Ecs({Components: {Position}});
|
||||||
let entity;
|
let entity;
|
||||||
entity = ecs.create({Position});
|
entity = await ecs.create({Position});
|
||||||
ecs.remove(entity, ['Position']);
|
ecs.remove(entity, ['Position']);
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
.to.deep.equal({[entity]: {Position: false}});
|
.to.deep.equal({[entity]: {Position: false}});
|
||||||
|
@ -323,10 +336,10 @@ test('generates coalesced diffs for components', () => {
|
||||||
.to.deep.equal({[entity]: {Position: {}}});
|
.to.deep.equal({[entity]: {Position: {}}});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generates coalesced diffs for mutations', () => {
|
test('generates coalesced diffs for mutations', async () => {
|
||||||
const ecs = new Ecs({Components: {Position}});
|
const ecs = new Ecs({Components: {Position}});
|
||||||
let entity;
|
let entity;
|
||||||
entity = ecs.create({Position});
|
entity = await ecs.create({Position});
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
ecs.get(entity).Position.x = 128;
|
ecs.get(entity).Position.x = 128;
|
||||||
ecs.get(entity).Position.x = 256;
|
ecs.get(entity).Position.x = 256;
|
||||||
|
@ -335,10 +348,10 @@ test('generates coalesced diffs for mutations', () => {
|
||||||
.to.deep.equal({[entity]: {Position: {x: 512}}});
|
.to.deep.equal({[entity]: {Position: {x: 512}}});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generates diffs for deletions', () => {
|
test('generates diffs for deletions', async () => {
|
||||||
const ecs = new Ecs();
|
const ecs = new Ecs();
|
||||||
let entity;
|
let entity;
|
||||||
entity = ecs.create();
|
entity = await ecs.create();
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
ecs.destroy(entity);
|
ecs.destroy(entity);
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
|
@ -391,7 +404,7 @@ test('calculates entity size', () => {
|
||||||
.to.equal(30);
|
.to.equal(30);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('serializes and deserializes', () => {
|
test('serializes and deserializes', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||||
|
@ -404,7 +417,7 @@ test('serializes and deserializes', () => {
|
||||||
systems: [],
|
systems: [],
|
||||||
});
|
});
|
||||||
const view = Ecs.serialize(ecs);
|
const view = Ecs.serialize(ecs);
|
||||||
const deserialized = Ecs.deserialize(
|
const deserialized = await Ecs.deserialize(
|
||||||
new Ecs({Components: {Empty, Name, Position}}),
|
new Ecs({Components: {Empty, Name, Position}}),
|
||||||
view,
|
view,
|
||||||
);
|
);
|
||||||
|
@ -424,12 +437,12 @@ test('serializes and deserializes', () => {
|
||||||
.to.equal('foobar');
|
.to.equal('foobar');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('deserializes from compatible ECS', () => {
|
test('deserializes from compatible ECS', async () => {
|
||||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||||
const view = Ecs.serialize(ecs);
|
const view = Ecs.serialize(ecs);
|
||||||
const deserialized = Ecs.deserialize(
|
const deserialized = await Ecs.deserialize(
|
||||||
new Ecs({Components: {Empty, Name}}),
|
new Ecs({Components: {Empty, Name}}),
|
||||||
view,
|
view,
|
||||||
);
|
);
|
||||||
|
@ -438,3 +451,30 @@ test('deserializes from compatible ECS', () => {
|
||||||
expect(deserialized.get(16).toJSON())
|
expect(deserialized.get(16).toJSON())
|
||||||
.to.deep.equal({Name: {name: 'foobar'}});
|
.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() {}
|
finalize() {}
|
||||||
|
|
||||||
insertComponents(entityId, components) {
|
async insertComponents(entityId, components) {
|
||||||
this.ecs.insert(entityId, components);
|
return this.ecs.insert(entityId, components);
|
||||||
}
|
}
|
||||||
|
|
||||||
insertManyComponents(components) {
|
async insertManyComponents(components) {
|
||||||
this.ecs.insertMany(components);
|
return this.ecs.insertMany(components);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get priority() {
|
static get priority() {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import Ecs from '@/ecs/ecs.js';
|
||||||
import Components from '@/ecs-components/index.js';
|
import Components from '@/ecs-components/index.js';
|
||||||
import Systems from '@/ecs-systems/index.js';
|
import Systems from '@/ecs-systems/index.js';
|
||||||
import {decode, encode} from '@/packets/index.js';
|
import {decode, encode} from '@/packets/index.js';
|
||||||
import Script from '@/util/script.js';
|
|
||||||
|
|
||||||
function join(...parts) {
|
function join(...parts) {
|
||||||
return parts.join('/');
|
return parts.join('/');
|
||||||
|
@ -32,7 +31,12 @@ export default class Engine {
|
||||||
super.transmit(connection, encode(packet));
|
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.server.addPacketListener('Action', (connection, payload) => {
|
||||||
this.incomingActions.push([this.connectedPlayers.get(connection).entity, payload]);
|
this.incomingActions.push([this.connectedPlayers.get(connection).entity, payload]);
|
||||||
});
|
});
|
||||||
|
@ -43,7 +47,7 @@ export default class Engine {
|
||||||
entity,
|
entity,
|
||||||
payload,
|
payload,
|
||||||
] of this.incomingActions) {
|
] of this.incomingActions) {
|
||||||
const {Controlled, Inventory, Ticking, Wielder} = entity;
|
const {Controlled, Inventory, Wielder} = entity;
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case 'changeSlot': {
|
case 'changeSlot': {
|
||||||
if (!Controlled.locked) {
|
if (!Controlled.locked) {
|
||||||
|
@ -66,25 +70,7 @@ export default class Engine {
|
||||||
}
|
}
|
||||||
case 'use': {
|
case 'use': {
|
||||||
if (!Controlled.locked) {
|
if (!Controlled.locked) {
|
||||||
Inventory.item(Wielder.activeSlot + 1).then(async (item) => {
|
Wielder.useActiveItem(payload.value);
|
||||||
if (item) {
|
|
||||||
const code = await(
|
|
||||||
this.server.readAsset([
|
|
||||||
item.source,
|
|
||||||
payload.value ? 'start.js' : 'stop.js',
|
|
||||||
].join('/'))
|
|
||||||
.then((script) => (script.ok ? script.text() : ''))
|
|
||||||
);
|
|
||||||
if (code) {
|
|
||||||
const context = {
|
|
||||||
ecs: this.ecses[entity.Ecs.path],
|
|
||||||
item,
|
|
||||||
wielder: entity,
|
|
||||||
};
|
|
||||||
Ticking.addTickingPromise(Script.tickingPromise(code, context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -99,7 +85,7 @@ export default class Engine {
|
||||||
await this.loadEcs(entityJson.Ecs.path);
|
await this.loadEcs(entityJson.Ecs.path);
|
||||||
}
|
}
|
||||||
const ecs = this.ecses[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.connections.push(connection);
|
||||||
this.connectedPlayers.set(
|
this.connectedPlayers.set(
|
||||||
connection,
|
connection,
|
||||||
|
@ -112,13 +98,13 @@ export default class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
createEcs() {
|
createEcs() {
|
||||||
return new Ecs({Components, Systems});
|
return new this.Ecs({Components, Systems});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createHomestead(id) {
|
async createHomestead(id) {
|
||||||
const ecs = this.createEcs();
|
const ecs = this.createEcs();
|
||||||
const area = {x: 100, y: 60};
|
const area = {x: 100, y: 60};
|
||||||
ecs.create({
|
await ecs.create({
|
||||||
AreaSize: {x: area.x * 16, y: area.y * 16},
|
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||||
Engine: {},
|
Engine: {},
|
||||||
TileLayers: {
|
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 = [
|
const defaultSystems = [
|
||||||
'ResetForces',
|
'ResetForces',
|
||||||
'ApplyControlMovement',
|
'ApplyControlMovement',
|
||||||
'ApplyForces',
|
'ApplyForces',
|
||||||
'ClampPositions',
|
'ClampPositions',
|
||||||
|
'PlantGrowth',
|
||||||
'FollowCamera',
|
'FollowCamera',
|
||||||
'CalculateAabbs',
|
'CalculateAabbs',
|
||||||
'UpdateSpatialHash',
|
'UpdateSpatialHash',
|
||||||
|
@ -217,11 +231,10 @@ export default class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEcs(path) {
|
async loadEcs(path) {
|
||||||
this.ecses[path] = Ecs.deserialize(
|
this.ecses[path] = await this.Ecs.deserialize(
|
||||||
this.createEcs(),
|
this.createEcs(),
|
||||||
await this.server.readData(path),
|
await this.server.readData(path),
|
||||||
);
|
);
|
||||||
this.ecses[path].get(1).Engine.engine = this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPlayer(id) {
|
async loadPlayer(id) {
|
||||||
|
@ -240,7 +253,7 @@ export default class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveEcs(path, ecs) {
|
async saveEcs(path, ecs) {
|
||||||
const view = Ecs.serialize(ecs);
|
const view = this.Ecs.serialize(ecs);
|
||||||
await this.server.writeData(path, view);
|
await this.server.writeData(path, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ test('visibility-based updates', async () => {
|
||||||
await engine.connectPlayer(0, 0);
|
await engine.connectPlayer(0, 0);
|
||||||
const ecs = engine.ecses['homesteads/0'];
|
const ecs = engine.ecses['homesteads/0'];
|
||||||
// Create an entity.
|
// Create an entity.
|
||||||
const entity = ecs.get(ecs.create({
|
const entity = ecs.get(await ecs.create({
|
||||||
Forces: {forceX: 1},
|
Forces: {forceX: 1},
|
||||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
||||||
VisibleAabb: {},
|
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('/');
|
return ['UNIVERSE', path].join('/');
|
||||||
}
|
}
|
||||||
async readAsset(path) {
|
async readAsset(path) {
|
||||||
return fetch(path);
|
return fetch(path).then((response) => (
|
||||||
|
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
async readData(path) {
|
async readData(path) {
|
||||||
const data = await get(this.constructor.qualify(path));
|
const data = await get(this.constructor.qualify(path));
|
||||||
|
|
|
@ -2,9 +2,8 @@ import {Container} from '@pixi/react';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
import {RESOLUTION} from '@/constants.js';
|
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 {useMainEntity} from '@/context/main-entity.js';
|
||||||
import usePacket from '@/hooks/use-packet.js';
|
|
||||||
|
|
||||||
import Entities from './entities.jsx';
|
import Entities from './entities.jsx';
|
||||||
import TargetingGhost from './targeting-ghost.jsx';
|
import TargetingGhost from './targeting-ghost.jsx';
|
||||||
|
@ -25,14 +24,11 @@ export default function EcsComponent() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [ecs, mainEntity]);
|
}, [ecs, mainEntity]);
|
||||||
usePacket('Tick', (payload) => {
|
useEcsTick((payload) => {
|
||||||
if (0 === Object.keys(payload.ecs).length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
mainEntity
|
mainEntity
|
||||||
&& payload.ecs[mainEntity]
|
&& payload[mainEntity]
|
||||||
&& (payload.ecs[mainEntity].Inventory || payload.ecs[mainEntity].Wielder)
|
&& (payload[mainEntity].Inventory || payload[mainEntity].Wielder)
|
||||||
) {
|
) {
|
||||||
ecs.get(mainEntity)
|
ecs.get(mainEntity)
|
||||||
.Wielder.activeItem()
|
.Wielder.activeItem()
|
||||||
|
@ -41,8 +37,8 @@ export default function EcsComponent() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const updatedEntities = {...entities};
|
const updatedEntities = {...entities};
|
||||||
for (const id in payload.ecs) {
|
for (const id in payload) {
|
||||||
const update = payload.ecs[id];
|
const update = payload[id];
|
||||||
if (false === update) {
|
if (false === update) {
|
||||||
delete updatedEntities[id];
|
delete updatedEntities[id];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {Container} from '@pixi/react';
|
||||||
|
|
||||||
import Entity from './entity.jsx';
|
import Entity from './entity.jsx';
|
||||||
|
|
||||||
export default function Entities({entities}) {
|
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 false;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container
|
||||||
|
zIndex={entity.Position?.y}
|
||||||
|
>
|
||||||
{entity.Sprite && (
|
{entity.Sprite && (
|
||||||
<Sprite
|
<Sprite
|
||||||
entity={entity}
|
entity={entity}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {BaseTexture} from '@pixi/core';
|
||||||
import {createElement, useContext} from 'react';
|
import {createElement, useContext} from 'react';
|
||||||
|
|
||||||
import {RESOLUTION} from '@/constants.js';
|
import {RESOLUTION} from '@/constants.js';
|
||||||
|
import AssetsContext from '@/context/assets.js';
|
||||||
import ClientContext from '@/context/client.js';
|
import ClientContext from '@/context/client.js';
|
||||||
import DebugContext from '@/context/debug.js';
|
import DebugContext from '@/context/debug.js';
|
||||||
import EcsContext from '@/context/ecs.js';
|
import EcsContext from '@/context/ecs.js';
|
||||||
|
@ -16,7 +17,7 @@ import styles from './pixi.module.css';
|
||||||
|
|
||||||
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST;
|
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST;
|
||||||
|
|
||||||
const Contexts = [ClientContext, DebugContext, EcsContext, MainEntityContext];
|
const Contexts = [AssetsContext, ClientContext, DebugContext, EcsContext, MainEntityContext];
|
||||||
|
|
||||||
const ContextBridge = ({children, render}) => {
|
const ContextBridge = ({children, render}) => {
|
||||||
const contexts = Contexts.map(useContext);
|
const contexts = Contexts.map(useContext);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {Sprite as PixiSprite} from '@pixi/react';
|
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}) {
|
export default function Sprite({entity}) {
|
||||||
const asset = useAsset(entity.Sprite.source);
|
const asset = useAsset(entity.Sprite.source);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {PixiComponent} from '@pixi/react';
|
||||||
import '@pixi/spritesheet'; // NECESSARY!
|
import '@pixi/spritesheet'; // NECESSARY!
|
||||||
import {CompositeTilemap} from '@pixi/tilemap';
|
import {CompositeTilemap} from '@pixi/tilemap';
|
||||||
|
|
||||||
import useAsset from '@/hooks/use-asset.js';
|
import {useAsset} from '@/context/assets.js';
|
||||||
|
|
||||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||||
create: () => new CompositeTilemap(),
|
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 addKeyListener from '@/add-key-listener.js';
|
||||||
import {RESOLUTION} from '@/constants.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 {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 {useMainEntity} from '@/context/main-entity.js';
|
||||||
import usePacket from '@/hooks/use-packet.js';
|
|
||||||
|
|
||||||
import Disconnected from './disconnected.jsx';
|
import Disconnected from './disconnected.jsx';
|
||||||
import Dom from './dom.jsx';
|
import Dom from './dom.jsx';
|
||||||
|
@ -22,7 +21,7 @@ function emptySlots() {
|
||||||
|
|
||||||
export default function Ui({disconnected}) {
|
export default function Ui({disconnected}) {
|
||||||
// Key input.
|
// Key input.
|
||||||
const client = useContext(ClientContext);
|
const client = useClient();
|
||||||
const [mainEntity, setMainEntity] = useMainEntity();
|
const [mainEntity, setMainEntity] = useMainEntity();
|
||||||
const [debug, setDebug] = useDebug();
|
const [debug, setDebug] = useDebug();
|
||||||
const [ecs] = useEcs();
|
const [ecs] = useEcs();
|
||||||
|
@ -150,15 +149,20 @@ export default function Ui({disconnected}) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [client, debug, setDebug]);
|
}, [client, debug, setDebug]);
|
||||||
usePacket('Tick', (payload) => {
|
usePacket('Tick', async (payload, client) => {
|
||||||
if (0 === Object.keys(payload.ecs).length) {
|
if (0 === Object.keys(payload.ecs).length) {
|
||||||
return;
|
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;
|
let localMainEntity = mainEntity;
|
||||||
for (const id in payload.ecs) {
|
for (const id in payload) {
|
||||||
const entity = ecs.get(id);
|
const entity = ecs.get(id);
|
||||||
const update = payload.ecs[id];
|
const update = payload[id];
|
||||||
if (update.Sound?.play) {
|
if (update.Sound?.play) {
|
||||||
for (const sound of update.Sound.play) {
|
for (const sound of update.Sound.play) {
|
||||||
(new Audio(sound)).play();
|
(new Audio(sound)).play();
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {json} from "@remix-run/node";
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {useOutletContext, useParams} from 'react-router-dom';
|
import {useOutletContext, useParams} from 'react-router-dom';
|
||||||
|
|
||||||
|
import AssetsContext from '@/context/assets.js';
|
||||||
import ClientContext from '@/context/client.js';
|
import ClientContext from '@/context/client.js';
|
||||||
import DebugContext from '@/context/debug.js';
|
import DebugContext from '@/context/debug.js';
|
||||||
import EcsContext from '@/context/ecs.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 Ui from '@/react-components/ui.jsx';
|
||||||
import {juggleSession} from '@/session.server';
|
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}) {
|
export async function loader({request}) {
|
||||||
await juggleSession(request);
|
await juggleSession(request);
|
||||||
return json({});
|
return json({});
|
||||||
|
@ -19,10 +45,11 @@ export async function loader({request}) {
|
||||||
|
|
||||||
export default function PlaySpecific() {
|
export default function PlaySpecific() {
|
||||||
const Client = useOutletContext();
|
const Client = useOutletContext();
|
||||||
|
const assetsTuple = useState({});
|
||||||
const [client, setClient] = useState();
|
const [client, setClient] = useState();
|
||||||
const mainEntityTuple = useState();
|
const mainEntityTuple = useState();
|
||||||
const debugTuple = useState(false);
|
const debugTuple = useState(false);
|
||||||
const ecsTuple = useState(new Ecs({Components, Systems}));
|
const ecsTuple = useState(new ClientEcs({Components, Systems}));
|
||||||
const [disconnected, setDisconnected] = useState(false);
|
const [disconnected, setDisconnected] = useState(false);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [type, url] = params['*'].split('/');
|
const [type, url] = params['*'].split('/');
|
||||||
|
@ -96,7 +123,9 @@ export default function PlaySpecific() {
|
||||||
<MainEntityContext.Provider value={mainEntityTuple}>
|
<MainEntityContext.Provider value={mainEntityTuple}>
|
||||||
<EcsContext.Provider value={ecsTuple}>
|
<EcsContext.Provider value={ecsTuple}>
|
||||||
<DebugContext.Provider value={debugTuple}>
|
<DebugContext.Provider value={debugTuple}>
|
||||||
<Ui disconnected={disconnected} />
|
<AssetsContext.Provider value={assetsTuple}>
|
||||||
|
<Ui disconnected={disconnected} />
|
||||||
|
</AssetsContext.Provider>
|
||||||
</DebugContext.Provider>
|
</DebugContext.Provider>
|
||||||
</EcsContext.Provider>
|
</EcsContext.Provider>
|
||||||
</MainEntityContext.Provider>
|
</MainEntityContext.Provider>
|
||||||
|
|
|
@ -69,16 +69,18 @@ export default class Script {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
evaluateSync() {
|
||||||
|
this.sandbox.reset();
|
||||||
|
const {value: {value}} = this.sandbox.step();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
static async fromCode(code, context = {}) {
|
static async fromCode(code, context = {}) {
|
||||||
let ast;
|
if (!cache.has(code)) {
|
||||||
if (cache.has(code)) {
|
cache.set(code, this.parse(code));
|
||||||
ast = cache.get(code);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cache.set(code, ast = await this.parse(code));
|
|
||||||
}
|
}
|
||||||
return new this(
|
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) {
|
static qualify(path) {
|
||||||
return join(import.meta.dirname, 'data', 'remote', 'UNIVERSE', 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) {
|
async readData(path) {
|
||||||
const qualified = this.constructor.qualify(path);
|
const qualified = this.constructor.qualify(path);
|
||||||
await this.ensurePath(dirname(qualified));
|
await this.ensurePath(dirname(qualified));
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix vite:build",
|
"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 .",
|
"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": "storybook dev -p 6006",
|
||||||
"storybook:build": "storybook build",
|
"storybook:build": "storybook build",
|
||||||
"test": "vitest app"
|
"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