fertig-classic-games/docs/puddingmonsters-mechanics-p...

32 KiB
Raw Blame History

Jell-o Monsters (Pudding Monsters) — mechanics build-out 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: ALL WAVES COMPLETE (2026-07-25). Eight mechanics, a 75-level curriculum in 5 named chapters, stars and crowns as separate awards, 1295 checks green. What is left is optional polish (§3.4 achievements) and the parked mechanics in §2.5. Not browser-verified at any point — Brian playtests.


Where things stand

Files that make up the game:

File Role
src/games/puddingmonsters/PuddingMonstersLogic.js Pure model + BFS solver. No Phaser.
src/games/puddingmonsters/PuddingMonstersGame.js Phaser scene: level select + board + HUD.
tools/verifyPuddingMonsters.js Regression harness, 1295 checks.
tools/genPuddingMonsters.js Curriculum generator → data/puddingmonsters.json.
data/puddingmonsters.json 75 levels in 5 chapters, plus a chapters block.
assets/images/jello-items.png 528×132 sheet = 4 frames @132px: 0 spikes, 1-3 wall variants.
src/data/assetManifest.js:125 Lazy-load entry (sheet('jello-items', …, 132, 132)).

tools/verifyPuddingMonsters.js is the regression harness — run it after every change. Each new mechanic changes the solver, and without it a subtly wrong par is indistinguishable from a right one.

Implemented mechanics: slide-until-blocked, stick/merge on contact, open edges (leaving the board is fatal), walls, spikes (our invention — not in the original), 3 target squares judged at the final configuration, par medal — plus sleepers, ice, slime trails, hypno goos, springs, tunnels, buttons+bricks and powerlifters+crates. Level schema: {level, cols, rows, walls, spikes, targets, monsters, par} with optional ice, slime, sleepers, green, hypno, springs, tunnels, bricks, buttons, bricksDown, crates, powerlifters, tip, element.

Scoring is the original's: stars = how many of the 3 yellow squares the final blob covers, crown = solved in par or fewer. They are independent awards.

Still missing from the original: tunnel cloning, magnets, splitters, conveyor belts and the mustache monster — all parked for want of documented behaviour, see §2.5 — and the achievement list (§3.4).


Recommendation on sequencing (asked for explicitly)

Yes — complete Wave 1 and Wave 2 mechanics before overhauling the level set, and treat the level curriculum as a distinct Wave 3. But do not build two waves of engine work with nothing playable: each wave ends with a small hand-authored sandbox tier (56 levels per new element) appended to the existing bank, purely so the mechanic can be played and judged. That is throwaway content, explicitly not the final curriculum.

Reasoning:

  1. Progression design is a function of the complete element set. A difficulty curve that introduces sleeping monsters at level 9 and ice at level 18 is worthless once springs, tunnels and powerlifters need slots too. Designing the ramp before the elements exist means designing it twice, and the second pass is the expensive one (hand-authored teaching levels, par bands, chapter boundaries).
  2. Levels are cheap; the curve is not. genPuddingMonsters.js fills the whole 40-level bank in seconds. Regenerating after each wave costs nothing. Re-deciding the pedagogy costs a session each time.
  3. Wave 0's state-key refactor invalidates assumptions, not data. Existing levels contain no new elements so their par stays correct, but every generator tier definition gets rewritten in Wave 3 anyway.

The real risk of deferring is finding out late that a mechanic is unfun or that the generator can't produce good levels with it. The per-wave sandbox tier is the mitigation — it makes each mechanic playable within its own wave, without committing to where it sits in the final game.


Wave 0 — prerequisites — DONE 2026-07-25

