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(); 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;