import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DungeonRenderer } from '../DungeonRenderer'; import { type World } from '../../core/types'; // Mock Phaser vi.mock('phaser', () => { const mockSprite = { setDepth: vi.fn().mockReturnThis(), setScale: vi.fn().mockReturnThis(), play: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(), destroy: vi.fn(), }; const mockGraphics = { clear: vi.fn().mockReturnThis(), fillStyle: vi.fn().mockReturnThis(), fillRect: vi.fn().mockReturnThis(), lineStyle: vi.fn().mockReturnThis(), strokeRect: vi.fn().mockReturnThis(), }; const mockContainer = { add: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(), setScrollFactor: vi.fn().mockReturnThis(), setDepth: vi.fn().mockReturnThis(), }; const mockRectangle = { setStrokeStyle: vi.fn().mockReturnThis(), setInteractive: vi.fn().mockReturnThis(), }; return { default: { GameObjects: { Sprite: vi.fn(() => mockSprite), Graphics: vi.fn(() => mockGraphics), Container: vi.fn(() => mockContainer), Rectangle: vi.fn(() => mockRectangle), }, Scene: vi.fn(), Math: { Clamp: vi.fn((v, min, max) => Math.min(Math.max(v, min), max)), }, }, }; }); describe('DungeonRenderer', () => { let mockScene: any; let renderer: DungeonRenderer; let mockWorld: World; beforeEach(() => { vi.clearAllMocks(); mockScene = { add: { graphics: vi.fn().mockReturnValue({ clear: vi.fn(), fillStyle: vi.fn(), fillRect: vi.fn(), }), sprite: vi.fn(() => ({ setDepth: vi.fn().mockReturnThis(), setScale: vi.fn().mockReturnThis(), play: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(), destroy: vi.fn(), })), container: vi.fn().mockReturnValue({ add: vi.fn(), setPosition: vi.fn(), setVisible: vi.fn(), setScrollFactor: vi.fn(), setDepth: vi.fn(), }), rectangle: vi.fn().mockReturnValue({ setStrokeStyle: vi.fn().mockReturnThis(), setInteractive: vi.fn().mockReturnThis(), }), }, cameras: { main: { width: 800, height: 600, }, }, anims: { create: vi.fn(), exists: vi.fn().mockReturnValue(true), generateFrameNumbers: vi.fn(), }, make: { tilemap: vi.fn().mockReturnValue({ addTilesetImage: vi.fn().mockReturnValue({}), createLayer: vi.fn().mockReturnValue({ setDepth: vi.fn(), forEachTile: vi.fn(), }), destroy: vi.fn(), }), }, tweens: { add: vi.fn(), killTweensOf: vi.fn(), }, }; mockWorld = { width: 10, height: 10, tiles: new Array(100).fill(0), actors: new Map(), exit: { x: 9, y: 9 }, }; renderer = new DungeonRenderer(mockScene); }); it('should track and clear corpse sprites on floor initialization', () => { renderer.initializeFloor(mockWorld, 1); // Spawn a couple of corpses renderer.spawnCorpse(1, 1, 'rat'); renderer.spawnCorpse(2, 2, 'bat'); // Get the mock sprites that were returned by scene.add.sprite const corpse1 = mockScene.add.sprite.mock.results[1].value; const corpse2 = mockScene.add.sprite.mock.results[2].value; expect(mockScene.add.sprite).toHaveBeenCalledTimes(3); // Initialize floor again (changing level) renderer.initializeFloor(mockWorld, 1); // Verify destroy was called on both corpse sprites expect(corpse1.destroy).toHaveBeenCalledTimes(1); expect(corpse2.destroy).toHaveBeenCalledTimes(1); }); it('should render exp_orb as a circle and not as an enemy sprite', () => { renderer.initializeFloor(mockWorld, 1); // Add an exp_orb to the world mockWorld.actors.set(2, { id: 2, category: "collectible", type: "exp_orb", pos: { x: 2, y: 1 }, expAmount: 10 }); // Make the tile visible for it to render (renderer as any).fovManager.visibleArray[1 * mockWorld.width + 2] = 1; // Reset mocks mockScene.add.sprite.mockClear(); // Mock scene.add.circle mockScene.add.circle = vi.fn().mockReturnValue({ setStrokeStyle: vi.fn().mockReturnThis(), setDepth: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(), }); renderer.render([]); // Should NOT have added an enemy sprite for the orb const spriteCalls = mockScene.add.sprite.mock.calls; // Any sprite added that isn't the player (which isn't in mockWorld.actors here except if we added it) // The current loop skips a.isPlayer and then checks if type is in GAME_CONFIG.enemies expect(spriteCalls.length).toBe(0); // Should HAVE added a circle for the orb expect(mockScene.add.circle).toHaveBeenCalled(); }); it('should render any enemy type defined in config as a sprite', () => { renderer.initializeFloor(mockWorld, 1); // Add a rat (defined in config) mockWorld.actors.set(3, { id: 3, category: "combatant", isPlayer: false, type: "rat", pos: { x: 3, y: 1 }, speed: 10, stats: { hp: 10, maxHp: 10, attack: 2, defense: 0 } as any }); (renderer as any).fovManager.visibleArray[1 * mockWorld.width + 3] = 1; mockScene.add.sprite.mockClear(); renderer.render([]); // Should have added a sprite for the rat const ratSpriteCall = mockScene.add.sprite.mock.calls.find((call: any) => call[2] === 'rat'); expect(ratSpriteCall).toBeDefined(); }); });