Begin refactoring GameScene

This commit is contained in:
Peter Stockings
2026-01-26 15:30:14 +11:00
parent 1d7be54fd9
commit ef7d85750f
46 changed files with 2459 additions and 1291 deletions

View File

@@ -1,6 +1,9 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DungeonRenderer } from '../DungeonRenderer';
import { type World } from '../../core/types';
import type { World, EntityId } from '../../core/types';
import { ECSWorld } from '../../engine/ecs/World';
import { EntityAccessor } from '../../engine/EntityAccessor';
// Mock Phaser
vi.mock('phaser', () => {
@@ -11,6 +14,10 @@ vi.mock('phaser', () => {
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 = {
@@ -27,6 +34,7 @@ vi.mock('phaser', () => {
setVisible: vi.fn().mockReturnThis(),
setScrollFactor: vi.fn().mockReturnThis(),
setDepth: vi.fn().mockReturnThis(),
y: 0
};
const mockRectangle = {
@@ -41,6 +49,13 @@ vi.mock('phaser', () => {
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: {
@@ -54,6 +69,8 @@ describe('DungeonRenderer', () => {
let mockScene: any;
let renderer: DungeonRenderer;
let mockWorld: World;
let ecsWorld: ECSWorld;
let accessor: EntityAccessor;
beforeEach(() => {
vi.clearAllMocks();
@@ -72,13 +89,25 @@ describe('DungeonRenderer', () => {
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(),
@@ -89,6 +118,7 @@ describe('DungeonRenderer', () => {
main: {
width: 800,
height: 600,
shake: vi.fn(),
},
},
anims: {
@@ -110,6 +140,9 @@ describe('DungeonRenderer', () => {
add: vi.fn(),
killTweensOf: vi.fn(),
},
time: {
now: 0
}
};
@@ -117,15 +150,16 @@ describe('DungeonRenderer', () => {
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors: new Map(),
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, 1);
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
// Spawn a couple of corpses
@@ -133,31 +167,29 @@ describe('DungeonRenderer', () => {
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);
expect(mockScene.add.sprite).toHaveBeenCalledTimes(3); // Player + 2 corpses
// Initialize floor again (changing level)
renderer.initializeFloor(mockWorld, 1);
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
// Verify destroy was called on both corpse sprites
// 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 as a circle and not as an enemy sprite', () => {
renderer.initializeFloor(mockWorld, 1);
it('should render exp_orb correctly', () => {
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
// 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
});
// 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;
@@ -165,40 +197,19 @@ describe('DungeonRenderer', () => {
// 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);
it('should render any enemy type as a sprite', () => {
renderer.initializeFloor(mockWorld, ecsWorld, accessor);
// 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,
energy: 10
});
// 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();
@@ -211,21 +222,16 @@ describe('DungeonRenderer', () => {
});
it('should initialize new enemy sprites at target position and not tween them', () => {
renderer.initializeFloor(mockWorld, 1);
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;
mockWorld.actors.set(999, {
id: 999,
category: "combatant",
isPlayer: false,
type: "rat",
pos: { x: 5, y: 5 },
stats: { hp: 10, maxHp: 10 } as any,
} as any);
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();