The current solver assumes the board is immutable: stateKey() (PuddingMonstersLogic.js:173) hashes only blob positions, because walls/spikes/targets never change. Slime trails, shattered ice, toggled bricks, awake flags, pushed crates and clone counts are all mutable board state. If any of them lands without entering the state key, BFS will treat two different positions as identical, return a too-short "optimal" path, and silently write wrong par values into the bank — which then corrupts the star medal for every affected level.

  • state.board holds every mutable layer. A layer may be a Set of cell keys, a Map, an array or a scalar; boardKey() / cloneBoard() handle all four generically, so adding a layer needs no changes to hashing or cloning. Immutable walls/spikes still share by reference.
  • stateKey() folds the board in (blobs!board). Empty layers contribute nothing, so a level with no elements hashes exactly as before — that is why the existing bank is unaffected.
  • cloneState() deep-copies board layers (verified: mutating a child's layer does not write through to its parent — the exact bug that would corrupt a BFS search).
  • computeSlide() rewritten as one ordered per-step walk with the resolution order documented inline as numbered steps: 1 BLOCK → 2 ENTER → 3 DEATH → 4 STOP-ON → 5 REDIRECT. Steps 4 and 5 are empty hooks with comments naming the wave that fills them. Note the BLOCK/STOP-ON distinction: walls stop a blob before the cell, slime stops it after entering.
  • computeSlide() now also returns offset (final displacement) and, under { trace: true }, path (offset after each step). Use offset, not dir * maxSteps — once springs and tunnels redirect mid-slide those stop being the same thing. trace is what Wave 1 needs to lay slime and shatter ice along the walk; slide() has the comment marking where.
  • tools/verifyPuddingMonsters.js written (house style, check() + exit 1). 246 checks: bank schema, all 40 levels re-solved with par matched and the optimal path replayed to a 3-star win, engine primitives, solver minimality (brute-force check that no shorter path exists), and a whole section on board-layer / asleep-flag state identity.
  • Baseline green before any mechanic: 246 passed, 0 failed.

Also landed while in here:

  • asleep flag plumbed through the state model (Wave 1.1's foundation, since it is a state-identity concern): levels may declare sleepers: [[x,y]], newState flags those blobs, mergeBlobs wakes a blob unless every part was asleep, stateKey distinguishes asleep from awake, cloneState preserves it. Not yet gated — a sleeper can still be flicked; that is Wave 1.1's remaining work (legalMoves + scene + generator).
  • Deleted the dead legacy star-collection machinery (pickUpStars, state.collected, starsCollected, state.stars, level.stars) — unused since targets replaced collectibles, and it cost a Set allocation on every cloneState, i.e. on every node the solver expands.

Behaviour-preservation proof (re-run this after any future engine refactor):

node tools/verifyPuddingMonsters.js                       # 246 passed, 0 failed
node tools/genPuddingMonsters.js 1592594996 /tmp/regen.json
# levels array is byte-identical to data/puddingmonsters.json (same 462 attempts)

Still holds after Wave 1: the sandbox tier draws from the RNG after the main bank, so node tools/genPuddingMonsters.js 1592594996 still reproduces levels 140 byte-identically (the verifier and this check were both re-run after Wave 1). Anything that changes the main TIERS or the draw order inside them breaks it — re-baseline then.

Solver perf baseline: all 40 levels solved in 208 ms total, worst level 50 ms (maxStates: 200000). Compare against this when slime and tunnels expand the state space.


Wave 1 — the chapter 12 mechanics — DONE 2026-07-25

Landed: all four mechanics in the engine, the scene, the generator and the verifier.

  • Level JSON gained five optional fields: ice: [[x,y]] (board cells) and sleepers / green / hypno / slime (the first three list monster start cells, typing those monsters). Plus tip (one-line lesson shown above the board) and element (which mechanic the level teaches).
  • planFlick() / applyPlan() replaced the scene's use of computeSlide + slide. One plan drives both the animation and the move, so what the player watches can never disagree with what the engine did — a hypno flick returns one part per hive member, and the verifier asserts plan + apply === slide() for every direction.
  • Scene: green and hypno monsters get their own body colour, sleepers get shut eyes and a "z", hypno eyes get spiral rings; slime is a bridged ooze layer under the monsters; ice is a frosted slab that pops when struck. Ice art is optional — frame 4 of jello-items.png is used if it exists, otherwise the slab is drawn procedurally, so nothing is blocked on art.
  • Undo/Reset resync ice and slime (syncElements()), since both are now mutable board state.
  • Level-select grid now scales its tile size and column count with the bank (it overflowed the buttons at 60 levels; it will hold ~125 for Wave 3), and element levels show a coloured pip.
  • Bank: 60 levels. Levels 140 are byte-identical to before; 4160 are the sandbox tier.

Deviation from the plan, deliberate: the sandbox tier is generated and gated rather than hand-authored. genPuddingMonsters.js grew SANDBOX_TIERS (5 levels per mechanic) and an isLoadBearing() gate — a candidate is rejected unless removing its element changes the par or makes the level unsolvable. That is the Wave 3 anti-decorative gate, built early, and it produced better levels than hand-authoring would have. verifyPuddingMonsters.js re-checks load-bearing independently rather than trusting generation. Hand-authored teaching levels are still Wave 3 work — a gated random level proves the mechanic matters, it does not teach it gently.

Verification: node tools/verifyPuddingMonsters.js605 passed, 0 failed (was 246). Solver: all 60 levels in 237 ms, worst 45 ms, so the in-game Hint stays instant.

Rules decided while building (all covered by checks):

  • A flick that only shatters ice — the blob is already touching it — is a legal move that travels 0 cells. Without that, ice you are up against could never be broken.
  • Ice shatters even when a wall blocks the same step, and a wide blob striking two ice blocks breaks both.
  • A green blob is never stopped by its own fresh trail (slime is laid when the move is applied, so the walk only sees pre-existing slime) but is stopped by older slime.
  • The hive resolves furthest-along-the-direction first, so a leader clears the way and the follower can close up and stick. Merging happens once, after every member has moved.
  • A sleeping hypno does not move with the hive until something wakes it.
  • If any hive member would die, the whole flick is fatal and legalMoves excludes it.

Original plan (kept for reference)

Chosen because they fit the rigid-blob model, need no change to monster count or board topology, and cover the original's first two chapters. Order within the wave is deliberate: cheapest and most independent first.

Where the seams are (all in src/games/puddingmonsters/PuddingMonstersLogic.js, added in Wave 0 — search for the wave number in comments):

Seam What plugs in
initialBoard(level) Seed a layer from the level def (board.ice = new Set(...)). Two example lines are already in the comment.
computeSlide() step 1 BLOCK Solid things: intact ice, raised bricks, crates.
computeSlide() step 4 STOP-ON Slime — halts the blob after it enters the cell.
computeSlide() step 5 REDIRECT Springs, tunnels (Wave 2). dir/dx/dy/offset are loop-local for this.
slide() Re-walk with { trace: true } and apply board mutations along the path.
legalMoves() Skip blobs that can't be flicked (asleep).
stateKey() / cloneBoard() Nothing to do — new layers are picked up automatically.

Every new element needs (a) a rule in the walk, (b) a generator tier that can place it, (c) unit checks in verifyPuddingMonsters.js §3, and (d) scene rendering.

1.1 Sleeping monsters ("Sleepyheads", original 1-6 / 1-9)

Asleep monsters cannot be flicked. They wake only when another blob slides into them and merges. A merged blob is awake if any member is awake.

  • Logic: level JSON carries a parallel sleepers: [[x,y]] array (chosen over a per-monster tuple flag — it keeps the existing monsters shape and the generator simpler). Done in Wave 0.
  • Merge wakes the whole blob; a blob is asleep only if every part was. Wake state is in stateKey. Done in Wave 0.
  • legalMoves() skips asleep blobs; slide() rejects them. ← the actual gate, still open
  • Decide: can a sleeping blob be pushed by an arriving blob, or does the arriving blob simply stop against it and stick? (Original: it sticks and the merged blob is awake — so no push.)
  • Scene: closed-eye + "Z" rendering, wake animation (eyes pop open, quick jiggle — reuse wobbleBlob). No sheet art needed, faces are drawn.
  • Solver check: a level where the only solution requires waking in a specific order.

Art: none (procedural).

1.2 Ice blocks ("Breaking the Ice", 1-18 / 1-19 / 1-25)

Acts exactly like a wall, but shatters when struck once and the cell becomes empty. The striking blob still stops at it that move (it does not continue through on the same flick).

  • Logic: ice:Set in mutable board state; blocks a slide like a wall, then is removed on impact. Enters stateKey.
  • Decide and document: does a blob that stops against ice merge with anything behind the ice? (No — ice is gone but the blob has already come to rest.)
  • Scene: shatter animation, cracked intermediate frame optional.

Art: frame 4 = intact ice block, frame 5 = shattered/cracked (optional but nice).

1.3 Slime trails (green monsters, "Why So Green?", 1-12)

A green monster leaves slime on every cell it passes through, including its start cell. Any monster (green or not) that slides onto a slimed cell stops there. Slime is permanent.

  • Logic: slime:Set in board state; laid down during a green blob's slide; a slimed cell is a stop-on tile, not a block-before tile (the blob enters the cell, then halts) — this is the key rules distinction versus walls.
  • A merged blob containing a green monster is green (matches the original: the trail follows the whole blob).
  • Slime enters stateKey. Branching factor rises but BFS depth stays small; watch SOLVE_MAX_STATES (currently 80 000 in the generator).
  • Scene: slime tile rendering under monsters (depth between floor and monsters), green monster body colour + drip detail.

Art: frame 6 = slime tile (or draw procedurally — decide when rendering; procedural is probably better since it needs to tile seamlessly across a trail).

1.4 Hypno / synchronous goos ("Running Together" achievement)

All monsters of the hypno type share a hive mind: flicking any one of them slides every hypno blob in that direction, in the same move.

  • Logic: group id on monsters; slide() resolves a group move. Resolution order matters — they can block each other. Resolve iteratively: repeatedly slide whichever group member can still move furthest until no member moves, or define a strict order (furthest-along-the- direction first) and document the choice.
  • Death: if any member dies (edge/spike), the run fails.
  • Merging a hypno with a normal monster — decide whether the merged blob stays hypno (original suggests yes) and document.
  • Scene: spiral/hypno eyes, simultaneous slide animation.
  • Solver: no key change (positions already capture it), but legalMoves must emit one move per group, not per blob.

Art: none (procedural eyes).

Wave 1 exit criteria

  • verifyPuddingMonsters.js covers all four mechanics with unit-level rule checks.
  • Sandbox tier: ~6 hand-authored levels per mechanic appended to the bank (throwaway).
  • Played in-browser by Brian; each mechanic reads clearly without explanation.

Wave 2 — the chapter 35 mechanics — DONE 2026-07-25

Landed: springs, tunnels, buttons+bricks, powerlifters+crates — in the engine, the scene, the generator and the verifier. Bank grew to 80 levels (levels 160 byte-identical; 6180 are the new sandbox blocks). 1228 checks green; solver does all 80 levels in 464 ms, worst 82 ms.

The engine walk was restructured so that the walk is the single authority for board mutation. It now works copy-on-write: computeSlide clones the board only if the flick actually changes it, returns it as board, and applyPlan installs it. Levels using no elements still allocate nothing, which is why levels 140 are untouched and the solver stayed fast.

Rules decided while building (the original's behaviour is undocumented for most of these — these are our rules, and every one is covered by checks):

  • Springs are omnidirectional bumpers: a blob that would enter one is thrown back the way it came and keeps sliding, so the route bends. Each spring bounces once per flick — hit a spent one and it just blocks. That is what guarantees the walk terminates: two springs facing each other cannot ping-pong forever. Chosen over a fixed-distance launch because it needs no rotation, reads instantly, and cannot deadlock.
  • Tunnels are linked pairs. A blob whose cell enters a mouth is translated so that cell lands on the partner, then keeps sliding — so a whole multi-cell blob goes through rigidly, which is exactly what the original's "Tunnel Master — move a 3-eyed monster through a tunnel" describes. One teleport per flick; a teleport that would land on anything solid simply does not happen.
  • Buttons toggle their brick group when a monster slides over them, once per button per flick, and it happens mid-slide — a button can raise a brick in time to stop the very slide that pressed it. A brick that would rise through a monster or crate jams instead of crushing it.
  • Powerlifters push crates; everyone else is stopped dead by them. No crate trains — a crate backed by anything solid will not budge. A crate shoved past the rim is gone (and the pusher usually follows it off, which is the player's problem).
  • maxSteps now means cells actually travelled, not loop iterations — a spring bounce costs an iteration but no step. This was wrong when springs landed and is now asserted.
  • computeSlide returns path (per-step offsets, with a flag marking teleports) and crateMoves. The scene animates each blob along the polyline and slides pushed crates with it; a teleport is snapped, never tweened through. A check asserts every path ends exactly on its part's offset, because the scene no longer slices the path by step count.

Scope call — tunnel cloning was NOT implemented. Sources conflict: the "Tunnel Master" achievement describes moving a blob through a tunnel (teleport), while a review describes tunnels reproducing monsters, and the "Reproduction" achievement counts clones. Teleport is the better-attested reading, it keeps the monster count fixed, and it keeps the BFS state space bounded — cloning makes the number of blobs variable, which is the one change that could make the generator's solve() blow up. Cloning is parked in §2.5 with magnets and splitters until there is footage to copy.

Original plan (kept for reference)

These change monster count or board topology, so they need more solver care.

2.1 Buttons + retractable bricks

Sliding a monster over a round button raises/lowers a set of brick walls.

  • Logic: buttons:[{cell, group}], bricks:[{cell, group, up:bool}]. Triggered when a blob passes over or stops on the button (decide: original appears to trigger on pass-over).
  • Toggle state enters stateKey.
  • Edge case: bricks rising under a resting blob — forbid at generation time rather than inventing a rule.

Art: frame 7 button up, 8 button down, 9 brick raised, 10 brick lowered/flush.

2.2 Springs / bumpers ("Spring Pusher": bump 150 monsters with a spring)

A spring bumps a monster back instead of stopping it.

  • Nail the rule from gameplay footage before coding: does it reverse the blob, launch it a fixed distance, or launch it until the next obstacle? Written source doesn't say.
  • Logic: redirect inside the per-step scan built in Wave 0. Must be loop-safe (spring facing spring) — cap total steps.

Art: frame 11 spring (4 rotations, or one frame rotated in-scene — prefer rotation).

2.3 Tunnels + cloning ("Tunnel Master", "Reproduction": create 30 monster clones on a single level)

Monsters entering a tunnel emerge elsewhere, and tunnels clone monsters — so the monster count is not fixed.

  • This is the biggest solver change in the plan: the win condition ("all blobs merged into one") is currently state.blobs.length === 1, which still holds, but the state space grows and stateKey must handle a variable number of blobs (it already does — it sorts blobs).
  • Watch for infinite clone loops; cap clones per level and per state.
  • Decide whether clones can themselves clone.

Art: frame 12 tunnel mouth (rotatable), 13 tunnel exit if visually distinct.

2.4 Powerlifters + crates ("Powerlifter": move the same object 10 times)

Powerlifter monsters push objects instead of sticking to them.

  • Logic: crates as movable non-monster occupants. A powerlifter blob pushes a crate along its slide; crate stops on wall/blob/edge — decide whether a pushed crate can fall off the table (probably yes, and probably not fatal).
  • Crate positions enter stateKey.

Art: frame 14 crate.

2.5 Deferred / needs research

Not scheduled — insufficient information to specify, revisit with gameplay footage:

  • Tunnel cloning ("Reproduction": create 30 monster clones on a single level) — tunnels are implemented as teleports (see the Wave 2 notes). Cloning would make the number of blobs variable, which is the one change that can blow up the BFS state space the generator depends on. Needs footage plus a hard per-level clone cap before it is worth attempting.
  • Magnet monster ("Positive Attraction": use a magnet to attract a plus-shaped monster).
  • Splitters ("Separation": split a 6-eyed monster into 3 equal parts) — something cuts blobs apart; lowest confidence item on the whole list.
  • Conveyor belts and the mustache monster — named by the fan wiki, no documented behaviour.

Wave 2 exit criteria

  • Verify covers every element; generator produces valid levels using each in isolation (5 per mechanic, all independently re-checked as load-bearing).
  • Sandbox tier extended — 80 levels total.
  • Solver performance still fine: 464 ms for all 80 levels, worst 82 ms (was 237 ms / 45 ms at 60 levels). SOLVE_MAX_STATES untouched. In-game Hint stays instant.

Wave 3 — level overhaul, progression, and meta — DONE 2026-07-25

Landed: the whole bank was thrown away and regenerated as a curriculum. genPuddingMonsters.js was rewritten around CHAPTERS: five named chapters of fifteen levels, each built from blocksbasic (no elements), teach (one gentle introduction), drill (reinforcement), mix (combinations). Every level is named. Regenerate with node tools/genPuddingMonsters.js 20260725 (~4 minutes; the gates are expensive).

Ch Name Levels Introduces
1 Cold Storage 115 sleepers, ice
2 Kitchen Counter 1630 slime, hypno
3 The Pantry 3145 springs, tunnels
4 Dining Room 4660 buttons+bricks, powerlifters+crates
5 Midnight Feast 6175 nothing new — every combination

Three gates decide whether a candidate level survives, and the verifier re-checks all three rather than trusting generation:

  1. Par band — per block.
  2. Load-bearing — strip an element and the par must change or the level must break. On a combination level every element is checked on its own (57 levels, 88 element checks).
  3. Gentle (teaching levels only) — no opening flick may be fatal. You cannot lose a lesson on move one.

That third gate could not be met by random walls — on an open-edged table nearly every layout has some suicidal flick, and 400 000 candidates produced zero survivors. So teaching levels are repaired into gentleness: monsters are placed off the rim, then addGuardRails() drops a wall on the last cell of any ray that would run a monster off the table. The gate then passes by construction, and is still verified independently.

Crown vs stars are now separate, as in the original: stars = targetsCovered(final) (03) and crown = moves <= par. The HUD shows both live ("★ 2/3 squares" and "crown: on track / lost"), the solved overlay shows both with a reason for whichever you missed, and the level select shows stars under the number with a crown in the corner. Simulated perfect play scores 225/225 stars and 75/75 crowns, so the two awards are always simultaneously achievable.

Level select rebuilt as one row per chapter: heading, blurb, per-chapter cleared count, and 15 tiles. Tiles carry a mechanic pip, a crown, a green NEW badge on teaching levels, and a hover tooltip with the level name, par and lesson (via the shared src/ui/Tooltip.js). Finishing a chapter's last level shows a chapter-complete panel with its star and crown tally, and the Next button reads "Start The Pantry".

localStorage keys are namespaced pm3- (pm3-stars-<n>, pm3-crown-<n>). Wave 3 renumbered every level, so the old pm-stars-<n> medals described puzzles that no longer exist; Reset Progress clears both generations.

Deviations, deliberate

  • 75 levels, not the original's 125. Five chapters of fifteen keeps every level earning its place and the level select readable on one screen. The curriculum is data — raising a block's count and adding names is the only work needed to grow it.
  • Achievements (§3.4) not built. They need a subsystem this game does not have, and none of the rest depends on them. Left open below.
  • Per-chapter completion screens landed as part of the solved overlay rather than as a separate scene.

Original plan (kept for reference)

Only start once Waves 12 are playable. This is where the game becomes fun rather than featureful.

3.1 Chapter structure

Original: 5 chapters × 25 levels = 125, named — Escape the Fridge, Room Invaders, The Neighborhood, City Tour, and a fifth. We have 40 flat levels.

  • Decide our chapter count and size (125 is a lot to hand-tune; 5 × 15 = 75 is defensible).
  • Chapter metadata in data/puddingmonsters.json; level-select grouped by chapter with per-chapter unlock.
  • Level names. The original names every level ("Sleepyheads", "Breaking the Ice", "Third Slime's a Charm") — that is how it telegraphs a new mechanic. Cheap, high value.

3.2 Teaching curve

  • Each new element gets a hand-authored teaching level: minimal board, one idea, hard to fail. Generated levels alone will never teach.
  • Then 35 generated levels reinforcing it, then combinations with earlier elements.
  • Rewrite TIERS in genPuddingMonsters.js as a per-chapter element budget (which elements are legal, how many, par band) rather than the current flat 5-tier ramp.
  • Generator must reject levels where an element is decorativebuilt early in Wave 1: isLoadBearing(lvl, element, par) in genPuddingMonsters.js strips the element, re-solves, and requires the par to change or the level to become unsolvable. verifyPuddingMonsters.js re-checks it independently for every level tagged with an element. Reuse both for the Wave 2 elements; they take the element name, so no new machinery is needed.

3.3 Crown vs stars (scoring fidelity)

The original awards these separately: 3 stars for the final blob covering the 3 star tiles, and a crown for solving in the minimum number of moves ("Pudding King — crown 100 levels"). We currently fold both into min(coverage, parMedal) in liveStars() (PuddingMonstersGame.js:472), so a player who covers all three tiles but takes an extra move just sees a lower star count and can't tell which half they missed.

  • Split: stars = targetsCovered(final) (03), crown = moves <= par.
  • HUD shows both; level-select shows stars + crown badge.
  • localStorage key change (pm-stars-<level> → add pm-crown-<level>); handle existing saved progress gracefully.

3.4 Nice-to-have meta — STILL OPEN

  • Achievements (the original has 26; shape-based ones like Donut Monster / Hot Dog Monster are charming and nearly free once blob shape is inspectable).
  • Per-chapter completion screens — done, folded into the solved overlay.

Art requests (sheet is append-only)

assets/images/jello-items.png, 132×132 frames, currently 4 frames (528×132). Appending frame N means widening the sheet to 132 × (N+1); existing frames must not move.

Frame Wave Content
4 1 Ice block, intact (frosted, slightly translucent). OptionalmakeIce() uses frame 4 the moment it exists, and draws a procedural slab until then. Nothing is blocked.
5 1 Cracked ice — dropped; the shatter is a pop-and-fade tween, no second frame needed.
6 1 Slime tile — dropped; drawn procedurally so a trail bridges between cells.
7 / 8 2 Button — drawn procedurally (a red pad inlaid in the floor). Not needed.
9 / 10 2 Bricks — drawn procedurally, raised vs flush-in-floor. Not needed.
11 2 Spring — drawn procedurally (omnidirectional, so no rotation needed).
12 / 13 2 Tunnel mouths — drawn procedurally, colour-coded per pair.
14 2 Crate — drawn procedurally (banded wooden box).

Every Wave 2 element ended up procedural, so frame 4 (ice) is the only art request still open — and even that is optional, since makeIce() falls back to a drawn slab. Monster types (sleeping, green, hypno, powerlifter) are drawn in code too: type shows in body colour plus a face detail (shut eyes and a "z", spiral eye-rings, heavy brows), so none of them depends on hue alone.


Sources for original-game behaviour