Files
rogue/src/rendering/__tests__/DungeonRenderer.test.ts
2026-01-27 13:46:19 +11:00

249 lines
7.7 KiB
TypeScript

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