diff --git a/src/babylon_setup.js b/src/babylon_setup.js index d04bf1d..306c117 100644 --- a/src/babylon_setup.js +++ b/src/babylon_setup.js @@ -1,21 +1,7 @@ import * as BABYLON from "babylonjs"; - -// Shared game state -const sharedState = (window.mazeGameState ??= { - config: { - seed: Math.floor(Math.random() * 100000), - level: 1, - mazeWidth: 11, - mazeHeight: 11, - minChestDeadEnds: 2, - }, - runtime: { - runActive: false, - hasKey: false, - elapsedSeconds: 0, - message: "Adjust settings, then start a run.", - }, -}); +import { sharedState } from "./game/state.js"; +import { seededRng, generateMazeGrid, findDeadEnds } from "./game/maze.js"; +import { gridCellToWorld, isWalkableCell } from "./game/grid.js"; // Initialize Babylon.js engine and scene const canvas = document.getElementById("renderCanvas"); @@ -143,88 +129,6 @@ function clearLevelMeshes() { spawnGridPos = null; } -// Simple seeded RNG -function seededRng(seed) { - let s = seed % 2147483647; - if (s <= 0) s += 2147483646; - return function () { - s = (s * 16807) % 2147483647; - return (s - 1) / 2147483646; - }; -} - -// Maze generation (recursive backtracker) returning grid: 0 path, 1 wall -function generateMazeGrid(w, h, seed) { - if (w % 2 === 0) w += 1; - if (h % 2 === 0) h += 1; - const rng = seededRng(seed); - const grid = Array.from({ length: h }, () => Array(w).fill(1)); - - function carve(x, y) { - grid[y][x] = 0; - const dirs = [ [0,-2],[2,0],[0,2],[-2,0] ]; - for (let i = dirs.length -1; i > 0; i--) { - const j = Math.floor(rng() * (i+1)); - [dirs[i], dirs[j]] = [dirs[j], dirs[i]]; - } - for (const [dx,dy] of dirs) { - const nx = x + dx; - const ny = y + dy; - if (nx > 0 && nx < w-1 && ny > 0 && ny < h-1 && grid[ny][nx] === 1) { - grid[y + dy/2][x + dx/2] = 0; - carve(nx, ny); - } - } - } - - const sx = 1 + Math.floor(rng() * ((w-1)/2)) * 2; - const sy = 1 + Math.floor(rng() * ((h-1)/2)) * 2; - carve(sx, sy); - - const extra = Math.max(0, Math.floor((w*h) * 0.02)); - for (let i = 0; i < extra; i++) { - const rx = 1 + Math.floor(rng() * ((w-1)/2)) * 2; - const ry = 1 + Math.floor(rng() * ((h-1)/2)) * 2; - const dirs = [ [0,-1],[1,0],[0,1],[-1,0] ]; - const [dx,dy] = dirs[Math.floor(rng()*dirs.length)]; - const nx = rx + dx; - const ny = ry + dy; - if (nx > 0 && nx < w-1 && ny > 0 && ny < h-1) grid[ny][nx] = 0; - } - - return grid; -} - -function findDeadEnds(grid) { - const h = grid.length; - const w = grid[0].length; - const dead = []; - for (let y = 1; y < h-1; y++) { - for (let x = 1; x < w-1; x++) { - if (grid[y][x] !== 0) continue; - let neighbors = 0; - const deltas = [[0,1],[1,0],[0,-1],[-1,0]]; - for (const [dx,dy] of deltas) if (grid[y+dy][x+dx] === 0) neighbors++; - if (neighbors === 1) dead.push([x,y]); - } - } - return dead; -} - -function gridCellToWorld(grid, x, y) { - const halfW = (grid[0].length * cellSize) / 2; - const halfH = (grid.length * cellSize) / 2; - return new BABYLON.Vector3( - x * cellSize - halfW + cellSize / 2, - 0, - y * cellSize - halfH + cellSize / 2, - ); -} - -function isWalkableCell(grid, x, y) { - return y >= 0 && y < grid.length && x >= 0 && x < grid[0].length && grid[y][x] === 0; -} - function isReservedCell(x, y) { if (chestMap.has(`${x},${y}`)) return true; if (exitGridPos && exitGridPos.x === x && exitGridPos.y === y) return true; @@ -322,7 +226,7 @@ function placeExit(grid, seed) { console.warn("Exit selected on non-walkable cell", { x, y }); return; } - const exitWorld = gridCellToWorld(grid, x, y); + const exitWorld = gridCellToWorld(grid, x, y, cellSize); const ex = exitWorld.x; const ez = exitWorld.z; const worldSpan = Math.max(grid[0].length, grid.length) * cellSize; @@ -386,7 +290,7 @@ function spawnCameraAt(grid) { } spawnGridPos = bestCell; - const spawnWorld = gridCellToWorld(grid, bestCell.x, bestCell.y); + const spawnWorld = gridCellToWorld(grid, bestCell.x, bestCell.y, cellSize); const px = spawnWorld.x; const pz = spawnWorld.z; diff --git a/src/game/grid.js b/src/game/grid.js new file mode 100644 index 0000000..68bb1a2 --- /dev/null +++ b/src/game/grid.js @@ -0,0 +1,15 @@ +import * as BABYLON from "babylonjs"; + +export function gridCellToWorld(grid, x, y, cellSize) { + const halfW = (grid[0].length * cellSize) / 2; + const halfH = (grid.length * cellSize) / 2; + return new BABYLON.Vector3( + x * cellSize - halfW + cellSize / 2, + 0, + y * cellSize - halfH + cellSize / 2, + ); +} + +export function isWalkableCell(grid, x, y) { + return y >= 0 && y < grid.length && x >= 0 && x < grid[0].length && grid[y][x] === 0; +} diff --git a/src/game/maze.js b/src/game/maze.js new file mode 100644 index 0000000..25846f6 --- /dev/null +++ b/src/game/maze.js @@ -0,0 +1,66 @@ +export function seededRng(seed) { + let s = seed % 2147483647; + if (s <= 0) s += 2147483646; + return function () { + s = (s * 16807) % 2147483647; + return (s - 1) / 2147483646; + }; +} + +// Maze generation (recursive backtracker): 0 path, 1 wall +export function generateMazeGrid(w, h, seed) { + if (w % 2 === 0) w += 1; + if (h % 2 === 0) h += 1; + const rng = seededRng(seed); + const grid = Array.from({ length: h }, () => Array(w).fill(1)); + + function carve(x, y) { + grid[y][x] = 0; + const dirs = [[0, -2], [2, 0], [0, 2], [-2, 0]]; + for (let i = dirs.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)); + [dirs[i], dirs[j]] = [dirs[j], dirs[i]]; + } + for (const [dx, dy] of dirs) { + const nx = x + dx; + const ny = y + dy; + if (nx > 0 && nx < w - 1 && ny > 0 && ny < h - 1 && grid[ny][nx] === 1) { + grid[y + dy / 2][x + dx / 2] = 0; + carve(nx, ny); + } + } + } + + const sx = 1 + Math.floor(rng() * ((w - 1) / 2)) * 2; + const sy = 1 + Math.floor(rng() * ((h - 1) / 2)) * 2; + carve(sx, sy); + + const extra = Math.max(0, Math.floor((w * h) * 0.02)); + for (let i = 0; i < extra; i++) { + const rx = 1 + Math.floor(rng() * ((w - 1) / 2)) * 2; + const ry = 1 + Math.floor(rng() * ((h - 1) / 2)) * 2; + const dirs = [[0, -1], [1, 0], [0, 1], [-1, 0]]; + const [dx, dy] = dirs[Math.floor(rng() * dirs.length)]; + const nx = rx + dx; + const ny = ry + dy; + if (nx > 0 && nx < w - 1 && ny > 0 && ny < h - 1) grid[ny][nx] = 0; + } + + return grid; +} + +export function findDeadEnds(grid) { + const h = grid.length; + const w = grid[0].length; + const dead = []; + for (let y = 1; y < h - 1; y++) { + for (let x = 1; x < w - 1; x++) { + if (grid[y][x] !== 0) continue; + let neighbors = 0; + const deltas = [[0, 1], [1, 0], [0, -1], [-1, 0]]; + for (const [dx, dy] of deltas) if (grid[y + dy][x + dx] === 0) neighbors++; + if (neighbors === 1) dead.push([x, y]); + } + } + return dead; +} diff --git a/src/game/state.js b/src/game/state.js new file mode 100644 index 0000000..4104efb --- /dev/null +++ b/src/game/state.js @@ -0,0 +1,15 @@ +export const sharedState = (window.mazeGameState ??= { + config: { + seed: Math.floor(Math.random() * 100000), + level: 1, + mazeWidth: 11, + mazeHeight: 11, + minChestDeadEnds: 2, + }, + runtime: { + runActive: false, + hasKey: false, + elapsedSeconds: 0, + message: "Adjust settings, then start a run.", + }, +}); diff --git a/src/html_panel.js b/src/html_panel.js index ad1fbdb..ab3beab 100644 --- a/src/html_panel.js +++ b/src/html_panel.js @@ -1,4 +1,4 @@ -import { sharedState } from "./babylon_setup.js"; +import { sharedState } from "./game/state.js"; // Handler functions (same as p5_panel but without p5 scoping) function resetRun(message) {