Files
rogue/src/engine/__tests__/EntityAccessor.test.ts

277 lines
8.5 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { EntityAccessor } from "../EntityAccessor";
import { ECSWorld } from "../ecs/World";
import type { World, CombatantActor, CollectibleActor, ItemDropActor, Actor, EntityId } from "../../core/types";
function createMockWorld(): World {
return {
width: 10,
height: 10,
tiles: new Array(100).fill(0),
exit: { x: 9, y: 9 },
trackPath: []
};
}
function createPlayer(id: number, x: number, y: number): CombatantActor {
return {
id: id as EntityId,
pos: { x, y },
category: "combatant",
isPlayer: true,
type: "player",
speed: 100,
energy: 0,
stats: {
maxHp: 20, hp: 20, maxMana: 10, mana: 10,
attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0,
evasion: 5, blockChance: 0, luck: 0,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
passiveNodes: [],
},
};
}
function createEnemy(id: number, x: number, y: number, type: "rat" | "bat" = "rat"): CombatantActor {
return {
id: id as EntityId,
pos: { x, y },
category: "combatant",
isPlayer: false,
type,
speed: 80,
energy: 0,
stats: {
maxHp: 10, hp: 10, maxMana: 0, mana: 0,
attack: 3, defense: 1, level: 1, exp: 0, expToNextLevel: 10,
critChance: 0, critMultiplier: 100, accuracy: 80, lifesteal: 0,
evasion: 0, blockChance: 0, luck: 0,
statPoints: 0, skillPoints: 0, strength: 5, dexterity: 5, intelligence: 5,
passiveNodes: [],
},
};
}
function createExpOrb(id: number, x: number, y: number): CollectibleActor {
return {
id: id as EntityId,
pos: { x, y },
category: "collectible",
type: "exp_orb",
expAmount: 5,
};
}
function createItemDrop(id: number, x: number, y: number): ItemDropActor {
return {
id: id as EntityId,
pos: { x, y },
category: "item_drop",
item: {
id: "health_potion",
name: "Health Potion",
type: "Consumable",
textureKey: "items",
spriteIndex: 0,
},
};
}
describe("EntityAccessor", () => {
let world: World;
let ecsWorld: ECSWorld;
let accessor: EntityAccessor;
const PLAYER_ID = 1;
beforeEach(() => {
world = createMockWorld();
ecsWorld = new ECSWorld();
accessor = new EntityAccessor(world, PLAYER_ID as EntityId, ecsWorld);
});
function syncActor(actor: Actor) {
ecsWorld.addComponent(actor.id, "position", actor.pos);
ecsWorld.addComponent(actor.id, "name", { name: actor.id.toString() });
if (actor.category === "combatant") {
const c = actor as CombatantActor;
ecsWorld.addComponent(actor.id, "stats", c.stats);
ecsWorld.addComponent(actor.id, "energy", { current: c.energy, speed: c.speed });
ecsWorld.addComponent(actor.id, "actorType", { type: c.type });
if (c.isPlayer) {
ecsWorld.addComponent(actor.id, "player", {});
} else {
ecsWorld.addComponent(actor.id, "ai", { state: "wandering" });
}
} else if (actor.category === "collectible") {
ecsWorld.addComponent(actor.id, "collectible", { type: "exp_orb", amount: (actor as CollectibleActor).expAmount });
} else if (actor.category === "item_drop") {
ecsWorld.addComponent(actor.id, "groundItem", { item: (actor as ItemDropActor).item });
}
}
describe("Player Access", () => {
it("getPlayer returns player when exists", () => {
const player = createPlayer(PLAYER_ID, 5, 5);
syncActor(player);
expect(accessor.getPlayer()?.id).toBe(player.id);
});
it("getPlayer returns null when player doesn't exist", () => {
expect(accessor.getPlayer()).toBeNull();
});
it("getPlayerPos returns position copy", () => {
const player = createPlayer(PLAYER_ID, 3, 4);
syncActor(player);
const pos = accessor.getPlayerPos();
expect(pos).toEqual({ x: 3, y: 4 });
// Verify it's a copy
if (pos) {
pos.x = 99;
const freshPlayer = accessor.getPlayer();
expect(freshPlayer?.pos.x).toBe(3);
}
});
it("isPlayerAlive returns true when player exists", () => {
syncActor(createPlayer(PLAYER_ID, 5, 5));
expect(accessor.isPlayerAlive()).toBe(true);
});
it("isPlayerAlive returns false when player is dead", () => {
expect(accessor.isPlayerAlive()).toBe(false);
});
});
describe("Generic Actor Access", () => {
it("getActor returns actor by ID", () => {
const enemy = createEnemy(2, 3, 3);
syncActor(enemy);
expect(accessor.getActor(2 as EntityId)?.id).toBe(enemy.id);
});
it("getActor returns null for non-existent ID", () => {
expect(accessor.getActor(999 as EntityId)).toBeNull();
});
it("getCombatant returns combatant by ID", () => {
const enemy = createEnemy(2, 3, 3);
syncActor(enemy);
expect(accessor.getCombatant(2 as EntityId)?.id).toBe(enemy.id);
});
it("getCombatant returns null for non-combatant", () => {
const orb = createExpOrb(3, 5, 5);
syncActor(orb);
expect(accessor.getCombatant(3 as EntityId)).toBeNull();
});
it("hasActor returns true for existing actor", () => {
syncActor(createEnemy(2, 3, 3));
expect(accessor.hasActor(2 as EntityId)).toBe(true);
});
it("hasActor returns false for non-existent ID", () => {
expect(accessor.hasActor(999 as EntityId)).toBe(false);
});
});
describe("Spatial Queries", () => {
it("findEnemyAt returns enemy at position", () => {
const enemy = createEnemy(2, 4, 4);
syncActor(enemy);
expect(accessor.findEnemyAt(4, 4)?.id).toBe(enemy.id);
});
it("findEnemyAt returns null when no enemy at position", () => {
syncActor(createPlayer(PLAYER_ID, 4, 4));
expect(accessor.findEnemyAt(4, 4)).toBeNull();
});
it("hasEnemyAt returns true when enemy exists at position", () => {
syncActor(createEnemy(2, 4, 4));
expect(accessor.hasEnemyAt(4, 4)).toBe(true);
});
it("findCollectibleAt returns collectible at position", () => {
const orb = createExpOrb(3, 6, 6);
syncActor(orb);
expect(accessor.findCollectibleAt(6, 6)?.id).toBe(orb.id);
});
it("findItemDropAt returns item drop at position", () => {
const drop = createItemDrop(4, 7, 7);
syncActor(drop);
expect(accessor.findItemDropAt(7, 7)?.id).toBe(drop.id);
});
});
describe("Collection Queries", () => {
beforeEach(() => {
syncActor(createPlayer(PLAYER_ID, 5, 5));
syncActor(createEnemy(2, 3, 3));
syncActor(createEnemy(3, 4, 4, "bat"));
syncActor(createExpOrb(4, 6, 6));
syncActor(createItemDrop(5, 7, 7));
});
it("getEnemies returns only non-player combatants", () => {
const enemies = accessor.getEnemies();
expect(enemies.length).toBe(2);
expect(enemies.every(e => !e.isPlayer)).toBe(true);
});
it("getCombatants returns player and enemies", () => {
const combatants = accessor.getCombatants();
expect(combatants.length).toBe(3);
});
it("getCollectibles returns only collectibles", () => {
const collectibles = accessor.getCollectibles();
expect(collectibles.length).toBe(1);
expect(collectibles[0].id).toBe(4);
});
it("getItemDrops returns only item drops", () => {
const drops = accessor.getItemDrops();
expect(drops.length).toBe(1);
expect(drops[0].id).toBe(5);
});
});
describe("updateWorld", () => {
it("updates references correctly", () => {
syncActor(createPlayer(PLAYER_ID, 1, 1));
const newWorld = createMockWorld();
const newEcsWorld = new ECSWorld();
const newPlayerId = 10;
const newPlayer = createPlayer(newPlayerId, 8, 8);
// Manually add to newEcsWorld
newEcsWorld.addComponent(newPlayer.id, "position", newPlayer.pos);
newEcsWorld.addComponent(newPlayer.id, "actorType", { type: "player" });
newEcsWorld.addComponent(newPlayer.id, "stats", newPlayer.stats);
newEcsWorld.addComponent(newPlayer.id, "player", {});
accessor.updateWorld(newWorld, newPlayerId as EntityId, newEcsWorld);
const player = accessor.getPlayer();
expect(player?.id).toBe(newPlayerId);
expect(player?.pos).toEqual({ x: 8, y: 8 });
});
});
});