Add tests and move constants to config
This commit is contained in:
136
src/game/__tests__/generator.test.ts
Normal file
136
src/game/__tests__/generator.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
125
src/game/__tests__/simulation.test.ts
Normal file
125
src/game/__tests__/simulation.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
110
src/game/__tests__/world.test.ts
Normal file
110
src/game/__tests__/world.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
62
src/game/config/GameConfig.ts
Normal file
62
src/game/config/GameConfig.ts
Normal 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;
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
|
||||
interface Room {
|
||||
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;
|
||||
const height = 40;
|
||||
/**
|
||||
* Generates a procedural dungeon world with rooms and corridors
|
||||
* @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 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
|
||||
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++) {
|
||||
const roomWidth = 5 + Math.floor(random() * 8); // 5-12
|
||||
const roomHeight = 4 + Math.floor(random() * 7); // 4-10
|
||||
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));
|
||||
|
||||
@@ -108,7 +115,7 @@ export function makeTestWorld(level: number, runState: RunState): { world: World
|
||||
id: playerId,
|
||||
isPlayer: true,
|
||||
pos: { x: playerX, y: playerY },
|
||||
speed: 100,
|
||||
speed: GAME_CONFIG.player.speed,
|
||||
energy: 0,
|
||||
stats: { ...runState.stats },
|
||||
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)
|
||||
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++) {
|
||||
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));
|
||||
|
||||
// Vary enemy stats by level
|
||||
const baseHp = 8 + level * 2;
|
||||
const baseAttack = 3 + Math.floor(level / 2);
|
||||
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;
|
||||
|
||||
actors.set(enemyId, {
|
||||
id: enemyId,
|
||||
isPlayer: false,
|
||||
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,
|
||||
stats: {
|
||||
maxHp: baseHp + Math.floor(random() * 4),
|
||||
hp: baseHp + Math.floor(random() * 4),
|
||||
attack: baseAttack + Math.floor(random() * 2),
|
||||
defense: Math.floor(random() * 3)
|
||||
defense: Math.floor(random() * (GAME_CONFIG.enemy.maxDefense + 1))
|
||||
}
|
||||
});
|
||||
enemyId++;
|
||||
@@ -147,3 +154,8 @@ export function makeTestWorld(level: number, runState: RunState): { world: World
|
||||
|
||||
return { world: { width, height, tiles, actors, exit }, playerId };
|
||||
}
|
||||
|
||||
// Backward compatibility - will be removed in Phase 2
|
||||
/** @deprecated Use generateWorld instead */
|
||||
export const makeTestWorld = generateWorld;
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ export type World = {
|
||||
exit: Vec2;
|
||||
};
|
||||
|
||||
export const TILE_SIZE = 24;
|
||||
export const ENERGY_THRESHOLD = 100;
|
||||
export const ACTION_COST = 100;
|
||||
// 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;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
|
||||
export class DungeonRenderer {
|
||||
private scene: Phaser.Scene;
|
||||
@@ -12,7 +13,6 @@ export class DungeonRenderer {
|
||||
private seen!: Uint8Array;
|
||||
private visible!: Uint8Array;
|
||||
private visibleStrength!: Float32Array;
|
||||
private viewRadius = 8;
|
||||
|
||||
// State refs
|
||||
private world!: World;
|
||||
@@ -21,8 +21,6 @@ export class DungeonRenderer {
|
||||
private minimapGfx!: Phaser.GameObjects.Graphics;
|
||||
private minimapContainer!: Phaser.GameObjects.Container;
|
||||
private minimapBg!: Phaser.GameObjects.Rectangle;
|
||||
private minimapPanelWidth = 340; // Match menu size
|
||||
private minimapPanelHeight = 220; // Match menu size
|
||||
private minimapVisible = false; // Off by default
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
@@ -40,7 +38,7 @@ export class DungeonRenderer {
|
||||
|
||||
// Background panel (like menu)
|
||||
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)
|
||||
.setInteractive(); // Capture clicks
|
||||
|
||||
@@ -94,7 +92,7 @@ export class DungeonRenderer {
|
||||
const ox = player.pos.x;
|
||||
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;
|
||||
|
||||
const i = idx(this.world, x, y);
|
||||
@@ -102,7 +100,7 @@ export class DungeonRenderer {
|
||||
this.seen[i] = 1;
|
||||
|
||||
// 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 strength = Phaser.Math.Clamp(v * falloff, 0, 1);
|
||||
|
||||
@@ -139,14 +137,14 @@ export class DungeonRenderer {
|
||||
}
|
||||
|
||||
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;
|
||||
if (isVis) {
|
||||
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 {
|
||||
alpha = wall ? 0.35 : 0.15;
|
||||
alpha = wall ? GAME_CONFIG.rendering.fogAlphaWall : GAME_CONFIG.rendering.fogAlphaFloor;
|
||||
}
|
||||
|
||||
this.gfx.fillStyle(base, alpha);
|
||||
@@ -160,15 +158,15 @@ export class DungeonRenderer {
|
||||
const ey = this.world.exit.y;
|
||||
const i = idx(this.world, ex, ey);
|
||||
if (this.seen[i] === 1) {
|
||||
const alpha = this.visible[i] === 1 ? 1.0 : 0.35;
|
||||
this.gfx.fillStyle(0xffd166, alpha);
|
||||
const alpha = this.visible[i] === 1 ? 1.0 : GAME_CONFIG.rendering.visibleMinAlpha;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Path preview (seen only)
|
||||
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) {
|
||||
// We can check isSeen via internal helper or just local array since we're inside
|
||||
const i = idx(this.world, p.x, p.y);
|
||||
@@ -183,7 +181,7 @@ export class DungeonRenderer {
|
||||
const isVis = this.visible[i] === 1;
|
||||
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.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;
|
||||
|
||||
// Calculate scale to fit map within panel
|
||||
const padding = 20;
|
||||
const availableWidth = this.minimapPanelWidth - padding * 2;
|
||||
const availableHeight = this.minimapPanelHeight - padding * 2;
|
||||
const padding = GAME_CONFIG.ui.minimapPadding;
|
||||
const availableWidth = GAME_CONFIG.ui.minimapPanelWidth - padding * 2;
|
||||
const availableHeight = GAME_CONFIG.ui.minimapPanelHeight - padding * 2;
|
||||
|
||||
const scaleX = availableWidth / this.world.width;
|
||||
const scaleY = availableHeight / this.world.height;
|
||||
|
||||
@@ -12,6 +12,7 @@ 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";
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private world!: World;
|
||||
@@ -20,7 +21,7 @@ export class GameScene extends Phaser.Scene {
|
||||
private levelIndex = 1;
|
||||
|
||||
private runState: RunState = {
|
||||
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
|
||||
stats: { ...GAME_CONFIG.player.initialStats },
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
|
||||
@@ -41,7 +42,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.cursors = this.input.keyboard!.createCursorKeys();
|
||||
|
||||
// Camera
|
||||
this.cameras.main.setZoom(2);
|
||||
this.cameras.main.setZoom(GAME_CONFIG.rendering.cameraZoom);
|
||||
|
||||
// Initialize Sub-systems
|
||||
this.dungeonRenderer = new DungeonRenderer(this);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Phaser from "phaser";
|
||||
import { type World, type EntityId } from "../game/types";
|
||||
import { GAME_CONFIG } from "../game/config/GameConfig";
|
||||
|
||||
export default class GameUI extends Phaser.Scene {
|
||||
// HUD
|
||||
@@ -84,8 +85,8 @@ export default class GameUI extends Phaser.Scene {
|
||||
mapBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleMap());
|
||||
|
||||
// Panel (center)
|
||||
const panelW = 340;
|
||||
const panelH = 220;
|
||||
const panelW = GAME_CONFIG.ui.menuPanelWidth;
|
||||
const panelH = GAME_CONFIG.ui.menuPanelHeight;
|
||||
|
||||
this.menuBg = this.add
|
||||
.rectangle(0, 0, panelW, panelH, 0x000000, 0.8)
|
||||
|
||||
Reference in New Issue
Block a user