diff --git a/assets/gamedata/gootower/level-009.json b/assets/gamedata/gootower/level-009.json index 57f3d8b..784e2b8 100644 --- a/assets/gamedata/gootower/level-009.json +++ b/assets/gamedata/gootower/level-009.json @@ -70,30 +70,364 @@ 1000 ] ] + }, + { + "kind": "solid", + "poly": [ + [ + 497, + 189 + ], + [ + 7, + 187 + ], + [ + 9, + 433 + ], + [ + 67, + 433 + ], + [ + 84, + 337 + ], + [ + 135, + 291 + ], + [ + 253, + 247 + ], + [ + 399, + 222 + ], + [ + 497, + 214 + ] + ] + }, + { + "kind": "solid", + "poly": [ + [ + 590, + 189 + ], + [ + 588, + 216 + ], + [ + 744, + 214 + ], + [ + 817, + 230 + ], + [ + 872, + 271 + ], + [ + 951, + 283 + ], + [ + 1012, + 260 + ], + [ + 1047, + 216 + ], + [ + 1055, + 3 + ], + [ + 955, + 3 + ], + [ + 951, + 138 + ], + [ + 938, + 170 + ], + [ + 872, + 192 + ], + [ + 796, + 184 + ] + ] + }, + { + "kind": "fire", + "poly": [ + [ + 423, + 408 + ], + [ + 697, + 411 + ], + [ + 697, + 520 + ], + [ + 424, + 515 + ] + ] + }, + { + "kind": "solid", + "poly": [ + [ + 12, + 436 + ], + [ + 209, + 438 + ], + [ + 270, + 428 + ], + [ + 313, + 406 + ], + [ + 371, + 394 + ], + [ + 424, + 386 + ], + [ + 426, + 515 + ], + [ + 699, + 521 + ], + [ + 699, + 394 + ], + [ + 743, + 394 + ], + [ + 788, + 409 + ], + [ + 848, + 433 + ], + [ + 929, + 436 + ], + [ + 1037, + 433 + ], + [ + 1036, + 568 + ], + [ + 1329, + 565 + ], + [ + 1327, + 424 + ], + [ + 1590, + 430 + ], + [ + 1593, + 666 + ], + [ + 10, + 650 + ] + ] + }, + { + "kind": "fire", + "poly": [ + [ + 1039, + 444 + ], + [ + 1037, + 567 + ], + [ + 1332, + 565 + ], + [ + 1330, + 431 + ] + ] + }, + { + "kind": "fire", + "poly": [ + [ + 9, + 660 + ], + [ + 9, + 844 + ], + [ + 1590, + 844 + ], + [ + 1592, + 671 + ] + ] + }, + { + "kind": "solid", + "poly": [ + [ + 1040, + 433 + ], + [ + 1034, + 401 + ], + [ + 1009, + 400 + ], + [ + 1007, + 435 + ] + ] + }, + { + "kind": "solid", + "poly": [ + [ + 705, + 316 + ], + [ + 700, + 391 + ], + [ + 740, + 394 + ], + [ + 739, + 352 + ], + [ + 744, + 332 + ], + [ + 791, + 310 + ], + [ + 809, + 310 + ], + [ + 807, + 294 + ], + [ + 704, + 290 + ] + ] + }, + { + "kind": "solid", + "poly": [ + [ + 1110, + 370 + ], + [ + 1110, + 389 + ], + [ + 1275, + 387 + ], + [ + 1274, + 367 + ] + ] } ], "balls": [ { - "x": 722, - "y": 836, + "x": 470, + "y": 164, "type": "common", - "pinned": true + "pinned": false }, { - "x": 784, - "y": 836, + "x": 399, + "y": 164, "type": "common", - "pinned": true + "pinned": false }, { - "x": 753, - "y": 782, - "type": "common" + "x": 434, + "y": 110, + "type": "common", + "pinned": false } ], "strands": [ [ - 2, + 1, 0 ], [ @@ -101,135 +435,215 @@ 1 ], [ - 0, - 1 + 2, + 0 ] ], "pile": [ { - "x": 1013, - "y": 834, + "x": 45, + "y": 159, "type": "common" }, { - "x": 1043, - "y": 834, + "x": 100, + "y": 160, "type": "common" }, { - "x": 1073, - "y": 834, + "x": 149, + "y": 162, "type": "common" }, { - "x": 1103, - "y": 834, + "x": 212, + "y": 164, "type": "common" }, { - "x": 1133, - "y": 834, + "x": 273, + "y": 165, "type": "common" }, { - "x": 1163, - "y": 834, + "x": 327, + "y": 164, "type": "common" }, { - "x": 1193, - "y": 834, + "x": 298, + "y": 124, "type": "common" }, { - "x": 1223, - "y": 834, + "x": 239, + "y": 129, "type": "common" }, { - "x": 1013, - "y": 804, + "x": 185, + "y": 131, "type": "common" }, { - "x": 1043, - "y": 804, + "x": 122, + "y": 126, "type": "common" }, { - "x": 1073, - "y": 804, + "x": 75, + "y": 118, "type": "common" }, { - "x": 1103, - "y": 804, + "x": 158, + "y": 85, "type": "common" }, { - "x": 1133, - "y": 804, + "x": 218, + "y": 90, "type": "common" }, { - "x": 1163, - "y": 804, + "x": 806, + "y": 390, + "type": "ivy" + }, + { + "x": 850, + "y": 405, + "type": "ivy" + }, + { + "x": 888, + "y": 412, + "type": "ivy" + }, + { + "x": 927, + "y": 414, + "type": "ivy" + }, + { + "x": 969, + "y": 412, + "type": "ivy" + }, + { + "x": 990, + "y": 373, + "type": "ivy" + }, + { + "x": 958, + "y": 379, + "type": "ivy" + }, + { + "x": 900, + "y": 372, + "type": "ivy" + }, + { + "x": 864, + "y": 373, + "type": "ivy" + }, + { + "x": 832, + "y": 362, + "type": "ivy" + }, + { + "x": 862, + "y": 329, + "type": "ivy" + }, + { + "x": 908, + "y": 331, + "type": "ivy" + }, + { + "x": 944, + "y": 338, + "type": "ivy" + }, + { + "x": 979, + "y": 335, + "type": "ivy" + }, + { + "x": 261, + "y": 86, "type": "common" }, { - "x": 1193, - "y": 804, + "x": 102, + "y": 74, "type": "common" }, { - "x": 1223, - "y": 804, + "x": 35, + "y": 77, "type": "common" }, { - "x": 1013, - "y": 774, + "x": 187, + "y": 49, "type": "common" }, { - "x": 1043, - "y": 774, + "x": 245, + "y": 33, "type": "common" }, { - "x": 1073, - "y": 774, + "x": 127, + "y": 39, "type": "common" }, { - "x": 1103, - "y": 774, + "x": 59, + "y": 39, "type": "common" }, { - "x": 1133, - "y": 774, - "type": "common" + "x": 881, + "y": 296, + "type": "ivy" }, { - "x": 1163, - "y": 774, - "type": "common" + "x": 932, + "y": 301, + "type": "ivy" }, { - "x": 1193, - "y": 774, - "type": "common" + "x": 974, + "y": 298, + "type": "ivy" }, { - "x": 1223, - "y": 774, - "type": "common" + "x": 763, + "y": 379, + "type": "ivy" + }, + { + "x": 779, + "y": 337, + "type": "ivy" + }, + { + "x": 817, + "y": 329, + "type": "ivy" } ], "pipe": { - "x": 753, - "y": 455, + "x": 1474, + "y": 310, "r": 46 }, "required": 6, diff --git a/assets/gamedata/zuma/level-007.json b/assets/gamedata/zuma/level-007.json index 59667b2..84378bc 100644 --- a/assets/gamedata/zuma/level-007.json +++ b/assets/gamedata/zuma/level-007.json @@ -1,384 +1,109 @@ { - "level": 7, - "name": "Rapids", - "frog": [ - 880, - 770 - ], - "colors": 4, - "background": "zuma-background-2", - "seed": 56433, - "starScores": [ - 910, - 1360, - 1810 - ], - "paths": [ - { - "shape": "sCurve", - "points": [ - [ - -80, - 140 - ], - [ - -11, - 140 - ], - [ - 57, - 140 - ], - [ - 126, - 140 - ], - [ - 195, - 140 - ], - [ - 263, - 140 - ], - [ - 332, - 140 - ], - [ - 401, - 140 - ], - [ - 470, - 140 - ], - [ - 538, - 140 - ], - [ - 607, - 140 - ], - [ - 676, - 140 - ], - [ - 744, - 140 - ], - [ - 813, - 140 - ], - [ - 882, - 140 - ], - [ - 950, - 140 - ], - [ - 1019, - 140 - ], - [ - 1088, - 140 - ], - [ - 1157, - 140 - ], - [ - 1225, - 140 - ], - [ - 1294, - 140 - ], - [ - 1363, - 140 - ], - [ - 1431, - 140 - ], - [ - 1500, - 140 - ], - [ - 1572, - 153 - ], - [ - 1635, - 189 - ], - [ - 1682, - 245 - ], - [ - 1707, - 314 - ], - [ - 1707, - 386 - ], - [ - 1682, - 455 - ], - [ - 1635, - 511 - ], - [ - 1572, - 547 - ], - [ - 1500, - 560 - ], - [ - 1431, - 560 - ], - [ - 1362, - 560 - ], - [ - 1293, - 560 - ], - [ - 1224, - 560 - ], - [ - 1156, - 560 - ], - [ - 1087, - 560 - ], - [ - 1018, - 560 - ], - [ - 949, - 560 - ], - [ - 880, - 560 - ], - [ - 811, - 560 - ], - [ - 742, - 560 - ], - [ - 673, - 560 - ], - [ - 604, - 560 - ], - [ - 536, - 560 - ], - [ - 467, - 560 - ], - [ - 398, - 560 - ], - [ - 329, - 560 - ], - [ - 260, - 560 - ], - [ - 188, - 573 - ], - [ - 125, - 609 - ], - [ - 78, - 665 - ], - [ - 53, - 734 - ], - [ - 53, - 806 - ], - [ - 78, - 875 - ], - [ - 125, - 931 - ], - [ - 188, - 967 - ], - [ - 260, - 980 - ], - [ - 329, - 980 - ], - [ - 398, - 980 - ], - [ - 467, - 980 - ], - [ - 536, - 980 - ], - [ - 604, - 980 - ], - [ - 673, - 980 - ], - [ - 742, - 980 - ], - [ - 811, - 980 - ], - [ - 880, - 980 - ], - [ - 949, - 980 - ], - [ - 1018, - 980 - ], - [ - 1087, - 980 - ], - [ - 1156, - 980 - ], - [ - 1224, - 980 - ], - [ - 1293, - 980 - ], - [ - 1362, - 980 - ], - [ - 1431, - 980 - ], - [ - 1500, - 980 - ], - [ - 1573, - 966 - ], - [ - 1634, - 924 - ], - [ - 1676, - 863 - ], - [ - 1690, - 790 - ], - [ - 1690, - 717 - ], - [ - 1690, - 643 - ], - [ - 1690, - 570 - ], - [ - 1678, - 509 - ], - [ - 1643, - 457 - ], - [ - 1591, - 422 - ], - [ - 1530, - 410 - ] - ], - "tunnels": [], - "quota": 26, - "introBalls": 8, - "pushSpeed": 108, - "powerUpRate": 0.06 - } - ] + "level": 7, + "name": "New Path", + "frog": [ + 958, + 559 + ], + "colors": 4, + "seed": 12345, + "starScores": [ + 640, + 960, + 1280 + ], + "paths": [ + { + "shape": "custom", + "points": [ + [ + -59, + 106 + ], + [ + 380, + 126 + ], + [ + 1113, + 130 + ], + [ + 1646, + 102 + ], + [ + 1815, + 288 + ], + [ + 1689, + 514 + ], + [ + 1247, + 327 + ], + [ + 956, + 251 + ], + [ + 591, + 288 + ], + [ + 347, + 294 + ] + ], + "tunnels": [], + "quota": 24, + "introBalls": 8, + "pushSpeed": 30, + "powerUpRate": 0.06 + }, + { + "shape": "custom", + "points": [ + [ + 1923, + 907 + ], + [ + 1048, + 999 + ], + [ + 199, + 915 + ], + [ + 164, + 663 + ], + [ + 435, + 587 + ], + [ + 824, + 862 + ], + [ + 1349, + 856 + ], + [ + 1704, + 702 + ] + ], + "tunnels": [], + "quota": 24, + "introBalls": 8, + "pushSpeed": 30, + "powerUpRate": 0.06 + } + ] } diff --git a/assets/images/shift/backpacking-goldens.png b/assets/images/shift/backpacking-goldens.png new file mode 100644 index 0000000..ce24962 Binary files /dev/null and b/assets/images/shift/backpacking-goldens.png differ diff --git a/assets/images/shift/elven-warrior.png b/assets/images/shift/elven-warrior.png new file mode 100644 index 0000000..1a2a6ab Binary files /dev/null and b/assets/images/shift/elven-warrior.png differ diff --git a/assets/images/shift/japanese-treehouses.png b/assets/images/shift/japanese-treehouses.png new file mode 100644 index 0000000..99b82fe Binary files /dev/null and b/assets/images/shift/japanese-treehouses.png differ diff --git a/assets/images/shift/knights-and-goblins.png b/assets/images/shift/knights-and-goblins.png new file mode 100644 index 0000000..9eea0a0 Binary files /dev/null and b/assets/images/shift/knights-and-goblins.png differ diff --git a/assets/images/shift/serving-the-patrons.png b/assets/images/shift/serving-the-patrons.png new file mode 100644 index 0000000..59fa5f6 Binary files /dev/null and b/assets/images/shift/serving-the-patrons.png differ diff --git a/assets/images/shift/surfing-corgi.png b/assets/images/shift/surfing-corgi.png new file mode 100644 index 0000000..04043bf Binary files /dev/null and b/assets/images/shift/surfing-corgi.png differ diff --git a/assets/images/shift/tree-village.png b/assets/images/shift/tree-village.png new file mode 100644 index 0000000..5b510d2 Binary files /dev/null and b/assets/images/shift/tree-village.png differ diff --git a/assets/images/shift/zen-budha.png b/assets/images/shift/zen-budha.png new file mode 100644 index 0000000..f2c8ed5 Binary files /dev/null and b/assets/images/shift/zen-budha.png differ diff --git a/assets/images/worms/background-menu.png b/assets/images/worms/background-menu.png deleted file mode 100644 index 74ed169..0000000 Binary files a/assets/images/worms/background-menu.png and /dev/null differ diff --git a/data/shift-artwork.json b/data/shift-artwork.json index 8b46f56..8eff69a 100644 --- a/data/shift-artwork.json +++ b/data/shift-artwork.json @@ -143,6 +143,54 @@ "name": "Tiger Warrior", "key": "tiger-warrior", "path": "assets/images/shift/tiger-warrior.png" + }, + { + "id": "backpacking-goldens", + "name": "Backpacking in the Goldens", + "key": "backpacking-goldens", + "path": "assets/images/shift/backpacking-goldens.png" + }, + { + "id": "elven-warrior", + "name": "Elven Warrior", + "key": "elven-warrior", + "path": "assets/images/shift/elven-warrior.png" + }, + { + "id": "japanese-treehouses", + "name": "Japanese Treehouses", + "key": "japanese-treehouses", + "path": "assets/images/shift/japanese-treehouses.png" + }, + { + "id": "knights-and-goblins", + "name": "Knights and Goblins", + "key": "knights-and-goblins", + "path": "assets/images/shift/knights-and-goblins.png" + }, + { + "id": "serving-the-patrons", + "name": "Serving the Patrons", + "key": "serving-the-patrons", + "path": "assets/images/shift/serving-the-patrons.png" + }, + { + "id": "surfing-corgi", + "name": "Surfing Corgi", + "key": "surfing-corgi", + "path": "assets/images/shift/surfing-corgi.png" + }, + { + "id": "tree-village", + "name": "Tree Village", + "key": "tree-village", + "path": "assets/images/shift/tree-village.png" + }, + { + "id": "zen-budha", + "name": "Zen Buddha", + "key": "zen-budha", + "path": "assets/images/shift/zen-budha.png" } ] } \ No newline at end of file diff --git a/data/worms-artwork.json b/data/worms-artwork.json deleted file mode 100644 index 065f656..0000000 --- a/data/worms-artwork.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "_readme": [ - "Drop-in art for Worms. Every slot starts null, which means 'not painted' —", - "the game renders that slot procedurally from the theme palette in", - "src/games/worms/WormsThemes.js and looks complete either way. Fill in a", - "'path' (relative to the repo root, e.g. assets/images/worms/bg-forest.png)", - "and that slot upgrades on the next load. No code change is needed.", - "", - "Per theme:", - " backdrop a single image, 1920x1080 or wider, parallaxed behind the world", - " fill a SEAMLESS 256x256 tile for the inside of the terrain", - " surface a SEAMLESS 256x48 strip for the top crust of the terrain", - " objects a spritesheet of background scenery, 160x160 cells, 8 per sheet", - "", - "Shared:", - " worms a spritesheet of worm frames, 64x64 cells (see sprites.md)", - " icons weapon icons for the weapon panel, 64x64 cells, in PANEL_ORDER" - ], - - "themes": { - "forest": { "backdrop": { "key": "worms-bg-forest", "path": null }, - "fill": { "key": "worms-fill-forest", "path": null }, - "surface": { "key": "worms-surface-forest", "path": null }, - "objects": { "key": "worms-objects-forest", "path": null, "frameWidth": 160, "frameHeight": 160 } }, - "desert": { "backdrop": { "key": "worms-bg-desert", "path": null }, - "fill": { "key": "worms-fill-desert", "path": null }, - "surface": { "key": "worms-surface-desert", "path": null }, - "objects": { "key": "worms-objects-desert", "path": null, "frameWidth": 160, "frameHeight": 160 } }, - "farm": { "backdrop": { "key": "worms-bg-farm", "path": null }, - "fill": { "key": "worms-fill-farm", "path": null }, - "surface": { "key": "worms-surface-farm", "path": null }, - "objects": { "key": "worms-objects-farm", "path": null, "frameWidth": 160, "frameHeight": 160 } }, - "hell": { "backdrop": { "key": "worms-bg-hell", "path": null }, - "fill": { "key": "worms-fill-hell", "path": null }, - "surface": { "key": "worms-surface-hell", "path": null }, - "objects": { "key": "worms-objects-hell", "path": null, "frameWidth": 160, "frameHeight": 160 } }, - "arctic": { "backdrop": { "key": "worms-bg-arctic", "path": null }, - "fill": { "key": "worms-fill-arctic", "path": null }, - "surface": { "key": "worms-surface-arctic", "path": null }, - "objects": { "key": "worms-objects-arctic", "path": null, "frameWidth": 160, "frameHeight": 160 } }, - "jungle": { "backdrop": { "key": "worms-bg-jungle", "path": null }, - "fill": { "key": "worms-fill-jungle", "path": null }, - "surface": { "key": "worms-surface-jungle", "path": null }, - "objects": { "key": "worms-objects-jungle", "path": null, "frameWidth": 160, "frameHeight": 160 } }, - "construction": { "backdrop": { "key": "worms-bg-construction", "path": null }, - "fill": { "key": "worms-fill-construction", "path": null }, - "surface": { "key": "worms-surface-construction", "path": null }, - "objects": { "key": "worms-objects-construction", "path": null, "frameWidth": 160, "frameHeight": 160 } }, - "space": { "backdrop": { "key": "worms-bg-space", "path": null }, - "fill": { "key": "worms-fill-space", "path": null }, - "surface": { "key": "worms-surface-space", "path": null }, - "objects": { "key": "worms-objects-space", "path": null, "frameWidth": 160, "frameHeight": 160 } } - }, - - "shared": { - "worms": { "key": "worms-sprites", "path": null, "frameWidth": 64, "frameHeight": 64 }, - "icons": { "key": "worms-icons", "path": null, "frameWidth": 64, "frameHeight": 64 } - } -} diff --git a/docs/worms-build-plan.md b/docs/worms-build-plan.md deleted file mode 100644 index 897feb2..0000000 --- a/docs/worms-build-plan.md +++ /dev/null @@ -1,212 +0,0 @@ -# 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. diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index c370480..22c3080 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -47,31 +47,6 @@ function imagesFrom(scene, jsonKey) { .map((a) => ({ type: 'image', key: a.key, path: a.path })); } -// Worms art is two levels deep (theme → slot) and mixes plain images with a -// spritesheet, so it gets its own resolver rather than bending sheetsFrom. -// Slots whose `path` is still null render procedurally from the theme palette -// in src/games/worms/WormsThemes.js, so an empty result here is the normal, -// fully playable state — not a missing asset. -function wormsArt(scene) { - const art = scene.cache.json.get('worms-artwork'); - if (!art) return []; - const out = []; - const add = (slot, asSheet) => { - if (!slot || !slot.key || !slot.path) return; - out.push(asSheet - ? { type: 'spritesheet', key: slot.key, path: slot.path, frameWidth: slot.frameWidth, frameHeight: slot.frameHeight } - : { type: 'image', key: slot.key, path: slot.path }); - }; - for (const theme of Object.values(art.themes ?? {})) { - add(theme.backdrop, false); - add(theme.fill, false); - add(theme.surface, false); - add(theme.objects, true); - } - for (const slot of Object.values(art.shared ?? {})) add(slot, true); - return out; -} - // Drop-in per-game soundtrack tracks declared in a cached `-music.json` // (see services/soundtrack.js). Audio bytes lazy-load only when the owning // game is entered — never part of the shared default-soundtrack preload. @@ -86,12 +61,6 @@ const sheet = (key, path, frameWidth, frameHeight) => ({ type: 'spritesheet', ke const image = (key, path) => ({ type: 'image', key, path }); export const MANIFEST = { - // All of Worms' art is optional drop-in theme art; the game is fully - // playable with none of it (see data/worms-artwork.json). - worms: [ - image('worms-bg-menu', 'assets/images/worms/background-menu.png'), - (scene) => wormsArt(scene), - ], gootower: [ image('gootower-bg', 'assets/images/gootower/background-1.png'), image('gootower-pipe', 'assets/images/gootower/pipe.png'), diff --git a/src/data/gamesRegistry.js b/src/data/gamesRegistry.js index 3552028..0904458 100644 --- a/src/data/gamesRegistry.js +++ b/src/data/gamesRegistry.js @@ -118,5 +118,4 @@ registerGame({ slug: 'tetrisattack', name: 'Tetris Attack', category: 'arcade-co registerGame({ slug: 'totalannihilation', name: 'Total Annihilation', category: 'arcade-console-pc', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 88 }); registerGame({ slug: 'bloxorz', name: 'Bloxorz', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 89 }); registerGame({ slug: 'gootower', name: 'Goo Tower', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 90 }); -registerGame({ slug: 'worms', name: 'Worms', category: 'arcade-console-pc', minPlayers: 1, maxPlayers: 2, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 92 }); registerGame({ slug: 'excitebike', name: 'Excitebike', category: 'arcade-console-pc', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 91 }); diff --git a/src/games/gootower/GooTowerAuto.js b/src/games/gootower/GooTowerAuto.js index 29bf6cb..cc29437 100644 --- a/src/games/gootower/GooTowerAuto.js +++ b/src/games/gootower/GooTowerAuto.js @@ -53,7 +53,7 @@ export function autoPlay(level, { maxBalls = 60, maxSeconds = 60, seed } = {}) { // level author records the intended solution and the gate replays THAT. if (Array.isArray(level.reference) && level.reference.length) { for (const step of level.reference) { - const ball = st.balls.find((b) => !b.dead && !b.attached && !b.held + const ball = st.balls.find((b) => !b.dead && !b.attached && !b.held && !b.asleep && (!step.type || b.type === step.type)); if (!ball) break; beginDrag(st, ball, null); @@ -75,8 +75,8 @@ export function autoPlay(level, { maxBalls = 60, maxSeconds = 60, seed } = {}) { for (let i = 0; i < maxBalls; i += 1) { if (!st.pipe || st.pipe.open) break; - const ball = st.balls.find((b) => !b.dead && !b.attached && !b.held && !b.walking) - || st.balls.find((b) => !b.dead && !b.attached && !b.held); + const ball = st.balls.find((b) => !b.dead && !b.attached && !b.held && !b.asleep && !b.walking) + || st.balls.find((b) => !b.dead && !b.attached && !b.held && !b.asleep); if (!ball) break; while (ti < targets.length - 1 diff --git a/src/games/gootower/GooTowerEditor.js b/src/games/gootower/GooTowerEditor.js index 9b1fc27..fb6f7d9 100644 --- a/src/games/gootower/GooTowerEditor.js +++ b/src/games/gootower/GooTowerEditor.js @@ -63,6 +63,7 @@ export default class GooTowerEditor extends Phaser.Scene { this.kind = 'solid'; this.gooType = 'common'; this.pinned = false; + this.asleep = false; this.pendingPoly = null; this.manifest = []; this.undoStack = []; @@ -126,7 +127,7 @@ export default class GooTowerEditor extends Phaser.Scene { ...(this.gears.length ? { gears: this.gears.map((g) => ({ ...g })) } : {}), balls: this.balls.map((b) => ({ x: b.x, y: b.y, type: b.type, pinned: !!b.pinned })), strands: this.strands.map((s) => [s[0], s[1]]), - pile: this.pileItems.map((p) => ({ x: p.x, y: p.y, type: p.type })), + pile: this.pileItems.map((p) => ({ x: p.x, y: p.y, type: p.type, ...(p.asleep ? { asleep: true } : {}) })), pipe: { x: this.pipe.x, y: this.pipe.y, r: this.pipe.r }, required: this.meta.required, ocdTarget: this.meta.ocdTarget, @@ -224,6 +225,7 @@ export default class GooTowerEditor extends Phaser.Scene {
+