escape/README.md

196 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` |
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`, `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) |
| `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.
- Pickups (`ammo`, `health`, `treasure`) and the `exit` are built-in because
they have hardcoded game effects.
- 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, and **Erase** tool buttons.
Focusing a dropdown or clicking a button makes it the active brush.
- **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`.
- **Doors / keys**: door tiles are flagged `isDoor` in `walls.json`;
wire sliding + locked-door logic into `GameMap`/`Raycaster`.
- **Sound, more campaigns**: add campaign entries to `campaigns.json`; hook
audio into the scenes in `src/game`.