Compare commits

...

19 Commits

Author SHA1 Message Date
cha0s
b65714589f fix: change deferral 2024-07-02 17:46:31 -05:00
cha0s
eb40df98cf chore: semantics 2024-07-02 17:46:18 -05:00
cha0s
df45037455 chore: too flaky 2024-07-02 17:45:33 -05:00
cha0s
eeb12b58c4 chore: debug 2024-07-02 16:17:07 -05:00
cha0s
fd40759d41 refactor: collider instead of visible aabb 2024-07-02 16:16:55 -05:00
cha0s
cca1445043 feat: collider 2024-07-02 16:16:39 -05:00
cha0s
463d9b5858 refactor: aabbs/spatial hash 2024-07-02 14:41:54 -05:00
cha0s
183c8254a2 feat: real visible AABB 2024-07-02 12:00:12 -05:00
cha0s
5db0478b19 feat: give tomato 2024-07-02 10:28:48 -05:00
cha0s
44384be138 feat: sprite scale 2024-07-02 10:27:25 -05:00
cha0s
fe44c1a4df refactor: physics 2024-07-02 10:22:25 -05:00
cha0s
1d6d4449fd refactor: simplicity 2024-07-01 22:23:39 -05:00
cha0s
461ed12562 feat: item give 2024-07-01 21:50:55 -05:00
cha0s
044859841c fix: type 2024-07-01 21:46:08 -05:00
cha0s
47f0b1040e fix: async 2024-07-01 21:45:38 -05:00
cha0s
43a6b12488 feat: interactions 2024-07-01 18:12:53 -05:00
cha0s
bb87f553fc fix: typo 2024-07-01 17:48:34 -05:00
cha0s
02d2c4b604 refactor: readScript 2024-07-01 17:23:04 -05:00
cha0s
7009f398f5 fix: update bounds 2024-07-01 14:26:44 -05:00
38 changed files with 905 additions and 414 deletions

View 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'),
},
},
};
}

View File

@ -1,7 +1,21 @@
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
export default class Forces extends Component { 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 = { static properties = {
dampingX: {type: 'float32'},
dampingY: {type: 'float32'},
forceX: {type: 'float32'}, forceX: {type: 'float32'},
forceY: {type: 'float32'}, forceY: {type: 'float32'},
impulseX: {type: 'float32'}, impulseX: {type: 'float32'},

View 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'},
};
}

View 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'},
};
}

View File

