feat: Add traps
This commit is contained in:
288
src/engine/ecs/__tests__/SystemRegistry.test.ts
Normal file
288
src/engine/ecs/__tests__/SystemRegistry.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user