import { describe, it, expect } from 'vitest'; import { applyAction, decideEnemyAction } from '../simulation/simulation'; import { type World, type Actor, type EntityId, type CombatantActor } from '../../core/types'; import { EntityManager } from '../EntityManager'; describe('Combat Simulation', () => { let entityManager: EntityManager; const createTestWorld = (actors: Map): World => { return { width: 10, height: 10, tiles: new Array(100).fill(0), actors, exit: { x: 9, y: 9 } }; }; const createTestStats = (overrides: Partial = {}) => ({ maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: [], critChance: 0, critMultiplier: 100, accuracy: 100, lifesteal: 0, evasion: 0, blockChance: 0, luck: 0, ...overrides }); describe('applyAction - success paths', () => { it('should deal damage when player attacks enemy', () => { const actors = new Map(); actors.set(1, { id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: createTestStats() } as any); actors.set(2, { id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 }) } as any); const world = createTestWorld(actors); entityManager = new EntityManager(world); const events = applyAction(world, 1, { type: "attack", targetId: 2 }, entityManager); const enemy = world.actors.get(2) as CombatantActor; expect(enemy.stats.hp).toBeLessThan(10); expect(events.some(e => e.type === "attacked")).toBe(true); }); it("should kill enemy and spawn EXP orb without ID reuse collision", () => { const actors = new Map(); actors.set(1, { id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: createTestStats({ attack: 50 }) } as any); actors.set(2, { id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 }) } as any); const world = createTestWorld(actors); entityManager = new EntityManager(world); applyAction(world, 1, { type: "attack", targetId: 2 }, entityManager); // Enemy (id 2) should be gone expect(world.actors.has(2)).toBe(false); // A new ID should be generated for the orb (should be 3) const orb = [...world.actors.values()].find(a => a.type === "exp_orb"); expect(orb).toBeDefined(); expect(orb!.id).toBe(3); }); }); describe("decideEnemyAction - AI Logic", () => { it("should path around walls", () => { const actors = new Map(); const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 5, y: 3 }, stats: createTestStats() } as any; const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 3, y: 3 }, stats: createTestStats() } as any; actors.set(1, player); actors.set(2, enemy); const world = createTestWorld(actors); world.tiles[3 * 10 + 4] = 4; // Wall entityManager = new EntityManager(world); const decision = decideEnemyAction(world, enemy, player, entityManager); expect(decision.action.type).toBe("move"); }); it("should attack if player is adjacent", () => { const actors = new Map(); const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 4, y: 3 }, stats: createTestStats() } as any; const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 3, y: 3 }, stats: createTestStats(), // Set AI state to pursuing so the enemy will attack when adjacent aiState: "pursuing", lastKnownPlayerPos: { x: 4, y: 3 } } as any; actors.set(1, player); actors.set(2, enemy); const world = createTestWorld(actors); entityManager = new EntityManager(world); const decision = decideEnemyAction(world, enemy, player, entityManager); expect(decision.action).toEqual({ type: "attack", targetId: 1 }); }); }); });