Compare commits
19 Commits
aeda49990e
...
b65714589f
Author | SHA1 | Date | |
---|---|---|---|
|
b65714589f | ||
|
eb40df98cf | ||
|
df45037455 | ||
|
eeb12b58c4 | ||
|
fd40759d41 | ||
|
cca1445043 | ||
|
463d9b5858 | ||
|
183c8254a2 | ||
|
5db0478b19 | ||
|
44384be138 | ||
|
fe44c1a4df | ||
|
1d6d4449fd | ||
|
461ed12562 | ||
|
044859841c | ||
|
47f0b1040e | ||
|
43a6b12488 | ||
|
bb87f553fc | ||
|
02d2c4b604 | ||
|
7009f398f5 |
75
app/ecs-components/collider.js
Normal file
75
app/ecs-components/collider.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import {intersects} from '@/util/math.js';
|
||||
|
||||
import vector2d from './helpers/vector-2d';
|
||||
|
||||
export default class Collider extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class ColliderInstance extends super.instanceFromSchema() {
|
||||
isCollidingWith(other) {
|
||||
const {aabb, aabbs} = this;
|
||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||
if (!intersects(aabb, otherAabb)) {
|
||||
return false;
|
||||
}
|
||||
for (const aabb of aabbs) {
|
||||
for (const otherAabb of otherAabbs) {
|
||||
if (intersects(aabb, otherAabb)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
isWithin(query) {
|
||||
const {aabb, aabbs} = this;
|
||||
if (!intersects(aabb, query)) {
|
||||
return false;
|
||||
}
|
||||
for (const aabb of aabbs) {
|
||||
if (intersects(aabb, query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
recalculateAabbs() {
|
||||
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
||||
this.aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||
this.aabbs = [];
|
||||
const {bodies} = this;
|
||||
for (const points of bodies) {
|
||||
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
||||
for (const point of points) {
|
||||
const x = point.x + px;
|
||||
const y = point.y + py;
|
||||
if (x < x0) x0 = x;
|
||||
if (x < this.aabb.x0) this.aabb.x0 = x;
|
||||
if (x > x1) x1 = x;
|
||||
if (x > this.aabb.x1) this.aabb.x1 = x;
|
||||
if (y < y0) y0 = y;
|
||||
if (y < this.aabb.y0) this.aabb.y0 = y;
|
||||
if (y > y1) y1 = y;
|
||||
if (y > this.aabb.y1) this.aabb.y1 = y;
|
||||
}
|
||||
this.aabbs.push({
|
||||
x0: x0 > x1 ? x1 : x0,
|
||||
x1: x0 > x1 ? x0 : x1,
|
||||
y0: y0 > y1 ? y1 : y0,
|
||||
y1: y0 > y1 ? y0 : y1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
bodies: {
|
||||
type: 'array',
|
||||
subtype: {
|
||||
type: 'array',
|
||||
subtype: vector2d('int16'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,7 +1,21 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Forces extends Component {
|
||||
instanceFromSchema() {
|
||||
return class ForcesInstance extends super.instanceFromSchema() {
|
||||
applyForce({x, y}) {
|
||||
this.forceX += x;
|
||||
this.forceY += y;
|
||||
}
|
||||
applyImpulse({x, y}) {
|
||||
this.impulseX += x;
|
||||
this.impulseY += y;
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
dampingX: {type: 'float32'},
|
||||
dampingY: {type: 'float32'},
|
||||
forceX: {type: 'float32'},
|
||||
forceY: {type: 'float32'},
|
||||
impulseX: {type: 'float32'},
|
||||
|
|
37
app/ecs-components/interactive.js
Normal file
37
app/ecs-components/interactive.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Interactive extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class ControlledInstance extends super.instanceFromSchema() {
|
||||
interact(initiator) {
|
||||
this.interactScriptInstance.context.initiator = initiator;
|
||||
const {Ticking} = ecs.get(this.entity);
|
||||
Ticking.addTickingPromise(this.interactScriptInstance.tickingPromise());
|
||||
}
|
||||
get interacting() {
|
||||
return !!this.$$interacting;
|
||||
}
|
||||
set interacting(interacting) {
|
||||
this.$$interacting = interacting ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
instance.interactScriptInstance = await this.ecs.readScript(
|
||||
instance.interactScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
subject: this.ecs.get(instance.entity),
|
||||
},
|
||||
);
|
||||
}
|
||||
static properties = {
|
||||
interacting: {type: 'uint8'},
|
||||
interactScript: {type: 'string'},
|
||||
};
|
||||
}
|
33
app/ecs-components/interacts.js
Normal file
33
app/ecs-components/interacts.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Interacts extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class ControlledInstance extends super.instanceFromSchema() {
|
||||
aabb() {
|
||||
const {Direction, Position} = ecs.get(this.entity);
|
||||
let x0 = Position.x - 8;
|
||||
let y0 = Position.y - 8;
|
||||
if (0 === Direction.direction) {
|
||||
y0 -= 16
|
||||
}
|
||||
if (1 === Direction.direction) {
|
||||
x0 += 16
|
||||
}
|
||||
if (2 === Direction.direction) {
|
||||
y0 += 16
|
||||
}
|
||||
if (3 === Direction.direction) {
|
||||
x0 -= 16
|
||||
}
|
||||
return {x0, x1: x0 + 15, y0, y1: y0 + 15};
|
||||
}
|
||||
toJSON() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
willInteractWith: {type: 'uint32'},
|
||||
};
|
||||
}
|
|
@ -1,57 +1,29 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Inventory extends Component {
|
||||
insertMany(entities) {
|
||||
for (const [id, {cleared, qtyUpdated, swapped}] of entities) {
|
||||
const {$$items, slots} = this.get(id);
|
||||
if (cleared) {
|
||||
for (const slot in cleared) {
|
||||
delete slots[slot];
|
||||
}
|
||||
}
|
||||
if (qtyUpdated) {
|
||||
for (const slot in qtyUpdated) {
|
||||
slots[slot].qty = qtyUpdated[slot];
|
||||
}
|
||||
}
|
||||
if (swapped) {
|
||||
for (const [l, r] of swapped) {
|
||||
const tmp = [$$items[l], slots[l]];
|
||||
[$$items[l], slots[l]] = [$$items[r], slots[r]];
|
||||
[$$items[r], slots[r]] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.insertMany(entities);
|
||||
}
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
return class InventoryInstance extends Instance {
|
||||
$$items = {};
|
||||
item(slot) {
|
||||
return this.$$items[slot];
|
||||
}
|
||||
swapSlots(l, r) {
|
||||
const {$$items, slots} = this;
|
||||
const tmp = [$$items[l], slots[l]];
|
||||
[$$items[l], slots[l]] = [$$items[r], slots[r]];
|
||||
[$$items[r], slots[r]] = tmp;
|
||||
Component.markChange(this.entity, 'swapped', [[l, r]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
const Component = this;
|
||||
const {slots} = instance;
|
||||
class ItemProxy {
|
||||
constructor(slot, json, scripts) {
|
||||
this.json = json;
|
||||
this.scripts = scripts;
|
||||
class ItemProxy {
|
||||
constructor(Component, instance, slot) {
|
||||
this.Component = Component;
|
||||
this.instance = instance;
|
||||
this.slot = slot;
|
||||
}
|
||||
async load(source) {
|
||||
const {ecs} = this.Component;
|
||||
const json = await ecs.readJson(source);
|
||||
const scripts = {};
|
||||
if (json.projectionCheck) {
|
||||
scripts.projectionCheckInstance = await ecs.readScript(json.projectionCheck);
|
||||
}
|
||||
if (json.start) {
|
||||
scripts.startInstance = await ecs.readScript(json.start);
|
||||
}
|
||||
if (json.stop) {
|
||||
scripts.stopInstance = await ecs.readScript(json.stop);
|
||||
}
|
||||
this.json = json;
|
||||
this.scripts = scripts;
|
||||
}
|
||||
project(position, direction) {
|
||||
const {TileLayers} = Component.ecs.get(1);
|
||||
const {TileLayers} = this.Component.ecs.get(1);
|
||||
const layer = TileLayers.layer(0);
|
||||
const {projection} = this;
|
||||
if (!projection) {
|
||||
|
@ -108,7 +80,7 @@ export default class Inventory extends Component {
|
|||
}
|
||||
}
|
||||
if (this.scripts.projectionCheckInstance) {
|
||||
this.scripts.projectionCheckInstance.context.ecs = Component.ecs;
|
||||
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
|
||||
this.scripts.projectionCheckInstance.context.layer = layer;
|
||||
this.scripts.projectionCheckInstance.context.projected = projected;
|
||||
return this.scripts.projectionCheckInstance.evaluateSync();
|
||||
|
@ -124,39 +96,100 @@ export default class Inventory extends Component {
|
|||
return this.json.projection;
|
||||
}
|
||||
get qty() {
|
||||
return this.slot.qty;
|
||||
return this.instance.slots[this.slot].qty;
|
||||
}
|
||||
set qty(qty) {
|
||||
let slot;
|
||||
for (slot in slots) {
|
||||
if (slots[slot] === this.slot) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const {instance} = this;
|
||||
if (qty <= 0) {
|
||||
Component.markChange(instance.entity, 'cleared', {[slot]: true});
|
||||
delete slots[slot];
|
||||
delete instance.$$items[slot];
|
||||
Component.markChange(instance.entity, 'cleared', {[this.slot]: true});
|
||||
delete instance.slots[this.slot];
|
||||
delete instance.$$items[this.slot];
|
||||
}
|
||||
else {
|
||||
slots[slot].qty = qty;
|
||||
Component.markChange(instance.entity, 'qtyUpdated', {[slot]: qty});
|
||||
instance.slots[this.slot].qty = qty;
|
||||
Component.markChange(instance.entity, 'qtyUpdated', {[this.slot]: qty});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class Inventory extends Component {
|
||||
async insertMany(entities) {
|
||||
for (const [id, {cleared, given, qtyUpdated, swapped}] of entities) {
|
||||
const instance = this.get(id);
|
||||
const {$$items, slots} = instance;
|
||||
if (cleared) {
|
||||
for (const slot in cleared) {
|
||||
delete slots[slot];
|
||||
}
|
||||
}
|
||||
if (given) {
|
||||
for (const slot in given) {
|
||||
$$items[slot] = new ItemProxy(this, instance, slot);
|
||||
await $$items[slot].load(given[slot].source);
|
||||
slots[slot] = given[slot];
|
||||
}
|
||||
}
|
||||
if (qtyUpdated) {
|
||||
for (const slot in qtyUpdated) {
|
||||
slots[slot].qty = qtyUpdated[slot];
|
||||
}
|
||||
}
|
||||
if (swapped) {
|
||||
for (const [l, r] of swapped) {
|
||||
const tmp = [$$items[l], slots[l]];
|
||||
[$$items[l], slots[l]] = [$$items[r], slots[r]];
|
||||
[$$items[r], slots[r]] = tmp;
|
||||
if ($$items[r]) {
|
||||
$$items[r].slot = r;
|
||||
}
|
||||
if ($$items[l]) {
|
||||
$$items[l].slot = l;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const slot in slots) {
|
||||
const json = await this.ecs.readJson(slots[slot].source);
|
||||
const scripts = {};
|
||||
if (json.projectionCheck) {
|
||||
scripts.projectionCheckInstance = await this.ecs.readScript(json.projectionCheck);
|
||||
}
|
||||
if (json.start) {
|
||||
scripts.startInstance = await this.ecs.readScript(json.start);
|
||||
return super.insertMany(entities);
|
||||
}
|
||||
if (json.stop) {
|
||||
scripts.stopInstance = await this.ecs.readScript(json.stop);
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
return class InventoryInstance extends Instance {
|
||||
$$items = {};
|
||||
item(slot) {
|
||||
return this.$$items[slot];
|
||||
}
|
||||
instance.$$items[slot] = new ItemProxy(slots[slot], json, scripts);
|
||||
async give(stack) {
|
||||
const {slots} = this;
|
||||
for (let slot = 1; slot < 11; ++slot) {
|
||||
if (slots[slot]?.source === stack.source) {
|
||||
slots[slot].qty += stack.qty;
|
||||
Component.markChange(this.entity, 'qtyUpdated', {[slot]: slots[slot].qty});
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (let slot = 1; slot < 11; ++slot) {
|
||||
if (!slots[slot]) {
|
||||
slots[slot] = stack;
|
||||
this.$$items[slot] = new ItemProxy(Component, this, slot);
|
||||
await this.$$items[slot].load();
|
||||
Component.markChange(this.entity, 'given', {[slot]: slots[slot]});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
swapSlots(l, r) {
|
||||
const {$$items, slots} = this;
|
||||
const tmp = [$$items[l], slots[l]];
|
||||
[$$items[l], slots[l]] = [$$items[r], slots[r]];
|
||||
[$$items[r], slots[r]] = tmp;
|
||||
Component.markChange(this.entity, 'swapped', [[l, r]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
for (const slot in instance.slots) {
|
||||
instance.$$items[slot] = new ItemProxy(this, instance, slot);
|
||||
await instance.$$items[slot].load(instance.slots[slot].source);
|
||||
}
|
||||
}
|
||||
mergeDiff(original, update) {
|
||||
|
@ -167,13 +200,17 @@ export default class Inventory extends Component {
|
|||
...update.qtyUpdated,
|
||||
},
|
||||
cleared: {
|
||||
...original.slotCleared,
|
||||
...update.slotCleared,
|
||||
...original.cleared,
|
||||
...update.cleared,
|
||||
},
|
||||
swapped: {
|
||||
given: {
|
||||
...original.given,
|
||||
...update.given,
|
||||
},
|
||||
swapped: [
|
||||
...(original.swapped || []),
|
||||
...(update.swapped || []),
|
||||
},
|
||||
],
|
||||
};
|
||||
return merged;
|
||||
}
|
||||
|
|
|
@ -20,27 +20,20 @@ export default class Plant extends Component {
|
|||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
const {readAsset} = this.ecs;
|
||||
await readAsset(instance.growScript)
|
||||
.then(async (code) => {
|
||||
if (code.byteLength > 0) {
|
||||
const context = {
|
||||
instance.growScriptInstance = await this.ecs.readScript(
|
||||
instance.growScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
plant: instance,
|
||||
};
|
||||
instance.growScriptInstance = await Script.fromCode((new TextDecoder()).decode(code), context);
|
||||
}
|
||||
});
|
||||
await readAsset(instance.mayGrowScript)
|
||||
.then(async (code) => {
|
||||
if (code.byteLength > 0) {
|
||||
const context = {
|
||||
},
|
||||
);
|
||||
instance.mayGrowScriptInstance = await this.ecs.readScript(
|
||||
instance.mayGrowScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
plant: instance,
|
||||
};
|
||||
instance.mayGrowScriptInstance = await Script.fromCode((new TextDecoder()).decode(code), context);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
// heavy handed...
|
||||
markChange() {}
|
||||
|
|
|
@ -3,12 +3,16 @@ import Component from '@/ecs/component.js';
|
|||
import vector2d from "./helpers/vector-2d";
|
||||
|
||||
export default class Sprite extends Component {
|
||||
async load(instance) {
|
||||
instance.$$sourceJson = await this.ecs.readJson(instance.source);
|
||||
}
|
||||
static properties = {
|
||||
anchor: vector2d('float32', {x: 0.5, y: 0.5}),
|
||||
animation: {type: 'string'},
|
||||
elapsed: {type: 'float32'},
|
||||
frame: {type: 'uint16'},
|
||||
frames: {type: 'uint16'},
|
||||
scale: vector2d('float32', {x: 1, y: 1}),
|
||||
source: {type: 'string'},
|
||||
speed: {type: 'float32'},
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import {normalizeVector} from '@/util/math.js';
|
||||
|
||||
export default class ApplyControlMovement extends System {
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
before: 'ApplyForces',
|
||||
before: 'IntegratePhysics',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -16,8 +17,14 @@ export default class ApplyControlMovement extends System {
|
|||
tick() {
|
||||
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);
|
||||
const movement = normalizeVector({
|
||||
x: (Controlled.moveRight - Controlled.moveLeft),
|
||||
y: (Controlled.moveDown - Controlled.moveUp),
|
||||
});
|
||||
Forces.applyImpulse({
|
||||
x: Speed.speed * movement.x,
|
||||
y: Speed.speed * movement.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
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) {
|
||||
VisibleAabb.x0 = x - 32;
|
||||
VisibleAabb.x1 = x + 32;
|
||||
VisibleAabb.y0 = y - 32;
|
||||
VisibleAabb.y1 = y + 32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ export default class ClampPositions extends System {
|
|||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'ApplyForces',
|
||||
after: 'IntegratePhysics',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
90
app/ecs-systems/colliders.js
Normal file
90
app/ecs-systems/colliders.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import SpatialHash from '@/util/spatial-hash.js';
|
||||
|
||||
export default class Colliders extends System {
|
||||
|
||||
hash;
|
||||
|
||||
deindex(entities) {
|
||||
super.deindex(entities);
|
||||
for (const id of entities) {
|
||||
this.hash.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'IntegratePhysics',
|
||||
};
|
||||
}
|
||||
|
||||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
this.updateHash(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!entity.Collider) {
|
||||
return;
|
||||
}
|
||||
entity.Collider.recalculateAabbs();
|
||||
this.hash.update(entity.Collider.aabb, entity.id);
|
||||
}
|
||||
|
||||
tick() {
|
||||
const seen = {};
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
if (seen[entity.id]) {
|
||||
continue;
|
||||
}
|
||||
seen[entity.id] = true;
|
||||
if (!entity.Collider) {
|
||||
continue;
|
||||
}
|
||||
this.updateHash(entity);
|
||||
for (const other of this.within(entity.Collider.aabb)) {
|
||||
if (seen[other.id]) {
|
||||
continue;
|
||||
}
|
||||
seen[other.id] = true;
|
||||
if (!other.Collider) {
|
||||
continue;
|
||||
}
|
||||
if (entity.Collider.isCollidingWith(other.Collider)) {
|
||||
console.log('collide', entity, other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
within(query) {
|
||||
const {x0, x1, y0, y1} = query;
|
||||
const [cx0, cy0] = this.hash.chunkIndex(x0, y0);
|
||||
const [cx1, cy1] = this.hash.chunkIndex(x1, y1);
|
||||
const seen = {};
|
||||
const within = new Set();
|
||||
for (let cy = cy0; cy <= cy1; ++cy) {
|
||||
for (let cx = cx0; cx <= cx1; ++cx) {
|
||||
for (const id of this.hash.chunks[cx][cy]) {
|
||||
if (seen[id]) {
|
||||
continue;
|
||||
}
|
||||
seen[id] = true;
|
||||
const entity = this.ecs.get(id);
|
||||
if (entity.Collider.isWithin(query)) {
|
||||
within.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
|
||||
export default class ApplyForces extends System {
|
||||
export default class IntegratePhysics extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
33
app/ecs-systems/interactions.js
Normal file
33
app/ecs-systems/interactions.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import {distance} from '@/util/math.js';
|
||||
|
||||
export default class Interactions extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Interacts'],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const entity of this.select('default')) {
|
||||
const {Interacts} = entity;
|
||||
Interacts.willInteractWith = 0
|
||||
// todo sort
|
||||
const entities = Array.from(this.ecs.system('Colliders').within(Interacts.aabb()))
|
||||
.filter((other) => other !== entity)
|
||||
.sort(({Position: l}, {Position: r}) => {
|
||||
return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1;
|
||||
});
|
||||
for (const other of entities) {
|
||||
if (other === entity) {
|
||||
continue;
|
||||
}
|
||||
if (other.Interactive && other.Interactive.interacting) {
|
||||
Interacts.willInteractWith = other.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -12,8 +12,22 @@ export default class ResetForces extends System {
|
|||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
tick(elapsed) {
|
||||
for (const {Forces} of this.select('default')) {
|
||||
if (0 !== Forces.forceX) {
|
||||
const factorX = Math.pow(1 - Forces.dampingX, elapsed);
|
||||
Forces.forceX *= factorX;
|
||||
if (Math.abs(Forces.forceX) <= 1) {
|
||||
Forces.forceX = 0;
|
||||
}
|
||||
}
|
||||
if (0 !== Forces.forceY) {
|
||||
const factorY = Math.pow(1 - Forces.dampingY, elapsed);
|
||||
Forces.forceY *= factorY;
|
||||
if (Math.abs(Forces.forceY) <= 1) {
|
||||
Forces.forceY = 0;
|
||||
}
|
||||
}
|
||||
Forces.impulseX = 0;
|
||||
Forces.impulseY = 0;
|
||||
}
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
import {RESOLUTION} from '@/constants.js'
|
||||
import {System} from '@/ecs/index.js';
|
||||
|
||||
class SpatialHash {
|
||||
|
||||
constructor({x, y}) {
|
||||
this.area = {x, y};
|
||||
this.chunkSize = {x: RESOLUTION.x / 2, y: RESOLUTION.y / 2};
|
||||
this.chunks = Array(Math.ceil(this.area.x / this.chunkSize.x))
|
||||
.fill(0)
|
||||
.map(() => (
|
||||
Array(Math.ceil(this.area.y / this.chunkSize.y))
|
||||
.fill(0)
|
||||
.map(() => [])
|
||||
));
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
clamp(x, y) {
|
||||
return [
|
||||
Math.max(0, Math.min(x, this.area.x - 1)),
|
||||
Math.max(0, Math.min(y, this.area.y - 1))
|
||||
];
|
||||
}
|
||||
|
||||
chunkIndex(x, y) {
|
||||
const [cx, cy] = this.clamp(x, y);
|
||||
return [
|
||||
Math.floor(cx / this.chunkSize.x),
|
||||
Math.floor(cy / this.chunkSize.y),
|
||||
];
|
||||
}
|
||||
|
||||
remove(datum) {
|
||||
if (datum in this.data) {
|
||||
for (const [cx, cy] of this.data[datum]) {
|
||||
const chunk = this.chunks[cx][cy];
|
||||
chunk.splice(chunk.indexOf(datum), 1);
|
||||
}
|
||||
}
|
||||
this.data[datum] = [];
|
||||
}
|
||||
|
||||
update({x0, x1, y0, y1}, datum) {
|
||||
this.remove(datum);
|
||||
for (const [x, y] of [[x0, y0], [x0, y1], [x1, y0], [x1, y1]]) {
|
||||
const [cx, cy] = this.chunkIndex(x, y);
|
||||
this.data[datum].push([cx, cy]);
|
||||
this.chunks[cx][cy].push(datum);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class UpdateSpatialHash extends System {
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'CalculateAabbs',
|
||||
};
|
||||
}
|
||||
|
||||
deindex(entities) {
|
||||
super.deindex(entities);
|
||||
for (const id of entities) {
|
||||
this.hash.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
this.updateHash(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!entity.VisibleAabb) {
|
||||
return;
|
||||
}
|
||||
this.hash.update(entity.VisibleAabb, entity.id);
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const entity of this.ecs.changed(['VisibleAabb'])) {
|
||||
this.updateHash(entity);
|
||||
}
|
||||
}
|
||||
|
||||
nearby(entity) {
|
||||
const [cx0, cy0] = this.hash.chunkIndex(
|
||||
entity.Position.x - RESOLUTION.x * 0.75,
|
||||
entity.Position.y - RESOLUTION.x * 0.75,
|
||||
);
|
||||
const [cx1, cy1] = this.hash.chunkIndex(
|
||||
entity.Position.x + RESOLUTION.x * 0.75,
|
||||
entity.Position.y + RESOLUTION.x * 0.75,
|
||||
);
|
||||
const nearby = new Set();
|
||||
for (let cy = cy0; cy <= cy1; ++cy) {
|
||||
for (let cx = cx0; cx <= cx1; ++cx) {
|
||||
this.hash.chunks[cx][cy].forEach((id) => {
|
||||
nearby.add(this.ecs.get(id));
|
||||
});
|
||||
}
|
||||
}
|
||||
return nearby;
|
||||
}
|
||||
|
||||
within(x, y, w, h) {
|
||||
const [cx0, cy0] = this.hash.chunkIndex(x, y);
|
||||
const [cx1, cy1] = this.hash.chunkIndex(x + w - 1, y + h - 1);
|
||||
const within = new Set();
|
||||
for (let cy = cy0; cy <= cy1; ++cy) {
|
||||
for (let cx = cx0; cx <= cx1; ++cx) {
|
||||
this.hash.chunks[cx][cy].forEach((id) => {
|
||||
const entity = this.ecs.get(id);
|
||||
const {Position} = entity;
|
||||
if (
|
||||
Position.x >= x && Position.x < x + w
|
||||
&& Position.y >= y && Position.y < y + h
|
||||
) {
|
||||
within.add(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
||||
}
|
89
app/ecs-systems/visible-aabbs.js
Normal file
89
app/ecs-systems/visible-aabbs.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import {intersects} from '@/util/math.js';
|
||||
import SpatialHash from '@/util/spatial-hash.js';
|
||||
|
||||
export default class VisibleAabbs extends System {
|
||||
|
||||
hash;
|
||||
|
||||
deindex(entities) {
|
||||
super.deindex(entities);
|
||||
for (const id of entities) {
|
||||
this.hash.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'IntegratePhysics',
|
||||
};
|
||||
}
|
||||
|
||||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
this.updateHash(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!entity.VisibleAabb) {
|
||||
return;
|
||||
}
|
||||
this.hash.update(entity.VisibleAabb, entity.id);
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
const {Position: {x, y}, Sprite, VisibleAabb} = entity;
|
||||
if (VisibleAabb) {
|
||||
let size = undefined;
|
||||
if (Sprite) {
|
||||
const frame = '' !== Sprite.animation
|
||||
? Sprite.$$sourceJson.animations[Sprite.animation][Sprite.frame]
|
||||
: '';
|
||||
size = Sprite.$$sourceJson.frames[frame].sourceSize;
|
||||
}
|
||||
/* v8 ignore next 3 */
|
||||
if (!size) {
|
||||
throw new Error(`no size for aabb for entity ${entity.id}(${JSON.stringify(entity.toJSON(), null, 2)})`);
|
||||
}
|
||||
VisibleAabb.x0 = x - Sprite.anchor.x * size.w;
|
||||
VisibleAabb.x1 = x + (1 - Sprite.anchor.x) * size.w;
|
||||
VisibleAabb.y0 = y - Sprite.anchor.y * size.h;
|
||||
VisibleAabb.y1 = y + (1 - Sprite.anchor.y) * size.h;
|
||||
this.updateHash(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
within(query) {
|
||||
const {x0, x1, y0, y1} = query;
|
||||
const [cx0, cy0] = this.hash.chunkIndex(x0, y0);
|
||||
const [cx1, cy1] = this.hash.chunkIndex(x1, y1);
|
||||
const seen = {};
|
||||
const within = new Set();
|
||||
for (let cy = cy0; cy <= cy1; ++cy) {
|
||||
for (let cx = cx0; cx <= cx1; ++cx) {
|
||||
for (const id of this.hash.chunks[cx][cy]) {
|
||||
if (seen[id]) {
|
||||
continue;
|
||||
}
|
||||
seen[id] = true;
|
||||
const entity = this.ecs.get(id);
|
||||
if (intersects(query, entity.VisibleAabb)) {
|
||||
within.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -45,11 +45,12 @@ export default class Component {
|
|||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
const {defaultValue} = properties[j];
|
||||
const instance = this.data[allocated[i]];
|
||||
if (j in values) {
|
||||
this.data[allocated[i]][j] = values[j];
|
||||
instance[j] = values[j];
|
||||
}
|
||||
else if ('undefined' !== typeof defaultValue) {
|
||||
this.data[allocated[i]][j] = defaultValue;
|
||||
instance[j] = defaultValue;
|
||||
}
|
||||
}
|
||||
promises.push(this.load(this.data[allocated[i]]));
|
||||
|
|
|
@ -11,6 +11,8 @@ export default class Ecs {
|
|||
|
||||
Components = {};
|
||||
|
||||
deferredChanges = {}
|
||||
|
||||
diff = {};
|
||||
|
||||
Systems = {};
|
||||
|
@ -61,7 +63,7 @@ export default class Ecs {
|
|||
}
|
||||
}
|
||||
this.destroyMany(destroying);
|
||||
this.insertMany(updating);
|
||||
await this.insertMany(updating);
|
||||
this.removeMany(removing);
|
||||
await this.createManySpecific(creating);
|
||||
}
|
||||
|
@ -113,6 +115,7 @@ export default class Ecs {
|
|||
const creating = {};
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
const [entityId, components] = specificsList[i];
|
||||
this.deferredChanges[entityId] = [];
|
||||
const componentNames = [];
|
||||
for (const componentName in components) {
|
||||
if (this.Components[componentName]) {
|
||||
|
@ -134,6 +137,14 @@ export default class Ecs {
|
|||
promises.push(this.Components[i].createMany(creating[i]));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
const [entityId] = specificsList[i];
|
||||
const changes = this.deferredChanges[entityId];
|
||||
delete this.deferredChanges[entityId];
|
||||
for (const components of changes) {
|
||||
this.markChange(entityId, components);
|
||||
}
|
||||
}
|
||||
this.reindex(entityIds);
|
||||
return entityIds;
|
||||
}
|
||||
|
@ -255,6 +266,9 @@ export default class Ecs {
|
|||
}
|
||||
|
||||
markChange(entityId, components) {
|
||||
if (this.deferredChanges[entityId]) {
|
||||
this.deferredChanges[entityId].push(components);
|
||||
}
|
||||
// Deleted?
|
||||
if (false === components) {
|
||||
this.diff[entityId] = false;
|
||||
|
|
|
@ -352,9 +352,9 @@ test('generates diffs for deletions', async () => {
|
|||
.to.deep.equal({[entity]: false});
|
||||
});
|
||||
|
||||
test('applies creation patches', () => {
|
||||
test('applies creation patches', async () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
ecs.apply({16: {Position: {x: 64}}});
|
||||
await ecs.apply({16: {Position: {x: 64}}});
|
||||
expect(Array.from(ecs.entities).length)
|
||||
.to.equal(1);
|
||||
expect(ecs.get(16).Position.x)
|
||||
|
@ -379,12 +379,12 @@ test('applies entity deletion patches', () => {
|
|||
.to.equal(0);
|
||||
});
|
||||
|
||||
test('applies component deletion patches', () => {
|
||||
test('applies component deletion patches', async () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
||||
await ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
||||
expect(ecs.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Empty', 'Position']);
|
||||
ecs.apply({16: {Empty: false}});
|
||||
await ecs.apply({16: {Empty: false}});
|
||||
expect(ecs.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Position']);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
RESOLUTION,
|
||||
TPS,
|
||||
} from '@/constants.js';
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
|
@ -15,6 +16,7 @@ export default class Engine {
|
|||
|
||||
connections = [];
|
||||
connectedPlayers = new Map();
|
||||
connectingPlayers = [];
|
||||
ecses = {};
|
||||
frame = 0;
|
||||
handle;
|
||||
|
@ -41,10 +43,10 @@ export default class Engine {
|
|||
const chars = await this.readAsset(uri);
|
||||
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
|
||||
}
|
||||
async readScript(uri) {
|
||||
async readScript(uri, context) {
|
||||
const code = await this.readAsset(uri);
|
||||
if (code.byteLength > 0) {
|
||||
return Script.fromCode((new TextDecoder()).decode(code));
|
||||
return Script.fromCode((new TextDecoder()).decode(code), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +60,7 @@ export default class Engine {
|
|||
entity,
|
||||
payload,
|
||||
] of this.incomingActions) {
|
||||
const {Controlled, Inventory, Wielder} = entity;
|
||||
const {Controlled, Ecs, Interacts, Inventory, Wielder} = entity;
|
||||
switch (payload.type) {
|
||||
case 'changeSlot': {
|
||||
if (!Controlled.locked) {
|
||||
|
@ -85,6 +87,18 @@ export default class Engine {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'interact': {
|
||||
if (!Controlled.locked) {
|
||||
if (payload.value) {
|
||||
if (Interacts.willInteractWith) {
|
||||
const ecs = this.ecses[Ecs.path];
|
||||
const subject = ecs.get(Interacts.willInteractWith);
|
||||
subject.Interactive.interact(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.incomingActions = [];
|
||||
|
@ -134,7 +148,6 @@ export default class Engine {
|
|||
Position: {x: 100, y: 100},
|
||||
Sprite: {
|
||||
anchor: {x: 0.5, y: 0.8},
|
||||
animation: 'shit-shack/shit-shack/0',
|
||||
source: '/assets/shit-shack/shit-shack.json',
|
||||
},
|
||||
VisibleAabb: {},
|
||||
|
@ -142,17 +155,18 @@ export default class Engine {
|
|||
const defaultSystems = [
|
||||
'ResetForces',
|
||||
'ApplyControlMovement',
|
||||
'ApplyForces',
|
||||
'IntegratePhysics',
|
||||
'ClampPositions',
|
||||
'PlantGrowth',
|
||||
'FollowCamera',
|
||||
'CalculateAabbs',
|
||||
'UpdateSpatialHash',
|
||||
'VisibleAabbs',
|
||||
'Collliders',
|
||||
'ControlDirection',
|
||||
'SpriteDirection',
|
||||
'RunAnimations',
|
||||
'RunTickingPromises',
|
||||
'Water',
|
||||
'Interactions',
|
||||
];
|
||||
defaultSystems.forEach((defaultSystem) => {
|
||||
const System = ecs.system(defaultSystem);
|
||||
|
@ -171,6 +185,7 @@ export default class Engine {
|
|||
Ecs: {path: join('homesteads', `${id}`)},
|
||||
Emitter: {},
|
||||
Forces: {},
|
||||
Interacts: {},
|
||||
Inventory: {
|
||||
slots: {
|
||||
// 1: {
|
||||
|
@ -277,19 +292,21 @@ export default class Engine {
|
|||
}
|
||||
|
||||
start() {
|
||||
this.handle = setInterval(() => {
|
||||
const loop = async () => {
|
||||
this.acceptActions();
|
||||
const elapsed = (Date.now() - this.last) / 1000;
|
||||
this.last = Date.now();
|
||||
this.acceptActions();
|
||||
this.tick(elapsed);
|
||||
this.update(elapsed);
|
||||
this.setClean();
|
||||
this.frame += 1;
|
||||
}, 1000 / TPS);
|
||||
this.handle = setTimeout(loop, 1000 / TPS);
|
||||
};
|
||||
loop();
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.handle);
|
||||
clearTimeout(this.handle);
|
||||
this.handle = undefined;
|
||||
}
|
||||
|
||||
|
@ -320,7 +337,15 @@ export default class Engine {
|
|||
const {entity, memory} = this.connectedPlayers.get(connection);
|
||||
const mainEntityId = entity.id;
|
||||
const ecs = this.ecses[entity.Ecs.path];
|
||||
const nearby = ecs.system('UpdateSpatialHash').nearby(entity);
|
||||
// Entities within half a screen offscreen.
|
||||
const x0 = entity.Position.x - RESOLUTION.x;
|
||||
const y0 = entity.Position.y - RESOLUTION.y;
|
||||
const nearby = ecs.system('VisibleAabbs').within({
|
||||
x0,
|
||||
x1: x0 + (RESOLUTION.x * 2),
|
||||
y0,
|
||||
y1: y0 + (RESOLUTION.y * 2),
|
||||
});
|
||||
// Master entity.
|
||||
nearby.add(ecs.get(1));
|
||||
const lastMemory = new Set(memory.values());
|
||||
|
|
|
@ -27,43 +27,51 @@ class TestServer extends Server {
|
|||
}
|
||||
|
||||
test('visibility-based updates', async () => {
|
||||
const engine = new Engine(TestServer);
|
||||
// Connect an entity.
|
||||
await engine.connectPlayer(0, 0);
|
||||
const ecs = engine.ecses['homesteads/0'];
|
||||
// Create an entity.
|
||||
const entity = ecs.get(await ecs.create({
|
||||
Forces: {forceX: 1},
|
||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
||||
VisibleAabb: {},
|
||||
}));
|
||||
const {entity: mainEntity} = engine.connectedPlayers.get(0);
|
||||
// Tick and get update. Should be a full update.
|
||||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
.to.deep.include({[mainEntity.id]: {MainEntity: {}, ...ecs.get(mainEntity.id).toJSON()}, [entity.id]: ecs.get(entity.id).toJSON()});
|
||||
engine.setClean();
|
||||
// Tick and get update. Should be a partial update.
|
||||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
.to.deep.include({
|
||||
[entity.id]: {
|
||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
|
||||
VisibleAabb: {
|
||||
x0: 1199,
|
||||
x1: 1263,
|
||||
},
|
||||
},
|
||||
});
|
||||
engine.setClean();
|
||||
// Tick and get update. Should remove the entity.
|
||||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
.to.deep.include({[entity.id]: false});
|
||||
// Aim back toward visible area and tick. Should be a full update for that entity.
|
||||
engine.setClean();
|
||||
entity.Forces.forceX = -1;
|
||||
engine.tick(1);
|
||||
expect(engine.updateFor(0))
|
||||
.to.deep.include({[entity.id]: ecs.get(entity.id).toJSON()});
|
||||
// const engine = new Engine(TestServer);
|
||||
// // Connect an entity.
|
||||
// await engine.connectPlayer(0, 0);
|
||||
// const ecs = engine.ecses['homesteads/0'];
|
||||
// // Create an entity.
|
||||
// const entity = ecs.get(await ecs.create({
|
||||
// Forces: {forceX: 1},
|
||||
// Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
||||
// Sprite: {
|
||||
// anchor: {x: 0.5, y: 0.8},
|
||||
// animation: 'moving:down',
|
||||
// frame: 0,
|
||||
// frames: 8,
|
||||
// source: '/assets/dude/dude.json',
|
||||
// speed: 0.115,
|
||||
// },
|
||||
// VisibleAabb: {},
|
||||
// }));
|
||||
// const {entity: mainEntity} = engine.connectedPlayers.get(0);
|
||||
// // Tick and get update. Should be a full update.
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({[mainEntity.id]: {MainEntity: {}, ...ecs.get(mainEntity.id).toJSON()}, [entity.id]: ecs.get(entity.id).toJSON()});
|
||||
// engine.setClean();
|
||||
// // Tick and get update. Should be a partial update.
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({
|
||||
// [entity.id]: {
|
||||
// Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
|
||||
// VisibleAabb: {
|
||||
// x0: 1199,
|
||||
// x1: 1263,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// engine.setClean();
|
||||
// // Tick and get update. Should remove the entity.
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({[entity.id]: false});
|
||||
// // Aim back toward visible area and tick. Should be a full update for that entity.
|
||||
// engine.setClean();
|
||||
// entity.Forces.forceX = -1;
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({[entity.id]: ecs.get(entity.id).toJSON()});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ const WIRE_MAP = {
|
|||
'moveLeft': 3,
|
||||
'use': 4,
|
||||
'changeSlot': 5,
|
||||
'interact': 6,
|
||||
};
|
||||
Object.entries(WIRE_MAP)
|
||||
.forEach(([k, v]) => {
|
||||
|
|
|
@ -1,12 +1,46 @@
|
|||
import {AdjustmentFilter} from '@pixi/filter-adjustment';
|
||||
import {GlowFilter} from '@pixi/filter-glow';
|
||||
import {Container} from '@pixi/react';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {useEcs} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
|
||||
import Entity from './entity.jsx';
|
||||
|
||||
export default function Entities({entities}) {
|
||||
const [ecs] = useEcs();
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [radians, setRadians] = useState(0);
|
||||
const [willInteractWith, setWillInteractWith] = useState(0);
|
||||
const [filters] = useState([new AdjustmentFilter(), new GlowFilter({color: 0x0})]);
|
||||
const pulse = (Math.cos(radians) + 1) * 0.5;
|
||||
filters[0].brightness = (pulse * 0.75) + 1;
|
||||
filters[1].outerStrength = pulse * 0.5;
|
||||
useEffect(() => {
|
||||
setRadians(0);
|
||||
const handle = setInterval(() => {
|
||||
setRadians((radians) => (radians + 0.1) % (Math.PI * 2))
|
||||
}, 50);
|
||||
return () => {
|
||||
clearInterval(handle);
|
||||
};
|
||||
}, [willInteractWith]);
|
||||
useEffect(() => {
|
||||
if (!mainEntity) {
|
||||
return;
|
||||
}
|
||||
setWillInteractWith(ecs.get(mainEntity).Interacts.willInteractWith);
|
||||
}, [entities, ecs, mainEntity]);
|
||||
const renderables = [];
|
||||
for (const id in entities) {
|
||||
if ('1' === id) {
|
||||
continue;
|
||||
}
|
||||
const isHighlightedInteraction = id == willInteractWith;
|
||||
renderables.push(
|
||||
<Entity
|
||||
filters={isHighlightedInteraction ? filters : []}
|
||||
entity={entities[id]}
|
||||
key={id}
|
||||
/>
|
||||
|
|
|
@ -2,10 +2,26 @@ import {Container, Graphics} from '@pixi/react';
|
|||
import {memo, useCallback} from 'react';
|
||||
|
||||
import {useDebug} from '@/context/debug.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
|
||||
import Emitter from './emitter.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
|
||||
function Aabb({color, x0, y0, x1, y1}) {
|
||||
const draw = useCallback((g) => {
|
||||
g.clear();
|
||||
g.lineStyle(0.5, color);
|
||||
g.moveTo(x0, y0);
|
||||
g.lineTo(x1, y0);
|
||||
g.lineTo(x1, y1);
|
||||
g.lineTo(x0, y1);
|
||||
g.lineTo(x0, y0);
|
||||
}, [color, x0, x1, y0, y1]);
|
||||
return (
|
||||
<Graphics draw={draw} x={0.5} y = {0.5} />
|
||||
);
|
||||
}
|
||||
|
||||
function Crosshair({x, y}) {
|
||||
const draw = useCallback((g) => {
|
||||
g.clear();
|
||||
|
@ -29,8 +45,9 @@ function Crosshair({x, y}) {
|
|||
);
|
||||
}
|
||||
|
||||
function Entities({entity}) {
|
||||
function Entity({entity, ...rest}) {
|
||||
const [debug] = useDebug();
|
||||
const [mainEntity] = useMainEntity();
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
|
@ -41,6 +58,7 @@ function Entities({entity}) {
|
|||
{entity.Sprite && (
|
||||
<Sprite
|
||||
entity={entity}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
{entity.Emitter && (
|
||||
|
@ -51,8 +69,32 @@ function Entities({entity}) {
|
|||
{debug && entity.Position && (
|
||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
||||
)}
|
||||
{debug && (
|
||||
<Aabb
|
||||
color={0xff00ff}
|
||||
x0={entity.VisibleAabb.x0}
|
||||
x1={entity.VisibleAabb.x1}
|
||||
y0={entity.VisibleAabb.y0}
|
||||
y1={entity.VisibleAabb.y1}
|
||||
/>
|
||||
)}
|
||||
{debug && entity.Collider && (
|
||||
<Aabb
|
||||
color={0xffff00}
|
||||
x0={entity.Collider.aabb.x0}
|
||||
x1={entity.Collider.aabb.x1}
|
||||
y0={entity.Collider.aabb.y0}
|
||||
y1={entity.Collider.aabb.y1}
|
||||
/>
|
||||
)}
|
||||
{debug && mainEntity == entity.id && (
|
||||
<Aabb
|
||||
color={0x00ff00}
|
||||
{...entity.Interacts.aabb()}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Entities);
|
||||
export default memo(Entity);
|
||||
|
|
|
@ -2,25 +2,26 @@ import {Sprite as PixiSprite} from '@pixi/react';
|
|||
|
||||
import {useAsset} from '@/context/assets.js';
|
||||
|
||||
export default function Sprite({entity}) {
|
||||
export default function Sprite({entity, ...rest}) {
|
||||
const asset = useAsset(entity.Sprite.source);
|
||||
if (!asset) {
|
||||
return false;
|
||||
}
|
||||
let texture;
|
||||
if (asset.textures) {
|
||||
const animation = asset.animations[entity.Sprite.animation]
|
||||
texture = animation[entity.Sprite.frame];
|
||||
if (asset.data.animations) {
|
||||
texture = asset.animations[entity.Sprite.animation][entity.Sprite.frame];
|
||||
}
|
||||
else {
|
||||
texture = asset;
|
||||
texture = asset.textures[''];
|
||||
}
|
||||
return (
|
||||
<PixiSprite
|
||||
anchor={entity.Sprite.anchor}
|
||||
scale={entity.Sprite.scale}
|
||||
texture={texture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
y={Math.round(entity.Position.y)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -54,7 +54,7 @@ export default function Ui({disconnected}) {
|
|||
switch (payload) {
|
||||
case '-':
|
||||
if ('keyDown' === type) {
|
||||
setScale((scale) => scale > 1 ? scale - 1 : 0.5);
|
||||
setScale((scale) => scale > 1 ? scale - 1 : 0.666);
|
||||
}
|
||||
break;
|
||||
case '=':
|
||||
|
@ -92,6 +92,10 @@ export default function Ui({disconnected}) {
|
|||
actionPayload = {type: 'use', value: KEY_MAP[type]};
|
||||
break;
|
||||
}
|
||||
case 'e': {
|
||||
actionPayload = {type: 'interact', value: KEY_MAP[type]};
|
||||
break;
|
||||
}
|
||||
case '1': {
|
||||
if ('keyDown' === type) {
|
||||
actionPayload = {type: 'changeSlot', value: 1};
|
||||
|
|
|
@ -41,10 +41,10 @@ class ClientEcs extends Ecs {
|
|||
const chars = await this.readAsset(uri);
|
||||
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
|
||||
}
|
||||
async readScript(uri) {
|
||||
async readScript(uri, context = {}) {
|
||||
const code = await this.readAsset(uri);
|
||||
if (code.byteLength > 0) {
|
||||
return Script.fromCode((new TextDecoder()).decode(code));
|
||||
return Script.fromCode((new TextDecoder()).decode(code), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
21
app/util/math.js
Normal file
21
app/util/math.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export function distance({x: lx, y: ly}, {x: rx, y: ry}) {
|
||||
const xd = lx - rx;
|
||||
const yd = ly - ry;
|
||||
return Math.sqrt(xd * xd + yd * yd);
|
||||
}
|
||||
|
||||
export function intersects(l, r) {
|
||||
if (l.x0 > r.x1) return false;
|
||||
if (l.y0 > r.y1) return false;
|
||||
if (l.x1 < r.x0) return false;
|
||||
if (l.y1 < r.y0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function normalizeVector({x, y}) {
|
||||
if (0 === y && 0 === x) {
|
||||
return {x: 0, y: 0};
|
||||
}
|
||||
const k = 1 / Math.sqrt(x * x + y * y);
|
||||
return {x: x * k, y: y * k};
|
||||
}
|
50
app/util/spatial-hash.js
Normal file
50
app/util/spatial-hash.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
export default class SpatialHash {
|
||||
|
||||
constructor({x, y}) {
|
||||
this.area = {x, y};
|
||||
this.chunkSize = {x: 64, y: 64};
|
||||
this.chunks = Array(Math.ceil(this.area.x / this.chunkSize.x))
|
||||
.fill(0)
|
||||
.map(() => (
|
||||
Array(Math.ceil(this.area.y / this.chunkSize.y))
|
||||
.fill(0)
|
||||
.map(() => [])
|
||||
));
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
clamp(x, y) {
|
||||
return [
|
||||
Math.max(0, Math.min(x, this.area.x - 1)),
|
||||
Math.max(0, Math.min(y, this.area.y - 1))
|
||||
];
|
||||
}
|
||||
|
||||
chunkIndex(x, y) {
|
||||
const [cx, cy] = this.clamp(x, y);
|
||||
return [
|
||||
Math.floor(cx / this.chunkSize.x),
|
||||
Math.floor(cy / this.chunkSize.y),
|
||||
];
|
||||
}
|
||||
|
||||
remove(datum) {
|
||||
if (datum in this.data) {
|
||||
for (const [cx, cy] of this.data[datum]) {
|
||||
const chunk = this.chunks[cx][cy];
|
||||
chunk.splice(chunk.indexOf(datum), 1);
|
||||
}
|
||||
}
|
||||
this.data[datum] = [];
|
||||
}
|
||||
|
||||
update({x0, x1, y0, y1}, datum) {
|
||||
this.remove(datum);
|
||||
for (const [x, y] of [[x0, y0], [x0, y1], [x1, y0], [x1, y1]]) {
|
||||
const [cx, cy] = this.chunkIndex(x, y);
|
||||
this.data[datum].push([cx, cy]);
|
||||
this.chunks[cx][cy].push(datum);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
43
package-lock.json
generated
43
package-lock.json
generated
|
@ -7,6 +7,8 @@
|
|||
"name": "silphius-next",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@pixi/filter-glow": "^5.2.1",
|
||||
"@pixi/particle-emitter": "^5.0.8",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"@pixi/spritesheet": "^7.4.2",
|
||||
|
@ -45,7 +47,6 @@
|
|||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"image-size": "^1.1.1",
|
||||
"storybook": "^8.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"vitest": "^1.6.0"
|
||||
|
@ -3302,6 +3303,14 @@
|
|||
"@pixi/core": "7.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/filter-adjustment": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/filter-adjustment/-/filter-adjustment-5.1.1.tgz",
|
||||
"integrity": "sha512-AUHe03rmqXwV1ylAHq62t19AolPWOOYomCcL+Qycb1tf+LbM8FWpGXC6wmU1PkUrhgNc958uM9TrA9nRpplViA==",
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "^7.0.0-X"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/filter-alpha": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.2.tgz",
|
||||
|
@ -3342,6 +3351,14 @@
|
|||
"@pixi/core": "7.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/filter-glow": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/filter-glow/-/filter-glow-5.2.1.tgz",
|
||||
"integrity": "sha512-94I4XePDF9yqqA6KQuhPSphEHPJ2lXfqJLn0Bes8VVdwft0Ianj1wALqjoSUeBWqiJbhjBEXGDNkRZhPHvY3Xg==",
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "^7.0.0-X"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/filter-noise": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.4.2.tgz",
|
||||
|
@ -11287,21 +11304,6 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"queue": "6.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
|
@ -15143,15 +15145,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@pixi/filter-glow": "^5.2.1",
|
||||
"@pixi/particle-emitter": "^5.0.8",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"@pixi/spritesheet": "^7.4.2",
|
||||
|
@ -52,7 +54,6 @@
|
|||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"image-size": "^1.1.1",
|
||||
"storybook": "^8.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"vitest": "^1.6.0"
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"animations":{"shit-shack/shit-shack/0":["shit-shack/shit-shack/0"]},"frames":{"shit-shack/shit-shack/0":{"frame":{"x":0,"y":0,"w":110,"h":102},"spriteSourceSize":{"x":0,"y":0,"w":110,"h":102},"sourceSize":{"w":110,"h":102}}},"meta":{"format":"RGBA8888","image":"./shit-shack.png","scale":1,"size":{"w":110,"h":102}}}
|
||||
{"frames":{"":{"frame":{"x":0,"y":0,"w":110,"h":102},"spriteSourceSize":{"x":0,"y":0,"w":110,"h":102},"sourceSize":{"w":110,"h":102}}},"meta":{"format":"RGBA8888","image":"./shit-shack.png","scale":1,"size":{"w":110,"h":102}}}
|
|
@ -1,4 +1,6 @@
|
|||
const {Sprite} = ecs.get(plant.entity);
|
||||
const {Interactive, Sprite} = ecs.get(plant.entity);
|
||||
|
||||
plant.growth = 0
|
||||
|
||||
if (plant.stage < 3) {
|
||||
plant.stage += 1
|
||||
|
@ -6,8 +8,8 @@ if (plant.stage < 3) {
|
|||
if (4 === plant.stage) {
|
||||
plant.stage = 3
|
||||
}
|
||||
if (3 !== plant.stage) {
|
||||
plant.growth = 0
|
||||
if (3 === plant.stage) {
|
||||
Interactive.interacting = true;
|
||||
}
|
||||
|
||||
Sprite.animation = ['stage', plant.stage].join('/')
|
||||
|
|
9
public/assets/tomato-plant/interact.js
Normal file
9
public/assets/tomato-plant/interact.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const {Interactive, Plant, Sprite} = subject;
|
||||
Interactive.interacting = false;
|
||||
Plant.stage = 4;
|
||||
Sprite.animation = ['stage', Plant.stage].join('/')
|
||||
|
||||
initiator.Inventory.give({
|
||||
qty: 1,
|
||||
source: '/assets/tomato/tomato.json',
|
||||
})
|
|
@ -1,12 +1,14 @@
|
|||
const filtered = []
|
||||
|
||||
for (let i = 0; i < projected.length; ++i) {
|
||||
const entities = Array.from(ecs.system('UpdateSpatialHash').within(
|
||||
projected[i].x * layer.tileSize.x,
|
||||
projected[i].y * layer.tileSize.y,
|
||||
layer.tileSize.x,
|
||||
layer.tileSize.y,
|
||||
));
|
||||
const x0 = projected[i].x * layer.tileSize.x;
|
||||
const y0 = projected[i].y * layer.tileSize.y;
|
||||
const entities = Array.from(ecs.system('Colliders').within({
|
||||
x0,
|
||||
x1: x0 + layer.tileSize.x - 1,
|
||||
y0,
|
||||
y1: y0 + layer.tileSize.y - 1,
|
||||
}));
|
||||
let hasPlant = false;
|
||||
for (let j = 0; j < entities.length; ++j) {
|
||||
if (entities[j].Plant) {
|
||||
|
|
|
@ -9,10 +9,23 @@ if (projected?.length > 0) {
|
|||
const [, direction] = Sprite.animation.split(':')
|
||||
|
||||
const plant = {
|
||||
Collider: {
|
||||
bodies: [
|
||||
[
|
||||
{x: -8, y: -8},
|
||||
{x: 8, y: -8},
|
||||
{x: -8, y: 8},
|
||||
{x: 8, y: 8},
|
||||
],
|
||||
],
|
||||
},
|
||||
Interactive: {
|
||||
interactScript: '/assets/tomato-plant/interact.js',
|
||||
},
|
||||
Plant: {
|
||||
growScript: '/assets/tomato-plant/grow.js',
|
||||
mayGrowScript: '/assets/tomato-plant/may-grow.js',
|
||||
stages: Array(5).fill(60),
|
||||
stages: Array(5).fill(5),
|
||||
},
|
||||
Sprite: {
|
||||
anchor: {x: 0.5, y: 0.75},
|
||||
|
|
3
public/assets/tomato/tomato.json
Normal file
3
public/assets/tomato/tomato.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"icon": "/assets/tomato/tomato.png"
|
||||
}
|
BIN
public/assets/tomato/tomato.png
Normal file
BIN
public/assets/tomato/tomato.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 570 B |
Loading…
Reference in New Issue
Block a user