Go to file
Brian Fertig c067a35245 feat: add BT_level1 and new overlay types (cell, wsmi)
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.
2026-07-04 22:35:52 -06:00
assets feat: add BT_level1 and new overlay types (cell, wsmi) 2026-07-04 22:35:52 -06:00
src feat: add BT_level1 and new overlay types (cell, wsmi) 2026-07-04 22:35:52 -06:00
vendor first commit 2026-06-30 22:54:04 -06:00
.gitignore feat: data-driven content with JSON configs, textured walls, and editor overhaul 2026-07-01 22:14:37 -06:00
README.md feat: add door/keys system, patrol paths, and data-driven pickups 2026-07-01 23:08:48 -06:00
editor.html feat: add door/keys system, patrol paths, and data-driven pickups 2026-07-01 23:08:48 -06:00
index.html first commit 2026-06-30 22:54:04 -06:00
sprites.md feat: data-driven weapon system, projectiles, and enemy AI 2026-07-02 18:11:10 -06:00
start_web.sh first commit 2026-06-30 22:54:04 -06:00

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:

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.

{
  "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 (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.