fertig-classic-games/docs/worms-build-plan.md

213 lines
12 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.

# Worms (Worms Armageddon) — build plan
Living plan doc. Survives context clears: **read this file first**, pick the next unchecked
item, update the checkboxes and the Status line as work lands.
**Status:** Waves 01 done (2026-07-31). `node tools/verifyWorms.js`**207 checks green**
(~4 min; `--quick` ≈ 30 s). The game is wired into the menu and a full match is playable
with 9 of the 16 weapons. **Never opened in a browser** — Brian playtests, and nothing here
is signed off until he has.
---
## Decisions taken up front
| Question | Answer |
|---|---|
| Slug / name / category | `worms` / "Worms" / `arcade-console-pc` ("Video Games") |
| iconFrame | **92** — freed by the Angry Birds removal; 091 were contiguous with no gaps |
| Physics | Bespoke deterministic solver. Matter.js ships inside the CDN Phaser build but is not importable in bare Node, which would forfeit the verifier and the AI's shot replay |
| Weapons | 16 core: Bazooka, Grenade, Cluster, Shotgun, Uzi, Fire Punch, Dynamite, Mine, Air Strike, Homing Missile, Holy Hand Grenade, Blowtorch, Girder, Teleport, Ninja Rope, Skip Go (+ Surrender) |
| Single-player | 20-match campaign ladder fronted by the shared opponent roster, plus free Quick Match |
| 2 player | Hotseat round-robin, one shared control set (turns never overlap) |
| Themes | 8 — Forest, Desert, Farm, Hell, Arctic, Jungle, Construction, Space |
| Art | Terrain shape procedural; worms procedural with a drop-in hook. Brian supplies theme backdrops, terrain fill/surface textures and background-object sheets |
| Map shapes | Island (open sky) and Cavern (indestructible frame) |
| Look | Clean late-90s PC. **No CRT overlay, no m6x11 pixel font** |
| Rules | 45 s turns, 5 s retreat, fall damage, wind, sudden death + rising water, crates, mines, oil drums |
---
## Files
| File | Role | State |
|---|---|---|
| `src/games/worms/WormsTerrain.js` | Mask terrain: generation, destruction, queries, spawn lint | ✅ Wave 0 |
| `src/games/worms/WormsPhysics.js` | Worm locomotion, projectiles, rope, blast geometry | ✅ Wave 0 |
| `tools/verifyWorms.js` | Headless verifier, sections 19 | ✅ Wave 0 (152 checks) |
| `src/games/worms/WormsWeapons.js` | The 16-weapon table | ✅ Wave 1 |
| `src/games/worms/WormsLogic.js` | Match rules, turn order, event list | ✅ Wave 1 |
| `src/games/worms/WormsThemes.js` | 8 theme palettes + art slot names | ✅ Wave 1 |
| `src/games/worms/WormsRender.js` | Procedural worms, terrain painting, FX | ✅ Wave 1 |
| `src/games/worms/WormsGame.js` | Phaser scene | ✅ Wave 1 |
| `src/games/worms/tutorial.md` | Player-facing rules and controls | ✅ Wave 1 |
| `data/worms-artwork.json` | Drop-in art slots, all `path: null` | ✅ Wave 1 |
| `src/games/worms/WormsAI.js` | Skill 15 opponent | ⬜ Wave 3 |
| `data/worms-campaign.json` | 20 ladder rungs | ⬜ Wave 3 |
| `src/games/worms/sprites.md` | Art spec for Brian | ⬜ Wave 4 |
**Wiring touchpoints** — all done in Wave 1: `src/data/gamesRegistry.js` (iconFrame 92),
`src/main.js` (import + scene array), `src/scenes/GameRoomScene.js:26` (`slugDispatch`),
`src/data/assetManifest.js` (`wormsArt` resolver), `src/scenes/PreloadScene.js`
(`worms-artwork` JSON). The `?worms-maplab=1` entrance is still Wave 4.
**Deviation from the original plan:** the weapon table lives in `WormsWeapons.js`, not
`data/worms-rules.json`. The verifier, the AI and the scene must agree on those numbers
exactly, and a plain ESM import gives that in both the browser and bare Node with no fetch
and no async boot ordering. Theme palettes are in `WormsThemes.js` for the same reason;
only the *art paths* are data (`data/worms-artwork.json`).
---
## Wave 0 — terrain + physics core, headless — ✅ DONE 2026-07-31
Landed: `WormsTerrain.js`, `WormsPhysics.js`, `tools/verifyWorms.js` sections 19.
- [x] Seeded RNG + integer-hash value noise, no `Math.random` anywhere in the sim
- [x] Island generation — heightfield crust, coastline envelope, noise caves, arches, speck prune
- [x] Cavern generation — ceiling/floor band, interior pillars and ledges, largest-void filter, ROCK frame
- [x] `carveCircle` / `addCircle` / `addGirder`, ROCK immunity, order independence
- [x] `raycast`, `normalAt`, `surfaceY`, `firstSolidBelow`, `hashMask`
- [x] Worm locomotion — step-up walking, jump, backflip, ballistic flight, fall damage
- [x] Projectiles — swept collision, bounce, fuse, `maxAge`, wind, homing
- [x] Ninja rope — raycast anchor, pendulum, corner push/pop wrapping
- [x] Blast geometry — linear falloff, impulse direction
- [x] Fairness lint + `generateValidMap` retry loop
### Wave 0 findings
1. **A plain noise threshold cannot generate a cavern.** The base octave decides huge regions
at once, so thresholding fBm gives *one* usable chamber and welds two thirds of the map
shut — measured 70 % solid with the right-hand 60 % of the map unreachable. The fix is to
carve the void as a band between a wavy ceiling and a wavy floor, which is connected across
the full width **by construction**, and get the interest from what is put *back*: pillars,
stalactites and floating ledges from a second noise field. Verifier pins this: "cavern void
spans the full width" requires >98 % of columns to contain air.
2. **Only the ROOT rope anchor is attached to terrain.** Every later anchor is a corner the
rope merely bends around, and by construction it sits in AIR (it is the last *free* point
before the raycast contact). Validating those the same way as the root made the rope
release itself the instant it wrapped anything — 14 of 35 swings dropped mid-flight. Bends
are geometry, not attachments.
3. **Sub-pixel residue from a landing rides along forever.** The mask is only sampled at
integers, so a worm that settles at y=290.625 stands a visible pixel higher than one at
y=291 and reports a different standing height for the rest of the match — it made a
correct 6px step-climb look 1.4px wrong. `snapToGround` now rounds to integers on landing.
4. **`resolveOverlap` has to be a per-frame safety net, not just a landing fixup.** Backing
off 1.5px along the surface normal after a bounce is not enough in a concave corner, and
terrain can be built or destroyed around an airborne worm. Without the net, a randomised
walk had a worm inside terrain on 14 of 14 400 frames. The invariant worth having is
absolute: **a worm is never inside terrain at the end of a step.**
5. **A 6px drop is a step, not a fall — and that is load-bearing.** `MAX_STEP` (10px) governs
both climbing up and walking down. Terrain features below it are invisible to movement;
features above it are walls. This is what makes terrain shape tactical rather than
incidental, and it means "walked off a ledge" tests need a drop taller than `MAX_STEP`.
6. **Explosion falloff divides by the radius.** A zero-radius blast (which a weapon table
typo will produce sooner or later) put NaN into worm velocities and from there into the
whole sim. Guarded at the source.
### Wave 0 baselines
| Measurement | Result |
|---|---|
| Map generation, medium island / cavern | 93 ms / 199 ms |
| Map generation, large island / cavern | 148 ms / 266 ms |
| `generateValidMap` retries | 1.01.2 mean, ≤8 worst |
| `carveCircle` r=30 / r=75 / r=120 | 0.011 / 0.033 / 0.080 ms |
| 8 worms × 120 substeps (1 s of sim) | 6.0 ms |
| 500 AI shot replays | 40 ms |
| Bazooka range, full power, flat ground | ~1030 px (matches `2v²sin θ cos θ / g` to 2 %) |
The AI budget risk in the original plan is **resolved**: at 0.08 ms per shot replay the AI can
evaluate a few thousand candidates inside a 200 ms turn, so no coarse-to-fine sweep is needed.
Terrain repaint is a non-issue on the logic side; the CanvasTexture upload still wants
measuring in the browser during Wave 1.
---
## Wave 1 — rules engine + one playable match — ✅ DONE 2026-07-31
Landed: the 16-weapon table, the match rules engine, the 8 theme palettes, the renderer,
the scene, the tutorial, and all five wiring touchpoints. Verifier sections 1012 added.
- [x] `WormsWeapons.js` — 16 weapons + surrender, crate table, charge curve
- [x] `WormsLogic.js` — teams, turn rotation, timer, retreat, wind, crates, mines, barrels,
sudden death + rising water, drowning, corpse blasts, chain reactions, win check
- [x] `WormsThemes.js` — 8 palettes with named art slots
- [x] `WormsRender.js` — terrain RenderTexture, procedural worms, water, explosions
- [x] `WormsGame.js` — mode select, quick-match setup, camera, HUD, weapon panel,
keyboard + slingshot mouse aim, 2-player hotseat
- [x] Wiring: registry (iconFrame 92), `main.js`, `slugDispatch`, manifest resolver, preload
- [x] Verifier §10 weapon table, §11 match rules, §12 scripted match soak
**9 of 16 weapons are live** (bazooka, grenade, cluster, shotgun, uzi, fire punch, dynamite,
mine, holy hand grenade, plus skip/surrender). The `target` and `utility` kinds — air strike,
homing missile, blowtorch, girder, teleport, ninja rope — deliberately **refuse to fire**
rather than silently eating the ammo and the turn; the verifier pins that. The rope already
works in physics and is reachable from the scene, it just has no ammo accounting yet.
### Wave 1 findings
1. **`pendingDeath` must not block the settle phase.** "Is the world quiet?" and "is anything
waiting to be resolved?" are different questions. Treating a worm at 0 HP as *still
moving* stranded every kill until the 14-second settle timeout fired, turning a two-second
turn into a sixteen-second one. Deaths are resolved by the phase that comes *after* settle.
2. **A body has to be seated by its own radius.** Barrels were being placed 8 px above the
surface with a 13 px radius, so they started half-buried — and a half-buried body is never
"grounded" by the contact test, so it fell through its own first frame.
3. **A two-shot weapon costs one ammo for the pair.** Spending per trigger pull halves the
shotgun. The fix is to spend only on the pull that *starts* the pair, which needs the
decision made before the switch, not inside the multi-shot bookkeeping.
4. **Phaser Containers swallow `scrollFactor` the same way they swallow depth.** Parenting
the parallax layers to one backdrop container made every layer move at the container's
rate — the backdrop looked painted onto the terrain. `makeBackdrop` returns loose root
objects with explicit depths instead. (Same family as the container-depth gotcha already
burned into the repo.)
5. **"Leave match" leaves `this.state` set.** Tearing down the graphics while the state
object survives means the next `update()` draws into destroyed objects. A single
`matchLive` flag set by `teardownMatch`/`buildWorld` guards every path — rematch, menu,
scene shutdown — instead of null-checking six fields.
6. **Craters must be smaller than the damage radius.** Otherwise every hit that hurts also
opens a hole big enough to fall through, and matches devolve into everyone drowning.
Pinned by the verifier across the whole weapon table.
### Wave 1 baselines
| Measurement | Result |
|---|---|
| Scripted match, 4v4 | terminates 30/30, 0 NaN, 0 draws, mean 50 turns |
| Wall time per scripted match | ~5.7 s (~2.5 s with `retreatSeconds: 0`) |
| Whole-match determinism | same seed → same `hashState` |
The soak cost matters for Wave 3: 400 AI matches at ~5.7 s is 38 minutes. The soak config
sets `retreatSeconds: 0` (the retreat wait is dead time headlessly), which is why §12 runs
24 matches by default and `--games=400` is the long run.
## Wave 2 — full arsenal and rule extras — ⬜ NEXT
- [ ] `target` kind: air strike, homing missile, girder, teleport (click-to-place flow)
- [ ] `utility` kind: blowtorch, ninja rope ammo + turn integration
- [ ] Verifier §13: a fixture per weapon pinning damage, blast and behaviour
## Wave 3 — AI and campaign ladder — ⬜
## Wave 4 — presentation, themes, 2-player — ⬜
## Wave 5 — retune after Brian's playtest — ⬜
---
## Sources for original-game behaviour
Worms Armageddon (Team17, 1999): 45 s default turn with 5 s retreat, 100 HP worms, wind
re-rolled each turn and affecting only unpowered arcing weapons, fall damage from the peak of
the fall, sudden death dropping every worm to 1 HP with the water rising per turn, crates
parachuting in between turns, and the ninja rope wrapping terrain corners rather than passing
through them.