Compare commits

...

23 Commits

Author SHA1 Message Date
cha0s
6ff824ace6 chore: tidy 2024-07-04 15:17:49 -05:00
cha0s
1a8cefd325 feat: day/night 2024-07-04 15:17:33 -05:00
cha0s
f1d1422e7a refactor: better magnetism 2024-07-04 12:58:33 -05:00
cha0s
fcefe1a620 fix: ensure clean script context 2024-07-04 09:24:49 -05:00
cha0s
271b944796 fun: tomato harvest 2024-07-04 09:10:49 -05:00
cha0s
36be786348 perf: avoid work 2024-07-04 09:10:37 -05:00
cha0s
be7ec5c243 refactor: ticker ownership 2024-07-04 09:10:19 -05:00
cha0s
82c1358ecc fix: add delta 2024-07-04 09:09:37 -05:00
cha0s
447774f0da fix: properly handle multiple promises 2024-07-04 09:09:26 -05:00
cha0s
e02a63e7b5 fix: median 0 support 2024-07-04 09:09:06 -05:00
cha0s
ec3cef8ee7 refactor: script globals 2024-07-04 09:08:47 -05:00
cha0s
48ef8a6cbd fix: transition perfect end 2024-07-04 09:08:20 -05:00
cha0s
7bdd598915 chore: clean up 2024-07-04 09:06:50 -05:00
cha0s
dad9b1d3e7 feat: frame 2024-07-04 09:06:41 -05:00
cha0s
875449e816 chore: cleanup 2024-07-04 09:05:38 -05:00
cha0s
3de969ce1e feat: mutation 2024-07-03 21:58:24 -05:00
cha0s
d412a08810 chore: tidy 2024-07-03 21:58:03 -05:00
cha0s
5b42654892 fix: nop -> undefined 2024-07-03 21:57:52 -05:00
cha0s
a553ef99c6 feat: player 2024-07-03 21:57:35 -05:00
cha0s
4a7006a48d refactor: scale and anchor 2024-07-03 21:56:55 -05:00
cha0s
64cb88ae2f feat: magnetism 2024-07-03 19:05:40 -05:00
cha0s
4eb1cf5772 fix: test 2024-07-03 18:16:28 -05:00
cha0s
b96566d0a0 refactor: much improved collision 2024-07-03 16:13:14 -05:00
42 changed files with 1041 additions and 165 deletions

View File

@ -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]) {

View File

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

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import Component from '@/ecs/component.js';
export default class Magnet extends Component {
static properties = {
strength: {type: 'uint8'},
};
}

View File

@ -0,0 +1,4 @@
import Component from '@/ecs/component.js';
export default class Magnetic extends Component {
}

View File

@ -0,0 +1,3 @@
import Component from '@/ecs/component.js';
export default class Player extends Component {}

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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) {

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

View File

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

View File

@ -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}) => {

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

View File

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

View File

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

View File

@ -118,7 +118,9 @@ export default class System {
}
tickDestruction() {
this.deindex(this.destroying);
if (this.destroying.length > 0) {
this.deindex(this.destroying);
}
this.destroying = [];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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
View File

@ -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",

View File

@ -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",

View File

@ -4,7 +4,7 @@ ecs.switchEcs(
{
Position: {
x: 74,
y: 108,
y: 128,
},
},
);

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
if (other.Inventory) {
other.Inventory.give({
qty: 1,
source: '/assets/tomato/tomato.json',
})
ecs.destroy(entity.id)
}

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