import { type World, type EntityId, type RunState, type Tile, type Actor, type Vec2 } from "../../core/types"; import { TileType } from "../../core/terrain"; import { idx } from "./world-logic"; import { GAME_CONFIG } from "../../core/config/GameConfig"; import { ITEMS } from "../../core/config/Items"; import { seededRandom } from "../../core/math"; import * as ROT from "rot-js"; interface Room { x: number; y: number; width: number; height: number; } /** * Generates a procedural dungeon world with rooms and corridors using rot-js Uniform algorithm * @param floor The floor number (affects difficulty) * @param runState Player's persistent state across floors * @returns Generated world and player ID */ export function generateWorld(floor: 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(TileType.WALL); const random = seededRandom(floor * 12345); // Set ROT's RNG seed for consistent dungeon generation ROT.RNG.setSeed(floor * 12345); const rooms = generateRooms(width, height, tiles, floor, random); // 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); const actors = new Map(); const playerId = 1; actors.set(playerId, { id: playerId, category: "combatant", isPlayer: true, type: "player", pos: { x: playerX, y: playerY }, speed: GAME_CONFIG.player.speed, stats: { ...runState.stats }, inventory: { gold: runState.inventory.gold, items: [ ...runState.inventory.items, // Add starting items for testing if empty ...(runState.inventory.items.length === 0 ? [ITEMS["health_potion"], ITEMS["health_potion"], ITEMS["iron_sword"], ITEMS["throwing_dagger"], ITEMS["throwing_dagger"], ITEMS["throwing_dagger"], ITEMS["pistol"]] : []) ] }, energy: 0 }); // Place exit in last room const lastRoom = rooms[rooms.length - 1]; const exit: Vec2 = { x: lastRoom.x + Math.floor(lastRoom.width / 2), y: lastRoom.y + Math.floor(lastRoom.height / 2) }; placeEnemies(floor, rooms, actors, random); // Place doors for dungeon levels (Uniform/Digger) // Caves (Floors 10+) shouldn't have manufactured doors if (floor <= 9) { placeDoors(width, height, tiles, rooms, random); } decorate(width, height, tiles, random, exit); // CRITICAL FIX: Ensure player start position is always clear! // Otherwise spawning in Grass (which blocks vision) makes the player blind. tiles[playerY * width + playerX] = TileType.EMPTY; return { world: { width, height, tiles, actors, exit }, playerId }; } // Update generateRooms signature to accept random function generateRooms(width: number, height: number, tiles: Tile[], floor: number, random: () => number): Room[] { const rooms: Room[] = []; // Choose dungeon algorithm based on floor depth let dungeon: any; if (floor <= 4) { // Floors 1-4: Uniform (organic, irregular rooms) dungeon = new ROT.Map.Uniform(width, height, { roomWidth: [GAME_CONFIG.map.roomMinWidth, GAME_CONFIG.map.roomMaxWidth], roomHeight: [GAME_CONFIG.map.roomMinHeight, GAME_CONFIG.map.roomMaxHeight], roomDugPercentage: 0.3, }); } else if (floor <= 9) { // Floors 5-9: Digger (traditional rectangular rooms + corridors) dungeon = new ROT.Map.Digger(width, height, { roomWidth: [GAME_CONFIG.map.roomMinWidth, GAME_CONFIG.map.roomMaxWidth], roomHeight: [GAME_CONFIG.map.roomMinHeight, GAME_CONFIG.map.roomMaxHeight], corridorLength: [2, 6], }); } else { // Floors 10+: Cellular (natural cave systems) dungeon = new ROT.Map.Cellular(width, height, { born: [4, 5, 6, 7, 8], survive: [2, 3, 4, 5], }); // Cellular needs randomization and smoothing dungeon.randomize(0.5); for (let i = 0; i < 4; i++) { dungeon.create(); } } // Generate the dungeon dungeon.create((x: number, y: number, value: number) => { if (value === 0) { // 0 = floor, 1 = wall tiles[y * width + x] = TileType.EMPTY; } }); // Extract room information from the generated dungeon const roomData = (dungeon as any).getRooms?.(); if (roomData && roomData.length > 0) { // Traditional dungeons (Uniform/Digger) have explicit rooms for (const room of roomData) { rooms.push({ x: room.getLeft(), y: room.getTop(), width: room.getRight() - room.getLeft() + 1, height: room.getBottom() - room.getTop() + 1 }); } } else { // Cellular caves don't have explicit rooms, so find connected floor areas rooms.push(...extractRoomsFromCave(width, height, tiles)); // Connect the isolated cave rooms connectRooms(width, tiles, rooms, random); } // Ensure we have at least 2 rooms for player/exit placement if (rooms.length < 2) { // Fallback: create two basic rooms rooms.push( { x: 5, y: 5, width: 5, height: 5 }, { x: width - 10, y: height - 10, width: 5, height: 5 } ); // Connect the fallback rooms connectRooms(width, tiles, rooms, random); } return rooms; } function connectRooms(width: number, tiles: Tile[], rooms: Room[], random: () => number) { for (let i = 0; i < rooms.length - 1; i++) { const r1 = rooms[i]; const r2 = rooms[i+1]; const c1x = r1.x + Math.floor(r1.width / 2); const c1y = r1.y + Math.floor(r1.height / 2); const c2x = r2.x + Math.floor(r2.width / 2); const c2y = r2.y + Math.floor(r2.height / 2); if (random() < 0.5) { digH(width, tiles, c1x, c2x, c1y); digV(width, tiles, c1y, c2y, c2x); } else { digV(width, tiles, c1y, c2y, c1x); digH(width, tiles, c1x, c2x, c2y); } } } function digH(width: number, tiles: Tile[], x1: number, x2: number, y: number) { const start = Math.min(x1, x2); const end = Math.max(x1, x2); for (let x = start; x <= end; x++) { const idx = y * width + x; if (tiles[idx] === TileType.WALL) { tiles[idx] = TileType.EMPTY; } } } function digV(width: number, tiles: Tile[], y1: number, y2: number, x: number) { const start = Math.min(y1, y2); const end = Math.max(y1, y2); for (let y = start; y <= end; y++) { const idx = y * width + x; if (tiles[idx] === TileType.WALL) { tiles[idx] = TileType.EMPTY; } } } /** * For cellular/cave maps, find clusters of floor tiles to use as "rooms" */ function extractRoomsFromCave(width: number, height: number, tiles: Tile[]): Room[] { const rooms: Room[] = []; const visited = new Set(); // Find large connected floor areas for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { const idx = y * width + x; if (tiles[idx] === TileType.EMPTY && !visited.has(idx)) { const cluster = floodFill(width, height, tiles, x, y, visited); // Only consider clusters larger than 20 tiles if (cluster.length > 20) { // Create bounding box for this cluster let minX = width, maxX = 0, minY = height, maxY = 0; for (const pos of cluster) { const cx = pos % width; const cy = Math.floor(pos / width); minX = Math.min(minX, cx); maxX = Math.max(maxX, cx); minY = Math.min(minY, cy); maxY = Math.max(maxY, cy); } rooms.push({ x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 }); } } } } return rooms; } /** * Flood fill to find connected floor tiles */ function floodFill(width: number, height: number, tiles: Tile[], startX: number, startY: number, visited: Set): number[] { const cluster: number[] = []; const queue: number[] = [startY * width + startX]; while (queue.length > 0) { const idx = queue.shift()!; if (visited.has(idx)) continue; visited.add(idx); cluster.push(idx); const x = idx % width; const y = Math.floor(idx / width); // Check 4 directions const neighbors = [ { nx: x + 1, ny: y }, { nx: x - 1, ny: y }, { nx: x, ny: y + 1 }, { nx: x, ny: y - 1 }, ]; for (const { nx, ny } of neighbors) { if (nx >= 0 && nx < width && ny >= 0 && ny < height) { const nIdx = ny * width + nx; if (tiles[nIdx] === TileType.EMPTY && !visited.has(nIdx)) { queue.push(nIdx); } } } } return cluster; } function decorate(width: number, height: number, tiles: Tile[], random: () => number, exit: Vec2): void { const world = { width, height }; // Set exit tile tiles[idx(world as any, exit.x, exit.y)] = TileType.EXIT; // Use Simplex noise for natural-looking grass distribution const grassNoise = new ROT.Noise.Simplex(); const decorationNoise = new ROT.Noise.Simplex(); // Offset noise to get different patterns for grass vs decorations const grassOffset = random() * 1000; const decorOffset = random() * 1000; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const i = idx(world as any, x, y); if (tiles[i] === TileType.EMPTY) { // Grass patches: use noise to create organic shapes const grassValue = grassNoise.get((x + grassOffset) / 15, (y + grassOffset) / 15); // Create grass patches where noise is above threshold if (grassValue > 0.35) { tiles[i] = TileType.GRASS; } else if (grassValue > 0.25) { // Transition zone: Saplings around grass clumps tiles[i] = TileType.GRASS_SAPLINGS; } else { // Floor decorations (moss/rocks): clustered distribution const decoValue = decorationNoise.get((x + decorOffset) / 8, (y + decorOffset) / 8); // Dense clusters where noise is high if (decoValue > 0.5) { tiles[i] = TileType.EMPTY_DECO; } else if (decoValue > 0.3 && random() < 0.3) { // Sparse decorations at medium noise levels tiles[i] = TileType.EMPTY_DECO; } } } } } // Wall decorations (moss near grass) for (let y = 0; y < height - 1; y++) { for (let x = 0; x < width; x++) { const i = idx(world as any, x, y); const nextY = idx(world as any, x, y + 1); if (tiles[i] === TileType.WALL && tiles[nextY] === TileType.GRASS && random() < 0.25) { tiles[i] = TileType.WALL_DECO; } } } } function placeEnemies(floor: number, rooms: Room[], actors: Map, random: () => number): void { let enemyId = 2; const numEnemies = GAME_CONFIG.enemyScaling.baseCount + floor * GAME_CONFIG.enemyScaling.baseCountPerFloor; const enemyTypes = Object.keys(GAME_CONFIG.enemies); const occupiedPositions = new Set(); for (let i = 0; i < numEnemies; i++) { // Pick a random room (not the starting room 0) const roomIdx = 1 + Math.floor(random() * (rooms.length - 1)); const room = rooms[roomIdx]; // Try to find an empty spot in the room for (let attempts = 0; attempts < 5; attempts++) { const ex = room.x + 1 + Math.floor(random() * (room.width - 2)); const ey = room.y + 1 + Math.floor(random() * (room.height - 2)); const k = `${ex},${ey}`; if (!occupiedPositions.has(k)) { const type = enemyTypes[Math.floor(random() * enemyTypes.length)] as keyof typeof GAME_CONFIG.enemies; const enemyDef = GAME_CONFIG.enemies[type]; const scaledHp = enemyDef.baseHp + floor * GAME_CONFIG.enemyScaling.hpPerFloor; const scaledAttack = enemyDef.baseAttack + Math.floor(floor / 2) * GAME_CONFIG.enemyScaling.attackPerTwoFloors; actors.set(enemyId, { id: enemyId, category: "combatant", isPlayer: false, type, pos: { x: ex, y: ey }, speed: enemyDef.minSpeed + Math.floor(random() * (enemyDef.maxSpeed - enemyDef.minSpeed)), stats: { maxHp: scaledHp + Math.floor(random() * 4), hp: scaledHp + Math.floor(random() * 4), maxMana: 0, mana: 0, attack: scaledAttack + Math.floor(random() * 2), defense: enemyDef.baseDefense, level: 0, exp: 0, expToNextLevel: 0, statPoints: 0, skillPoints: 0, strength: 0, dexterity: 0, intelligence: 0, critChance: 0, critMultiplier: 100, accuracy: 80, lifesteal: 0, evasion: 0, blockChance: 0, luck: 0, passiveNodes: [] }, energy: 0 }); occupiedPositions.add(k); enemyId++; break; } } } } export const makeTestWorld = generateWorld; function placeDoors(width: number, height: number, tiles: Tile[], rooms: Room[], random: () => number): void { const checkAndPlaceDoor = (x: number, y: number) => { const i = idx({ width, height } as any, x, y); if (tiles[i] === TileType.EMPTY) { // Found a connection (floor tile on perimeter) // 50% chance to place a door if (random() < 0.5) { // 90% chance for closed door, 10% for open tiles[i] = random() < 0.9 ? TileType.DOOR_CLOSED : TileType.DOOR_OPEN; } } }; for (const room of rooms) { // Scan top and bottom walls const topY = room.y - 1; const bottomY = room.y + room.height; // Scan horizontal perimeters (iterate x from left-1 to right+1 to cover corners too if needed, // but usually doors are in the middle segments. Let's cover the full range adjacent to room.) for (let x = room.x; x < room.x + room.width; x++) { if (topY >= 0) checkAndPlaceDoor(x, topY); if (bottomY < height) checkAndPlaceDoor(x, bottomY); } // Scan left and right walls const leftX = room.x - 1; const rightX = room.x + room.width; for (let y = room.y; y < room.y + room.height; y++) { if (leftX >= 0) checkAndPlaceDoor(leftX, y); if (rightX < width) checkAndPlaceDoor(rightX, y); } } }