import { describe, it, expect, beforeEach } from 'vitest'; import { traceProjectile } from '../CombatLogic'; import type { World, EntityId } from '../../../core/types'; import { EntityAccessor } from '../../EntityAccessor'; import { TileType } from '../../../core/terrain'; import { ECSWorld } from '../../ecs/World'; describe('CombatLogic', () => { // Mock World let mockWorld: World; let ecsWorld: ECSWorld; let accessor: EntityAccessor; // Helper to set wall const setWall = (x: number, y: number) => { mockWorld.tiles[y * mockWorld.width + x] = TileType.WALL; }; beforeEach(() => { mockWorld = { width: 10, height: 10, tiles: new Array(100).fill(TileType.EMPTY), exit: { x: 9, y: 9 }, trackPath: [] }; ecsWorld = new ECSWorld(); // Shooter ID 1 accessor = new EntityAccessor(mockWorld, 1 as EntityId, ecsWorld); }); function syncActor(actor: any) { ecsWorld.addComponent(actor.id as EntityId, "position", actor.pos); if (actor.category === 'combatant') { ecsWorld.addComponent(actor.id as EntityId, "actorType", { type: actor.type }); ecsWorld.addComponent(actor.id as EntityId, "stats", { hp: 10 } as any); if (actor.isPlayer) ecsWorld.addComponent(actor.id as EntityId, "player", {}); } else if (actor.category === 'item_drop') { ecsWorld.addComponent(actor.id as EntityId, "groundItem", { item: actor.item || {} }); } } describe('traceProjectile', () => { it('should travel full path if no obstacles', () => { const start = { x: 0, y: 0 }; const end = { x: 5, y: 0 }; const result = traceProjectile(mockWorld, start, end, accessor); expect(result.blockedPos).toEqual(end); expect(result.hitActorId).toBeUndefined(); expect(result.path).toHaveLength(6); }); it('should stop at wall', () => { const start = { x: 0, y: 0 }; const end = { x: 5, y: 0 }; setWall(3, 0); // Wall at (3,0) const result = traceProjectile(mockWorld, start, end, accessor); expect(result.blockedPos).toEqual({ x: 2, y: 0 }); expect(result.hitActorId).toBeUndefined(); }); it('should stop at enemy', () => { const start = { x: 0, y: 0 }; const end = { x: 5, y: 0 }; // Place enemy at (3,0) const enemyId = 2 as EntityId; const enemy = { id: enemyId, type: 'rat', category: 'combatant', pos: { x: 3, y: 0 }, isPlayer: false }; syncActor(enemy); const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId); // Shooter 1 expect(result.blockedPos).toEqual({ x: 3, y: 0 }); expect(result.hitActorId).toBe(enemyId); }); it('should ignore shooter position', () => { const start = { x: 0, y: 0 }; const end = { x: 5, y: 0 }; // Shooter at start const shooter = { id: 1 as EntityId, type: 'player', category: 'combatant', pos: { x: 0, y: 0 }, isPlayer: true }; syncActor(shooter); const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId); // Should not hit self expect(result.hitActorId).toBeUndefined(); expect(result.blockedPos).toEqual(end); }); it('should ignore non-combatant actors (e.g. items)', () => { const start = { x: 0, y: 0 }; const end = { x: 5, y: 0 }; // Item at (3,0) const item = { id: 99 as EntityId, category: 'item_drop', pos: { x: 3, y: 0 }, item: { name: 'Test Item' } }; syncActor(item); const result = traceProjectile(mockWorld, start, end, accessor); // Should pass through item expect(result.blockedPos).toEqual(end); expect(result.hitActorId).toBeUndefined(); }); }); });