249 lines
7.7 KiB
TypeScript
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();
|
|
});
|
|
});
|