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';
|
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'},
|
||||||
|
|
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';
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {}
|
||||||
|
|
|
@ -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'},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
static get priority() {
|
||||||
return {
|
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';
|
import {System} from '@/ecs/index.js';
|
||||||
|
|
||||||
export default class ApplyForces extends System {
|
export default class IntegratePhysics extends System {
|
||||||
|
|
||||||
static queries() {
|
static queries() {
|
||||||
return {
|
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')) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
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]]));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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()});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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]) => {
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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};
|
||||||
|
|
|
@ -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
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",
|
"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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
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('/')
|
||||||
|
|
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 = []
|
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) {
|
||||||
|
|
|
@ -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},
|
||||||
|
|
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