feat: Add traps

This commit is contained in:
Peter Stockings
2026-01-25 16:37:46 +11:00
parent 18d4f0cdd4
commit 9552364a60
14 changed files with 2225 additions and 11 deletions

View File

@@ -0,0 +1,288 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { System, SystemRegistry } from "../System";
import { ECSWorld } from "../World";
import { EventBus } from "../EventBus";
import { type EntityId } from "../../../core/types";
import { type ComponentType } from "../components";
// Test system implementations
class TestSystemA extends System {
readonly name = "TestA";
readonly requiredComponents: readonly ComponentType[] = ["position"];
readonly priority = 0;
updateCalls: EntityId[][] = [];
update(entities: EntityId[], _world: ECSWorld): void {
this.updateCalls.push([...entities]);
}
}
class TestSystemB extends System {
readonly name = "TestB";
readonly requiredComponents: readonly ComponentType[] = ["position", "stats"];
readonly priority = 10; // Lower priority = runs later
updateCalls: EntityId[][] = [];
update(entities: EntityId[], _world: ECSWorld): void {
this.updateCalls.push([...entities]);
}
}
class TestSystemWithHooks extends System {
readonly name = "TestWithHooks";
readonly requiredComponents: readonly ComponentType[] = ["position"];
registered = false;
unregistered = false;
addedEntities: EntityId[] = [];
removedEntities: EntityId[] = [];
update(_entities: EntityId[], _world: ECSWorld): void {}
onRegister(_world: ECSWorld): void {
this.registered = true;
}
onUnregister(_world: ECSWorld): void {
this.unregistered = true;
}
onEntityAdded(entityId: EntityId, _world: ECSWorld): void {
this.addedEntities.push(entityId);
}
onEntityRemoved(entityId: EntityId, _world: ECSWorld): void {
this.removedEntities.push(entityId);
}
}
describe("SystemRegistry", () => {
let world: ECSWorld;
let registry: SystemRegistry;
beforeEach(() => {
world = new ECSWorld();
registry = new SystemRegistry(world);
});
describe("register()", () => {
it("should register a system", () => {
const system = new TestSystemA();
registry.register(system);
expect(registry.has("TestA")).toBe(true);
});
it("should call onRegister when registering", () => {
const system = new TestSystemWithHooks();
registry.register(system);
expect(system.registered).toBe(true);
});
it("should inject event bus into system", () => {
const eventBus = new EventBus();
const registryWithEvents = new SystemRegistry(world, eventBus);
const system = new TestSystemA();
const setEventBusSpy = vi.spyOn(system, "setEventBus");
registryWithEvents.register(system);
expect(setEventBusSpy).toHaveBeenCalledWith(eventBus);
});
});
describe("unregister()", () => {
it("should unregister by instance", () => {
const system = new TestSystemA();
registry.register(system);
const result = registry.unregister(system);
expect(result).toBe(true);
expect(registry.has("TestA")).toBe(false);
});
it("should unregister by name", () => {
registry.register(new TestSystemA());
const result = registry.unregister("TestA");
expect(result).toBe(true);
expect(registry.has("TestA")).toBe(false);
});
it("should call onUnregister when unregistering", () => {
const system = new TestSystemWithHooks();
registry.register(system);
registry.unregister(system);
expect(system.unregistered).toBe(true);
});
it("should return false for unknown system", () => {
const result = registry.unregister("Unknown");
expect(result).toBe(false);
});
});
describe("get()", () => {
it("should return system by name", () => {
const system = new TestSystemA();
registry.register(system);
expect(registry.get("TestA")).toBe(system);
});
it("should return undefined for unknown system", () => {
expect(registry.get("Unknown")).toBeUndefined();
});
});
describe("updateAll()", () => {
it("should update all systems", () => {
const systemA = new TestSystemA();
const systemB = new TestSystemB();
registry.register(systemA);
registry.register(systemB);
// Create entity with position
const id1 = world.createEntity();
world.addComponent(id1, "position", { x: 0, y: 0 });
registry.updateAll();
expect(systemA.updateCalls.length).toBe(1);
expect(systemA.updateCalls[0]).toContain(id1);
});
it("should pass only matching entities to each system", () => {
const systemA = new TestSystemA(); // needs position
const systemB = new TestSystemB(); // needs position + stats
registry.register(systemA);
registry.register(systemB);
// Entity with only position
const id1 = world.createEntity();
world.addComponent(id1, "position", { x: 0, y: 0 });
// Entity with position and stats
const id2 = world.createEntity();
world.addComponent(id2, "position", { x: 1, y: 1 });
world.addComponent(id2, "stats", { hp: 10, maxHp: 10 } as any);
registry.updateAll();
// SystemA should get both entities
expect(systemA.updateCalls[0]).toContain(id1);
expect(systemA.updateCalls[0]).toContain(id2);
// SystemB should only get entity with both components
expect(systemB.updateCalls[0]).not.toContain(id1);
expect(systemB.updateCalls[0]).toContain(id2);
});
it("should respect priority order", () => {
const callOrder: string[] = [];
class PrioritySystemLow extends System {
readonly name = "Low";
readonly requiredComponents: readonly ComponentType[] = ["position"];
readonly priority = 100;
update() { callOrder.push("Low"); }
}
class PrioritySystemHigh extends System {
readonly name = "High";
readonly requiredComponents: readonly ComponentType[] = ["position"];
readonly priority = -10;
update() { callOrder.push("High"); }
}
// Register in reverse order
registry.register(new PrioritySystemLow());
registry.register(new PrioritySystemHigh());
const id = world.createEntity();
world.addComponent(id, "position", { x: 0, y: 0 });
registry.updateAll();
expect(callOrder).toEqual(["High", "Low"]);
});
it("should skip disabled systems", () => {
const system = new TestSystemA();
registry.register(system);
const id = world.createEntity();
world.addComponent(id, "position", { x: 0, y: 0 });
system.enabled = false;
registry.updateAll();
expect(system.updateCalls.length).toBe(0);
});
});
describe("setEnabled()", () => {
it("should enable/disable system by name", () => {
const system = new TestSystemA();
registry.register(system);
registry.setEnabled("TestA", false);
expect(system.enabled).toBe(false);
registry.setEnabled("TestA", true);
expect(system.enabled).toBe(true);
});
it("should return false for unknown system", () => {
expect(registry.setEnabled("Unknown", false)).toBe(false);
});
});
describe("entity notifications", () => {
it("should notify systems when entity is added", () => {
const system = new TestSystemWithHooks();
registry.register(system);
const id = world.createEntity();
world.addComponent(id, "position", { x: 0, y: 0 });
registry.notifyEntityAdded(id);
expect(system.addedEntities).toContain(id);
});
it("should notify systems when entity is removed", () => {
const system = new TestSystemWithHooks();
registry.register(system);
const id = world.createEntity();
world.addComponent(id, "position", { x: 0, y: 0 });
registry.notifyEntityRemoved(id);
expect(system.removedEntities).toContain(id);
});
});
describe("getSystems()", () => {
it("should return all registered systems", () => {
registry.register(new TestSystemA());
registry.register(new TestSystemB());
const systems = registry.getSystems();
expect(systems.length).toBe(2);
expect(systems.map(s => s.name)).toContain("TestA");
expect(systems.map(s => s.name)).toContain("TestB");
});
});
});