Add tests and move constants to config

This commit is contained in:
Peter Stockings
2026-01-04 10:27:27 +11:00
parent 6e3763a17b
commit f3607bc167
9 changed files with 484 additions and 35 deletions

View File

@@ -0,0 +1,136 @@
import { describe, it, expect } from 'vitest';
import { generateWorld } from '../generator';
import { isWall, inBounds } from '../world';
describe('World Generator', () => {
describe('generateWorld', () => {
it('should generate a world with correct dimensions', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world } = generateWorld(1, runState);
expect(world.width).toBe(60);
expect(world.height).toBe(40);
expect(world.tiles.length).toBe(60 * 40);
});
it('should place player actor', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world, playerId } = generateWorld(1, runState);
expect(playerId).toBe(1);
const player = world.actors.get(playerId);
expect(player).toBeDefined();
expect(player?.isPlayer).toBe(true);
expect(player?.stats).toEqual(runState.stats);
});
it('should create walkable rooms', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world, playerId } = generateWorld(1, runState);
const player = world.actors.get(playerId)!;
// Player should spawn in a walkable area
expect(isWall(world, player.pos.x, player.pos.y)).toBe(false);
});
it('should place exit in valid location', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world } = generateWorld(1, runState);
expect(inBounds(world, world.exit.x, world.exit.y)).toBe(true);
// Exit should be on a floor tile
expect(isWall(world, world.exit.x, world.exit.y)).toBe(false);
});
it('should create enemies', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world, playerId } = generateWorld(1, runState);
// Should have player + enemies
expect(world.actors.size).toBeGreaterThan(1);
// All non-player actors should be enemies
const enemies = Array.from(world.actors.values()).filter(a => !a.isPlayer);
expect(enemies.length).toBeGreaterThan(0);
// Enemies should have stats
enemies.forEach(enemy => {
expect(enemy.stats).toBeDefined();
expect(enemy.stats!.hp).toBeGreaterThan(0);
expect(enemy.stats!.attack).toBeGreaterThan(0);
});
});
it('should generate deterministic maps for same level', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world: world1, playerId: player1 } = generateWorld(1, runState);
const { world: world2, playerId: player2 } = generateWorld(1, runState);
// Same level should generate identical layouts
expect(world1.tiles).toEqual(world2.tiles);
expect(world1.exit).toEqual(world2.exit);
const player1Pos = world1.actors.get(player1)!.pos;
const player2Pos = world2.actors.get(player2)!.pos;
expect(player1Pos).toEqual(player2Pos);
});
it('should generate different maps for different levels', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world: world1 } = generateWorld(1, runState);
const { world: world2 } = generateWorld(2, runState);
// Different levels should have different layouts
expect(world1.tiles).not.toEqual(world2.tiles);
});
it('should scale enemy difficulty with level', () => {
const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
inventory: { gold: 0, items: [] }
};
const { world: world1 } = generateWorld(1, runState);
const { world: world5 } = generateWorld(5, runState);
const enemies1 = Array.from(world1.actors.values()).filter(a => !a.isPlayer);
const enemies5 = Array.from(world5.actors.values()).filter(a => !a.isPlayer);
// Higher level should have more enemies
expect(enemies5.length).toBeGreaterThan(enemies1.length);
// Higher level enemies should have higher stats
const avgHp1 = enemies1.reduce((sum, e) => sum + (e.stats?.hp || 0), 0) / enemies1.length;
const avgHp5 = enemies5.reduce((sum, e) => sum + (e.stats?.hp || 0), 0) / enemies5.length;
expect(avgHp5).toBeGreaterThan(avgHp1);
});
});
});

View File

