Begin refactoring GameScene
This commit is contained in:
@@ -9,6 +9,7 @@ import { MinimapRenderer } from "./MinimapRenderer";
|
||||
import { FxRenderer } from "./FxRenderer";
|
||||
import { ItemSpriteFactory } from "./ItemSpriteFactory";
|
||||
import { type ECSWorld } from "../engine/ecs/World";
|
||||
import { type EntityAccessor } from "../engine/EntityAccessor";
|
||||
|
||||
export class DungeonRenderer {
|
||||
private scene: Phaser.Scene;
|
||||
@@ -25,7 +26,8 @@ export class DungeonRenderer {
|
||||
private fxRenderer: FxRenderer;
|
||||
|
||||
private world!: World;
|
||||
private ecsWorld?: ECSWorld;
|
||||
private entityAccessor!: EntityAccessor;
|
||||
private ecsWorld!: ECSWorld;
|
||||
private trapSprites: Map<number, Phaser.GameObjects.Sprite> = new Map();
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
@@ -35,17 +37,33 @@ export class DungeonRenderer {
|
||||
this.fxRenderer = new FxRenderer(scene);
|
||||
}
|
||||
|
||||
initializeFloor(world: World, playerId: EntityId, ecsWorld?: ECSWorld) {
|
||||
initializeFloor(world: World, ecsWorld: ECSWorld, entityAccessor: EntityAccessor) {
|
||||
this.world = world;
|
||||
this.ecsWorld = ecsWorld;
|
||||
this.entityAccessor = entityAccessor;
|
||||
this.fovManager.initialize(world);
|
||||
|
||||
// Clear old trap sprites
|
||||
// Clear old sprites from maps
|
||||
for (const [, sprite] of this.trapSprites) {
|
||||
sprite.destroy();
|
||||
}
|
||||
this.trapSprites.clear();
|
||||
|
||||
for (const [, sprite] of this.enemySprites) {
|
||||
sprite.destroy();
|
||||
}
|
||||
this.enemySprites.clear();
|
||||
|
||||
for (const [, sprite] of this.orbSprites) {
|
||||
sprite.destroy();
|
||||
}
|
||||
this.orbSprites.clear();
|
||||
|
||||
for (const [, sprite] of this.itemSprites) {
|
||||
sprite.destroy();
|
||||
}
|
||||
this.itemSprites.clear();
|
||||
|
||||
// Setup Tilemap
|
||||
if (this.map) this.map.destroy();
|
||||
this.map = this.scene.make.tilemap({
|
||||
@@ -81,8 +99,8 @@ export class DungeonRenderer {
|
||||
// Kill any active tweens on the player sprite
|
||||
this.scene.tweens.killTweensOf(this.playerSprite);
|
||||
|
||||
// Get player position in new world using provided playerId
|
||||
const player = world.actors.get(playerId);
|
||||
|
||||
const player = this.entityAccessor.getPlayer();
|
||||
if (player && player.category === "combatant") {
|
||||
this.playerSprite.setPosition(
|
||||
player.pos.x * TILE_SIZE + TILE_SIZE / 2,
|
||||
@@ -122,8 +140,11 @@ export class DungeonRenderer {
|
||||
return this.minimapRenderer.isVisible();
|
||||
}
|
||||
|
||||
computeFov(playerId: EntityId) {
|
||||
this.fovManager.compute(this.world, playerId);
|
||||
computeFov() {
|
||||
const player = this.entityAccessor.getPlayer();
|
||||
if (player && player.category === "combatant") {
|
||||
this.fovManager.compute(this.world, player.pos);
|
||||
}
|
||||
}
|
||||
|
||||
isSeen(x: number, y: number): boolean {
|
||||
@@ -210,7 +231,8 @@ export class DungeonRenderer {
|
||||
const activeOrbIds = new Set<EntityId>();
|
||||
const activeItemIds = new Set<EntityId>();
|
||||
|
||||
for (const a of this.world.actors.values()) {
|
||||
const actors = this.entityAccessor.getAllActors();
|
||||
for (const a of actors) {
|
||||
const i = idx(this.world, a.pos.x, a.pos.y);
|
||||
const isVis = visible[i] === 1;
|
||||
|
||||
@@ -310,7 +332,7 @@ export class DungeonRenderer {
|
||||
for (const [id, sprite] of this.enemySprites.entries()) {
|
||||
if (!activeEnemyIds.has(id)) {
|
||||
sprite.setVisible(false);
|
||||
if (!this.world.actors.has(id)) {
|
||||
if (!this.entityAccessor.hasActor(id)) {
|
||||
sprite.destroy();
|
||||
this.enemySprites.delete(id);
|
||||
}
|
||||
@@ -320,7 +342,7 @@ export class DungeonRenderer {
|
||||
for (const [id, orb] of this.orbSprites.entries()) {
|
||||
if (!activeOrbIds.has(id)) {
|
||||
orb.setVisible(false);
|
||||
if (!this.world.actors.has(id)) {
|
||||
if (!this.entityAccessor.hasActor(id)) {
|
||||
orb.destroy();
|
||||
this.orbSprites.delete(id);
|
||||
}
|
||||
@@ -330,14 +352,14 @@ export class DungeonRenderer {
|
||||
for (const [id, item] of this.itemSprites.entries()) {
|
||||
if (!activeItemIds.has(id)) {
|
||||
item.setVisible(false);
|
||||
if (!this.world.actors.has(id)) {
|
||||
if (!this.entityAccessor.hasActor(id)) {
|
||||
item.destroy();
|
||||
this.itemSprites.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.minimapRenderer.render(this.world, seen, visible);
|
||||
this.minimapRenderer.render(this.world, seen, visible, this.entityAccessor);
|
||||
}
|
||||
|
||||
// FX Delegations
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FOV } from "rot-js";
|
||||
import type ROT from "rot-js";
|
||||
import { type World, type EntityId } from "../core/types";
|
||||
import { type World } from "../core/types";
|
||||
import { idx, inBounds } from "../engine/world/world-logic";
|
||||
import { blocksSight } from "../core/terrain";
|
||||
import { GAME_CONFIG } from "../core/config/GameConfig";
|
||||
@@ -28,13 +28,12 @@ export class FovManager {
|
||||
});
|
||||
}
|
||||
|
||||
compute(world: World, playerId: EntityId) {
|
||||
compute(world: World, origin: { x: number; y: number }) {
|
||||
this.visible.fill(0);
|
||||
this.visibleStrength.fill(0);
|
||||
|
||||
const player = world.actors.get(playerId)!;
|
||||
const ox = player.pos.x;
|
||||
const oy = player.pos.y;
|
||||
const ox = origin.x;
|
||||
const oy = origin.y;
|
||||
|
||||
this.fov.compute(ox, oy, GAME_CONFIG.player.viewRadius, (x: number, y: number, r: number, v: number) => {
|
||||
if (!inBounds(world, x, y)) return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Phaser from "phaser";
|
||||
import { type World, type CombatantActor } from "../core/types";
|
||||
import { type World } from "../core/types";
|
||||
import { type EntityAccessor } from "../engine/EntityAccessor";
|
||||
import { idx, isWall } from "../engine/world/world-logic";
|
||||
import { GAME_CONFIG } from "../core/config/GameConfig";
|
||||
|
||||
@@ -47,7 +48,7 @@ export class MinimapRenderer {
|
||||
return this.minimapVisible;
|
||||
}
|
||||
|
||||
render(world: World, seen: Uint8Array, visible: Uint8Array) {
|
||||
render(world: World, seen: Uint8Array, visible: Uint8Array, accessor: EntityAccessor) {
|
||||
this.minimapGfx.clear();
|
||||
if (!world) return;
|
||||
|
||||
@@ -84,20 +85,17 @@ export class MinimapRenderer {
|
||||
this.minimapGfx.fillRect(offsetX + ex * tileSize, offsetY + ey * tileSize, tileSize, tileSize);
|
||||
}
|
||||
|
||||
const player = [...world.actors.values()].find(a => a.category === "combatant" && a.isPlayer) as CombatantActor;
|
||||
const player = accessor.getPlayer();
|
||||
if (player) {
|
||||
this.minimapGfx.fillStyle(0x66ff66, 1);
|
||||
this.minimapGfx.fillRect(offsetX + player.pos.x * tileSize, offsetY + player.pos.y * tileSize, tileSize, tileSize);
|
||||
}
|
||||
|
||||
for (const a of world.actors.values()) {
|
||||
if (a.category === "combatant") {
|
||||
if (a.isPlayer) continue;
|
||||
const i = idx(world, a.pos.x, a.pos.y);
|
||||
if (visible[i] === 1) {
|
||||
this.minimapGfx.fillStyle(0xff6666, 1);
|
||||
this.minimapGfx.fillRect(offsetX + a.pos.x * tileSize, offsetY + a.pos.y * tileSize, tileSize, tileSize);
|
||||
}
|
||||
for (const a of accessor.getEnemies()) {
|
||||
const i = idx(world, a.pos.x, a.pos.y);
|
||||
if (visible[i] === 1) {
|
||||
this.minimapGfx.fillStyle(0xff6666, 1);
|
||||
this.minimapGfx.fillRect(offsetX + a.pos.x * tileSize, offsetY + a.pos.y * tileSize, tileSize, tileSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user