add sounds and textures

This commit is contained in:
pobadoba
2026-05-10 16:49:59 +09:00
parent 021877902a
commit 7e2d6243b2
21 changed files with 438 additions and 19 deletions

View File

@@ -2,11 +2,17 @@ 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 chestTextureUrl from "../img/img_chest.png";
import wallTextureUrl from "../img/img_wall.png";
import groundTextureUrl from "../img/img_ground.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 scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color4(0.05, 0.07, 0.1, 1);
@@ -40,11 +46,15 @@ overviewCamera.inertia = 0.7;
let cameraMode = "fp";
scene.activeCamera = camera;
let lastFootstepPosition = null;
let footstepAccumulator = 0;
let footstepElapsed = 0;
scene.gravity = new BABYLON.Vector3(0, -0.2, 0);
scene.collisionsEnabled = true;
canvas.addEventListener("click", () => {
primeSfx();
if (cameraMode === "fp" && document.pointerLockElement !== canvas) {
canvas.requestPointerLock();
}
@@ -77,6 +87,9 @@ function switchCameraMode() {
}
window.addEventListener("keydown", (event) => {
if (event.code === "KeyW" || event.code === "KeyA" || event.code === "KeyS" || event.code === "KeyD" || event.code === "KeyV") {
primeSfx();
}
if (event.code === "KeyV") {
switchCameraMode();
}
@@ -103,6 +116,12 @@ engine.runRenderLoop(() => {
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();
});
@@ -118,9 +137,29 @@ 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;
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) {}
}
@@ -151,12 +190,16 @@ function buildLevelFromGrid(grid) {
floor.checkCollisions = true;
const fm = new BABYLON.StandardMaterial('floorMat', scene);
fm.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
fm.diffuseColor = new BABYLON.Color3(0.58, 0.58, 0.59);
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.diffuseColor = new BABYLON.Color3(0.33, 0.28, 0.22);
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++) {
@@ -182,7 +225,8 @@ function placeChestsOnDeadEnds(grid, deadEnds, minCount, seed) {
const halfH = (grid.length * cellSize) / 2;
const chestMat = new BABYLON.StandardMaterial('chestMat', scene);
chestMat.diffuseColor = new BABYLON.Color3(0.75, 0.45, 0.15);
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);
@@ -200,7 +244,9 @@ function placeChestsOnDeadEnds(grid, deadEnds, minCount, seed) {
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);
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;
}
}
@@ -304,6 +350,9 @@ function spawnCameraAt(grid) {
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;
if (spawnMarker) {
try { spawnMarker.dispose(); } catch(e) {}
@@ -344,6 +393,9 @@ function generateLevel() {
} 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 */ });
}
@@ -361,16 +413,18 @@ scene.onPointerObservable.add((pi) => {
const entry = chestMap.get(coords);
if (!entry) return;
if (entry.opened) {
primeSfx();
playSfx("chestClose", 0.8);
sharedState.runtime.runActive = false;
sharedState.runtime.message = 'Opened chest again — game over.';
return;
}
primeSfx();
playSfx("chestOpen", 0.8);
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;
playSfx("key", 0.85);
sharedState.runtime.message = 'You found the key! Find the exit.';
} else {
sharedState.runtime.message = 'This chest was empty.';
@@ -379,16 +433,44 @@ scene.onPointerObservable.add((pi) => {
// 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) {
sharedState.runtime.elapsedSeconds += engine.getDeltaTime() / 1000;
}
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 = 0;
sharedState.runtime.message = `Level ${sharedState.config.level} starting.`;
generateLevel();

View File

@@ -9,6 +9,7 @@ export const sharedState = (window.mazeGameState ??= {
runtime: {
runActive: false,
hasKey: false,
roundsCompleted: 0,
elapsedSeconds: 0,
message: "Adjust settings, then start a run.",
},

View File

@@ -1,9 +1,11 @@
import { sharedState } from "./game/state.js";
import { playSfx, primeSfx } from "./sfx.js";
// Handler functions (same as p5_panel but without p5 scoping)
function resetRun(message) {
sharedState.runtime.runActive = true;
sharedState.runtime.hasKey = false;
sharedState.runtime.roundsCompleted = 0;
sharedState.runtime.elapsedSeconds = 0;
sharedState.runtime.message = message;
sharedState.config.level = 1;
@@ -35,35 +37,45 @@ function updateDisplay() {
document.getElementById("status-level").textContent = sharedState.config.level;
document.getElementById("status-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no";
document.getElementById("status-rounds").textContent = sharedState.runtime.roundsCompleted;
document.getElementById("status-message").textContent = sharedState.runtime.message;
}
// Initialize event listeners
document.getElementById("btn-start").addEventListener("click", () => {
primeSfx();
playSfx("click", 0.7);
resetRun("Run started.");
});
document.getElementById("btn-restart").addEventListener("click", () => {
primeSfx();
playSfx("click", 0.7);
restartLevel("Level restarted.");
});
document.getElementById("btn-randomize").addEventListener("click", () => {
primeSfx();
playSfx("click", 0.7);
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;
@@ -74,6 +86,7 @@ setInterval(() => {
if (sharedState.runtime.runActive) {
document.getElementById("status-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no";
document.getElementById("status-rounds").textContent = sharedState.runtime.roundsCompleted;
}
}, 100);

57
src/sfx.js Normal file
View File

@@ -0,0 +1,57 @@
const soundFiles = {
chestClose: "/sfx/sfx_chest_close.wav",
chestOpen: "/sfx/sfx_chest_open.wav",
click: "/sfx/sfx_click.wav",
key: "/sfx/sfx_key.wav",
step: "/sfx/sfx_step.wav",
win: "/sfx/sfx_win.wav",
};
const sounds = {};
for (const [name, filePath] of Object.entries(soundFiles)) {
const audio = new Audio(filePath);
audio.preload = "auto";
sounds[name] = audio;
}
let audioPrimed = false;
function playAudio(audio, volume = 1) {
const instance = audio.cloneNode();
instance.volume = volume;
instance.currentTime = 0;
const promise = instance.play();
if (promise && typeof promise.catch === "function") {
promise.catch(() => {});
}
}
export function primeSfx() {
if (audioPrimed) {
return;
}
audioPrimed = true;
for (const audio of Object.values(sounds)) {
const probe = audio.cloneNode();
probe.volume = 0;
const promise = probe.play();
if (promise && typeof promise.then === "function") {
promise
.then(() => {
probe.pause();
probe.currentTime = 0;
})
.catch(() => {});
}
}
}
export function playSfx(name, volume = 1) {
const audio = sounds[name];
if (!audio) {
return;
}
playAudio(audio, volume);
}