import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock Phaser vi.mock('phaser', () => { const mockGraphics = { setDepth: vi.fn().mockReturnThis(), clear: vi.fn().mockReturnThis(), lineStyle: vi.fn().mockReturnThis(), lineBetween: vi.fn().mockReturnThis(), }; const mockSprite = { setDepth: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(), setAlpha: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(), }; return { default: { GameObjects: { Sprite: vi.fn(() => mockSprite), Graphics: vi.fn(() => mockGraphics), }, Scene: class { add = { graphics: vi.fn(() => mockGraphics), sprite: vi.fn(() => mockSprite), }; } } }; }); // Mock CombatLogic vi.mock('../../../engine/gameplay/CombatLogic', () => ({ traceProjectile: vi.fn(), getClosestVisibleEnemy: vi.fn(), })); import { TargetingSystem } from '../TargetingSystem'; import { traceProjectile, getClosestVisibleEnemy } from '../../../engine/gameplay/CombatLogic'; import { TILE_SIZE } from '../../../core/constants'; import type { EntityId } from '../../../core/types'; describe('TargetingSystem', () => { let targetingSystem: TargetingSystem; let mockWorld: any; let mockScene: any; let mockGraphics: any; let mockSprite: any; beforeEach(() => { vi.clearAllMocks(); mockGraphics = { setDepth: vi.fn().mockReturnThis(), clear: vi.fn().mockReturnThis(), lineStyle: vi.fn().mockReturnThis(), lineBetween: vi.fn().mockReturnThis(), }; mockSprite = { setDepth: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(), setAlpha: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(), }; mockScene = { add: { graphics: vi.fn(() => mockGraphics), sprite: vi.fn(() => mockSprite), }, } as any; targetingSystem = new TargetingSystem(mockScene); mockWorld = { width: 10, height: 10 }; // Default return for traceProjectile (traceProjectile as any).mockReturnValue({ blockedPos: { x: 0, y: 0 }, hitActorId: undefined, path: [] }); }); it('should initialize with graphics and crosshair sprite hidden', () => { expect(mockScene.add.graphics).toHaveBeenCalled(); expect(mockScene.add.sprite).toHaveBeenCalled(); expect(mockSprite.setVisible).toHaveBeenCalledWith(false); }); it('should start targeting and auto-select closest enemy', () => { const playerPos = { x: 1, y: 1 }; const enemyPos = { x: 3, y: 3 }; (getClosestVisibleEnemy as any).mockReturnValue(enemyPos); const mockAccessor = { getCombatant: vi.fn().mockReturnValue({ pos: playerPos, inventory: { items: [{ id: 'item-1' }] } }), context: {} }; targetingSystem.startTargeting( 'item-1', playerPos, mockWorld, mockAccessor as any, 1 as EntityId, // playerId new Uint8Array(100), 10 ); expect(targetingSystem.isActive).toBe(true); expect(targetingSystem.itemId).toBe('item-1'); expect(targetingSystem.cursorPos).toEqual(enemyPos); expect(mockSprite.setVisible).toHaveBeenCalledWith(true); }); it('should fallback to mouse position if no enemy found', () => { const playerPos = { x: 1, y: 1 }; const mousePos = { x: 5, y: 5 }; (getClosestVisibleEnemy as any).mockReturnValue(null); const mockAccessor = { getCombatant: vi.fn().mockReturnValue({ pos: playerPos, inventory: { items: [{ id: 'item-1' }] } }), context: {} }; targetingSystem.startTargeting( 'item-1', playerPos, mockWorld, mockAccessor as any, 1 as EntityId, // playerId new Uint8Array(100), 10, mousePos ); expect(targetingSystem.cursorPos).toEqual(mousePos); }); it('should update visuals with predictive impact point', () => { const playerPos = { x: 1, y: 1 }; const targetPos = { x: 5, y: 1 }; const blockedPos = { x: 3, y: 1 }; // Wall at 3,1 (traceProjectile as any).mockReturnValue({ blockedPos: blockedPos, hitActorId: undefined, path: [] }); const mockAccessor = { getCombatant: vi.fn().mockReturnValue({ pos: playerPos, inventory: { items: [{ id: 'item-1' }] } }), context: {} }; // Start targeting targetingSystem.startTargeting( 'item-1', playerPos, mockWorld, mockAccessor as any, 1 as EntityId, new Uint8Array(100), 10, targetPos ); // The crosshair should be at blockedPos, not targetPos const expectedX = blockedPos.x * TILE_SIZE + TILE_SIZE / 2; const expectedY = blockedPos.y * TILE_SIZE + TILE_SIZE / 2; expect(mockSprite.setPosition).toHaveBeenCalledWith(expectedX, expectedY); // Verify dashed line was drawn (multiple lineBetween calls) expect(mockGraphics.lineBetween).toHaveBeenCalled(); expect(mockGraphics.lineBetween.mock.calls.length).toBeGreaterThan(1); }); it('should clear visuals on cancel', () => { targetingSystem.cancel(); expect(targetingSystem.isActive).toBe(false); expect(mockGraphics.clear).toHaveBeenCalled(); expect(mockSprite.setVisible).toHaveBeenCalledWith(false); }); it('should prevent targeting self', () => { const playerPos = { x: 1, y: 1 }; // Setup targeting targetingSystem.startTargeting( 'item-1', playerPos, mockWorld, { getCombatant: vi.fn().mockReturnValue({ pos: playerPos, inventory: { items: [{ id: 'item-1' }] } }) } as any, 1 as EntityId, new Uint8Array(100), 10 ); // Manually set cursor to player pos (startTargeting might do it, but we ensure it) targetingSystem.updateCursor(playerPos, playerPos); const callback = vi.fn(); const result = targetingSystem.executeThrow( mockWorld, 1 as EntityId, { getCombatant: vi.fn().mockReturnValue({ pos: playerPos, inventory: { items: [{ id: 'item-1' }] } }) } as any, callback ); expect(result).toBe(false); expect(callback).not.toHaveBeenCalled(); }); });