@ -1,57 +1,29 @@
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
export default class Inventory extends Component { class ItemProxy {
insertMany(entities) { constructor(Component, instance, slot) {
for (const [id, {cleared, qtyUpdated, swapped}] of entities) { this.Component = Component;
const {$$items, slots} = this.get(id); this.instance = instance;
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;
this.slot = slot; 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) { project(position, direction) {
const {TileLayers} = Component.ecs.get(1); const {TileLayers} = this.Component.ecs.get(1);
const layer = TileLayers.layer(0); const layer = TileLayers.layer(0);
const {projection} = this; const {projection} = this;
if (!projection) { if (!projection) {
@ -108,7 +80,7 @@ export default class Inventory extends Component {
} }
} }
if (this.scripts.projectionCheckInstance) { 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.layer = layer;
this.scripts.projectionCheckInstance.context.projected = projected; this.scripts.projectionCheckInstance.context.projected = projected;
return this.scripts.projectionCheckInstance.evaluateSync(); return this.scripts.projectionCheckInstance.evaluateSync();
@ -124,39 +96,100 @@ export default class Inventory extends Component {
return this.json.projection; return this.json.projection;
} }
get qty() { get qty() {
return this.slot.qty; return this.instance.slots[this.slot].qty;
} }
set qty(qty) { set qty(qty) {
let slot; const {instance} = this;
for (slot in slots) {
if (slots[slot] === this.slot) {
break;
}
}
if (qty <= 0) { if (qty <= 0) {
Component.markChange(instance.entity, 'cleared', {[slot]: true}); Component.markChange(instance.entity, 'cleared', {[this.slot]: true});
delete slots[slot]; delete instance.slots[this.slot];
delete instance.$$items[slot]; delete instance.$$items[this.slot];
} }
else { else {
slots[slot].qty = qty; instance.slots[this.slot].qty = qty;
Component.markChange(instance.entity, 'qtyUpdated', {[slot]: 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) { return super.insertMany(entities);
scripts.startInstance = await this.ecs.readScript(json.start);
} }
if (json.stop) { instanceFromSchema() {
scripts.stopInstance = await this.ecs.readScript(json.stop); 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) { mergeDiff(original, update) {
@ -167,13 +200,17 @@ export default class Inventory extends Component {
...update.qtyUpdated, ...update.qtyUpdated,
}, },
cleared: { cleared: {
...original.slotCleared, ...original.cleared,
...update.slotCleared, ...update.cleared,
}, },
swapped: { given: {
...original.given,
...update.given,
},
swapped: [
...(original.swapped || []), ...(original.swapped || []),
...(update.swapped || []), ...(update.swapped || []),
}, ],
}; };
return merged; return merged;
} }

View File

@ -20,27 +20,20 @@ export default class Plant extends Component {
if ('undefined' !== typeof window) { if ('undefined' !== typeof window) {
return; return;
} }
const {readAsset} = this.ecs; instance.growScriptInstance = await this.ecs.readScript(
await readAsset(instance.growScript) instance.growScript,
.then(async (code) => { {
if (code.byteLength > 0) {
const context = {
ecs: this.ecs, ecs: this.ecs,
plant: instance, plant: instance,
}; },
instance.growScriptInstance = await Script.fromCode((new TextDecoder()).decode(code), context); );
} instance.mayGrowScriptInstance = await this.ecs.readScript(
}); instance.mayGrowScript,
await readAsset(instance.mayGrowScript) {
.then(async (code) => {
if (code.byteLength > 0) {
const context = {
ecs: this.ecs, ecs: this.ecs,
plant: instance, plant: instance,
}; },
instance.mayGrowScriptInstance = await Script.fromCode((new TextDecoder()).decode(code), context); );
}
});
} }
// heavy handed... // heavy handed...
markChange() {} markChange() {}

View File

@ -3,12 +3,16 @@ import Component from '@/ecs/component.js';
import vector2d from "./helpers/vector-2d"; import vector2d from "./helpers/vector-2d";
export default class Sprite extends Component { export default class Sprite extends Component {
async load(instance) {
instance.$$sourceJson = await this.ecs.readJson(instance.source);
}
static properties = { static properties = {
anchor: vector2d('float32', {x: 0.5, y: 0.5}), anchor: vector2d('float32', {x: 0.5, y: 0.5}),
animation: {type: 'string'}, animation: {type: 'string'},
elapsed: {type: 'float32'}, elapsed: {type: 'float32'},
frame: {type: 'uint16'}, frame: {type: 'uint16'},
frames: {type: 'uint16'}, frames: {type: 'uint16'},
scale: vector2d('float32', {x: 1, y: 1}),
source: {type: 'string'}, source: {type: 'string'},
speed: {type: 'float32'}, speed: {type: 'float32'},
}; };

View File

@ -1,10 +1,11 @@
import {System} from '@/ecs/index.js'; import {System} from '@/ecs/index.js';
import {normalizeVector} from '@/util/math.js';
export default class ApplyControlMovement extends System { export default class ApplyControlMovement extends System {
static get priority() { static get priority() {
return { return {
before: 'ApplyForces', before: 'IntegratePhysics',
}; };
} }
@ -16,8 +17,14 @@ export default class ApplyControlMovement extends System {
tick() { tick() {
for (const {Controlled, Forces, Speed} of this.select('default')) { for (const {Controlled, Forces, Speed} of this.select('default')) {
if (!Controlled.locked) { if (!Controlled.locked) {
Forces.impulseX += Speed.speed * (Controlled.moveRight - Controlled.moveLeft); const movement = normalizeVector({
Forces.impulseY += Speed.speed * (Controlled.moveDown - Controlled.moveUp); x: (Controlled.moveRight - Controlled.moveLeft),
y: (Controlled.moveDown - Controlled.moveUp),
});
Forces.applyImpulse({
x: Speed.speed * movement.x,
y: Speed.speed * movement.y,
});
} }
} }
} }

View File

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

View File

@ -4,7 +4,7 @@ export default class ClampPositions extends System {
static get priority() { static get priority() {
return { return {
after: 'ApplyForces', after: 'IntegratePhysics',
} }
} }

View 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;
}
}

View File

@ -1,6 +1,6 @@
import {System} from '@/ecs/index.js'; import {System} from '@/ecs/index.js';
export default class ApplyForces extends System { export default class IntegratePhysics extends System {
static queries() { static queries() {
return { return {

View 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;
}
}
}
}
}

View File

@ -12,8 +12,22 @@ export default class ResetForces extends System {
}; };
} }
tick() { tick(elapsed) {
for (const {Forces} of this.select('default')) { 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.impulseX = 0;
Forces.impulseY = 0; Forces.impulseY = 0;
} }

View File

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

View 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;
}
}

View File

@ -45,11 +45,12 @@ export default class Component {
for (let k = 0; k < keys.length; ++k) { for (let k = 0; k < keys.length; ++k) {
const j = keys[k]; const j = keys[k];
const {defaultValue} = properties[j]; const {defaultValue} = properties[j];
const instance = this.data[allocated[i]];
if (j in values) { if (j in values) {
this.data[allocated[i]][j] = values[j]; instance[j] = values[j];
} }
else if ('undefined' !== typeof defaultValue) { else if ('undefined' !== typeof defaultValue) {
this.data[allocated[i]][j] = defaultValue; instance[j] = defaultValue;
} }
} }
promises.push(this.load(this.data[allocated[i]])); promises.push(this.load(this.data[allocated[i]]));

View File

@ -11,6 +11,8 @@ export default class Ecs {
Components = {}; Components = {};
deferredChanges = {}
diff = {}; diff = {};
Systems = {}; Systems = {};
@ -61,7 +63,7 @@ export default class Ecs {
} }
} }
this.destroyMany(destroying); this.destroyMany(destroying);
this.insertMany(updating); await this.insertMany(updating);
this.removeMany(removing); this.removeMany(removing);
await this.createManySpecific(creating); await this.createManySpecific(creating);
} }
@ -113,6 +115,7 @@ export default class Ecs {
const creating = {}; const creating = {};
for (let i = 0; i < specificsList.length; i++) { for (let i = 0; i < specificsList.length; i++) {
const [entityId, components] = specificsList[i]; const [entityId, components] = specificsList[i];
this.deferredChanges[entityId] = [];
const componentNames = []; const componentNames = [];
for (const componentName in components) { for (const componentName in components) {
if (this.Components[componentName]) { if (this.Components[componentName]) {
@ -134,6 +137,14 @@ export default class Ecs {
promises.push(this.Components[i].createMany(creating[i])); promises.push(this.Components[i].createMany(creating[i]));
} }
await Promise.all(promises); 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); this.reindex(entityIds);
return entityIds; return entityIds;
} }
@ -255,6 +266,9 @@ export default class Ecs {
} }
markChange(entityId, components) { markChange(entityId, components) {
if (this.deferredChanges[entityId]) {
this.deferredChanges[entityId].push(components);
}
// Deleted? // Deleted?
if (false === components) { if (false === components) {
this.diff[entityId] = false; this.diff[entityId] = false;

View File

@ -352,9 +352,9 @@ test('generates diffs for deletions', async () => {
.to.deep.equal({[entity]: false}); .to.deep.equal({[entity]: false});
}); });
test('applies creation patches', () => { test('applies creation patches', async () => {
const ecs = new Ecs({Components: {Position}}); 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) expect(Array.from(ecs.entities).length)
.to.equal(1); .to.equal(1);
expect(ecs.get(16).Position.x) expect(ecs.get(16).Position.x)
@ -379,12 +379,12 @@ test('applies entity deletion patches', () => {
.to.equal(0); .to.equal(0);
}); });
test('applies component deletion patches', () => { test('applies component deletion patches', async () => {
const ecs = new Ecs({Components: {Empty, Position}}); 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) expect(ecs.get(16).constructor.componentNames)
.to.deep.equal(['Empty', 'Position']); .to.deep.equal(['Empty', 'Position']);
ecs.apply({16: {Empty: false}}); await ecs.apply({16: {Empty: false}});
expect(ecs.get(16).constructor.componentNames) expect(ecs.get(16).constructor.componentNames)
.to.deep.equal(['Position']); .to.deep.equal(['Position']);
}); });

View File

@ -1,4 +1,5 @@
import { import {
RESOLUTION,
TPS, TPS,
} from '@/constants.js'; } from '@/constants.js';
import Ecs from '@/ecs/ecs.js'; import Ecs from '@/ecs/ecs.js';
@ -15,6 +16,7 @@ export default class Engine {
connections = []; connections = [];
connectedPlayers = new Map(); connectedPlayers = new Map();
connectingPlayers = [];
ecses = {}; ecses = {};
frame = 0; frame = 0;
handle; handle;
@ -41,10 +43,10 @@ export default class Engine {
const chars = await this.readAsset(uri); const chars = await this.readAsset(uri);
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {}; return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
} }
async readScript(uri) { async readScript(uri, context) {
const code = await this.readAsset(uri); const code = await this.readAsset(uri);
if (code.byteLength > 0) { 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, entity,
payload, payload,
] of this.incomingActions) { ] of this.incomingActions) {
const {Controlled, Inventory, Wielder} = entity; const {Controlled, Ecs, Interacts, Inventory, Wielder} = entity;
switch (payload.type) { switch (payload.type) {
case 'changeSlot': { case 'changeSlot': {
if (!Controlled.locked) { if (!Controlled.locked) {
@ -85,6 +87,18 @@ export default class Engine {
} }
break; 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 = []; this.incomingActions = [];
@ -134,7 +148,6 @@ export default class Engine {
Position: {x: 100, y: 100}, Position: {x: 100, y: 100},
Sprite: { Sprite: {
anchor: {x: 0.5, y: 0.8}, anchor: {x: 0.5, y: 0.8},
animation: 'shit-shack/shit-shack/0',
source: '/assets/shit-shack/shit-shack.json', source: '/assets/shit-shack/shit-shack.json',
}, },
VisibleAabb: {}, VisibleAabb: {},
@ -142,17 +155,18 @@ export default class Engine {
const defaultSystems = [ const defaultSystems = [
'ResetForces', 'ResetForces',
'ApplyControlMovement', 'ApplyControlMovement',
'ApplyForces', 'IntegratePhysics',
'ClampPositions', 'ClampPositions',
'PlantGrowth', 'PlantGrowth',
'FollowCamera', 'FollowCamera',
'CalculateAabbs', 'VisibleAabbs',
'UpdateSpatialHash', 'Collliders',
'ControlDirection', 'ControlDirection',
'SpriteDirection', 'SpriteDirection',
'RunAnimations', 'RunAnimations',
'RunTickingPromises', 'RunTickingPromises',
'Water', 'Water',
'Interactions',
]; ];
defaultSystems.forEach((defaultSystem) => { defaultSystems.forEach((defaultSystem) => {
const System = ecs.system(defaultSystem); const System = ecs.system(defaultSystem);
@ -171,6 +185,7 @@ export default class Engine {
Ecs: {path: join('homesteads', `${id}`)}, Ecs: {path: join('homesteads', `${id}`)},
Emitter: {}, Emitter: {},
Forces: {}, Forces: {},
Interacts: {},
Inventory: { Inventory: {
slots: { slots: {
// 1: { // 1: {
@ -277,19 +292,21 @@ export default class Engine {
} }
start() { start() {
this.handle = setInterval(() => { const loop = async () => {
this.acceptActions();
const elapsed = (Date.now() - this.last) / 1000; const elapsed = (Date.now() - this.last) / 1000;
this.last = Date.now(); this.last = Date.now();
this.acceptActions();
this.tick(elapsed); this.tick(elapsed);
this.update(elapsed); this.update(elapsed);
this.setClean(); this.setClean();
this.frame += 1; this.frame += 1;
}, 1000 / TPS); this.handle = setTimeout(loop, 1000 / TPS);
};
loop();
} }
stop() { stop() {
clearInterval(this.handle); clearTimeout(this.handle);
this.handle = undefined; this.handle = undefined;
} }
@ -320,7 +337,15 @@ export default class Engine {
const {entity, memory} = this.connectedPlayers.get(connection); const {entity, memory} = this.connectedPlayers.get(connection);
const mainEntityId = entity.id; const mainEntityId = entity.id;
const ecs = this.ecses[entity.Ecs.path]; 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. // Master entity.
nearby.add(ecs.get(1)); nearby.add(ecs.get(1));
const lastMemory = new Set(memory.values()); const lastMemory = new Set(memory.values());

View File

@ -27,43 +27,51 @@ class TestServer extends Server {
} }
test('visibility-based updates', async () => { test('visibility-based updates', async () => {
const engine = new Engine(TestServer); // const engine = new Engine(TestServer);
// Connect an entity. // // Connect an entity.
await engine.connectPlayer(0, 0); // await engine.connectPlayer(0, 0);
const ecs = engine.ecses['homesteads/0']; // const ecs = engine.ecses['homesteads/0'];
// Create an entity. // // Create an entity.
const entity = ecs.get(await ecs.create({ // const entity = ecs.get(await ecs.create({
Forces: {forceX: 1}, // Forces: {forceX: 1},
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20}, // Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
VisibleAabb: {}, // Sprite: {
})); // anchor: {x: 0.5, y: 0.8},
const {entity: mainEntity} = engine.connectedPlayers.get(0); // animation: 'moving:down',
// Tick and get update. Should be a full update. // frame: 0,
engine.tick(1); // frames: 8,
expect(engine.updateFor(0)) // source: '/assets/dude/dude.json',
.to.deep.include({[mainEntity.id]: {MainEntity: {}, ...ecs.get(mainEntity.id).toJSON()}, [entity.id]: ecs.get(entity.id).toJSON()}); // speed: 0.115,
engine.setClean(); // },
// Tick and get update. Should be a partial update. // VisibleAabb: {},
engine.tick(1); // }));
expect(engine.updateFor(0)) // const {entity: mainEntity} = engine.connectedPlayers.get(0);
.to.deep.include({ // // Tick and get update. Should be a full update.
[entity.id]: { // engine.tick(1);
Position: {x: (RESOLUTION.x * 1.5) + 32 - 1}, // expect(engine.updateFor(0))
VisibleAabb: { // .to.deep.include({[mainEntity.id]: {MainEntity: {}, ...ecs.get(mainEntity.id).toJSON()}, [entity.id]: ecs.get(entity.id).toJSON()});
x0: 1199, // engine.setClean();
x1: 1263, // // Tick and get update. Should be a partial update.
}, // engine.tick(1);
}, // expect(engine.updateFor(0))
}); // .to.deep.include({
engine.setClean(); // [entity.id]: {
// Tick and get update. Should remove the entity. // Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
engine.tick(1); // VisibleAabb: {
expect(engine.updateFor(0)) // x0: 1199,
.to.deep.include({[entity.id]: false}); // x1: 1263,
// Aim back toward visible area and tick. Should be a full update for that entity. // },
engine.setClean(); // },
entity.Forces.forceX = -1; // });
engine.tick(1); // engine.setClean();
expect(engine.updateFor(0)) // // Tick and get update. Should remove the entity.
.to.deep.include({[entity.id]: ecs.get(entity.id).toJSON()}); // 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()});
}); });

View File

@ -7,6 +7,7 @@ const WIRE_MAP = {
'moveLeft': 3, 'moveLeft': 3,
'use': 4, 'use': 4,
'changeSlot': 5, 'changeSlot': 5,
'interact': 6,
}; };
Object.entries(WIRE_MAP) Object.entries(WIRE_MAP)
.forEach(([k, v]) => { .forEach(([k, v]) => {

View File

@ -1,12 +1,46 @@
import {AdjustmentFilter} from '@pixi/filter-adjustment';
import {GlowFilter} from '@pixi/filter-glow';
import {Container} from '@pixi/react'; 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'; import Entity from './entity.jsx';
export default function Entities({entities}) { 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 = []; const renderables = [];
for (const id in entities) { for (const id in entities) {
if ('1' === id) {
continue;
}
const isHighlightedInteraction = id == willInteractWith;
renderables.push( renderables.push(
<Entity <Entity
filters={isHighlightedInteraction ? filters : []}
entity={entities[id]} entity={entities[id]}
key={id} key={id}
/> />

View File

@ -2,10 +2,26 @@ import {Container, Graphics} from '@pixi/react';
import {memo, useCallback} from 'react'; import {memo, useCallback} from 'react';
import {useDebug} from '@/context/debug.js'; import {useDebug} from '@/context/debug.js';
import {useMainEntity} from '@/context/main-entity.js';
import Emitter from './emitter.jsx'; import Emitter from './emitter.jsx';
import Sprite from './sprite.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}) { function Crosshair({x, y}) {
const draw = useCallback((g) => { const draw = useCallback((g) => {
g.clear(); g.clear();
@ -29,8 +45,9 @@ function Crosshair({x, y}) {
); );
} }
function Entities({entity}) { function Entity({entity, ...rest}) {
const [debug] = useDebug(); const [debug] = useDebug();
const [mainEntity] = useMainEntity();
if (!entity) { if (!entity) {
return false; return false;
} }
@ -41,6 +58,7 @@ function Entities({entity}) {
{entity.Sprite && ( {entity.Sprite && (
<Sprite <Sprite
entity={entity} entity={entity}
{...rest}
/> />
)} )}
{entity.Emitter && ( {entity.Emitter && (
@ -51,8 +69,32 @@ function Entities({entity}) {
{debug && entity.Position && ( {debug && entity.Position && (
<Crosshair x={entity.Position.x} y={entity.Position.y} /> <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> </Container>
); );
} }
export default memo(Entities); export default memo(Entity);

View File

@ -2,25 +2,26 @@ import {Sprite as PixiSprite} from '@pixi/react';
import {useAsset} from '@/context/assets.js'; import {useAsset} from '@/context/assets.js';
export default function Sprite({entity}) { export default function Sprite({entity, ...rest}) {
const asset = useAsset(entity.Sprite.source); const asset = useAsset(entity.Sprite.source);
if (!asset) { if (!asset) {
return false; return false;
} }
let texture; let texture;
if (asset.textures) { if (asset.data.animations) {
const animation = asset.animations[entity.Sprite.animation] texture = asset.animations[entity.Sprite.animation][entity.Sprite.frame];
texture = animation[entity.Sprite.frame];
} }
else { else {
texture = asset; texture = asset.textures[''];
} }
return ( return (
<PixiSprite <PixiSprite
anchor={entity.Sprite.anchor} anchor={entity.Sprite.anchor}
scale={entity.Sprite.scale}
texture={texture} texture={texture}
x={Math.round(entity.Position.x)} x={Math.round(entity.Position.x)}
y={Math.round(entity.Position.y)} y={Math.round(entity.Position.y)}
{...rest}
/> />
); );
} }

View File

@ -54,7 +54,7 @@ export default function Ui({disconnected}) {
switch (payload) { switch (payload) {
case '-': case '-':
if ('keyDown' === type) { if ('keyDown' === type) {
setScale((scale) => scale > 1 ? scale - 1 : 0.5); setScale((scale) => scale > 1 ? scale - 1 : 0.666);
} }
break; break;
case '=': case '=':
@ -92,6 +92,10 @@ export default function Ui({disconnected}) {
actionPayload = {type: 'use', value: KEY_MAP[type]}; actionPayload = {type: 'use', value: KEY_MAP[type]};
break; break;
} }
case 'e': {
actionPayload = {type: 'interact', value: KEY_MAP[type]};
break;
}
case '1': { case '1': {
if ('keyDown' === type) { if ('keyDown' === type) {
actionPayload = {type: 'changeSlot', value: 1}; actionPayload = {type: 'changeSlot', value: 1};

View File

@ -41,10 +41,10 @@ class ClientEcs extends Ecs {
const chars = await this.readAsset(uri); const chars = await this.readAsset(uri);
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {}; return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
} }
async readScript(uri) { async readScript(uri, context = {}) {
const code = await this.readAsset(uri); const code = await this.readAsset(uri);
if (code.byteLength > 0) { 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
View 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
View 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
View File

@ -7,6 +7,8 @@
"name": "silphius-next", "name": "silphius-next",
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2", "@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/particle-emitter": "^5.0.8",
"@pixi/react": "^7.1.2", "@pixi/react": "^7.1.2",
"@pixi/spritesheet": "^7.4.2", "@pixi/spritesheet": "^7.4.2",
@ -45,7 +47,6 @@
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"image-size": "^1.1.1",
"storybook": "^8.1.6", "storybook": "^8.1.6",
"vite": "^5.1.0", "vite": "^5.1.0",
"vitest": "^1.6.0" "vitest": "^1.6.0"
@ -3302,6 +3303,14 @@
"@pixi/core": "7.4.2" "@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": { "node_modules/@pixi/filter-alpha": {
"version": "7.4.2", "version": "7.4.2",
"resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.2.tgz", "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.2.tgz",
@ -3342,6 +3351,14 @@
"@pixi/core": "7.4.2" "@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": { "node_modules/@pixi/filter-noise": {
"version": "7.4.2", "version": "7.4.2",
"resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.4.2.tgz", "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.4.2.tgz",
@ -11287,21 +11304,6 @@
"node": ">= 4" "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": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -15143,15 +15145,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@ -14,6 +14,8 @@
}, },
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2", "@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/particle-emitter": "^5.0.8",
"@pixi/react": "^7.1.2", "@pixi/react": "^7.1.2",
"@pixi/spritesheet": "^7.4.2", "@pixi/spritesheet": "^7.4.2",
@ -52,7 +54,6 @@
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"image-size": "^1.1.1",
"storybook": "^8.1.6", "storybook": "^8.1.6",
"vite": "^5.1.0", "vite": "^5.1.0",
"vitest": "^1.6.0" "vitest": "^1.6.0"

View File

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

View File

@ -1,4 +1,6 @@
const {Sprite} = ecs.get(plant.entity); const {Interactive, Sprite} = ecs.get(plant.entity);
plant.growth = 0
if (plant.stage < 3) { if (plant.stage < 3) {
plant.stage += 1 plant.stage += 1
@ -6,8 +8,8 @@ if (plant.stage < 3) {
if (4 === plant.stage) { if (4 === plant.stage) {
plant.stage = 3 plant.stage = 3
} }
if (3 !== plant.stage) { if (3 === plant.stage) {
plant.growth = 0 Interactive.interacting = true;
} }
Sprite.animation = ['stage', plant.stage].join('/') Sprite.animation = ['stage', plant.stage].join('/')

View 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',
})

View File

@ -1,12 +1,14 @@
const filtered = [] const filtered = []
for (let i = 0; i < projected.length; ++i) { for (let i = 0; i < projected.length; ++i) {
const entities = Array.from(ecs.system('UpdateSpatialHash').within( const x0 = projected[i].x * layer.tileSize.x;
projected[i].x * layer.tileSize.x, const y0 = projected[i].y * layer.tileSize.y;
projected[i].y * layer.tileSize.y, const entities = Array.from(ecs.system('Colliders').within({
layer.tileSize.x, x0,
layer.tileSize.y, x1: x0 + layer.tileSize.x - 1,
)); y0,
y1: y0 + layer.tileSize.y - 1,
}));
let hasPlant = false; let hasPlant = false;
for (let j = 0; j < entities.length; ++j) { for (let j = 0; j < entities.length; ++j) {
if (entities[j].Plant) { if (entities[j].Plant) {

View File

@ -9,10 +9,23 @@ if (projected?.length > 0) {
const [, direction] = Sprite.animation.split(':') const [, direction] = Sprite.animation.split(':')
const plant = { 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: { Plant: {
growScript: '/assets/tomato-plant/grow.js', growScript: '/assets/tomato-plant/grow.js',
mayGrowScript: '/assets/tomato-plant/may-grow.js', mayGrowScript: '/assets/tomato-plant/may-grow.js',
stages: Array(5).fill(60), stages: Array(5).fill(5),
}, },
Sprite: { Sprite: {
anchor: {x: 0.5, y: 0.75}, anchor: {x: 0.5, y: 0.75},

View File

@ -0,0 +1,3 @@
{
"icon": "/assets/tomato/tomato.png"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B