# Sunken Atrium — a single-room metroidvania demo

A small, self-contained vertical slice built to show off a hand-painted
metroidvania feel in a static web app. One room, one ability gate, one enemy.
Vanilla TypeScript + canvas, no game engine.

![title](docs/title.png)

## The slice

- **One camera-bounded room**, ~1.5× the viewport wide, so there's a bit to explore.
- **One ability gate**: a raised plateau whose cliff is too tall for a normal
  jump. You can't reach the exit until you collect the **double-jump wings**.
- **One patrolling enemy** (a stone beetle-crab) that deals contact damage.
- Player can **move, jump, take damage, die, and respawn at the room entry**
  (abilities persist across death, metroidvania-style).
- Win by reaching the **exit portal** on the plateau.

## Controls

| Action | Keys |
| --- | --- |
| Move | `←` `→` or `A` `D` |
| Jump (hold for height) | `Space` / `W` / `↑` |
| Double jump | jump again in mid-air (after collecting the wings) |
| Start / restart | `Space` / `Enter` / `R` |

## Run it

```bash
npm install
npm run gen:sprites   # optional — PNGs are cached in public/sprites/ (needs OPENAI_API_KEY)
npm run dev           # play locally with HMR

npm run build         # type-check + bundle to dist/
python3 -m http.server 8000 -d dist   # dist serves cleanly from any static host

npm run test:e2e      # Playwright: builds dist, serves it, plays through the demo
```

`base: './'` is set so the built `dist/` works behind a reverse proxy / from a subpath.

## Sprites — generated with `gpt-image-2`

`scripts/gen-sprites.mjs` is a build-time script that calls OpenAI's
**`gpt-image-2`** and writes PNGs into `public/sprites/`. It **caches on disk** —
a sprite is only generated if its file is missing, so reruns never re-bill.

`gpt-image` has no reliable alpha channel, so cutout sprites are generated on a
solid **pure-magenta `#FF00FF`** field and **chroma-keyed at load time**
(`src/assets.ts`: key magenta → transparent, despill the edge halo, crop to a
tight bounding box). Full-bleed art (background, title) is opaque and used as-is.

| sprite | size | chroma-keyed | use |
| --- | --- | --- | --- |
| `player-idle` | 1024² | ✅ magenta | hero (mirrored + squash/stretch by code) |
| `enemy-crawler` | 1024² | ✅ magenta | patrolling enemy |
| `pickup-wings` | 1024² | ✅ magenta | double-jump ability pickup |
| `platform-slab` | 1024² | ✅ magenta | mossy stone, tiled as ground/platform caps |
| `background` | 1024×768 | — full-bleed | parallax room backdrop |
| `title-art` | 1024×1536 | — full-bleed | title-screen key art |

**6 images generated, 0 failed.** Estimated image-gen cost: **~$0.25–0.39**
(my per-size heuristic says $0.25; pricing the captured output image tokens at
gpt-image-1 rates gives ~$0.39 — gpt-image-2's exact price isn't published).
Either way it's within the $0.40 / 10-image cap. See `public/sprites/gen-summary.json`.

If image generation ever fails (API down, bad key, rate limit), the loader marks
that sprite `fallback` and the renderer draws a hand-coded procedural stand-in,
so the game always runs.

## Generated vs. drawn-by-code

- **Generated (`gpt-image-2`)** — visual identity: hero, enemy, ability pickup,
  background plate, title art, platform texture.
- **Procedural / drawn-by-code** — everything that has to feel responsive:
  physics & hitboxes, landing dust, air-jump sparkle, damage flash & knockback,
  ambient motes, HUD (hearts / ability chip / room name), the ability-gate
  chevron shimmer, the exit portal glow, contact shadows, screen shake, vignette,
  and the title / death / win screens.

## Project layout

```
scripts/gen-sprites.mjs   sprite generation (gpt-image-2 + on-disk cache)
src/config.ts             tunables (physics, palette, combat)
src/assets.ts             image load + chroma key + procedural fallbacks
src/input.ts              keyboard (held + edge-triggered, coyote/buffer-friendly)
src/level.ts              the room: solids, spawn, patrol, pickup, gate, exit
src/entities.ts           Player + Enemy (physics, collision, damage, rendering)
src/particles.ts          dust / sparkle / burst / ambient motes
src/game.ts               state machine, camera, HUD, gate, screens, debug hook
src/main.ts               bootstrap + canvas scaling
e2e/game.spec.ts          end-to-end playthrough test
```
