Compare commits

...

12 Commits

Author SHA1 Message Date
cha0s
73082aad94 chore: tidy & perf 2024-06-27 02:57:28 -05:00
cha0s
d33f849e26 refactor: share less 2024-06-27 02:14:47 -05:00
cha0s
929b6a5071 chore: gardening 2024-06-27 02:05:10 -05:00
cha0s
45cb158f2a refactor: less magic 2024-06-26 21:36:45 -05:00
cha0s
9bae378ac7 chore: format 2024-06-26 11:40:14 -05:00
cha0s
ee96c69a30 refactor: component 2024-06-26 10:41:09 -05:00
cha0s
6d6904bd22 refactor: dude assets 2024-06-26 09:17:56 -05:00
cha0s
b4c38d1ee0 feat: projection verification 2024-06-26 09:17:35 -05:00
cha0s
559d77c92c refactor: queries 2024-06-26 07:41:07 -05:00
cha0s
219f796570 fix: forces 2024-06-26 07:39:51 -05:00
cha0s
6de77e5c73 fix: outer continue 2024-06-26 07:39:28 -05:00
cha0s
9151c5b9b3 fix: broken test 2024-06-26 07:39:11 -05:00
53 changed files with 975 additions and 1002 deletions

View File

@ -1,3 +1,7 @@
export default {
frame: {type: 'uint16'},
};
import Component from '@/ecs/component.js';
export default class Animation extends Component {
static properties = {
frame: {type: 'uint16'},
};
}

View File

