Compare commits

...

10 Commits

Author SHA1 Message Date
pobadoba
b95438dfd0 screens cleanup 2026-05-10 18:05:17 +09:00
pobadoba
b88f47c70d feat: game over screen 2026-05-10 17:43:03 +09:00
pobadoba
808b2545e2 update GUI 2026-05-10 17:26:54 +09:00
pobadoba
c28a1d1f6a use timer based mechanic instead 2026-05-10 17:13:54 +09:00
pobadoba
9b68630764 add back game over from chest 2026-05-10 17:01:02 +09:00
pobadoba
7e2d6243b2 add sounds and textures 2026-05-10 16:49:59 +09:00
pobadoba
021877902a feat(gui): add time GUI
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 11:16:54 +09:00
pobadoba
47d4ba8bfe refactor(javascript): split babylon_setup into multiple files
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 01:58:12 +09:00
pobadoba
176ad34214 fix(babylon_setup.js): fix exit not generating properly
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 01:47:27 +09:00
pobadoba
4bb46115ca feat(babylon_setup.js): v to change view
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 01:32:35 +09:00
32 changed files with 2134 additions and 381 deletions

212
PROPOSAL_20240905.md Normal file
View 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.

155
README.md
View File

@@ -1,141 +1,34 @@
# P5.js-vite Starter Template 🚀
# Untitled Maze Game - ID30011 Midterm Project README
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
- **Name:** Bumgyu Suh
- **Student ID:** 20240905
- **Student Email:** bumgyu@kaist.ac.kr
- **Repository URL:** https://git.prototyping.id/20240905/???
- **Video URL:** youtube.com
[Vite](https://vitejs.dev/) starter template to scaffold a new [p5.js](https://p5js.org) project.
## The Game
A description of the game - how it works and what the user has to do
This is an unopinionated template; aside from P5.js and Vite, the rest of your project's tools are entirely up to you.
WASD, V to switch view
## Live demo
## 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
For a live demo please [visit this page](https://p5js-vite-demo.surge.sh).
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.
## Installation
## 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
Pull the template files with [degit](https://github.com/Rich-Harris/degit) and install dependencies.
The game can be played through the multi_sketch.html
"Start run" to generate first leve, etc, seed,
left0click chests
```
npx degit makinteract/p5js-vite my-project
How well and complete is your documentation? Your documentation should contain the following information:
cd my-project
npm install
npm run dev
```
Write a README.md file that contains the following information.
## npm scripts
- `npm run dev` - Starts the development server at port [3000](http://localhost:3000/)
- `npm run build` - Builds the application in a `dist` folder
- `npm run preview` - Serves the build files (`dist` folder) locally at port [5000](http://localhost:3000/)
Note that if after this last command you do not see anything, you can use instead this other command:
- `npm run preview --host` - You should then be able to see your files locally at port [5000](http://localhost:3000/)
## A single p5.js sketch
```js
import '../css/style.css';
import { sketch } from 'p5js-wrapper';
sketch.setup = function () {
createCanvas(800, 600);
};
sketch.draw = function () {
background(127); // grey
fill(255, 0, 0); // red
noStroke();
rectMode(CENTER);
rect(width / 2, height / 2, 50, 50);
};
sketch.mousePressed = function () {
console.log(`I am here at ${mouseX}:${mouseY}`);
};
```
And here the body of the html file:
```html
<body>
<script type="module" src="/src/single_sketch.js"></script>
</body>
```
## Multiple p5.js sketches
If you want to use multiple sketches, you need to use a different syntax.
```js
import '../css/style.css';
import { p5 } from 'p5js-wrapper';
let sketch1 = new p5((p) => {
p.setup = () => {
const one = document.getElementById('one');
p.createCanvas(one.clientWidth, one.clientHeight);
};
p.draw = () => {
p.background(100);
};
}, 'one');
// Sketch2
let sketch2 = new p5((p) => {
p.setup = () => {
const two = document.getElementById('two');
p.createCanvas(two.clientWidth, two.clientHeight);
};
p.draw = () => {
p.background(170);
};
}, 'two');
```
This file is expecting two divs in the html file:
```html
<body>
<script type="module" src="/src/multi_sketch.js"></script>
<div id="one"></div>
<div id="two"></div>
</body>
```
## Adding sound
Sound is an [experimental feature](https://github.com/makinteract/p5js-wrapper/blob/main/README_SOUND.md).
Examples usage:
```js
import { sketch } from 'p5js-wrapper';
import 'p5js-wrapper/sound';
import mysound from './mysound.mp3';
let soundEffect;
sketch.setup = function () {
createCanvas(100, 100);
soundEffect = loadSound(mysound);
};
sketch.draw = function () {
background('#eeeeee');
};
// Play sound on click
sketch.mousePressed = function () {
soundEffect.play();
};
```
This example assumes you have a file _mysound.mp3_ in the _src_ folder.
## License
This project is open source and available under the [MIT License](LICENSE).

34
ZZREADME.md Normal file
View 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.

View File

@@ -15,11 +15,24 @@ canvas {
display: block;
}
.canvas-stage {
position: relative;
}
.maze-layout {
width: min(1100px, calc(100vw - 24px));
margin: 12px auto 20px;
width: 100vw;
margin: 0;
display: grid;
gap: 12px;
grid-template-columns: 1fr;
gap: 0;
align-items: start;
min-height: 100vh;
}
.maze-layout > .panel:first-child {
margin: 12px;
border-radius: 16px;
height: calc(100vh - 24px);
}
.panel {
@@ -41,7 +54,194 @@ canvas {
#renderCanvas {
width: 100%;
height: min(62vh, 680px);
height: min(76vh, 820px);
}
.canvas-hud {
position: absolute;
top: 14px;
left: 14px;
z-index: 2;
padding: 10px 12px;
border-radius: 12px;
background: rgba(6, 10, 15, 0.72);
border: 1px solid rgba(121, 174, 242, 0.3);
backdrop-filter: blur(8px);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
pointer-events: none;
}
.canvas-hud-label {
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #93a4b8;
margin-bottom: 4px;
}
.canvas-hud-value {
font-size: 22px;
font-weight: 700;
line-height: 1;
color: #eef5ff;
text-shadow: 0 0 12px rgba(121, 174, 242, 0.35);
transition: all 0.2s ease;
}
.canvas-hud-value.low-time {
color: #ff4444;
text-shadow: 0 0 16px rgba(255, 68, 68, 0.6);
animation: pulse-warning 0.6s infinite;
}
@keyframes pulse-warning {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.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;
}
.game-over-overlay {
position: absolute;
inset: 0;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background: rgba(0, 0, 0, 0.78);
text-align: center;
padding: 20px;
}
.game-over-overlay[hidden] {
display: none;
}
.p5-particles-container {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
.p5-game-over-panel {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
background: rgba(0, 0, 0, 0.95);
z-index: 9999;
}
.p5-game-over-panel[hidden] {
display: none;
pointer-events: none;
}
.p5-start-panel {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
background: rgba(0, 0, 0, 0.95);
z-index: 9999;
}
.p5-start-panel[hidden] {
display: none;
pointer-events: none;
}
.p5-sketch-container {
position: relative;
width: 100%;
height: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.game-over-info {
position: relative;
z-index: 10;
text-align: center;
padding: 20px;
}
.game-over-info .game-over-text {
margin-top: 0;
font-size: clamp(24px, 4vw, 40px);
font-weight: 700;
color: #f3f7ff;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.game-over-info .game-over-subtext {
font-size: 14px;
color: #c9d8ea;
letter-spacing: 0.05em;
}
.game-over-image {
width: min(70%, 460px);
max-height: 52vh;
object-fit: contain;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.22);
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.45);
}
.game-over-text {
margin-top: 4px;
font-size: clamp(24px, 4vw, 40px);
font-weight: 700;
color: #f3f7ff;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.game-over-subtext {
font-size: 14px;
color: #c9d8ea;
letter-spacing: 0.05em;
}
.control-panel {
@@ -50,6 +250,19 @@ canvas {
max-height: 400px;
}
#control-panel-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
margin: 0;
border-radius: 16px 16px 0 0;
max-height: 40vh;
overflow-y: auto;
z-index: 1000;
}
.control-group {
margin-bottom: 20px;
}
@@ -189,6 +402,7 @@ canvas {
.maze-layout {
width: calc(100vw - 16px);
margin: 8px auto 16px;
grid-template-columns: 1fr;
}
#renderCanvas {

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
img/img_door.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
img/img_ground.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
img/img_jobapplication.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
img/img_start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
img/img_wall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -3,18 +3,37 @@
<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>
<body class="maze-page">
<main class="maze-layout">
<section class="panel">
<div class="panel-label">Babylon Scene</div>
<canvas id="renderCanvas"></canvas>
<div class="panel-label">UNTITLED MAZE GAME</div>
<div class="canvas-stage">
<canvas id="renderCanvas"></canvas>
<div class="canvas-hud">
<div class="canvas-hud-label">Time left</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>
<section class="panel">
<section id="p5-start-panel" class="p5-start-panel">
<div id="p5-start-container" class="p5-sketch-container"></div>
</section>
<section id="p5-game-over-panel" class="p5-game-over-panel" hidden>
<div id="p5-sketch-container" class="p5-sketch-container"></div>
<div class="game-over-info">
<div class="game-over-subtext">Press R to play again</div>
</div>
</section>
<section class="panel" id="control-panel-section" hidden>
<div class="panel-label">Game Controls</div>
<div id="control-panel" class="control-panel">
<div class="control-group">
@@ -26,42 +45,25 @@
</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">
<div class="status-line"><strong>Seed:</strong> <span id="status-seed">0</span></div>
<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>s</div>
<div class="status-line"><strong>Maze:</strong> <span id="status-maze-size">11x11</span></div>
<div class="status-line"><strong>Chests:</strong> <span id="status-chests">2</span></div>
<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-message" id="status-message">Adjust settings, then start a run.</div>
<div class="status-line"><strong>Rounds:</strong> <span id="status-rounds">0</span></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>

335
package-lock.json generated
View File

@@ -1,20 +1,36 @@
{
"name": "p5js-vite",
"name": "untitled-maze-game",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "p5js-vite",
"name": "untitled-maze-game",
"version": "0.1.0",
"dependencies": {
"babylonjs": "^9.5.1",
"p5": "^2.2.3",
"p5js-wrapper": "^1.2.3"
},
"devDependencies": {
"vite": "^8.0.10"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@davepagurek/bezier-path": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@davepagurek/bezier-path/-/bezier-path-0.0.7.tgz",
"integrity": "sha512-CVlnCOrV1iy4Z12T756i9l4G6kF7r8uhlnb+xqDemAMmWQB+8Q0b+8VEqIiUfywgZDSiDr18Rm7pZlnA69rE8Q==",
"license": "MIT"
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -49,6 +65,12 @@
"tslib": "^2.4.0"
}
},
"node_modules/@japont/unicode-range": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@japont/unicode-range/-/unicode-range-1.0.0.tgz",
"integrity": "sha512-BckHvA2XdjRBVAWe2uceNuRf78lBeI28kyWEbfr/Q2pE17POkwuZ6WWY/UMv8FL9iBxhW4xfDoNLM9UVZaTeUQ==",
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
@@ -371,6 +393,30 @@
"tslib": "^2.4.0"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/babylonjs": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-9.5.1.tgz",
@@ -378,6 +424,16 @@
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/colorjs.io": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz",
"integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/color"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -388,6 +444,58 @@
"node": ">=8"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -421,6 +529,36 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/gifenc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz",
"integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==",
"license": "MIT"
},
"node_modules/i18next": {
"version": "19.9.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz",
"integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.3.1.tgz",
"integrity": "sha512-KIToAzf8zwWvacgnRwJp63ase26o24AuNUlfNVJ5YZAFmdGhsJpmFClxXPuk9rv1FMI4lnc8zLSqgZPEZMrW4g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.5.5"
}
},
"node_modules/libtess": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/libtess/-/libtess-1.2.2.tgz",
"integrity": "sha512-Nps8HPeVVcsmJxUvFLKVJcCgcz+1ajPTXDVAVPs6+giOQP4AHV31uZFFkh+CKow/bkB7GbZWKmwmit7myaqDSw==",
"license": "SGI-B-2.0"
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -713,10 +851,32 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/omggif": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
"license": "MIT"
},
"node_modules/p5": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/p5/-/p5-1.4.1.tgz",
"integrity": "sha512-3/X+qb0bK2Cg8nuZNpZZvzxkeUSRghOf0S+l8c+U8yIkUTVSbbcV0R8y96rx3InVBVhk8cH9kFC93VlZZElqSw=="
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/p5/-/p5-2.2.3.tgz",
"integrity": "sha512-jz9uy0k3Fcj9vKSOafQlIrpaPZZjO4rAEBZF6dGkbokisshP0M3aFm4qtLHYCoEW1XJSkFaVaOMILCQAQxUHHA==",
"license": "LGPL-2.1",
"dependencies": {
"@davepagurek/bezier-path": "^0.0.7",
"@japont/unicode-range": "^1.0.0",
"acorn": "^8.15.0",
"acorn-walk": "^8.3.4",
"colorjs.io": "^0.6.0",
"escodegen": "^2.1.0",
"gifenc": "^1.0.3",
"i18next": "^19.0.2",
"i18next-browser-languagedetector": "^4.0.1",
"libtess": "^1.2.2",
"omggif": "^1.0.10",
"pako": "^2.1.0",
"zod": "^4.2.1"
}
},
"node_modules/p5js-wrapper": {
"version": "1.2.3",
@@ -726,6 +886,18 @@
"p5": "^1.4.1"
}
},
"node_modules/p5js-wrapper/node_modules/p5": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/p5/-/p5-1.11.13.tgz",
"integrity": "sha512-gfGo4AkyuNMs6Ko7UNFM9K2edqFRGyLrFaYUB+XXF127JVdEPu0BIaC5uDDNJpsRMOD9hJMUpsOH4HkfuNhvhA==",
"license": "LGPL-2.1"
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -809,6 +981,16 @@
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -921,9 +1103,28 @@
"optional": true
}
}
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
},
"dependencies": {
"@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="
},
"@davepagurek/bezier-path": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@davepagurek/bezier-path/-/bezier-path-0.0.7.tgz",
"integrity": "sha512-CVlnCOrV1iy4Z12T756i9l4G6kF7r8uhlnb+xqDemAMmWQB+8Q0b+8VEqIiUfywgZDSiDr18Rm7pZlnA69rE8Q=="
},
"@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -955,6 +1156,11 @@
"tslib": "^2.4.0"
}
},
"@japont/unicode-range": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@japont/unicode-range/-/unicode-range-1.0.0.tgz",
"integrity": "sha512-BckHvA2XdjRBVAWe2uceNuRf78lBeI28kyWEbfr/Q2pE17POkwuZ6WWY/UMv8FL9iBxhW4xfDoNLM9UVZaTeUQ=="
},
"@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
@@ -1097,17 +1303,61 @@
"tslib": "^2.4.0"
}
},
"acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="
},
"acorn-walk": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"requires": {
"acorn": "^8.11.0"
}
},
"babylonjs": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-9.5.1.tgz",
"integrity": "sha512-EXROfSz1k+Mg88Qh/SrElEz1zp5QVFG4H+2OyCZ+uEXe4fFeY667N9kjMx2+6Y2XFCxKxf4q+za6k+B1x1qbGg=="
},
"colorjs.io": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz",
"integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg=="
},
"detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true
},
"escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"requires": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2",
"source-map": "~0.6.1"
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
},
"fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1122,6 +1372,32 @@
"dev": true,
"optional": true
},
"gifenc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz",
"integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw=="
},
"i18next": {
"version": "19.9.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz",
"integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==",
"requires": {
"@babel/runtime": "^7.12.0"
}
},
"i18next-browser-languagedetector": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.3.1.tgz",
"integrity": "sha512-KIToAzf8zwWvacgnRwJp63ase26o24AuNUlfNVJ5YZAFmdGhsJpmFClxXPuk9rv1FMI4lnc8zLSqgZPEZMrW4g==",
"requires": {
"@babel/runtime": "^7.5.5"
}
},
"libtess": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/libtess/-/libtess-1.2.2.tgz",
"integrity": "sha512-Nps8HPeVVcsmJxUvFLKVJcCgcz+1ajPTXDVAVPs6+giOQP4AHV31uZFFkh+CKow/bkB7GbZWKmwmit7myaqDSw=="
},
"lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -1225,10 +1501,30 @@
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true
},
"omggif": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="
},
"p5": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/p5/-/p5-1.4.1.tgz",
"integrity": "sha512-3/X+qb0bK2Cg8nuZNpZZvzxkeUSRghOf0S+l8c+U8yIkUTVSbbcV0R8y96rx3InVBVhk8cH9kFC93VlZZElqSw=="
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/p5/-/p5-2.2.3.tgz",
"integrity": "sha512-jz9uy0k3Fcj9vKSOafQlIrpaPZZjO4rAEBZF6dGkbokisshP0M3aFm4qtLHYCoEW1XJSkFaVaOMILCQAQxUHHA==",
"requires": {
"@davepagurek/bezier-path": "^0.0.7",
"@japont/unicode-range": "^1.0.0",
"acorn": "^8.15.0",
"acorn-walk": "^8.3.4",
"colorjs.io": "^0.6.0",
"escodegen": "^2.1.0",
"gifenc": "^1.0.3",
"i18next": "^19.0.2",
"i18next-browser-languagedetector": "^4.0.1",
"libtess": "^1.2.2",
"omggif": "^1.0.10",
"pako": "^2.1.0",
"zod": "^4.2.1"
}
},
"p5js-wrapper": {
"version": "1.2.3",
@@ -1236,8 +1532,20 @@
"integrity": "sha512-nG4xiyydY7v+q0+/6wbE2gj+L7RPf3FDwM68govCcuyrBckXgrfDy+0SKCTNTGwbpypxjVAu5juU4XOlz4IRPw==",
"requires": {
"p5": "^1.4.1"
},
"dependencies": {
"p5": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/p5/-/p5-1.11.13.tgz",
"integrity": "sha512-gfGo4AkyuNMs6Ko7UNFM9K2edqFRGyLrFaYUB+XXF127JVdEPu0BIaC5uDDNJpsRMOD9hJMUpsOH4HkfuNhvhA=="
}
}
},
"pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1286,6 +1594,12 @@
"@rolldown/pluginutils": "1.0.0-rc.17"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true
},
"source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1322,6 +1636,11 @@
"rolldown": "1.0.0-rc.17",
"tinyglobby": "^0.2.16"
}
},
"zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="
}
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "p5js-vite",
"name": "untitled-maze-game",
"version": "0.1.0",
"scripts": {
"dev": "vite",
@@ -11,6 +11,7 @@
},
"dependencies": {
"babylonjs": "^9.5.1",
"p5": "^2.2.3",
"p5js-wrapper": "^1.2.3"
}
}

