update GUI
This commit is contained in:
28
index.html
28
index.html
@@ -39,28 +39,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>Settings</h3>
|
||||
<div class="slider-group">
|
||||
<label>Maze width:
|
||||
<input id="slider-width" type="range" min="9" max="31" step="2" value="11" />
|
||||
<span id="value-width">11</span> cells
|
||||
</label>
|
||||
</div>
|
||||
<div class="slider-group">
|
||||
<label>Maze height:
|
||||
<input id="slider-height" type="range" min="9" max="31" step="2" value="11" />
|
||||
<span id="value-height">11</span> cells
|
||||
</label>
|
||||
</div>
|
||||
<div class="slider-group">
|
||||
<label>Min chest dead-ends:
|
||||
<input id="slider-deadends" type="range" min="1" max="10" step="1" value="2" />
|
||||
<span id="value-deadends">2</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>Status</h3>
|
||||
<div class="status-display">
|
||||
@@ -71,13 +49,15 @@
|
||||
<div class="status-line"><strong>Time left:</strong> <span id="status-time">60.0</span></div>
|
||||
<div class="status-line"><strong>Key:</strong> <span id="status-key">no</span></div>
|
||||
<div class="status-line"><strong>Rounds:</strong> <span id="status-rounds">0</span></div>
|
||||
<div class="status-message" id="status-message">Adjust settings, then start a run.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/src/multi_sketch.js"></script>
|
||||
<script type="module">
|
||||
import "./src/babylon_panel.js";
|
||||
import "./src/html_panel.js";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
526
src/babylon_panel.js
Normal file
526
src/babylon_panel.js
Normal file
@@ -0,0 +1,526 @@
|
||||
import * as BABYLON from "babylonjs";
|
||||
import { sharedState } from "./game/state.js";
|
||||
import { seededRng, generateMazeGrid, findDeadEnds } from "./game/maze.js";
|
||||
import { gridCellToWorld, isWalkableCell } from "./game/grid.js";
|
||||
import { playSfx, primeSfx } from "./game/sfx.js";
|
||||
import chestTextureUrl from "../img/img_chest.png";
|
||||
import wallTextureUrl from "../img/img_wall.png";
|
||||
import groundTextureUrl from "../img/img_ground.png";
|
||||
import doorTextureUrl from "../img/img_door.png";
|
||||
import gameOverImageUrl from "../img/img_jobapplication.png";
|
||||
|
||||
// Initialize Babylon.js engine and scene
|
||||
const canvas = document.getElementById("renderCanvas");
|
||||
const engine = new BABYLON.Engine(canvas, true);
|
||||
const canvasTime = document.getElementById("canvas-time");
|
||||
const canvasKey = document.getElementById("canvas-key");
|
||||
const canvasRounds = document.getElementById("canvas-rounds");
|
||||
const gameOverOverlay = document.getElementById("game-over-overlay");
|
||||
const gameOverImage = document.getElementById("game-over-image");
|
||||
if (gameOverImage) {
|
||||
gameOverImage.src = gameOverImageUrl;
|
||||
}
|
||||
|
||||
const scene = new BABYLON.Scene(engine);
|
||||
scene.clearColor = new BABYLON.Color4(0.05, 0.07, 0.1, 1);
|
||||
|
||||
const camera = new BABYLON.UniversalCamera("cam", new BABYLON.Vector3(0, 1.6, 0), scene);
|
||||
camera.minZ = 0.1;
|
||||
camera.speed = 1.12;
|
||||
camera.angularSensibility = 1000;
|
||||
camera.inertia = 0.6;
|
||||
camera.keysUp = [87]; // W
|
||||
camera.keysDown = [83]; // S
|
||||
camera.keysLeft = [65]; // A
|
||||
camera.keysRight = [68]; // D
|
||||
camera.checkCollisions = true;
|
||||
camera.applyGravity = true;
|
||||
camera.ellipsoid = new BABYLON.Vector3(0.35, 0.9, 0.35);
|
||||
camera.attachControl(canvas, true);
|
||||
|
||||
const overviewCamera = new BABYLON.ArcRotateCamera(
|
||||
"overviewCam",
|
||||
-Math.PI / 2,
|
||||
Math.PI / 3.2,
|
||||
40,
|
||||
new BABYLON.Vector3(0, 0, 0),
|
||||
scene,
|
||||
);
|
||||
overviewCamera.lowerBetaLimit = 0.2;
|
||||
overviewCamera.upperBetaLimit = Math.PI / 2.05;
|
||||
overviewCamera.lowerRadiusLimit = 8;
|
||||
overviewCamera.inertia = 0.7;
|
||||
|
||||
let cameraMode = "fp";
|
||||
scene.activeCamera = camera;
|
||||
let lastFootstepPosition = null;
|
||||
let footstepAccumulator = 0;
|
||||
let footstepElapsed = 0;
|
||||
let gameOverActive = false;
|
||||
let lowTimeAlertPlayed = false;
|
||||
|
||||
scene.gravity = new BABYLON.Vector3(0, -0.2, 0);
|
||||
scene.collisionsEnabled = true;
|
||||
|
||||
canvas.addEventListener("click", () => {
|
||||
primeSfx();
|
||||
if (cameraMode === "fp" && document.pointerLockElement !== canvas) {
|
||||
canvas.requestPointerLock();
|
||||
}
|
||||
});
|
||||
|
||||
function updateOverviewCameraForMaze(w, h) {
|
||||
const mazeSpan = Math.max(w, h) * cellSize;
|
||||
overviewCamera.radius = Math.max(mazeSpan * 1.05, 16);
|
||||
overviewCamera.target = new BABYLON.Vector3(0, 0, 0);
|
||||
}
|
||||
|
||||
function showGameOverScreen() {
|
||||
gameOverActive = true;
|
||||
if (gameOverOverlay) {
|
||||
gameOverOverlay.hidden = false;
|
||||
}
|
||||
if (document.pointerLockElement === canvas && document.exitPointerLock) {
|
||||
document.exitPointerLock();
|
||||
}
|
||||
}
|
||||
|
||||
function hideGameOverScreen() {
|
||||
gameOverActive = false;
|
||||
if (gameOverOverlay) {
|
||||
gameOverOverlay.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function restartRunFromGameOver() {
|
||||
sharedState.runtime.runActive = true;
|
||||
sharedState.runtime.hasKey = false;
|
||||
sharedState.runtime.roundsCompleted = 0;
|
||||
sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS;
|
||||
sharedState.runtime.message = "Run restarted.";
|
||||
sharedState.config.level = 1;
|
||||
hideGameOverScreen();
|
||||
generateLevel();
|
||||
}
|
||||
|
||||
function switchCameraMode() {
|
||||
if (cameraMode === "fp") {
|
||||
if (document.pointerLockElement === canvas && document.exitPointerLock) {
|
||||
document.exitPointerLock();
|
||||
}
|
||||
camera.detachControl(canvas);
|
||||
overviewCamera.attachControl(canvas, true);
|
||||
scene.activeCamera = overviewCamera;
|
||||
cameraMode = "overview";
|
||||
sharedState.runtime.message = "Overview camera (press V to return to first-person).";
|
||||
return;
|
||||
}
|
||||
|
||||
overviewCamera.detachControl(canvas);
|
||||
camera.attachControl(canvas, true);
|
||||
scene.activeCamera = camera;
|
||||
cameraMode = "fp";
|
||||
sharedState.runtime.message = "First-person camera (W/A/S/D + mouse).";
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.code === "KeyW" || event.code === "KeyA" || event.code === "KeyS" || event.code === "KeyD" || event.code === "KeyV" || event.code === "KeyR") {
|
||||
primeSfx();
|
||||
}
|
||||
if (event.code === "KeyR" && gameOverActive) {
|
||||
restartRunFromGameOver();
|
||||
return;
|
||||
}
|
||||
if (event.code === "KeyV") {
|
||||
switchCameraMode();
|
||||
}
|
||||
});
|
||||
|
||||
new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
|
||||
|
||||
// Central sphere (hidden but kept for reference)
|
||||
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene);
|
||||
sphere.isVisible = false;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
if (canvasTime) {
|
||||
canvasTime.textContent = `${sharedState.runtime.elapsedSeconds.toFixed(1)}s`;
|
||||
}
|
||||
if (canvasKey) {
|
||||
canvasKey.textContent = sharedState.runtime.hasKey ? "yes" : "no";
|
||||
}
|
||||
if (canvasRounds) {
|
||||
canvasRounds.textContent = String(sharedState.runtime.roundsCompleted);
|
||||
}
|
||||
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;
|
||||
let exitGridPos = null; // track exit grid position for collision checking
|
||||
let spawnGridPos = null; // track spawn grid position for validation
|
||||
let spawnMarker = null;
|
||||
let highlightedChest = null;
|
||||
const cellSize = 2;
|
||||
const ROUND_TIME_SECONDS = 60;
|
||||
|
||||
function setChestHighlight(mesh) {
|
||||
if (highlightedChest === mesh) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (highlightedChest) {
|
||||
highlightedChest.renderOutline = false;
|
||||
}
|
||||
|
||||
highlightedChest = mesh;
|
||||
|
||||
if (highlightedChest) {
|
||||
highlightedChest.outlineColor = new BABYLON.Color3(0.85, 0.85, 0.85);
|
||||
highlightedChest.outlineWidth = 0.08;
|
||||
highlightedChest.renderOutline = true;
|
||||
}
|
||||
}
|
||||
|
||||
function clearLevelMeshes() {
|
||||
setChestHighlight(null);
|
||||
for (const m of levelMeshes) {
|
||||
try { m.dispose(); } catch(e) {}
|
||||
}
|
||||
levelMeshes = [];
|
||||
chestMap.clear();
|
||||
keyChestKey = null;
|
||||
if (exitBox) { try { exitBox.dispose(); } catch(e){}; exitBox = null; }
|
||||
if (spawnMarker) { try { spawnMarker.dispose(); } catch(e){}; spawnMarker = null; }
|
||||
exitGridPos = null;
|
||||
spawnGridPos = null;
|
||||
}
|
||||
|
||||
function isReservedCell(x, y) {
|
||||
if (chestMap.has(`${x},${y}`)) return true;
|
||||
if (exitGridPos && exitGridPos.x === x && exitGridPos.y === y) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
floor.checkCollisions = true;
|
||||
const fm = new BABYLON.StandardMaterial('floorMat', scene);
|
||||
fm.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
|
||||
fm.diffuseTexture = new BABYLON.Texture(groundTextureUrl, scene);
|
||||
fm.diffuseTexture.uScale = Math.max(1, Math.floor(w / 2));
|
||||
fm.diffuseTexture.vScale = Math.max(1, Math.floor(h / 2));
|
||||
fm.diffuseColor = new BABYLON.Color3(0.9, 0.9, 0.9);
|
||||
floor.material = fm;
|
||||
levelMeshes.push(floor);
|
||||
|
||||
const wallMat = new BABYLON.StandardMaterial('wallMat', scene);
|
||||
wallMat.diffuseTexture = new BABYLON.Texture(wallTextureUrl, scene);
|
||||
wallMat.diffuseColor = new BABYLON.Color3(0.95, 0.95, 0.95);
|
||||
|
||||
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.diffuseTexture = new BABYLON.Texture(chestTextureUrl, scene);
|
||||
chestMat.diffuseColor = new BABYLON.Color3(0.95, 0.95, 0.95);
|
||||
|
||||
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.diffuseTexture = new BABYLON.Texture(chestTextureUrl, scene);
|
||||
km.diffuseColor = new BABYLON.Color3(0.95, 0.95, 0.95);
|
||||
km.emissiveColor = new BABYLON.Color3(0.3, 0.22, 0.02);
|
||||
entry.mesh.material = km;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function placeExit(grid, seed) {
|
||||
const dead = findDeadEnds(grid);
|
||||
const rng = seededRng(seed+1);
|
||||
if (dead.length === 0) return;
|
||||
|
||||
// Filter out dead ends that have chests
|
||||
const availableDead = dead.filter(([x, y]) => !chestMap.has(`${x},${y}`));
|
||||
if (availableDead.length === 0) {
|
||||
// Fallback: use any dead end if no chest-free spot available
|
||||
// This shouldn't happen in normal cases
|
||||
const idx = Math.floor(rng() * dead.length);
|
||||
const [x,y] = dead[idx];
|
||||
exitGridPos = { x, y };
|
||||
// Continue below
|
||||
} else {
|
||||
const idx = Math.floor(rng() * availableDead.length);
|
||||
const [x,y] = availableDead[idx];
|
||||
exitGridPos = { x, y };
|
||||
}
|
||||
|
||||
const [x,y] = [exitGridPos.x, exitGridPos.y];
|
||||
if (!isWalkableCell(grid, x, y)) {
|
||||
console.warn("Exit selected on non-walkable cell", { x, y });
|
||||
return;
|
||||
}
|
||||
const exitWorld = gridCellToWorld(grid, x, y, cellSize);
|
||||
const ex = exitWorld.x;
|
||||
const ez = exitWorld.z;
|
||||
|
||||
const plane = BABYLON.MeshBuilder.CreatePlane('exitDoor', {
|
||||
width: cellSize * 1.35,
|
||||
height: cellSize * 1.85,
|
||||
sideOrientation: BABYLON.Mesh.DOUBLESIDE,
|
||||
}, scene);
|
||||
const exitMat = new BABYLON.StandardMaterial('exitMat', scene);
|
||||
exitMat.diffuseTexture = new BABYLON.Texture(doorTextureUrl, scene);
|
||||
exitMat.diffuseColor = new BABYLON.Color3(0.95, 0.95, 0.95);
|
||||
exitMat.emissiveColor = new BABYLON.Color3(0.07, 0.07, 0.07);
|
||||
plane.material = exitMat;
|
||||
plane.position = new BABYLON.Vector3(ex, cellSize * 0.92, ez);
|
||||
plane.billboardMode = BABYLON.Mesh.BILLBOARDMODE_Y;
|
||||
exitBox = plane;
|
||||
levelMeshes.push(plane);
|
||||
}
|
||||
|
||||
function spawnCameraAt(grid) {
|
||||
const h = grid.length;
|
||||
const w = grid[0].length;
|
||||
let bestCell = null;
|
||||
let bestDist = -1;
|
||||
|
||||
// Choose a valid spawn that is far from exit when possible.
|
||||
for (let y = 1; y < h - 1; y++) {
|
||||
for (let x = 1; x < w - 1; x++) {
|
||||
if (!isWalkableCell(grid, x, y)) continue;
|
||||
if (isReservedCell(x, y)) continue;
|
||||
|
||||
const d = exitGridPos ? Math.hypot(x - exitGridPos.x, y - exitGridPos.y) : 0;
|
||||
if (d > bestDist) {
|
||||
bestDist = d;
|
||||
bestCell = { x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestCell) {
|
||||
console.warn("No valid spawn cell found.");
|
||||
return;
|
||||
}
|
||||
|
||||
spawnGridPos = bestCell;
|
||||
const spawnWorld = gridCellToWorld(grid, bestCell.x, bestCell.y, cellSize);
|
||||
const px = spawnWorld.x;
|
||||
const pz = spawnWorld.z;
|
||||
|
||||
try {
|
||||
if (camera && camera.position) {
|
||||
camera.position = new BABYLON.Vector3(px, 1.6, pz);
|
||||
}
|
||||
} catch (e) {}
|
||||
lastFootstepPosition = camera && camera.position ? camera.position.clone() : new BABYLON.Vector3(px, 1.6, pz);
|
||||
footstepAccumulator = 0;
|
||||
footstepElapsed = 0;
|
||||
}
|
||||
|
||||
function generateLevel() {
|
||||
hideGameOverScreen();
|
||||
lowTimeAlertPlayed = false;
|
||||
if (canvasTime) {
|
||||
canvasTime.classList.remove("low-time");
|
||||
}
|
||||
const cfg = sharedState.config;
|
||||
const seed = cfg.seed;
|
||||
const roundScale = Math.max(0, cfg.level - 1);
|
||||
const w = Math.max(9, cfg.mazeWidth + roundScale * 2);
|
||||
const h = Math.max(9, cfg.mazeHeight + roundScale * 2);
|
||||
const chestCount = Math.max(1, cfg.minChestDeadEnds + roundScale);
|
||||
const grid = generateMazeGrid(w, h, seed + cfg.level);
|
||||
updateOverviewCameraForMaze(w, h);
|
||||
const dead = findDeadEnds(grid);
|
||||
buildLevelFromGrid(grid);
|
||||
placeChestsOnDeadEnds(grid, dead, chestCount, seed + cfg.level);
|
||||
placeExit(grid, seed + cfg.level);
|
||||
spawnCameraAt(grid);
|
||||
|
||||
const placementValid =
|
||||
!!exitGridPos &&
|
||||
!!spawnGridPos &&
|
||||
isWalkableCell(grid, exitGridPos.x, exitGridPos.y) &&
|
||||
isWalkableCell(grid, spawnGridPos.x, spawnGridPos.y) &&
|
||||
!(exitGridPos.x === spawnGridPos.x && exitGridPos.y === spawnGridPos.y);
|
||||
|
||||
if (!placementValid) {
|
||||
sharedState.runtime.message = `Placement warning: spawn/exit invalid on level ${cfg.level}.`;
|
||||
console.warn("Invalid spawn/exit placement", { exitGridPos, spawnGridPos });
|
||||
} else {
|
||||
sharedState.runtime.message = `Level ${cfg.level} generated (spawn ${spawnGridPos.x},${spawnGridPos.y} / exit ${exitGridPos.x},${exitGridPos.y}).`;
|
||||
}
|
||||
lastFootstepPosition = camera && camera.position ? camera.position.clone() : lastFootstepPosition;
|
||||
footstepAccumulator = 0;
|
||||
footstepElapsed = 0;
|
||||
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;
|
||||
if (!sharedState.runtime.runActive || gameOverActive) 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) {
|
||||
primeSfx();
|
||||
playSfx("chestClose", 0.8);
|
||||
playSfx("lose", 0.85);
|
||||
sharedState.runtime.runActive = false;
|
||||
sharedState.runtime.message = 'Opened chest again — game over.';
|
||||
showGameOverScreen();
|
||||
return;
|
||||
}
|
||||
primeSfx();
|
||||
playSfx("chestOpen", 0.8);
|
||||
entry.opened = true;
|
||||
if (coords === keyChestKey) {
|
||||
sharedState.runtime.hasKey = true;
|
||||
playSfx("key", 0.85);
|
||||
sharedState.runtime.message = 'You found the key! Find the exit.';
|
||||
} else {
|
||||
sharedState.runtime.message = 'This chest was empty.';
|
||||
}
|
||||
});
|
||||
|
||||
// Level transition check
|
||||
scene.registerBeforeRender(() => {
|
||||
const targetRay = camera.getForwardRay(cellSize * 3.5);
|
||||
const targetPick = scene.pickWithRay(targetRay, (mesh) => mesh.name.startsWith('chest_'));
|
||||
if (targetPick && targetPick.hit && targetPick.pickedMesh) {
|
||||
setChestHighlight(targetPick.pickedMesh);
|
||||
} else {
|
||||
setChestHighlight(null);
|
||||
}
|
||||
|
||||
if (sharedState.runtime.runActive) {
|
||||
const dt = engine.getDeltaTime() / 1000;
|
||||
sharedState.runtime.elapsedSeconds = Math.max(0, sharedState.runtime.elapsedSeconds - dt);
|
||||
|
||||
const isLowTime = sharedState.runtime.elapsedSeconds < 10;
|
||||
if (isLowTime && !lowTimeAlertPlayed) {
|
||||
lowTimeAlertPlayed = true;
|
||||
playSfx("clock", 0.75);
|
||||
if (canvasTime) {
|
||||
canvasTime.classList.add("low-time");
|
||||
}
|
||||
}
|
||||
if (!isLowTime && lowTimeAlertPlayed) {
|
||||
lowTimeAlertPlayed = false;
|
||||
if (canvasTime) {
|
||||
canvasTime.classList.remove("low-time");
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedState.runtime.elapsedSeconds <= 0) {
|
||||
sharedState.runtime.runActive = false;
|
||||
sharedState.runtime.message = "Time up — game over.";
|
||||
playSfx("lose", 0.85);
|
||||
showGameOverScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (sharedState.runtime.runActive && cameraMode === "fp" && camera && camera.position && document.pointerLockElement === canvas) {
|
||||
const currentPosition = camera.position;
|
||||
if (!lastFootstepPosition) {
|
||||
lastFootstepPosition = currentPosition.clone();
|
||||
}
|
||||
const horizontalDistance = Math.hypot(
|
||||
currentPosition.x - lastFootstepPosition.x,
|
||||
currentPosition.z - lastFootstepPosition.z,
|
||||
);
|
||||
footstepAccumulator += horizontalDistance;
|
||||
footstepElapsed += engine.getDeltaTime();
|
||||
if (footstepAccumulator > 0.75 && footstepElapsed > 220) {
|
||||
playSfx("step", 0.65);
|
||||
footstepAccumulator = 0;
|
||||
footstepElapsed = 0;
|
||||
lastFootstepPosition = currentPosition.clone();
|
||||
}
|
||||
}
|
||||
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) {
|
||||
playSfx("win", 0.85);
|
||||
sharedState.config.level += 1;
|
||||
sharedState.runtime.hasKey = false;
|
||||
sharedState.runtime.roundsCompleted += 1;
|
||||
sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS;
|
||||
sharedState.runtime.message = `Level ${sharedState.config.level} starting.`;
|
||||
generateLevel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Export shared state for p5 to use
|
||||
export { sharedState };
|
||||
@@ -2,7 +2,7 @@ import * as BABYLON from "babylonjs";
|
||||
import { sharedState } from "./game/state.js";
|
||||
import { seededRng, generateMazeGrid, findDeadEnds } from "./game/maze.js";
|
||||
import { gridCellToWorld, isWalkableCell } from "./game/grid.js";
|
||||
import { playSfx, primeSfx } from "./sfx.js";
|
||||
import { playSfx, primeSfx } from "./game/sfx.js";
|
||||
import chestTextureUrl from "../img/img_chest.png";
|
||||
import wallTextureUrl from "../img/img_wall.png";
|
||||
import groundTextureUrl from "../img/img_ground.png";
|
||||
@@ -138,8 +138,9 @@ window.addEventListener("keydown", (event) => {
|
||||
|
||||
new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
|
||||
|
||||
// Placeholder sphere
|
||||
// Central sphere (hidden but kept for reference)
|
||||
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene);
|
||||
sphere.isVisible = false;
|
||||
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);
|
||||
@@ -151,9 +152,6 @@ engine.runRenderLoop(() => {
|
||||
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);
|
||||
if (canvasTime) {
|
||||
canvasTime.textContent = `${sharedState.runtime.elapsedSeconds.toFixed(1)}s`;
|
||||
}
|
||||
@@ -377,18 +375,6 @@ function spawnCameraAt(grid) {
|
||||
lastFootstepPosition = camera && camera.position ? camera.position.clone() : new BABYLON.Vector3(px, 1.6, pz);
|
||||
footstepAccumulator = 0;
|
||||
footstepElapsed = 0;
|
||||
|
||||
if (spawnMarker) {
|
||||
try { spawnMarker.dispose(); } catch(e) {}
|
||||
}
|
||||
const marker = BABYLON.MeshBuilder.CreateSphere('spawnMarker', { diameter: cellSize*0.4 }, scene);
|
||||
const markerMat = new BABYLON.StandardMaterial('spawnMarkerMat', scene);
|
||||
markerMat.diffuseColor = new BABYLON.Color3(0.2, 0.6, 0.95);
|
||||
markerMat.emissiveColor = new BABYLON.Color3(0.1, 0.3, 0.5);
|
||||
marker.material = markerMat;
|
||||
marker.position = new BABYLON.Vector3(px, cellSize*0.2, pz);
|
||||
spawnMarker = marker;
|
||||
levelMeshes.push(marker);
|
||||
}
|
||||
|
||||
function generateLevel() {
|
||||
|
||||
@@ -11,6 +11,6 @@ export const sharedState = (window.mazeGameState ??= {
|
||||
hasKey: false,
|
||||
roundsCompleted: 0,
|
||||
elapsedSeconds: 60,
|
||||
message: "Adjust settings, then start a run.",
|
||||
message: "Press Start to play.",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sharedState } from "./game/state.js";
|
||||
import { playSfx, primeSfx } from "./sfx.js";
|
||||
import { playSfx, primeSfx } from "./game/sfx.js";
|
||||
|
||||
// Handler functions (same as p5_panel but without p5 scoping)
|
||||
function resetRun(message) {
|
||||
@@ -29,9 +29,8 @@ function randomizeSeed() {
|
||||
|
||||
// Update display from shared state
|
||||
function updateDisplay() {
|
||||
document.getElementById("value-width").textContent = sharedState.config.mazeWidth;
|
||||
document.getElementById("value-height").textContent = sharedState.config.mazeHeight;
|
||||
document.getElementById("value-deadends").textContent = sharedState.config.minChestDeadEnds;
|
||||
document.getElementById("status-seed").textContent = sharedState.config.seed;
|
||||
document.getElementById("status-level").textContent = sharedState.config.level;
|
||||
|
||||
// Calculate effective maze size and chest count based on current level
|
||||
const roundScale = Math.max(0, sharedState.config.level - 1);
|
||||
@@ -39,8 +38,6 @@ function updateDisplay() {
|
||||
const effectiveHeight = Math.max(9, sharedState.config.mazeHeight + roundScale * 2);
|
||||
const effectiveChests = Math.max(1, sharedState.config.minChestDeadEnds + roundScale);
|
||||
|
||||
document.getElementById("status-seed").textContent = sharedState.config.seed;
|
||||
document.getElementById("status-level").textContent = sharedState.config.level;
|
||||
document.getElementById("status-maze-size").textContent = `${effectiveWidth}x${effectiveHeight}`;
|
||||
document.getElementById("status-chests").textContent = effectiveChests;
|
||||
document.getElementById("status-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
|
||||
@@ -68,27 +65,6 @@ document.getElementById("btn-randomize").addEventListener("click", () => {
|
||||
randomizeSeed();
|
||||
});
|
||||
|
||||
document.getElementById("slider-width").addEventListener("input", (e) => {
|
||||
primeSfx();
|
||||
const value = parseInt(e.target.value) | 1;
|
||||
sharedState.config.mazeWidth = value;
|
||||
document.getElementById("value-width").textContent = value;
|
||||
});
|
||||
|
||||
document.getElementById("slider-height").addEventListener("input", (e) => {
|
||||
primeSfx();
|
||||
const value = parseInt(e.target.value) | 1;
|
||||
sharedState.config.mazeHeight = value;
|
||||
document.getElementById("value-height").textContent = value;
|
||||
});
|
||||
|
||||
document.getElementById("slider-deadends").addEventListener("input", (e) => {
|
||||
primeSfx();
|
||||
const value = parseInt(e.target.value);
|
||||
sharedState.config.minChestDeadEnds = value;
|
||||
document.getElementById("value-deadends").textContent = value;
|
||||
});
|
||||
|
||||
// Update status display on game loop
|
||||
setInterval(() => {
|
||||
if (sharedState.runtime.runActive) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// Import Babylon.js game logic and HTML control panel
|
||||
import "./babylon_setup.js";
|
||||
import "./html_panel.js";
|
||||
Reference in New Issue
Block a user