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.");