@@ -0,0 +1,125 @@
import { describe, it, expect } from 'vitest';
import { applyAction } from '../simulation';
import { type World, type Actor, type EntityId } from '../types';
describe('Combat Simulation', () => {
const createTestWorld = (actors: Map<EntityId, Actor>): World => ({
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors,
exit: { x: 9, y: 9 }
});
describe('applyAction - attack', () => {
it('should deal damage when player attacks enemy', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 }
});
actors.set(2, {
id: 2,
isPlayer: false,
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 1 }
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: 'attack', targetId: 2 });
const enemy = world.actors.get(2)!;
expect(enemy.hp).toBeLessThan(10);
// Should have attack event
expect(events.some(e => e.type === 'attacked')).toBe(true);
});
it('should kill enemy when damage exceeds hp', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 50, defense: 2 }
});
actors.set(2, {
id: 2,
isPlayer: false,
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 1 }
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: 'attack', targetId: 2 });
// Enemy should be removed from world
expect(world.actors.has(2)).toBe(false);
// Should have killed event
expect(events.some(e => e.type === 'killed')).toBe(true);
});
it('should apply defense to reduce damage', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 }
});
actors.set(2, {
id: 2,
isPlayer: false,
pos: { x: 4, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 3 }
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: 'attack', targetId: 2 });
const enemy = world.actors.get(2)!;
const damage = 10 - enemy.stats!. hp;
// Damage should be reduced by defense (5 attack - 3 defense = 2 damage)
expect(damage).toBe(2);
});
});
describe('applyAction - move', () => {
it('should move actor to new position', () => {
const actors = new Map<EntityId, Actor>();
actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 }
});
const world = createTestWorld(actors);
const events = applyAction(world, 1, { type: 'move', dx: 1, dy: 0 });
const player = world.actors.get(1)!;
expect(player.pos).toEqual({ x: 4, y: 3 });
// Should have moved event
expect(events.some(e => e.type === 'moved')).toBe(true);
});
});
});

View File

@@ -0,0 +1,110 @@
import { describe, it, expect } from 'vitest';
import { idx, inBounds, isWall, isBlocked } from '../world';
import { type World, type Tile } from '../types';
describe('World Utilities', () => {
const createTestWorld = (width: number, height: number, tiles: Tile[]): World => ({
width,
height,
tiles,
actors: new Map(),
exit: { x: 0, y: 0 }
});
describe('idx', () => {
it('should calculate correct index for 2D coordinates', () => {
const world = createTestWorld(10, 10, []);
expect(idx(world, 0, 0)).toBe(0);
expect(idx(world, 5, 0)).toBe(5);
expect(idx(world, 0, 1)).toBe(10);
expect(idx(world, 5, 3)).toBe(35);
});
});
describe('inBounds', () => {
it('should return true for coordinates within bounds', () => {
const world = createTestWorld(10, 10, []);
expect(inBounds(world, 0, 0)).toBe(true);
expect(inBounds(world, 5, 5)).toBe(true);
expect(inBounds(world, 9, 9)).toBe(true);
});
it('should return false for coordinates outside bounds', () => {
const world = createTestWorld(10, 10, []);
expect(inBounds(world, -1, 0)).toBe(false);
expect(inBounds(world, 0, -1)).toBe(false);
expect(inBounds(world, 10, 0)).toBe(false);
expect(inBounds(world, 0, 10)).toBe(false);
expect(inBounds(world, 11, 11)).toBe(false);
});
});
describe('isWall', () => {
it('should return true for wall tiles', () => {
const tiles: Tile[] = new Array(100).fill(0);
tiles[0] = 1; // wall at 0,0
tiles[55] = 1; // wall at 5,5
const world = createTestWorld(10, 10, tiles);
expect(isWall(world, 0, 0)).toBe(true);
expect(isWall(world, 5, 5)).toBe(true);
});
it('should return false for floor tiles', () => {
const tiles: Tile[] = new Array(100).fill(0);
const world = createTestWorld(10, 10, tiles);
expect(isWall(world, 3, 3)).toBe(false);
expect(isWall(world, 7, 7)).toBe(false);
});
it('should return false for out of bounds coordinates', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
expect(isWall(world, -1, 0)).toBe(false);
expect(isWall(world, 10, 10)).toBe(false);
});
});
describe('isBlocked', () => {
it('should return true for walls', () => {
const tiles: Tile[] = new Array(100).fill(0);
tiles[55] = 1; // wall at 5,5
const world = createTestWorld(10, 10, tiles);
expect(isBlocked(world, 5, 5)).toBe(true);
});
it('should return true for actor positions', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
world.actors.set(1, {
id: 1,
isPlayer: true,
pos: { x: 3, y: 3 },
speed: 100,
energy: 0
});
expect(isBlocked(world, 3, 3)).toBe(true);
});
it('should return false for empty floor tiles', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
expect(isBlocked(world, 3, 3)).toBe(false);
expect(isBlocked(world, 7, 7)).toBe(false);
});
it('should return true for out of bounds', () => {
const world = createTestWorld(10, 10, new Array(100).fill(0));
expect(isBlocked(world, -1, 0)).toBe(true);
expect(isBlocked(world, 10, 10)).toBe(true);
});
});
});

