32 KiB
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 (5–6 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:
- 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).
- Levels are cheap; the curve is not.
genPuddingMonsters.jsfills the whole 40-level bank in seconds. Regenerating after each wave costs nothing. Re-deciding the pedagogy costs a session each time. - Wave 0's state-key refactor invalidates assumptions, not data. Existing levels contain no
new elements so their
parstays 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.boardholds 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. Immutablewalls/spikesstill 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 returnsoffset(final displacement) and, under{ trace: true },path(offset after each step). Useoffset, notdir * maxSteps— once springs and tunnels redirect mid-slide those stop being the same thing.traceis what Wave 1 needs to lay slime and shatter ice along the walk;slide()has the comment marking where.tools/verifyPuddingMonsters.jswritten (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:
asleepflag plumbed through the state model (Wave 1.1's foundation, since it is a state-identity concern): levels may declaresleepers: [[x,y]],newStateflags those blobs,mergeBlobswakes a blob unless every part was asleep,stateKeydistinguishes asleep from awake,cloneStatepreserves 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 aSetallocation on everycloneState, 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 1–40 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 1–2 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) andsleepers/green/hypno/slime(the first three list monster start cells, typing those monsters). Plustip(one-line lesson shown above the board) andelement(which mechanic the level teaches). planFlick()/applyPlan()replaced the scene's use ofcomputeSlide+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 onepartper hive member, and the verifier assertsplan + 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.pngis 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 1–40 are byte-identical to before; 41–60 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.js → 605 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
legalMovesexcludes 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 existingmonstersshape 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:Setin mutable board state; blocks a slide like a wall, then is removed on impact. EntersstateKey. - 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:Setin 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; watchSOLVE_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
legalMovesmust emit one move per group, not per blob.
Art: none (procedural eyes).
Wave 1 exit criteria
verifyPuddingMonsters.jscovers 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 3–5 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 1–60 byte-identical; 61–80 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 1–40 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).
maxStepsnow 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.computeSlidereturnspath(per-step offsets, with a flag marking teleports) andcrateMoves. 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 andstateKeymust 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:
cratesas 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_STATESuntouched. 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 blocks —
basic (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 | 1–15 | sleepers, ice |
| 2 | Kitchen Counter | 16–30 | slime, hypno |
| 3 | The Pantry | 31–45 | springs, tunnels |
| 4 | Dining Room | 46–60 | buttons+bricks, powerlifters+crates |
| 5 | Midnight Feast | 61–75 | nothing new — every combination |
Three gates decide whether a candidate level survives, and the verifier re-checks all three rather than trusting generation:
- Par band — per block.
- 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).
- 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) (0–3) 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
countand 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 1–2 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 3–5 generated levels reinforcing it, then combinations with earlier elements.
- Rewrite
TIERSingenPuddingMonsters.jsas 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 decorative — built early in Wave 1:
isLoadBearing(lvl, element, par)ingenPuddingMonsters.jsstrips the element, re-solves, and requires the par to change or the level to become unsolvable.verifyPuddingMonsters.jsre-checks it independently for every level tagged with anelement. 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)(0–3),crown = moves <= par. - HUD shows both; level-select shows stars + crown badge.
- localStorage key change (
pm-stars-<level>→ addpm-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). Optional — makeIce() uses frame 4 the moment it exists, and draws a procedural slab until then. Nothing is blocked. |
| 1 | Cracked ice — dropped; the shatter is a pop-and-fade tween, no second frame needed. | |
| 1 | Slime tile — dropped; drawn procedurally so a trail bridges between cells. | |
| 2 | Button — drawn procedurally (a red pad inlaid in the floor). Not needed. | |
| 2 | Bricks — drawn procedurally, raised vs flush-in-floor. Not needed. | |
| 2 | Spring — drawn procedurally (omnidirectional, so no rotation needed). | |
| 2 | Tunnel mouths — drawn procedurally, colour-coded per pair. | |
| 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
- Wikipedia — core slide/fuse mechanic.
- Jay is Games review — ice blocks, buttons and bricks, star-tile rule.
- PS4Blog Switch review — sleeping monsters, green slime trails.
- LadiesGamers review — tunnels/cloning, star tiles, 125 levels.
- Pocket Gamer chapter 1 guide — level names, which mechanic arrives when.
- VGTimes achievement list — the definitive element roster (springs, magnets, tunnels, powerlifters, synchronous monsters, separation, crowns).