213 lines
11 KiB
Markdown
213 lines
11 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, with
|
||
textured walls, wall overlays (paintings etc.), decorative items, and multiple
|
||
enemy types — **all defined in JSON config files**, so adding content requires
|
||
no code changes. Until you drop in real spritesheets, every texture gets a
|
||
procedurally generated placeholder so the game always runs.
|
||
|
||
## 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` |
|
||
| Open / close door | `E` |
|
||
|
||
The game opens on a **main menu** listing the campaigns from
|
||
`src/config/campaigns.json`. Pick one to play its levels in order; finishing
|
||
the last level shows the victory screen with a **Return to Menu** button.
|
||
The HUD shows health, ammo, and score. Walk over pickups to collect them; each
|
||
enemy type is worth its configured points. Click the view once to capture the
|
||
mouse for look controls.
|
||
|
||
## Content is data-driven — `src/config/`
|
||
|
||
All content types live in JSON registries. **Adding an entry to a file is all
|
||
it takes** — it appears in the editor's dropdowns and renders in game (with
|
||
placeholder art until you provide a spritesheet):
|
||
|
||
| File | Defines | Key fields |
|
||
| ---------------- | ---------------------- | ---------- |
|
||
| `walls.json` | Wall tiles (ids 1+) | `id` (int, used in level `tiles`), `name`, `solid`, `isDoor`, `key` (key id a locked door needs, e.g. `"gold"`), `color` (`#rrggbb`, placeholder/editor tint), `texture` (sheet path), `frame` |
|
||
| `overlays.json` | Wall overlays | `id` (string), `name`, `color`, `texture`, `frame` — transparent PNGs composited over the wall texture |
|
||
| `items.json` | Decorative items | `id`, `name`, `color`, `blocking` (true = its cell is impassable), `scale` (height as a fraction of wall height), `texture`, `frame` |
|
||
| `enemies.json` | Enemy types | `type`, `name`, `color`, `health`, `speed`, `points`, `texture`, `frame`; plus top-level `defaultType` (what v1 levels' generic "enemy" migrates to) |
|
||
| `pickups.json` | Pickups & keys | `id` (used as the entity type), `name`, `color`, `effect` (`ammo` \| `health` \| `score` \| `key`), `amount`, `key` (key id granted when `effect` is `key`), `texture`, `frame` |
|
||
| `campaigns.json` | Campaigns for the menu | `id`, `name`, `description`, `levels` (ordered filenames in `src/levels/`) |
|
||
|
||
Notes:
|
||
|
||
- Tile id `0` (Empty floor) is built in; don't define it.
|
||
- Overlays sit on **wall cells** (one per cell, shown on every face) — think
|
||
paintings, banners, vines.
|
||
- Items sit on **floor cells**. Blocking items stop the player like a wall but
|
||
are not walls: shots and sight pass over them.
|
||
- **Doors** (`isDoor: true` walls) slide open Wolf3D-style when the player
|
||
presses `E` facing them, stay open a few seconds, then close (never on top
|
||
of the player). Give a door a `key` (e.g. `"gold"`) to lock it — the player
|
||
must first collect a pickup whose `effect` is `"key"` with the matching
|
||
`key` id. Collected keys show as colored chips in the HUD and reset each
|
||
level. The stock set ships an unlocked Door (id 5), a Gold Door (id 6), and
|
||
a Silver Door (id 7).
|
||
- Pickup effects (`ammo`/`health`/`score`/`key`) and amounts live in
|
||
`pickups.json`; only the `exit` is built-in because ending the level is a
|
||
game mechanic.
|
||
- Each config file has an embedded fallback copy in `src/engine/registry.js`,
|
||
so a missing/broken file can't stop the game booting. Keep them in sync if
|
||
you change defaults you care about offline.
|
||
|
||
### Spritesheets — dropping in real art
|
||
|
||
Every definition points at a `texture` path (relative to the project root) and
|
||
a `frame` index. Sheets are **horizontal strips of square frames** (192×192 by
|
||
default; `walls.json` has a `textureSize` for other sizes). If the PNG is
|
||
missing, the engine bakes a distinctive placeholder pattern in the entry's
|
||
`color` instead — so you can build and play everything first and do art last.
|
||
**See `sprites.md` for the full list of sheets to draw, with exact dimensions
|
||
and frame maps.** Overlay/item/enemy sheets should be transparent PNGs.
|
||
|
||
## Level editor (`/editor.html`)
|
||
|
||
- **Palette** (top): dropdowns for **Wall / Overlay / Item / Enemy** (populated
|
||
from the config JSONs) plus **Start**, pickup, **Path**, and **Erase** tool
|
||
buttons. Focusing a dropdown or clicking a button makes it the active brush.
|
||
- **Path tool**: click an enemy to select it (yellow ring), then click floor
|
||
cells to append patrol points. Click an existing point for a **Move /
|
||
Remove** menu (Move = the next click relocates it). Right-click deselects.
|
||
Paths draw as looped lines — faint for all enemies, highlighted for the
|
||
selection. In game the enemy walks spawn → points → spawn in a loop at its
|
||
`speed` from `enemies.json` (cells/sec), pausing briefly when shot. Paths
|
||
are author-trusted: one drawn through a wall will walk through it.
|
||
- **Left-click / drag**: paint the selected brush. Overlays only apply to wall
|
||
cells; items only to floor cells. Painting a wall clears anything inside it;
|
||
painting floor clears the cell's overlay.
|
||
- **Right-click / drag**: erase (clears the tile, overlay, and any entity/item).
|
||
- **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
|
||
(bypassing the menu).
|
||
|
||
In the grid: overlays show as a corner triangle on their wall cell, items are
|
||
squares, enemies are large circles, pickups small circles, and the player start
|
||
is the blue arrow.
|
||
|
||
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. A level with no exit instead
|
||
completes when every enemy is defeated.
|
||
|
||
## Level file format (v2)
|
||
|
||
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, drop the
|
||
file in `src/levels/`, and list it in a campaign in `src/config/campaigns.json`.
|
||
|
||
```jsonc
|
||
{
|
||
"version": 2, // 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+ = wall ids from walls.json
|
||
"overlays": [ // one per wall cell, shown on all faces
|
||
{ "x": 3, "y": 0, "id": "painting1" }
|
||
],
|
||
"items": [ // decorative sprites on floor cells
|
||
{ "id": "plant", "x": 4.5, "y": 5.5 }
|
||
],
|
||
"playerStart": { "x": 2.5, "y": 3.5, "dir": 0 }, // cell units; dir in radians
|
||
"entities": [ // enemies (types from enemies.json) + pickups
|
||
{ "type": "guard", "x": 8.5, "y": 5.5 },
|
||
{ "type": "heavy", "x": 9.5, "y": 2.5 },
|
||
{ "type": "ammo", "x": 4.5, "y": 4.5 },
|
||
{ "type": "exit", "x": 14.5, "y": 8.5 }
|
||
]
|
||
}
|
||
```
|
||
|
||
Files are validated on load (`parseLevel`) and **v1 files migrate
|
||
automatically** (generic `"enemy"` becomes `enemies.json`'s `defaultType`;
|
||
missing `overlays`/`items` default to empty). Unknown ids fail safe rather
|
||
than crashing.
|
||
|
||
## Campaigns
|
||
|
||
The menu lists the campaigns in `src/config/campaigns.json`; each plays its
|
||
`levels` files in order. Reaching an **Exit** advances to the next file; after
|
||
the last one the player sees the victory screen. Health, ammo, and score carry
|
||
across levels within a campaign. Three campaigns ship as stubs: **Escape**
|
||
(the original two levels), **The Depths**, and **Rooftop Run** (one placeholder
|
||
level each — `c2_level1.json` / `c3_level1.json`).
|
||
|
||
`src/levels/levels.json` remains only as a legacy fallback manifest for booting
|
||
the game scene directly without the menu.
|
||
|
||
## Project layout
|
||
|
||
```
|
||
index.html Game page (import map → vendored Phaser)
|
||
editor.html Editor page (import map → vendored Phaser; palette DOM)
|
||
vendor/ phaser.esm.min.js (vendored so no npm install is needed)
|
||
assets/ Your spritesheets (optional — placeholders bake if absent)
|
||
src/
|
||
config/ walls.json, overlays.json, items.json, enemies.json, campaigns.json
|
||
engine/ Shared core: constants, registry (JSON configs + texture baking),
|
||
GameMap, Raycaster (DDA + textures + overlays + ZBuffer),
|
||
SpriteRenderer, Player, Entity, levelIO
|
||
game/ Phaser bootstrap + Menu/Boot/Game/Hud scenes
|
||
editor/ Phaser bootstrap + EditorScene
|
||
levels/ level files (+ level1.data.js, the bundled offline fallback kept
|
||
in sync with level1.json; levels.json, the legacy manifest)
|
||
```
|
||
|
||
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
|
||
|
||
- **Real art**: drop spritesheets at the paths named in `src/config/*.json`
|
||
(see `assets/README.md`) — zero code changes.
|
||
- **Enemy AI**: flesh out `Entity.update()` (patrol → chase → attack); each
|
||
enemy def already carries a data-driven `speed`.
|
||
- **More door types**: doors + keys are data-driven — add a wall entry with
|
||
`isDoor: true` and a `key`, plus a matching key pickup in `pickups.json`.
|
||
- **Sound, more campaigns**: add campaign entries to `campaigns.json`; hook
|
||
audio into the scenes in `src/game`.
|