@ -1,4 +1,8 @@
export default {
x: {type: 'uint16'},
y: {type: 'uint16'},
import Component from '@/ecs/component.js';
export default class AreaSize extends Component {
static properties = {
x: {type: 'uint16'},
y: {type: 'uint16'},
};
}

View File

@ -1,4 +1,8 @@
export default {
x: {type: 'uint16'},
y: {type: 'uint16'},
import Component from '@/ecs/component.js';
export default class Camera extends Component {
static properties = {
x: {type: 'uint16'},
y: {type: 'uint16'},
};
}

View File

@ -1,8 +1,12 @@
export default {
locked: {type: 'uint8'},
moveUp: {type: 'float32'},
moveRight: {type: 'float32'},
moveDown: {type: 'float32'},
moveLeft: {type: 'float32'},
changeSlot: {type: 'int8'},
};
import Component from '@/ecs/component.js';
export default class Controlled extends Component {
static properties = {
locked: {type: 'uint8'},
moveUp: {type: 'float32'},
moveRight: {type: 'float32'},
moveDown: {type: 'float32'},
moveLeft: {type: 'float32'},
changeSlot: {type: 'int8'},
};
}

View File

@ -1,3 +1,7 @@
export default {
direction: {type: 'uint8'},
};
import Component from '@/ecs/component.js';
export default class Direction extends Component {
static properties = {
direction: {type: 'uint8'},
};
}

View File

@ -1,3 +1,7 @@
export default {
path: {type: 'string'},
import Component from '@/ecs/component.js';
export default class Ecs extends Component {
static properties = {
path: {type: 'string'},
};
}

View File

@ -1,31 +1,25 @@
import Schema from '@/ecs/schema.js';
import Component from '@/ecs/component.js';
export default function(Component) {
return class Emitter extends Component {
mergeDiff(original, update) {
const merged = {};
if (update.emit) {
merged.emit = {
...original.emit,
...update.emit,
}
export default class Emitter extends Component {
mergeDiff(original, update) {
const merged = {};
if (update.emit) {
merged.emit = {
...original.emit,
...update.emit,
}
return merged;
}
instanceFromSchema() {
const Component = this;
const Instance = super.instanceFromSchema();
return class EmitterInstance extends Instance {
emitting = [];
id = 0;
emit(specification) {
Component.markChange(this.entity, 'emit', {[this.id++]: specification});
}
};
}
static schema = new Schema({
type: 'object',
properties: {},
});
return merged;
}
}
instanceFromSchema() {
const Component = this;
const Instance = super.instanceFromSchema();
return class EmitterInstance extends Instance {
emitting = [];
id = 0;
emit(specification) {
Component.markChange(this.entity, 'emit', {[this.id++]: specification});
}
};
}
}

View File

@ -1 +1,3 @@
export default {};
import Component from '@/ecs/component.js';
export default class Engine extends Component {}

View File

@ -1,4 +1,10 @@
export default {
impulseX: {type: 'float32'},
impulseY: {type: 'float32'},
import Component from '@/ecs/component.js';
export default class Forces extends Component {
static properties = {
forceX: {type: 'float32'},
forceY: {type: 'float32'},
impulseX: {type: 'float32'},
impulseY: {type: 'float32'},
};
}

View File

@ -1,3 +1,7 @@
export default {
health: {type: 'uint32'},
};
import Component from '@/ecs/component.js';
export default class Health extends Component {
static properties = {
health: {type: 'uint32'},
};
}

View File

@ -1,37 +1,14 @@
import Arbitrary from '@/ecs/arbitrary.js';
import Schema from '@/ecs/schema.js';
import gather from '@/util/gather.js';
const specificationsAndOrDecorators = gather(
const Gathered = gather(
import.meta.glob('./*.js', {eager: true, import: 'default'}),
);
const Components = {};
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,
});
}
}
for (const componentName in Gathered) {
Components[componentName] = class Named extends Gathered[componentName] {
static componentName = componentName;
};
}
export default Components;

View File

@ -1,114 +1,109 @@
import Schema from '@/ecs/schema.js';
import Component from '@/ecs/component.js';
export default function(Component) {
return class Inventory extends Component {
insertMany(entities) {
for (const [id, {slotChange}] of entities) {
if (slotChange) {
const {slots} = this.get(id);
for (const slotIndex in slotChange) {
if (false === slotChange[slotIndex]) {
delete slots[slotIndex];
}
else {
slots[slotIndex] = {
...slots[slotIndex],
...slotChange[slotIndex],
};
}
export default class Inventory extends Component {
insertMany(entities) {
for (const [id, {slotChange}] of entities) {
if (slotChange) {
const {slots} = this.get(id);
for (const slotIndex in slotChange) {
if (false === slotChange[slotIndex]) {
delete slots[slotIndex];
}
else {
slots[slotIndex] = {
...slots[slotIndex],
...slotChange[slotIndex],
};
}
}
}
return super.insertMany(entities);
}
mergeDiff(original, update) {
if (!update.slotChange) {
return super.mergeDiff(original, update);
}
const slotChange = {
...original.slotChange,
};
for (const index in update.slotChange) {
if (false === update.slotChange[index]) {
slotChange[index] = false;
}
else {
slotChange[index] = {
...slotChange[index],
...update.slotChange[index],
};
}
}
return {slotChange};
return super.insertMany(entities);
}
mergeDiff(original, update) {
if (!update.slotChange) {
return super.mergeDiff(original, update);
}
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.item = async function (slot) {
const {slots} = this;
if (!(slot in slots)) {
return undefined;
}
const json = await (
fetch([slots[slot].source, 'item.json'].join('/'))
.then((response) => (response.ok ? response.json() : {}))
);
const item = {
...slots[slot],
...json,
const slotChange = {
...original.slotChange,
};
for (const index in update.slotChange) {
if (false === update.slotChange[index]) {
slotChange[index] = false;
}
else {
slotChange[index] = {
...slotChange[index],
...update.slotChange[index],
};
const instance = this;
const proxy = new Proxy(item, {
set(target, property, value) {
slots[slot][property] = value;
if ('qty' === property && value <= 0) {
Component.markChange(instance.entity, 'slotChange', {[slot]: false});
delete slots[slot];
}
else {
Component.markChange(instance.entity, 'slotChange', {[slot]: {[property]: value}});
}
return true;
},
});
return proxy;
};
Instance.prototype.swapSlots = function(l, r) {
const {slots} = this;
const tmp = slots[l];
const change = {};
if (slots[r]) {
change[l] = slots[l] = slots[r];
}
else {
change[l] = false;
delete slots[l];
}
if (tmp) {
change[r] = slots[r] = tmp;
}
else {
change[r] = false;
delete slots[r];
}
Component.markChange(this.entity, 'slotChange', change);
};
return Instance;
}
}
static schema = new Schema({
type: 'object',
properties: {
slots: {
type: 'map',
value: {
type: 'object',
properties: {
quantity: {type: 'uint16'},
source: {type: 'string'},
},
},
return {slotChange};
}
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.item = async function (slot) {
const {slots} = this;
if (!(slot in slots)) {
return undefined;
}
const json = await (
fetch([slots[slot].source, 'item.json'].join('/'))
.then((response) => (response.ok ? response.json() : {}))
);
const item = {
...slots[slot],
...json,
};
const instance = this;
const proxy = new Proxy(item, {
set(target, property, value) {
slots[slot][property] = value;
if ('qty' === property && value <= 0) {
Component.markChange(instance.entity, 'slotChange', {[slot]: false});
delete slots[slot];
}
else {
Component.markChange(instance.entity, 'slotChange', {[slot]: {[property]: value}});
}
return true;
},
});
return proxy;
};
Instance.prototype.swapSlots = function(l, r) {
const {slots} = this;
const tmp = slots[l];
const change = {};
if (slots[r]) {
change[l] = slots[l] = slots[r];
}
else {
change[l] = false;
delete slots[l];
}
if (tmp) {
change[r] = slots[r] = tmp;
}
else {
change[r] = false;
delete slots[r];
}
Component.markChange(this.entity, 'slotChange', change);
};
return Instance;
}
static properties = {
slots: {
type: 'map',
value: {
type: 'object',
properties: {
quantity: {type: 'uint16'},
source: {type: 'string'},
},
},
});
}
},
};
}

View File

@ -1 +1,3 @@
export default {};
import Component from '@/ecs/component.js';
export default class MainEntity extends Component {}

View File

@ -1,29 +1,24 @@
import Schema from '@/ecs/schema.js';
import Component from '@/ecs/component.js';
export default function(Component) {
return class Wielder extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Object.defineProperty(Instance.prototype, 'tile', {
get: function () {
const {TileLayers} = Component.ecs.get(1);
const {Position: {x, y}} = Component.ecs.get(this.entity);
const {tileSize} = TileLayers.layers[0];
return {
x: (x - (x % tileSize.x)) / tileSize.x,
y: (y - (y % tileSize.y)) / tileSize.y,
}
},
});
return Instance;
}
static schema = new Schema({
type: 'object',
properties: {
x: {type: 'float32'},
y: {type: 'float32'},
export default class Position extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Object.defineProperty(Instance.prototype, 'tile', {
get: function () {
const {TileLayers} = Component.ecs.get(1);
const {Position: {x, y}} = Component.ecs.get(this.entity);
const {tileSize} = TileLayers.layers[0];
return {
x: (x - (x % tileSize.x)) / tileSize.x,
y: (y - (y % tileSize.y)) / tileSize.y,
}
},
});
return Instance;
}
static properties = {
x: {type: 'float32'},
y: {type: 'float32'},
};
}

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,29 +1,23 @@
import Schema from '@/ecs/schema.js';
import Component from '@/ecs/component.js';
export default function(Component) {
return class Sound extends Component {
mergeDiff(original, update) {
const merged = {};
if (update.play) {
merged.play = [
...(original.play ?? []),
...update.play,
];
export default class Sound extends Component {
mergeDiff(original, update) {
const merged = {};
if (update.play) {
merged.play = [
...(original.play ?? []),
...update.play,
];
}
return merged;
}
instanceFromSchema() {
const Component = this;
const Instance = super.instanceFromSchema();
return class SoundInstance extends Instance {
play(source) {
Component.markChange(this.entity, 'play', [source]);
}
return merged;
}
instanceFromSchema() {
const Component = this;
const Instance = super.instanceFromSchema();
return class SoundInstance extends Instance {
play(source) {
Component.markChange(this.entity, 'play', [source]);
}
};
}
static schema = new Schema({
type: 'object',
properties: {},
});
};
}
}

View File

@ -1,3 +1,7 @@
export default {
speed: {type: 'float32'},
};
import Component from '@/ecs/component.js';
export default class Speed extends Component {
static properties = {
speed: {type: 'float32'},
};
}

View File

@ -1,10 +1,15 @@
import Component from '@/ecs/component.js';
import vector2d from "./helpers/vector-2d";
export default {
anchor: vector2d('float32', {x: 0.5, y: 0.5}),
animation: {type: 'string'},
elapsed: {type: 'float32'},
frame: {type: 'uint16'},
frames: {type: 'uint16'},
source: {type: 'string'},
speed: {type: 'float32'},
};
export default class Sprite extends Component {
static properties = {
anchor: vector2d('float32', {x: 0.5, y: 0.5}),
animation: {type: 'string'},
elapsed: {type: 'float32'},
frame: {type: 'uint16'},
frames: {type: 'uint16'},
source: {type: 'string'},
speed: {type: 'float32'},
};
}

View File

@ -1,44 +1,39 @@
import Schema from '@/ecs/schema.js';
import Component from '@/ecs/component.js';
export default function(Component) {
return class Ticking extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
export default class Ticking extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
Instance.prototype.$$finished = [];
Instance.prototype.$$tickingPromises = [];
Instance.prototype.addTickingPromise = function(tickingPromise) {
this.$$tickingPromises.push(tickingPromise);
tickingPromise.then(() => {
this.$$finished.push(tickingPromise);
});
}
Instance.prototype.tick = function(elapsed) {
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
for (const tickingPromise of this.$$tickingPromises) {
tickingPromise.tick(elapsed);
}
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
}
return Instance;
Instance.prototype.$$finished = [];
Instance.prototype.$$tickingPromises = [];
Instance.prototype.addTickingPromise = function(tickingPromise) {
this.$$tickingPromises.push(tickingPromise);
tickingPromise.then(() => {
this.$$finished.push(tickingPromise);
});
}
static schema = new Schema({
type: 'object',
properties: {
isTicking: {defaultValue: 1, type: 'uint8'},
},
});
Instance.prototype.tick = function(elapsed) {
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
for (const tickingPromise of this.$$tickingPromises) {
tickingPromise.tick(elapsed);
}
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
}
return Instance;
}
static properties = {
isTicking: {defaultValue: 1, type: 'uint8'},
};
}

View File

@ -1,112 +1,107 @@
import Component from '@/ecs/component.js';
import vector2d from './helpers/vector-2d';
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) {
const component = this.get(id);
const {layers} = component;
for (const layerIndex in layerChange) {
for (const calculated in layerChange[layerIndex]) {
const tile = layerChange[layerIndex][calculated];
layers[layerIndex].data[calculated] = tile;
}
layers[layerIndex] = {...layers[layerIndex]};
export default class TileLayers extends Component {
insertMany(entities) {
for (const [id, {layerChange}] of entities) {
if (layerChange) {
const component = this.get(id);
const {layers} = component;
for (const layerIndex in layerChange) {
for (const calculated in layerChange[layerIndex]) {
const tile = layerChange[layerIndex][calculated];
layers[layerIndex].data[calculated] = tile;
}
layers[layerIndex] = {...layers[layerIndex]};
}
}
return super.insertMany(entities);
}
mergeDiff(original, update) {
if (!update.layerChange) {
return super.mergeDiff(original, update);
}
const layerChange = {
...original.layerChange,
return super.insertMany(entities);
}
mergeDiff(original, update) {
if (!update.layerChange) {
return super.mergeDiff(original, update);
}
const layerChange = {
...original.layerChange,
};
for (const index in update.layerChange) {
layerChange[index] = {
...layerChange[index],
...update.layerChange[index],
};
for (const index in update.layerChange) {
layerChange[index] = {
...layerChange[index],
...update.layerChange[index],
};
}
return {layerChange};
}
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.layer = function (index) {
const {layers} = this;
if (!(index in layers)) {
return undefined;
return {layerChange};
}
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.layer = function (index) {
const {layers} = this;
if (!(index in layers)) {
return undefined;
}
const instance = this;
class LayerProxy {
constructor(layer) {
this.layer = layer;
}
const instance = this;
class LayerProxy {
constructor(layer) {
this.layer = layer;
}
get area() {
return this.layer.area;
}
get source() {
return this.layer.source;
}
stamp(at, data) {
const changes = {};
for (const row in data) {
const columns = data[row];
for (const column in columns) {
const tile = columns[column];
const x = at.x + parseInt(column);
const y = at.y + parseInt(row);
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
continue;
}
const calculated = y * this.layer.area.x + x;
this.layer.data[calculated] = tile;
changes[calculated] = tile;
get area() {
return this.layer.area;
}
get source() {
return this.layer.source;
}
stamp(at, data) {
const changes = {};
for (const row in data) {
const columns = data[row];
for (const column in columns) {
const tile = columns[column];
const x = at.x + parseInt(column);
const y = at.y + parseInt(row);
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
continue;
}
const calculated = y * this.layer.area.x + x;
this.layer.data[calculated] = tile;
changes[calculated] = tile;
}
Component.markChange(instance.entity, 'layerChange', {[index]: changes});
}
tile({x, y}) {
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
return undefined;
}
return this.layer.data[y * this.layer.area.x + x];
}
get tileSize() {
return this.layer.tileSize;
}
Component.markChange(instance.entity, 'layerChange', {[index]: changes});
}
return new LayerProxy(layers[index]);
};
return Instance;
}
static schema = new Schema({
type: 'object',
properties: {
layers: {
type: 'array',
subtype: {
type: 'object',
properties: {
area: vector2d('float32'),
data: {
type: 'array',
subtype: {
type: 'uint16',
},
},
source: {type: 'string'},
tileSize: vector2d('float32'),
tile({x, y}) {
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
return undefined;
}
return this.layer.data[y * this.layer.area.x + x];
}
get tileSize() {
return this.layer.tileSize;
}
}
return new LayerProxy(layers[index]);
};
return Instance;
}
static properties = {
layers: {
type: 'array',
subtype: {
type: 'object',
properties: {
area: vector2d('float32'),
data: {
type: 'array',
subtype: {
type: 'uint16',
},
},
source: {type: 'string'},
tileSize: vector2d('float32'),
},
},
});
}
},
};
}

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,6 +1,10 @@
export default {
x0: {type: 'float32'},
x1: {type: 'float32'},
y0: {type: 'float32'},
y1: {type: 'float32'},
import Component from '@/ecs/component.js';
export default class VisibleAabb extends Component {
static properties = {
x0: {type: 'float32'},
x1: {type: 'float32'},
y0: {type: 'float32'},
y1: {type: 'float32'},
};
}

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,73 +1,71 @@
import Schema from '@/ecs/schema.js';
import Component from '@/ecs/component.js';
export default function(Component) {
return class Wielder extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.activeItem = async function () {
const {Inventory, Wielder} = Component.ecs.get(this.entity);
return Inventory.item(Wielder.activeSlot + 1);
};
Instance.prototype.project = function(position, projection) {
const {Direction: {direction}} = Component.ecs.get(this.entity);
let startX = position.x;
let startY = position.y;
switch (direction) {
case 0:
startX += projection.distance[1];
startY -= projection.distance[0];
break;
case 1:
startX += projection.distance[0];
startY += projection.distance[1];
break;
case 2:
startX -= projection.distance[1];
startY += projection.distance[0];
break;
case 3:
startX -= projection.distance[0];
startY -= projection.distance[1];
break;
}
const projected = [];
for (const row in projection.grid) {
const columns = projection.grid[row];
for (const column in columns) {
const targeted = projection.grid[row][column];
if (targeted) {
let axe;
switch (direction) {
case 0:
axe = [column, row];
break;
case 1:
axe = [-row, column];
break;
case 2:
axe = [-column, -row];
break;
case 3:
axe = [row, -column];
break;
}
projected.push({
x: startX + parseInt(axe[0]),
y: startY + parseInt(axe[1]),
})
export default class Wielder extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.activeItem = async function () {
const {Inventory, Wielder} = Component.ecs.get(this.entity);
return Inventory.item(Wielder.activeSlot + 1);
};
Instance.prototype.project = function(position, projection) {
const {TileLayers: {layers: [layer]}} = Component.ecs.get(1);
const {Direction: {direction}} = Component.ecs.get(this.entity);
let startX = position.x;
let startY = position.y;
switch (direction) {
case 0:
startX += projection.distance[1];
startY -= projection.distance[0];
break;
case 1:
startX += projection.distance[0];
startY += projection.distance[1];
break;
case 2:
startX -= projection.distance[1];
startY += projection.distance[0];
break;
case 3:
startX -= projection.distance[0];
startY -= projection.distance[1];
break;
}
const projected = [];
for (const row in projection.grid) {
const columns = projection.grid[row];
for (const column in columns) {
const targeted = projection.grid[row][column];
if (targeted) {
let axe;
switch (direction) {
case 0:
axe = [column, row];
break;
case 1:
axe = [-row, column];
break;
case 2:
axe = [-column, -row];
break;
case 3:
axe = [row, -column];
break;
}
const x = startX + parseInt(axe[0]);
const y = startY + parseInt(axe[1]);
if (x < 0 || y < 0 || x >= layer.area.x || y >= layer.area.y) {
continue;
}
projected.push({x, y});
}
}
return projected;
}
return Instance;
return projected;
}
static schema = new Schema({
type: 'object',
properties: {
activeSlot: {type: 'uint16'},
},
});
return Instance;
}
static properties = {
activeSlot: {type: 'uint16'},
};
}

View File

@ -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);

View File

@ -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;
Position.y += elapsed * Forces.impulseY;
for (const {Position, Forces} of this.select('default')) {
Position.x += elapsed * (Forces.impulseX + Forces.forceX);
Position.y += elapsed * (Forces.impulseY + Forces.forceY);
}
}

View File

@ -2,6 +2,12 @@ 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) {

View File

@ -1,8 +1,5 @@
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() {
@ -19,8 +16,8 @@ export default class FollowCamera extends System {
}
tick(elapsed) {
for (const [, , entityId] of this.select('default')) {
this.updateCamera(elapsed * 3, this.ecs.get(entityId));
for (const {id} of this.select('default')) {
this.updateCamera(elapsed * 3, this.ecs.get(id));
}
}

View File

@ -3,7 +3,7 @@ import {System} from '@/ecs/index.js';
export default class ResetForces extends System {
static get priority() {
return {phase: 'pre'};
return {phase: 'post'};
}
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;
}

View File

@ -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;

View File

@ -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);
}

View File

@ -9,11 +9,10 @@ export default class SpriteDirection extends System {
}
tick() {
for (const [Sprite, entityId] of this.select('default')) {
const entity = this.ecs.get(entityId);
for (const {Controlled, Direction, Sprite} of this.select('default')) {
const parts = [];
if (entity.Controlled) {
const {locked, moveUp, moveRight, moveDown, moveLeft} = entity.Controlled;
if (Controlled) {
const {locked, moveUp, moveRight, moveDown, moveLeft} = Controlled;
if (locked) {
continue;
}
@ -24,14 +23,14 @@ export default class SpriteDirection extends System {
parts.push('idle');
}
}
if (entity.Direction) {
if (Direction) {
const name = {
0: 'up',
1: 'right',
2: 'down',
3: 'left',
};
parts.push(name[entity.Direction.direction]);
parts.push(name[Direction.direction]);
}
if (parts.length > 0) {
Sprite.animation = parts.join(':');

View File

@ -54,6 +54,12 @@ class SpatialHash {
export default class UpdateSpatialHash extends System {
static get priority() {
return {
after: 'CalculateAabbs',
};
}
deindex(entities) {
super.deindex(entities);
for (const id of entities) {

View File

@ -1,109 +0,0 @@
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;
}
}

View File

@ -1,56 +0,0 @@
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);
});

View File

@ -1,101 +0,0 @@
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));
}
}

199
app/ecs/component.js Normal file
View File

@ -0,0 +1,199 @@
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));
}
}

52
app/ecs/component.test.js Normal file
View File

@ -0,0 +1,52 @@
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);
});

View File

@ -1,5 +1,4 @@
import EntityFactory from './entity-factory.js';
import Schema from './schema.js';
import {Encoder, Decoder} from '@msgpack/msgpack';
@ -75,11 +74,11 @@ export default class Ecs {
},
next: () => {
let result = it.next();
while (!result.done) {
hasResult: while (!result.done) {
for (const componentName of criteria) {
if (!(componentName in result.value[1])) {
result = it.next();
continue;
continue hasResult;
}
}
break;
@ -281,6 +280,9 @@ 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);
}
@ -329,21 +331,6 @@ 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];
}

View File

@ -1,27 +1,23 @@
import {expect, test} from 'vitest';
import Arbitrary from './arbitrary.js';
import Component from './component.js';
import Ecs from './ecs.js';
import Schema from './schema.js';
import System from './system.js';
function wrapSpecification(name, specification) {
return class Component extends Arbitrary {
function wrapProperties(name, properties) {
return class WrappedComponent extends Component {
static componentName = name;
static schema = new Schema({
type: 'object',
properties: specification,
});
static properties = properties;
};
}
const Empty = wrapSpecification('Empty', {});
const Empty = wrapProperties('Empty', {});
const Name = wrapSpecification('Name', {
const Name = wrapProperties('Name', {
name: {type: 'string'},
});
const Position = wrapSpecification('Position', {
const Position = wrapProperties('Position', {
x: {type: 'int32', defaultValue: 32},
y: {type: 'int32'},
z: {type: 'int32'},
@ -117,7 +113,7 @@ test('inserts components into entities', () => {
});
test('ticks systems', () => {
const Momentum = wrapSpecification('Momentum', {
const Momentum = wrapProperties('Momentum', {
x: {type: 'int32'},
y: {type: 'int32'},
z: {type: 'int32'},
@ -134,10 +130,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;
}
}
@ -220,7 +216,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: wrapSpecification('Foo', {bar: {type: 'uint8'}})},
Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
Systems: {
AddComponent: class extends System {
static queries() {

View File

@ -1,18 +1,19 @@
export default class Query {
$$criteria = {with: [], without: []};
$$ecs;
$$index = new Set();
constructor(parameters, Components) {
constructor(parameters, ecs) {
this.$$ecs = ecs;
for (let i = 0; i < parameters.length; ++i) {
const parameter = parameters[i];
switch (parameter.charCodeAt(0)) {
case '!'.charCodeAt(0):
this.$$criteria.without.push(Components[parameter.slice(1)]);
this.$$criteria.without.push(ecs.Components[parameter.slice(1)]);
break;
default:
this.$$criteria.with.push(Components[parameter]);
this.$$criteria.with.push(ecs.Components[parameter]);
break;
}
}
@ -62,7 +63,6 @@ export default class Query {
select() {
const it = this.$$index.values();
const value = [];
return {
[Symbol.iterator]() {
return this;
@ -72,11 +72,7 @@ export default class Query {
if (result.done) {
return {done: true, value: undefined};
}
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};
return {done: false, value: this.$$ecs.get(result.value)};
},
};
}

View File

@ -1,37 +1,42 @@
import {expect, test} from 'vitest';
import Arbitrary from './arbitrary.js';
import Component from './component.js';
import Query from './query.js';
import Schema from './schema.js';
function wrapSpecification(name, specification) {
return class Component extends Arbitrary {
function wrapProperties(name, properties) {
return class WrappedComponent extends Component {
static name = name;
static schema = new Schema({
type: 'object',
properties: specification,
});
static properties = properties;
};
}
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 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 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, Components);
const query = new Query(parameters, fakeEcs(Components));
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()))
expect(expected.includes(_.id))
.to.equal(true);
}
}
@ -51,7 +56,7 @@ test('can query excluding', () => {
});
test('can deindex', () => {
const query = new Query(['A'], Components);
const query = new Query(['A'], fakeEcs(Components));
query.reindex([1, 2, 3]);
expect(query.count)
.to.equal(2);
@ -61,9 +66,9 @@ test('can deindex', () => {
});
test('can reindex', () => {
const Test = new (wrapSpecification('Test', {a: {type: 'int32', defaultValue: 420}}));
const Test = new (wrapProperties('Test', {a: {type: 'int32', defaultValue: 420}}));
Test.createMany([[2], [3]]);
const query = new Query(['Test'], {Test});
const query = new Query(['Test'], fakeEcs({Test}));
query.reindex([2, 3]);
expect(query.count)
.to.equal(2);
@ -74,10 +79,10 @@ test('can reindex', () => {
});
test('can select', () => {
const query = new Query(['A'], Components);
const query = new Query(['A'], fakeEcs(Components));
query.reindex([1, 2, 3]);
const it = query.select();
const result = it.next();
expect(result.value[0].a)
const {value: {A}} = it.next();
expect(A.a)
.to.equal(420);
});

View File

@ -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.Components);
this.queries[i] = new Query(queries[i], ecs);
}
this.reindex(ecs.entities);
}

View File

@ -13,20 +13,13 @@ function join(...parts) {
export default class Engine {
incomingActions = [];
connections = [];
connectedPlayers = new Map();
ecses = {};
frame = 0;
handle;
incomingActions = [];
last = Date.now();
server;
constructor(Server) {
@ -195,7 +188,7 @@ export default class Engine {
animation: 'moving:down',
frame: 0,
frames: 8,
source: '/assets/dude.json',
source: '/assets/dude/dude.json',
speed: 0.115,
},
Ticking: {},

View File

@ -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: {momentum: {x: 1, y: 0}},
Forces: {forceX: 1},
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
VisibleAabb: {},
}));
@ -38,6 +38,7 @@ 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))
@ -50,12 +51,14 @@ 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.
entity.Momentum.x = -1;
engine.setClean();
entity.Forces.forceX = -1;
engine.tick(1);
expect(engine.updateFor(0))
.to.deep.include({3: ecs.get(3).toJSON()});

View File

@ -63,7 +63,7 @@ export default function EcsComponent() {
}
const entity = ecs.get(mainEntity);
const {Camera} = entity;
const {TileLayers} = ecs.get(1);
const {TileLayers: {layers: [layer]}} = 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={TileLayers.layers[0]} />
<TileLayer tileLayer={layer} />
{activeTool && (
<TargetingGrid
ecs={ecs}
entity={entity}
tileLayer={layer}
/>
)}
<Entities entities={entities} />
{activeTool && (
<TargetingGhost
ecs={ecs}
entity={entity}
projection={activeTool.projection}
tileLayer={layer}
/>
)}
</Container>

View File

@ -1,65 +1,14 @@
import {Container, Graphics} from '@pixi/react';
import {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} />
);
}
import Entity from './entity.jsx';
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
<Entity
entity={entities[id]}
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}
</>
)
return <>{renderables}</>;
}

View File

@ -0,0 +1,56 @@
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);

View File

@ -33,7 +33,7 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
},
})
export default function TargetingGhost({ecs, entity, projection}) {
export default function TargetingGhost({entity, projection, tileLayer}) {
const [radians, setRadians] = useState(0);
useEffect(() => {
const handle = setInterval(() => {
@ -43,12 +43,12 @@ export default function TargetingGhost({ecs, entity, projection}) {
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 >= layer.area.x || y >= layer.area.y) {
if (x < 0 || y < 0 || x >= area.x || y >= area.y) {
continue;
}
ghosts.push(

View File

@ -28,7 +28,7 @@ function makeFade(renderer) {
}
const TargetingGridInternal = PixiComponent('TargetingGrid', {
create: ({app, ecs}) => {
create: ({app, tileLayer}) => {
const fade = makeFade(app.renderer);
const grid = new Graphics();
const lineWidth = 1;
@ -59,11 +59,10 @@ const TargetingGridInternal = PixiComponent('TargetingGrid', {
innerGrid.mask = fade;
const container = new Container();
container.addChild(fade, grid, innerGrid);
// Clamp within layer area.
// Clamp within tile layer area.
const area = new Graphics();
area.beginFill(0xffffff);
const {TileLayers: {layers: [layer]}} = ecs.get(1)
const {x, y} = layer.area;
const {x, y} = tileLayer.area;
area.drawRect(0, 0, x * tileSize.x, y * tileSize.y);
container.mask = area;
const top = new Container();
@ -86,7 +85,7 @@ const TargetingGridInternal = PixiComponent('TargetingGrid', {
},
})
export default function TargetingGrid({ecs, entity}) {
export default function TargetingGrid({entity, tileLayer}) {
const app = useApp();
const [radians, setRadians] = useState(0);
useEffect(() => {
@ -100,9 +99,9 @@ export default function TargetingGrid({ecs, entity}) {
return (
<TargetingGridInternal
app={app}
ecs={ecs}
entity={entity}
radians={radians}
tileLayer={tileLayer}
/>
);
}

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -1,123 +1,128 @@
const {Controlled, Emitter, Position, Sound, Sprite, Wielder} = wielder
const {TileLayers} = ecs.get(1)
const layer = TileLayers.layer(0)
const {Position, Wielder} = wielder
const projected = Wielder.project(Position.tile, item.tool.projection)
if (projected.length > 0) {
Controlled.locked = 1;
const [, direction] = Sprite.animation.split(':');
const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers} = ecs.get(1)
const layer = TileLayers.layer(0)
const dirtParticles = {
behaviors: [
{
type: 'moveAcceleration',
config: {
accel: {
x: 0,
y: 200,
},
minStart: 0,
maxStart: 0,
rotate: false,
}
},
{
type: 'moveSpeed',
config: {
speed: {
list: [
{
time: 0,
value: 60
},
{
time: 1,
value: 10
}
]
Controlled.locked = 1
const [, direction] = Sprite.animation.split(':')
const dirtParticles = {
behaviors: [
{
type: 'moveAcceleration',
config: {
accel: {
x: 0,
y: 200,
},
minStart: 0,
maxStart: 0,
rotate: false,
}
}
},
{
type: 'rotation',
config: {
accel: 0,
minSpeed: 0,
maxSpeed: 0,
minStart: 225,
maxStart: 320
}
},
{
type: 'scale',
config: {
scale: {
list: [
{
value: 0.25,
time: 0,
},
{
value: 0.125,
time: 1,
},
]
}
}
},
{
type: 'textureSingle',
config: {
texture: 'tileset/7',
}
},
],
lifetime: {
min: 0.25,
max: 0.25,
},
frequency: 0.01,
emitterLifetime: 0.25,
pos: {
x: 0,
y: 0
},
};
for (let i = 0; i < 2; ++i) {
Sound.play('/assets/hoe/dig.wav');
for (let i = 0; i < projected.length; ++i) {
Emitter.emit({
...dirtParticles,
behaviors: [
...dirtParticles.behaviors,
{
type: 'spawnShape',
config: {
type: 'rect',
data: {
x: projected[i].x * layer.tileSize.x,
y: projected[i].y * layer.tileSize.y,
w: layer.tileSize.x,
h: layer.tileSize.y,
}
},
{
type: 'moveSpeed',
config: {
speed: {
list: [
{
time: 0,
value: 60
},
{
time: 1,
value: 10
}
]
}
}
]
})
}
Sprite.animation = ['moving', direction].join(':');
await wait(300)
Sprite.animation = ['idle', direction].join(':');
await wait(100)
}
},
{
type: 'rotation',
config: {
accel: 0,
minSpeed: 0,
maxSpeed: 0,
minStart: 225,
maxStart: 320
}
},
{
type: 'scale',
config: {
scale: {
list: [
{
value: 0.25,
time: 0,
},
{
value: 0.125,
time: 1,
},
]
}
}
},
{
type: 'textureSingle',
config: {
texture: 'tileset/7',
}
},
],
lifetime: {
min: 0.25,
max: 0.25,
},
frequency: 0.01,
emitterLifetime: 0.25,
pos: {
x: 0,
y: 0
},
};
for (let i = 0; i < projected.length; ++i) {
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
layer.stamp(projected[i], [[6]])
for (let i = 0; i < 2; ++i) {
Sound.play('/assets/hoe/dig.wav');
for (let i = 0; i < projected.length; ++i) {
Emitter.emit({
...dirtParticles,
behaviors: [
...dirtParticles.behaviors,
{
type: 'spawnShape',
config: {
type: 'rect',
data: {
x: projected[i].x * layer.tileSize.x,
y: projected[i].y * layer.tileSize.y,
w: layer.tileSize.x,
h: layer.tileSize.y,
}
}
}
]
})
}
Sprite.animation = ['moving', direction].join(':');
await wait(300)
Sprite.animation = ['idle', direction].join(':');
await wait(100)
}
else if ([6].includes(layer.tile(projected[i]))) {
layer.stamp(projected[i], [[7]])
}
}
Controlled.locked = 0;
for (let i = 0; i < projected.length; ++i) {
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
layer.stamp(projected[i], [[6]])
}
else if ([6].includes(layer.tile(projected[i]))) {
layer.stamp(projected[i], [[7]])
}
}
Controlled.locked = 0;
}