133 lines
5.2 KiB
TypeScript
133 lines
5.2 KiB
TypeScript
import {
|
|
World,
|
|
defineComponent,
|
|
query,
|
|
QueryUpdate,
|
|
WorldEvent,
|
|
} from "../src/index";
|
|
|
|
// ── Define components ─────────────────────────────────
|
|
const Position = defineComponent({ x: 0, y: 0 });
|
|
const Velocity = defineComponent({ vx: 0, vy: 0 });
|
|
const Health = defineComponent({ current: 100, max: 100 });
|
|
const Dead = defineComponent({ timestamp: 0 });
|
|
|
|
// Type inference check
|
|
const _p: { x: number; y: number } = Position.defaults;
|
|
|
|
// ── World setup ──────────────────────────────────────
|
|
const world = new World();
|
|
|
|
let events: WorldEvent[] = [];
|
|
world.events$.subscribe((e) => events.push(e));
|
|
|
|
// ── Entity lifecycle ─────────────────────────────────
|
|
const player = world.spawn();
|
|
const enemy = world.spawn();
|
|
|
|
console.assert(events.length === 2, "spawn events");
|
|
console.assert(events[0].type === "spawned" && events[0].entity === player);
|
|
console.assert(events[1].type === "spawned" && events[1].entity === enemy);
|
|
|
|
console.assert(world.isAlive(player), "player alive");
|
|
console.assert(world.isAlive(enemy), "enemy alive");
|
|
console.assert(world.entityCount === 2, "two entities");
|
|
|
|
// ── Add components ───────────────────────────────────
|
|
const pos = world.add(player, Position, { x: 10, y: 20 });
|
|
world.add(player, Velocity, { vx: 1, vy: 0 });
|
|
world.add(enemy, Position, { x: 50, y: 0 });
|
|
world.add(enemy, Health, { current: 50 });
|
|
|
|
console.assert(pos.x === 10 && pos.y === 20, "add with init");
|
|
console.assert(world.has(player, Position), "has Position");
|
|
console.assert(world.has(player, Velocity), "has Velocity");
|
|
console.assert(!world.has(player, Health), "no Health");
|
|
console.assert(events.length === 6, "component add events");
|
|
|
|
// ── Sync query ───────────────────────────────────────
|
|
const movable = [...world.query(query(Position, Velocity))];
|
|
console.assert(movable.length === 1, "player only in movable");
|
|
console.assert(movable[0] === player);
|
|
|
|
const allPos = [...world.query(query(Position))];
|
|
console.assert(allPos.length === 2, "both have Position");
|
|
|
|
// ── Observable query ─────────────────────────────────
|
|
const queryLog: QueryUpdate[] = [];
|
|
world.observe(query(Position, Velocity)).subscribe((u) => {
|
|
if (u.added.length || u.removed.length || u.changed.length) {
|
|
queryLog.push(u);
|
|
}
|
|
});
|
|
|
|
// ── Mutation + change tracking ───────────────────────
|
|
world.get(player, Position).x += 5;
|
|
world.markDirty(player, Position);
|
|
world.get(player, Velocity).vx *= 2;
|
|
world.markDirty(player, Velocity);
|
|
|
|
// flush should emit componentChanged events and update queries
|
|
world.flush();
|
|
|
|
console.assert(
|
|
events.some((e) => e.type === "componentChanged"),
|
|
"change events",
|
|
);
|
|
|
|
// The query observer should have received changed: [player]
|
|
const lastUpdate = queryLog[queryLog.length - 1];
|
|
console.assert(
|
|
lastUpdate.changed.length === 1 && lastUpdate.changed[0] === player,
|
|
"player in changed",
|
|
);
|
|
|
|
// ── Remove component ─────────────────────────────────
|
|
world.remove(player, Velocity);
|
|
|
|
console.assert(!world.has(player, Velocity), "Velocity removed");
|
|
const movableAfter = [...world.query(query(Position, Velocity))];
|
|
console.assert(movableAfter.length === 0, "no one movable after remove");
|
|
|
|
// The observer should have emitted {removed: [player]}
|
|
const remUpdate = queryLog[queryLog.length - 1];
|
|
console.assert(
|
|
remUpdate.removed.length === 1 && remUpdate.removed[0] === player,
|
|
"player removed from query",
|
|
);
|
|
|
|
// ── Destroy ──────────────────────────────────────────
|
|
world.destroy(enemy);
|
|
console.assert(!world.isAlive(enemy), "enemy destroyed");
|
|
console.assert(world.entityCount === 1, "one entity left");
|
|
|
|
// ── componentChanged query update ────────────────────
|
|
// Add enemy back, observe query(Health).without(Dead)
|
|
const enemy2 = world.spawn();
|
|
world.add(enemy2, Health, { current: 75 });
|
|
|
|
const healthLog: QueryUpdate[] = [];
|
|
world.observe(query(Health).without(Dead)).subscribe((u) => {
|
|
healthLog.push(u);
|
|
});
|
|
|
|
// Enemy won't be in the initial seed yet (subscribe happened after spawn)
|
|
// Let's add Dead to trigger the removal
|
|
world.add(enemy2, Dead, { timestamp: 123 });
|
|
world.flush();
|
|
|
|
console.assert(
|
|
healthLog.some((u) => u.removed[0] === enemy2),
|
|
"enemy removed from health-not-dead query after gaining Dead",
|
|
);
|
|
|
|
// ── Entity recycling ─────────────────────────────────
|
|
world.destroy(player);
|
|
const recycled = world.spawn();
|
|
|
|
console.assert(recycled !== player, "recycled entity has new generation");
|
|
console.assert(world.isAlive(recycled), "recycled entity is alive");
|
|
console.assert(!world.isAlive(player), "old handle is dead");
|
|
|
|
console.log("✅ All smoke tests passed.");
|