213 lines
12 KiB
Markdown
213 lines
12 KiB
Markdown
# 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 0–1 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; 0–91 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 1–9 | ✅ 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 1–5 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 1–9.
|
||
|
||
- [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.0–1.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 10–12 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.
|