Files
rogue/src/rendering/__tests__/DungeonRenderer.test.ts
2026-01-05 15:41:27 +11:00

212 lines
6.1 KiB
TypeScript

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();
});
});