BIN
sfx/sfx_chest_close.wav Normal file

Binary file not shown.

BIN
sfx/sfx_chest_open.wav Normal file

Binary file not shown.

BIN
sfx/sfx_click.wav Normal file

Binary file not shown.

BIN
sfx/sfx_clock.wav Normal file

Binary file not shown.

BIN
sfx/sfx_key.wav Normal file

Binary file not shown.

BIN
sfx/sfx_lose.wav Normal file

Binary file not shown.

BIN
sfx/sfx_step.wav Normal file

Binary file not shown.

BIN
sfx/sfx_win.wav Normal file

Binary file not shown.

580
src/babylon_panel.js Normal file
View File

@@ -0,0 +1,580 @@
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 { startParticleSketch, stopParticleSketch, startStartScreenSketch, stopStartScreenSketch } from "./p5_particles.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 p5GameOverPanel = document.getElementById("p5-game-over-panel");
const p5StartPanel = document.getElementById("p5-start-panel");
const controlPanelSection = document.getElementById("control-panel-section");
let controlsVisible = false;
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;
const canvasStage = document.querySelector(".canvas-stage");
if (canvasStage) {
canvasStage.hidden = true;
}
if (p5GameOverPanel) {
p5GameOverPanel.hidden = false;
const sketchContainer = document.getElementById("p5-sketch-container");
if (sketchContainer) {
startParticleSketch(sketchContainer);
}
}
if (document.pointerLockElement === canvas && document.exitPointerLock) {
document.exitPointerLock();
}
}
function hideGameOverScreen() {
gameOverActive = false;
const canvasStage = document.querySelector(".canvas-stage");
if (canvasStage) {
canvasStage.hidden = false;
}
if (p5GameOverPanel) {
p5GameOverPanel.hidden = true;
stopParticleSketch();
}
}
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 === "KeyV" || event.code === "KeyR") {
primeSfx();
}
if (event.code === "KeyB") {
toggleControlsPanel();
return;
}
if (event.code === "KeyR") {
if (gameOverActive) {
restartRunFromGameOver();
} else if (!sharedState.runtime.runActive && p5StartPanel && !p5StartPanel.hidden) {
startRunFromStartScreen();
}
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();
}
}
});
function toggleControlsPanel() {
controlsVisible = !controlsVisible;
if (controlPanelSection) {
controlPanelSection.hidden = !controlsVisible;
}
}
function startRunFromStartScreen() {
if (p5StartPanel && !p5StartPanel.hidden) {
p5StartPanel.hidden = true;
stopStartScreenSketch();
}
sharedState.runtime.runActive = true;
sharedState.runtime.hasKey = false;
sharedState.runtime.roundsCompleted = 0;
sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS;
sharedState.runtime.message = "Game started.";
sharedState.config.level = 1;
generateLevel();
primeSfx();
playSfx("click", 0.7);
}
// Initialize start screen on page load
window.addEventListener("load", () => {
if (p5StartPanel && !p5StartPanel.hidden) {
const startContainer = document.getElementById("p5-start-container");
if (startContainer) {
startStartScreenSketch(startContainer);
}
}
});
// Export shared state for p5 to use
export { sharedState };

