Compare commits
17 Commits
5fe346372b
...
5c619b26c0
Author | SHA1 | Date | |
---|---|---|---|
|
5c619b26c0 | ||
|
fb747b38e6 | ||
|
2b91a49997 | ||
|
ebf62613ef | ||
|
08dfe8ac29 | ||
|
091e19c7de | ||
|
5ff4eb2991 | ||
|
c19beead68 | ||
|
628e0cae48 | ||
|
91d0f481d6 | ||
|
8978ee89b8 | ||
|
df56438656 | ||
|
dd675efa5c | ||
|
eaf144668a | ||
|
a2b30d2f8b | ||
|
123c95ca1c | ||
|
adcdc81423 |
|
@ -446,6 +446,26 @@ test('executes member expressions', async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.todo('declares with assignment pattern', async () => {
|
||||||
|
let sandbox;
|
||||||
|
sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
const {Player: {id: owner = 0} = {}, Position} = {};
|
||||||
|
owner;
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
expect(sandbox.run().value)
|
||||||
|
.to.equal(0);
|
||||||
|
sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
const {Player: {id: [owner = 0]} = {id: []}, Position} = {};
|
||||||
|
owner;
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
expect(sandbox.run().value)
|
||||||
|
.to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('handles nested yields', async () => {
|
test('handles nested yields', async () => {
|
||||||
expect(
|
expect(
|
||||||
await finish(new Sandbox(
|
await finish(new Sandbox(
|
||||||
|
|
53
app/ecs/components/alive.js
Normal file
53
app/ecs/components/alive.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export default class Alive extends Component {
|
||||||
|
instanceFromSchema() {
|
||||||
|
const {ecs} = this;
|
||||||
|
return class AliveInstance extends super.instanceFromSchema() {
|
||||||
|
$$dead = false;
|
||||||
|
acceptDamage(amount) {
|
||||||
|
const health = Math.min(this.maxHealth, Math.max(0, this.health + amount));
|
||||||
|
this.health = health;
|
||||||
|
if (0 === health) {
|
||||||
|
this.die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
die() {
|
||||||
|
if (this.$$dead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$$dead = true;
|
||||||
|
const {Ticking} = ecs.get(this.entity);
|
||||||
|
if (Ticking) {
|
||||||
|
const ticker = this.$$death.ticker();
|
||||||
|
ecs.addDestructionDependency(this.entity.id, ticker);
|
||||||
|
Ticking.add(ticker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async load(instance) {
|
||||||
|
// heavy handed...
|
||||||
|
if ('undefined' !== typeof window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance.$$death = await this.ecs.readScript(
|
||||||
|
instance.deathScript,
|
||||||
|
{
|
||||||
|
ecs: this.ecs,
|
||||||
|
entity: this.ecs.get(instance.entity),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (0 === instance.maxHealth) {
|
||||||
|
instance.maxHealth = instance.health;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static properties = {
|
||||||
|
deathScript: {
|
||||||
|
defaultValue: '/assets/misc/death-default.js',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
health: {type: 'uint32'},
|
||||||
|
maxHealth: {type: 'uint32'},
|
||||||
|
};
|
||||||
|
}
|
|
@ -9,7 +9,9 @@ export default class Collider extends Component {
|
||||||
return class ColliderInstance extends super.instanceFromSchema() {
|
return class ColliderInstance extends super.instanceFromSchema() {
|
||||||
$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||||
$$aabbs = [];
|
$$aabbs = [];
|
||||||
collidingWith = {};
|
$$collisionStart;
|
||||||
|
$$collisionEnd;
|
||||||
|
$$intersections = new Map();
|
||||||
get aabb() {
|
get aabb() {
|
||||||
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
||||||
return {
|
return {
|
||||||
|
@ -32,6 +34,137 @@ export default class Collider extends Component {
|
||||||
}
|
}
|
||||||
return aabbs;
|
return aabbs;
|
||||||
}
|
}
|
||||||
|
endIntersections(other, intersections) {
|
||||||
|
const otherEntity = ecs.get(other.entity);
|
||||||
|
const thisEntity = ecs.get(this.entity);
|
||||||
|
for (const intersection of intersections) {
|
||||||
|
const [body, otherBody] = [
|
||||||
|
intersection.entity.bodies[intersection.i],
|
||||||
|
intersection.other.bodies[intersection.j],
|
||||||
|
];
|
||||||
|
if (this.$$collisionEnd) {
|
||||||
|
const script = this.$$collisionEnd.clone();
|
||||||
|
script.context.other = otherEntity;
|
||||||
|
script.context.pair = [body, otherBody];
|
||||||
|
const ticker = script.ticker();
|
||||||
|
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||||
|
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||||
|
thisEntity.Ticking.add(ticker);
|
||||||
|
}
|
||||||
|
if (other.$$collisionEnd) {
|
||||||
|
const script = other.$$collisionEnd.clone();
|
||||||
|
script.context.other = thisEntity;
|
||||||
|
script.context.pair = [otherBody, body];
|
||||||
|
const ticker = script.ticker();
|
||||||
|
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||||
|
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||||
|
otherEntity.Ticking.add(ticker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$$intersections.delete(other);
|
||||||
|
other.$$intersections.delete(this);
|
||||||
|
}
|
||||||
|
checkCollision(other) {
|
||||||
|
const otherEntity = ecs.get(other.entity);
|
||||||
|
const thisEntity = ecs.get(this.entity);
|
||||||
|
const intersections = this.intersectionsWith(other);
|
||||||
|
const activeIntersections = this.$$intersections.get(other) || new Set();
|
||||||
|
if (0 === intersections.length) {
|
||||||
|
// had none; have none
|
||||||
|
if (0 === activeIntersections.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.endIntersections(other, intersections);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const intersection of intersections) {
|
||||||
|
// new pair - start
|
||||||
|
const [body, otherBody] = [
|
||||||
|
intersection.entity.bodies[intersection.i],
|
||||||
|
intersection.other.bodies[intersection.j],
|
||||||
|
];
|
||||||
|
let hasMatchingIntersection = false;
|
||||||
|
for (const activeIntersection of activeIntersections) {
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
activeIntersection.entity === intersection.entity
|
||||||
|
&& activeIntersection.other === intersection.other
|
||||||
|
&& activeIntersection.i === intersection.i
|
||||||
|
&& activeIntersection.j === intersection.j
|
||||||
|
)
|
||||||
|
|| (
|
||||||
|
activeIntersection.entity === intersection.other
|
||||||
|
&& activeIntersection.other === intersection.entity
|
||||||
|
&& activeIntersection.i === intersection.j
|
||||||
|
&& activeIntersection.j === intersection.i
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hasMatchingIntersection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasMatchingIntersection) {
|
||||||
|
if (this.$$collisionStart) {
|
||||||
|
const script = this.$$collisionStart.clone();
|
||||||
|
script.context.other = otherEntity;
|
||||||
|
script.context.pair = [body, otherBody];
|
||||||
|
const ticker = script.ticker();
|
||||||
|
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||||
|
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||||
|
thisEntity.Ticking.add(ticker);
|
||||||
|
}
|
||||||
|
if (other.$$collisionStart) {
|
||||||
|
const script = other.$$collisionStart.clone();
|
||||||
|
script.context.other = thisEntity;
|
||||||
|
script.context.pair = [otherBody, body];
|
||||||
|
const ticker = script.ticker();
|
||||||
|
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||||
|
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||||
|
otherEntity.Ticking.add(ticker);
|
||||||
|
}
|
||||||
|
activeIntersections.add(intersection);
|
||||||
|
}
|
||||||
|
// undo restricted movement
|
||||||
|
if (!body.unstoppable && otherBody.impassable) {
|
||||||
|
const j = this.bodies.indexOf(body);
|
||||||
|
const oj = other.bodies.indexOf(otherBody);
|
||||||
|
const aabb = this.$$aabbs[j];
|
||||||
|
const otherAabb = other.aabbs[oj];
|
||||||
|
const {Position} = thisEntity;
|
||||||
|
if (!intersects(
|
||||||
|
{
|
||||||
|
x0: aabb.x0 + Position.lastX,
|
||||||
|
x1: aabb.x1 + Position.lastX,
|
||||||
|
y0: aabb.y0 + Position.y,
|
||||||
|
y1: aabb.y1 + Position.y,
|
||||||
|
},
|
||||||
|
otherAabb,
|
||||||
|
)) {
|
||||||
|
Position.x = Position.lastX
|
||||||
|
}
|
||||||
|
else if (!intersects(
|
||||||
|
{
|
||||||
|
x0: aabb.x0 + Position.x,
|
||||||
|
x1: aabb.x1 + Position.x,
|
||||||
|
y0: aabb.y0 + Position.lastY,
|
||||||
|
y1: aabb.y1 + Position.lastY,
|
||||||
|
},
|
||||||
|
otherAabb,
|
||||||
|
)) {
|
||||||
|
Position.y = Position.lastY
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Position.x = Position.lastX
|
||||||
|
Position.y = Position.lastY
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activeIntersections.size > 0) {
|
||||||
|
this.$$intersections.set(other, activeIntersections);
|
||||||
|
other.$$intersections.set(this, activeIntersections);
|
||||||
|
}
|
||||||
|
}
|
||||||
closest(aabb) {
|
closest(aabb) {
|
||||||
const entity = ecs.get(this.entity);
|
const entity = ecs.get(this.entity);
|
||||||
return Array.from(ecs.system('Colliders').within(aabb))
|
return Array.from(ecs.system('Colliders').within(aabb))
|
||||||
|
@ -41,14 +174,12 @@ export default class Collider extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
destroy() {
|
destroy() {
|
||||||
const entity = ecs.get(this.entity);
|
for (const [other] of this.$$intersections) {
|
||||||
for (const otherId in this.collidingWith) {
|
other.$$intersections.delete(this);
|
||||||
const other = ecs.get(otherId);
|
|
||||||
delete entity.Collider.collidingWith[other.id];
|
|
||||||
delete other.Collider.collidingWith[entity.id];
|
|
||||||
}
|
}
|
||||||
|
this.$$intersections.clear();
|
||||||
}
|
}
|
||||||
isCollidingWith(other) {
|
intersectionsWith(other) {
|
||||||
const {aabb, aabbs} = this;
|
const {aabb, aabbs} = this;
|
||||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||||
const intersections = [];
|
const intersections = [];
|
||||||
|
@ -57,10 +188,26 @@ export default class Collider extends Component {
|
||||||
}
|
}
|
||||||
for (const i in aabbs) {
|
for (const i in aabbs) {
|
||||||
const aabb = aabbs[i];
|
const aabb = aabbs[i];
|
||||||
|
const body = this.bodies[i];
|
||||||
for (const j in otherAabbs) {
|
for (const j in otherAabbs) {
|
||||||
const otherAabb = otherAabbs[j];
|
const otherAabb = otherAabbs[j];
|
||||||
|
const otherBody = other.bodies[j];
|
||||||
|
if (body.group === otherBody.group && body.group < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (body.group !== otherBody.group || 0 === body.group) {
|
||||||
|
if (0 === (otherBody.mask & body.bits) || 0 === (body.mask & otherBody.bits)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// todo accuracy
|
||||||
if (intersects(aabb, otherAabb)) {
|
if (intersects(aabb, otherAabb)) {
|
||||||
intersections.push([this.bodies[i], other.bodies[j]]);
|
intersections.push({
|
||||||
|
entity: this,
|
||||||
|
other,
|
||||||
|
i,
|
||||||
|
j,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,19 +254,27 @@ export default class Collider extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async load(instance) {
|
async load(instance) {
|
||||||
|
for (const i in instance.bodies) {
|
||||||
|
instance.bodies[i] = {
|
||||||
|
...this.constructor.schema.constructor.defaultValue(
|
||||||
|
this.constructor.schema.specification.concrete.properties.bodies.concrete.subtype,
|
||||||
|
),
|
||||||
|
...instance.bodies[i],
|
||||||
|
};
|
||||||
|
}
|
||||||
instance.updateAabbs();
|
instance.updateAabbs();
|
||||||
// heavy handed...
|
// heavy handed...
|
||||||
if ('undefined' !== typeof window) {
|
if ('undefined' !== typeof window) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
instance.collisionEndScriptInstance = await this.ecs.readScript(
|
instance.$$collisionEnd = await this.ecs.readScript(
|
||||||
instance.collisionEndScript,
|
instance.collisionEndScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
entity: this.ecs.get(instance.entity),
|
entity: this.ecs.get(instance.entity),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
instance.collisionStartScriptInstance = await this.ecs.readScript(
|
instance.$$collisionStart = await this.ecs.readScript(
|
||||||
instance.collisionStartScript,
|
instance.collisionStartScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
|
@ -133,15 +288,15 @@ export default class Collider extends Component {
|
||||||
subtype: {
|
subtype: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
bits: {defaultValue: 0x00000001, type: 'uint32'},
|
||||||
impassable: {type: 'uint8'},
|
impassable: {type: 'uint8'},
|
||||||
|
group: {type: 'int32'},
|
||||||
|
mask: {defaultValue: 0xFFFFFFFF, type: 'uint32'},
|
||||||
points: {
|
points: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
subtype: vector2d('int16'),
|
subtype: vector2d('int16'),
|
||||||
},
|
},
|
||||||
tags: {
|
unstoppable: {type: 'uint8'},
|
||||||
type: 'array',
|
|
||||||
subtype: {type: 'string'},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,8 +4,9 @@ export default class Interactive extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const {ecs} = this;
|
const {ecs} = this;
|
||||||
return class ControlledInstance extends super.instanceFromSchema() {
|
return class ControlledInstance extends super.instanceFromSchema() {
|
||||||
|
$$interact;
|
||||||
interact(initiator) {
|
interact(initiator) {
|
||||||
const script = this.interactScriptInstance.clone();
|
const script = this.$$interact.clone();
|
||||||
script.context.initiator = initiator;
|
script.context.initiator = initiator;
|
||||||
const {Ticking} = ecs.get(this.entity);
|
const {Ticking} = ecs.get(this.entity);
|
||||||
Ticking.add(script.ticker());
|
Ticking.add(script.ticker());
|
||||||
|
@ -23,7 +24,7 @@ export default class Interactive extends Component {
|
||||||
if ('undefined' !== typeof window) {
|
if ('undefined' !== typeof window) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
instance.interactScriptInstance = await this.ecs.readScript(
|
instance.$$interact = await this.ecs.readScript(
|
||||||
instance.interactScript,
|
instance.interactScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
|
|
7
app/ecs/components/owned.js
Normal file
7
app/ecs/components/owned.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export default class Owned extends Component {
|
||||||
|
static properties = {
|
||||||
|
owner: {type: 'string'},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,17 +1,18 @@
|
||||||
import Component from '@/ecs/component.js';
|
import Component from '@/ecs/component.js';
|
||||||
import Script from '@/util/script.js';
|
|
||||||
|
|
||||||
export default class Plant extends Component {
|
export default class Plant extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const {ecs} = this;
|
const {ecs} = this;
|
||||||
const Instance = super.instanceFromSchema();
|
const Instance = super.instanceFromSchema();
|
||||||
return class PlantInstance extends Instance {
|
return class PlantInstance extends Instance {
|
||||||
mayGrow() {
|
$$grow;
|
||||||
return this.mayGrowScriptInstance.evaluate();
|
$$mayGrow;
|
||||||
}
|
|
||||||
grow() {
|
grow() {
|
||||||
const {Ticking} = ecs.get(this.entity);
|
const {Ticking} = ecs.get(this.entity);
|
||||||
Ticking.add(this.growScriptInstance.ticker());
|
Ticking.add(this.$$grow.ticker());
|
||||||
|
}
|
||||||
|
mayGrow() {
|
||||||
|
return this.$$mayGrow.evaluate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,14 +21,14 @@ export default class Plant extends Component {
|
||||||
if ('undefined' !== typeof window) {
|
if ('undefined' !== typeof window) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
instance.growScriptInstance = await this.ecs.readScript(
|
instance.$$grow = await this.ecs.readScript(
|
||||||
instance.growScript,
|
instance.growScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
plant: instance,
|
plant: instance,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
instance.mayGrowScriptInstance = await this.ecs.readScript(
|
instance.$$mayGrow = await this.ecs.readScript(
|
||||||
instance.mayGrowScript,
|
instance.mayGrowScript,
|
||||||
{
|
{
|
||||||
ecs: this.ecs,
|
ecs: this.ecs,
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class Ticking extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
destroy() {
|
||||||
this.$$finished = [];
|
this.$$finished = [];
|
||||||
this.$$tickers = [];
|
this.$$tickers = [];
|
||||||
}
|
}
|
||||||
|
|
38
app/ecs/components/vulnerable.js
Normal file
38
app/ecs/components/vulnerable.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export const DamageTypes = {
|
||||||
|
PAIN: 0,
|
||||||
|
HEALING: 1,
|
||||||
|
MANA: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Vulnerable extends Component {
|
||||||
|
mergeDiff(original, update) {
|
||||||
|
const merged = {};
|
||||||
|
if (update.damage) {
|
||||||
|
merged.damage = {
|
||||||
|
...original.damage,
|
||||||
|
...update.damage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
instanceFromSchema() {
|
||||||
|
const Component = this;
|
||||||
|
return class VulnerableInstance extends super.instanceFromSchema() {
|
||||||
|
id = 0;
|
||||||
|
Types = DamageTypes;
|
||||||
|
damage(specification) {
|
||||||
|
const {Alive} = Component.ecs.get(this.entity);
|
||||||
|
if (Alive) {
|
||||||
|
switch (specification.type) {
|
||||||
|
case DamageTypes.PAIN: {
|
||||||
|
Alive.acceptDamage(specification.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.markChange(this.entity, 'damage', {[this.id++]: specification});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ export default class Ecs {
|
||||||
|
|
||||||
deferredChanges = {}
|
deferredChanges = {}
|
||||||
|
|
||||||
destroying = new Set();
|
$$destructionDependencies = new Map();
|
||||||
|
|
||||||
diff = {};
|
diff = {};
|
||||||
|
|
||||||
|
@ -42,6 +42,20 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addDestructionDependency(id, promise) {
|
||||||
|
if (!this.$$destructionDependencies.has(id)) {
|
||||||
|
this.$$destructionDependencies.set(id, {promises: new Set()})
|
||||||
|
}
|
||||||
|
const {promises} = this.$$destructionDependencies.get(id);
|
||||||
|
promises.add(promise);
|
||||||
|
promise.then(() => {
|
||||||
|
promises.delete(promise);
|
||||||
|
if (!this.$$destructionDependencies.get(id)?.resolvers) {
|
||||||
|
this.$$destructionDependencies.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async apply(patch) {
|
async apply(patch) {
|
||||||
const creating = [];
|
const creating = [];
|
||||||
const destroying = [];
|
const destroying = [];
|
||||||
|
@ -213,11 +227,14 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(entityId) {
|
destroy(entityId) {
|
||||||
this.destroying.add(entityId);
|
if (!this.$$destructionDependencies.has(entityId)) {
|
||||||
|
this.$$destructionDependencies.set(entityId, {promises: new Set()});
|
||||||
}
|
}
|
||||||
|
const dependencies = this.$$destructionDependencies.get(entityId);
|
||||||
destroyImmediately(entityId) {
|
if (!dependencies.resolvers) {
|
||||||
this.destroyMany([entityId]);
|
dependencies.resolvers = withResolvers();
|
||||||
|
}
|
||||||
|
return dependencies.resolvers.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyAll() {
|
destroyAll() {
|
||||||
|
@ -444,9 +461,18 @@ export default class Ecs {
|
||||||
System.elapsed -= System.frequency;
|
System.elapsed -= System.frequency;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.destroying.size > 0) {
|
const destroying = new Set();
|
||||||
this.destroyMany(this.destroying);
|
for (const [entityId, {promises, resolvers}] of this.$$destructionDependencies) {
|
||||||
this.destroying.clear();
|
if (0 === promises.size && resolvers) {
|
||||||
|
destroying.add(entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (destroying.size > 0) {
|
||||||
|
this.destroyMany(destroying);
|
||||||
|
for (const entityId of destroying) {
|
||||||
|
this.$$destructionDependencies.get(entityId).resolvers.resolve();
|
||||||
|
this.$$destructionDependencies.delete(entityId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,7 @@ test('destroys entities', async () => {
|
||||||
expect(ecs.get(entity))
|
expect(ecs.get(entity))
|
||||||
.to.be.undefined;
|
.to.be.undefined;
|
||||||
expect(() => {
|
expect(() => {
|
||||||
ecs.destroyImmediately(entity);
|
ecs.destroyMany([entity]);
|
||||||
})
|
})
|
||||||
.to.throw();
|
.to.throw();
|
||||||
});
|
});
|
||||||
|
@ -275,7 +275,8 @@ test('generates diffs for deletions', async () => {
|
||||||
let entity;
|
let entity;
|
||||||
entity = await ecs.create();
|
entity = await ecs.create();
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
ecs.destroyImmediately(entity);
|
ecs.destroy(entity);
|
||||||
|
ecs.tick(0);
|
||||||
expect(ecs.diff)
|
expect(ecs.diff)
|
||||||
.to.deep.equal({[entity]: false});
|
.to.deep.equal({[entity]: false});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,12 @@ import {expect, test} from 'vitest';
|
||||||
|
|
||||||
import Schema from './schema.js';
|
import Schema from './schema.js';
|
||||||
|
|
||||||
test('defaults values', () => {
|
|
||||||
const compare = (specification, value) => {
|
const compare = (specification, value) => {
|
||||||
expect(new Schema(specification).defaultValue())
|
expect(new Schema(specification).defaultValue())
|
||||||
.to.deep.equal(value);
|
.to.deep.equal(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test('defaults values', () => {
|
||||||
[
|
[
|
||||||
'uint8',
|
'uint8',
|
||||||
'int8',
|
'int8',
|
||||||
|
@ -54,6 +55,22 @@ test('defaults values', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.todo('defaults nested values', () => {
|
||||||
|
compare(
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
subtype: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
foo: {defaultValue: 'bar', type: 'string'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[{}],
|
||||||
|
[{foo: 'bar'}],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('validates a schema', () => {
|
test('validates a schema', () => {
|
||||||
[
|
[
|
||||||
'uint8',
|
'uint8',
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {System} from '@/ecs/index.js';
|
import {System} from '@/ecs/index.js';
|
||||||
import {intersects} from '@/util/math.js';
|
|
||||||
import SpatialHash from '@/util/spatial-hash.js';
|
import SpatialHash from '@/util/spatial-hash.js';
|
||||||
|
|
||||||
export default class Colliders extends System {
|
export default class Colliders extends System {
|
||||||
|
@ -39,7 +38,7 @@ export default class Colliders extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
const collisions = new Map();
|
const checked = new Map();
|
||||||
for (const entity of this.ecs.changed(['Direction'])) {
|
for (const entity of this.ecs.changed(['Direction'])) {
|
||||||
if (!entity.Collider) {
|
if (!entity.Collider) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -47,99 +46,32 @@ export default class Colliders extends System {
|
||||||
entity.Collider.updateAabbs();
|
entity.Collider.updateAabbs();
|
||||||
}
|
}
|
||||||
for (const entity of this.ecs.changed(['Position'])) {
|
for (const entity of this.ecs.changed(['Position'])) {
|
||||||
if (!entity.Collider) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.updateHash(entity);
|
this.updateHash(entity);
|
||||||
}
|
}
|
||||||
for (const entity of this.ecs.changed(['Position'])) {
|
for (const entity of this.ecs.changed(['Position'])) {
|
||||||
if (!entity.Collider) {
|
if (!entity.Collider) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
collisions.set(entity, new Set());
|
if (!checked.has(entity)) {
|
||||||
for (const other of this.within(entity.Collider.aabb)) {
|
checked.set(entity, new Set());
|
||||||
if (entity === other) {
|
}
|
||||||
|
const within = this.within(entity.Collider.aabb);
|
||||||
|
for (const other of within) {
|
||||||
|
if (entity === other || !other.Collider) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!collisions.has(other)) {
|
if (!checked.has(other)) {
|
||||||
collisions.set(other, new Set());
|
checked.set(other, new Set());
|
||||||
}
|
}
|
||||||
if (!other.Collider || collisions.get(other).has(entity)) {
|
if (checked.get(entity).has(other)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const intersections = entity.Collider.isCollidingWith(other.Collider);
|
checked.get(other).add(entity);
|
||||||
if (intersections.length > 0) {
|
entity.Collider.checkCollision(other.Collider);
|
||||||
collisions.get(entity).add(other);
|
|
||||||
if (!entity.Collider.collidingWith[other.id]) {
|
|
||||||
entity.Collider.collidingWith[other.id] = true;
|
|
||||||
other.Collider.collidingWith[entity.id] = true;
|
|
||||||
if (entity.Collider.collisionStartScriptInstance) {
|
|
||||||
const script = entity.Collider.collisionStartScriptInstance.clone();
|
|
||||||
script.context.intersections = intersections;
|
|
||||||
script.context.other = other;
|
|
||||||
entity.Ticking.add(script.ticker());
|
|
||||||
}
|
|
||||||
if (other.Collider.collisionStartScriptInstance) {
|
|
||||||
const script = other.Collider.collisionStartScriptInstance.clone();
|
|
||||||
script.context.intersections = intersections
|
|
||||||
.map(([l, r]) => [r, l]);
|
|
||||||
script.context.other = entity;
|
|
||||||
other.Ticking.add(script.ticker());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const i in intersections) {
|
|
||||||
const [body, otherBody] = intersections[i];
|
|
||||||
const {impassable} = otherBody;
|
|
||||||
if (impassable) {
|
|
||||||
const j = entity.Collider.bodies.indexOf(body);
|
|
||||||
const oj = other.Collider.bodies.indexOf(otherBody);
|
|
||||||
const aabb = entity.Collider.$$aabbs[j];
|
|
||||||
const otherAabb = other.Collider.aabbs[oj];
|
|
||||||
if (!intersects(
|
|
||||||
{
|
|
||||||
x0: aabb.x0 + entity.Position.lastX,
|
|
||||||
x1: aabb.x1 + entity.Position.lastX,
|
|
||||||
y0: aabb.y0 + entity.Position.y,
|
|
||||||
y1: aabb.y1 + entity.Position.y,
|
|
||||||
},
|
|
||||||
otherAabb,
|
|
||||||
)) {
|
|
||||||
entity.Position.x = entity.Position.lastX
|
|
||||||
}
|
|
||||||
else if (!intersects(
|
|
||||||
{
|
|
||||||
x0: aabb.x0 + entity.Position.x,
|
|
||||||
x1: aabb.x1 + entity.Position.x,
|
|
||||||
y0: aabb.y0 + entity.Position.lastY,
|
|
||||||
y1: aabb.y1 + entity.Position.lastY,
|
|
||||||
},
|
|
||||||
otherAabb,
|
|
||||||
)) {
|
|
||||||
entity.Position.y = entity.Position.lastY
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
entity.Position.x = entity.Position.lastX
|
|
||||||
entity.Position.y = entity.Position.lastY
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (entity.Collider.collidingWith[other.id]) {
|
|
||||||
if (entity.Collider.collisionEndScriptInstance) {
|
|
||||||
const script = entity.Collider.collisionEndScriptInstance.clone();
|
|
||||||
script.context.other = other;
|
|
||||||
entity.Ticking.add(script.ticker());
|
|
||||||
}
|
|
||||||
if (other.Collider.collisionEndScriptInstance) {
|
|
||||||
const script = other.Collider.collisionEndScriptInstance.clone();
|
|
||||||
script.context.other = entity;
|
|
||||||
other.Ticking.add(script.ticker());
|
|
||||||
}
|
|
||||||
delete entity.Collider.collidingWith[other.id];
|
|
||||||
delete other.Collider.collidingWith[entity.id];
|
|
||||||
}
|
}
|
||||||
|
for (const [other, intersections] of entity.Collider.$$intersections) {
|
||||||
|
if (!within.has(this.ecs.get(other.entity))) {
|
||||||
|
entity.Collider.endIntersections(other, intersections);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ export default class ClientEcs extends Ecs {
|
||||||
constructor(specification) {
|
constructor(specification) {
|
||||||
super(specification);
|
super(specification);
|
||||||
[
|
[
|
||||||
'Colliders',
|
|
||||||
].forEach((defaultSystem) => {
|
].forEach((defaultSystem) => {
|
||||||
const System = this.system(defaultSystem);
|
const System = this.system(defaultSystem);
|
||||||
if (System) {
|
if (System) {
|
||||||
|
|
52
app/react/components/dom/damage.jsx
Normal file
52
app/react/components/dom/damage.jsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import {memo, useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import {DamageTypes} from '@/ecs/components/vulnerable.js';
|
||||||
|
|
||||||
|
import styles from './damage.module.css';
|
||||||
|
|
||||||
|
const damageTypeMap = {
|
||||||
|
[DamageTypes.PAIN]: styles.pain,
|
||||||
|
[DamageTypes.HEALING]: styles.healing,
|
||||||
|
[DamageTypes.MANA]: styles.mana,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Damage({
|
||||||
|
camera,
|
||||||
|
damage,
|
||||||
|
scale,
|
||||||
|
zIndex,
|
||||||
|
}) {
|
||||||
|
const [randomness] = useState({
|
||||||
|
radians: Math.random() * (Math.PI / 16) - (Math.PI / 32),
|
||||||
|
x: 1 * (Math.random() - 0.5),
|
||||||
|
y: Math.random(),
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
const handle = setTimeout(() => {
|
||||||
|
damage.onClose();
|
||||||
|
}, 1_500);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handle);
|
||||||
|
};
|
||||||
|
}, [damage]);
|
||||||
|
const {amount, position} = damage;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.damage}
|
||||||
|
style={{
|
||||||
|
'--magnitude': Math.max(1, Math.floor(Math.log10(Math.abs(amount)))),
|
||||||
|
'--positionX': `${position.x * scale - camera.x}px`,
|
||||||
|
'--positionY': `${position.y * scale - camera.y}px`,
|
||||||
|
'--randomnessX': randomness.x,
|
||||||
|
'--randomnessY': randomness.y,
|
||||||
|
'--shimmer': damageTypeMap[damage.type],
|
||||||
|
rotate: `${randomness.radians}rad`,
|
||||||
|
zIndex,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>{Math.abs(amount)}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Damage);
|
149
app/react/components/dom/damage.module.css
Normal file
149
app/react/components/dom/damage.module.css
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
@property --hue {
|
||||||
|
initial-value: 0;
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<number>';
|
||||||
|
}
|
||||||
|
@property --opacity {
|
||||||
|
initial-value: 0;
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<number>';
|
||||||
|
}
|
||||||
|
@property --scale {
|
||||||
|
initial-value: 0;
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<number>';
|
||||||
|
}
|
||||||
|
@property --shimmer {
|
||||||
|
initial-value: '';
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<custom-ident>';
|
||||||
|
}
|
||||||
|
@property --offsetX {
|
||||||
|
initial-value: 0;
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<number>';
|
||||||
|
}
|
||||||
|
@property --offsetY {
|
||||||
|
initial-value: 0;
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<number>';
|
||||||
|
}
|
||||||
|
@property --randomnessX {
|
||||||
|
initial-value: 0;
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<number>';
|
||||||
|
}
|
||||||
|
@property --randomnessY {
|
||||||
|
initial-value: 0;
|
||||||
|
inherits: false;
|
||||||
|
syntax: '<number>';
|
||||||
|
}
|
||||||
|
|
||||||
|
.damage {
|
||||||
|
--hue: 0;
|
||||||
|
--opacity: 0.75;
|
||||||
|
--randomnessX: 0;
|
||||||
|
--randomnessY: 0;
|
||||||
|
--scale: 0.35;
|
||||||
|
--background: hsl(var(--hue) 100% 12.5%);
|
||||||
|
--foreground: hsl(var(--hue) 100% 50%);
|
||||||
|
animation:
|
||||||
|
fade 1.5s linear forwards,
|
||||||
|
grow 1.5s ease-in forwards,
|
||||||
|
move 1.5s cubic-bezier(0.5, 1, 0.89, 1),
|
||||||
|
var(--shimmer) 300ms infinite cubic-bezier(0.87, 0, 0.13, 1)
|
||||||
|
;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: calc(10px + (var(--magnitude) * 12px));
|
||||||
|
opacity: var(--opacity);
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow:
|
||||||
|
0px -1px 0px var(--background),
|
||||||
|
1px 0px 0px var(--background),
|
||||||
|
0px 1px 0px var(--background),
|
||||||
|
-1px 0px 0px var(--background),
|
||||||
|
0px -2px 0px var(--background),
|
||||||
|
2px 0px 0px var(--background),
|
||||||
|
0px 2px 0px var(--background),
|
||||||
|
-2px 0px 0px var(--background)
|
||||||
|
;
|
||||||
|
scale: var(--scale);
|
||||||
|
translate:
|
||||||
|
calc(-50% + (1px * var(--offsetX)) + var(--positionX))
|
||||||
|
calc(-50% + (1px * var(--offsetY)) + var(--positionY))
|
||||||
|
;
|
||||||
|
user-select: none;
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes move {
|
||||||
|
0% {
|
||||||
|
--offsetX: 0;
|
||||||
|
--offsetY: 0;
|
||||||
|
}
|
||||||
|
33%, 91.6% {
|
||||||
|
--offsetX: calc(80 * var(--randomnessX));
|
||||||
|
--offsetY: calc(-1 * (10 + var(--randomnessY) * 90));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
0% {
|
||||||
|
--opacity: 0.75;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
--opacity: 1;
|
||||||
|
}
|
||||||
|
91.6% {
|
||||||
|
--opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
--opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes grow {
|
||||||
|
0% {
|
||||||
|
--scale: 0.35;
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
--scale: 1;
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
--scale: 1;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
--scale: 1.5;
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
--scale: 1;
|
||||||
|
}
|
||||||
|
91.6% {
|
||||||
|
--scale: 1;
|
||||||
|
}
|
||||||
|
95% {
|
||||||
|
--scale: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pain {
|
||||||
|
0% { --hue: 0 }
|
||||||
|
50% { --hue: 30 }
|
||||||
|
100% { --hue: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes healing {
|
||||||
|
0% { --hue: 120 }
|
||||||
|
50% { --hue: 90 }
|
||||||
|
100% { --hue: 120 }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mana {
|
||||||
|
0% { --hue: 220 }
|
||||||
|
50% { --hue: 215 }
|
||||||
|
100% { --hue: 220 }
|
||||||
|
}
|
23
app/react/components/dom/damages.jsx
Normal file
23
app/react/components/dom/damages.jsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import styles from './damages.module.css';
|
||||||
|
|
||||||
|
import Damage from './damage.jsx';
|
||||||
|
|
||||||
|
export default function Damages({camera, damages, scale}) {
|
||||||
|
const elements = [];
|
||||||
|
for (const key in damages) {
|
||||||
|
elements.push(
|
||||||
|
<Damage
|
||||||
|
camera={camera}
|
||||||
|
damage={damages[key]}
|
||||||
|
key={key}
|
||||||
|
scale={scale}
|
||||||
|
zIndex={key}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (0 === elements.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return <div className={styles.damages}>{elements}</div>;
|
||||||
|
}
|
||||||
|
|
3
app/react/components/dom/damages.module.css
Normal file
3
app/react/components/dom/damages.module.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.damages {
|
||||||
|
font-family: Joystix, 'Courier New', Courier, monospace;
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import {usePacket} from '@/react/context/client.js';
|
||||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||||
import {parseLetters} from '@/util/dialogue.js';
|
import {parseLetters} from '@/util/dialogue.js';
|
||||||
|
|
||||||
|
import Damages from './damages.jsx';
|
||||||
import Entity from './entity.jsx';
|
import Entity from './entity.jsx';
|
||||||
|
|
||||||
export default function Entities({
|
export default function Entities({
|
||||||
|
@ -14,6 +15,7 @@ export default function Entities({
|
||||||
}) {
|
}) {
|
||||||
const [ecs] = useEcs();
|
const [ecs] = useEcs();
|
||||||
const [entities, setEntities] = useState({});
|
const [entities, setEntities] = useState({});
|
||||||
|
const [damages, setDamages] = useState({});
|
||||||
usePacket('EcsChange', async () => {
|
usePacket('EcsChange', async () => {
|
||||||
setEntities({});
|
setEntities({});
|
||||||
}, [setEntities]);
|
}, [setEntities]);
|
||||||
|
@ -88,6 +90,22 @@ export default function Entities({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const {damage} = update.Vulnerable || {};
|
||||||
|
if (damage) {
|
||||||
|
for (const key in damage) {
|
||||||
|
const composite = [id, key].join('-');
|
||||||
|
damage[key].onClose = () => {
|
||||||
|
setDamages((damages) => {
|
||||||
|
const {[composite]: _, ...rest} = damages; // eslint-disable-line no-unused-vars
|
||||||
|
return rest;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
setDamages((damages) => ({
|
||||||
|
...damages,
|
||||||
|
[composite]: damage[key],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setEntities((entities) => {
|
setEntities((entities) => {
|
||||||
for (const id in deleting) {
|
for (const id in deleting) {
|
||||||
|
@ -113,6 +131,11 @@ export default function Entities({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderables}
|
{renderables}
|
||||||
|
<Damages
|
||||||
|
camera={camera}
|
||||||
|
damages={damages}
|
||||||
|
scale={scale}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,13 +13,13 @@ function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
||||||
g.clear();
|
g.clear();
|
||||||
g.lineStyle(width, color);
|
g.lineStyle(width, color);
|
||||||
g.moveTo(x0, y0);
|
g.moveTo(x0, y0);
|
||||||
g.lineTo(x1 + 1, y0);
|
g.lineTo(x1, y0);
|
||||||
g.lineTo(x1 + 1, y1 + 1);
|
g.lineTo(x1, y1);
|
||||||
g.lineTo(x0, y1 + 1);
|
g.lineTo(x0, y1);
|
||||||
g.lineTo(x0, y0);
|
g.lineTo(x0, y0);
|
||||||
}, [color, width, x0, x1, y0, y1]);
|
}, [color, width, x0, x1, y0, y1]);
|
||||||
return (
|
return (
|
||||||
<Graphics draw={draw} x={0.5} y={0.5} {...rest} />
|
<Graphics draw={draw} x={0} y={0} {...rest} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,3 +31,8 @@ body {
|
||||||
font-family: "Cookbook";
|
font-family: "Cookbook";
|
||||||
src: url("/assets/fonts/Cookbook.woff") format("woff");
|
src: url("/assets/fonts/Cookbook.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Joystix";
|
||||||
|
src: url("/assets/fonts/Joystix.ttf") format("ttf");
|
||||||
|
}
|
||||||
|
|
|
@ -28,15 +28,6 @@ export default async function createHomestead(id) {
|
||||||
entities.push({
|
entities.push({
|
||||||
Collider: {
|
Collider: {
|
||||||
bodies: [
|
bodies: [
|
||||||
{
|
|
||||||
points: [
|
|
||||||
{x: -36, y: 8},
|
|
||||||
{x: -21, y: 8},
|
|
||||||
{x: -36, y: 17},
|
|
||||||
{x: -21, y: 17},
|
|
||||||
],
|
|
||||||
tags: ['door'],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
impassable: 1,
|
impassable: 1,
|
||||||
points: [
|
points: [
|
||||||
|
@ -47,7 +38,6 @@ export default async function createHomestead(id) {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
collisionStartScript: '/assets/shit-shack/collision-start.js',
|
|
||||||
},
|
},
|
||||||
Ecs: {
|
Ecs: {
|
||||||
path: ['houses', `${id}`].join('/'),
|
path: ['houses', `${id}`].join('/'),
|
||||||
|
@ -61,6 +51,36 @@ export default async function createHomestead(id) {
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
});
|
||||||
|
entities.push({
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
{x: -8, y: -5},
|
||||||
|
{x: 8, y: -5},
|
||||||
|
{x: 8, y: 5},
|
||||||
|
{x: -8, y: 5},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
collisionStartScript: `
|
||||||
|
if (other.Player) {
|
||||||
|
ecs.switchEcs(
|
||||||
|
other,
|
||||||
|
'houses/${id}',
|
||||||
|
{
|
||||||
|
Position: {
|
||||||
|
x: 72,
|
||||||
|
y: 304,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
Position: {x: 71, y: 113},
|
||||||
|
Ticking: {},
|
||||||
|
});
|
||||||
entities.push({
|
entities.push({
|
||||||
Collider: {
|
Collider: {
|
||||||
bodies: [
|
bodies: [
|
||||||
|
@ -118,6 +138,7 @@ export default async function createHomestead(id) {
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
});
|
||||||
const kitty = {
|
const kitty = {
|
||||||
|
Alive: {health: 100},
|
||||||
Behaving: {
|
Behaving: {
|
||||||
routines: {
|
routines: {
|
||||||
initial: '/assets/kitty/initial.js',
|
initial: '/assets/kitty/initial.js',
|
||||||
|
@ -127,10 +148,10 @@ export default async function createHomestead(id) {
|
||||||
bodies: [
|
bodies: [
|
||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
{x: -4, y: -4},
|
{x: -3.5, y: -3.5},
|
||||||
{x: 3, y: -4},
|
{x: 3.5, y: -3.5},
|
||||||
{x: 3, y: 3},
|
{x: 3.5, y: 3.5},
|
||||||
{x: -4, y: 3},
|
{x: -3.5, y: 3.5},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -171,8 +192,9 @@ export default async function createHomestead(id) {
|
||||||
Tags: {tags: ['kittan']},
|
Tags: {tags: ['kittan']},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
|
Vulnerable: {},
|
||||||
};
|
};
|
||||||
for (let i = 0; i < 30; ++i) {
|
for (let i = 0; i < 10; ++i) {
|
||||||
entities.push(kitty);
|
entities.push(kitty);
|
||||||
}
|
}
|
||||||
entities.push({
|
entities.push({
|
||||||
|
|
|
@ -29,10 +29,20 @@ export default async function createHouse(Ecs, id) {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
collisionStartScript: '/assets/house/collision-start.js',
|
collisionStartScript: `
|
||||||
|
if (other.Player) {
|
||||||
|
ecs.switchEcs(
|
||||||
|
other,
|
||||||
|
'homesteads/${id}',
|
||||||
|
{
|
||||||
|
Position: {
|
||||||
|
x: 74,
|
||||||
|
y: 128,
|
||||||
},
|
},
|
||||||
Ecs: {
|
},
|
||||||
path: ['homesteads', `${id}`].join('/'),
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
Position: {
|
Position: {
|
||||||
x: 72,
|
x: 72,
|
||||||
|
|
|
@ -5,10 +5,10 @@ export default async function createPlayer(id) {
|
||||||
bodies: [
|
bodies: [
|
||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
{x: -4, y: -4},
|
{x: -3.5, y: -3.5},
|
||||||
{x: 3, y: -4},
|
{x: 3.5, y: -3.5},
|
||||||
{x: 3, y: 3},
|
{x: 3.5, y: 3.5},
|
||||||
{x: -4, y: 3},
|
{x: -3.5, y: 3.5},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -26,6 +26,10 @@ export default async function createPlayer(id) {
|
||||||
qty: 100,
|
qty: 100,
|
||||||
source: '/assets/potion/potion.json',
|
source: '/assets/potion/potion.json',
|
||||||
},
|
},
|
||||||
|
2: {
|
||||||
|
qty: 1,
|
||||||
|
source: '/assets/magic-swords/magic-swords.json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Health: {health: 100},
|
Health: {health: 100},
|
||||||
|
@ -46,7 +50,7 @@ export default async function createPlayer(id) {
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
Wielder: {
|
Wielder: {
|
||||||
activeSlot: 0,
|
activeSlot: 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return player;
|
return player;
|
||||||
|
|
|
@ -48,6 +48,9 @@ export default class Engine {
|
||||||
get frame() {
|
get frame() {
|
||||||
return engine.frame;
|
return engine.frame;
|
||||||
}
|
}
|
||||||
|
lookupPlayerEntity(id) {
|
||||||
|
return engine.lookupPlayerEntity(id);
|
||||||
|
}
|
||||||
async readAsset(uri) {
|
async readAsset(uri) {
|
||||||
if (!cache.has(uri)) {
|
if (!cache.has(uri)) {
|
||||||
const {promise, resolve, reject} = withResolvers();
|
const {promise, resolve, reject} = withResolvers();
|
||||||
|
@ -86,15 +89,18 @@ export default class Engine {
|
||||||
Ecs: {path},
|
Ecs: {path},
|
||||||
...updates,
|
...updates,
|
||||||
};
|
};
|
||||||
// remove from old ECS
|
const promises = [];
|
||||||
this.destroyImmediately(entity.id);
|
|
||||||
// load if necessary
|
// load if necessary
|
||||||
if (!engine.ecses[path]) {
|
if (!engine.ecses[path]) {
|
||||||
await engine.loadEcs(path);
|
promises.push(engine.loadEcs(path));
|
||||||
}
|
}
|
||||||
|
// remove from old ECS
|
||||||
|
promises.push(this.destroy(entity.id));
|
||||||
|
Promise.all(promises).then(async () => {
|
||||||
// recreate the entity in the new ECS and again associate it with the connection
|
// recreate the entity in the new ECS and again associate it with the connection
|
||||||
connectedPlayer.entity = engine.ecses[path].get(await engine.ecses[path].create(dumped));
|
connectedPlayer.entity = engine.ecses[path].get(await engine.ecses[path].create(dumped));
|
||||||
connectedPlayer.entity.Player.id = id
|
connectedPlayer.entity.Player.id = id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,12 +258,15 @@ export default class Engine {
|
||||||
if (!connectedPlayer) {
|
if (!connectedPlayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {entity, id} = connectedPlayer;
|
|
||||||
const ecs = this.ecses[entity.Ecs.path];
|
|
||||||
await this.savePlayer(id, entity);
|
|
||||||
ecs.destroyImmediately(entity.id);
|
|
||||||
this.connectedPlayers.delete(connection);
|
this.connectedPlayers.delete(connection);
|
||||||
this.incomingActions.delete(connection);
|
this.incomingActions.delete(connection);
|
||||||
|
const {entity, id} = connectedPlayer;
|
||||||
|
const json = entity.toJSON();
|
||||||
|
const ecs = this.ecses[entity.Ecs.path];
|
||||||
|
return Promise.all([
|
||||||
|
ecs.destroy(entity.id),
|
||||||
|
this.savePlayer(id, json),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -325,6 +334,14 @@ export default class Engine {
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lookupPlayerEntity(id) {
|
||||||
|
for (const [, player] of this.connectedPlayers) {
|
||||||
|
if (player.id == id) {
|
||||||
|
return player.entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async saveEcs(path, ecs) {
|
async saveEcs(path, ecs) {
|
||||||
const view = this.Ecs.serialize(ecs);
|
const view = this.Ecs.serialize(ecs);
|
||||||
await this.server.writeData(path, view);
|
await this.server.writeData(path, view);
|
||||||
|
|
|
@ -67,8 +67,8 @@ if (import.meta.hot) {
|
||||||
const before = withResolvers();
|
const before = withResolvers();
|
||||||
const promises = [before.promise];
|
const promises = [before.promise];
|
||||||
import.meta.hot.on('vite:beforeUpdate', async () => {
|
import.meta.hot.on('vite:beforeUpdate', async () => {
|
||||||
engine.stop();
|
|
||||||
await engine.disconnectPlayer(0);
|
await engine.disconnectPlayer(0);
|
||||||
|
engine.stop();
|
||||||
await engine.saveEcses();
|
await engine.saveEcses();
|
||||||
before.resolve();
|
before.resolve();
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const {
|
||||||
log10,
|
log10,
|
||||||
max,
|
max,
|
||||||
min,
|
min,
|
||||||
|
pow,
|
||||||
round,
|
round,
|
||||||
sign,
|
sign,
|
||||||
sin,
|
sin,
|
||||||
|
@ -317,6 +318,10 @@ export function removeCollinear([...vertices]) {
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const smoothstep = (x) => x * x * (3 - 2 * x);
|
||||||
|
|
||||||
|
export const smootherstep = (x) => x * x * x * (x * (x * 6 - 15) + 10);
|
||||||
|
|
||||||
export function transform(
|
export function transform(
|
||||||
vertices,
|
vertices,
|
||||||
{
|
{
|
||||||
|
|
BIN
public/assets/fonts/Joystix.ttf
Normal file
BIN
public/assets/fonts/Joystix.ttf
Normal file
Binary file not shown.
|
@ -1,10 +0,0 @@
|
||||||
ecs.switchEcs(
|
|
||||||
other,
|
|
||||||
entity.Ecs.path,
|
|
||||||
{
|
|
||||||
Position: {
|
|
||||||
x: 74,
|
|
||||||
y: 128,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
12
public/assets/magic-swords/collision-start.js
Normal file
12
public/assets/magic-swords/collision-start.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const playerEntity = ecs.lookupPlayerEntity(entity.Owned.owner);
|
||||||
|
if (playerEntity !== other && other.Vulnerable) {
|
||||||
|
const magnitude = Math.floor(Math.random() * 2)
|
||||||
|
other.Vulnerable.damage({
|
||||||
|
amount: -Math.floor(
|
||||||
|
Math.pow(10, magnitude)
|
||||||
|
+ Math.random() * (Math.pow(10, magnitude + 1) - Math.pow(10, magnitude)),
|
||||||
|
),
|
||||||
|
position: other.Position.toJSON(),
|
||||||
|
type: other.Vulnerable.Types.PAIN,
|
||||||
|
})
|
||||||
|
}
|
BIN
public/assets/magic-swords/icon.png
Normal file
BIN
public/assets/magic-swords/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
1
public/assets/magic-swords/magic-sword-shot.json
Normal file
1
public/assets/magic-swords/magic-sword-shot.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"frames":{"":{"frame":{"x":0,"y":0,"w":22,"h":22},"spriteSourceSize":{"x":0,"y":0,"w":22,"h":22},"sourceSize":{"w":22,"h":22}}},"meta":{"format":"RGBA8888","image":"./magic-sword-shot.png","rotation":2.356194490192345,"scale":1,"size":{"w":22,"h":22}}}
|
BIN
public/assets/magic-swords/magic-sword-shot.png
Normal file
BIN
public/assets/magic-swords/magic-sword-shot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
5
public/assets/magic-swords/magic-swords.json
Normal file
5
public/assets/magic-swords/magic-swords.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"icon": "/assets/magic-swords/icon.png",
|
||||||
|
"label": "Magic swords",
|
||||||
|
"start": "/assets/magic-swords/start.js"
|
||||||
|
}
|
90
public/assets/magic-swords/start.js
Normal file
90
public/assets/magic-swords/start.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
const {Player, Position} = wielder;
|
||||||
|
|
||||||
|
const shots = [];
|
||||||
|
|
||||||
|
const EVERY = 0.03;
|
||||||
|
const N = 14;
|
||||||
|
const SPREAD = 1;
|
||||||
|
|
||||||
|
const creating = [];
|
||||||
|
const promises = []
|
||||||
|
|
||||||
|
for (let i = 0; i < N; ++i) {
|
||||||
|
promises.push(ecs.create({
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
group: -1,
|
||||||
|
points: [
|
||||||
|
{x: -2.5, y: -2.5},
|
||||||
|
{x: 14, y: -2.5},
|
||||||
|
{x: 14, y: 2.5},
|
||||||
|
{x: -2.5, y: 2.5},
|
||||||
|
],
|
||||||
|
unstoppable: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
collisionStartScript: '/assets/magic-swords/collision-start.js',
|
||||||
|
},
|
||||||
|
Controlled: {},
|
||||||
|
Direction: {direction: Math.TAU * (i / N)},
|
||||||
|
Forces: {},
|
||||||
|
Owned: {owner: Player ? Player.id : 0},
|
||||||
|
Position: {x: Position.x, y: Position.y},
|
||||||
|
Speed: {},
|
||||||
|
Sprite: {
|
||||||
|
alpha: 0,
|
||||||
|
source: '/assets/magic-swords/magic-sword-shot.json',
|
||||||
|
},
|
||||||
|
Ticking: {},
|
||||||
|
VisibleAabb: {},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of await Promise.all(promises)) {
|
||||||
|
creating.push(ecs.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const accumulated = {};
|
||||||
|
const shot = creating.shift();
|
||||||
|
shot.Sprite.alpha = 1;
|
||||||
|
accumulated[shot.id] = 0;
|
||||||
|
shots.push(shot)
|
||||||
|
|
||||||
|
let spawner = 0;
|
||||||
|
while (shots.length > 0) {
|
||||||
|
spawner += elapsed;
|
||||||
|
if (creating.length > 0 && spawner >= EVERY) {
|
||||||
|
const shot = creating.shift();
|
||||||
|
shot.Sprite.alpha = 1;
|
||||||
|
accumulated[shot.id] = 0;
|
||||||
|
shots.push(shot)
|
||||||
|
spawner -= EVERY;
|
||||||
|
}
|
||||||
|
const destroying = [];
|
||||||
|
for (const shot of shots) {
|
||||||
|
accumulated[shot.id] += elapsed;
|
||||||
|
if (accumulated[shot.id] <= SPREAD) {
|
||||||
|
shot.Speed.speed = 100 * (1 - (accumulated[shot.id] / SPREAD))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const toward = Math.atan2(
|
||||||
|
where.y - shot.Position.y,
|
||||||
|
where.x - shot.Position.x,
|
||||||
|
)
|
||||||
|
shot.Speed.speed = 400;
|
||||||
|
shot.Direction.direction = (Math.TAU + toward) % Math.TAU;
|
||||||
|
if (Math.distance(where, shot.Position) < 4) {
|
||||||
|
delete accumulated[shot.id];
|
||||||
|
shot.Sprite.alpha = 0;
|
||||||
|
ecs.destroy(shot.id);
|
||||||
|
destroying.push(shot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shot.Controlled.directionMove(shot.Direction.direction);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < destroying.length; ++i) {
|
||||||
|
shots.splice(shots.indexOf(destroying[i]), 1);
|
||||||
|
}
|
||||||
|
await wait(0);
|
||||||
|
}
|
19
public/assets/misc/death-default.js
Normal file
19
public/assets/misc/death-default.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
const {Sprite, Ticking} = entity;
|
||||||
|
if (Sprite) {
|
||||||
|
const {promise} = transition(
|
||||||
|
entity.Sprite,
|
||||||
|
{
|
||||||
|
scaleX: {
|
||||||
|
duration: 0.25,
|
||||||
|
magnitude: -entity.Sprite.scaleX,
|
||||||
|
},
|
||||||
|
scaleY: {
|
||||||
|
duration: 0.25,
|
||||||
|
magnitude: entity.Sprite.scaleY * 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Ticking.add(promise);
|
||||||
|
await promise;
|
||||||
|
ecs.destroy(entity.id);
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
for (const [{tags}] of intersections) {
|
|
||||||
if (tags && tags.includes('door')) {
|
|
||||||
if (other.Player) {
|
|
||||||
ecs.switchEcs(
|
|
||||||
other,
|
|
||||||
entity.Ecs.path,
|
|
||||||
{
|
|
||||||
Position: {
|
|
||||||
x: 72,
|
|
||||||
y: 304,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,7 +17,15 @@ for (let i = 0; i < 10; ++i) {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
collisionStartScript: '/assets/tomato/collision-start.js',
|
collisionStartScript: `
|
||||||
|
if (other.Inventory) {
|
||||||
|
other.Inventory.give({
|
||||||
|
qty: 1,
|
||||||
|
source: '/assets/tomato/tomato.json',
|
||||||
|
})
|
||||||
|
ecs.destroy(entity.id)
|
||||||
|
}
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
Forces: {},
|
Forces: {},
|
||||||
Magnetic: {},
|
Magnetic: {},
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
if (other.Inventory) {
|
|
||||||
other.Inventory.give({
|
|
||||||
qty: 1,
|
|
||||||
source: '/assets/tomato/tomato.json',
|
|
||||||
})
|
|
||||||
ecs.destroy(entity.id)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user