import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DungeonRenderer } from '../DungeonRenderer'; import type { World, EntityId } from '../../core/types'; import { ECSWorld } from '../../engine/ecs/World'; import { EntityAccessor } from '../../engine/EntityAccessor'; // 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(), frame: { name: '0' }, setFrame: vi.fn(), setAlpha: vi.fn(), clearTint: 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(), y: 0 }; 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), Arc: vi.fn(() => ({ setStrokeStyle: vi.fn().mockReturnThis(), setDepth: vi.fn().mockReturnThis(), setPosition: vi.fn().mockReturnThis(), setVisible: vi.fn().mockReturnThis(), destroy: vi.fn(), })), }, 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; let ecsWorld: ECSWorld; let accessor: EntityAccessor; 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(), frame: { name: '0' }, setFrame: vi.fn(), setAlpha: vi.fn(), clearTint: vi.fn(), })), circle: vi.fn().mockReturnValue({ setStrokeStyle: vi.fn().mockReturnThis(), setDepth: 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(), y: 0 }), rectangle: vi.fn().mockReturnValue({ setStrokeStyle: vi.fn().mockReturnThis(), setInteractive: vi.fn().mockReturnThis(), }), }, cameras: { main: { width: 800, height: 600, shake: vi.fn(), }, }, 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(), }, time: { now: 0 } }; mockWorld = { width: 10, height: 10, tiles: new Array(100).fill(0), exit: { x: 9, y: 9 }, }; ecsWorld = new ECSWorld(); accessor = new EntityAccessor(mockWorld, 1 as EntityId, ecsWorld); renderer = new DungeonRenderer(mockScene); }); it('should track and clear corpse sprites on floor initialization', () => { renderer.initializeFloor(mockWorld, ecsWorld, accessor); // 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 // The player sprite is created first in initializeFloor if it doesn't exist // Then the two corpses const corpse1 = mockScene.add.sprite.mock.results[1].value; const corpse2 = mockScene.add.sprite.mock.results[2].value; expect(mockScene.add.sprite).toHaveBeenCalledTimes(3); // Player + 2 corpses // Initialize floor again (changing level) renderer.initializeFloor(mockWorld, ecsWorld, accessor); // Verify destroy was called on both corpse sprites (via fxRenderer.clearCorpses) expect(corpse1.destroy).toHaveBeenCalledTimes(1); expect(corpse2.destroy).toHaveBeenCalledTimes(1); }); it('should render exp_orb correctly', () => { renderer.initializeFloor(mockWorld, ecsWorld, accessor); // Add an exp_orb to the ECS world ecsWorld.addComponent(2 as EntityId, "position", { x: 2, y: 1 }); ecsWorld.addComponent(2 as EntityId, "collectible", { type: "exp_orb", amount: 10 }); ecsWorld.addComponent(2 as EntityId, "actorType", { type: "exp_orb" as any }); // Make the tile visible for it to render (renderer as any).fovManager.visibleArray[1 * mockWorld.width + 2] = 1; // Reset mocks mockScene.add.sprite.mockClear(); renderer.render([]); // Should HAVE added a circle for the orb expect(mockScene.add.circle).toHaveBeenCalled(); }); it('should render any enemy type as a sprite', () => { renderer.initializeFloor(mockWorld, ecsWorld, accessor); // Add a rat ecsWorld.addComponent(3 as EntityId, "position", { x: 3, y: 1 }); ecsWorld.addComponent(3 as EntityId, "actorType", { type: "rat" }); ecsWorld.addComponent(3 as EntityId, "stats", { hp: 10, maxHp: 10 } 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(); }); it('should initialize new enemy sprites at target position and not tween them', () => { renderer.initializeFloor(mockWorld, ecsWorld, accessor); // Position 5,5 -> 5*16 + 8 = 88 const TILE_SIZE = 16; const targetX = 5 * TILE_SIZE + TILE_SIZE / 2; const targetY = 5 * TILE_SIZE + TILE_SIZE / 2; ecsWorld.addComponent(999 as EntityId, "position", { x: 5, y: 5 }); ecsWorld.addComponent(999 as EntityId, "actorType", { type: "rat" }); ecsWorld.addComponent(999 as EntityId, "stats", { hp: 10, maxHp: 10 } as any); (renderer as any).fovManager.visibleArray[5 * mockWorld.width + 5] = 1; mockScene.add.sprite.mockClear(); mockScene.tweens.add.mockClear(); renderer.render([]); // Check spawn position expect(mockScene.add.sprite).toHaveBeenCalledWith(targetX, targetY, 'rat', 0); // Should NOT tween because it's the first spawn expect(mockScene.tweens.add).not.toHaveBeenCalled(); }); });