View File

@@ -1,25 +1,25 @@
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.",
},
});
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);
@@ -38,19 +38,109 @@ 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", () => {
if (document.pointerLockElement !== canvas) {
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);
// 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);
@@ -62,9 +152,15 @@ 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`;
}
if (canvasKey) {
canvasKey.textContent = sharedState.runtime.hasKey ? "yes" : "no";
}
if (canvasRounds) {
canvasRounds.textContent = String(sharedState.runtime.roundsCompleted);
}
scene.render();
});
@@ -78,10 +174,32 @@ 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) {}
}
@@ -91,74 +209,13 @@ function clearLevelMeshes() {
if (exitBox) { try { exitBox.dispose(); } catch(e){}; exitBox = null; }
if (spawnMarker) { try { spawnMarker.dispose(); } catch(e){}; spawnMarker = null; }
exitGridPos = null;
spawnGridPos = 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 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) {
@@ -172,12 +229,17 @@ function buildLevelFromGrid(grid) {
floor.position = new BABYLON.Vector3(0, 0, 0);
floor.checkCollisions = true;
const fm = new BABYLON.StandardMaterial('floorMat', scene);
fm.diffuseColor = new BABYLON.Color3(0.08, 0.08, 0.09);
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.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++) {
@@ -203,7 +265,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);
@@ -221,7 +284,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;
}
}
@@ -247,85 +312,107 @@ function placeExit(grid, seed) {
exitGridPos = { x, y };
}
const [x,y] = exitGridPos;
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;
// Ground plane indicator
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);
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);
// Tall pillar for visibility
const pillar = BABYLON.MeshBuilder.CreateCylinder('exitPillar', { diameter: cellSize*0.3, height: cellSize*3.0 }, scene);
const pillarMat = new BABYLON.StandardMaterial('exitPillarMat', scene);
pillarMat.diffuseColor = new BABYLON.Color3(0.95, 0.8, 0.1);
pillarMat.emissiveColor = new BABYLON.Color3(0.3, 0.25, 0.05);
pillar.material = pillarMat;
pillar.position = new BABYLON.Vector3(ex, cellSize*0.75, ez);
levelMeshes.push(pillar);
}
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++) {
// Skip walls
if (grid[y][x] !== 0) continue;
// Skip chests
if (chestMap.has(`${x},${y}`)) continue;
// Skip exit
if (exitGridPos && exitGridPos.x === x && exitGridPos.y === y) continue;
// Found valid spawn point
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) {}
// Add spawn marker sphere
if (spawnMarker) {
try { spawnMarker.dispose(); } catch(e) {}
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 };
}
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);
return;
}
}
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 w = Math.max(9, cfg.mazeWidth);
const h = Math.max(9, cfg.mazeHeight);
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, cfg.minChestDeadEnds, seed + cfg.level);
placeChestsOnDeadEnds(grid, dead, chestCount, seed + cfg.level);
placeExit(grid, seed + cfg.level);
spawnCameraAt(grid);
sharedState.runtime.message = `Level ${cfg.level} generated.`;
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 */ });
}
@@ -335,6 +422,7 @@ 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;
@@ -343,16 +431,20 @@ scene.onPointerObservable.add((pi) => {
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;
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.';
@@ -361,17 +453,69 @@ 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;
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.elapsedSeconds = 0;
sharedState.runtime.roundsCompleted += 1;
sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS;
sharedState.runtime.message = `Level ${sharedState.config.level} starting.`;
generateLevel();
}

15
src/game/grid.js Normal file
View File

@@ -0,0 +1,15 @@
import * as BABYLON from "babylonjs";
export function gridCellToWorld(grid, x, y, cellSize) {
const halfW = (grid[0].length * cellSize) / 2;
const halfH = (grid.length * cellSize) / 2;
return new BABYLON.Vector3(
x * cellSize - halfW + cellSize / 2,
0,
y * cellSize - halfH + cellSize / 2,
);
}
export function isWalkableCell(grid, x, y) {
return y >= 0 && y < grid.length && x >= 0 && x < grid[0].length && grid[y][x] === 0;
}

66
src/game/maze.js Normal file
View File

@@ -0,0 +1,66 @@
export 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): 0 path, 1 wall
export 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;
}
export 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;
}

59
src/game/sfx.js Normal file
View File

@@ -0,0 +1,59 @@
const soundFiles = {
chestClose: "/sfx/sfx_chest_close.wav",
chestOpen: "/sfx/sfx_chest_open.wav",
click: "/sfx/sfx_click.wav",
clock: "/sfx/sfx_clock.wav",
key: "/sfx/sfx_key.wav",
lose: "/sfx/sfx_lose.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);
}

16
src/game/state.js Normal file
View File

@@ -0,0 +1,16 @@
export const sharedState = (window.mazeGameState ??= {
config: {
seed: Math.floor(Math.random() * 100000),
level: 1,
mazeWidth: 11,
mazeHeight: 11,
minChestDeadEnds: 2,
},
runtime: {
runActive: false,
hasKey: false,
roundsCompleted: 0,
elapsedSeconds: 60,
message: "Press Start to play.",
},
});

View File

@@ -1,10 +1,12 @@
import { sharedState } from "./babylon_setup.js";
import { sharedState } from "./game/state.js";
import { playSfx, primeSfx } from "./game/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.elapsedSeconds = 0;
sharedState.runtime.roundsCompleted = 0;
sharedState.runtime.elapsedSeconds = 60;
sharedState.runtime.message = message;
sharedState.config.level = 1;
try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); }
@@ -13,7 +15,7 @@ function resetRun(message) {
function restartLevel(message) {
sharedState.runtime.hasKey = false;
sharedState.runtime.elapsedSeconds = 0;
sharedState.runtime.elapsedSeconds = 60;
sharedState.runtime.message = message;
try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); }
updateDisplay();
@@ -27,53 +29,85 @@ 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;
const statusSeed = document.getElementById("status-seed");
const statusLevel = document.getElementById("status-level");
const statusMazeSize = document.getElementById("status-maze-size");
const statusChests = document.getElementById("status-chests");
const statusTime = document.getElementById("status-time");
const statusKey = document.getElementById("status-key");
const statusRounds = document.getElementById("status-rounds");
const statusMessage = document.getElementById("status-message");
document.getElementById("status-seed").textContent = sharedState.config.seed;
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-message").textContent = sharedState.runtime.message;
if (!statusSeed || !statusLevel || !statusMazeSize || !statusChests || !statusTime || !statusKey || !statusRounds || !statusMessage) {
console.warn("Some status display elements are missing from DOM");
return;
}
statusSeed.textContent = sharedState.config.seed;
statusLevel.textContent = sharedState.config.level;
// Calculate effective maze size and chest count based on current level
const roundScale = Math.max(0, sharedState.config.level - 1);
const effectiveWidth = Math.max(9, sharedState.config.mazeWidth + roundScale * 2);
const effectiveHeight = Math.max(9, sharedState.config.mazeHeight + roundScale * 2);
const effectiveChests = Math.max(1, sharedState.config.minChestDeadEnds + roundScale);
statusMazeSize.textContent = `${effectiveWidth}x${effectiveHeight}`;
statusChests.textContent = effectiveChests;
statusTime.textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
statusKey.textContent = sharedState.runtime.hasKey ? "yes" : "no";
statusRounds.textContent = sharedState.runtime.roundsCompleted;
statusMessage.textContent = sharedState.runtime.message;
}
// Initialize event listeners
document.getElementById("btn-start").addEventListener("click", () => {
resetRun("Run started.");
});
const btnStart = document.getElementById("btn-start");
const btnRestart = document.getElementById("btn-restart");
const btnRandomize = document.getElementById("btn-randomize");
document.getElementById("btn-restart").addEventListener("click", () => {
restartLevel("Level restarted.");
});
if (btnStart) {
btnStart.addEventListener("click", () => {
primeSfx();
playSfx("click", 0.7);
resetRun("Run started.");
});
}
document.getElementById("btn-randomize").addEventListener("click", () => {
randomizeSeed();
});
if (btnRestart) {
btnRestart.addEventListener("click", () => {
primeSfx();
playSfx("click", 0.7);
restartLevel("Level restarted.");
});
}
document.getElementById("slider-width").addEventListener("input", (e) => {
const value = parseInt(e.target.value) | 1;
sharedState.config.mazeWidth = value;
document.getElementById("value-width").textContent = value;
});
document.getElementById("slider-height").addEventListener("input", (e) => {
const value = parseInt(e.target.value) | 1;
sharedState.config.mazeHeight = value;
document.getElementById("value-height").textContent = value;
});
document.getElementById("slider-deadends").addEventListener("input", (e) => {
const value = parseInt(e.target.value);
sharedState.config.minChestDeadEnds = value;
document.getElementById("value-deadends").textContent = value;
});
if (btnRandomize) {
btnRandomize.addEventListener("click", () => {
primeSfx();
playSfx("click", 0.7);
randomizeSeed();
});
}
// Update status display on game loop
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";
const roundScale = Math.max(0, sharedState.config.level - 1);
const effectiveWidth = Math.max(9, sharedState.config.mazeWidth + roundScale * 2);
const effectiveHeight = Math.max(9, sharedState.config.mazeHeight + roundScale * 2);
const effectiveChests = Math.max(1, sharedState.config.minChestDeadEnds + roundScale);
const statusMazeSize = document.getElementById("status-maze-size");
const statusChests = document.getElementById("status-chests");
const statusTime = document.getElementById("status-time");
const statusKey = document.getElementById("status-key");
const statusRounds = document.getElementById("status-rounds");
if (statusMazeSize) statusMazeSize.textContent = `${effectiveWidth}x${effectiveHeight}`;
if (statusChests) statusChests.textContent = effectiveChests;
if (statusTime) statusTime.textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
if (statusKey) statusKey.textContent = sharedState.runtime.hasKey ? "yes" : "no";
if (statusRounds) statusRounds.textContent = sharedState.runtime.roundsCompleted;
}
}, 100);

View File

@@ -1,3 +0,0 @@
// Import Babylon.js game logic and HTML control panel
import "./babylon_setup.js";
import "./html_panel.js";

175
src/p5_particles.js Normal file
View File

@@ -0,0 +1,175 @@
import p5 from "p5";
import gameOverImageUrl from "../img/img_jobapplication.png";
import startImageUrl from "../img/img_start.png";
let sketch;
export function startParticleSketch(containerElement) {
if (sketch) {
sketch.remove();
}
sketch = new p5((p) => {
let particles = [];
let gameOverImg;
const imgSize = 50;
const particleCount = 15;
p.setup = async function() {
const width = containerElement.clientWidth || 800;
const height = containerElement.clientHeight || 600;
console.log("p5 setup:", { width, height });
const canv = p.createCanvas(width, height);
canv.parent(containerElement);
// Load image asynchronously
try {
gameOverImg = await p.loadImage(gameOverImageUrl);
console.log("Game over image loaded:", gameOverImg.width, "x", gameOverImg.height);
} catch (err) {
console.error("Failed to load particle image:", err);
}
// Initialize particles with random starting positions
for (let i = 0; i < particleCount; i++) {
particles.push({
x: p.random(width),
y: p.random(height),
vx: p.random(-2, 2),
vy: p.random(-2, 2),
life: 200,
rotation: p.random(p.TWO_PI),
rotationSpeed: p.random(-0.08, 0.08),
scale: p.random(0.7, 1.3),
});
}
console.log("Particles initialized:", particles.length);
};
p.draw = function() {
// Draw background image full screen
if (gameOverImg) {
p.imageMode(p.CORNER);
p.image(gameOverImg, 0, 0, p.width, p.height);
}
// Semi-transparent overlay for visibility
p.background(0, 0, 0, 20);
for (let i = 0; i < particles.length; i++) {
const part = particles[i];
// Update position
part.x += part.vx;
part.y += part.vy;
part.rotation += part.rotationSpeed;
// Bounce off walls with friction
if (part.x < 0 || part.x > p.width) {
part.vx *= -0.8;
part.x = p.constrain(part.x, 0, p.width);
}
if (part.y < 0 || part.y > p.height) {
part.vy *= -0.8;
part.y = p.constrain(part.y, 0, p.height);
}
// Add some gravity
part.vy += 0.1;
part.vy = p.constrain(part.vy, -5, 5);
// Randomly change direction slightly
if (p.random() < 0.03) {
part.vx += p.random(-0.5, 0.5);
part.vy += p.random(-0.5, 0.5);
part.vx = p.constrain(part.vx, -3, 3);
part.vy = p.constrain(part.vy, -3, 3);
}
// Draw particle
p.push();
p.translate(part.x, part.y);
p.rotate(part.rotation);
p.scale(part.scale);
if (gameOverImg) {
p.imageMode(p.CENTER);
p.tint(255, 200);
p.image(gameOverImg, 0, 0, imgSize, imgSize);
}
p.pop();
}
};
p.windowResized = function() {
if (containerElement && containerElement.offsetParent !== null) {
const width = containerElement.clientWidth;
const height = containerElement.clientHeight;
if (width > 0 && height > 0) {
p.resizeCanvas(width, height);
}
}
};
});
}
export function stopParticleSketch() {
if (sketch) {
sketch.remove();
sketch = null;
}
}
let startSketch;
export function startStartScreenSketch(containerElement) {
if (startSketch) {
startSketch.remove();
}
startSketch = new p5((p) => {
let startImg;
p.setup = async function() {
const width = containerElement.clientWidth || 800;
const height = containerElement.clientHeight || 600;
console.log("p5 start screen setup:", { width, height });
const canv = p.createCanvas(width, height);
canv.parent(containerElement);
// Load image asynchronously
try {
startImg = await p.loadImage(startImageUrl);
console.log("Start image loaded:", startImg.width, "x", startImg.height);
} catch (err) {
console.error("Failed to load start image:", err);
}
};
p.draw = function() {
// Draw background image full screen
if (startImg) {
p.imageMode(p.CORNER);
p.image(startImg, 0, 0, p.width, p.height);
}
};
p.windowResized = function() {
if (containerElement && containerElement.offsetParent !== null) {
const width = containerElement.clientWidth;
const height = containerElement.clientHeight;
if (width > 0 && height > 0) {
p.resizeCanvas(width, height);
}
}
};
});
}
export function stopStartScreenSketch() {
if (startSketch) {
startSketch.remove();
startSketch = null;
}
}