add sounds and textures
This commit is contained in:
212
PROPOSAL_20240905.md
Normal file
212
PROPOSAL_20240905.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Untitled Maze Game - ID30011 Midterm Project Proposal (Revised)
|
||||
|
||||
- **Name:** Bumgyu Suh
|
||||
- **Student ID:** 20240905
|
||||
- **Repository URL:** https://git.prototyping.id/20240905/homework5
|
||||
- **Engine:** Babylon.js
|
||||
|
||||
## 1. Project Summary
|
||||
|
||||
Untitled Maze Game is a 3D first-person dungeon maze game. Each level generates a new maze, and the player must find the key hidden in one chest, then reach the exit area to progress.
|
||||
|
||||
Main twist:
|
||||
- Opening a chest that has already been opened in the current level causes immediate game over.
|
||||
- This memory pressure creates the core challenge.
|
||||
|
||||
Scoring tracked for now:
|
||||
- Total time spent
|
||||
- Progress (level reached)
|
||||
|
||||
## 2. Finalized Design Decisions
|
||||
|
||||
These are locked decisions for implementation:
|
||||
|
||||
1. Chest reset scope:
|
||||
- Opened chest state resets every level.
|
||||
|
||||
2. Chest interaction:
|
||||
- Player must left click while targeting a chest.
|
||||
- Interaction prompt shown: "Click to open chest" when chest is targetable.
|
||||
|
||||
3. Maze topology:
|
||||
- Maze is allowed to contain loops (not necessarily a perfect maze).
|
||||
- Each level must guarantee a minimum number of dead-end cells with chests.
|
||||
- Additional dead-ends without chests are allowed.
|
||||
|
||||
4. Difficulty scaling:
|
||||
- Increase maze width and height by level.
|
||||
- Lighting settings stay fixed (no light-based difficulty scaling).
|
||||
|
||||
5. Win condition:
|
||||
- Exit is a highlighted zone/area.
|
||||
- Entering exit zone with key automatically transitions to next level.
|
||||
|
||||
6. Visual implementation order:
|
||||
- Start with primitive meshes for all gameplay-critical elements.
|
||||
|
||||
## 3. Gameplay Rules
|
||||
|
||||
## Objective
|
||||
- Search chests to find the key.
|
||||
- Avoid reopening previously opened chests in that level.
|
||||
- Reach exit area after obtaining key.
|
||||
- Clear levels as fast as possible.
|
||||
|
||||
## Win / Lose Conditions
|
||||
- Win level: player enters exit zone while holding key.
|
||||
- Soft block: entering exit without key shows message and does not transition.
|
||||
- Lose run: player opens a chest already opened in current level.
|
||||
|
||||
## Player Controls
|
||||
- `W/A/S/D`: movement
|
||||
- Mouse: look direction
|
||||
- Left click: open chest when targeted
|
||||
|
||||
## 4. Technical Architecture Plan
|
||||
|
||||
To keep implementation clean, split into four layers:
|
||||
|
||||
1. Maze Logic Layer (pure functions)
|
||||
- Generate grid layout
|
||||
- Detect dead-ends
|
||||
- Place chests, key chest, exit, and spawn
|
||||
- Return plain data only
|
||||
|
||||
2. Game State Layer
|
||||
- Track current level
|
||||
- Track timer / elapsed time
|
||||
- Track hasKey
|
||||
- Track chest open state (per level)
|
||||
- Resolve win/lose transitions
|
||||
|
||||
3. Babylon Scene Layer
|
||||
- Build meshes from grid
|
||||
- Configure first-person camera and collisions
|
||||
- Handle raycast/pick chest targeting and click interaction
|
||||
- Render highlighted exit zone
|
||||
|
||||
4. UI Layer
|
||||
- HUD: level, time, key possession, prompt/status text
|
||||
- Messages: wrong chest, key found, find key first, game over, level clear
|
||||
|
||||
## 5. Data Model (Revised)
|
||||
|
||||
Use separate static and dynamic state instead of overloading one numeric ID.
|
||||
|
||||
## Static cell data
|
||||
- `cellType`: wall | path | chest | exit | spawn
|
||||
|
||||
## Dynamic state
|
||||
- `openedChests`: set/map keyed by cell coordinate
|
||||
- `keyChestCoord`: coordinate of the one correct chest
|
||||
- `hasKey`: boolean
|
||||
|
||||
Benefits:
|
||||
- Cleaner logic
|
||||
- Easier debugging
|
||||
- Lower chance of state bugs from mixed meanings
|
||||
|
||||
## 6. Level Generation Specification
|
||||
|
||||
Per level:
|
||||
|
||||
1. Compute maze dimensions from level number.
|
||||
2. Generate a solvable maze grid (loops allowed).
|
||||
3. Find dead-end candidates.
|
||||
4. Place at least `minChestDeadEnds` chests on dead-ends.
|
||||
5. Choose one chest as key chest.
|
||||
6. Choose one exit location (highlighted zone).
|
||||
7. Choose valid spawn point.
|
||||
8. Validate reachability:
|
||||
- Spawn can reach key chest
|
||||
- Spawn (after key) can reach exit
|
||||
|
||||
If validation fails, regenerate.
|
||||
|
||||
## 7. Babylon.js Implementation Notes
|
||||
|
||||
- Camera: use first-person style camera (for example `UniversalCamera`) with pointer lock.
|
||||
- Collision: enable gravity/collision with wall meshes.
|
||||
- Interaction: center-screen ray pick + left click.
|
||||
- Chest meshes: primitive boxes in MVP.
|
||||
- Exit zone: highlighted plane or emissive ground area.
|
||||
- Keep one scene; rebuild level meshes on level transition.
|
||||
|
||||
## 8. MVP Checklist (Implementation Order)
|
||||
|
||||
## Phase A - Core scaffold
|
||||
- [ ] Refactor current Babylon template into modular files/functions.
|
||||
- [ ] Add first-person camera controls and pointer lock.
|
||||
- [ ] Add movement collision against wall meshes.
|
||||
|
||||
## Phase B - Maze system
|
||||
- [ ] Implement maze generation with loops allowed.
|
||||
- [ ] Implement dead-end detection.
|
||||
- [ ] Implement chest placement with minimum dead-end chest count.
|
||||
- [ ] Implement key chest assignment and exit placement.
|
||||
- [ ] Implement reachability validation and regenerate on failure.
|
||||
|
||||
## Phase C - Gameplay rules
|
||||
- [ ] Implement per-level chest open tracking.
|
||||
- [ ] Implement chest click interaction and "Click to open chest" prompt.
|
||||
- [ ] Implement outcomes: wrong chest / key found / reopened chest game over.
|
||||
- [ ] Implement exit-zone behavior: block without key, auto-next-level with key.
|
||||
|
||||
## Phase D - UI and scoring
|
||||
- [ ] Display level number.
|
||||
- [ ] Display elapsed total time.
|
||||
- [ ] Display key possession status.
|
||||
- [ ] Display gameplay feedback messages.
|
||||
|
||||
## Phase E - Level progression
|
||||
- [ ] Increase maze width/height per level.
|
||||
- [ ] Reset per-level state correctly on transition.
|
||||
- [ ] Preserve run-level state (time, current level progression).
|
||||
|
||||
## 9. Testing Checklist
|
||||
|
||||
- [ ] Reopening an opened chest always ends run.
|
||||
- [ ] Chest-open state resets at new level.
|
||||
- [ ] Entering exit without key never transitions.
|
||||
- [ ] Entering exit with key always transitions.
|
||||
- [ ] Every generated level is solvable.
|
||||
- [ ] Minimum chest dead-end count is always satisfied.
|
||||
- [ ] Performance remains stable across first several levels.
|
||||
|
||||
## 10. Assets Plan
|
||||
|
||||
Immediate plan (MVP):
|
||||
- Use primitives for wall, floor, chest, and exit zone.
|
||||
- Use simple material colors to distinguish gameplay objects.
|
||||
|
||||
Prepare soon (after core loop works):
|
||||
- Tileable wall texture
|
||||
- Tileable floor texture
|
||||
- Chest texture/material
|
||||
- Basic SFX set:
|
||||
- chest open
|
||||
- wrong chest
|
||||
- key found
|
||||
- level clear
|
||||
- game over
|
||||
|
||||
Optional polish later:
|
||||
- Better chest model
|
||||
- Exit marker model
|
||||
- Ambient loop audio
|
||||
|
||||
## 11. Class Concept Coverage
|
||||
|
||||
This project uses:
|
||||
- JavaScript arrays and object state
|
||||
- Functional decomposition in generation pipeline
|
||||
- Event handling (keyboard/mouse)
|
||||
- External library usage (Babylon.js)
|
||||
- Real-time tracking (elapsed time HUD)
|
||||
|
||||
## 12. Scope Guardrails
|
||||
|
||||
To keep delivery reliable:
|
||||
- Prioritize clean gameplay loop over art polish.
|
||||
- Keep lighting simple and fixed.
|
||||
- Do not add non-essential mechanics before MVP checklist is complete.
|
||||
34
ZZREADME.md
Normal file
34
ZZREADME.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Untitled Maze Game - ID30011 Midterm Project README
|
||||
|
||||
- **Name:** Bumgyu Suh
|
||||
- **Student ID:** 20240905
|
||||
- **Student Email:** bumgyu@kaist.ac.kr
|
||||
- **Repository URL:** https://git.prototyping.id/20240905/???
|
||||
- **Video URL:** youtube.com
|
||||
|
||||
## The Game
|
||||
A description of the game - how it works and what the user has to do
|
||||
|
||||
WASD, V to switch view
|
||||
|
||||
## Code Documentation
|
||||
A description of the organization of your code. Feel free to use diagrams, UML, or others. What are the main functions/classes? If you used patterns, what did you use them for, and how do different parts of your code speak to each other?
|
||||
Highlight any issue you want us to know about or whether the code has any known bug. If there are special features you want us to know about, write them here
|
||||
|
||||
Acknowledge any help or resource you used
|
||||
Writing style might be considered in grading (not the grammar, but rather the clarity of your writing)
|
||||
Be visual so use images and tables
|
||||
Try to be complete in your explanation - you do not need to write a lot, but the professor and the TA should be able to understand your documentation and your code by reading this file.
|
||||
|
||||
## Help from AI
|
||||
I got help from AI in criticizing my original plan in structuring my code. For example, although I originally planned to have a single 2D array that stores both the "static" information (the generated maze), and the state of whether the chests have been open or not, but chatgpt suggested that separating these two (one static, one dynamically changing during gameplay) is better as it prevents everything being coupled to this array.
|
||||
It also suggested ot
|
||||
|
||||
The game can be played through the multi_sketch.html
|
||||
"Start run" to generate first leve, etc, seed,
|
||||
left0click chests
|
||||
|
||||
How well and complete is your documentation? Your documentation should contain the following information:
|
||||
|
||||
Write a README.md file that contains the following information.
|
||||
|
||||
@@ -20,10 +20,12 @@ canvas {
|
||||
}
|
||||
|
||||
.maze-layout {
|
||||
width: min(1100px, calc(100vw - 24px));
|
||||
width: min(1200px, calc(100vw - 24px));
|
||||
margin: 12px auto 20px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@@ -45,7 +47,7 @@ canvas {
|
||||
|
||||
#renderCanvas {
|
||||
width: 100%;
|
||||
height: min(62vh, 680px);
|
||||
height: min(76vh, 820px);
|
||||
}
|
||||
|
||||
.canvas-hud {
|
||||
@@ -78,6 +80,28 @@ canvas {
|
||||
text-shadow: 0 0 12px rgba(121, 174, 242, 0.35);
|
||||
}
|
||||
|
||||
.canvas-hud-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 11px;
|
||||
color: #dbe6f2;
|
||||
}
|
||||
|
||||
.canvas-hud-row span {
|
||||
color: #93a4b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.canvas-hud-row strong {
|
||||
color: #79aef2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
@@ -223,6 +247,7 @@ canvas {
|
||||
.maze-layout {
|
||||
width: calc(100vw - 16px);
|
||||
margin: 8px auto 16px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#renderCanvas {
|
||||
|
||||
BIN
favicon.png
Normal file
BIN
favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 401 B |
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
|
||||
<path fill="#ED225D" stroke="#ED225D" stroke-miterlimit="10" d="M16.909,10.259l8.533-2.576l1.676,5.156l-8.498,2.899l5.275,7.48
|
||||
l-4.447,3.225l-5.553-7.348L8.487,26.25l-4.318-3.289l5.275-7.223L0.88,12.647l1.678-5.16l8.598,2.771V1.364h5.754V10.259z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 692 B |
BIN
img/img_chest.png
Normal file
BIN
img/img_chest.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
img/img_ground.png
Normal file
BIN
img/img_ground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
img/img_wall.png
Normal file
BIN
img/img_wall.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/style.css" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Untitled Maze Game</title>
|
||||
</head>
|
||||
@@ -16,6 +16,8 @@
|
||||
<div class="canvas-hud">
|
||||
<div class="canvas-hud-label">Elapsed time</div>
|
||||
<div id="canvas-time" class="canvas-hud-value">0.0s</div>
|
||||
<div class="canvas-hud-row"><span>Has key</span><strong id="canvas-key">no</strong></div>
|
||||
<div class="canvas-hud-row"><span>Rounds</span><strong id="canvas-rounds">0</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -61,6 +63,7 @@
|
||||
<div class="status-line"><strong>Level:</strong> <span id="status-level">1</span></div>
|
||||
<div class="status-line"><strong>Time:</strong> <span id="status-time">0.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>
|
||||
|
||||
BIN
sfx/sfx_chest_close.wav
Normal file
BIN
sfx/sfx_chest_close.wav
Normal file
Binary file not shown.
BIN
sfx/sfx_chest_open.wav
Normal file
BIN
sfx/sfx_chest_open.wav
Normal file
Binary file not shown.
BIN
sfx/sfx_click.wav
Normal file
BIN
sfx/sfx_click.wav
Normal file
Binary file not shown.
BIN
sfx/sfx_clock.wav
Normal file
BIN
sfx/sfx_clock.wav
Normal file
Binary file not shown.
BIN
sfx/sfx_key.wav
Normal file
BIN
sfx/sfx_key.wav
Normal file
Binary file not shown.
BIN
sfx/sfx_lose.wav
Normal file
BIN
sfx/sfx_lose.wav
Normal file
Binary file not shown.
BIN
sfx/sfx_step.wav
Normal file
BIN
sfx/sfx_step.wav
Normal file
Binary file not shown.
BIN
sfx/sfx_win.wav
Normal file
BIN
sfx/sfx_win.wav
Normal file
Binary file not shown.
@@ -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();
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
|
||||
@@ -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
57
src/sfx.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user