Refactor codebase

This commit is contained in:
Peter Stockings
2026-01-04 15:56:18 +11:00
parent 3785885abe
commit bfe5ebae8c
18 changed files with 380 additions and 191 deletions

5
src/core/constants.ts Normal file
View File

@@ -0,0 +1,5 @@
import { GAME_CONFIG } from "./config/GameConfig";
export const TILE_SIZE = GAME_CONFIG.rendering.tileSize;
export const ENERGY_THRESHOLD = GAME_CONFIG.gameplay.energyThreshold;
export const ACTION_COST = GAME_CONFIG.gameplay.actionCost;

17
src/core/math.ts Normal file
View File

@@ -0,0 +1,17 @@
import type { Vec2 } from "./types";
export function seededRandom(seed: number): () => number {
let state = seed;
return () => {
state = (state * 1103515245 + 12345) & 0x7fffffff;
return state / 0x7fffffff;
};
}
export function manhattan(a: Vec2, b: Vec2): number {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
export function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}

View File

@@ -52,11 +52,3 @@ export type World = {
actors: Map<EntityId, Actor>;
exit: Vec2;
};
// Import constants from config
import { GAME_CONFIG } from "./config/GameConfig";
export const TILE_SIZE = GAME_CONFIG.rendering.tileSize;
export const ENERGY_THRESHOLD = GAME_CONFIG.gameplay.energyThreshold;
export const ACTION_COST = GAME_CONFIG.gameplay.actionCost;

