Compare commits
No commits in common. "73082aad947129eea2c31917e43912f806079b57" and "56e5db68988d1abfa2570d8c2ec48a7b37fab502" have entirely different histories.
73082aad94
...
56e5db6898
|
@ -1,7 +1,3 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Animation extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
frame: {type: 'uint16'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class AreaSize extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
x: {type: 'uint16'},
|
||||
y: {type: 'uint16'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Camera extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
x: {type: 'uint16'},
|
||||
y: {type: 'uint16'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Controlled extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
locked: {type: 'uint8'},
|
||||
moveUp: {type: 'float32'},
|
||||
moveRight: {type: 'float32'},
|
||||
|
@ -9,4 +6,3 @@ export default class Controlled extends Component {
|
|||
moveLeft: {type: 'float32'},
|
||||
changeSlot: {type: 'int8'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Direction extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
direction: {type: 'uint8'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Ecs extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
path: {type: 'string'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default class Emitter extends Component {
|
||||
export default function(Component) {
|
||||
return class Emitter extends Component {
|
||||
mergeDiff(original, update) {
|
||||
const merged = {};
|
||||
if (update.emit) {
|
||||
|
@ -22,4 +23,9 @@ export default class Emitter extends Component {
|
|||
}
|
||||
};
|
||||
}
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,3 +1 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Engine extends Component {}
|
||||
export default {};
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Forces extends Component {
|
||||
static properties = {
|
||||
forceX: {type: 'float32'},
|
||||
forceY: {type: 'float32'},
|
||||
export default {
|
||||
impulseX: {type: 'float32'},
|
||||
impulseY: {type: 'float32'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Health extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
health: {type: 'uint32'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,14 +1,37 @@
|
|||
import Arbitrary from '@/ecs/arbitrary.js';
|
||||
import Schema from '@/ecs/schema.js';
|
||||
import gather from '@/util/gather.js';
|
||||
|
||||
const Gathered = gather(
|
||||
const specificationsAndOrDecorators = gather(
|
||||
import.meta.glob('./*.js', {eager: true, import: 'default'}),
|
||||
);
|
||||
|
||||
const Components = {};
|
||||
for (const componentName in Gathered) {
|
||||
Components[componentName] = class Named extends Gathered[componentName] {
|
||||
for (const componentName in specificationsAndOrDecorators) {
|
||||
// TODO: byKey, byId, ...
|
||||
if (Number.isInteger(+componentName)) {
|
||||
continue;
|
||||
}
|
||||
const specificationOrDecorator = specificationsAndOrDecorators[componentName];
|
||||
if ('function' === typeof specificationOrDecorator) {
|
||||
Components[componentName] = specificationOrDecorator(
|
||||
class Decorated extends Arbitrary {
|
||||
static componentName = componentName;
|
||||
};
|
||||
}
|
||||
);
|
||||
if (!Components[componentName]) {
|
||||
throw new Error(`Component ${componentName} decorator returned nothing`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Components[componentName] = class Component extends Arbitrary {
|
||||
static componentName = componentName;
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: specificationOrDecorator,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Components;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default class Inventory extends Component {
|
||||
export default function(Component) {
|
||||
return class Inventory extends Component {
|
||||
insertMany(entities) {
|
||||
for (const [id, {slotChange}] of entities) {
|
||||
if (slotChange) {
|
||||
|
@ -94,7 +95,9 @@ export default class Inventory extends Component {
|
|||
};
|
||||
return Instance;
|
||||
}
|
||||
static properties = {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
slots: {
|
||||
type: 'map',
|
||||
value: {
|
||||
|
@ -105,5 +108,7 @@ export default class Inventory extends Component {
|
|||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class MainEntity extends Component {}
|
||||
export default {};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default class Position extends Component {
|
||||
export default function(Component) {
|
||||
return class Wielder extends Component {
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
|
@ -17,8 +18,12 @@ export default class Position extends Component {
|
|||
});
|
||||
return Instance;
|
||||
}
|
||||
static properties = {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
x: {type: 'float32'},
|
||||
y: {type: 'float32'},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
1
app/ecs-components/rendered.js
Normal file
1
app/ecs-components/rendered.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default {};
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default class Sound extends Component {
|
||||
export default function(Component) {
|
||||
return class Sound extends Component {
|
||||
mergeDiff(original, update) {
|
||||
const merged = {};
|
||||
if (update.play) {
|
||||
|
@ -20,4 +21,9 @@ export default class Sound extends Component {
|
|||
}
|
||||
};
|
||||
}
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Speed extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
speed: {type: 'float32'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
import vector2d from "./helpers/vector-2d";
|
||||
|
||||
export default class Sprite extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
anchor: vector2d('float32', {x: 0.5, y: 0.5}),
|
||||
animation: {type: 'string'},
|
||||
elapsed: {type: 'float32'},
|
||||
|
@ -12,4 +8,3 @@ export default class Sprite extends Component {
|
|||
source: {type: 'string'},
|
||||
speed: {type: 'float32'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default class Ticking extends Component {
|
||||
export default function(Component) {
|
||||
return class Ticking extends Component {
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
|
||||
|
@ -33,7 +34,11 @@ export default class Ticking extends Component {
|
|||
}
|
||||
return Instance;
|
||||
}
|
||||
static properties = {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
isTicking: {defaultValue: 1, type: 'uint8'},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
import vector2d from './helpers/vector-2d';
|
||||
|
||||
export default class TileLayers extends Component {
|
||||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default function(Component) {
|
||||
return class TileLayers extends Component {
|
||||
insertMany(entities) {
|
||||
for (const [id, {layerChange}] of entities) {
|
||||
if (layerChange) {
|
||||
|
@ -85,7 +86,9 @@ export default class TileLayers extends Component {
|
|||
};
|
||||
return Instance;
|
||||
}
|
||||
static properties = {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
layers: {
|
||||
type: 'array',
|
||||
subtype: {
|
||||
|
@ -103,5 +106,7 @@ export default class TileLayers extends Component {
|
|||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
1
app/ecs-components/transient.js
Normal file
1
app/ecs-components/transient.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default {};
|
|
@ -1,10 +1,6 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class VisibleAabb extends Component {
|
||||
static properties = {
|
||||
export default {
|
||||
x0: {type: 'float32'},
|
||||
x1: {type: 'float32'},
|
||||
y0: {type: 'float32'},
|
||||
y1: {type: 'float32'},
|
||||
};
|
||||
}
|
||||
|
|
1
app/ecs-components/wandering.js
Normal file
1
app/ecs-components/wandering.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default {};
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import Schema from '@/ecs/schema.js';
|
||||
|
||||
export default class Wielder extends Component {
|
||||
export default function(Component) {
|
||||
return class Wielder extends Component {
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
|
@ -9,7 +10,6 @@ export default class Wielder extends Component {
|
|||
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;
|
||||
|
@ -52,12 +52,10 @@ export default class Wielder extends Component {
|
|||
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});
|
||||
projected.push({
|
||||
x: startX + parseInt(axe[0]),
|
||||
y: startY + parseInt(axe[1]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +63,11 @@ export default class Wielder extends Component {
|
|||
}
|
||||
return Instance;
|
||||
}
|
||||
static properties = {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
activeSlot: {type: 'uint16'},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class ApplyControlMovement extends System {
|
|||
};
|
||||
}
|
||||
tick() {
|
||||
for (const {Controlled, Forces, Speed} of this.select('default')) {
|
||||
for (const [Controlled, Forces, Speed] of this.select('default')) {
|
||||
if (!Controlled.locked) {
|
||||
Forces.impulseX += Speed.speed * (Controlled.moveRight - Controlled.moveLeft);
|
||||
Forces.impulseY += Speed.speed * (Controlled.moveDown - Controlled.moveUp);
|
||||
|
|
|
@ -9,9 +9,9 @@ export default class ApplyForces extends System {
|
|||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const {Position, Forces} of this.select('default')) {
|
||||
Position.x += elapsed * (Forces.impulseX + Forces.forceX);
|
||||
Position.y += elapsed * (Forces.impulseY + Forces.forceY);
|
||||
for (const [Position, Forces] of this.select('default')) {
|
||||
Position.x += elapsed * Forces.impulseX;
|
||||
Position.y += elapsed * Forces.impulseY;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,6 @@ import {System} from '@/ecs/index.js';
|
|||
|
||||
export default class CalculateAabbs extends System {
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'ApplyForces',
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const {Position: {x, y}, VisibleAabb} of this.ecs.changed(['Position'])) {
|
||||
if (VisibleAabb) {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import {RESOLUTION} from '@/constants.js'
|
||||
import {System} from '@/ecs/index.js';
|
||||
|
||||
const [hx, hy] = [RESOLUTION.x / 2, RESOLUTION.y / 2];
|
||||
|
||||
export default class FollowCamera extends System {
|
||||
|
||||
static queries() {
|
||||
|
@ -16,8 +19,8 @@ export default class FollowCamera extends System {
|
|||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const {id} of this.select('default')) {
|
||||
this.updateCamera(elapsed * 3, this.ecs.get(id));
|
||||
for (const [, , entityId] of this.select('default')) {
|
||||
this.updateCamera(elapsed * 3, this.ecs.get(entityId));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import {System} from '@/ecs/index.js';
|
|||
export default class ResetForces extends System {
|
||||
|
||||
static get priority() {
|
||||
return {phase: 'post'};
|
||||
return {phase: 'pre'};
|
||||
}
|
||||
|
||||
static queries() {
|
||||
|
@ -13,7 +13,7 @@ export default class ResetForces extends System {
|
|||
}
|
||||
|
||||
tick() {
|
||||
for (const {Forces} of this.select('default')) {
|
||||
for (const [Forces] of this.select('default')) {
|
||||
Forces.impulseX = 0;
|
||||
Forces.impulseY = 0;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export default class ControlMovement extends System {
|
|||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const {Sprite} of this.select('default')) {
|
||||
for (const [Sprite] of this.select('default')) {
|
||||
Sprite.elapsed += elapsed / Sprite.speed;
|
||||
while (Sprite.elapsed > 1) {
|
||||
Sprite.elapsed -= 1;
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class RunTickingPromises extends System {
|
|||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const {Ticking} of this.select('default')) {
|
||||
for (const [Ticking] of this.select('default')) {
|
||||
if (Ticking.isTicking) {
|
||||
Ticking.tick(elapsed);
|
||||
}
|
||||
|
|
|
@ -9,10 +9,11 @@ export default class SpriteDirection extends System {
|
|||
}
|
||||
|
||||
tick() {
|
||||
for (const {Controlled, Direction, Sprite} of this.select('default')) {
|
||||
for (const [Sprite, entityId] of this.select('default')) {
|
||||
const entity = this.ecs.get(entityId);
|
||||
const parts = [];
|
||||
if (Controlled) {
|
||||
const {locked, moveUp, moveRight, moveDown, moveLeft} = Controlled;
|
||||
if (entity.Controlled) {
|
||||
const {locked, moveUp, moveRight, moveDown, moveLeft} = entity.Controlled;
|
||||
if (locked) {
|
||||
continue;
|
||||
}
|
||||
|
@ -23,14 +24,14 @@ export default class SpriteDirection extends System {
|
|||
parts.push('idle');
|
||||
}
|
||||
}
|
||||
if (Direction) {
|
||||
if (entity.Direction) {
|
||||
const name = {
|
||||
0: 'up',
|
||||
1: 'right',
|
||||
2: 'down',
|
||||
3: 'left',
|
||||
};
|
||||
parts.push(name[Direction.direction]);
|
||||
parts.push(name[entity.Direction.direction]);
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
Sprite.animation = parts.join(':');
|
||||
|
|
|
@ -54,12 +54,6 @@ class SpatialHash {
|
|||
|
||||
export default class UpdateSpatialHash extends System {
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'CalculateAabbs',
|
||||
};
|
||||
}
|
||||
|
||||
deindex(entities) {
|
||||
super.deindex(entities);
|
||||
for (const id of entities) {
|
||||
|
|
109
app/ecs/arbitrary.js
Normal file
109
app/ecs/arbitrary.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import Base from './base.js';
|
||||
|
||||
export default class Arbitrary extends Base {
|
||||
|
||||
data = [];
|
||||
|
||||
serializer;
|
||||
|
||||
Instance;
|
||||
|
||||
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 keys = Object.keys(this.constructor.properties);
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [entityId, values = {}] = entries[i];
|
||||
this.map[entityId] = allocated[i];
|
||||
this.data[allocated[i]].entity = entityId;
|
||||
if (false === values) {
|
||||
continue;
|
||||
}
|
||||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
const {defaultValue} = this.constructor.properties[j];
|
||||
if (j in values) {
|
||||
this.data[allocated[i]][j] = values[j];
|
||||
}
|
||||
else if ('undefined' !== typeof defaultValue) {
|
||||
this.data[allocated[i]][j] = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserialize(entityId, view, offset) {
|
||||
const {properties} = this.constructor;
|
||||
const instance = this.get(entityId);
|
||||
const deserialized = this.constructor.schema.deserialize(view, offset);
|
||||
for (const key in properties) {
|
||||
instance[key] = deserialized[key];
|
||||
}
|
||||
}
|
||||
|
||||
serialize(entityId, view, offset) {
|
||||
this.constructor.schema.serialize(this.get(entityId), view, offset);
|
||||
}
|
||||
|
||||
get(entityId) {
|
||||
return this.data[this.map[entityId]];
|
||||
}
|
||||
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const Instance = class {
|
||||
$$entity = 0;
|
||||
constructor() {
|
||||
this.$$reset();
|
||||
}
|
||||
$$reset() {
|
||||
const {properties} = Component.constructor;
|
||||
for (const key in properties) {
|
||||
const {defaultValue} = properties[key];
|
||||
this[`$$${key}`] = defaultValue;
|
||||
}
|
||||
}
|
||||
toJSON() {
|
||||
return Component.constructor.filterDefaults(this);
|
||||
}
|
||||
};
|
||||
const properties = {};
|
||||
properties.entity = {
|
||||
get: function get() {
|
||||
return this.$$entity;
|
||||
},
|
||||
set: function set(v) {
|
||||
this.$$entity = v;
|
||||
this.$$reset();
|
||||
},
|
||||
};
|
||||
for (const key in Component.constructor.properties) {
|
||||
properties[key] = {
|
||||
get: function get() {
|
||||
return this[`$$${key}`];
|
||||
},
|
||||
set: function set(value) {
|
||||
if (this[`$$${key}`] !== value) {
|
||||
this[`$$${key}`] = value;
|
||||
Component.markChange(this.entity, key, value);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Object.defineProperties(Instance.prototype, properties);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
}
|
56
app/ecs/arbitrary.test.js
Normal file
56
app/ecs/arbitrary.test.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Schema from './schema.js';
|
||||
import Arbitrary from './arbitrary.js';
|
||||
|
||||
test('creates instances', () => {
|
||||
class CreatingArbitrary extends Arbitrary {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {foo: {defaultValue: 'bar', type: 'string'}},
|
||||
});
|
||||
}
|
||||
const Component = new CreatingArbitrary();
|
||||
Component.create(1);
|
||||
expect(Component.get(1).entity)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('does not serialize default values', () => {
|
||||
class CreatingArbitrary extends Arbitrary {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'}},
|
||||
});
|
||||
}
|
||||
const fakeEcs = {markChange() {}};
|
||||
const Component = new CreatingArbitrary(fakeEcs);
|
||||
Component.create(1)
|
||||
expect(Component.get(1).toJSON())
|
||||
.to.deep.equal({});
|
||||
Component.get(1).bar = 1;
|
||||
expect(Component.get(1).toJSON())
|
||||
.to.deep.equal({bar: 1});
|
||||
});
|
||||
|
||||
test('reuses instances', () => {
|
||||
class ReusingArbitrary extends Arbitrary {
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {foo: {type: 'string'}},
|
||||
});
|
||||
}
|
||||
const Component = new ReusingArbitrary();
|
||||
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);
|
||||
});
|
101
app/ecs/base.js
Normal file
101
app/ecs/base.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import Schema from './schema.js';
|
||||
|
||||
export default class Base {
|
||||
|
||||
ecs;
|
||||
|
||||
map = {};
|
||||
|
||||
pool = [];
|
||||
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
});
|
||||
|
||||
constructor(ecs) {
|
||||
this.ecs = ecs;
|
||||
}
|
||||
|
||||
allocateMany(count) {
|
||||
const results = [];
|
||||
while (count-- > 0 && this.pool.length > 0) {
|
||||
results.push(this.pool.pop());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
create(entityId, values) {
|
||||
this.createMany([[entityId, values]]);
|
||||
}
|
||||
|
||||
destroy(entityId) {
|
||||
this.destroyMany([entityId]);
|
||||
}
|
||||
|
||||
destroyMany(entities) {
|
||||
this.freeMany(
|
||||
entities
|
||||
.map((entityId) => {
|
||||
if ('undefined' !== typeof this.map[entityId]) {
|
||||
return this.map[entityId];
|
||||
}
|
||||
throw new Error(`can't free for non-existent id ${entityId}`);
|
||||
}),
|
||||
);
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
this.map[entities[i]] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static filterDefaults(instance) {
|
||||
const json = {};
|
||||
for (const key in this.properties) {
|
||||
const {defaultValue} = this.properties[key];
|
||||
if (key in instance && instance[key] !== defaultValue) {
|
||||
json[key] = instance[key];
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
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 [entityId, values] = entities[i];
|
||||
if (!this.get(entityId)) {
|
||||
creating.push([entityId, values]);
|
||||
}
|
||||
else {
|
||||
const instance = this.get(entityId);
|
||||
for (const i in values) {
|
||||
instance[i] = values[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.createMany(creating);
|
||||
}
|
||||
|
||||
markChange(entityId, key, value) {
|
||||
this.ecs.markChange(entityId, {[this.constructor.componentName]: {[key]: value}})
|
||||
}
|
||||
|
||||
mergeDiff(original, update) {
|
||||
return {...original, ...update};
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return this.schema.specification.properties;
|
||||
}
|
||||
|
||||
sizeOf(entityId) {
|
||||
return this.constructor.schema.sizeOf(this.get(entityId));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
import Schema from './schema.js';
|
||||
|
||||
export default class Component {
|
||||
|
||||
data = [];
|
||||
ecs;
|
||||
Instance;
|
||||
map = {};
|
||||
pool = [];
|
||||
static properties = {};
|
||||
static $$schema;
|
||||
|
||||
constructor(ecs) {
|
||||
this.ecs = ecs;
|
||||
this.Instance = this.instanceFromSchema();
|
||||
}
|
||||
|
||||
allocateMany(count) {
|
||||
const results = [];
|
||||
while (count > 0) {
|
||||
results.push(
|
||||
this.pool.length > 0
|
||||
? this.pool.pop()
|
||||
: this.data.push(new this.Instance()) - 1,
|
||||
)
|
||||
count -= 1;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
create(entityId, values) {
|
||||
this.createMany([[entityId, values]]);
|
||||
}
|
||||
|
||||
createMany(entries) {
|
||||
if (entries.length > 0) {
|
||||
const allocated = this.allocateMany(entries.length);
|
||||
const {properties} = this.constructor.schema.specification;
|
||||
const keys = Object.keys(properties);
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [entityId, values = {}] = entries[i];
|
||||
this.map[entityId] = allocated[i];
|
||||
this.data[allocated[i]].entity = entityId;
|
||||
if (false === values) {
|
||||
continue;
|
||||
}
|
||||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
const {defaultValue} = properties[j];
|
||||
if (j in values) {
|
||||
this.data[allocated[i]][j] = values[j];
|
||||
}
|
||||
else if ('undefined' !== typeof defaultValue) {
|
||||
this.data[allocated[i]][j] = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserialize(entityId, view, offset) {
|
||||
const {properties} = this.constructor.schema.specification;
|
||||
const instance = this.get(entityId);
|
||||
const deserialized = this.constructor.schema.deserialize(view, offset);
|
||||
for (const key in properties) {
|
||||
instance[key] = deserialized[key];
|
||||
}
|
||||
}
|
||||
|
||||
destroy(entityId) {
|
||||
this.destroyMany([entityId]);
|
||||
}
|
||||
|
||||
destroyMany(entities) {
|
||||
this.freeMany(
|
||||
entities
|
||||
.map((entityId) => {
|
||||
if ('undefined' !== typeof this.map[entityId]) {
|
||||
return this.map[entityId];
|
||||
}
|
||||
throw new Error(`can't free for non-existent id ${entityId}`);
|
||||
}),
|
||||
);
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
this.map[entities[i]] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static filterDefaults(instance) {
|
||||
const {properties} = this.schema.specification;
|
||||
const json = {};
|
||||
for (const key in properties) {
|
||||
const {defaultValue} = properties[key];
|
||||
if (key in instance && instance[key] !== defaultValue) {
|
||||
json[key] = instance[key];
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
freeMany(indices) {
|
||||
for (let i = 0; i < indices.length; ++i) {
|
||||
this.pool.push(indices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
get(entityId) {
|
||||
return this.data[this.map[entityId]];
|
||||
}
|
||||
|
||||
insertMany(entities) {
|
||||
const creating = [];
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const [entityId, values] = entities[i];
|
||||
if (!this.get(entityId)) {
|
||||
creating.push([entityId, values]);
|
||||
}
|
||||
else {
|
||||
const instance = this.get(entityId);
|
||||
for (const i in values) {
|
||||
instance[i] = values[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.createMany(creating);
|
||||
}
|
||||
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const {specification} = Component.constructor.schema;
|
||||
const Instance = class {
|
||||
$$entity = 0;
|
||||
constructor() {
|
||||
this.$$reset();
|
||||
}
|
||||
$$reset() {
|
||||
for (const key in specification.properties) {
|
||||
const {defaultValue} = specification.properties[key];
|
||||
this[`$$${key}`] = defaultValue;
|
||||
}
|
||||
}
|
||||
toJSON() {
|
||||
return Component.constructor.filterDefaults(this);
|
||||
}
|
||||
};
|
||||
const properties = {};
|
||||
properties.entity = {
|
||||
get: function get() {
|
||||
return this.$$entity;
|
||||
},
|
||||
set: function set(v) {
|
||||
this.$$entity = v;
|
||||
this.$$reset();
|
||||
},
|
||||
};
|
||||
for (const key in specification.properties) {
|
||||
properties[key] = {
|
||||
get: function get() {
|
||||
return this[`$$${key}`];
|
||||
},
|
||||
set: function set(value) {
|
||||
if (this[`$$${key}`] !== value) {
|
||||
this[`$$${key}`] = value;
|
||||
Component.markChange(this.entity, key, value);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Object.defineProperties(Instance.prototype, properties);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
markChange(entityId, key, value) {
|
||||
this.ecs.markChange(entityId, {[this.constructor.componentName]: {[key]: value}})
|
||||
}
|
||||
|
||||
mergeDiff(original, update) {
|
||||
return {...original, ...update};
|
||||
}
|
||||
|
||||
static get schema() {
|
||||
if (!this.$$schema) {
|
||||
this.$$schema = new Schema({
|
||||
type: 'object',
|
||||
properties: this.properties,
|
||||
});
|
||||
}
|
||||
return this.$$schema;
|
||||
}
|
||||
|
||||
serialize(entityId, view, offset) {
|
||||
this.constructor.schema.serialize(this.get(entityId), view, offset);
|
||||
}
|
||||
|
||||
sizeOf(entityId) {
|
||||
return this.constructor.schema.sizeOf(this.get(entityId));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Component from './component.js';
|
||||
|
||||
test('creates instances', () => {
|
||||
class CreatingComponent extends Component {
|
||||
static properties = {
|
||||
foo: {defaultValue: 'bar', type: 'string'},
|
||||
};
|
||||
}
|
||||
const ComponentInstance = new CreatingComponent();
|
||||
ComponentInstance.create(1);
|
||||
expect(ComponentInstance.get(1).entity)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('does not serialize default values', () => {
|
||||
class CreatingComponent extends Component {
|
||||
static properties = {
|
||||
foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'},
|
||||
};
|
||||
}
|
||||
const fakeEcs = {markChange() {}};
|
||||
const ComponentInstance = new CreatingComponent(fakeEcs);
|
||||
ComponentInstance.create(1)
|
||||
expect(ComponentInstance.get(1).toJSON())
|
||||
.to.deep.equal({});
|
||||
ComponentInstance.get(1).bar = 1;
|
||||
expect(ComponentInstance.get(1).toJSON())
|
||||
.to.deep.equal({bar: 1});
|
||||
});
|
||||
|
||||
test('reuses instances', () => {
|
||||
class ReusingComponent extends Component {
|
||||
static properties = {
|
||||
foo: {type: 'string'},
|
||||
};
|
||||
}
|
||||
const ComponentInstance = new ReusingComponent();
|
||||
ComponentInstance.create(1);
|
||||
const instance = ComponentInstance.get(1);
|
||||
ComponentInstance.destroy(1);
|
||||
expect(ComponentInstance.get(1))
|
||||
.to.be.undefined;
|
||||
expect(() => {
|
||||
ComponentInstance.destroy(1);
|
||||
})
|
||||
.to.throw();
|
||||
ComponentInstance.create(1);
|
||||
expect(ComponentInstance.get(1))
|
||||
.to.equal(instance);
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import EntityFactory from './entity-factory.js';
|
||||
import Schema from './schema.js';
|
||||
|
||||
import {Encoder, Decoder} from '@msgpack/msgpack';
|
||||
|
||||
|
@ -74,11 +75,11 @@ export default class Ecs {
|
|||
},
|
||||
next: () => {
|
||||
let result = it.next();
|
||||
hasResult: while (!result.done) {
|
||||
while (!result.done) {
|
||||
for (const componentName of criteria) {
|
||||
if (!(componentName in result.value[1])) {
|
||||
result = it.next();
|
||||
continue hasResult;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -280,9 +281,6 @@ export default class Ecs {
|
|||
existing.push(...this.$$entities[entityId].constructor.componentNames);
|
||||
}
|
||||
const Class = this.$$entityFactory.makeClass(componentNames(existing), this.Components);
|
||||
if (this.$$entities[entityId] && Class === this.$$entities[entityId].constructor) {
|
||||
// Eventually - memoizable.
|
||||
}
|
||||
this.$$entities[entityId] = new Class(entityId);
|
||||
}
|
||||
|
||||
|
@ -331,6 +329,21 @@ export default class Ecs {
|
|||
this.diff = {};
|
||||
}
|
||||
|
||||
size() {
|
||||
let size = 0;
|
||||
// # of components.
|
||||
size += 2;
|
||||
for (const type in this.Components) {
|
||||
size += Schema.sizeOf(type, {type: 'string'});
|
||||
}
|
||||
// # of entities.
|
||||
size += 4;
|
||||
for (const entityId of this.entities) {
|
||||
size += this.get(entityId).size();
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
system(systemName) {
|
||||
return this.Systems[systemName];
|
||||
}
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Component from './component.js';
|
||||
import Arbitrary from './arbitrary.js';
|
||||
import Ecs from './ecs.js';
|
||||
import Schema from './schema.js';
|
||||
import System from './system.js';
|
||||
|
||||
function wrapProperties(name, properties) {
|
||||
return class WrappedComponent extends Component {
|
||||
function wrapSpecification(name, specification) {
|
||||
return class Component extends Arbitrary {
|
||||
static componentName = name;
|
||||
static properties = properties;
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: specification,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const Empty = wrapProperties('Empty', {});
|
||||
const Empty = wrapSpecification('Empty', {});
|
||||
|
||||
const Name = wrapProperties('Name', {
|
||||
const Name = wrapSpecification('Name', {
|
||||
name: {type: 'string'},
|
||||
});
|
||||
|
||||
const Position = wrapProperties('Position', {
|
||||
const Position = wrapSpecification('Position', {
|
||||
x: {type: 'int32', defaultValue: 32},
|
||||
y: {type: 'int32'},
|
||||
z: {type: 'int32'},
|
||||
|
@ -113,7 +117,7 @@ test('inserts components into entities', () => {
|
|||
});
|
||||
|
||||
test('ticks systems', () => {
|
||||
const Momentum = wrapProperties('Momentum', {
|
||||
const Momentum = wrapSpecification('Momentum', {
|
||||
x: {type: 'int32'},
|
||||
y: {type: 'int32'},
|
||||
z: {type: 'int32'},
|
||||
|
@ -130,10 +134,10 @@ test('ticks systems', () => {
|
|||
}
|
||||
|
||||
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;
|
||||
for (const [position, momentum] of this.select('default')) {
|
||||
position.x += momentum.x * elapsed;
|
||||
position.y += momentum.y * elapsed;
|
||||
position.z += momentum.z * elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,7 +220,7 @@ test('schedules entities to be deleted when ticking systems', () => {
|
|||
test('adds components to and remove components from entities when ticking systems', () => {
|
||||
let addLength, removeLength;
|
||||
const ecs = new Ecs({
|
||||
Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
|
||||
Components: {Foo: wrapSpecification('Foo', {bar: {type: 'uint8'}})},
|
||||
Systems: {
|
||||
AddComponent: class extends System {
|
||||
static queries() {
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
export default class Query {
|
||||
|
||||
$$criteria = {with: [], without: []};
|
||||
$$ecs;
|
||||
|
||||
$$index = new Set();
|
||||
|
||||
constructor(parameters, ecs) {
|
||||
this.$$ecs = ecs;
|
||||
constructor(parameters, Components) {
|
||||
for (let i = 0; i < parameters.length; ++i) {
|
||||
const parameter = parameters[i];
|
||||
switch (parameter.charCodeAt(0)) {
|
||||
case '!'.charCodeAt(0):
|
||||
this.$$criteria.without.push(ecs.Components[parameter.slice(1)]);
|
||||
this.$$criteria.without.push(Components[parameter.slice(1)]);
|
||||
break;
|
||||
default:
|
||||
this.$$criteria.with.push(ecs.Components[parameter]);
|
||||
this.$$criteria.with.push(Components[parameter]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +62,7 @@ export default class Query {
|
|||
|
||||
select() {
|
||||
const it = this.$$index.values();
|
||||
const value = [];
|
||||
return {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
|
@ -72,7 +72,11 @@ export default class Query {
|
|||
if (result.done) {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
return {done: false, value: this.$$ecs.get(result.value)};
|
||||
for (let i = 0; i < this.$$criteria.with.length; ++i) {
|
||||
value[i] = this.$$criteria.with[i].get(result.value);
|
||||
}
|
||||
value[this.$$criteria.with.length] = result.value;
|
||||
return {done: false, value};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,42 +1,37 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Component from './component.js';
|
||||
import Arbitrary from './arbitrary.js';
|
||||
import Query from './query.js';
|
||||
import Schema from './schema.js';
|
||||
|
||||
function wrapProperties(name, properties) {
|
||||
return class WrappedComponent extends Component {
|
||||
function wrapSpecification(name, specification) {
|
||||
return class Component extends Arbitrary {
|
||||
static name = name;
|
||||
static properties = properties;
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: specification,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const A = new (wrapProperties('A', {a: {type: 'int32', defaultValue: 420}}));
|
||||
const B = new (wrapProperties('B', {b: {type: 'int32', defaultValue: 69}}));
|
||||
const C = new (wrapProperties('C', {c: {type: 'int32'}}));
|
||||
const A = new (wrapSpecification('A', {a: {type: 'int32', defaultValue: 420}}));
|
||||
const B = new (wrapSpecification('B', {b: {type: 'int32', defaultValue: 69}}));
|
||||
const C = new (wrapSpecification('C', {c: {type: 'int32'}}));
|
||||
|
||||
const Components = {A, B, C};
|
||||
Components.A.createMany([[2], [3]]);
|
||||
Components.B.createMany([[1], [2]]);
|
||||
Components.C.createMany([[2], [4]]);
|
||||
|
||||
const fakeEcs = (Components) => ({
|
||||
Components,
|
||||
get(id) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(Components)
|
||||
.map(([componentName, Component]) => [componentName, Component.get(id)])
|
||||
.concat([['id', id]])
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
function testQuery(parameters, expected) {
|
||||
const query = new Query(parameters, fakeEcs(Components));
|
||||
const query = new Query(parameters, Components);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(expected.length);
|
||||
for (const _ of query.select()) {
|
||||
expect(expected.includes(_.id))
|
||||
expect(_.length)
|
||||
.to.equal(parameters.filter((spec) => '!'.charCodeAt(0) !== spec.charCodeAt(0)).length + 1);
|
||||
expect(expected.includes(_.pop()))
|
||||
.to.equal(true);
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +51,7 @@ test('can query excluding', () => {
|
|||
});
|
||||
|
||||
test('can deindex', () => {
|
||||
const query = new Query(['A'], fakeEcs(Components));
|
||||
const query = new Query(['A'], Components);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(2);
|
||||
|
@ -66,9 +61,9 @@ test('can deindex', () => {
|
|||
});
|
||||
|
||||
test('can reindex', () => {
|
||||
const Test = new (wrapProperties('Test', {a: {type: 'int32', defaultValue: 420}}));
|
||||
const Test = new (wrapSpecification('Test', {a: {type: 'int32', defaultValue: 420}}));
|
||||
Test.createMany([[2], [3]]);
|
||||
const query = new Query(['Test'], fakeEcs({Test}));
|
||||
const query = new Query(['Test'], {Test});
|
||||
query.reindex([2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(2);
|
||||
|
@ -79,10 +74,10 @@ test('can reindex', () => {
|
|||
});
|
||||
|
||||
test('can select', () => {
|
||||
const query = new Query(['A'], fakeEcs(Components));
|
||||
const query = new Query(['A'], Components);
|
||||
query.reindex([1, 2, 3]);
|
||||
const it = query.select();
|
||||
const {value: {A}} = it.next();
|
||||
expect(A.a)
|
||||
const result = it.next();
|
||||
expect(result.value[0].a)
|
||||
.to.equal(420);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ export default class System {
|
|||
this.ecs = ecs;
|
||||
const queries = this.constructor.queries();
|
||||
for (const i in queries) {
|
||||
this.queries[i] = new Query(queries[i], ecs);
|
||||
this.queries[i] = new Query(queries[i], ecs.Components);
|
||||
}
|
||||
this.reindex(ecs.entities);
|
||||
}
|
||||
|
|
|
@ -13,13 +13,20 @@ function join(...parts) {
|
|||
|
||||
export default class Engine {
|
||||
|
||||
connections = [];
|
||||
connectedPlayers = new Map();
|
||||
ecses = {};
|
||||
frame = 0;
|
||||
handle;
|
||||
incomingActions = [];
|
||||
|
||||
connections = [];
|
||||
|
||||
connectedPlayers = new Map();
|
||||
|
||||
ecses = {};
|
||||
|
||||
frame = 0;
|
||||
|
||||
handle;
|
||||
|
||||
last = Date.now();
|
||||
|
||||
server;
|
||||
|
||||
constructor(Server) {
|
||||
|
@ -188,7 +195,7 @@ export default class Engine {
|
|||
animation: 'moving:down',
|
||||
frame: 0,
|
||||
frames: 8,
|
||||
source: '/assets/dude/dude.json',
|
||||
source: '/assets/dude.json',
|
||||
speed: 0.115,
|
||||
},
|
||||
Ticking: {},
|
||||
|
|
|
@ -30,7 +30,7 @@ test('visibility-based updates', async () => {
|
|||
const ecs = engine.ecses['homesteads/0'];
|
||||
// Create an entity.
|
||||
const entity = ecs.get(ecs.create({
|
||||
Forces: {forceX: 1},
|
||||
Forces: {momentum: {x: 1, y: 0}},
|
||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
||||
VisibleAabb: {},
|
||||
}));
|
||||
|
@ -38,7 +38,6 @@ test('visibility-based updates', async () => {
|
|||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
.to.deep.include({2: {MainEntity: {}, ...ecs.get(2).toJSON()}, 3: ecs.get(3).toJSON()});
|
||||
engine.setClean();
|
||||
// Tick and get update. Should be a partial update.
|
||||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
|
@ -51,14 +50,12 @@ test('visibility-based updates', async () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
engine.setClean();
|
||||
// Tick and get update. Should remove the entity.
|
||||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
.to.deep.include({3: false});
|
||||
// Aim back toward visible area and tick. Should be a full update for that entity.
|
||||
engine.setClean();
|
||||
entity.Forces.forceX = -1;
|
||||
entity.Momentum.x = -1;
|
||||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
.to.deep.include({3: ecs.get(3).toJSON()});
|
||||
|
|
|
@ -63,7 +63,7 @@ export default function EcsComponent() {
|
|||
}
|
||||
const entity = ecs.get(mainEntity);
|
||||
const {Camera} = entity;
|
||||
const {TileLayers: {layers: [layer]}} = ecs.get(1);
|
||||
const {TileLayers} = ecs.get(1);
|
||||
const [cx, cy] = [
|
||||
Math.round(Camera.x - RESOLUTION.x / 2),
|
||||
Math.round(Camera.y - RESOLUTION.y / 2),
|
||||
|
@ -73,19 +73,19 @@ export default function EcsComponent() {
|
|||
x={-cx}
|
||||
y={-cy}
|
||||
>
|
||||
<TileLayer tileLayer={layer} />
|
||||
<TileLayer tileLayer={TileLayers.layers[0]} />
|
||||
{activeTool && (
|
||||
<TargetingGrid
|
||||
ecs={ecs}
|
||||
entity={entity}
|
||||
tileLayer={layer}
|
||||
/>
|
||||
)}
|
||||
<Entities entities={entities} />
|
||||
{activeTool && (
|
||||
<TargetingGhost
|
||||
ecs={ecs}
|
||||
entity={entity}
|
||||
projection={activeTool.projection}
|
||||
tileLayer={layer}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
|
|
@ -1,14 +1,65 @@
|
|||
import Entity from './entity.jsx';
|
||||
import {Container, Graphics} from '@pixi/react';
|
||||
import {useCallback} from 'react';
|
||||
|
||||
export default function Entities({entities}) {
|
||||
const renderables = [];
|
||||
for (const id in entities) {
|
||||
renderables.push(
|
||||
<Entity
|
||||
entity={entities[id]}
|
||||
key={id}
|
||||
/>
|
||||
import {useDebug} from '@/context/debug.js';
|
||||
|
||||
import Emitter from './emitter.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
|
||||
function Crosshair({x, y}) {
|
||||
const draw = useCallback((g) => {
|
||||
g.clear();
|
||||
g.lineStyle(1, 0x000000);
|
||||
g.moveTo(-5, 0);
|
||||
g.lineTo(5, 0);
|
||||
g.moveTo(0, -5);
|
||||
g.lineTo(0, 5);
|
||||
g.lineStyle(0.5, 0xffffff);
|
||||
g.moveTo(-5, 0);
|
||||
g.lineTo(5, 0);
|
||||
g.moveTo(0, -5);
|
||||
g.lineTo(0, 5);
|
||||
g.lineStyle(1, 0x000000);
|
||||
g.drawCircle(0, 0, 3);
|
||||
g.lineStyle(0.5, 0xffffff);
|
||||
g.drawCircle(0, 0, 3);
|
||||
}, []);
|
||||
return (
|
||||
<Graphics draw={draw} x={x} y={y} />
|
||||
);
|
||||
}
|
||||
return <>{renderables}</>;
|
||||
|
||||
export default function Entities({entities}) {
|
||||
const [debug] = useDebug();
|
||||
const renderables = [];
|
||||
for (const id in entities) {
|
||||
const entity = entities[id];
|
||||
if (!entity || !entity.Position || !entity.Sprite) {
|
||||
continue;
|
||||
}
|
||||
renderables.push(
|
||||
<Container
|
||||
key={id}
|
||||
>
|
||||
{entity.Sprite && (
|
||||
<Sprite
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{entity.Emitter && (
|
||||
<Emitter
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{debug && (
|
||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{renderables}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import {Container, Graphics} from '@pixi/react';
|
||||
import {memo, useCallback} from 'react';
|
||||
|
||||
import {useDebug} from '@/context/debug.js';
|
||||
|
||||
import Emitter from './emitter.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
|
||||
function Crosshair({x, y}) {
|
||||
const draw = useCallback((g) => {
|
||||
g.clear();
|
||||
g.lineStyle(1, 0x000000);
|
||||
g.moveTo(-5, 0);
|
||||
g.lineTo(5, 0);
|
||||
g.moveTo(0, -5);
|
||||
g.lineTo(0, 5);
|
||||
g.lineStyle(0.5, 0xffffff);
|
||||
g.moveTo(-5, 0);
|
||||
g.lineTo(5, 0);
|
||||
g.moveTo(0, -5);
|
||||
g.lineTo(0, 5);
|
||||
g.lineStyle(1, 0x000000);
|
||||
g.drawCircle(0, 0, 3);
|
||||
g.lineStyle(0.5, 0xffffff);
|
||||
g.drawCircle(0, 0, 3);
|
||||
}, []);
|
||||
return (
|
||||
<Graphics draw={draw} x={x} y={y} />
|
||||
);
|
||||
}
|
||||
|
||||
function Entities({entity}) {
|
||||
const [debug] = useDebug();
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
{entity.Sprite && (
|
||||
<Sprite
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{entity.Emitter && (
|
||||
<Emitter
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{debug && entity.Position && (
|
||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Entities);
|
|
@ -33,7 +33,7 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
|||
},
|
||||
})
|
||||
|
||||
export default function TargetingGhost({entity, projection, tileLayer}) {
|
||||
export default function TargetingGhost({ecs, entity, projection}) {
|
||||
const [radians, setRadians] = useState(0);
|
||||
useEffect(() => {
|
||||
const handle = setInterval(() => {
|
||||
|
@ -43,12 +43,12 @@ export default function TargetingGhost({entity, projection, tileLayer}) {
|
|||
clearInterval(handle);
|
||||
};
|
||||
}, []);
|
||||
const {TileLayers: {layers: [layer]}} = ecs.get(1);
|
||||
const {Position, Wielder} = entity;
|
||||
const projected = Wielder.project(Position.tile, projection)
|
||||
const ghosts = [];
|
||||
const {area} = tileLayer;
|
||||
for (const {x, y} of projected) {
|
||||
if (x < 0 || y < 0 || x >= area.x || y >= area.y) {
|
||||
if (x < 0 || y < 0 || x >= layer.area.x || y >= layer.area.y) {
|
||||
continue;
|
||||
}
|
||||
ghosts.push(
|
||||
|
|
|
@ -28,7 +28,7 @@ function makeFade(renderer) {
|
|||
}
|
||||
|
||||
const TargetingGridInternal = PixiComponent('TargetingGrid', {
|
||||
create: ({app, tileLayer}) => {
|
||||
create: ({app, ecs}) => {
|
||||
const fade = makeFade(app.renderer);
|
||||
const grid = new Graphics();
|
||||
const lineWidth = 1;
|
||||
|
@ -59,10 +59,11 @@ const TargetingGridInternal = PixiComponent('TargetingGrid', {
|
|||
innerGrid.mask = fade;
|
||||
const container = new Container();
|
||||
container.addChild(fade, grid, innerGrid);
|
||||
// Clamp within tile layer area.
|
||||
// Clamp within layer area.
|
||||
const area = new Graphics();
|
||||
area.beginFill(0xffffff);
|
||||
const {x, y} = tileLayer.area;
|
||||
const {TileLayers: {layers: [layer]}} = ecs.get(1)
|
||||
const {x, y} = layer.area;
|
||||
area.drawRect(0, 0, x * tileSize.x, y * tileSize.y);
|
||||
container.mask = area;
|
||||
const top = new Container();
|
||||
|
@ -85,7 +86,7 @@ const TargetingGridInternal = PixiComponent('TargetingGrid', {
|
|||
},
|
||||
})
|
||||
|
||||
export default function TargetingGrid({entity, tileLayer}) {
|
||||
export default function TargetingGrid({ecs, entity}) {
|
||||
const app = useApp();
|
||||
const [radians, setRadians] = useState(0);
|
||||
useEffect(() => {
|
||||
|
@ -99,9 +100,9 @@ export default function TargetingGrid({entity, tileLayer}) {
|
|||
return (
|
||||
<TargetingGridInternal
|
||||
app={app}
|
||||
ecs={ecs}
|
||||
entity={entity}
|
||||
radians={radians}
|
||||
tileLayer={tileLayer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
@ -1,13 +1,10 @@
|
|||
const {Position, Wielder} = wielder
|
||||
const projected = Wielder.project(Position.tile, item.tool.projection)
|
||||
if (projected.length > 0) {
|
||||
|
||||
const {Controlled, Emitter, Sound, Sprite} = wielder
|
||||
const {Controlled, Emitter, Position, Sound, Sprite, Wielder} = wielder
|
||||
const {TileLayers} = ecs.get(1)
|
||||
const layer = TileLayers.layer(0)
|
||||
const projected = Wielder.project(Position.tile, item.tool.projection)
|
||||
|
||||
Controlled.locked = 1
|
||||
const [, direction] = Sprite.animation.split(':')
|
||||
Controlled.locked = 1;
|
||||
const [, direction] = Sprite.animation.split(':');
|
||||
|
||||
const dirtParticles = {
|
||||
behaviors: [
|
||||
|
@ -124,5 +121,3 @@ if (projected.length > 0) {
|
|||
}
|
||||
|
||||
Controlled.locked = 0;
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user