Compare commits
No commits in common. "52dca790f2211ff3a90f3bbb486e57a68a51af66" and "aae9d27b31be8b9576d7e65a780d1f5ffa8591f4" have entirely different histories.
52dca790f2
...
aae9d27b31
|
@ -1,25 +0,0 @@
|
|||
// eslint.config.js
|
||||
import globals from 'globals';
|
||||
import js from '@eslint/js';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
process: false,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true,
|
||||
jsx: true
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// 'no-unused-vars': 'warn',
|
||||
// 'no-undef': 'warn',
|
||||
}
|
||||
}
|
||||
];
|
1411
package-lock.json
generated
1411
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -14,7 +14,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.4.0",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@storybook/addon-essentials": "^8.1.3",
|
||||
"@storybook/addon-interactions": "^8.1.3",
|
||||
"@storybook/addon-links": "^8.1.3",
|
||||
|
@ -24,16 +23,11 @@
|
|||
"@storybook/react-vite": "^8.1.3",
|
||||
"@storybook/test": "^8.1.3",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"eslint": "^9.3.0",
|
||||
"globals": "^15.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^8.1.3",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.6.0"
|
||||
"vite": "^5.2.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"pixi.js": "^8.1.5",
|
||||
"react": "^18.3.1",
|
||||
|
|
10
src/cell.js
10
src/cell.js
|
@ -1,10 +0,0 @@
|
|||
import {Ecs} from './ecs/index.js';
|
||||
|
||||
export default class Cell {
|
||||
constructor() {
|
||||
this.ecs = new Ecs();
|
||||
}
|
||||
tick(elapsed) {
|
||||
this.ecs.tick(elapsed);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
import Packet from '../packet/packet.js';
|
||||
|
||||
export default class Client {
|
||||
constructor() {
|
||||
this.listeners = [];
|
||||
}
|
||||
accept(packed) {
|
||||
const decoded = Packet.accept(packed);
|
||||
accept(data) {
|
||||
for (const i in this.listeners) {
|
||||
this.listeners[i](decoded);
|
||||
this.listeners[i](data);
|
||||
}
|
||||
}
|
||||
addMessageListener(listener) {
|
||||
|
@ -19,7 +16,4 @@ export default class Client {
|
|||
this.listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
send(packet) {
|
||||
this.transmit(Packet.transmit(packet));
|
||||
}
|
||||
}
|
|
@ -2,12 +2,12 @@ import Client from './client.js';
|
|||
|
||||
export default class LocalClient extends Client {
|
||||
async connect() {
|
||||
this.worker = new Worker('/net/server/worker.js', {type: 'module'});
|
||||
this.worker = new Worker('../server/worker.js', {type: 'module'});
|
||||
this.worker.onmessage = (event) => {
|
||||
this.accept(event.data);
|
||||
};
|
||||
}
|
||||
transmit(packed) {
|
||||
this.worker.postMessage(packed);
|
||||
send(message) {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
}
|
|
@ -3,15 +3,15 @@ import Client from './client.js';
|
|||
export default class RemoteClient extends Client {
|
||||
async connect() {
|
||||
this.socket = new WebSocket(`ws://${window.location.host}/ws`);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.onmessage = (event) => {
|
||||
this.accept(event.data);
|
||||
this.accept(JSON.parse(event.data));
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
this.socket.onopen = resolve;
|
||||
});
|
||||
}
|
||||
transmit(packed) {
|
||||
this.socket.send(packed);
|
||||
send(message) {
|
||||
this.socket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '../constants.js';
|
||||
import {RESOLUTION} from '../constants';
|
||||
import styles from './dom.module.css';
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Slot from './slot.jsx';
|
||||
import Slot from './slot';
|
||||
|
||||
import styles from './hotbar.module.css';
|
||||
|
||||
|
|
|
@ -5,12 +5,10 @@ import {
|
|||
import {useContext, useEffect, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '../constants.js';
|
||||
import ClientContext from '../context/client.js';
|
||||
import Entities from './entities.jsx';
|
||||
import ClientContext from '../context/client';
|
||||
import Entities from './entities';
|
||||
import styles from './pixi.module.css';
|
||||
|
||||
import {Ecs} from '../ecs/index.js';
|
||||
|
||||
export default function Pixi() {
|
||||
const client = useContext(ClientContext);
|
||||
const [entities, setEntities] = useState([]);
|
||||
|
@ -23,19 +21,7 @@ export default function Pixi() {
|
|||
break;
|
||||
}
|
||||
case 'tick': {
|
||||
const {buffer, byteLength, byteOffset} = payload.entities;
|
||||
const view = new DataView(buffer, byteOffset, byteLength);
|
||||
const ecs = new Ecs();
|
||||
ecs.decode(view);
|
||||
const entities = [];
|
||||
for (const entity of ecs.entities) {
|
||||
const {Position, Visible} = ecs.get(entity);
|
||||
entities.push({
|
||||
image: Visible.image,
|
||||
position: [Position.x, Position.y],
|
||||
})
|
||||
}
|
||||
setEntities(entities);
|
||||
setEntities(payload.entities);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
import ClientContext from '../context/client.js';
|
||||
import Title from './title.jsx';
|
||||
import Ui from './ui.jsx';
|
||||
import Title from './title';
|
||||
import Ui from './ui';
|
||||
|
||||
export default function Silphius() {
|
||||
const connectionTuple = useState();
|
||||
|
@ -15,10 +15,10 @@ export default function Silphius() {
|
|||
let Client;
|
||||
switch (connectionTuple[0]) {
|
||||
case 'local':
|
||||
({default: Client} = await import('../net/client/local.js'));
|
||||
({default: Client} = await import('../client/local.js'));
|
||||
break;
|
||||
case 'remote':
|
||||
({default: Client} = await import('../net/client/remote.js'));
|
||||
({default: Client} = await import('../client/remote.js'));
|
||||
break;
|
||||
}
|
||||
const client = new Client();
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import {useContext, useEffect} from 'react';
|
||||
|
||||
import addKeyListener from '../add-key-listener.js';
|
||||
import {ACTION_MAP, RESOLUTION} from '../constants.js';
|
||||
import ClientContext from '../context/client.js';
|
||||
import Dom from './dom.jsx';
|
||||
import Pixi from './pixi.jsx';
|
||||
import {RESOLUTION} from '../constants';
|
||||
import ClientContext from '../context/client';
|
||||
import Dom from './dom';
|
||||
import Pixi from './pixi';
|
||||
import styles from './ui.module.css';
|
||||
|
||||
const ratio = RESOLUTION[0] / RESOLUTION[1];
|
||||
|
||||
// Handle input.
|
||||
const ACTION_MAP = {
|
||||
w: 'moveUp',
|
||||
d: 'moveRight',
|
||||
s: 'moveDown',
|
||||
a: 'moveLeft',
|
||||
};
|
||||
const KEY_MAP = {
|
||||
keyDown: 1,
|
||||
keyUp: 0,
|
||||
|
|
|
@ -2,17 +2,3 @@ export const RESOLUTION = [
|
|||
800,
|
||||
450,
|
||||
];
|
||||
|
||||
export const ACTION_MAP = {
|
||||
w: 'moveUp',
|
||||
d: 'moveRight',
|
||||
s: 'moveDown',
|
||||
a: 'moveLeft',
|
||||
};
|
||||
|
||||
export const MOVE_MAP = {
|
||||
'moveUp': 'up',
|
||||
'moveRight': 'right',
|
||||
'moveDown': 'down',
|
||||
'moveLeft': 'left',
|
||||
};
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
export default class Bundle {
|
||||
|
||||
Components = {};
|
||||
|
||||
constructor(Components) {
|
||||
this.Components = this.constructor.Components.reduce(
|
||||
(r, Component, i) => {
|
||||
/* v8 ignore next 3 */
|
||||
if (!Components[i]) {
|
||||
throw new TypeError(`Bundle(): no such component '${Component}'`);
|
||||
}
|
||||
return {...r, [Component]: Components[i]};
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
configure(configuration) {
|
||||
return this.constructor.configure(configuration, this.Components);
|
||||
}
|
||||
|
||||
static configure(configuration, Components) {
|
||||
const result = {};
|
||||
const entries = Object.entries(Components);
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const [component] = entries[i];
|
||||
result[component] = configuration[component];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static maybeNormalize(BundleLike) {
|
||||
const normalized = this.normalize(BundleLike);
|
||||
return normalized.prototype instanceof Bundle ? normalized : undefined;
|
||||
}
|
||||
|
||||
static normalize(BundleLike) {
|
||||
if (Array.isArray(BundleLike)) {
|
||||
return class AdhocBundle extends Bundle {
|
||||
static Components = BundleLike;
|
||||
};
|
||||
}
|
||||
return BundleLike;
|
||||
}
|
||||
|
||||
static select(entity, Components) {
|
||||
const bundle = {};
|
||||
for (const i in Components) {
|
||||
bundle[i] = Components[i].get(entity);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
select(entity) {
|
||||
return this.constructor.select(entity, this.Components);
|
||||
}
|
||||
|
||||
}
|
107
src/ecs/chain.js
107
src/ecs/chain.js
|
@ -1,107 +0,0 @@
|
|||
import Ecs from './ecs.js';
|
||||
|
||||
export default class Chain {
|
||||
|
||||
index = 0;
|
||||
|
||||
proposals = [];
|
||||
|
||||
constructor(init) {
|
||||
this.addProposal({
|
||||
ecs: new Ecs(init),
|
||||
elapsed: 0,
|
||||
index: this.index++,
|
||||
});
|
||||
}
|
||||
|
||||
addProposal(proposal) {
|
||||
this.proposals.push({
|
||||
at: Date.now(),
|
||||
...proposal,
|
||||
});
|
||||
}
|
||||
|
||||
get dirty() {
|
||||
const it = this.proposals[Symbol.iterator]()
|
||||
return {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
let result = it.next();
|
||||
while (!result.done && !result.value?.ecs.$$dirty) {
|
||||
result = it.next();
|
||||
}
|
||||
if (result.done) {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get latest() {
|
||||
return this.proposals[this.proposals.length - 1];
|
||||
}
|
||||
|
||||
mutate(when, mutator) {
|
||||
let walk = this.proposals.length - 1;
|
||||
let index = -1;
|
||||
while (walk > 0) {
|
||||
if (this.proposals[walk].index === when.index) {
|
||||
index = walk;
|
||||
break;
|
||||
}
|
||||
walk -= 1;
|
||||
}
|
||||
if (-1 === index) {
|
||||
mutator(new Error('too late'));
|
||||
return false;
|
||||
}
|
||||
let mutated = false;
|
||||
while (index < this.proposals.length) {
|
||||
const {ecs: previous} = this.proposals[index];
|
||||
// opt: not clone
|
||||
this.proposals[index].ecs = this.proposals[index - 1].ecs.clone();
|
||||
const {ecs, elapsed} = this.proposals[index];
|
||||
ecs.setClean();
|
||||
if (mutated) {
|
||||
ecs.tick(elapsed);
|
||||
}
|
||||
else {
|
||||
ecs.tick(when.offset);
|
||||
mutator(undefined, ecs);
|
||||
ecs.tick(elapsed - when.offset);
|
||||
mutated = true;
|
||||
}
|
||||
// opt: not strings
|
||||
let actuallyDirty = false;
|
||||
for (const {id} of ecs.dirty) {
|
||||
if (JSON.stringify(ecs.get(id)) !== JSON.stringify(previous.get(id))) {
|
||||
actuallyDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!actuallyDirty) {
|
||||
ecs.setClean();
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
// opt: not clone
|
||||
const ecs = this.latest.ecs.clone();
|
||||
ecs.tick(elapsed);
|
||||
this.addProposal({
|
||||
ecs,
|
||||
elapsed,
|
||||
index: this.index++,
|
||||
});
|
||||
if (10 === this.proposals.length) {
|
||||
this.proposals.shift();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Chain from './chain.js';
|
||||
import System from './system.js';
|
||||
|
||||
const Controlled = {
|
||||
input: 'uint8',
|
||||
};
|
||||
|
||||
const Position = {
|
||||
x: 'float32',
|
||||
y: 'float32',
|
||||
};
|
||||
|
||||
const Velocity = {
|
||||
x: 'float32',
|
||||
y: 'float32',
|
||||
};
|
||||
|
||||
class Physics extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Position', 'Velocity'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const [position, velocity] of this.select('default')) {
|
||||
position.x += velocity.x * elapsed;
|
||||
position.y += velocity.y * elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Controlling extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Controlled', 'Velocity'],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const [controlled, velocity] of this.select('default')) {
|
||||
velocity.y = controlled.input ? 10 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
test('efficiently rewrites history', () => {
|
||||
const chain = new Chain({Controlled, Position, Velocity});
|
||||
const {ecs} = chain.proposals[0];
|
||||
const [one, two] = ecs.createMany([
|
||||
{Controlled: {input: 0}, Position: {x: 10, y: 10}, Velocity: {x: 0, y: 0}},
|
||||
{Controlled: {input: 0}, Position: {x: 20, y: 10}, Velocity: {x: -5, y: 0}},
|
||||
]);
|
||||
const {latest: first} = chain;
|
||||
chain.latest.ecs.setClean();
|
||||
ecs.addSystem(Controlling);
|
||||
ecs.addSystem(Physics);
|
||||
chain.tick(1);
|
||||
const {latest: second} = chain;
|
||||
chain.latest.ecs.setClean();
|
||||
expect(chain.latest.ecs.get(two).Position.x)
|
||||
.to.equal(15);
|
||||
chain.tick(1);
|
||||
const {latest: third} = chain;
|
||||
chain.latest.ecs.setClean();
|
||||
expect(chain.latest.ecs.get(two).Position.x)
|
||||
.to.equal(10);
|
||||
expect(Array.from(chain.dirty).length)
|
||||
.to.equal(0);
|
||||
chain.mutate(
|
||||
{index: 1, offset: 0.25},
|
||||
(error, ecs) => {
|
||||
ecs.get(one).Controlled.input = 1;
|
||||
},
|
||||
);
|
||||
expect(Array.from(chain.dirty).length)
|
||||
.to.equal(2);
|
||||
expect(chain.latest.ecs.get(one).Position.y)
|
||||
.to.equal(27.5);
|
||||
expect(Array.from(first.ecs.dirty).length)
|
||||
.to.equal(0);
|
||||
expect(Array.from(second.ecs.dirty).length)
|
||||
.to.not.equal(0);
|
||||
second.ecs.setClean();
|
||||
chain.mutate(
|
||||
{index: 2, offset: 0.5},
|
||||
(error, ecs) => {
|
||||
ecs.get(one).Controlled.input = 0;
|
||||
},
|
||||
);
|
||||
expect(Array.from(chain.dirty).length)
|
||||
.to.equal(1);
|
||||
expect(chain.latest.ecs.get(one).Position.y)
|
||||
.to.equal(22.5);
|
||||
expect(Array.from(chain.dirty).length)
|
||||
.to.equal(1);
|
||||
third.ecs.setClean();
|
||||
third.ecs.get(one).Position.y = 22.5;
|
||||
chain.mutate(
|
||||
{index: 2, offset: 0.5},
|
||||
(error, ecs) => {
|
||||
ecs.get(one).Controlled.input = 0;
|
||||
},
|
||||
);
|
||||
expect(Array.from(chain.dirty).length)
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
test('fails when out of range', () => {
|
||||
const chain = new Chain({});
|
||||
let error;
|
||||
expect(chain.mutate(
|
||||
{index: 0, offset: 0.25},
|
||||
(error_) => {
|
||||
error = error_;
|
||||
},
|
||||
))
|
||||
.to.be.false;
|
||||
expect(error)
|
||||
.to.be.instanceOf(Error);
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
import Arbitrary from './component/arbitrary.js';
|
||||
import Flat from './component/flat.js';
|
||||
import Schema from './schema.js';
|
||||
|
||||
function Component() {
|
||||
const schema = new Schema(this.constructor.specification);
|
||||
const Component = schema.size > 0
|
||||
? class extends Flat { static size = schema.size + 5; } // 5 = 1 for dirty + 4 for entity
|
||||
: Arbitrary;
|
||||
return new Component(schema);
|
||||
}
|
||||
|
||||
export function createComponent(specification) {
|
||||
return class AdhocComponent extends Component {
|
||||
static specification = specification;
|
||||
};
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import {createComponent} from './component.js';
|
||||
|
||||
test('creates flat components', () => {
|
||||
const Component = createComponent({foo: {type: 'int32'}});
|
||||
expect(new Component().schema.size)
|
||||
.to.equal(4);
|
||||
});
|
||||
|
||||
test('creates arbitrarily-sized components', () => {
|
||||
const Component = createComponent({foo: {type: 'string'}});
|
||||
expect(new Component().schema.size)
|
||||
.to.equal(0);
|
||||
});
|
|
@ -1,123 +0,0 @@
|
|||
import Serializer from '../serializer.js';
|
||||
|
||||
import BaseComponent from './base.js';
|
||||
|
||||
export default class ArbitraryComponent extends BaseComponent {
|
||||
|
||||
data = [];
|
||||
|
||||
serializer;
|
||||
|
||||
Instance;
|
||||
|
||||
constructor(schema) {
|
||||
super(schema);
|
||||
this.serializer = new Serializer(schema);
|
||||
}
|
||||
|
||||
allocateMany(count) {
|
||||
if (!this.Instance) {
|
||||
this.Instance = this.instanceFromSchema();
|
||||
}
|
||||
const results = super.allocateMany(count);
|
||||
count -= results.length; while (count--) {
|
||||
results.push(this.data.push(new this.Instance()) - 1);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
createMany(entries) {
|
||||
if (entries.length > 0) {
|
||||
const allocated = this.allocateMany(entries.length);
|
||||
const {defaultValues} = this.schema;
|
||||
const keys = Object.keys(this.schema.specification);
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [entity, values = {}] = entries[i];
|
||||
this.map[entity] = allocated[i];
|
||||
this.data[allocated[i]].entity = entity;
|
||||
if (false === values) {
|
||||
continue;
|
||||
}
|
||||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
if (j in values) {
|
||||
this.data[allocated[i]][j] = values[j];
|
||||
}
|
||||
else if ('undefined' !== typeof defaultValues[j]) {
|
||||
this.data[allocated[i]][j] = defaultValues[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decode(entity, view, offset) {
|
||||
this.serializer.decode(view, this.get(entity), offset);
|
||||
}
|
||||
|
||||
encode(entity, view, offset) {
|
||||
this.serializer.encode(this.get(entity), view, offset);
|
||||
}
|
||||
|
||||
get(entity) {
|
||||
return this.data[this.map[entity]];
|
||||
}
|
||||
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const Instance = class {
|
||||
$$dirty = 1;
|
||||
$$entity = 0;
|
||||
toJSON() {
|
||||
const json = {};
|
||||
for (const [i] of Component.schema) {
|
||||
json[i] = this[i];
|
||||
}
|
||||
return json;
|
||||
}
|
||||
};
|
||||
const properties = {};
|
||||
properties.dirty = {
|
||||
get: function get() {
|
||||
return !!this.$$dirty;
|
||||
},
|
||||
set: function set(v) {
|
||||
this.$$dirty = v;
|
||||
},
|
||||
};
|
||||
properties.entity = {
|
||||
get: function get() {
|
||||
return this.$$entity;
|
||||
},
|
||||
set: function set(v) {
|
||||
this.$$entity = v;
|
||||
},
|
||||
};
|
||||
for (const [i] of Component.schema) {
|
||||
properties[i] = {
|
||||
get: function get() {
|
||||
return this[`$$${i}`];
|
||||
},
|
||||
set: function set(v) {
|
||||
this[`$$${i}`] = v;
|
||||
this.$$dirty = 1;
|
||||
Component.setDirty(this.entity);
|
||||
},
|
||||
};
|
||||
}
|
||||
Object.defineProperties(Instance.prototype, properties);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
setClean() {
|
||||
/* v8 ignore next 3 */
|
||||
if (!this.dirty) {
|
||||
return;
|
||||
}
|
||||
super.setClean();
|
||||
for (let i = 0; i < this.data.length; i++) {
|
||||
this.data[i].dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Schema from '../schema.js';
|
||||
import Arbitrary from './arbitrary.js';
|
||||
|
||||
const schema = new Schema({foo: {defaultValue: 'bar', type: 'string'}});
|
||||
|
||||
test('creates instances', () => {
|
||||
const Component = new Arbitrary(schema);
|
||||
Component.create(1)
|
||||
expect(Component.get(1).entity)
|
||||
.to.equal(1);
|
||||
expect(JSON.stringify(Component.get(1)))
|
||||
.to.equal(JSON.stringify({foo: 'bar'}));
|
||||
Component.create(2, {foo: 'baz'})
|
||||
expect(JSON.stringify(Component.get(2)))
|
||||
.to.equal(JSON.stringify({foo: 'baz'}));
|
||||
});
|
||||
|
||||
test('manages dirty state', () => {
|
||||
const Component = new Arbitrary(new Schema({foo: {type: 'string'}}));
|
||||
Component.create(1);
|
||||
Component.setClean();
|
||||
expect(Component.get(1).dirty)
|
||||
.to.be.false;
|
||||
Component.get(1).foo = 'boo';
|
||||
expect(Component.get(1).dirty)
|
||||
.to.be.true;
|
||||
});
|
||||
|
||||
test('reuses instances', () => {
|
||||
const Component = new Arbitrary(new Schema({foo: {type: 'string'}}));
|
||||
Component.create(1);
|
||||
const instance = Component.get(1);
|
||||
Component.destroy(1);
|
||||
expect(Component.get(1))
|
||||
.to.be.undefined;
|
||||
expect(() => {
|
||||
Component.destroy(1);
|
||||
})
|
||||
.to.throw();
|
||||
Component.create(1);
|
||||
expect(Component.get(1))
|
||||
.to.equal(instance);
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
export default class BaseComponent {
|
||||
|
||||
$$dirty = true;
|
||||
|
||||
map = [];
|
||||
|
||||
pool = [];
|
||||
|
||||
schema;
|
||||
|
||||
constructor(schema) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
allocateMany(count) {
|
||||
const results = [];
|
||||
while (count-- > 0 && this.pool.length > 0) {
|
||||
results.push(this.pool.pop());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
create(entity, values) {
|
||||
this.createMany([[entity, values]]);
|
||||
}
|
||||
|
||||
destroy(entity) {
|
||||
this.destroyMany([entity]);
|
||||
}
|
||||
|
||||
destroyMany(entities) {
|
||||
this.freeMany(
|
||||
entities
|
||||
.map((entity) => {
|
||||
if ('undefined' !== typeof this.map[entity]) {
|
||||
return this.map[entity];
|
||||
}
|
||||
throw new Error(`can't free for non-existent entity ${entity}`);
|
||||
}),
|
||||
);
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
this.map[entities[i]] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
get dirty() {
|
||||
return this.$$dirty;
|
||||
}
|
||||
|
||||
freeMany(indices) {
|
||||
for (let i = 0; i < indices.length; ++i) {
|
||||
this.pool.push(indices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
insertMany(entities) {
|
||||
const creating = [];
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const [entity, values] = entities[i];
|
||||
if (!this.get(entity)) {
|
||||
creating.push([entity, values]);
|
||||
}
|
||||
else {
|
||||
this.set(entity, values);
|
||||
}
|
||||
}
|
||||
this.createMany(creating);
|
||||
}
|
||||
|
||||
set(entity, values) {
|
||||
const instance = this.get(entity);
|
||||
for (const i in values) {
|
||||
instance[i] = values[i];
|
||||
}
|
||||
}
|
||||
|
||||
setClean() {
|
||||
this.$$dirty = false;
|
||||
}
|
||||
|
||||
setDirty() {
|
||||
this.$$dirty = true;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
import BaseComponent from './base.js';
|
||||
import Schema from '../schema.js';
|
||||
|
||||
export default class FlatComponent extends BaseComponent {
|
||||
|
||||
chunkSize = 64;
|
||||
|
||||
caret = 0;
|
||||
|
||||
data = new ArrayBuffer(0);
|
||||
|
||||
Window;
|
||||
|
||||
window;
|
||||
|
||||
allocateMany(count) {
|
||||
const results = super.allocateMany(count);
|
||||
count -= results.length;
|
||||
if (count > 0) {
|
||||
const required = (this.caret + count) * this.constructor.size;
|
||||
if (required > this.data.byteLength) {
|
||||
const chunkSize = this.chunkSize * this.constructor.size;
|
||||
const remainder = required % chunkSize;
|
||||
/* v8 ignore next */
|
||||
const extra = 0 === remainder ? 0 : chunkSize - remainder;
|
||||
const size = required + extra;
|
||||
// soon...
|
||||
const data = new ArrayBuffer(size);
|
||||
(new Uint8Array(data)).set(this.data);
|
||||
this.data = data;
|
||||
this.Window = undefined;
|
||||
this.window = undefined;
|
||||
}
|
||||
for (let i = 0; i < count; ++i) {
|
||||
results.push(this.caret++);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
createMany(entries) {
|
||||
if (entries.length > 0) {
|
||||
const allocated = this.allocateMany(entries.length);
|
||||
/* v8 ignore next 3 */
|
||||
if (!this.Window) {
|
||||
this.Window = this.makeWindowClass();
|
||||
}
|
||||
const window = new this.Window(this.data, this);
|
||||
const {defaultValues} = this.schema;
|
||||
const keys = Object.keys(this.schema.specification);
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [entity, values = {}] = entries[i];
|
||||
this.map[entity] = allocated[i];
|
||||
window.cursor = allocated[i] * this.constructor.size;
|
||||
window.entity = entity;
|
||||
if (false === values) {
|
||||
continue;
|
||||
}
|
||||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
if (j in values) {
|
||||
window[j] = values[j];
|
||||
}
|
||||
else if ('undefined' !== typeof defaultValues[j]) {
|
||||
window[j] = defaultValues[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decode(entity, view, offset) {
|
||||
/* v8 ignore next 6 */
|
||||
if (!this.Window) {
|
||||
this.Window = this.makeWindowClass();
|
||||
}
|
||||
if (!this.window) {
|
||||
this.window = new this.Window(this.data, this);
|
||||
}
|
||||
const cursor = this.map[entity] * this.constructor.size;
|
||||
for (let i = 0; i < this.schema.size; ++i) {
|
||||
this.window.view.setUint8(cursor + i, view.getUint8(offset + i));
|
||||
}
|
||||
}
|
||||
|
||||
encode(entity, view, offset) {
|
||||
/* v8 ignore next 6 */
|
||||
if (!this.Window) {
|
||||
this.Window = this.makeWindowClass();
|
||||
}
|
||||
if (!this.window) {
|
||||
this.window = new this.Window(this.data, this);
|
||||
}
|
||||
const cursor = this.map[entity] * this.constructor.size;
|
||||
for (let i = 0; i < this.schema.size; ++i) {
|
||||
view.setUint8(offset + i, this.window.view.getUint8(cursor + i));
|
||||
}
|
||||
}
|
||||
|
||||
get(entity) {
|
||||
const offset = this.map[entity];
|
||||
if (undefined === offset) {
|
||||
return undefined;
|
||||
}
|
||||
/* v8 ignore next 6 */
|
||||
if (!this.Window) {
|
||||
this.Window = this.makeWindowClass();
|
||||
}
|
||||
if (!this.window) {
|
||||
this.window = new this.Window(this.data, this);
|
||||
}
|
||||
this.window.cursor = offset * this.constructor.size;
|
||||
return this.window;
|
||||
}
|
||||
|
||||
makeWindowClass() {
|
||||
const Component = this;
|
||||
class Window {
|
||||
|
||||
cursor = 0;
|
||||
|
||||
parent;
|
||||
|
||||
view;
|
||||
|
||||
constructor(data, parent) {
|
||||
if (data) {
|
||||
this.view = new DataView(data);
|
||||
}
|
||||
if (parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const json = {};
|
||||
for (const [i] of Component.schema) {
|
||||
json[i] = this[i];
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
}
|
||||
let offset = 0;
|
||||
const properties = {};
|
||||
const {size} = this.constructor;
|
||||
const get = (type) => (
|
||||
`return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);`
|
||||
);
|
||||
const set = (type) => [
|
||||
`if (this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true) === v) {`,
|
||||
' return;',
|
||||
'}',
|
||||
`this.parent.setDirty(this.view.getUint32(this.cursor + ${size - 5}, true));`,
|
||||
`this.view.set${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, v, true);`,
|
||||
`this.view.setUint8(this.cursor + ${size - 1}, 1, true);`,
|
||||
].join('\n');
|
||||
properties.dirty = {
|
||||
get: new Function('', `return !!this.view.getUint8(this.cursor + ${size - 1}, true);`),
|
||||
set: new Function('v', `this.view.setUint8(this.cursor + ${size - 1}, v ? 1 : 0, true);`),
|
||||
};
|
||||
properties.entity = {
|
||||
get: new Function('', `return this.view.getUint32(this.cursor + ${size - 5}, true);`),
|
||||
set: new Function('v', `this.view.setUint32(this.cursor + ${size - 5}, v, true);`),
|
||||
};
|
||||
for (const [i, spec] of this.schema) {
|
||||
const {type} = spec;
|
||||
properties[i] = {};
|
||||
properties[i].get = new Function('', get(type));
|
||||
properties[i].set = new Function('v', set(type));
|
||||
offset += Schema.sizeOfType(type);
|
||||
}
|
||||
Object.defineProperties(Window.prototype, properties);
|
||||
return Window;
|
||||
}
|
||||
|
||||
setClean() {
|
||||
/* v8 ignore next 3 */
|
||||
if (!this.dirty) {
|
||||
return;
|
||||
}
|
||||
super.setClean();
|
||||
/* v8 ignore next 3 */
|
||||
if (!this.Window) {
|
||||
this.Window = this.makeWindowClass();
|
||||
}
|
||||
const window = new this.Window(this.data, this);
|
||||
for (let i = 0; i < this.caret; ++i) {
|
||||
window.dirty = false;
|
||||
window.cursor += this.constructor.size;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Schema from '../schema.js';
|
||||
import Flat from './flat.js';
|
||||
|
||||
const schema = new Schema({foo: {defaultValue: 42, type: 'uint8'}});
|
||||
|
||||
class FixedSize extends Flat { static size = schema.size + 5; }
|
||||
|
||||
test('creates instances', () => {
|
||||
const Component = new FixedSize(schema);
|
||||
Flat.size = 10;
|
||||
Component.create(1)
|
||||
expect(Component.get(1).entity)
|
||||
.to.equal(1);
|
||||
expect(Component.get(1).foo)
|
||||
.to.equal(42);
|
||||
Component.create(2, {foo: 12})
|
||||
expect(Component.get(2).foo)
|
||||
.to.equal(12);
|
||||
});
|
||||
|
||||
test('manages dirty state', () => {
|
||||
const Component = new FixedSize(schema);
|
||||
Component.create(1);
|
||||
Component.setClean();
|
||||
expect(Component.get(1).dirty)
|
||||
.to.be.false;
|
||||
Component.get(1).foo = 'boo';
|
||||
expect(Component.get(1).dirty)
|
||||
.to.be.true;
|
||||
});
|
||||
|
||||
test('reuses instances', () => {
|
||||
const Component = new FixedSize(schema);
|
||||
Component.create(1);
|
||||
const instance = Component.get(1);
|
||||
Component.destroy(1);
|
||||
expect(Component.get(1))
|
||||
.to.be.undefined;
|
||||
expect(() => {
|
||||
Component.destroy(1);
|
||||
})
|
||||
.to.throw();
|
||||
Component.create(1);
|
||||
expect(Component.get(1))
|
||||
.to.equal(instance);
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
export const Controlled = {
|
||||
up: 'float32',
|
||||
right: 'float32',
|
||||
down: 'float32',
|
||||
left: 'float32',
|
||||
};
|
||||
|
||||
export const Position = {
|
||||
x: 'float32',
|
||||
y: 'float32',
|
||||
};
|
||||
|
||||
export const Visible = {
|
||||
image: 'string',
|
||||
};
|
||||
|
||||
export const Wandering = {};
|
497
src/ecs/ecs.js
497
src/ecs/ecs.js
|
@ -1,497 +0,0 @@
|
|||
import Bundle from './bundle.js';
|
||||
import {createComponent} from './component.js';
|
||||
import EntityFactory from './entity-factory.js';
|
||||
import System from './system.js';
|
||||
|
||||
export default class Ecs {
|
||||
|
||||
$$caret = 1;
|
||||
|
||||
$$dirty = false;
|
||||
|
||||
Bundles = {};
|
||||
|
||||
static ComponentLikesAndOrBundleLikes = {};
|
||||
|
||||
$$ComponentLikesAndOrBundleLikes;
|
||||
|
||||
Components = {};
|
||||
|
||||
$$entities = [];
|
||||
|
||||
$$entityFactory = new EntityFactory();
|
||||
|
||||
$$pool = [];
|
||||
|
||||
$$systems = [];
|
||||
|
||||
constructor() {
|
||||
const {ComponentLikesAndOrBundleLikes} = this.constructor;
|
||||
this.$$ComponentLikesAndOrBundleLikes = ComponentLikesAndOrBundleLikes;
|
||||
const Bundles = [];
|
||||
for (const i in ComponentLikesAndOrBundleLikes) {
|
||||
const MaybeBundle = Bundle.maybeNormalize(ComponentLikesAndOrBundleLikes[i]);
|
||||
if (MaybeBundle) {
|
||||
Bundles.push([i, MaybeBundle]);
|
||||
}
|
||||
else {
|
||||
this.Components[i] = this.withDirtyTracking(
|
||||
createComponent(ComponentLikesAndOrBundleLikes[i]),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < Bundles.length; i++) {
|
||||
const [j, Bundle] = Bundles[i];
|
||||
this.Bundles[j] = new Bundle(Bundle.Components.map((c) => this.Components[c]));
|
||||
}
|
||||
}
|
||||
|
||||
addSystem(source) {
|
||||
const ecs = this;
|
||||
class WrappedSystem extends System.normalize(source) {
|
||||
|
||||
constructor(Components) {
|
||||
super(Components);
|
||||
this.ecs = ecs;
|
||||
}
|
||||
|
||||
createEntity(components) {
|
||||
return this.ecs.create(components);
|
||||
}
|
||||
|
||||
createManyEntities(componentsList) {
|
||||
return this.ecs.createMany(componentsList);
|
||||
}
|
||||
|
||||
get source() {
|
||||
return source;
|
||||
}
|
||||
|
||||
insertComponents(entity, components) {
|
||||
this.ecs.insert(entity, components);
|
||||
}
|
||||
|
||||
insertManyComponents(components) {
|
||||
this.ecs.insertMany(components);
|
||||
}
|
||||
|
||||
removeComponents(entity, components) {
|
||||
this.ecs.remove(entity, components);
|
||||
}
|
||||
|
||||
removeManyComponents(entities) {
|
||||
this.ecs.removeMany(entities);
|
||||
}
|
||||
|
||||
}
|
||||
const wrappedSystem = new WrappedSystem(this.Components);
|
||||
wrappedSystem.reindex(this.entities);
|
||||
this.$$systems.push(wrappedSystem);
|
||||
}
|
||||
|
||||
clone() {
|
||||
const view = new DataView(new ArrayBuffer(this.sizeOf(this.entities)));
|
||||
this.encode(this.entities, view);
|
||||
const ecs = new this.constructor(this.$$ComponentLikesAndOrBundleLikes);
|
||||
ecs.decode(view);
|
||||
for (const system of this.$$systems) {
|
||||
ecs.addSystem(system.source);
|
||||
}
|
||||
return ecs;
|
||||
}
|
||||
|
||||
create(components = {}) {
|
||||
const [entity] = this.createMany([components]);
|
||||
return entity;
|
||||
}
|
||||
|
||||
createExact(entity, components) {
|
||||
this.createManyExact([[entity, components]]);
|
||||
}
|
||||
|
||||
createMany(componentsList) {
|
||||
const entities = [];
|
||||
const creating = {};
|
||||
for (let i = 0; i < componentsList.length; i++) {
|
||||
const components = componentsList[i];
|
||||
const componentKeys = [];
|
||||
for (const key of Object.keys(components)) {
|
||||
if (this.Components[key]) {
|
||||
componentKeys.push(key);
|
||||
}
|
||||
const Bundle = this.Bundles[key];
|
||||
if (Bundle) {
|
||||
const configured = Bundle.configure(components[key]);
|
||||
for (const subkey in configured) {
|
||||
components[subkey] = configured[subkey];
|
||||
componentKeys.push(subkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
let entity;
|
||||
if (this.$$pool.length > 0) {
|
||||
entity = this.$$pool.pop();
|
||||
}
|
||||
else {
|
||||
entity = this.$$caret++;
|
||||
}
|
||||
entities.push(entity);
|
||||
this.rebuild(entity, () => componentKeys);
|
||||
for (let j = 0; j < componentKeys.length; j++) {
|
||||
const component = componentKeys[j];
|
||||
if (!creating[component]) {
|
||||
creating[component] = [];
|
||||
}
|
||||
creating[component].push([entity, components[component]]);
|
||||
}
|
||||
}
|
||||
for (const i in creating) {
|
||||
this.Components[i].createMany(creating[i]);
|
||||
}
|
||||
for (let i = 0; i < this.$$systems.length; i++) {
|
||||
this.$$systems[i].reindex(entities);
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
createManyExact(entities) {
|
||||
const creating = {};
|
||||
for (const entry of entities) {
|
||||
let components = {};
|
||||
let entity;
|
||||
if (Array.isArray(entry)) {
|
||||
[entity, components = {}] = entry;
|
||||
}
|
||||
else {
|
||||
entity = entry;
|
||||
}
|
||||
if (this.$$entities[entity]) {
|
||||
throw new Error(`can't create existing entity ${entity}`);
|
||||
}
|
||||
const index = this.$$pool.indexOf(entity);
|
||||
if (-1 !== index) {
|
||||
this.$$pool.splice(index, 1);
|
||||
}
|
||||
this.rebuild(entity, () => Object.keys(components));
|
||||
for (const j in components) {
|
||||
if (!creating[j]) {
|
||||
creating[j] = [];
|
||||
}
|
||||
creating[j].push([entity, components[j]]);
|
||||
}
|
||||
}
|
||||
for (const i in creating) {
|
||||
this.Components[i].createMany(creating[i]);
|
||||
}
|
||||
}
|
||||
|
||||
decode(view) {
|
||||
let cursor = 0;
|
||||
const count = view.getUint32(cursor, true);
|
||||
if (0 === count) {
|
||||
return;
|
||||
}
|
||||
const keys = Object.keys(this.Components);
|
||||
cursor += 4;
|
||||
const creating = new Map();
|
||||
const updating = new Map();
|
||||
const cursors = new Map();
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const entity = view.getUint32(cursor, true);
|
||||
if (!this.$$entities[entity]) {
|
||||
creating.set(entity, {});
|
||||
}
|
||||
cursor += 4;
|
||||
const componentCount = view.getUint16(cursor, true);
|
||||
cursor += 2;
|
||||
cursors.set(entity, {});
|
||||
const addedComponents = [];
|
||||
for (let j = 0; j < componentCount; ++j) {
|
||||
const id = view.getUint16(cursor, true);
|
||||
cursor += 2;
|
||||
const component = keys[id];
|
||||
if (!component) {
|
||||
throw new Error(`can't decode component ${id}`);
|
||||
}
|
||||
if (!this.$$entities[entity]) {
|
||||
creating.get(entity)[component] = false;
|
||||
}
|
||||
else if (!this.$$entities[entity].constructor.types.includes(component)) {
|
||||
addedComponents.push(component);
|
||||
if (!updating.has(component)) {
|
||||
updating.set(component, []);
|
||||
}
|
||||
updating.get(component).push([entity, false]);
|
||||
}
|
||||
cursors.get(entity)[component] = cursor;
|
||||
cursor += this.Components[component].schema.readSize(view, cursor);
|
||||
}
|
||||
if (addedComponents.length > 0 && this.$$entities[entity]) {
|
||||
this.rebuild(entity, (types) => types.concat(addedComponents));
|
||||
}
|
||||
}
|
||||
this.createManyExact(creating.entries());
|
||||
for (const [component, entities] of updating) {
|
||||
this.Components[component].createMany(entities);
|
||||
}
|
||||
for (const [entity, components] of cursors) {
|
||||
for (const component in components) {
|
||||
this.Components[component].decode(entity, view, components[component]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy(entity) {
|
||||
this.destroyMany([entity]);
|
||||
}
|
||||
|
||||
destroyAll() {
|
||||
this.destroyMany(this.entities);
|
||||
}
|
||||
|
||||
destroyMany(entities) {
|
||||
const destroying = {};
|
||||
for (const entity of entities) {
|
||||
if (!this.$$entities[entity]) {
|
||||
throw new Error(`can't destroy non-existent entity ${entity}`);
|
||||
}
|
||||
for (const component of this.$$entities[entity].constructor.types) {
|
||||
if (!destroying[component]) {
|
||||
destroying[component] = [];
|
||||
}
|
||||
destroying[component].push(entity);
|
||||
}
|
||||
this.$$pool.push(entity);
|
||||
this.$$entities[entity] = undefined;
|
||||
}
|
||||
for (const i in destroying) {
|
||||
this.Components[i].destroyMany(destroying[i]);
|
||||
}
|
||||
for (let i = 0; i < this.$$systems.length; i++) {
|
||||
this.$$systems[i].deindex(entities);
|
||||
}
|
||||
}
|
||||
|
||||
get dirty() {
|
||||
const ecs = this;
|
||||
const it = this.$$entities[Symbol.iterator]()
|
||||
return {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
if (!ecs.$$dirty) {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
let result = it.next();
|
||||
while (!result.done && !result.value?.dirty) {
|
||||
result = it.next();
|
||||
}
|
||||
if (result.done) {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
encode(entities, view, options = {}) {
|
||||
const {onlyDirty = false} = options;
|
||||
let cursor = 0;
|
||||
let entitiesWritten = 0;
|
||||
cursor += 4;
|
||||
const keys = Object.keys(this.Components);
|
||||
for (const entity of entities) {
|
||||
if (onlyDirty && !this.$$entities[entity].dirty) {
|
||||
continue;
|
||||
}
|
||||
entitiesWritten += 1;
|
||||
view.setUint32(cursor, entity, true);
|
||||
cursor += 4;
|
||||
const entityComponents = this.$$entities[entity].constructor.types;
|
||||
view.setUint16(cursor, entityComponents.length, true);
|
||||
const componentsWrittenIndex = cursor;
|
||||
cursor += 2;
|
||||
if (0 === entityComponents.length) {
|
||||
continue;
|
||||
}
|
||||
let componentsWritten = 0;
|
||||
for (const component of entityComponents) {
|
||||
const instance = this.Components[component];
|
||||
if (onlyDirty && !instance.dirty) {
|
||||
continue;
|
||||
}
|
||||
componentsWritten += 1;
|
||||
view.setUint16(cursor, keys.indexOf(component), true);
|
||||
cursor += 2;
|
||||
const source = instance.get(entity);
|
||||
this.Components[component].encode(entity, view, cursor);
|
||||
cursor += instance.schema.sizeOf(source);
|
||||
}
|
||||
view.setUint16(componentsWrittenIndex, componentsWritten, true);
|
||||
}
|
||||
view.setUint32(0, entitiesWritten, true);
|
||||
}
|
||||
|
||||
get entities() {
|
||||
const it = this.$$entities[Symbol.iterator]()
|
||||
return {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
let result = it.next();
|
||||
while (!result.done && !result.value) {
|
||||
result = it.next();
|
||||
}
|
||||
if (result.done) {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
return {done: false, value: result.value.id};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get(entity) {
|
||||
return this.$$entities[entity];
|
||||
}
|
||||
|
||||
insert(entity, components) {
|
||||
this.insertMany([[entity, components]]);
|
||||
}
|
||||
|
||||
insertMany(entities) {
|
||||
const inserting = {};
|
||||
const unique = new Set();
|
||||
for (const [entity, components] of entities) {
|
||||
this.rebuild(entity, (types) => [...new Set(types.concat(Object.keys(components)))]);
|
||||
for (const component in components) {
|
||||
if (!inserting[component]) {
|
||||
inserting[component] = [];
|
||||
}
|
||||
inserting[component].push([entity, components[component]]);
|
||||
}
|
||||
unique.add(entity);
|
||||
}
|
||||
for (const component in inserting) {
|
||||
this.Components[component].insertMany(inserting[component]);
|
||||
}
|
||||
for (let i = 0; i < this.$$systems.length; i++) {
|
||||
this.$$systems[i].reindex(unique.values());
|
||||
}
|
||||
}
|
||||
|
||||
rebuild(entity, types) {
|
||||
let existing = [];
|
||||
if (this.$$entities[entity]) {
|
||||
existing.push(...this.$$entities[entity].constructor.types);
|
||||
}
|
||||
const Class = this.$$entityFactory.makeClass(types(existing), this.Components);
|
||||
this.$$entities[entity] = new Class(entity);
|
||||
}
|
||||
|
||||
remove(entity, components) {
|
||||
this.removeMany([[entity, components]]);
|
||||
}
|
||||
|
||||
removeMany(entities) {
|
||||
const removing = {};
|
||||
const unique = new Set();
|
||||
for (const [entity, components] of entities) {
|
||||
unique.add(entity);
|
||||
for (const component of components) {
|
||||
if (!removing[component]) {
|
||||
removing[component] = [];
|
||||
}
|
||||
removing[component].push(entity);
|
||||
}
|
||||
this.rebuild(entity, (types) => types.filter((type) => !components.includes(type)));
|
||||
}
|
||||
for (const component in removing) {
|
||||
this.Components[component].destroyMany(removing[component]);
|
||||
}
|
||||
for (let i = 0; i < this.$$systems.length; i++) {
|
||||
this.$$systems[i].reindex(unique.values());
|
||||
}
|
||||
}
|
||||
|
||||
removeSystem(SystemLike) {
|
||||
const index = this.$$systems.findIndex((system) => SystemLike === system.source);
|
||||
if (-1 !== index) {
|
||||
this.$$systems.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
setClean() {
|
||||
for (const i in this.Components) {
|
||||
this.Components[i].setClean();
|
||||
}
|
||||
for (const entity in this.$$entities) {
|
||||
if (this.$$entities[entity]) {
|
||||
this.$$entities[entity].dirty = false;
|
||||
}
|
||||
}
|
||||
this.$$dirty = false;
|
||||
}
|
||||
|
||||
sizeOf(entities, options = {}) {
|
||||
const {onlyDirty = false} = options;
|
||||
let size = 0;
|
||||
if (0 === entities.length) {
|
||||
return size;
|
||||
}
|
||||
size += 4;
|
||||
for (const entity of entities) {
|
||||
if (!this.$$entities[entity]) {
|
||||
throw new Error(`can't encode non-existent entity ${entity}`);
|
||||
}
|
||||
if (onlyDirty && !this.$$entities[entity].dirty) {
|
||||
continue;
|
||||
}
|
||||
// Entity + # of components.
|
||||
size += 4 + 2;
|
||||
for (const component of this.$$entities[entity].constructor.types) {
|
||||
const instance = this.Components[component];
|
||||
if (onlyDirty && !instance.dirty) {
|
||||
continue;
|
||||
}
|
||||
size += 2 + instance.schema.sizeOf(instance.get(entity));
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (let i = 0; i < this.$$systems.length; i++) {
|
||||
this.$$systems[i].tick(elapsed);
|
||||
}
|
||||
for (let i = 0; i < this.$$systems.length; i++) {
|
||||
this.$$systems[i].finalize(elapsed);
|
||||
}
|
||||
this.tickDestruction();
|
||||
}
|
||||
|
||||
tickDestruction() {
|
||||
const unique = new Set();
|
||||
for (let i = 0; i < this.$$systems.length; i++) {
|
||||
for (let j = 0; j < this.$$systems[i].destroying.length; j++) {
|
||||
unique.add(this.$$systems[i].destroying[j]);
|
||||
}
|
||||
this.$$systems[i].tickDestruction();
|
||||
}
|
||||
if (unique.size > 0) {
|
||||
this.destroyMany(unique.values());
|
||||
}
|
||||
}
|
||||
|
||||
withDirtyTracking(Component) {
|
||||
const component = new Component();
|
||||
component.setDirty = (entity) => {
|
||||
component.$$dirty = true;
|
||||
this.$$dirty = true;
|
||||
this.$$entities[entity].dirty = true;
|
||||
};
|
||||
return component;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,426 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Bundle from './bundle';
|
||||
import Ecs from './ecs';
|
||||
import System from './system';
|
||||
|
||||
const Empty = {};
|
||||
|
||||
const Position = {
|
||||
x: {type: 'int32', defaultValue: 32},
|
||||
y: 'int32',
|
||||
z: 'int32',
|
||||
};
|
||||
|
||||
test('can add and remove systems at runtime', () => {
|
||||
const ecs = new Ecs();
|
||||
let oneCount = 0;
|
||||
let twoCount = 0;
|
||||
const oneSystem = () => {
|
||||
oneCount++;
|
||||
};
|
||||
ecs.addSystem(oneSystem);
|
||||
ecs.tick();
|
||||
expect(oneCount)
|
||||
.to.equal(1);
|
||||
const twoSystem = () => {
|
||||
twoCount++;
|
||||
};
|
||||
ecs.addSystem(twoSystem);
|
||||
ecs.tick();
|
||||
expect(oneCount)
|
||||
.to.equal(2);
|
||||
expect(twoCount)
|
||||
.to.equal(1);
|
||||
ecs.removeSystem(oneSystem);
|
||||
ecs.tick();
|
||||
expect(oneCount)
|
||||
.to.equal(2);
|
||||
expect(twoCount)
|
||||
.to.equal(2);
|
||||
});
|
||||
|
||||
test('fails trying to exact create already existing', () => {
|
||||
const ecs = new Ecs();
|
||||
ecs.createExact(1);
|
||||
expect(() => {
|
||||
ecs.createExact(1);
|
||||
})
|
||||
.to.throw();
|
||||
});
|
||||
|
||||
test('reuses from pool', () => {
|
||||
const ecs = new Ecs();
|
||||
ecs.createExact(1);
|
||||
ecs.destroy(1);
|
||||
expect(ecs.$$pool.length)
|
||||
.to.equal(1);
|
||||
expect(ecs.create())
|
||||
.to.equal(1);
|
||||
expect(ecs.$$pool.length)
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
test('cleans pool when exact creating a previously existing entity', () => {
|
||||
const ecs = new Ecs();
|
||||
ecs.createManyExact([1]);
|
||||
ecs.destroy(1);
|
||||
expect(ecs.$$pool.length)
|
||||
.to.equal(1);
|
||||
ecs.createExact(1);
|
||||
expect(ecs.$$pool.length)
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
test('can create entities with components', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 420}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 420, z: 0}}));
|
||||
});
|
||||
|
||||
test("can remove entities' components", () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 420}});
|
||||
ecs.remove(entity, ['Position']);
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}}));
|
||||
});
|
||||
|
||||
test('gets entities', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 420}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 420, z: 0}}));
|
||||
});
|
||||
|
||||
test('destroys entities', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 420}});
|
||||
ecs.destroyAll();
|
||||
expect(ecs.get(entity))
|
||||
.to.be.undefined;
|
||||
expect(() => {
|
||||
ecs.destroy(entity);
|
||||
})
|
||||
.to.throw();
|
||||
});
|
||||
|
||||
test('can insert components into entities', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const entity = ecs.create({Empty: {}});
|
||||
ecs.insert(entity, {Position: {y: 420}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 420, z: 0}}));
|
||||
ecs.insert(entity, {Position: {y: 69}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 69, z: 0}}));
|
||||
});
|
||||
|
||||
test('can tick systems', () => {
|
||||
const Momentum = {
|
||||
x: 'int32',
|
||||
y: 'int32',
|
||||
z: 'int32',
|
||||
};
|
||||
const ecs = new Ecs({Momentum, Position});
|
||||
class Physics extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Position', 'Momentum'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const [position, momentum] of this.select('default')) {
|
||||
position.x += momentum.x * elapsed;
|
||||
position.y += momentum.y * elapsed;
|
||||
position.z += momentum.z * elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
ecs.addSystem(Physics);
|
||||
const entity = ecs.create({Momentum: {}, Position: {y: 420}});
|
||||
const position = JSON.stringify(ecs.get(entity).Position);
|
||||
ecs.tick(1);
|
||||
expect(JSON.stringify(ecs.get(entity).Position))
|
||||
.to.deep.equal(position);
|
||||
ecs.get(1).Momentum.y = 30;
|
||||
ecs.tick(1);
|
||||
expect(JSON.stringify(ecs.get(entity).Position))
|
||||
.to.deep.equal(JSON.stringify({x: 32, y: 450, z: 0}));
|
||||
});
|
||||
|
||||
test('can create many entities when ticking systems', () => {
|
||||
const ecs = new Ecs();
|
||||
class Spawn extends System {
|
||||
tick() {
|
||||
this.createManyEntities(Array.from({length: 5}).map(() => []));
|
||||
}
|
||||
}
|
||||
ecs.addSystem(Spawn);
|
||||
ecs.create();
|
||||
expect(ecs.get(5))
|
||||
.to.be.undefined;
|
||||
ecs.tick(1);
|
||||
expect(ecs.get(5))
|
||||
.to.not.be.undefined;
|
||||
});
|
||||
|
||||
test('can create entities when ticking systems', () => {
|
||||
const ecs = new Ecs();
|
||||
class Spawn extends System {
|
||||
tick() {
|
||||
this.createEntity();
|
||||
}
|
||||
}
|
||||
ecs.addSystem(Spawn);
|
||||
ecs.create();
|
||||
expect(ecs.get(2))
|
||||
.to.be.undefined;
|
||||
ecs.tick(1);
|
||||
expect(ecs.get(2))
|
||||
.to.not.be.undefined;
|
||||
});
|
||||
|
||||
test('can schedule entities to be deleted when ticking systems', () => {
|
||||
const ecs = new Ecs();
|
||||
let entity;
|
||||
class Despawn extends System {
|
||||
|
||||
finalize() {
|
||||
entity = ecs.get(1);
|
||||
}
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Despawn'],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.destroyEntity(1);
|
||||
}
|
||||
|
||||
}
|
||||
ecs.addSystem(Despawn);
|
||||
ecs.createExact(1);
|
||||
ecs.tick(1);
|
||||
expect(entity)
|
||||
.to.not.be.undefined;
|
||||
expect(ecs.get(1))
|
||||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('can add components to and remove components from entities when ticking systems', () => {
|
||||
const Foo = {bar: 'uint8'};
|
||||
const ecs = new Ecs({Foo});
|
||||
let addLength, removeLength;
|
||||
class AddComponent extends System {
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Foo'],
|
||||
};
|
||||
}
|
||||
tick() {
|
||||
this.insertComponents(1, {Foo: {}});
|
||||
}
|
||||
finalize() {
|
||||
addLength = Array.from(this.select('default')).length;
|
||||
}
|
||||
}
|
||||
class RemoveComponent extends System {
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Foo'],
|
||||
};
|
||||
}
|
||||
tick() {
|
||||
this.removeComponents(1, ['Foo']);
|
||||
}
|
||||
finalize() {
|
||||
removeLength = Array.from(this.select('default')).length;
|
||||
}
|
||||
}
|
||||
ecs.addSystem(AddComponent);
|
||||
ecs.createExact(1);
|
||||
ecs.tick(1);
|
||||
expect(addLength)
|
||||
.to.equal(1);
|
||||
expect(ecs.get(1).Foo)
|
||||
.to.not.be.undefined;
|
||||
ecs.removeSystem(AddComponent);
|
||||
ecs.addSystem(RemoveComponent);
|
||||
ecs.tick(1);
|
||||
expect(removeLength)
|
||||
.to.equal(0);
|
||||
expect(ecs.get(1).Foo)
|
||||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('can add many components to and remove many components from entities when ticking systems', () => {
|
||||
const Foo = {bar: 'uint8'};
|
||||
const ecs = new Ecs({Foo});
|
||||
class AddComponent extends System {
|
||||
tick() {
|
||||
this.insertManyComponents([
|
||||
[1, {Foo: {}}],
|
||||
[2, {Foo: {bar: 42}}],
|
||||
]);
|
||||
}
|
||||
}
|
||||
class RemoveComponent extends System {
|
||||
tick() {
|
||||
this.removeManyComponents([
|
||||
[1, ['Foo']],
|
||||
[2, ['Foo']],
|
||||
]);
|
||||
}
|
||||
}
|
||||
ecs.addSystem(AddComponent);
|
||||
ecs.createManyExact([1, 2]);
|
||||
ecs.tick(1);
|
||||
expect(ecs.get(2).Foo.bar)
|
||||
.to.equal(42);
|
||||
ecs.removeSystem(AddComponent);
|
||||
ecs.addSystem(RemoveComponent);
|
||||
ecs.tick(1);
|
||||
expect(ecs.get(2).Foo)
|
||||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('can encode and decode an ecs', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const view = new DataView(new ArrayBuffer(1024));
|
||||
expect(() => {
|
||||
ecs.encode([], view);
|
||||
})
|
||||
.not.to.throw();
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 420}});
|
||||
ecs.encode([entity], view, {onlyDirty: true});
|
||||
const newEcs = new Ecs({Empty, Position});
|
||||
newEcs.decode(view);
|
||||
expect(JSON.stringify(newEcs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify(ecs.get(entity)));
|
||||
ecs.setClean();
|
||||
ecs.encode([entity], view, {onlyDirty: true});
|
||||
const newEcs2 = new Ecs({Empty, Position});
|
||||
newEcs2.decode(view);
|
||||
expect(newEcs2.get(entity))
|
||||
.to.be.undefined;
|
||||
ecs.get(entity).Position.x = 42;
|
||||
ecs.encode([entity], view, {onlyDirty: true});
|
||||
newEcs2.decode(view);
|
||||
expect(newEcs2.get(entity))
|
||||
.to.not.be.undefined;
|
||||
expect(newEcs2.get(entity).Empty)
|
||||
.to.be.undefined;
|
||||
ecs.encode([entity], view);
|
||||
newEcs2.decode(view);
|
||||
expect(newEcs2.get(entity))
|
||||
.to.not.be.undefined;
|
||||
const empty = newEcs2.get(entity).Empty;
|
||||
expect(empty)
|
||||
.to.not.be.undefined;
|
||||
newEcs2.decode(view);
|
||||
expect(empty)
|
||||
.to.not.be.undefined;
|
||||
});
|
||||
|
||||
test('catches bad encoding', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const entity = ecs.create({Empty: {}});
|
||||
const view = new DataView(new ArrayBuffer(1024));
|
||||
ecs.encode([entity], view);
|
||||
view.setUint16(10, 2);
|
||||
const newEcs = new Ecs({Empty, Position});
|
||||
expect(() => {
|
||||
newEcs.decode(view);
|
||||
})
|
||||
.to.throw();
|
||||
});
|
||||
|
||||
test('manages dirtiness', () => {
|
||||
const ecs = new Ecs({Position});
|
||||
const entity = ecs.create({Position: {x: 42}});
|
||||
expect(ecs.Components['Position'].dirty)
|
||||
.to.be.true;
|
||||
ecs.setClean();
|
||||
expect(ecs.Components['Position'].dirty)
|
||||
.to.be.false;
|
||||
ecs.get(entity).Position.x = 13;
|
||||
expect(ecs.Components['Position'].dirty)
|
||||
.to.be.true;
|
||||
});
|
||||
|
||||
test('handles empty size', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
expect(ecs.sizeOf([]))
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
test('catches non-existent size', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
ecs.create({Empty: {}, Position: {x: 42}});
|
||||
expect(() => {
|
||||
ecs.sizeOf([2]);
|
||||
})
|
||||
.to.throw();
|
||||
});
|
||||
|
||||
test('calculates size based on dirtiness', () => {
|
||||
const ecs = new Ecs({Empty, Position});
|
||||
const first = ecs.create({Empty: {}, Position: {x: 42}});
|
||||
ecs.create({Empty: {}, Position: {y: 42}});
|
||||
const sizeOfBoth = ecs.sizeOf([1, 2], true);
|
||||
ecs.setClean();
|
||||
ecs.get(first).Position.x = 13;
|
||||
expect(ecs.sizeOf([1, 2], {onlyDirty: true}))
|
||||
.to.be.lessThan(sizeOfBoth);
|
||||
expect(ecs.sizeOf([1, 2]))
|
||||
.to.equal(sizeOfBoth);
|
||||
});
|
||||
|
||||
test('clones an ecs', () => {
|
||||
const ecs = new Ecs({Position});
|
||||
const entity = ecs.create({Position: {y: 420}});
|
||||
const entity2 = ecs.create();
|
||||
const newEcs = ecs.clone();
|
||||
expect(JSON.stringify(newEcs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify(ecs.get(entity)));
|
||||
expect(JSON.stringify(newEcs.get(entity2)))
|
||||
.to.deep.equal(JSON.stringify(ecs.get(entity2)));
|
||||
});
|
||||
|
||||
test('can add bundles', () => {
|
||||
const Height = {height: 'uint32'};
|
||||
const Width = {width: 'uint32'};
|
||||
const Area = ['Height', 'Width'];
|
||||
class ConfiguredArea extends Bundle {
|
||||
|
||||
static configure({height, width}) {
|
||||
return {Height: {height}, Width: {width}};
|
||||
}
|
||||
|
||||
static Components = ['Height', 'Width'];
|
||||
|
||||
}
|
||||
const ecs = new Ecs({
|
||||
Area,
|
||||
ConfiguredArea,
|
||||
Height,
|
||||
Width,
|
||||
});
|
||||
const entity = ecs.get(ecs.create({Area: {Width: {width: 420}}}));
|
||||
expect(entity.Height.height)
|
||||
.to.equal(0);
|
||||
expect(entity.Width.width)
|
||||
.to.equal(420);
|
||||
const configuredEntity = ecs.get(ecs.create({ConfiguredArea: {width: 420}}));
|
||||
expect(configuredEntity.Height.height)
|
||||
.to.equal(0);
|
||||
expect(configuredEntity.Width.width)
|
||||
.to.equal(420);
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
class Node {
|
||||
children = {};
|
||||
class;
|
||||
}
|
||||
|
||||
export default class EntityFactory {
|
||||
|
||||
$$tries = new Node();
|
||||
|
||||
makeClass(types, Components) {
|
||||
const sorted = types.toSorted();
|
||||
let walk = this.$$tries;
|
||||
let i = 0;
|
||||
while (i < sorted.length) {
|
||||
if (!walk.children[sorted[i]]) {
|
||||
walk.children[sorted[i]] = new Node();
|
||||
}
|
||||
walk = walk.children[sorted[i]];
|
||||
i += 1;
|
||||
}
|
||||
if (!walk.class) {
|
||||
class Entity {
|
||||
dirty = true;
|
||||
static types = sorted;
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
const properties = {};
|
||||
for (const type of sorted) {
|
||||
properties[type] = {};
|
||||
const get = Components[type].get.bind(Components[type]);
|
||||
properties[type].get = function() {
|
||||
return get(this.id);
|
||||
};
|
||||
}
|
||||
Object.defineProperties(Entity.prototype, properties);
|
||||
Entity.prototype.toJSON = new Function('', `
|
||||
return {
|
||||
${sorted.map((type) => `${type}: this.${type}`).join(', ')}
|
||||
};
|
||||
`);
|
||||
walk.class = Entity;
|
||||
}
|
||||
return walk.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
/* v8 ignore start */
|
||||
export {default as Ecs} from './ecs.js';
|
||||
export {default as System} from './system.js';
|
118
src/ecs/query.js
118
src/ecs/query.js
|
@ -1,118 +0,0 @@
|
|||
import BaseComponent from './component/base.js';
|
||||
|
||||
export default class Query {
|
||||
|
||||
$$compiled = {with: [], without: []};
|
||||
|
||||
$$index = new Set();
|
||||
|
||||
constructor(parameters, ComponentsAndOrBundles) {
|
||||
for (let i = 0; i < parameters.length; ++i) {
|
||||
const parameter = parameters[i];
|
||||
switch (parameter.charCodeAt(0)) {
|
||||
case '!'.charCodeAt(0):
|
||||
this.$$compiled.without.push(ComponentsAndOrBundles[parameter.slice(1)]);
|
||||
break;
|
||||
default:
|
||||
this.$$compiled.with.push(ComponentsAndOrBundles[parameter]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.$$index.size;
|
||||
}
|
||||
|
||||
deindex(entities) {
|
||||
for (let i = 0; i < entities.length; ++i) {
|
||||
this.$$index.delete(entities[i]);
|
||||
}
|
||||
}
|
||||
|
||||
reindex(entities) {
|
||||
if (0 === this.$$compiled.with.length && 0 === this.$$compiled.without.length) {
|
||||
for (const entity of entities) {
|
||||
this.$$index.add(entity);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const entity of entities) {
|
||||
let should = true;
|
||||
withCheck: for (let j = 0; j < this.$$compiled.with.length; ++j) {
|
||||
const ComponentOrBundle = this.$$compiled.with[j];
|
||||
if (ComponentOrBundle instanceof BaseComponent) {
|
||||
if ('undefined' === typeof ComponentOrBundle.get(entity)) {
|
||||
should = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const k in ComponentOrBundle.Components) {
|
||||
if ('undefined' === typeof ComponentOrBundle.Components[k].get(entity)) {
|
||||
should = false;
|
||||
break withCheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (should) {
|
||||
for (let j = 0; j < this.$$compiled.without.length; ++j) {
|
||||
const ComponentOrBundle = this.$$compiled.without[j];
|
||||
if (ComponentOrBundle instanceof BaseComponent) {
|
||||
if ('undefined' !== typeof ComponentOrBundle.get(entity)) {
|
||||
should = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
let shouldLocal = false;
|
||||
for (const k in ComponentOrBundle.Components) {
|
||||
if ('undefined' === typeof ComponentOrBundle.Components[k].get(entity)) {
|
||||
shouldLocal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!shouldLocal) {
|
||||
should = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (should) {
|
||||
this.$$index.add(entity);
|
||||
}
|
||||
else if (!should) {
|
||||
this.$$index.delete(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select() {
|
||||
const it = this.$$index.values();
|
||||
const value = [];
|
||||
return {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
const result = it.next();
|
||||
if (result.done) {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
for (let i = 0; i < this.$$compiled.with.length; ++i) {
|
||||
if (this.$$compiled.with[i] instanceof BaseComponent) {
|
||||
value[i] = this.$$compiled.with[i].get(result.value);
|
||||
}
|
||||
else {
|
||||
value[i] = this.$$compiled.with[i].select(result.value);
|
||||
}
|
||||
}
|
||||
value[this.$$compiled.with.length] = result.value;
|
||||
return {done: false, value};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Bundle from './bundle.js';
|
||||
import {createComponent} from './component.js';
|
||||
import Query from './query.js';
|
||||
|
||||
const A = createComponent({a: {type: 'int32', defaultValue: 420}});
|
||||
const B = createComponent({b: {type: 'int32', defaultValue: 69}});
|
||||
const C = createComponent({c: 'int32'});
|
||||
const D = Bundle.normalize(['B', 'C']);
|
||||
class E extends Bundle {
|
||||
|
||||
static Components = ['A', 'B'];
|
||||
|
||||
static select(entity, Components) {
|
||||
const {A: {a}, B: {b}} = super.select(entity, Components);
|
||||
return {a, b};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ComponentsAndOrBundles = {
|
||||
A: new A(),
|
||||
B: new B(),
|
||||
C: new C(),
|
||||
};
|
||||
ComponentsAndOrBundles.A.createMany([[2], [3]]);
|
||||
ComponentsAndOrBundles.B.createMany([[1], [2]]);
|
||||
ComponentsAndOrBundles.C.createMany([[2], [4]]);
|
||||
ComponentsAndOrBundles.D = new D([ComponentsAndOrBundles.B, ComponentsAndOrBundles.C]);
|
||||
ComponentsAndOrBundles.E = new E([ComponentsAndOrBundles.A, ComponentsAndOrBundles.B]);
|
||||
|
||||
function testQuery(parameters, expected) {
|
||||
const query = new Query(parameters, ComponentsAndOrBundles);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(expected.length);
|
||||
for (const _ of query.select()) {
|
||||
expect(_.length)
|
||||
.to.equal(parameters.filter((spec) => '!'.charCodeAt(0) !== spec.charCodeAt(0)).length + 1);
|
||||
expect(expected.includes(_.pop()))
|
||||
.to.equal(true);
|
||||
}
|
||||
}
|
||||
|
||||
test('can query all', () => {
|
||||
testQuery([], [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('can query some', () => {
|
||||
testQuery(['A'], [2, 3]);
|
||||
testQuery(['A', 'B'], [2]);
|
||||
});
|
||||
|
||||
test('can query excluding', () => {
|
||||
testQuery(['!A'], [1]);
|
||||
testQuery(['A', '!B'], [3]);
|
||||
});
|
||||
|
||||
test('can query bundles', () => {
|
||||
testQuery(['D'], [2]);
|
||||
});
|
||||
|
||||
test('can query excluding bundles', () => {
|
||||
testQuery(['!D'], [1, 3]);
|
||||
});
|
||||
|
||||
test('can deindex', () => {
|
||||
const query = new Query(['A'], ComponentsAndOrBundles);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(2);
|
||||
query.deindex([2]);
|
||||
expect(query.count)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('can reindex', () => {
|
||||
const Test = new (createComponent({a: {type: 'int32', defaultValue: 420}}))();
|
||||
Test.createMany([[2], [3]]);
|
||||
const query = new Query(['Test'], {Test});
|
||||
query.reindex([2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(2);
|
||||
Test.destroy(2);
|
||||
query.reindex([2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('can select', () => {
|
||||
const query = new Query(['A'], ComponentsAndOrBundles);
|
||||
query.reindex([1, 2, 3]);
|
||||
const it = query.select();
|
||||
const result = it.next();
|
||||
expect(result.value[0].a)
|
||||
.to.equal(420);
|
||||
});
|
||||
|
||||
test('can select bundles', () => {
|
||||
const query = new Query(['D'], ComponentsAndOrBundles);
|
||||
query.reindex([1, 2, 3]);
|
||||
const it = query.select();
|
||||
const result = it.next();
|
||||
expect(result.value[0].B.b)
|
||||
.to.equal(69);
|
||||
});
|
||||
|
||||
test('can select configured bundles', () => {
|
||||
const query = new Query(['E'], ComponentsAndOrBundles);
|
||||
query.reindex([1, 2, 3]);
|
||||
const it = query.select();
|
||||
const result = it.next();
|
||||
expect(result.value[0])
|
||||
.to.deep.equal({a: 420, b: 69});
|
||||
});
|
|
@ -1,153 +0,0 @@
|
|||
|
||||
export default class Schema {
|
||||
|
||||
defaultValues = {};
|
||||
|
||||
$$size = 0;
|
||||
|
||||
specification;
|
||||
|
||||
constructor(specification) {
|
||||
this.specification = this.constructor.normalize(specification);
|
||||
// Try to calculate static size.
|
||||
for (const i in this.specification) {
|
||||
const {type} = this.specification[i];
|
||||
const size = this.constructor.sizeOfType(type);
|
||||
if (0 === size) {
|
||||
this.$$size = 0;
|
||||
break;
|
||||
}
|
||||
this.$$size += size;
|
||||
}
|
||||
for (const i in this.specification) {
|
||||
const {type} = this.specification[i];
|
||||
this.defaultValues[i] = this.specification[i].defaultValue;
|
||||
if ('undefined' === typeof this.defaultValues[i] && 'string' === type) {
|
||||
this.defaultValues[i] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
const keys = Object.keys(this.specification);
|
||||
return {
|
||||
next: () => {
|
||||
if (0 === keys.length) {
|
||||
return {done: true};
|
||||
}
|
||||
const key = keys.shift();
|
||||
return {
|
||||
done: false,
|
||||
value: [key, this.specification[key]],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return key in this.specification;
|
||||
}
|
||||
|
||||
static normalize(specification) {
|
||||
const normalized = Object.create(null);
|
||||
for (const i in specification) {
|
||||
normalized[i] = 'string' === typeof specification[i]
|
||||
? {type: specification[i]}
|
||||
: specification[i];
|
||||
if (!this.validateType(normalized[i].type)) {
|
||||
throw new TypeError(`unknown schema type: ${normalized[i].type}`);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
readSize(view, cursor) {
|
||||
let fullSize = 0;
|
||||
for (const i in this.specification) {
|
||||
const {type} = this.specification[i];
|
||||
const size = this.constructor.sizeOfType(type);
|
||||
if (0 === size) {
|
||||
switch (type) {
|
||||
case 'string': {
|
||||
const length = view.getUint32(cursor, true);
|
||||
cursor += 4 + length;
|
||||
fullSize += 4;
|
||||
fullSize += length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
cursor += size;
|
||||
fullSize += size;
|
||||
}
|
||||
}
|
||||
return fullSize;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.$$size;
|
||||
}
|
||||
|
||||
sizeOf(instance) {
|
||||
let fullSize = 0;
|
||||
for (const i in this.specification) {
|
||||
const {type} = this.specification[i];
|
||||
const size = this.constructor.sizeOfType(type);
|
||||
if (0 === size) {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
fullSize += 4;
|
||||
fullSize += instance[i].length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
fullSize += size;
|
||||
}
|
||||
}
|
||||
return fullSize;
|
||||
}
|
||||
|
||||
static sizeOfType(type) {
|
||||
switch (type) {
|
||||
case 'uint8': case 'int8': return 1;
|
||||
case 'uint16': case 'int16': return 2;
|
||||
case 'uint32': case 'int32': case 'float32': return 4;
|
||||
case 'float64': case 'int64': case 'uint64': return 8;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static validateType(type) {
|
||||
return [
|
||||
'float32', 'float64',
|
||||
'int8', 'int16', 'int32', 'int64',
|
||||
'string',
|
||||
'uint8', 'uint16', 'uint32', 'uint64',
|
||||
]
|
||||
.includes(type);
|
||||
}
|
||||
|
||||
static viewMethodFromType(type) {
|
||||
const capitalizedType = `${type.slice(0, 1).toUpperCase()}${type.slice(1)}`;
|
||||
switch (type) {
|
||||
case 'uint8':
|
||||
case 'int8':
|
||||
case 'uint16':
|
||||
case 'int16':
|
||||
case 'uint32':
|
||||
case 'int32':
|
||||
case 'float32':
|
||||
case 'float64': {
|
||||
return capitalizedType;
|
||||
}
|
||||
case 'int64':
|
||||
case 'uint64': {
|
||||
return `Big${capitalizedType}`;
|
||||
}
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Schema from './schema.js';
|
||||
|
||||
test('validates a schema', () => {
|
||||
expect(() => {
|
||||
new Schema({test: 'unknown'})
|
||||
})
|
||||
.to.throw();
|
||||
expect(() => {
|
||||
new Schema({test: 'unknown'})
|
||||
})
|
||||
.to.throw();
|
||||
|
||||
});
|
||||
|
||||
test('calculates the size of an instance', () => {
|
||||
expect((new Schema({foo: 'uint8', bar: 'uint32'})).sizeOf({foo: 69, bar: 420}))
|
||||
.to.equal(5);
|
||||
expect((new Schema({foo: 'string'})).sizeOf({foo: 'hi'}))
|
||||
.to.equal(4 + 2);
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
import Schema from './schema.js';
|
||||
|
||||
export default class Serializer {
|
||||
|
||||
constructor(schema) {
|
||||
this.schema = schema instanceof Schema ? schema : new Schema(schema);
|
||||
}
|
||||
|
||||
decode(view, destination, offset = 0) {
|
||||
let cursor = offset;
|
||||
for (const [key, {type}] of this.schema) {
|
||||
const viewMethod = Schema.viewMethodFromType(type);
|
||||
let value;
|
||||
if (viewMethod) {
|
||||
value = view[`get${viewMethod}`](cursor, true);
|
||||
cursor += Schema.sizeOfType(type);
|
||||
}
|
||||
else {
|
||||
switch (type) {
|
||||
case 'string': {
|
||||
const length = view.getUint32(cursor, true);
|
||||
cursor += 4;
|
||||
const {buffer, byteOffset} = view;
|
||||
const decoder = new TextDecoder();
|
||||
value = decoder.decode(new DataView(buffer, byteOffset + cursor, length));
|
||||
cursor += length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
destination[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
encode(source, view, offset = 0) {
|
||||
let cursor = offset;
|
||||
for (const [key, {type}] of this.schema) {
|
||||
const viewMethod = Schema.viewMethodFromType(type);
|
||||
if (viewMethod) {
|
||||
view[`set${viewMethod}`](cursor, source[key], true);
|
||||
cursor += Schema.sizeOfType(type);
|
||||
}
|
||||
else {
|
||||
switch (type) {
|
||||
case 'string': {
|
||||
const lengthOffset = cursor;
|
||||
cursor += 4;
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(source[key]);
|
||||
for (let i = 0; i < bytes.length; ++i) {
|
||||
view.setUint8(cursor++, bytes[i]);
|
||||
}
|
||||
view.setUint32(lengthOffset, bytes.length, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Serializer from './serializer.js';
|
||||
|
||||
test('can encode and decode', () => {
|
||||
const entries = [
|
||||
['uint8', 255],
|
||||
['int8', -128],
|
||||
['int8', 127],
|
||||
['uint16', 65535],
|
||||
['int16', -32768],
|
||||
['int16', 32767],
|
||||
['uint32', 4294967295],
|
||||
['int32', -2147483648],
|
||||
['int32', 2147483647],
|
||||
['uint64', 18446744073709551615n],
|
||||
['int64', -9223372036854775808n],
|
||||
['int64', 9223372036854775807n],
|
||||
['float32', 0.5],
|
||||
['float64', 1.234],
|
||||
['string', 'hello world'],
|
||||
];
|
||||
const schema = entries.reduce((r, [type]) => ({...r, [Object.keys(r).length]: type}), {});
|
||||
const data = entries.reduce((r, [, value]) => ({...r, [Object.keys(r).length]: value}), {});
|
||||
const serializer = new Serializer(schema);
|
||||
const view = new DataView(new ArrayBuffer(serializer.schema.sizeOf(data)));
|
||||
serializer.encode(data, view);
|
||||
const result = {};
|
||||
serializer.decode(view, result);
|
||||
expect(data)
|
||||
.to.deep.equal(result);
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
|
||||
import Query from './query.js';
|
||||
|
||||
export default class System {
|
||||
|
||||
destroying = [];
|
||||
|
||||
queries = {};
|
||||
|
||||
constructor(Components) {
|
||||
const queries = this.constructor.queries();
|
||||
for (const i in queries) {
|
||||
this.queries[i] = new Query(queries[i], Components);
|
||||
}
|
||||
}
|
||||
|
||||
deindex(entities) {
|
||||
for (const i in this.queries) {
|
||||
this.queries[i].deindex(entities);
|
||||
}
|
||||
}
|
||||
|
||||
destroyEntity(entity) {
|
||||
this.destroyManyEntities([entity]);
|
||||
}
|
||||
|
||||
destroyManyEntities(entities) {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
this.destroying.push(entities[i]);
|
||||
}
|
||||
}
|
||||
|
||||
finalize() {}
|
||||
|
||||
static normalize(SystemLike) {
|
||||
if (SystemLike.prototype instanceof System) {
|
||||
return SystemLike;
|
||||
}
|
||||
if ('function' === typeof SystemLike) {
|
||||
class TickingSystem extends System {}
|
||||
TickingSystem.prototype.tick = SystemLike;
|
||||
return TickingSystem;
|
||||
}
|
||||
/* v8 ignore next */
|
||||
throw new TypeError(`Couldn't normalize '${SystemLike}' as a system`);
|
||||
}
|
||||
|
||||
static queries() {
|
||||
return {};
|
||||
}
|
||||
|
||||
reindex(entities) {
|
||||
for (const i in this.queries) {
|
||||
this.queries[i].reindex(entities);
|
||||
}
|
||||
}
|
||||
|
||||
select(query) {
|
||||
return this.queries[query].select();
|
||||
}
|
||||
|
||||
tickDestruction() {
|
||||
this.deindex(this.destroying);
|
||||
this.destroying = [];
|
||||
}
|
||||
|
||||
tick() {}
|
||||
|
||||
}
|
|
@ -3,8 +3,6 @@ import {createRoot} from 'react-dom/client';
|
|||
|
||||
import Silphius from './components/silphius.jsx';
|
||||
|
||||
await import('./isomorphinit.js');
|
||||
|
||||
// Setup DOM.
|
||||
createRoot(document.querySelector('.silphius'))
|
||||
.render(createElement(Silphius));
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
// Gathering.
|
||||
const {default: PacketClass} = await import('./net/packet/packet.js');
|
||||
Object.values(await import('./net/packet/packets.js'))
|
||||
.forEach((Packet) => {
|
||||
PacketClass.register(Packet);
|
||||
});
|
||||
PacketClass.mapRegistered();
|
||||
|
||||
const {default: Ecs} = await import('./ecs/ecs.js');
|
||||
Ecs.ComponentLikesAndOrBundleLikes = Object.fromEntries(
|
||||
Object.entries(
|
||||
await import('./ecs/components.js'),
|
||||
),
|
||||
);
|
|
@ -1,33 +0,0 @@
|
|||
import Packet from './packet.js';
|
||||
|
||||
const WIRE_MAP = {
|
||||
'moveUp': 0,
|
||||
'moveRight': 1,
|
||||
'moveDown': 2,
|
||||
'moveLeft': 3,
|
||||
};
|
||||
Object.entries(WIRE_MAP)
|
||||
.forEach(([k, v]) => {
|
||||
WIRE_MAP[v] = k;
|
||||
});
|
||||
|
||||
export default class Action extends Packet {
|
||||
|
||||
static type = 'action';
|
||||
|
||||
static pack(payload) {
|
||||
return super.pack({
|
||||
type: WIRE_MAP[payload.type],
|
||||
value: payload.value,
|
||||
});
|
||||
}
|
||||
|
||||
static unpack(packed) {
|
||||
const unpacked = super.unpack(packed);
|
||||
return {
|
||||
type: WIRE_MAP[unpacked.type],
|
||||
value: unpacked.value,
|
||||
};
|
||||
}
|
||||
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
import Packet from './packet.js';
|
||||
|
||||
export default class Connect extends Packet {
|
||||
|
||||
static type = 'connect';
|
||||
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
import Packet from './packet.js';
|
||||
|
||||
export default class Connected extends Packet {
|
||||
|
||||
static type = 'connected';
|
||||
|
||||
};
|
|
@ -1,68 +0,0 @@
|
|||
import {encode, decode} from '@msgpack/msgpack';
|
||||
|
||||
export default class Packet {
|
||||
|
||||
static registered = {};
|
||||
|
||||
constructor(payload = {}) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
static accept(packed) {
|
||||
const decoded = decode(packed);
|
||||
const Packet = this.registered[decoded.id];
|
||||
return {
|
||||
type: Packet.type,
|
||||
payload: Packet.unpack(decoded),
|
||||
};
|
||||
}
|
||||
|
||||
static decode(buffer) {
|
||||
try {
|
||||
return this.unpack(decode(buffer));
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`decoding ${this.type}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
static encode(payload) {
|
||||
try {
|
||||
return encode(this.pack(payload));
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`encoding ${this.type}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
static mapRegistered() {
|
||||
this.registered = Object.fromEntries([
|
||||
...Object.entries(this.registered),
|
||||
...Object.entries(this.registered)
|
||||
.map(([, Packet], id) => {
|
||||
Packet.id = id;
|
||||
return [id, Packet];
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
static pack(payload) {
|
||||
return {
|
||||
id: this.id,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
static register(Packet) {
|
||||
this.registered[Packet.type] = Packet;
|
||||
}
|
||||
|
||||
static transmit({type, payload}) {
|
||||
return this.registered[type].encode(payload);
|
||||
}
|
||||
|
||||
static unpack({payload}) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export {default as Action} from './action.js';
|
||||
export {default as Connect} from './connect.js';
|
||||
export {default as Connected} from './connected.js';
|
||||
export {default as Tick} from './tick.js';
|
|
@ -1,7 +0,0 @@
|
|||
import Packet from './packet.js';
|
||||
|
||||
export default class Tick extends Packet {
|
||||
|
||||
static type = 'tick';
|
||||
|
||||
};
|
|
@ -1,117 +0,0 @@
|
|||
import {MOVE_MAP} from '../../constants.js';
|
||||
import Cell from '../../cell.js';
|
||||
import {System} from '../../ecs/index.js';
|
||||
import Packet from "../packet/packet.js";
|
||||
|
||||
const SPEED = 100;
|
||||
const TPS = 60;
|
||||
|
||||
await import ('../../isomorphinit.js');
|
||||
|
||||
class MovementSystem extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Position', 'Controlled'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const [position, controlled] of this.select('default')) {
|
||||
position.x += SPEED * elapsed * (controlled.right - controlled.left);
|
||||
position.y += SPEED * elapsed * (controlled.down - controlled.up);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class Server {
|
||||
|
||||
constructor() {
|
||||
this.cells = [new Cell()];
|
||||
this.cells[0].ecs.addSystem(MovementSystem);
|
||||
this.connections = [];
|
||||
this.connectedPlayers = new Map();
|
||||
this.frame = 0;
|
||||
this.last = Date.now();
|
||||
}
|
||||
|
||||
accept(connection, packed) {
|
||||
const decoded = Packet.accept(packed);
|
||||
const {payload, type} = decoded;
|
||||
switch (type) {
|
||||
case 'connect': {
|
||||
this.send(
|
||||
connection,
|
||||
{
|
||||
type: 'connected',
|
||||
payload: [],
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
const {ecs} = this.cells[0];
|
||||
const connectedPlayer = this.connectedPlayers.get(connection);
|
||||
if (payload.type in MOVE_MAP) {
|
||||
ecs.get(connectedPlayer).Controlled[MOVE_MAP[payload.type]] = payload.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
connectPlayer(connection) {
|
||||
this.connections.push(connection);
|
||||
const {ecs} = this.cells[0];
|
||||
const entity = ecs.create({
|
||||
Controlled: {up: 0, right: 0, down: 0, left: 0},
|
||||
Position: {x: 50, y: 50},
|
||||
Visible: {image: './assets/bunny.png'},
|
||||
})
|
||||
this.connectedPlayers.set(connection, entity);
|
||||
}
|
||||
|
||||
disconnectPlayer(connection) {
|
||||
this.connectedPlayers.delete(connection);
|
||||
this.connections.splice(this.connections.indexOf(connection), 1);
|
||||
}
|
||||
|
||||
async load() {
|
||||
}
|
||||
|
||||
send(connection, packet) {
|
||||
this.transmit(connection, Packet.transmit(packet));
|
||||
}
|
||||
|
||||
start() {
|
||||
return setInterval(() => {
|
||||
const elapsed = (Date.now() - this.last) / 1000;
|
||||
this.last = Date.now();
|
||||
this.tick(elapsed);
|
||||
}, 1000 / TPS);
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
this.cells[0].ecs.tick(elapsed);
|
||||
const {ecs} = this.cells[0];
|
||||
const view = new DataView(new ArrayBuffer(ecs.sizeOf(ecs.entities)));
|
||||
ecs.encode(ecs.entities, view);
|
||||
for (const connection of this.connections) {
|
||||
this.send(
|
||||
connection,
|
||||
{
|
||||
type: 'tick',
|
||||
payload: {
|
||||
entities: view,
|
||||
elapsed,
|
||||
frame: this.frame,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
this.frame += 1;
|
||||
}
|
||||
|
||||
}
|
79
src/server/server.js
Normal file
79
src/server/server.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const SPEED = 100;
|
||||
const TPS = 20;
|
||||
|
||||
const MOVE_MAP = {
|
||||
'moveUp': 0,
|
||||
'moveRight': 1,
|
||||
'moveDown': 2,
|
||||
'moveLeft': 3,
|
||||
};
|
||||
|
||||
export default class Server {
|
||||
|
||||
constructor() {
|
||||
this.connections = [];
|
||||
this.connectedPlayers = new Map();
|
||||
this.entities = [];
|
||||
this.frame = 0;
|
||||
this.last = Date.now();
|
||||
}
|
||||
|
||||
accept(connection, {type, payload}) {
|
||||
switch (type) {
|
||||
case 'connect': {
|
||||
this.send(connection, {type: 'connected', payload: Array.from(this.connectedPlayers.values())});
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
const connectedPlayer = this.connectedPlayers.get(connection);
|
||||
if (payload.type in MOVE_MAP) {
|
||||
connectedPlayer.movement[MOVE_MAP[payload.type]] = payload.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
connectPlayer(connection) {
|
||||
this.connections.push(connection);
|
||||
const entity = {
|
||||
image: './assets/bunny.png',
|
||||
movement: [0, 0, 0, 0],
|
||||
position: [50, 50],
|
||||
};
|
||||
this.entities.push(entity);
|
||||
this.connectedPlayers.set(connection, entity);
|
||||
}
|
||||
|
||||
start() {
|
||||
return setInterval(() => {
|
||||
const elapsed = (Date.now() - this.last) / 1000;
|
||||
this.last = Date.now();
|
||||
this.tick(elapsed);
|
||||
}, 1000 / TPS);
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const connection of this.connections) {
|
||||
const {movement, position} = this.connectedPlayers.get(connection);
|
||||
position[0] += SPEED * elapsed * (movement[1] - movement[3]);
|
||||
position[1] += SPEED * elapsed * (movement[2] - movement[0]);
|
||||
}
|
||||
for (const connection of this.connections) {
|
||||
this.send(
|
||||
connection,
|
||||
{
|
||||
type: 'tick',
|
||||
payload: {
|
||||
entities: this.entities,
|
||||
elapsed,
|
||||
frame: this.frame,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
this.frame += 1;
|
||||
}
|
||||
|
||||
}
|
|
@ -9,18 +9,14 @@ const {
|
|||
const wss = new WebSocketServer({port: SILPHIUS_WEBSOCKET_PORT});
|
||||
|
||||
const server = new class SocketServer extends Server {
|
||||
transmit(ws, packed) { ws.send(packed); }
|
||||
send(ws, data) { ws.send(JSON.stringify(data)); }
|
||||
}
|
||||
|
||||
await server.load();
|
||||
server.start();
|
||||
|
||||
wss.on('connection', function connection(ws) {
|
||||
server.connectPlayer(ws);
|
||||
ws.on('close', () => {
|
||||
server.disconnectPlayer(ws);
|
||||
})
|
||||
ws.on('message', (packed) => {
|
||||
server.accept(ws, packed);
|
||||
ws.on('message', function message(data) {
|
||||
server.accept(ws, JSON.parse(data));
|
||||
});
|
||||
});
|
|
@ -1,12 +1,11 @@
|
|||
import Server from './server.js';
|
||||
|
||||
const server = new class WorkerServer extends Server {
|
||||
transmit(connection, packed) { postMessage(packed); }
|
||||
send(connection, data) { postMessage(data); }
|
||||
}
|
||||
|
||||
await server.load();
|
||||
server.start();
|
||||
|
||||
server.connectPlayer(undefined);
|
||||
|
||||
onmessage = (event) => { server.accept(undefined, event.data); };
|
||||
onmessage = ({data}) => { server.accept(undefined, data); };
|
|
@ -1,7 +1,7 @@
|
|||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
import Dom from '../components/dom.jsx';
|
||||
import {RESOLUTION} from '../constants.js';
|
||||
import Dom from '../components/dom';
|
||||
import {RESOLUTION} from '../constants';
|
||||
|
||||
function Decorator({children, style}) {
|
||||
const ref = useRef();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import {useArgs} from '@storybook/preview-api';
|
||||
import {fn} from '@storybook/test';
|
||||
import {createElement} from 'react';
|
||||
|
||||
import potion from '../assets/potion.png';
|
||||
import Hotbar from '../components/hotbar.jsx';
|
||||
import DomDecorator from './dom-decorator.jsx';
|
||||
import Hotbar from '../components/hotbar';
|
||||
import Dom from '../components/dom';
|
||||
import DomDecorator from './dom-decorator';
|
||||
|
||||
const slots = Array(10).fill({});
|
||||
slots[2] = {image: potion, qty: 24};
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {createElement} from 'react';
|
||||
|
||||
import potion from '../assets/potion.png';
|
||||
import Slot from '../components/slot.jsx';
|
||||
import DomDecorator from './dom-decorator.jsx';
|
||||
import Slot from '../components/slot';
|
||||
import DomDecorator from './dom-decorator';
|
||||
|
||||
export default {
|
||||
title: 'Dom/Inventory/Slot',
|
||||
|
|
Loading…
Reference in New Issue
Block a user