7
src/core/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
export function key(x: number, y: number): string {
return `${x},${y}`;
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { generateWorld } from '../generator';
import { isWall, inBounds } from '../world';
import { generateWorld } from '../world/generator';
import { isWall, inBounds } from '../world/world-logic';
describe('World Generator', () => {
describe('generateWorld', () => {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { applyAction } from '../simulation';
import { type World, type Actor, type EntityId } from '../types';
import { applyAction } from '../simulation/simulation';
import { type World, type Actor, type EntityId } from '../../core/types';
describe('Combat Simulation', () => {
const createTestWorld = (actors: Map<EntityId, Actor>): World => ({

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { idx, inBounds, isWall, isBlocked } from '../world';
import { type World, type Tile } from '../types';
import { idx, inBounds, isWall, isBlocked } from '../world/world-logic';
import { type World, type Tile } from '../../core/types';
describe('World Utilities', () => {
const createTestWorld = (width: number, height: number, tiles: Tile[]): World => ({

View File

@@ -1,6 +1,6 @@
import { ACTION_COST, ENERGY_THRESHOLD } from "./types";
import type { World, EntityId, Action, SimEvent, Actor } from "./types";
import { isBlocked } from "./world";
import { ACTION_COST, ENERGY_THRESHOLD } from "../../core/constants";
import type { World, EntityId, Action, SimEvent, Actor } from "../../core/types";
import { isBlocked } from "../world/world-logic";
export function applyAction(w: World, actorId: EntityId, action: Action): SimEvent[] {
const actor = w.actors.get(actorId);
@@ -8,54 +8,17 @@ export function applyAction(w: World, actorId: EntityId, action: Action): SimEve
const events: SimEvent[] = [];
if (action.type === "move") {
const from = { ...actor.pos };
const nx = actor.pos.x + action.dx;
const ny = actor.pos.y + action.dy;
if (!isBlocked(w, nx, ny)) {
actor.pos.x = nx;
actor.pos.y = ny;
const to = { ...actor.pos };
events.push({ type: "moved", actorId, from, to });
} else {
switch (action.type) {
case "move":
events.push(...handleMove(w, actor, action));
break;
case "attack":
events.push(...handleAttack(w, actor, action));
break;
case "wait":
default:
events.push({ type: "waited", actorId });
}
} else if (action.type === "attack") {
console.log("Sim: Processing Attack on", action.targetId);
const target = w.actors.get(action.targetId);
if (target && target.stats && actor.stats) {
events.push({ type: "attacked", attackerId: actorId, targetId: action.targetId });
const dmg = Math.max(1, actor.stats.attack - target.stats.defense);
console.log("Sim: calculated damage:", dmg);
target.stats.hp -= dmg;
events.push({
type: "damaged",
targetId: action.targetId,
amount: dmg,
hp: target.stats.hp,
x: target.pos.x,
y: target.pos.y
});
if (target.stats.hp <= 0) {
events.push({
type: "killed",
targetId: target.id,
killerId: actorId,
x: target.pos.x,
y: target.pos.y,
victimType: target.type
});
w.actors.delete(target.id);
}
} else {
events.push({ type: "waited", actorId }); // Missed or invalid target
}
} else {
events.push({ type: "waited", actorId });
break;
}
// Spend energy for any action (move/wait/attack)
@@ -64,9 +27,57 @@ export function applyAction(w: World, actorId: EntityId, action: Action): SimEve
return events;
}
function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }): SimEvent[] {
const from = { ...actor.pos };
const nx = actor.pos.x + action.dx;
const ny = actor.pos.y + action.dy;
if (!isBlocked(w, nx, ny)) {
actor.pos.x = nx;
actor.pos.y = ny;
const to = { ...actor.pos };
return [{ type: "moved", actorId: actor.id, from, to }];
} else {
return [{ type: "waited", actorId: actor.id }];
}
}
function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): SimEvent[] {
const target = w.actors.get(action.targetId);
if (target && target.stats && actor.stats) {
const events: SimEvent[] = [{ type: "attacked", attackerId: actor.id, targetId: action.targetId }];
const dmg = Math.max(1, actor.stats.attack - target.stats.defense);
target.stats.hp -= dmg;
events.push({
type: "damaged",
targetId: action.targetId,
amount: dmg,
hp: target.stats.hp,
x: target.pos.x,
y: target.pos.y
});
if (target.stats.hp <= 0) {
events.push({
type: "killed",
targetId: target.id,
killerId: actor.id,
x: target.pos.x,
y: target.pos.y,
victimType: target.type
});
w.actors.delete(target.id);
}
return events;
}
return [{ type: "waited", actorId: actor.id }];
}
/**
* Very basic enemy AI:
* - if adjacent to player, "wait" (placeholder for attack)
* - if adjacent to player, attack
* - else step toward player using greedy Manhattan
*/
export function decideEnemyAction(w: World, enemy: Actor, player: Actor): Action {

View File

@@ -1,6 +1,7 @@
import { type World, type EntityId, type RunState, type Tile, type Actor, type Vec2 } from "./types";
import { idx } from "./world";
import { GAME_CONFIG } from "./config/GameConfig";
import { type World, type EntityId, type RunState, type Tile, type Actor, type Vec2 } from "../../core/types";
import { idx } from "./world-logic";
import { GAME_CONFIG } from "../../core/config/GameConfig";
import { seededRandom } from "../../core/math";
interface Room {
x: number;
@@ -9,14 +10,6 @@ interface Room {
height: number;
}
function seededRandom(seed: number): () => number {
let state = seed;
return () => {
state = (state * 1103515245 + 12345) & 0x7fffffff;
return state / 0x7fffffff;
};
}
/**
* Generates a procedural dungeon world with rooms and corridors
* @param level The level number (affects difficulty and randomness seed)
@@ -28,74 +21,9 @@ export function generateWorld(level: number, runState: RunState): { world: World
const height = GAME_CONFIG.map.height;
const tiles: Tile[] = new Array(width * height).fill(1); // Start with all walls
const fakeWorldForIdx: World = { width, height, tiles, actors: new Map(), exit: { x: 0, y: 0 } };
const random = seededRandom(level * 12345);
// Generate rooms
const rooms: Room[] = [];
const numRooms = GAME_CONFIG.map.minRooms + Math.floor(random() * (GAME_CONFIG.map.maxRooms - GAME_CONFIG.map.minRooms + 1));
for (let i = 0; i < numRooms; i++) {
const roomWidth = GAME_CONFIG.map.roomMinWidth + Math.floor(random() * (GAME_CONFIG.map.roomMaxWidth - GAME_CONFIG.map.roomMinWidth + 1));
const roomHeight = GAME_CONFIG.map.roomMinHeight + Math.floor(random() * (GAME_CONFIG.map.roomMaxHeight - GAME_CONFIG.map.roomMinHeight + 1));
const roomX = 1 + Math.floor(random() * (width - roomWidth - 2));
const roomY = 1 + Math.floor(random() * (height - roomHeight - 2));
const newRoom: Room = { x: roomX, y: roomY, width: roomWidth, height: roomHeight };
// Check if room overlaps with existing rooms
let overlaps = false;
for (const room of rooms) {
if (
newRoom.x < room.x + room.width + 1 &&
newRoom.x + newRoom.width + 1 > room.x &&
newRoom.y < room.y + room.height + 1 &&
newRoom.y + newRoom.height + 1 > room.y
) {
overlaps = true;
break;
}
}
if (!overlaps) {
// Carve out the room
for (let x = newRoom.x; x < newRoom.x + newRoom.width; x++) {
for (let y = newRoom.y; y < newRoom.y + newRoom.height; y++) {
tiles[idx(fakeWorldForIdx, x, y)] = 0;
}
}
// Connect to previous room with a corridor
if (rooms.length > 0) {
const prevRoom = rooms[rooms.length - 1];
const prevCenterX = Math.floor(prevRoom.x + prevRoom.width / 2);
const prevCenterY = Math.floor(prevRoom.y + prevRoom.height / 2);
const newCenterX = Math.floor(newRoom.x + newRoom.width / 2);
const newCenterY = Math.floor(newRoom.y + newRoom.height / 2);
// Create L-shaped corridor
if (random() < 0.5) {
// Horizontal then vertical
for (let x = Math.min(prevCenterX, newCenterX); x <= Math.max(prevCenterX, newCenterX); x++) {
tiles[idx(fakeWorldForIdx, x, prevCenterY)] = 0;
}
for (let y = Math.min(prevCenterY, newCenterY); y <= Math.max(prevCenterY, newCenterY); y++) {
tiles[idx(fakeWorldForIdx, newCenterX, y)] = 0;
}
} else {
// Vertical then horizontal
for (let y = Math.min(prevCenterY, newCenterY); y <= Math.max(prevCenterY, newCenterY); y++) {
tiles[idx(fakeWorldForIdx, prevCenterX, y)] = 0;
}
for (let x = Math.min(prevCenterX, newCenterX); x <= Math.max(prevCenterX, newCenterX); x++) {
tiles[idx(fakeWorldForIdx, x, newCenterY)] = 0;
}
}
}
rooms.push(newRoom);
}
}
const rooms = generateRooms(width, height, tiles, random);
// Place player in first room
const firstRoom = rooms[0];
@@ -122,7 +50,86 @@ export function generateWorld(level: number, runState: RunState): { world: World
inventory: { gold: runState.inventory.gold, items: [...runState.inventory.items] }
});
// Place enemies in random rooms (skip first room with player)
placeEnemies(level, rooms, actors, random);
return { world: { width, height, tiles, actors, exit }, playerId };
}
function generateRooms(width: number, height: number, tiles: Tile[], random: () => number): Room[] {
const rooms: Room[] = [];
const numRooms = GAME_CONFIG.map.minRooms + Math.floor(random() * (GAME_CONFIG.map.maxRooms - GAME_CONFIG.map.minRooms + 1));
const fakeWorldForIdx = { width, height };
for (let i = 0; i < numRooms; i++) {
const roomWidth = GAME_CONFIG.map.roomMinWidth + Math.floor(random() * (GAME_CONFIG.map.roomMaxWidth - GAME_CONFIG.map.roomMinWidth + 1));
const roomHeight = GAME_CONFIG.map.roomMinHeight + Math.floor(random() * (GAME_CONFIG.map.roomMaxHeight - GAME_CONFIG.map.roomMinHeight + 1));
const roomX = 1 + Math.floor(random() * (width - roomWidth - 2));
const roomY = 1 + Math.floor(random() * (height - roomHeight - 2));
const newRoom: Room = { x: roomX, y: roomY, width: roomWidth, height: roomHeight };
if (!doesOverlap(newRoom, rooms)) {
carveRoom(newRoom, tiles, fakeWorldForIdx);
if (rooms.length > 0) {
carveCorridor(rooms[rooms.length - 1], newRoom, tiles, fakeWorldForIdx, random);
}
rooms.push(newRoom);
}
}
return rooms;
}
function doesOverlap(newRoom: Room, rooms: Room[]): boolean {
for (const room of rooms) {
if (
newRoom.x < room.x + room.width + 1 &&
newRoom.x + newRoom.width + 1 > room.x &&
newRoom.y < room.y + room.height + 1 &&
newRoom.y + newRoom.height + 1 > room.y
) {
return true;
}
}
return false;
}
function carveRoom(room: Room, tiles: Tile[], world: any): void {
for (let x = room.x; x < room.x + room.width; x++) {
for (let y = room.y; y < room.y + room.height; y++) {
tiles[idx(world, x, y)] = 0;
}
}
}
function carveCorridor(room1: Room, room2: Room, tiles: Tile[], world: any, random: () => number): void {
const x1 = Math.floor(room1.x + room1.width / 2);
const y1 = Math.floor(room1.y + room1.height / 2);
const x2 = Math.floor(room2.x + room2.width / 2);
const y2 = Math.floor(room2.y + room2.height / 2);
if (random() < 0.5) {
// Horizontal then vertical
for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
tiles[idx(world, x, y1)] = 0;
}
for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) {
tiles[idx(world, x2, y)] = 0;
}
} else {
// Vertical then horizontal
for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) {
tiles[idx(world, x1, y)] = 0;
}
for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
tiles[idx(world, x, y2)] = 0;
}
}
}
function placeEnemies(level: number, rooms: Room[], actors: Map<EntityId, Actor>, random: () => number): void {
let enemyId = 2;
const numEnemies = GAME_CONFIG.enemy.baseCount + level * GAME_CONFIG.enemy.baseCountPerLevel + Math.floor(random() * GAME_CONFIG.enemy.randomBonus);
@@ -133,7 +140,6 @@ export function generateWorld(level: number, runState: RunState): { world: World
const enemyX = room.x + 1 + Math.floor(random() * (room.width - 2));
const enemyY = room.y + 1 + Math.floor(random() * (room.height - 2));
// Vary enemy stats by level
const baseHp = GAME_CONFIG.enemy.baseHp + level * GAME_CONFIG.enemy.baseHpPerLevel;
const baseAttack = GAME_CONFIG.enemy.baseAttack + Math.floor(level / 2) * GAME_CONFIG.enemy.attackPerTwoLevels;
@@ -153,9 +159,6 @@ export function generateWorld(level: number, runState: RunState): { world: World
});
enemyId++;
}
return { world: { width, height, tiles, actors, exit }, playerId };
}
export const makeTestWorld = generateWorld;

View File

@@ -1,6 +1,7 @@
import type { World, Vec2 } from "./types";
import { key, manhattan } from "./utils";
import { inBounds, isWall, isBlocked, idx } from "./world";
import type { World, Vec2 } from "../../core/types";
import { key } from "../../core/utils";
import { manhattan } from "../../core/math";
import { inBounds, isWall, isBlocked, idx } from "./world-logic";
/**
* Simple 4-dir A* pathfinding.

View File

@@ -1,18 +1,18 @@
import type { World, EntityId } from "./types";
import type { World, EntityId } from "../../core/types";
export function inBounds(w: World, x: number, y: number) {
export function inBounds(w: World, x: number, y: number): boolean {
return x >= 0 && y >= 0 && x < w.width && y < w.height;
}
export function idx(w: World, x: number, y: number) {
export function idx(w: World, x: number, y: number): number {
return y * w.width + x;
}
export function isWall(w: World, x: number, y: number) {
export function isWall(w: World, x: number, y: number): boolean {
return w.tiles[idx(w, x, y)] === 1;
}
export function isBlocked(w: World, x: number, y: number) {
export function isBlocked(w: World, x: number, y: number): boolean {
if (!inBounds(w, x, y)) return true;
if (isWall(w, x, y)) return true;
@@ -22,7 +22,7 @@ export function isBlocked(w: World, x: number, y: number) {
return false;
}
export function isPlayerOnExit(w: World, playerId: EntityId) {
export function isPlayerOnExit(w: World, playerId: EntityId): boolean {
const p = w.actors.get(playerId);
if (!p) return false;
return p.pos.x === w.exit.x && p.pos.y === w.exit.y;

View File

@@ -1,9 +0,0 @@
import type { Vec2 } from "./types";
export function key(x: number, y: number) {
return `${x},${y}`;
}
export function manhattan(a: Vec2, b: Vec2) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}

View File

@@ -1,4 +1,5 @@
import GameUI from "./scenes/GameUI";
import Phaser from "phaser";
import GameUI from "./ui/GameUI";
import { GameScene } from "./scenes/GameScene";
import { SplashScene } from "./scenes/SplashScene";
import { StartScene } from "./scenes/StartScene";

View File

@@ -1,14 +1,16 @@
import Phaser from "phaser";
import { FOV } from "rot-js";
import { type World, type EntityId, type Vec2, TILE_SIZE } from "../game/types";
import { idx, inBounds, isWall } from "../game/world";
import { GAME_CONFIG } from "../game/config/GameConfig";
import { type World, type EntityId, type Vec2 } from "../core/types";
import { TILE_SIZE } from "../core/constants";
import { idx, inBounds, isWall } from "../engine/world/world-logic";
import { GAME_CONFIG } from "../core/config/GameConfig";
export class DungeonRenderer {
private scene: Phaser.Scene;
private gfx: Phaser.GameObjects.Graphics;
private playerSprite?: Phaser.GameObjects.Sprite;
private enemySprites: Map<EntityId, Phaser.GameObjects.Sprite> = new Map();
private corpseSprites: Phaser.GameObjects.Sprite[] = [];
// FOV
private fov!: any;
@@ -62,6 +64,12 @@ export class DungeonRenderer {
this.visible = new Uint8Array(this.world.width * this.world.height);
this.visibleStrength = new Float32Array(this.world.width * this.world.height);
// Clear old corpses
for (const sprite of this.corpseSprites) {
sprite.destroy();
}
this.corpseSprites = [];
// Setup player sprite
if (!this.playerSprite) {
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
@@ -423,5 +431,28 @@ export class DungeonRenderer {
corpse.setDepth(50);
corpse.setScale(TILE_SIZE / 15);
corpse.play(`${textureKey}-die`);
this.corpseSprites.push(corpse);
}
showWait(x: number, y: number) {
const screenX = x * TILE_SIZE + TILE_SIZE / 2;
const screenY = y * TILE_SIZE;
const text = this.scene.add.text(screenX, screenY, "zZz", {
fontSize: "14px",
color: "#aaaaff",
stroke: "#000",
strokeThickness: 2,
fontStyle: "bold"
}).setOrigin(0.5, 1).setDepth(200);
this.scene.tweens.add({
targets: text,
y: screenY - 20,
alpha: 0,
duration: 600,
ease: "Power1",
onComplete: () => text.destroy()
});
}
}

View File

@@ -0,0 +1,130 @@
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(),
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(),
},
};
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 level initialization', () => {
renderer.initializeLevel(mockWorld);
// 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 level again (changing level)
renderer.initializeLevel(mockWorld);
// Verify destroy was called on both corpse sprites
expect(corpse1.destroy).toHaveBeenCalledTimes(1);
expect(corpse2.destroy).toHaveBeenCalledTimes(1);
});
});

View File

@@ -4,15 +4,15 @@ import {
type Vec2,
type Action,
type RunState,
type World,
TILE_SIZE
} from "../game/types";
import { inBounds, isBlocked, isPlayerOnExit } from "../game/world";
import { findPathAStar } from "../game/pathfinding";
import { applyAction, stepUntilPlayerTurn } from "../game/simulation";
import { makeTestWorld } from "../game/generator";
import { DungeonRenderer } from "./DungeonRenderer";
import { GAME_CONFIG } from "../game/config/GameConfig";
type World
} from "../core/types";
import { TILE_SIZE } from "../core/constants";
import { inBounds, isBlocked, isPlayerOnExit } from "../engine/world/world-logic";
import { findPathAStar } from "../engine/world/pathfinding";
import { applyAction, stepUntilPlayerTurn } from "../engine/simulation/simulation";
import { makeTestWorld } from "../engine/world/generator";
import { DungeonRenderer } from "../rendering/DungeonRenderer";
import { GAME_CONFIG } from "../core/config/GameConfig";
export class GameScene extends Phaser.Scene {
private world!: World;
@@ -87,6 +87,12 @@ export class GameScene extends Phaser.Scene {
this.dungeonRenderer.toggleMinimap();
});
this.input.keyboard?.on("keydown-SPACE", () => {
if (!this.awaitingPlayer) return;
if (this.isMenuOpen || this.dungeonRenderer.isMinimapVisible()) return;
this.commitPlayerAction({ type: "wait" });
});
// Listen for Map button click from UI
this.events.on("toggle-minimap", () => {
this.dungeonRenderer.toggleMinimap();
@@ -176,17 +182,14 @@ export class GameScene extends Phaser.Scene {
else if (Phaser.Input.Keyboard.JustDown(this.cursors.down!)) dy = 1;
if (dx !== 0 || dy !== 0) {
console.log("Input: ", dx, dy);
const player = this.world.actors.get(this.playerId)!;
const targetX = player.pos.x + dx;
const targetY = player.pos.y + dy;
console.log("Target: ", targetX, targetY);
// Check for enemy at target position
const targetId = [...this.world.actors.values()].find(
a => a.pos.x === targetX && a.pos.y === targetY && !a.isPlayer
)?.id;
console.log("Found Target ID:", targetId);
if (targetId !== undefined) {
action = { type: "attack", targetId };
@@ -218,14 +221,16 @@ export class GameScene extends Phaser.Scene {
// Process events for visual fx
const allEvents = [...playerEvents, ...enemyStep.events];
if (allEvents.length > 0) console.log("Events:", allEvents);
for (const ev of allEvents) {
if (ev.type === "damaged") {
console.log("Showing damage:", ev.amount, "at", ev.x, ev.y);
this.dungeonRenderer.showDamage(ev.x, ev.y, ev.amount);
} else if (ev.type === "killed") {
console.log("Showing corpse for:", ev.victimType, "at", ev.x, ev.y);
this.dungeonRenderer.spawnCorpse(ev.x, ev.y, ev.victimType || "rat");
} else if (ev.type === "waited" && ev.actorId === this.playerId) {
const player = this.world.actors.get(this.playerId);
if (player) {
this.dungeonRenderer.showWait(player.pos.x, player.pos.y);
}
}
}

View File

@@ -1,6 +1,6 @@
import Phaser from "phaser";
import { type World, type EntityId } from "../game/types";
import { GAME_CONFIG } from "../game/config/GameConfig";
import { type World, type EntityId } from "../core/types";
import { GAME_CONFIG } from "../core/config/GameConfig";
export default class GameUI extends Phaser.Scene {
// HUD
@@ -29,11 +29,6 @@ export default class GameUI extends Phaser.Scene {
this.updateUI(data.world, data.playerId, data.levelIndex);
});
// Also listen for toggle request if needed, or stick to inputs in GameScene?
// GameScene handles Input 'I' -> calls events.emit('toggle-menu')?
// Or GameUI handles input?
// Let's keep input in GameScene for now to avoid conflicts, or move 'I' here.
// If 'I' is pressed, GameScene might need to know if menu is open (to pause).
gameScene.events.on("toggle-menu", () => this.toggleMenu());
gameScene.events.on("close-menu", () => this.setMenuOpen(false));
}