diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1330247 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.log +.DS_Store diff --git a/README.md b/README.md index 83fcde2..0176750 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # 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. +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 @@ -36,88 +38,137 @@ Then open: | 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. +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): 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). +- **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. +- **Play Test**: opens the game in a new tab running your current level + (bypassing the menu). -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. +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 (see below). A level with no exit -instead completes when every guard is defeated. +exit advances to the next level in the campaign. A level with no exit instead +completes when every enemy is defeated. -## Level file format +## 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, and drop -the file in `src/levels/` (see the campaign section). The format is: +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": 1, // schema version (stamped on export) + "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..5 = wall tiles (constants.js TILES) + // 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": [ // may be empty - { "type": "enemy", "x": 8.5, "y": 5.5 }, + "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": "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. +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. -## Campaign — using your files as game levels +## Campaigns -The game plays the level files listed, in order, in the manifest -`src/levels/levels.json`: +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`). -```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`. +`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) +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/ - engine/ Shared core: constants (TILE & entity registries), GameMap, - Raycaster (DDA + ZBuffer), SpriteRenderer, Player, Entity, levelIO - game/ Phaser bootstrap + Boot/Game/Hud scenes + 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/ levels.json (campaign manifest), level1.json / level2.json (levels), - level1.data.js (bundled offline fallback, kept in sync with level1.json) + 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 @@ -134,10 +185,11 @@ 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`; +- **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, multiple levels, menus**: hook into the scenes in `src/game`. +- **Sound, more campaigns**: add campaign entries to `campaigns.json`; hook + audio into the scenes in `src/game`. diff --git a/assets/RAW-Gun.png b/assets/RAW-Gun.png new file mode 100644 index 0000000..231ad9d Binary files /dev/null and b/assets/RAW-Gun.png differ diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..2da59d3 --- /dev/null +++ b/assets/README.md @@ -0,0 +1,30 @@ +# Spritesheets + +> Full art brief — every sheet with dimensions, frame maps, and drawing +> notes — lives in [`sprites.md`](../sprites.md) at the project root. + +Art is optional: any sheet that is missing (or too small for a requested +frame) gets a procedurally generated placeholder baked in its entry's `color` +from the config JSON, so the game always runs. + +## Layout + +Every sheet is a **horizontal strip of square frames**, left to right. The +`frame` field in the config JSON is the 0-based index into the strip. Frames +are **192×192** (walls can override via `textureSize` in `walls.json`; baked +output is always 192×192). + +Expected files (paths are what `src/config/*.json` references — change them +there if you prefer a different arrangement): + +| File | Used by | Notes | +| ----------------------------- | --------------- | ----- | +| `assets/textures/walls.png` | `walls.json` | Opaque. Frame per wall id (frame 0 = Gray Brick, …) | +| `assets/textures/overlays.png`| `overlays.json` | **Transparent PNG** — composited over wall textures | +| `assets/sprites/items.png` | `items.json` | **Transparent PNG** — billboard props (plant, lamp, …) | +| `assets/sprites/enemies.png` | `enemies.json` | **Transparent PNG** — one frame per enemy type for now | + +A 5-frame walls sheet is therefore 960×192; a 3-frame enemies sheet 576×192. + +You can also point different entries at different files — each unique +`texture` path is loaded once and shared. diff --git a/assets/gun1.png b/assets/gun1.png new file mode 100644 index 0000000..8a86b63 Binary files /dev/null and b/assets/gun1.png differ diff --git a/assets/gun2.png b/assets/gun2.png new file mode 100644 index 0000000..494ab13 Binary files /dev/null and b/assets/gun2.png differ diff --git a/assets/gun3.png b/assets/gun3.png new file mode 100644 index 0000000..2a1ee63 Binary files /dev/null and b/assets/gun3.png differ diff --git a/assets/gun4.png b/assets/gun4.png new file mode 100644 index 0000000..5d6d579 Binary files /dev/null and b/assets/gun4.png differ diff --git a/assets/gun5.png b/assets/gun5.png new file mode 100644 index 0000000..9f6630d Binary files /dev/null and b/assets/gun5.png differ diff --git a/assets/textures/overlays.png b/assets/textures/overlays.png new file mode 100644 index 0000000..3c899fc Binary files /dev/null and b/assets/textures/overlays.png differ diff --git a/assets/textures/overlays.psd b/assets/textures/overlays.psd new file mode 100644 index 0000000..37d9238 Binary files /dev/null and b/assets/textures/overlays.psd differ diff --git a/assets/textures/walls.png b/assets/textures/walls.png new file mode 100644 index 0000000..2bdb7fe Binary files /dev/null and b/assets/textures/walls.png differ diff --git a/assets/textures/walls.psd b/assets/textures/walls.psd new file mode 100644 index 0000000..ecfa43e Binary files /dev/null and b/assets/textures/walls.psd differ diff --git a/editor.html b/editor.html index 57be572..9859ff8 100644 --- a/editor.html +++ b/editor.html @@ -53,6 +53,54 @@ font-size: 12px; color: #9aa0a6; } + /* Brush palette: dropdowns populated from the src/config JSON files. */ + #palette { + position: absolute; + top: 44px; + left: 0; + right: 0; + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + padding: 6px 12px; + background: rgba(20, 20, 24, 0.92); + border-bottom: 1px solid #3a3a42; + z-index: 9; + font-size: 12px; + } + #palette label { + display: flex; + gap: 4px; + align-items: center; + color: #9aa0a6; + } + #palette select { + background: #2d2d36; + color: #e6e6e6; + border: 1px solid #45454f; + border-radius: 4px; + padding: 4px 6px; + font-size: 12px; + cursor: pointer; + } + #palette button { + background: #2d2d36; + color: #e6e6e6; + border: 1px solid #45454f; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 12px; + } + #palette .sep { + width: 1px; + align-self: stretch; + background: #3a3a42; + } + #palette .active-tool { + outline: 2px solid #ffe066; + } @@ -66,6 +114,18 @@ + +
+ + + + + + + + +