Add new level BT_level1.json featuring guards, enemies, and interactive elements. Introduce "cell" and "wsmi" overlay configurations in src/config/overlays.json. Update overlay textures (overlays.png, overlays.psd) to support the new overlay frames. |
||
|---|---|---|
| assets | ||
| src | ||
| vendor | ||
| .gitignore | ||
| README.md | ||
| editor.html | ||
| index.html | ||
| sprites.md | ||
| start_web.sh | ||
README.md
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 support, ~2023+).
./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: truewalls) slide open Wolf3D-style when the player pressesEfacing them, stay open a few seconds, then close (never on top of the player). Give a door akey(e.g."gold") to lock it — the player must first collect a pickup whoseeffectis"key"with the matchingkeyid. 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 inpickups.json; only theexitis 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
speedfromenemies.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/HEIGHTinsrc/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.
{
"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 (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(seeassets/README.md) — zero code changes. - Enemy AI: flesh out
Entity.update()(patrol → chase → attack); each enemy def already carries a data-drivenspeed. - More door types: doors + keys are data-driven — add a wall entry with
isDoor: trueand akey, plus a matching key pickup inpickups.json. - Sound, more campaigns: add campaign entries to
campaigns.json; hook audio into the scenes insrc/game.