'use strict'; // Browser: use globals directly; Node: use require const _C3 = typeof require !== 'undefined' ? require('../core/constants') : { COLS, ROWS, TILE_TYPES }; const RNG = typeof require !== 'undefined' ? require('../core/rng') : { createRNG }; const Puzzles = typeof require !== 'undefined' ? require('./puzzles') : { generatePuzzles }; const Placement = typeof require !== 'undefined' ? require('./placement') : { generatePlacement }; // Abstract grid for BSP layout — scales with room count function getLayoutSize(targetCount) { const area = Math.ceil(targetCount * 36 * 1.5); const w = Math.ceil(Math.sqrt(area * 1.33)); const h = Math.ceil(area / w); return { cols: Math.ceil(w / 4) * 4, rows: Math.ceil(h / 4) * 4 }; } function generateDungeon(dungeon) { const rng = RNG.createRNG(dungeon.seed); const targetCount = dungeon.roomCount; const rooms = generateBSPLayout(targetCount, rng); if (rooms.length === 0) return []; connectRooms(rooms, rng); ensureConnectivity(rooms, rng); assignRoomTypes(rooms, dungeon, rng); // Each room gets its own COLS x ROWS tile grid for rendering for (const room of rooms) { room.tiles = createRoomTileGrid(room, rng); room.decorations = generateDecorations(room, rng); } // Generate puzzles (modifies room tiles and puzzles arrays) Puzzles.generatePuzzles(rooms, dungeon, rng); // Place enemies, items, bosses (needs tile grid from above) Placement.generatePlacement(rooms, dungeon, rng); return rooms; } function generateBSPLayout(targetCount, rng) { const layout = getLayoutSize(targetCount); const maxDepth = Math.max(6, Math.ceil(Math.log2(targetCount)) + 2); const root = createBSP(0, 0, layout.cols, layout.rows, 0, 0, targetCount, maxDepth, rng); const leaves = []; collectLeaves(root, leaves); // Sort by y then x for consistent ordering leaves.sort((a, b) => (a.cy - b.cy) * layout.cols + (a.cx - b.cx)); const rooms = []; for (const leaf of leaves) { rooms.push({ id: rooms.length, x: leaf.cx, y: leaf.cy, width: leaf.width, height: leaf.height, type: 'normal', connections: [], enemies: [], items: [], puzzles: [], decorations: [], hasBoss: false, hasExit: false, hasEntrance: false, tiles: [], }); } return rooms; } function createBSP(x, y, width, height, depth, roomCount, targetCount, maxDepth, rng) { const node = { x, y, width, height, depth, cx: x + Math.floor(width / 2), cy: y + Math.floor(height / 2), left: null, right: null }; if (roomCount >= targetCount) return node; if (depth >= maxDepth) return node; const minRegion = 5; const minSplit = minRegion * 2; const canH = height >= minSplit; const canV = width >= minSplit; if (!canH && !canV) return node; let dir; if (canH && canV) { dir = width > height ? 'v' : (height > width ? 'h' : (rng() < 0.5 ? 'h' : 'v')); } else { dir = canH ? 'h' : 'v'; } let splitPos; if (dir === 'h') { const lo = Math.max(minRegion, Math.ceil(height * 0.35)); const hi = Math.min(height - minRegion, Math.floor(height * 0.65)); if (lo > hi) return node; splitPos = Math.floor(rng() * (hi - lo + 1)) + lo; } else { const lo = Math.max(minRegion, Math.ceil(width * 0.35)); const hi = Math.min(width - minRegion, Math.floor(width * 0.65)); if (lo > hi) return node; splitPos = Math.floor(rng() * (hi - lo + 1)) + lo; } if (dir === 'h') { node.left = createBSP(x, y, width, splitPos, depth + 1, roomCount, targetCount, maxDepth, rng); const lc = countLeaves(node.left); node.right = createBSP(x, y + splitPos, width, height - splitPos, depth + 1, roomCount + lc, targetCount, maxDepth, rng); } else { node.left = createBSP(x, y, splitPos, height, depth + 1, roomCount, targetCount, maxDepth, rng); const lc = countLeaves(node.left); node.right = createBSP(x + splitPos, y, width - splitPos, height, depth + 1, roomCount + lc, targetCount, maxDepth, rng); } return node; } function countLeaves(node) { if (!node.left && !node.right) return 1; let c = 0; if (node.left) c += countLeaves(node.left); if (node.right) c += countLeaves(node.right); return c; } function collectLeaves(node, leaves) { if (!node.left && !node.right) { leaves.push({ cx: node.cx, cy: node.cy, width: node.width, height: node.height }); return; } if (node.left) collectLeaves(node.left, leaves); if (node.right) collectLeaves(node.right, leaves); } // Connect rooms using MST (Prim's algorithm) function connectRooms(rooms, rng) { if (rooms.length < 2) return; const connected = new Set([0]); const unconnected = new Set(rooms.slice(1).map(r => r.id)); while (unconnected.size > 0) { let bestDist = Infinity, bestFrom = -1, bestTo = -1; for (const fromId of connected) { for (const toId of unconnected) { const d = Math.abs(rooms[fromId].x - rooms[toId].x) + Math.abs(rooms[fromId].y - rooms[toId].y); if (d < bestDist) { bestDist = d; bestFrom = fromId; bestTo = toId; } } } if (bestTo < 0) break; rooms[bestFrom].connections.push(bestTo); rooms[bestTo].connections.push(bestFrom); connected.add(bestTo); unconnected.delete(bestTo); } // Extra connections for non-linear paths for (let i = 0; i < rooms.length; i++) { for (let j = i + 1; j < rooms.length; j++) { if (rooms[i].connections.includes(j) || rooms[j].connections.includes(i)) continue; const d = Math.abs(rooms[i].x - rooms[j].x) + Math.abs(rooms[i].y - rooms[j].y); if (d < 20 && rng() < 0.25) { rooms[i].connections.push(j); rooms[j].connections.push(i); } } } } // BFS from room 0, connect unreachable rooms to nearest connected room function ensureConnectivity(rooms, rng) { if (rooms.length < 2) return; const visited = new Set([0]); const queue = [0]; while (queue.length > 0) { const rid = queue.shift(); for (const cid of rooms[rid].connections) { if (!visited.has(cid)) { visited.add(cid); queue.push(cid); } } } for (const room of rooms) { if (visited.has(room.id)) continue; let nearest = null, nearestDist = Infinity; for (const other of rooms) { if (!visited.has(other.id)) continue; const d = Math.abs(room.x - other.x) + Math.abs(room.y - other.y); if (d < nearestDist) { nearestDist = d; nearest = other; } } if (nearest) { room.connections.push(nearest.id); nearest.connections.push(room.id); visited.add(room.id); const q2 = [room.id]; while (q2.length > 0) { const rid = q2.shift(); for (const cid of rooms[rid].connections) { if (!visited.has(cid)) { visited.add(cid); q2.push(cid); } } } } } } function assignRoomTypes(rooms, dungeon, rng) { if (rooms.length === 0) return; rooms[0].type = 'entrance'; rooms[0].hasEntrance = true; const last = rooms[rooms.length - 1]; last.type = 'boss'; last.hasBoss = true; last.bossId = dungeon.id; // Treasure (~20%) const tc = Math.max(1, Math.floor(rooms.length * 0.2)); const tcands = rooms.filter((r, i) => i > 0 && i < rooms.length - 1 && r.type === 'normal'); shuffleRng(tcands, rng); for (let i = 0; i < Math.min(tc, tcands.length); i++) tcands[i].type = 'treasure'; // Puzzle (~15%) const pc = Math.max(1, Math.floor(rooms.length * 0.15)); const pcands = rooms.filter(r => r.type === 'normal'); shuffleRng(pcands, rng); for (let i = 0; i < Math.min(pc, pcands.length); i++) pcands[i].type = 'puzzle'; // Trap (~10%) const trapc = Math.max(0, Math.floor(rooms.length * 0.1)); const trapcands = rooms.filter(r => r.type === 'normal'); shuffleRng(trapcands, rng); for (let i = 0; i < Math.min(trapc, trapcands.length); i++) trapcands[i].type = 'trap'; } function shuffleRng(arr, rng) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } function createRoomTileGrid(room, rng) { const tiles = []; for (let y = 0; y < _C3.ROWS; y++) { tiles[y] = []; for (let x = 0; x < _C3.COLS; x++) { if (y === 0 || y === _C3.ROWS - 1 || x === 0 || x === _C3.COLS - 1) tiles[y][x] = _C3.TILE_TYPES.WALL; else tiles[y][x] = _C3.TILE_TYPES.FLOOR; } } // Doors on walls based on connections for (const connId of room.connections) { const side = Math.floor(rng() * 4); let dx, dy; if (side === 0) { dx = Math.floor(rng() * (_C3.COLS - 2)) + 1; dy = 0; } else if (side === 1) { dx = Math.floor(rng() * (_C3.COLS - 2)) + 1; dy = _C3.ROWS - 1; } else if (side === 2) { dx = 0; dy = Math.floor(rng() * (_C3.ROWS - 2)) + 1; } else { dx = _C3.COLS - 1; dy = Math.floor(rng() * (_C3.ROWS - 2)) + 1; } if (tiles[dy][dx] === _C3.TILE_TYPES.WALL) tiles[dy][dx] = _C3.TILE_TYPES.DOOR; } if (room.hasBoss) tiles[Math.floor(_C3.ROWS / 2)][_C3.COLS - 1] = _C3.TILE_TYPES.EXIT; if (room.hasEntrance) tiles[Math.floor(_C3.ROWS / 2)][_C3.COLS - 1] = _C3.TILE_TYPES.DOOR; return tiles; } function generateDecorations(room, rng) { const decorations = []; if (rng() < 0.7) decorations.push({ type: 'torch', x: 2, y: 2, lit: true }); if (rng() < 0.5) decorations.push({ type: 'torch', x: _C3.COLS - 3, y: 2, lit: true }); if (rng() < 0.5) decorations.push({ type: 'torch', x: 2, y: _C3.ROWS - 3, lit: true }); if (rng() < 0.7) decorations.push({ type: 'torch', x: _C3.COLS - 3, y: _C3.ROWS - 3, lit: true }); return decorations; } if (typeof module !== 'undefined' && module.exports) { module.exports = { generateDungeon }; }