277 lines
8.5 KiB
TypeScript
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 });
|
|
});
|
|
});
|
|
});
|