View File

@@ -0,0 +1,62 @@
export const GAME_CONFIG = {
player: {
initialStats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
speed: 100,
viewRadius: 8
},
map: {
width: 60,
height: 40,
minRooms: 8,
maxRooms: 13,
roomMinWidth: 5,
roomMaxWidth: 12,
roomMinHeight: 4,
roomMaxHeight: 10
},
enemy: {
baseHpPerLevel: 2,
baseHp: 8,
baseAttack: 3,
attackPerTwoLevels: 1,
minSpeed: 80,
maxSpeed: 130,
maxDefense: 2,
baseCountPerLevel: 1,
baseCount: 3,
randomBonus: 4
},
rendering: {
tileSize: 24,
cameraZoom: 2,
wallColor: 0x2b2b2b,
floorColor: 0x161616,
exitColor: 0xffd166,
playerColor: 0x66ff66,
enemyColor: 0xff6666,
pathPreviewColor: 0x3355ff,
fogAlphaFloor: 0.15,
fogAlphaWall: 0.35,
visibleMinAlpha: 0.35,
visibleMaxAlpha: 1.0,
visibleStrengthFactor: 0.65
},
ui: {
minimapPanelWidth: 340,
minimapPanelHeight: 220,
minimapPadding: 20,
menuPanelWidth: 340,
menuPanelHeight: 220
},
gameplay: {
energyThreshold: 100,
actionCost: 100
}
} as const;
export type GameConfig = typeof GAME_CONFIG;

View File

