Compare commits
23 Commits
3d862d39d5
...
6ff824ace6
Author | SHA1 | Date | |
---|---|---|---|
|
6ff824ace6 | ||
|
1a8cefd325 | ||
|
f1d1422e7a | ||
|
fcefe1a620 | ||
|
271b944796 | ||
|
36be786348 | ||
|
be7ec5c243 | ||
|
82c1358ecc | ||
|
447774f0da | ||
|
e02a63e7b5 | ||
|
ec3cef8ee7 | ||
|
48ef8a6cbd | ||
|
7bdd598915 | ||
|
dad9b1d3e7 | ||
|
875449e816 | ||
|
3de969ce1e | ||
|
d412a08810 | ||
|
5b42654892 | ||
|
a553ef99c6 | ||
|
4a7006a48d | ||
|
64cb88ae2f | ||
|
4eb1cf5772 | ||
|
b96566d0a0 |
|
@ -19,6 +19,15 @@ export default class Sandbox {
|
|||
this.compile();
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new this.constructor(
|
||||
this.ast,
|
||||
{
|
||||
...this.$$context,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
compile() {
|
||||
let scope = new Scope();
|
||||
scope.context = this.$$context;
|
||||
|
@ -327,6 +336,7 @@ export default class Sandbox {
|
|||
break;
|
||||
}
|
||||
case 'Program': {
|
||||
result = {value: undefined, yield: YIELD_NONE};
|
||||
let skipping = isReplaying;
|
||||
for (const child of node.body) {
|
||||
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||
|
|
|
@ -2,6 +2,8 @@ export const CLIENT_LATENCY = 0;
|
|||
|
||||
export const CLIENT_PREDICTION = true;
|
||||
|
||||
export const IRL_MINUTES_PER_GAME_DAY = 20;
|
||||
|
||||
export const RESOLUTION = {
|
||||
x: 800,
|
||||
y: 450,
|
||||
|
|
|
@ -4,6 +4,8 @@ import Systems from '@/ecs-systems/index.js';
|
|||
export default function createEcs(Ecs) {
|
||||
const ecs = new Ecs({Components, Systems});
|
||||
const defaultSystems = [
|
||||
'PassTime',
|
||||
'Attract',
|
||||
'ResetForces',
|
||||
'ApplyControlMovement',
|
||||
'IntegratePhysics',
|
||||
|
|
|
@ -16,26 +16,41 @@ export default async function createHomestead(Ecs) {
|
|||
}
|
||||
],
|
||||
},
|
||||
Time: {},
|
||||
Water: {water: {}},
|
||||
});
|
||||
await ecs.create({
|
||||
Collider: {
|
||||
bodies: [
|
||||
[
|
||||
{x: -36, y: -16},
|
||||
{x: -21, y: -16},
|
||||
{x: -36, y: -1},
|
||||
{x: -21, y: -1},
|
||||
],
|
||||
{
|
||||
points: [
|
||||
{x: -36, y: 8},
|
||||
{x: -21, y: 8},
|
||||
{x: -36, y: 17},
|
||||
{x: -21, y: 17},
|
||||
],
|
||||
tags: ['door'],
|
||||
},
|
||||
{
|
||||
impassable: 1,
|
||||
points: [
|
||||
{x: -52, y: -16},
|
||||
{x: 48, y: -16},
|
||||
{x: -52, y: 15},
|
||||
{x: 48, y: 15},
|
||||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: '/assets/shit-shack/collision-start.js',
|
||||
},
|
||||
Ecs: {},
|
||||
Position: {x: 100, y: 100},
|
||||
Sprite: {
|
||||
anchor: {x: 0.5, y: 0.8},
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.8,
|
||||
source: '/assets/shit-shack/shit-shack.json',
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
return ecs;
|
||||
|
|
|
@ -20,12 +20,14 @@ export default async function createHouse(Ecs) {
|
|||
await ecs.create({
|
||||
Collider: {
|
||||
bodies: [
|
||||
[
|
||||
{x: -8, y: -8},
|
||||
{x: 7, y: -8},
|
||||
{x: 7, y: 7},
|
||||
{x: -8, y: 7},
|
||||
],
|
||||
{
|
||||
points: [
|
||||
{x: -8, y: -8},
|
||||
{x: 7, y: -8},
|
||||
{x: 7, y: 7},
|
||||
{x: -8, y: 7},
|
||||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: '/assets/house/collision-start.js',
|
||||
},
|
||||
|
@ -34,6 +36,7 @@ export default async function createHouse(Ecs) {
|
|||
x: 72,
|
||||
y: 320,
|
||||
},
|
||||
Ticking: {},
|
||||
});
|
||||
return ecs;
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ export default async function createPlayer(id) {
|
|||
Camera: {},
|
||||
Collider: {
|
||||
bodies: [
|
||||
[
|
||||
{x: -8, y: -8},
|
||||
{x: 7, y: -8},
|
||||
{x: -8, y: 7},
|
||||
{x: 7, y: 7},
|
||||
],
|
||||
{
|
||||
points: [
|
||||
{x: -8, y: -8},
|
||||
{x: 7, y: -8},
|
||||
{x: 7, y: 7},
|
||||
{x: -8, y: 7},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Controlled: {},
|
||||
|
@ -38,11 +40,14 @@ export default async function createPlayer(id) {
|
|||
},
|
||||
},
|
||||
Health: {health: 100},
|
||||
Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 128},
|
||||
Speed: {speed: 100},
|
||||
Sound: {},
|
||||
Sprite: {
|
||||
anchor: {x: 0.5, y: 0.8},
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.8,
|
||||
animation: 'moving:down',
|
||||
frame: 0,
|
||||
frames: 8,
|
||||
|
|
|
@ -11,17 +11,20 @@ export default class Collider extends Component {
|
|||
isCollidingWith(other) {
|
||||
const {aabb, aabbs} = this;
|
||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||
const intersections = [];
|
||||
if (!intersects(aabb, otherAabb)) {
|
||||
return false;
|
||||
return intersections;
|
||||
}
|
||||
for (const aabb of aabbs) {
|
||||
for (const otherAabb of otherAabbs) {
|
||||
for (const i in aabbs) {
|
||||
const aabb = aabbs[i];
|
||||
for (const j in otherAabbs) {
|
||||
const otherAabb = otherAabbs[j];
|
||||
if (intersects(aabb, otherAabb)) {
|
||||
return true;
|
||||
intersections.push([this.bodies[i], other.bodies[j]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return intersections;
|
||||
}
|
||||
isWithin(query) {
|
||||
const {aabb, aabbs} = this;
|
||||
|
@ -40,9 +43,9 @@ export default class Collider extends Component {
|
|||
this.aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||
this.aabbs = [];
|
||||
const {bodies} = this;
|
||||
for (const points of bodies) {
|
||||
for (const body of bodies) {
|
||||
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
||||
for (const point of points) {
|
||||
for (const point of body.points) {
|
||||
const x = point.x + px;
|
||||
const y = point.y + py;
|
||||
if (x < x0) x0 = x;
|
||||
|
@ -88,8 +91,18 @@ export default class Collider extends Component {
|
|||
bodies: {
|
||||
type: 'array',
|
||||
subtype: {
|
||||
type: 'array',
|
||||
subtype: vector2d('int16'),
|
||||
type: 'object',
|
||||
properties: {
|
||||
impassable: {type: 'uint8'},
|
||||
points: {
|
||||
type: 'array',
|
||||
subtype: vector2d('int16'),
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
subtype: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collisionEndScript: {type: 'string'},
|
||||
|
|
|
@ -5,9 +5,10 @@ export default class Interactive extends Component {
|
|||
const {ecs} = this;
|
||||
return class ControlledInstance extends super.instanceFromSchema() {
|
||||
interact(initiator) {
|
||||
this.interactScriptInstance.context.initiator = initiator;
|
||||
const script = this.interactScriptInstance.clone();
|
||||
script.context.initiator = initiator;
|
||||
const {Ticking} = ecs.get(this.entity);
|
||||
Ticking.addTickingPromise(this.interactScriptInstance.tickingPromise());
|
||||
Ticking.addTickingPromise(script.tickingPromise());
|
||||
}
|
||||
get interacting() {
|
||||
return !!this.$$interacting;
|
||||
|
|
7
app/ecs-components/magnet.js
Normal file
7
app/ecs-components/magnet.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Magnet extends Component {
|
||||
static properties = {
|
||||
strength: {type: 'uint8'},
|
||||
};
|
||||
}
|
4
app/ecs-components/magnetic.js
Normal file
4
app/ecs-components/magnetic.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Magnetic extends Component {
|
||||
}
|
3
app/ecs-components/player.js
Normal file
3
app/ecs-components/player.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Player extends Component {}
|
|
@ -2,20 +2,34 @@ import Component from '@/ecs/component.js';
|
|||
|
||||
export default class Position extends Component {
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
Object.defineProperty(Instance.prototype, 'tile', {
|
||||
get: function () {
|
||||
const {TileLayers} = Component.ecs.get(1);
|
||||
const {Position: {x, y}} = Component.ecs.get(this.entity);
|
||||
const {ecs} = this;
|
||||
return class PositionInstance extends super.instanceFromSchema() {
|
||||
lastX;
|
||||
lastY;
|
||||
get x() {
|
||||
return super.x;
|
||||
}
|
||||
set x(x) {
|
||||
this.lastX = super.x;
|
||||
super.x = x;
|
||||
}
|
||||
get y() {
|
||||
return super.y;
|
||||
}
|
||||
set y(y) {
|
||||
this.lastY = super.y;
|
||||
super.y = y;
|
||||
}
|
||||
get tile() {
|
||||
const {TileLayers} = ecs.get(1);
|
||||
const {Position: {x, y}} = ecs.get(this.entity);
|
||||
const {tileSize} = TileLayers.layers[0];
|
||||
return {
|
||||
x: (x - (x % tileSize.x)) / tileSize.x,
|
||||
y: (y - (y % tileSize.y)) / tileSize.y,
|
||||
}
|
||||
},
|
||||
});
|
||||
return Instance;
|
||||
}
|
||||
};
|
||||
}
|
||||
static properties = {
|
||||
x: {type: 'float32'},
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
import vector2d from "./helpers/vector-2d";
|
||||
|
||||
export default class Sprite extends Component {
|
||||
instanceFromSchema() {
|
||||
return class SpriteInstance extends super.instanceFromSchema() {
|
||||
get anchor() {
|
||||
return {x: this.anchorX, y: this.anchorY};
|
||||
}
|
||||
get scale() {
|
||||
return {x: this.scaleX, y: this.scaleY};
|
||||
}
|
||||
};
|
||||
}
|
||||
async load(instance) {
|
||||
instance.$$sourceJson = await this.ecs.readJson(instance.source);
|
||||
}
|
||||
static properties = {
|
||||
anchor: vector2d('float32', {x: 0.5, y: 0.5}),
|
||||
anchorX: {defaultValue: 0.5, type: 'float32'},
|
||||
anchorY: {defaultValue: 0.5, type: 'float32'},
|
||||
animation: {type: 'string'},
|
||||
elapsed: {type: 'float32'},
|
||||
frame: {type: 'uint16'},
|
||||
frames: {type: 'uint16'},
|
||||
scale: vector2d('float32', {x: 1, y: 1}),
|
||||
scaleX: {defaultValue: 1, type: 'float32'},
|
||||
scaleY: {defaultValue: 1, type: 'float32'},
|
||||
source: {type: 'string'},
|
||||
speed: {type: 'float32'},
|
||||
};
|
||||
|
|
|
@ -2,9 +2,7 @@ import Component from '@/ecs/component.js';
|
|||
|
||||
export default class Ticking extends Component {
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
|
||||
return class TickingInstance extends Instance {
|
||||
return class TickingInstance extends super.instanceFromSchema() {
|
||||
|
||||
$$finished = [];
|
||||
$$tickingPromises = [];
|
||||
|
@ -16,6 +14,11 @@ export default class Ticking extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.$$finished = [];
|
||||
this.$$tickingPromises = [];
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const tickingPromise of this.$$finished) {
|
||||
this.$$tickingPromises.splice(
|
||||
|
|
23
app/ecs-components/time.js
Normal file
23
app/ecs-components/time.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {IRL_MINUTES_PER_GAME_DAY} from '@/constants';
|
||||
import Component from '@/ecs/component.js';
|
||||
|
||||
const realSecondsPerGameDay = 60 * IRL_MINUTES_PER_GAME_DAY;
|
||||
const realSecondsPerGameHour = realSecondsPerGameDay / 24;
|
||||
const realSecondsPerGameMinute = realSecondsPerGameHour / 60;
|
||||
|
||||
export default class Time extends Component {
|
||||
instanceFromSchema() {
|
||||
return class TimeInstance extends super.instanceFromSchema() {
|
||||
static gameDayLengthInRealSeconds = 24 * realSecondsPerGameHour;
|
||||
get hour() {
|
||||
return this.$$irlSeconds / realSecondsPerGameHour;
|
||||
}
|
||||
get minute() {
|
||||
return (this.$$irlSeconds % realSecondsPerGameHour) / realSecondsPerGameMinute;
|
||||
}
|
||||
};
|
||||
}
|
||||
static properties = {
|
||||
irlSeconds: {defaultValue: 18 * realSecondsPerGameHour, type: 'uint16'},
|
||||
};
|
||||
}
|
|
@ -3,8 +3,7 @@ import Component from '@/ecs/component.js';
|
|||
export default class Wielder extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
const Instance = super.instanceFromSchema();
|
||||
return class WielderInstance extends Instance {
|
||||
return class WielderInstance extends super.instanceFromSchema() {
|
||||
activeItem() {
|
||||
const {Inventory, Wielder} = ecs.get(this.entity);
|
||||
return Inventory.item(Wielder.activeSlot + 1);
|
||||
|
@ -15,8 +14,9 @@ export default class Wielder extends Component {
|
|||
const activeItem = this.activeItem();
|
||||
if (activeItem) {
|
||||
const {startInstance, stopInstance} = activeItem.scripts;
|
||||
const script = state ? startInstance : stopInstance;
|
||||
let script = state ? startInstance : stopInstance;
|
||||
if (script) {
|
||||
script = script.clone();
|
||||
script.context.ecs = ecs;
|
||||
script.context.item = activeItem;
|
||||
script.context.wielder = entity;
|
||||
|
|
|
@ -14,6 +14,7 @@ export default class ApplyControlMovement extends System {
|
|||
default: ['Controlled', 'Forces', 'Speed'],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const {Controlled, Forces, Speed} of this.select('default')) {
|
||||
if (!Controlled.locked) {
|
||||
|
|
52
app/ecs-systems/attract.js
Normal file
52
app/ecs-systems/attract.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import {distance, normalizeVector} from '@/util/math.js';
|
||||
|
||||
export default class Attract extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Magnet'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const entity of this.select('default')) {
|
||||
const {Magnet, Position} = entity;
|
||||
const aabb = {
|
||||
x0: Position.x - Magnet.strength / 2,
|
||||
x1: Position.x + (Magnet.strength / 2) - 1,
|
||||
y0: Position.y - Magnet.strength / 2,
|
||||
y1: Position.y + (Magnet.strength / 2) - 1,
|
||||
};
|
||||
let s = Magnet.strength;
|
||||
s = s * s;
|
||||
for (const other of this.ecs.system('Colliders').within(aabb)) {
|
||||
if (other === entity || !other.Magnetic) {
|
||||
continue;
|
||||
}
|
||||
const difference = {
|
||||
x: entity.Position.x - other.Position.x,
|
||||
y: entity.Position.y - other.Position.y,
|
||||
};
|
||||
const toward = normalizeVector(difference);
|
||||
let d = distance(entity.Position, other.Position);
|
||||
if (d > 0) {
|
||||
const inv = s * (1 / (d * d));
|
||||
let impulse = {
|
||||
x: toward.x * inv,
|
||||
y: toward.y * inv,
|
||||
};
|
||||
if (Math.sign(entity.Position.x - (impulse.x * elapsed + other.Position.x)) !== Math.sign(difference.x)) {
|
||||
impulse.x = difference.x / elapsed;
|
||||
}
|
||||
if (Math.sign(entity.Position.y - (impulse.y * elapsed + other.Position.y)) !== Math.sign(difference.y)) {
|
||||
impulse.y = difference.y / elapsed;
|
||||
}
|
||||
other.Forces.applyImpulse(impulse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -39,7 +39,6 @@ export default class Colliders extends System {
|
|||
}
|
||||
|
||||
tick() {
|
||||
const {Ticking} = this.ecs.get(1);
|
||||
const seen = {};
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
if (seen[entity.id]) {
|
||||
|
@ -61,17 +60,30 @@ export default class Colliders extends System {
|
|||
continue;
|
||||
}
|
||||
delete other.Collider.collidingWith[entity.id];
|
||||
if (entity.Collider.isCollidingWith(other.Collider)) {
|
||||
const intersections = entity.Collider.isCollidingWith(other.Collider);
|
||||
if (intersections.length > 0) {
|
||||
entity.Collider.collidingWith[other.id] = true;
|
||||
other.Collider.collidingWith[entity.id] = true;
|
||||
if (!wasCollidingWith[other.id]) {
|
||||
if (entity.Collider.collisionStartScriptInstance) {
|
||||
entity.Collider.collisionStartScriptInstance.context.other = other;
|
||||
Ticking.addTickingPromise(entity.Collider.collisionStartScriptInstance.tickingPromise());
|
||||
const script = entity.Collider.collisionStartScriptInstance.clone();
|
||||
script.context.intersections = intersections;
|
||||
script.context.other = other;
|
||||
entity.Ticking.addTickingPromise(script.tickingPromise());
|
||||
}
|
||||
if (other.Collider.collisionStartScriptInstance) {
|
||||
other.Collider.collisionStartScriptInstance.context.other = entity;
|
||||
Ticking.addTickingPromise(other.Collider.collisionStartScriptInstance.tickingPromise());
|
||||
const script = other.Collider.collisionStartScriptInstance.clone();
|
||||
script.context.intersections = intersections
|
||||
.map(([l, r]) => [r, l]);
|
||||
script.context.other = entity;
|
||||
other.Ticking.addTickingPromise(script.tickingPromise());
|
||||
}
|
||||
}
|
||||
for (const [, {impassable}] of intersections) {
|
||||
if (impassable) {
|
||||
entity.Position.x = entity.Position.lastX
|
||||
entity.Position.y = entity.Position.lastY
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,13 +91,18 @@ export default class Colliders extends System {
|
|||
for (const otherId in wasCollidingWith) {
|
||||
if (!entity.Collider.collidingWith[otherId]) {
|
||||
const other = this.ecs.get(otherId);
|
||||
if (!other || !other.Collider) {
|
||||
continue;
|
||||
}
|
||||
if (entity.Collider.collisionEndScriptInstance) {
|
||||
entity.Collider.collisionEndScriptInstance.context.other = other;
|
||||
Ticking.addTickingPromise(entity.Collider.collisionEndScriptInstance.tickingPromise());
|
||||
const script = entity.Collider.collisionEndScriptInstance.clone();
|
||||
script.context.other = other;
|
||||
entity.Ticking.addTickingPromise(script.tickingPromise());
|
||||
}
|
||||
if (other.Collider.collisionEndScriptInstance) {
|
||||
other.Collider.collisionEndScriptInstance.context.other = entity;
|
||||
Ticking.addTickingPromise(other.Collider.collisionEndScriptInstance.tickingPromise());
|
||||
const script = other.Collider.collisionEndScriptInstance.clone();
|
||||
script.context.other = entity;
|
||||
other.Ticking.addTickingPromise(script.tickingPromise());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,24 +110,9 @@ export default class Colliders extends System {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of this.hash.within(query)) {
|
||||
within.add(this.ecs.get(id));
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ export default class Interactions extends System {
|
|||
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}) => {
|
||||
|
|
15
app/ecs-systems/pass-time.js
Normal file
15
app/ecs-systems/pass-time.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
|
||||
export default class PassTime extends System {
|
||||
|
||||
tick(elapsed) {
|
||||
const {Time} = this.ecs.get(1);
|
||||
if (Time) {
|
||||
Time.irlSeconds += elapsed;
|
||||
while (Time.irlSeconds >= Time.constructor.gameDayLengthInRealSeconds) {
|
||||
Time.irlSeconds -= Time.constructor.gameDayLengthInRealSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
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 {
|
||||
|
@ -33,6 +32,7 @@ export default class VisibleAabbs extends System {
|
|||
|
||||
updateHash(entity) {
|
||||
if (!entity.VisibleAabb) {
|
||||
this.hash.remove(entity.id);
|
||||
return;
|
||||
}
|
||||
this.hash.update(entity.VisibleAabb, entity.id);
|
||||
|
@ -63,24 +63,9 @@ export default class VisibleAabbs extends System {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of this.hash.within(query)) {
|
||||
within.add(this.ecs.get(id));
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ export default class Component {
|
|||
}),
|
||||
);
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
this.data[this.map[entities[i]]].destroy();
|
||||
this.map[entities[i]] = undefined;
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +141,7 @@ export default class Component {
|
|||
this[`$$${key}`] = defaultValue;
|
||||
}
|
||||
}
|
||||
destroy() {}
|
||||
toJSON() {
|
||||
return Component.constructor.filterDefaults(this);
|
||||
}
|
||||
|
|
|
@ -118,7 +118,9 @@ export default class System {
|
|||
}
|
||||
|
||||
tickDestruction() {
|
||||
this.deindex(this.destroying);
|
||||
if (this.destroying.length > 0) {
|
||||
this.deindex(this.destroying);
|
||||
}
|
||||
this.destroying = [];
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ export default class Engine {
|
|||
const engine = this;
|
||||
const server = this.server = new SilphiusServer();
|
||||
this.Ecs = class EngineEcs extends Ecs {
|
||||
get frame() {
|
||||
return engine.frame;
|
||||
}
|
||||
async readAsset(uri) {
|
||||
return server.readAsset(uri);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Container} from '@pixi/react';
|
||||
import {useState} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import {usePacket} from '@/context/client.js';
|
||||
|
@ -12,10 +12,55 @@ import TargetingGrid from './targeting-grid.jsx';
|
|||
import TileLayer from './tile-layer.jsx';
|
||||
import Water from './water.jsx';
|
||||
|
||||
const NIGHTNESS = 0.1;
|
||||
|
||||
function calculateDarkness(hour) {
|
||||
let darkness = 0;
|
||||
if (hour >= 21 || hour < 4) {
|
||||
darkness = 0.8;
|
||||
}
|
||||
if (hour >= 4 && hour < 7) {
|
||||
darkness = 0.8 * ((7 - hour) / 3);
|
||||
}
|
||||
if (hour >= 18 && hour < 21) {
|
||||
darkness = 0.8 * ((3 - (21 - hour)) / 3);
|
||||
}
|
||||
return Math.floor(darkness * 1000) / 1000;
|
||||
}
|
||||
|
||||
export default function Ecs({scale}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [hour, setHour] = useState(10);
|
||||
const [night, setNight] = useState();
|
||||
useEffect(() => {
|
||||
async function buildNightFilter() {
|
||||
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
||||
class NightFilter extends ColorMatrixFilter {
|
||||
setIntensity(intensity) {
|
||||
const double = NIGHTNESS * 2;
|
||||
const half = NIGHTNESS / 2;
|
||||
const redDown = 1 - (intensity * (1 + double));
|
||||
const blueUp = 1 - (intensity * (1 - half));
|
||||
const scale = intensity * NIGHTNESS;
|
||||
this.uniforms.m = [
|
||||
redDown, -scale, 0, 0, 0,
|
||||
-scale, (1 - intensity), scale, 0, 0,
|
||||
0, scale, blueUp, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
setNight(new NightFilter());
|
||||
}
|
||||
buildNightFilter();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (night) {
|
||||
night.setIntensity(calculateDarkness(hour));
|
||||
}
|
||||
}, [hour, night]);
|
||||
usePacket('EcsChange', async () => {
|
||||
setEntities({});
|
||||
}, [setEntities]);
|
||||
|
@ -30,6 +75,11 @@ export default function Ecs({scale}) {
|
|||
delete updatedEntities[id];
|
||||
}
|
||||
else {
|
||||
if ('1' === id) {
|
||||
if (update.Time) {
|
||||
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60);
|
||||
}
|
||||
}
|
||||
updatedEntities[id] = ecs.get(id);
|
||||
if (update.Emitter?.emit) {
|
||||
updatedEntities[id].Emitter.emitting = {
|
||||
|
@ -52,12 +102,17 @@ export default function Ecs({scale}) {
|
|||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction)
|
||||
const {Camera} = entity;
|
||||
const {TileLayers: {layers: [layer]}, Water: WaterEcs} = ecs.get(1);
|
||||
const filters = [];
|
||||
if (night) {
|
||||
filters.push(night);
|
||||
}
|
||||
const [cx, cy] = [
|
||||
Math.round((Camera.x * scale) - RESOLUTION.x / 2),
|
||||
Math.round((Camera.y * scale) - RESOLUTION.y / 2),
|
||||
];
|
||||
return (
|
||||
<Container
|
||||
filters={filters}
|
||||
scale={scale}
|
||||
x={-cx}
|
||||
y={-cy}
|
||||
|
|
|
@ -7,18 +7,18 @@ import {useMainEntity} from '@/context/main-entity.js';
|
|||
import Emitter from './emitter.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
|
||||
function Aabb({color, x0, y0, x1, y1}) {
|
||||
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
||||
const draw = useCallback((g) => {
|
||||
g.clear();
|
||||
g.lineStyle(0.5, color);
|
||||
g.lineStyle(width, color);
|
||||
g.moveTo(x0, y0);
|
||||
g.lineTo(x1, y0);
|
||||
g.lineTo(x1, y1);
|
||||
g.lineTo(x0, y1);
|
||||
g.lineTo(x1 + 1, y0);
|
||||
g.lineTo(x1 + 1, y1 + 1);
|
||||
g.lineTo(x0, y1 + 1);
|
||||
g.lineTo(x0, y0);
|
||||
}, [color, x0, x1, y0, y1]);
|
||||
}, [color, width, x0, x1, y0, y1]);
|
||||
return (
|
||||
<Graphics draw={draw} x={0.5} y = {0.5} />
|
||||
<Graphics draw={draw} x={0.5} y={0.5} {...rest} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,9 @@ function Entity({entity, ...rest}) {
|
|||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
if (debug) {
|
||||
entity.Collider?.recalculateAabbs();
|
||||
}
|
||||
return (
|
||||
<Container
|
||||
zIndex={entity.Position?.y || 0}
|
||||
|
@ -72,20 +75,25 @@ function Entity({entity, ...rest}) {
|
|||
{debug && (
|
||||
<Aabb
|
||||
color={0xff00ff}
|
||||
x0={entity.VisibleAabb.x0}
|
||||
x1={entity.VisibleAabb.x1}
|
||||
y0={entity.VisibleAabb.y0}
|
||||
y1={entity.VisibleAabb.y1}
|
||||
{...entity.VisibleAabb}
|
||||
/>
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
<>
|
||||
<Aabb
|
||||
color={0xffffff}
|
||||
width={0.5}
|
||||
{...entity.Collider.aabb}
|
||||
/>
|
||||
{entity.Collider.aabbs.map((aabb, i) => (
|
||||
<Aabb
|
||||
color={0xffff00}
|
||||
width={0.25}
|
||||
key={i}
|
||||
{...aabb}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{debug && mainEntity == entity.id && (
|
||||
<Aabb
|
||||
|
|
32
app/util/delta.js
Normal file
32
app/util/delta.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import TickingPromise from '@/util/ticking-promise.js';
|
||||
|
||||
export default function delta(object, properties) {
|
||||
const deltas = {};
|
||||
for (const key in properties) {
|
||||
const property = properties[key];
|
||||
const delta = {
|
||||
duration: Infinity,
|
||||
elapsed: 0,
|
||||
...property,
|
||||
};
|
||||
deltas[key] = delta;
|
||||
}
|
||||
let stop;
|
||||
const promise = new TickingPromise(
|
||||
(resolve) => {
|
||||
stop = resolve;
|
||||
},
|
||||
(elapsed, resolve) => {
|
||||
for (const key in deltas) {
|
||||
deltas[key].elapsed += elapsed;
|
||||
if (deltas[key].elapsed >= deltas[key].duration) {
|
||||
object[key] += deltas[key].delta * (deltas[key].duration - deltas[key].elapsed);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
object[key] += deltas[key].delta * elapsed;
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, deltas, promise};
|
||||
}
|
74
app/util/lfo.js
Normal file
74
app/util/lfo.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
import TickingPromise from '@/util/ticking-promise.js';
|
||||
|
||||
const Modulators = {
|
||||
flat: () => 0.5,
|
||||
random: () => Math.random(),
|
||||
sawtooth: (unit) => unit,
|
||||
sine: (unit) => 0.5 * (1 + Math.sin(unit * Math.PI * 2)),
|
||||
square: (unit) => (unit < 0.5 ? 0 : 1),
|
||||
triangle: (unit) => 2 * Math.abs(((unit + 0.75) % 1) - 0.5),
|
||||
};
|
||||
|
||||
export default function lfo(object, properties) {
|
||||
const oscillators = {};
|
||||
const promises = [];
|
||||
for (const key in properties) {
|
||||
const property = properties[key];
|
||||
const oscillator = {
|
||||
count: Infinity,
|
||||
elapsed: 0,
|
||||
offset: 0,
|
||||
...property,
|
||||
};
|
||||
if (!('median' in oscillator)) {
|
||||
oscillator.median = oscillator.magnitude / 2;
|
||||
}
|
||||
if (!oscillator.modulators) {
|
||||
oscillator.modulators = [Modulators.triangle];
|
||||
}
|
||||
else {
|
||||
const {modulators} = oscillator;
|
||||
for (const i in modulators) {
|
||||
if ('string' === typeof modulators[i]) {
|
||||
modulators[i] = Modulators[modulators[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
oscillator.low = oscillator.median - oscillator.magnitude / 2;
|
||||
oscillator.promise = new Promise((resolve) => {
|
||||
oscillator.stop = resolve;
|
||||
});
|
||||
oscillator.promise.then(() => {
|
||||
delete oscillators[key];
|
||||
});
|
||||
promises.push(oscillator.promise);
|
||||
oscillators[key] = oscillator;
|
||||
}
|
||||
let stop;
|
||||
const promise = new TickingPromise(
|
||||
(resolve) => {
|
||||
stop = resolve;
|
||||
Promise.all(promises).then(resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
for (const key in oscillators) {
|
||||
const oscillator = oscillators[key];
|
||||
oscillator.elapsed += elapsed;
|
||||
if (oscillator.elapsed >= oscillator.frequency) {
|
||||
if (0 === --oscillator.count) {
|
||||
oscillator.stop();
|
||||
return;
|
||||
}
|
||||
oscillator.elapsed = oscillator.elapsed % oscillator.frequency;
|
||||
}
|
||||
const x = (oscillator.offset + (oscillator.elapsed / oscillator.frequency)) % 1;
|
||||
let y = 0;
|
||||
for (const modulator of oscillator.modulators) {
|
||||
y += modulator(x);
|
||||
}
|
||||
object[key] = oscillator.low + oscillator.magnitude * (y / oscillator.modulators.length);
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, oscillators, promise};
|
||||
}
|
|
@ -1,3 +1,51 @@
|
|||
export const {
|
||||
abs,
|
||||
acos,
|
||||
acosh,
|
||||
asin,
|
||||
asinh,
|
||||
atan,
|
||||
atanh,
|
||||
atan2,
|
||||
ceil,
|
||||
cbrt,
|
||||
expm1,
|
||||
clz32,
|
||||
cos,
|
||||
cosh,
|
||||
exp,
|
||||
floor,
|
||||
fround,
|
||||
hypot,
|
||||
imul,
|
||||
log,
|
||||
log1p,
|
||||
log2,
|
||||
log10,
|
||||
max,
|
||||
min,
|
||||
round,
|
||||
sign,
|
||||
sin,
|
||||
sinh,
|
||||
sqrt,
|
||||
tan,
|
||||
tanh,
|
||||
trunc,
|
||||
E,
|
||||
LN10,
|
||||
LN2,
|
||||
LOG10E,
|
||||
LOG2E,
|
||||
PI,
|
||||
SQRT1_2,
|
||||
SQRT2,
|
||||
} = Math;
|
||||
|
||||
export function clamp(n, min, max) {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
export function distance({x: lx, y: ly}, {x: rx, y: ry}) {
|
||||
const xd = lx - rx;
|
||||
const yd = ly - ry;
|
||||
|
@ -19,3 +67,7 @@ export function normalizeVector({x, y}) {
|
|||
const k = 1 / Math.sqrt(x * x + y * y);
|
||||
return {x: x * k, y: y * k};
|
||||
}
|
||||
|
||||
export function random() {
|
||||
return Math.random();
|
||||
}
|
||||
|
|
|
@ -3,6 +3,10 @@ import {LRUCache} from 'lru-cache';
|
|||
|
||||
import Sandbox from '@/astride/sandbox.js';
|
||||
import TickingPromise from '@/util/ticking-promise.js';
|
||||
import delta from '@/util/delta.js';
|
||||
import lfo from '@/util/lfo.js';
|
||||
import * as MathUtil from '@/util/math.js';
|
||||
import transition from '@/util/transition.js';
|
||||
|
||||
function parse(code, options = {}) {
|
||||
return acornParse(code, {
|
||||
|
@ -25,16 +29,23 @@ export default class Script {
|
|||
this.promise = null;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new this.constructor(this.sandbox.clone());
|
||||
}
|
||||
|
||||
get context() {
|
||||
return this.sandbox.context;
|
||||
}
|
||||
|
||||
static contextDefaults() {
|
||||
return {
|
||||
console,
|
||||
Array,
|
||||
Math,
|
||||
console,
|
||||
delta,
|
||||
lfo,
|
||||
Math: MathUtil,
|
||||
Promise,
|
||||
transition,
|
||||
wait: (seconds) => (
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, seconds * 1000);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {clamp, intersects} from '@/util/math.js';
|
||||
|
||||
export default class SpatialHash {
|
||||
|
||||
constructor({x, y}) {
|
||||
|
@ -8,43 +10,74 @@ export default class SpatialHash {
|
|||
.map(() => (
|
||||
Array(Math.ceil(this.area.y / this.chunkSize.y))
|
||||
.fill(0)
|
||||
.map(() => [])
|
||||
.map(() => new 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))
|
||||
];
|
||||
this.data = new Map();
|
||||
}
|
||||
|
||||
chunkIndex(x, y) {
|
||||
const [cx, cy] = this.clamp(x, y);
|
||||
return [
|
||||
Math.floor(cx / this.chunkSize.x),
|
||||
Math.floor(cy / this.chunkSize.y),
|
||||
];
|
||||
return {
|
||||
x: clamp(Math.floor(x / this.chunkSize.x), 0, this.chunks.length - 1),
|
||||
y: clamp(Math.floor(y / this.chunkSize.y), 0, this.chunks[0].length - 1),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (!this.data.has(datum)) {
|
||||
return;
|
||||
}
|
||||
this.data[datum] = [];
|
||||
for (const {x, y} of this.data.get(datum).chunks) {
|
||||
this.chunks[x][y].delete(datum);
|
||||
}
|
||||
this.data.delete(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);
|
||||
const [sx0, sx1] = x0 < x1 ? [x0, x1] : [x1, x0];
|
||||
const [sy0, sy1] = y0 < y1 ? [y0, y1] : [y1, y0];
|
||||
const {x: cx0, y: cy0} = this.chunkIndex(sx0, sy0);
|
||||
const {x: cx1, y: cy1} = this.chunkIndex(sx1, sy1);
|
||||
const chunks = [];
|
||||
for (let iy = cy0; iy <= cy1; ++iy) {
|
||||
for (let ix = cx0; ix <= cx1; ++ix) {
|
||||
const chunk = this.chunks[ix][iy];
|
||||
if (!chunk.has(datum)) {
|
||||
chunk.set(datum, true);
|
||||
}
|
||||
chunks.push({x: ix, y: iy});
|
||||
}
|
||||
}
|
||||
this.data.set(
|
||||
datum,
|
||||
{
|
||||
bounds: {x0: sx0, x1: sx1, y0: sy0, y1: sy1},
|
||||
chunks,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
within(query) {
|
||||
const {x0, x1, y0, y1} = query;
|
||||
const [sx0, sx1] = x0 < x1 ? [x0, x1] : [x1, x0];
|
||||
const [sy0, sy1] = y0 < y1 ? [y0, y1] : [y1, y0];
|
||||
const {x: cx0, y: cy0} = this.chunkIndex(sx0, sy0);
|
||||
const {x: cx1, y: cy1} = this.chunkIndex(sx1, sy1);
|
||||
const candidates = new Set();
|
||||
const within = new Set();
|
||||
for (let cy = cy0; cy <= cy1; ++cy) {
|
||||
for (let cx = cx0; cx <= cx1; ++cx) {
|
||||
for (const [datum] of this.chunks[cx][cy]) {
|
||||
candidates.add(datum);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const datum of candidates) {
|
||||
if (intersects(this.data.get(datum).bounds, query)) {
|
||||
within.add(datum);
|
||||
}
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
125
app/util/spatial-hash.test.js
Normal file
125
app/util/spatial-hash.test.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import SpatialHash from './spatial-hash.js';
|
||||
|
||||
test('creates chunks', async () => {
|
||||
const hash = new SpatialHash({x: 128, y: 128});
|
||||
expect(hash.chunks.length)
|
||||
.to.equal(2);
|
||||
expect(hash.chunks[0].length)
|
||||
.to.equal(2);
|
||||
expect(hash.chunks[1].length)
|
||||
.to.equal(2);
|
||||
});
|
||||
|
||||
test('clamps to actual chunks', async () => {
|
||||
const hash = new SpatialHash({x: 640, y: 640});
|
||||
expect(hash.chunkIndex(0, 0))
|
||||
.to.deep.equal({x: 0, y: 0});
|
||||
expect(hash.chunkIndex(320, 320))
|
||||
.to.deep.equal({x: 5, y: 5});
|
||||
expect(hash.chunkIndex(1280, 1280))
|
||||
.to.deep.equal({x: 9, y: 9});
|
||||
});
|
||||
|
||||
test('updates with data', async () => {
|
||||
const hash = new SpatialHash({x: 640, y: 640});
|
||||
hash.update({x0: 32, x1: 96, y0: 32, y1: 96}, 'foobar');
|
||||
expect(hash.data.get('foobar'))
|
||||
.to.deep.equal({
|
||||
bounds: {x0: 32, x1: 96, y0: 32, y1: 96},
|
||||
chunks: [
|
||||
{x: 0, y: 0},
|
||||
{x: 1, y: 0},
|
||||
{x: 0, y: 1},
|
||||
{x: 1, y: 1},
|
||||
],
|
||||
});
|
||||
expect(Array.from(hash.chunks[0][0]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[1][0]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[1][1]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[0][1]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
hash.update({x0: 48, x1: 32, y0: 32, y1: 96}, 'foobar');
|
||||
expect(hash.data.get('foobar'))
|
||||
.to.deep.equal({
|
||||
bounds: {x0: 32, x1: 48, y0: 32, y1: 96},
|
||||
chunks: [
|
||||
{x: 0, y: 0},
|
||||
{x: 0, y: 1},
|
||||
],
|
||||
});
|
||||
expect(Array.from(hash.chunks[0][0]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[1][0]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[1][1]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[0][1]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
hash.update({x0: 32, x1: 160, y0: 32, y1: 160}, 'foobar');
|
||||
expect(hash.data.get('foobar'))
|
||||
.to.deep.equal({
|
||||
bounds: {x0: 32, x1: 160, y0: 32, y1: 160},
|
||||
chunks: [
|
||||
{x: 0, y: 0},
|
||||
{x: 1, y: 0},
|
||||
{x: 2, y: 0},
|
||||
{x: 0, y: 1},
|
||||
{x: 1, y: 1},
|
||||
{x: 2, y: 1},
|
||||
{x: 0, y: 2},
|
||||
{x: 1, y: 2},
|
||||
{x: 2, y: 2},
|
||||
],
|
||||
});
|
||||
expect(Array.from(hash.chunks[0][0]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[1][0]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[2][0]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[3][0]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[0][1]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[1][1]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[2][1]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[3][1]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[0][2]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[1][2]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[2][2]))
|
||||
.to.deep.equal([['foobar', true]]);
|
||||
expect(Array.from(hash.chunks[3][2]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[3][3]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[3][3]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[3][3]))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.chunks[3][3]))
|
||||
.to.deep.equal([]);
|
||||
});
|
||||
|
||||
test('queries for data', async () => {
|
||||
const hash = new SpatialHash({x: 640, y: 640});
|
||||
hash.update({x0: 32, x1: 96, y0: 32, y1: 96}, 'foobar');
|
||||
expect(Array.from(hash.within({x0: 0, x1: 16, y0: 0, y1: 16})))
|
||||
.to.deep.equal([]);
|
||||
expect(Array.from(hash.within({x0: 0, x1: 48, y0: 0, y1: 48})))
|
||||
.to.deep.equal(['foobar']);
|
||||
expect(Array.from(hash.within({x0: 48, x1: 64, y0: 48, y1: 64})))
|
||||
.to.deep.equal(['foobar']);
|
||||
hash.update({x0: 32, x1: 160, y0: 32, y1: 160}, 'foobar');
|
||||
expect(Array.from(hash.within({x0: 80, x1: 90, y0: 80, y1: 90})))
|
||||
.to.deep.equal(['foobar']);
|
||||
});
|
178
app/util/transition.js
Normal file
178
app/util/transition.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
import TickingPromise from '@/util/ticking-promise.js';
|
||||
|
||||
/* eslint-disable */
|
||||
const Easing = {
|
||||
linear: function (t, b, c, d) {
|
||||
return b + c * t/d
|
||||
},
|
||||
easeInQuad: function (t, b, c, d) {
|
||||
return c*(t/=d)*t + b;
|
||||
},
|
||||
easeOutQuad: function (t, b, c, d) {
|
||||
return -c *(t/=d)*(t-2) + b;
|
||||
},
|
||||
easeInOutQuad: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t + b;
|
||||
return -c/2 * ((--t)*(t-2) - 1) + b;
|
||||
},
|
||||
easeInCubic: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t + b;
|
||||
},
|
||||
easeOutCubic: function (t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t + 1) + b;
|
||||
},
|
||||
easeInOutCubic: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t + 2) + b;
|
||||
},
|
||||
easeInQuart: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t + b;
|
||||
},
|
||||
easeOutQuart: function (t, b, c, d) {
|
||||
return -c * ((t=t/d-1)*t*t*t - 1) + b;
|
||||
},
|
||||
easeInOutQuart: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
|
||||
return -c/2 * ((t-=2)*t*t*t - 2) + b;
|
||||
},
|
||||
easeInQuint: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t*t + b;
|
||||
},
|
||||
easeOutQuint: function (t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t*t*t + 1) + b;
|
||||
},
|
||||
easeInOutQuint: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t*t*t + 2) + b;
|
||||
},
|
||||
easeInSine: function (t, b, c, d) {
|
||||
return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
|
||||
},
|
||||
easeOutSine: function (t, b, c, d) {
|
||||
return c * Math.sin(t/d * (Math.PI/2)) + b;
|
||||
},
|
||||
easeInOutSine: function (t, b, c, d) {
|
||||
return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
|
||||
},
|
||||
easeInExpo: function (t, b, c, d) {
|
||||
return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
|
||||
},
|
||||
easeOutExpo: function (t, b, c, d) {
|
||||
return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
|
||||
},
|
||||
easeInOutExpo: function (t, b, c, d) {
|
||||
if (t==0) return b;
|
||||
if (t==d) return b+c;
|
||||
if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
|
||||
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
|
||||
},
|
||||
easeInCirc: function (t, b, c, d) {
|
||||
return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
|
||||
},
|
||||
easeOutCirc: function (t, b, c, d) {
|
||||
return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
|
||||
},
|
||||
easeInOutCirc: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
|
||||
return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
|
||||
},
|
||||
easeInElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
},
|
||||
easeOutElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
|
||||
},
|
||||
easeInOutElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5);
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
|
||||
},
|
||||
easeInBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*(t/=d)*t*((s+1)*t - s) + b;
|
||||
},
|
||||
easeOutBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
|
||||
},
|
||||
easeInOutBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
|
||||
return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
|
||||
},
|
||||
easeInBounce: function (t, b, c, d) {
|
||||
return c - easing.easeOutBounce (d-t, 0, c, d) + b;
|
||||
},
|
||||
easeOutBounce: function (t, b, c, d) {
|
||||
if ((t/=d) < (1/2.75)) {
|
||||
return c*(7.5625*t*t) + b;
|
||||
} else if (t < (2/2.75)) {
|
||||
return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
|
||||
} else if (t < (2.5/2.75)) {
|
||||
return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
|
||||
} else {
|
||||
return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
|
||||
}
|
||||
},
|
||||
easeInOutBounce: function (t, b, c, d) {
|
||||
if (t < d/2) return easing.easeInBounce (t*2, 0, c, d) * .5 + b;
|
||||
return easing.easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b;
|
||||
}
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
export default function transition(object, properties) {
|
||||
const transitions = {};
|
||||
for (const key in properties) {
|
||||
const property = properties[key];
|
||||
const transition = {
|
||||
elapsed: 0,
|
||||
start: object[key],
|
||||
...property,
|
||||
};
|
||||
if (!transition.easing) {
|
||||
transition.easing = Easing.easeOutQuad;
|
||||
}
|
||||
else {
|
||||
if ('string' === typeof transition.easing) {
|
||||
transition.easing = Easing[transition.easing];
|
||||
}
|
||||
}
|
||||
transitions[key] = transition;
|
||||
}
|
||||
let stop;
|
||||
const promise = new TickingPromise(
|
||||
(resolve) => {
|
||||
stop = resolve;
|
||||
},
|
||||
(elapsed, resolve) => {
|
||||
for (const key in transitions) {
|
||||
const transition = transitions[key];
|
||||
transition.elapsed += elapsed;
|
||||
if (transition.elapsed >= transition.duration) {
|
||||
object[key] = transition.start + transition.magnitude;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
object[key] = transition.easing(
|
||||
transition.elapsed,
|
||||
transition.start,
|
||||
transition.magnitude,
|
||||
transition.duration,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, transitions, promise};
|
||||
}
|
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
|||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
"@pixi/filter-glow": "^5.2.1",
|
||||
"@pixi/particle-emitter": "^5.0.8",
|
||||
"@pixi/react": "^7.1.2",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
"@pixi/filter-glow": "^5.2.1",
|
||||
"@pixi/particle-emitter": "^5.0.8",
|
||||
"@pixi/react": "^7.1.2",
|
||||
|
|
|
@ -4,7 +4,7 @@ ecs.switchEcs(
|
|||
{
|
||||
Position: {
|
||||
x: 74,
|
||||
y: 108,
|
||||
y: 128,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
ecs.switchEcs(
|
||||
other,
|
||||
entity.Ecs.path,
|
||||
{
|
||||
Position: {
|
||||
x: 72,
|
||||
y: 304,
|
||||
},
|
||||
},
|
||||
);
|
||||
for (let i = 0; i < intersections.length; ++i) {
|
||||
if (intersections[i][0].tags) {
|
||||
if (intersections[i][0].tags.includes('door')) {
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
entity.Ecs.path,
|
||||
{
|
||||
Position: {
|
||||
x: 72,
|
||||
y: 304,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,119 @@
|
|||
const {Interactive, Plant, Sprite} = subject;
|
||||
const {Interactive, Position, 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',
|
||||
})
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
|
||||
const tomato = ecs.get(await ecs.create({
|
||||
Collider: {
|
||||
bodies: [
|
||||
{
|
||||
points: [
|
||||
{x: -4, y: -4},
|
||||
{x: 3, y: -4},
|
||||
{x: 3, y: 3},
|
||||
{x: -4, y: 3},
|
||||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: '/assets/tomato/collision-start.js',
|
||||
},
|
||||
Forces: {},
|
||||
Magnetic: {},
|
||||
Position: {x: Position.x, y: Position.y},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.5,
|
||||
scaleX: 0.333,
|
||||
scaleY: 0.333,
|
||||
source: '/assets/tomato/tomato-sprite.json',
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
}));
|
||||
|
||||
const {x, y} = Math.normalizeVector({
|
||||
x: (Math.random() * 2) - 1,
|
||||
y: (Math.random() * 2) - 1,
|
||||
});
|
||||
|
||||
const d = delta(
|
||||
tomato.Position,
|
||||
{
|
||||
y: {
|
||||
duration: 0.5,
|
||||
delta: 0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const l = lfo(
|
||||
d.deltas.y,
|
||||
{
|
||||
delta: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 64,
|
||||
median: 0,
|
||||
offset: -0.5,
|
||||
},
|
||||
},
|
||||
)
|
||||
const ls = lfo(
|
||||
tomato.Sprite,
|
||||
{
|
||||
scaleX: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 0.333,
|
||||
median: 0.333,
|
||||
elapsed: 0.25,
|
||||
offset: -0.5,
|
||||
},
|
||||
scaleY: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 0.333,
|
||||
median: 0.333,
|
||||
elapsed: 0.25,
|
||||
offset: -0.5,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
tomato.Ticking.addTickingPromise(
|
||||
d.promise,
|
||||
);
|
||||
tomato.Ticking.addTickingPromise(
|
||||
l.promise,
|
||||
);
|
||||
tomato.Ticking.addTickingPromise(
|
||||
ls.promise,
|
||||
);
|
||||
|
||||
tomato.Ticking.addTickingPromise(
|
||||
delta(
|
||||
tomato.Position,
|
||||
{
|
||||
x: {
|
||||
duration: 0.5,
|
||||
delta: (12 * x) + Math.random() * 8,
|
||||
},
|
||||
},
|
||||
).promise,
|
||||
);
|
||||
tomato.Ticking.addTickingPromise(
|
||||
delta(
|
||||
tomato.Position,
|
||||
{
|
||||
y: {
|
||||
duration: 0.5,
|
||||
delta: (12 * y) + Math.random() * 8,
|
||||
},
|
||||
},
|
||||
).promise,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,14 @@ if (projected?.length > 0) {
|
|||
const plant = {
|
||||
Collider: {
|
||||
bodies: [
|
||||
[
|
||||
{x: -8, y: -8},
|
||||
{x: 7, y: -8},
|
||||
{x: -8, y: 7},
|
||||
{x: 7, y: 7},
|
||||
],
|
||||
{
|
||||
points: [
|
||||
{x: -8, y: -8},
|
||||
{x: 7, y: -8},
|
||||
{x: -8, y: 7},
|
||||
{x: 7, y: 7},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Interactive: {
|
||||
|
@ -28,7 +30,8 @@ if (projected?.length > 0) {
|
|||
stages: Array(5).fill(5),
|
||||
},
|
||||
Sprite: {
|
||||
anchor: {x: 0.5, y: 0.75},
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.75,
|
||||
animation: 'stage/0',
|
||||
frame: 0,
|
||||
frames: 1,
|
||||
|
|
7
public/assets/tomato/collision-start.js
Normal file
7
public/assets/tomato/collision-start.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
if (other.Inventory) {
|
||||
other.Inventory.give({
|
||||
qty: 1,
|
||||
source: '/assets/tomato/tomato.json',
|
||||
})
|
||||
ecs.destroy(entity.id)
|
||||
}
|
1
public/assets/tomato/tomato-sprite.json
Normal file
1
public/assets/tomato/tomato-sprite.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":24,"h":24},"spriteSourceSize":{"x":0,"y":0,"w":24,"h":24},"sourceSize":{"w":24,"h":24}}},"meta":{"format":"RGBA8888","image":"./tomato.png","scale":1,"size":{"w":24,"h":24}}}
|
Loading…
Reference in New Issue
Block a user