144 lines
6.0 KiB
Markdown
144 lines
6.0 KiB
Markdown
# Escape
|
||
|
||
A Wolfenstein-3D-style raycaster game built with **Phaser 4**. The "3D" is an
|
||
authentic raycasting engine: a 2D grid rendered as vertical wall strips.
|
||
Graphics are intentionally stubbed (solid colors / placeholder shapes) so you
|
||
can drop in your own spritesheets later.
|
||
|
||
## Run
|
||
|
||
There is **no build step and no npm dependency**. The game is plain ES-module
|
||
JavaScript; Phaser ships vendored in `vendor/`. Serve the folder with any static
|
||
HTTP server and open it in a modern browser (Chrome/Edge/Firefox/Safari — needs
|
||
[import map](https://caniuse.com/import-maps) support, ~2023+).
|
||
|
||
```bash
|
||
./start_web.sh # python3 -m http.server 3000
|
||
# or any equivalent:
|
||
# npx serve .
|
||
# php -S localhost:3000
|
||
# (drop the folder behind nginx / Apache / GitHub Pages / S3 / …)
|
||
```
|
||
|
||
Then open:
|
||
|
||
- Game: http://localhost:3000/
|
||
- Editor: http://localhost:3000/editor.html
|
||
|
||
> Opening the files via `file://` will **not** work — ES modules require an HTTP
|
||
> origin. Any static server (above) is enough; no server-side code runs.
|
||
|
||
## Game controls
|
||
|
||
| Action | Keys |
|
||
| ----------------- | ------------------------------------- |
|
||
| Move / strafe | `W` `A` `S` `D` (arrows also move) |
|
||
| Turn | `←` `→`, or **mouse** (click to lock) |
|
||
| Shoot | **Left-click** (mouse locked) / `Space` |
|
||
|
||
The HUD shows health, ammo, and score. Walk over pickups to collect them; shoot
|
||
guards to score. Click the view once to capture the mouse for look controls.
|
||
|
||
## Level editor (`/editor.html`)
|
||
|
||
- **Palette** (top): pick a wall tile, the player **Start** marker, or an entity.
|
||
- **Left-click / drag**: paint the selected brush.
|
||
- **Right-click / drag**: erase (clears the tile and any entity in the cell).
|
||
- **Arrow keys**: scroll the view. **Middle-drag**: pan. **Mouse wheel**: zoom.
|
||
- **New**: start a fresh blank grid (default 72×72; see `NEW_LEVEL_WIDTH/HEIGHT`
|
||
in `src/engine/constants.js`). **Save**: persist to browser storage.
|
||
**Export/Import JSON**: files on disk.
|
||
- **Play Test**: opens the game in a new tab running your current level.
|
||
|
||
The grid is large enough to build big levels; only the on-screen cells are
|
||
drawn, so bigger maps stay fast. Scroll with the arrow keys as your level grows
|
||
past the visible area.
|
||
|
||
Place an **Exit** entity in a level so the player can finish it; walking onto the
|
||
exit advances to the next level in the campaign (see below). A level with no exit
|
||
instead completes when every guard is defeated.
|
||
|
||
## Level file format
|
||
|
||
Levels are plain JSON — exactly what the editor's **Export JSON** writes and what
|
||
the game reads. To author levels, paint one in the editor, Export it, and drop
|
||
the file in `src/levels/` (see the campaign section). The format is:
|
||
|
||
```jsonc
|
||
{
|
||
"version": 1, // schema version (stamped on export)
|
||
"name": "Dungeon Entrance", // display name; also the export filename
|
||
"width": 16, // grid columns
|
||
"height": 16, // grid rows
|
||
"tiles": [ /* … */ ], // row-major ints, length MUST equal width*height
|
||
// 0 = floor; 1..5 = wall tiles (constants.js TILES)
|
||
"playerStart": { "x": 2.5, "y": 3.5, "dir": 0 }, // cell units; dir in radians
|
||
"entities": [ // may be empty
|
||
{ "type": "enemy", "x": 8.5, "y": 5.5 },
|
||
{ "type": "ammo", "x": 4.5, "y": 4.5 },
|
||
{ "type": "health", "x": 6.5, "y": 9.5 },
|
||
{ "type": "treasure", "x": 12.5, "y": 2.5 },
|
||
{ "type": "exit", "x": 14.5, "y": 8.5 }
|
||
]
|
||
}
|
||
```
|
||
|
||
`entity.type` and tile ids come from the registries in `src/engine/constants.js`.
|
||
Files are validated on load (`parseLevel`): `tiles.length` must equal
|
||
`width*height` and `playerStart` must be present; unknown tile/entity ids fail
|
||
safe rather than crashing.
|
||
|
||
## Campaign — using your files as game levels
|
||
|
||
The game plays the level files listed, in order, in the manifest
|
||
`src/levels/levels.json`:
|
||
|
||
```jsonc
|
||
{
|
||
"levels": ["level1.json", "level2.json"]
|
||
}
|
||
```
|
||
|
||
To add a level: drop its `.json` in `src/levels/` and add its filename to the
|
||
`levels` array. Reaching an **Exit** advances to the next file; after the last
|
||
one the player sees a victory screen. Health, ammo, and score carry across
|
||
levels. If the manifest can't be fetched, the game falls back to the bundled
|
||
`level1.data.js`.
|
||
|
||
## Project layout
|
||
|
||
```
|
||
index.html Game page (import map → vendored Phaser)
|
||
editor.html Editor page (import map → vendored Phaser)
|
||
vendor/ phaser.esm.min.js (vendored so no npm install is needed)
|
||
src/
|
||
engine/ Shared core: constants (TILE & entity registries), GameMap,
|
||
Raycaster (DDA + ZBuffer), SpriteRenderer, Player, Entity, levelIO
|
||
game/ Phaser bootstrap + Boot/Game/Hud scenes
|
||
editor/ Phaser bootstrap + EditorScene
|
||
levels/ levels.json (campaign manifest), level1.json / level2.json (levels),
|
||
level1.data.js (bundled offline fallback, kept in sync with level1.json)
|
||
```
|
||
|
||
The game and editor are separate HTML entry points that share everything in
|
||
`src/engine`, so a level you paint is exactly what plays. Native ES modules mean
|
||
every relative import includes its `.js` extension, and the bare `phaser`
|
||
specifier is resolved by the `<script type="importmap">` block in each HTML file.
|
||
|
||
## Updating Phaser
|
||
|
||
`vendor/phaser.esm.min.js` is a copy of Phaser's official ESM build. To update,
|
||
download the matching `dist/phaser.esm.min.js` from the
|
||
[Phaser release](https://github.com/phaserjs/phaser/releases) (or `npm pack
|
||
phaser`) and replace the file — no other changes needed.
|
||
|
||
## Where to extend next
|
||
|
||
- **Textures**: swap the solid-color strip in `engine/Raycaster.js` and the
|
||
billboard `fillRect` in `engine/SpriteRenderer.js` for `drawImage` of your
|
||
spritesheet frames. Tile ids already map 1:1 to a texture index.
|
||
- **Enemy AI**: flesh out `Entity.update()` (patrol → chase → attack).
|
||
- **Doors / keys**: tile id 5 is flagged `isDoor` in `engine/constants.js`;
|
||
wire sliding + locked-door logic into `GameMap`/`Raycaster`.
|
||
- **Sound, multiple levels, menus**: hook into the scenes in `src/game`.
|