Files
Untitled-Maze-Game/src/babylon_setup.js

323 lines
11 KiB
JavaScript

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 };