Files
rogue/src/game/generator.ts
2026-01-04 10:27:27 +11:00

162 lines
6.2 KiB
TypeScript

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;
y: number;
width: number;
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)
* @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 } };
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);
}
}
// Place player in first room
const firstRoom = rooms[0];
const playerX = firstRoom.x + Math.floor(firstRoom.width / 2);
const playerY = firstRoom.y + Math.floor(firstRoom.height / 2);
// Place exit in last room
const lastRoom = rooms[rooms.length - 1];
const exitX = lastRoom.x + Math.floor(lastRoom.width / 2);
const exitY = lastRoom.y + Math.floor(lastRoom.height / 2);
const exit: Vec2 = { x: exitX, y: exitY };
const actors = new Map<EntityId, Actor>();
const playerId = 1;
actors.set(playerId, {
id: playerId,
isPlayer: true,
pos: { x: playerX, y: playerY },
speed: GAME_CONFIG.player.speed,
energy: 0,
stats: { ...runState.stats },
inventory: { gold: runState.inventory.gold, items: [...runState.inventory.items] }
});
// Place enemies in random rooms (skip first room with player)
let enemyId = 2;
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));
const room = rooms[roomIdx];
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;
actors.set(enemyId, {
id: enemyId,
isPlayer: false,
pos: { x: enemyX, y: enemyY },
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() * (GAME_CONFIG.enemy.maxDefense + 1))
}
});
enemyId++;
}
return { world: { width, height, tiles, actors, exit }, playerId };
}
// Backward compatibility - will be removed in Phase 2
/** @deprecated Use generateWorld instead */
export const makeTestWorld = generateWorld;