@@ -1,5 +1,6 @@
import { type World, type EntityId, type RunState, type Tile, type Actor, type Vec2 } from "./types"; import { type World, type EntityId, type RunState, type Tile, type Actor, type Vec2 } from "./types";
import { idx } from "./world"; import { idx } from "./world";
import { GAME_CONFIG } from "./config/GameConfig";
interface Room { interface Room {
x: number; x: number;
@@ -16,9 +17,15 @@ function seededRandom(seed: number): () => number {
}; };
} }
export function makeTestWorld(level: number, runState: RunState): { world: World; playerId: EntityId } { /**
const width = 60; * Generates a procedural dungeon world with rooms and corridors
const height = 40; * @param level The level number (affects difficulty and randomness seed)
* @param runState Player's persistent state across levels
* @returns Generated world and player ID
*/
export function generateWorld(level: number, runState: RunState): { world: World; playerId: EntityId } {
const width = GAME_CONFIG.map.width;
const height = GAME_CONFIG.map.height;
const tiles: Tile[] = new Array(width * height).fill(1); // Start with all walls 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 fakeWorldForIdx: World = { width, height, tiles, actors: new Map(), exit: { x: 0, y: 0 } };
@@ -26,11 +33,11 @@ export function makeTestWorld(level: number, runState: RunState): { world: World
// Generate rooms // Generate rooms
const rooms: Room[] = []; const rooms: Room[] = [];
const numRooms = 8 + Math.floor(random() * 6); // 8-13 rooms 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++) { for (let i = 0; i < numRooms; i++) {
const roomWidth = 5 + Math.floor(random() * 8); // 5-12 const roomWidth = GAME_CONFIG.map.roomMinWidth + Math.floor(random() * (GAME_CONFIG.map.roomMaxWidth - GAME_CONFIG.map.roomMinWidth + 1));
const roomHeight = 4 + Math.floor(random() * 7); // 4-10 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 roomX = 1 + Math.floor(random() * (width - roomWidth - 2));
const roomY = 1 + Math.floor(random() * (height - roomHeight - 2)); const roomY = 1 + Math.floor(random() * (height - roomHeight - 2));
@@ -108,7 +115,7 @@ export function makeTestWorld(level: number, runState: RunState): { world: World
id: playerId, id: playerId,
isPlayer: true, isPlayer: true,
pos: { x: playerX, y: playerY }, pos: { x: playerX, y: playerY },
speed: 100, speed: GAME_CONFIG.player.speed,
energy: 0, energy: 0,
stats: { ...runState.stats }, stats: { ...runState.stats },
inventory: { gold: runState.inventory.gold, items: [...runState.inventory.items] } inventory: { gold: runState.inventory.gold, items: [...runState.inventory.items] }
@@ -116,7 +123,7 @@ export function makeTestWorld(level: number, runState: RunState): { world: World
// Place enemies in random rooms (skip first room with player) // Place enemies in random rooms (skip first room with player)
let enemyId = 2; let enemyId = 2;
const numEnemies = 3 + level + Math.floor(random() * 4); // Increases with level const numEnemies = GAME_CONFIG.enemy.baseCount + level * GAME_CONFIG.enemy.baseCountPerLevel + Math.floor(random() * GAME_CONFIG.enemy.randomBonus);
for (let i = 0; i < numEnemies && i < rooms.length - 1; i++) { for (let i = 0; i < numEnemies && i < rooms.length - 1; i++) {
const roomIdx = 1 + Math.floor(random() * (rooms.length - 1)); const roomIdx = 1 + Math.floor(random() * (rooms.length - 1));
@@ -126,20 +133,20 @@ export function makeTestWorld(level: number, runState: RunState): { world: World
const enemyY = room.y + 1 + Math.floor(random() * (room.height - 2)); const enemyY = room.y + 1 + Math.floor(random() * (room.height - 2));
// Vary enemy stats by level // Vary enemy stats by level
const baseHp = 8 + level * 2; const baseHp = GAME_CONFIG.enemy.baseHp + level * GAME_CONFIG.enemy.baseHpPerLevel;
const baseAttack = 3 + Math.floor(level / 2); const baseAttack = GAME_CONFIG.enemy.baseAttack + Math.floor(level / 2) * GAME_CONFIG.enemy.attackPerTwoLevels;
actors.set(enemyId, { actors.set(enemyId, {
id: enemyId, id: enemyId,
isPlayer: false, isPlayer: false,
pos: { x: enemyX, y: enemyY }, pos: { x: enemyX, y: enemyY },
speed: 80 + Math.floor(random() * 50), // 80-130 speed speed: GAME_CONFIG.enemy.minSpeed + Math.floor(random() * (GAME_CONFIG.enemy.maxSpeed - GAME_CONFIG.enemy.minSpeed)),
energy: 0, energy: 0,
stats: { stats: {
maxHp: baseHp + Math.floor(random() * 4), maxHp: baseHp + Math.floor(random() * 4),
hp: baseHp + Math.floor(random() * 4), hp: baseHp + Math.floor(random() * 4),
attack: baseAttack + Math.floor(random() * 2), attack: baseAttack + Math.floor(random() * 2),
defense: Math.floor(random() * 3) defense: Math.floor(random() * (GAME_CONFIG.enemy.maxDefense + 1))
} }
}); });
enemyId++; enemyId++;
@@ -147,3 +154,8 @@ export function makeTestWorld(level: number, runState: RunState): { world: World
return { world: { width, height, tiles, actors, exit }, playerId }; return { world: { width, height, tiles, actors, exit }, playerId };
} }
// Backward compatibility - will be removed in Phase 2
/** @deprecated Use generateWorld instead */
export const makeTestWorld = generateWorld;

View File

@@ -53,6 +53,10 @@ export type World = {
exit: Vec2; exit: Vec2;
}; };
export const TILE_SIZE = 24; // Import constants from config
export const ENERGY_THRESHOLD = 100; import { GAME_CONFIG } from "./config/GameConfig";
export const ACTION_COST = 100;
export const TILE_SIZE = GAME_CONFIG.rendering.tileSize;
export const ENERGY_THRESHOLD = GAME_CONFIG.gameplay.energyThreshold;
export const ACTION_COST = GAME_CONFIG.gameplay.actionCost;

View File

@@ -2,6 +2,7 @@ import Phaser from "phaser";
import { FOV } from "rot-js"; import { FOV } from "rot-js";
import { type World, type EntityId, type Vec2, TILE_SIZE } from "../game/types"; import { type World, type EntityId, type Vec2, TILE_SIZE } from "../game/types";
import { idx, inBounds, isWall } from "../game/world"; import { idx, inBounds, isWall } from "../game/world";
import { GAME_CONFIG } from "../game/config/GameConfig";
export class DungeonRenderer { export class DungeonRenderer {
private scene: Phaser.Scene; private scene: Phaser.Scene;
@@ -12,7 +13,6 @@ export class DungeonRenderer {
private seen!: Uint8Array; private seen!: Uint8Array;
private visible!: Uint8Array; private visible!: Uint8Array;
private visibleStrength!: Float32Array; private visibleStrength!: Float32Array;
private viewRadius = 8;
// State refs // State refs
private world!: World; private world!: World;
@@ -21,8 +21,6 @@ export class DungeonRenderer {
private minimapGfx!: Phaser.GameObjects.Graphics; private minimapGfx!: Phaser.GameObjects.Graphics;
private minimapContainer!: Phaser.GameObjects.Container; private minimapContainer!: Phaser.GameObjects.Container;
private minimapBg!: Phaser.GameObjects.Rectangle; private minimapBg!: Phaser.GameObjects.Rectangle;
private minimapPanelWidth = 340; // Match menu size
private minimapPanelHeight = 220; // Match menu size
private minimapVisible = false; // Off by default private minimapVisible = false; // Off by default
constructor(scene: Phaser.Scene) { constructor(scene: Phaser.Scene) {
@@ -40,7 +38,7 @@ export class DungeonRenderer {
// Background panel (like menu) // Background panel (like menu)
this.minimapBg = this.scene.add this.minimapBg = this.scene.add
.rectangle(0, 0, this.minimapPanelWidth, this.minimapPanelHeight, 0x000000, 0.8) .rectangle(0, 0, GAME_CONFIG.ui.minimapPanelWidth, GAME_CONFIG.ui.minimapPanelHeight, 0x000000, 0.8)
.setStrokeStyle(1, 0xffffff, 0.9) .setStrokeStyle(1, 0xffffff, 0.9)
.setInteractive(); // Capture clicks .setInteractive(); // Capture clicks
@@ -94,7 +92,7 @@ export class DungeonRenderer {
const ox = player.pos.x; const ox = player.pos.x;
const oy = player.pos.y; const oy = player.pos.y;
this.fov.compute(ox, oy, this.viewRadius, (x: number, y: number, r: number, v: number) => { this.fov.compute(ox, oy, GAME_CONFIG.player.viewRadius, (x: number, y: number, r: number, v: number) => {
if (!inBounds(this.world, x, y)) return; if (!inBounds(this.world, x, y)) return;
const i = idx(this.world, x, y); const i = idx(this.world, x, y);
@@ -102,7 +100,7 @@ export class DungeonRenderer {
this.seen[i] = 1; this.seen[i] = 1;
// falloff: 1 at center, ~0.4 at radius edge // falloff: 1 at center, ~0.4 at radius edge
const radiusT = Phaser.Math.Clamp(r / this.viewRadius, 0, 1); const radiusT = Phaser.Math.Clamp(r / GAME_CONFIG.player.viewRadius, 0, 1);
const falloff = 1 - radiusT * 0.6; const falloff = 1 - radiusT * 0.6;
const strength = Phaser.Math.Clamp(v * falloff, 0, 1); const strength = Phaser.Math.Clamp(v * falloff, 0, 1);
@@ -139,14 +137,14 @@ export class DungeonRenderer {
} }
const wall = isWall(this.world, x, y); const wall = isWall(this.world, x, y);
const base = wall ? 0x2b2b2b : 0x161616; const base = wall ? GAME_CONFIG.rendering.wallColor : GAME_CONFIG.rendering.floorColor;
let alpha: number; let alpha: number;
if (isVis) { if (isVis) {
const s = this.visibleStrength[i]; const s = this.visibleStrength[i];
alpha = Phaser.Math.Clamp(0.35 + s * 0.65, 0.35, 1.0); alpha = Phaser.Math.Clamp(GAME_CONFIG.rendering.visibleMinAlpha + s * GAME_CONFIG.rendering.visibleStrengthFactor, GAME_CONFIG.rendering.visibleMinAlpha, GAME_CONFIG.rendering.visibleMaxAlpha);
} else { } else {
alpha = wall ? 0.35 : 0.15; alpha = wall ? GAME_CONFIG.rendering.fogAlphaWall : GAME_CONFIG.rendering.fogAlphaFloor;
} }
this.gfx.fillStyle(base, alpha); this.gfx.fillStyle(base, alpha);
@@ -160,15 +158,15 @@ export class DungeonRenderer {
const ey = this.world.exit.y; const ey = this.world.exit.y;
const i = idx(this.world, ex, ey); const i = idx(this.world, ex, ey);
if (this.seen[i] === 1) { if (this.seen[i] === 1) {
const alpha = this.visible[i] === 1 ? 1.0 : 0.35; const alpha = this.visible[i] === 1 ? 1.0 : GAME_CONFIG.rendering.visibleMinAlpha;
this.gfx.fillStyle(0xffd166, alpha); this.gfx.fillStyle(GAME_CONFIG.rendering.exitColor, alpha);
this.gfx.fillRect(ex * TILE_SIZE + 7, ey * TILE_SIZE + 7, TILE_SIZE - 14, TILE_SIZE - 14); this.gfx.fillRect(ex * TILE_SIZE + 7, ey * TILE_SIZE + 7, TILE_SIZE - 14, TILE_SIZE - 14);
} }
} }
// Path preview (seen only) // Path preview (seen only)
if (playerPath.length >= 2) { if (playerPath.length >= 2) {
this.gfx.fillStyle(0x3355ff, 0.35); this.gfx.fillStyle(GAME_CONFIG.rendering.pathPreviewColor, GAME_CONFIG.rendering.visibleMinAlpha);
for (const p of playerPath) { for (const p of playerPath) {
// We can check isSeen via internal helper or just local array since we're inside // We can check isSeen via internal helper or just local array since we're inside
const i = idx(this.world, p.x, p.y); const i = idx(this.world, p.x, p.y);
@@ -183,7 +181,7 @@ export class DungeonRenderer {
const isVis = this.visible[i] === 1; const isVis = this.visible[i] === 1;
if (!a.isPlayer && !isVis) continue; if (!a.isPlayer && !isVis) continue;
const color = a.isPlayer ? 0x66ff66 : 0xff6666; const color = a.isPlayer ? GAME_CONFIG.rendering.playerColor : GAME_CONFIG.rendering.enemyColor;
this.gfx.fillStyle(color, 1); this.gfx.fillStyle(color, 1);
this.gfx.fillRect(a.pos.x * TILE_SIZE + 4, a.pos.y * TILE_SIZE + 4, TILE_SIZE - 8, TILE_SIZE - 8); this.gfx.fillRect(a.pos.x * TILE_SIZE + 4, a.pos.y * TILE_SIZE + 4, TILE_SIZE - 8, TILE_SIZE - 8);
} }
@@ -198,9 +196,9 @@ export class DungeonRenderer {
if (!this.world) return; if (!this.world) return;
// Calculate scale to fit map within panel // Calculate scale to fit map within panel
const padding = 20; const padding = GAME_CONFIG.ui.minimapPadding;
const availableWidth = this.minimapPanelWidth - padding * 2; const availableWidth = GAME_CONFIG.ui.minimapPanelWidth - padding * 2;
const availableHeight = this.minimapPanelHeight - padding * 2; const availableHeight = GAME_CONFIG.ui.minimapPanelHeight - padding * 2;
const scaleX = availableWidth / this.world.width; const scaleX = availableWidth / this.world.width;
const scaleY = availableHeight / this.world.height; const scaleY = availableHeight / this.world.height;

View File

@@ -12,6 +12,7 @@ import { findPathAStar } from "../game/pathfinding";
import { applyAction, stepUntilPlayerTurn } from "../game/simulation"; import { applyAction, stepUntilPlayerTurn } from "../game/simulation";
import { makeTestWorld } from "../game/generator"; import { makeTestWorld } from "../game/generator";
import { DungeonRenderer } from "./DungeonRenderer"; import { DungeonRenderer } from "./DungeonRenderer";
import { GAME_CONFIG } from "../game/config/GameConfig";
export class GameScene extends Phaser.Scene { export class GameScene extends Phaser.Scene {
private world!: World; private world!: World;
@@ -20,7 +21,7 @@ export class GameScene extends Phaser.Scene {
private levelIndex = 1; private levelIndex = 1;
private runState: RunState = { private runState: RunState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 }, stats: { ...GAME_CONFIG.player.initialStats },
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -41,7 +42,7 @@ export class GameScene extends Phaser.Scene {
this.cursors = this.input.keyboard!.createCursorKeys(); this.cursors = this.input.keyboard!.createCursorKeys();
// Camera // Camera
this.cameras.main.setZoom(2); this.cameras.main.setZoom(GAME_CONFIG.rendering.cameraZoom);
// Initialize Sub-systems // Initialize Sub-systems
this.dungeonRenderer = new DungeonRenderer(this); this.dungeonRenderer = new DungeonRenderer(this);

View File

@@ -1,5 +1,6 @@
import Phaser from "phaser"; import Phaser from "phaser";
import { type World, type EntityId } from "../game/types"; import { type World, type EntityId } from "../game/types";
import { GAME_CONFIG } from "../game/config/GameConfig";
export default class GameUI extends Phaser.Scene { export default class GameUI extends Phaser.Scene {
// HUD // HUD
@@ -84,8 +85,8 @@ export default class GameUI extends Phaser.Scene {
mapBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleMap()); mapBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleMap());
// Panel (center) // Panel (center)
const panelW = 340; const panelW = GAME_CONFIG.ui.menuPanelWidth;
const panelH = 220; const panelH = GAME_CONFIG.ui.menuPanelHeight;
this.menuBg = this.add this.menuBg = this.add
.rectangle(0, 0, panelW, panelH, 0x000000, 0.8) .rectangle(0, 0, panelW, panelH, 0x000000, 0.8)