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.", }, }); // Initialize Babylon.js engine and scene const canvas = document.getElementById("renderCanvas"); const engine = new BABYLON.Engine(canvas, true); const scene = new BABYLON.Scene(engine); scene.clearColor = new BABYLON.Color4(0.05, 0.07, 0.1, 1); const camera = new BABYLON.ArcRotateCamera( "cam", -Math.PI / 2, Math.PI / 2.4, 10, BABYLON.Vector3.Zero(), scene, ); camera.attachControl(canvas, true); new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene); // Placeholder sphere const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene); const sphereMaterial = new BABYLON.StandardMaterial("sphereMaterial", scene); sphereMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.55, 0.95); sphereMaterial.emissiveColor = new BABYLON.Color3(0.05, 0.12, 0.2); sphere.material = sphereMaterial; // Ground const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 14, height: 14 }, scene); const groundMaterial = new BABYLON.StandardMaterial("groundMaterial", scene); groundMaterial.diffuseColor = new BABYLON.Color3(0.12, 0.14, 0.17); groundMaterial.specularColor = BABYLON.Color3.Black(); ground.material = groundMaterial; // Main render loop engine.runRenderLoop(() => { const level = sharedState.config.level; sphere.rotation.y += 0.01; sphere.scaling.x = 1 + (level - 1) * 0.05; sphere.scaling.z = 1 + (level - 1) * 0.05; sphereMaterial.diffuseColor = sharedState.runtime.hasKey ? new BABYLON.Color3(0.25, 0.8, 0.45) : new BABYLON.Color3(0.2, 0.55, 0.95); scene.render(); }); window.addEventListener("resize", () => { engine.resize(); }); // Maze data structures let levelMeshes = []; let chestMap = new Map(); // key: "x,y" -> {mesh, opened} let keyChestKey = null; let exitBox = null; const cellSize = 2; function clearLevelMeshes() { for (const m of levelMeshes) { try { m.dispose(); } catch(e) {} } levelMeshes = []; chestMap.clear(); keyChestKey = null; if (exitBox) { try { exitBox.dispose(); } catch(e){}; exitBox = 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 buildLevelFromGrid(grid) { clearLevelMeshes(); const h = grid.length; const w = grid[0].length; const halfW = (w * cellSize) / 2; const halfH = (h * cellSize) / 2; const floor = BABYLON.MeshBuilder.CreateGround('levelGround', { width: w*cellSize, height: h*cellSize }, scene); floor.position = new BABYLON.Vector3(0, 0, 0); const fm = new BABYLON.StandardMaterial('floorMat', scene); fm.diffuseColor = new BABYLON.Color3(0.08, 0.08, 0.09); floor.material = fm; levelMeshes.push(floor); const wallMat = new BABYLON.StandardMaterial('wallMat', scene); wallMat.diffuseColor = new BABYLON.Color3(0.33, 0.28, 0.22); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { if (grid[y][x] === 1) { const box = BABYLON.MeshBuilder.CreateBox(`wall_${x}_${y}`, { size: cellSize }, scene); box.position = new BABYLON.Vector3(x*cellSize - halfW + cellSize/2, cellSize/2, y*cellSize - halfH + cellSize/2); box.material = wallMat; box.checkCollisions = true; levelMeshes.push(box); } } } } function placeChestsOnDeadEnds(grid, deadEnds, minCount, seed) { const rng = seededRng(seed); for (let i = deadEnds.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i+1)); [deadEnds[i], deadEnds[j]] = [deadEnds[j], deadEnds[i]]; } const chosen = deadEnds.slice(0, Math.min(minCount, deadEnds.length)); const halfW = (grid[0].length * cellSize) / 2; const halfH = (grid.length * cellSize) / 2; const chestMat = new BABYLON.StandardMaterial('chestMat', scene); chestMat.diffuseColor = new BABYLON.Color3(0.75, 0.45, 0.15); for (const [x,y] of chosen) { const c = BABYLON.MeshBuilder.CreateBox(`chest_${x}_${y}`, { width: cellSize*0.8, height: cellSize*0.6, depth: cellSize*0.6 }, scene); c.position = new BABYLON.Vector3(x*cellSize - halfW + cellSize/2, cellSize*0.3, y*cellSize - halfH + cellSize/2); c.material = chestMat; c.isPickable = true; levelMeshes.push(c); chestMap.set(`${x},${y}`, { mesh: c, opened: false }); } if (chosen.length > 0) { const k = Math.floor(rng() * chosen.length); const [kx, ky] = chosen[k]; keyChestKey = `${kx},${ky}`; const entry = chestMap.get(keyChestKey); if (entry) { const km = new BABYLON.StandardMaterial('keyChestMat', scene); km.diffuseColor = new BABYLON.Color3(0.95, 0.8, 0.1); entry.mesh.material = km; } } } function placeExit(grid, seed) { const dead = findDeadEnds(grid); const rng = seededRng(seed+1); if (dead.length === 0) return; const idx = Math.floor(rng() * dead.length); const [x,y] = dead[idx]; const halfW = (grid[0].length * cellSize) / 2; const halfH = (grid.length * cellSize) / 2; const ex = x*cellSize - halfW + cellSize/2; const ez = y*cellSize - halfH + cellSize/2; const plane = BABYLON.MeshBuilder.CreateGround('exitZone', { width: cellSize*0.9, height: cellSize*0.9 }, scene); plane.material = new BABYLON.StandardMaterial('exitMat', scene); plane.material.emissiveColor = new BABYLON.Color3(0.9, 0.8, 0.2); plane.position = new BABYLON.Vector3(ex, 0.01, ez); exitBox = plane; levelMeshes.push(plane); } function spawnCameraAt(grid) { const h = grid.length, w = grid[0].length; for (let y = 1; y < h-1; y++) { for (let x = 1; x < w-1; x++) { if (grid[y][x] === 0) { const halfW = (w * cellSize) / 2; const halfH = (h * cellSize) / 2; const px = x*cellSize - halfW + cellSize/2; const pz = y*cellSize - halfH + cellSize/2; try { if (camera && camera.position) { camera.position = new BABYLON.Vector3(px, 1.6, pz); } } catch (e) {} return; } } } } function generateLevel() { const cfg = sharedState.config; const seed = cfg.seed; const w = Math.max(9, cfg.mazeWidth); const h = Math.max(9, cfg.mazeHeight); const grid = generateMazeGrid(w, h, seed + cfg.level); const dead = findDeadEnds(grid); buildLevelFromGrid(grid); placeChestsOnDeadEnds(grid, dead, cfg.minChestDeadEnds, seed + cfg.level); placeExit(grid, seed + cfg.level); spawnCameraAt(grid); sharedState.runtime.message = `Level ${cfg.level} generated.`; window.requestAnimationFrame(()=>{ /* let scene update */ }); } // Expose API for p5 to call window.mazeGameApi = { generateLevel }; // Pointer interaction for chests scene.onPointerObservable.add((pi) => { if (pi.type !== BABYLON.PointerEventTypes.POINTERDOWN) return; const pick = scene.pick(scene.pointerX, scene.pointerY); if (!pick || !pick.hit || !pick.pickedMesh) return; const m = pick.pickedMesh; if (!m.name.startsWith('chest_')) return; const coords = m.name.split('_').slice(1).join(','); const entry = chestMap.get(coords); if (!entry) return; if (entry.opened) { sharedState.runtime.runActive = false; sharedState.runtime.message = 'Opened chest again — game over.'; return; } entry.opened = true; const openedMat = new BABYLON.StandardMaterial('openedMat', scene); openedMat.diffuseColor = new BABYLON.Color3(0.25,0.25,0.25); entry.mesh.material = openedMat; if (coords === keyChestKey) { sharedState.runtime.hasKey = true; sharedState.runtime.message = 'You found the key! Find the exit.'; } else { sharedState.runtime.message = 'This chest was empty.'; } }); // Level transition check scene.registerBeforeRender(() => { if (sharedState.runtime.runActive) { sharedState.runtime.elapsedSeconds += engine.getDeltaTime() / 1000; } if (sharedState.runtime.hasKey && exitBox && camera && camera.position) { const pos = camera.position; const ex = exitBox.position.x, ez = exitBox.position.z; const dist = Math.hypot(pos.x - ex, pos.z - ez); if (dist < cellSize * 0.9) { sharedState.config.level += 1; sharedState.runtime.hasKey = false; sharedState.runtime.elapsedSeconds = 0; sharedState.runtime.message = `Level ${sharedState.config.level} starting.`; generateLevel(); } } }); // Export shared state for p5 to use export { sharedState };