Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mechanics Audit (2026-08)

Mechanics audit — hard-gate reachability + jump/obstacle-height physics (2026-08)

Requested after PLAYTEST-FINDINGS.md’s PF-4 (dressCodeGuard gated on hasTailoredClothes, currently unwinnable-by-design if not for an accidental jump-bypass, task #13 already dispatched to redesign that specific bypass into a real telegraphed jump). This doc does not re-investigate or touch PF-4/task #13 — it sweeps the rest of the game for the same two failure-mode classes that case revealed, so the next one gets found here instead of on a TV during a playtest:

  1. Hard-gate reachability — every place player state conditionally blocks movement (moveSolids) or gates a PortalDef/goal, whether the required state’s source is guaranteed-reachable given the fixed one-way level order, and if optional/skippable, whether a real (even if currently accidental) way through exists.
  2. Jump-height/obstacle-height physics — obstacle/hazard geometry vs. the jump envelope (GRAVITY=900, JUMP_VELOCITY=-300 → apex ≈50px above launch, airtime ≈0.667s → maxJumpGapPx(moveSpeed) = moveSpeed × 0.667, so ≈33px at WALK_SPEED, ≈53px at RUN_BASE, ≈93px at max speed tier).

Level order confirmed from main.ts: LEVELS = [MINIGOLF, LIDL_LUNCH, SOCCER, TANZSTUNDE, CLOUDS], LEVELS2 is the Season-2 parallel of the same five. advanceLevel() only ever does levelIndex++; nothing in main.ts ever decrements it or offers a way back to an earlier level. No backtracking exists anywhere, confirmed by reading the code rather than trusting the brief.

Exhaustive moveSolids sweep: grepped every conditional addition to moveSolids in main.ts. There are exactly two, in the whole game:

  • soccerBall (blocks only while !running; always resolvable — run into it to kick it away, or just jump over it, or walk around during its patrol swing). Not player-state gated, not a lock candidate.
  • dressCodeGuard (PF-4, gated on hasTailoredClothes). The only genuine hard blocker in the codebase.

No other item, hazard, or state anywhere gets added to moveSolids. Every other “obstacle” (discoBall, waterfall, lava, breakDancer, bat, dancePartner, kn00t ambushes, poisonEgg, airplane) is a damage-on-touch item, never solid — tall sizing on any of them (e.g. discoBall/waterfall at h:32) affects whether you can dodge without taking a hit, never whether you can physically pass. This means the jump-height audit’s practical scope is narrower than it might look: there is no second hidden hard blocker anywhere with an unnoticed height problem, because there is no second hard blocker at all.


Findings, ranked by severity

1. [Context correction, not a new bug] Tailored Clothes has two independent chances, not one — refines PF-4’s severity, doesn’t remove the need for task #13

What: PF-4 and the brief both state Tailored Clothes exists “in exactly one place in the game.” That’s true of the LevelData object (underworld_mushroom_house.ts is written once), but not true of player-reachable opportunities. Soccer’s secret door portal exists on both SOCCER (Season 1) and SOCCER_S2 (Season 2):

// soccer_s2.ts
export const SOCCER_S2: LevelData = {
  ...SOCCER,
  portal: SOCCER.portal && { ...SOCCER.portal, used: false },
  ...
};

This is the exact fix ARCHITECTURE-REVIEW-2026-08.md §1 made for the PortalDef.used-aliasing bug: each season gets its own PortalDef object with its own used flag, not a shared reference. The side effect (not a deliberate safety net, just an emergent property of that fix) is that Season 2’s own playthrough of Soccer (LEVELS2[2], immediately before TANZSTUNDE_S2[3] — the guard’s level) gets an independently-fresh, un-used copy of the same portal, leading to the same UNDERWORLD_MUSHROOM_HOUSE LevelData object — and since loadLevelData() always does items = data.items.map(i => ({...i})), that second visit spawns a fresh, uncollected copy of tailoredClothes regardless of what happened on the Season 1 visit.

Why it matters: a player who skips the Soccer secret in Season 1 is not yet in an unwinnable state — they get exactly one more shot at it in Season 2’s Soccer, the level immediately before the one with the guard. The real failure condition is “skip the Soccer secret door in both the Season 1 and Season 2 playthroughs,” not “skip it once.” That’s a materially different (still real, but rarer) risk than PF-4 describes.

Also checked while tracing this: uervelKnight (Lidl Lunch’s Season 1 pickup) gets the same accidental redundancy for a different structural reason — it’s a plain item in LIDL_LUNCH.items, not gated by any PortalDef, and LIDL_LUNCH_S2 spreads ...LIDL_LUNCH.items unconditionally, so it’s simply present again, uncollected, on the Season 2 visit. Two independent chances, no lock risk — noted here only to confirm it’s not a second instance of the problem below.

Fix direction: none needed here — this is a documentation/severity correction, not a code change. Worth folding into task #13’s writeup or PLAYTEST-FINDINGS.md PF-4 so whoever picks up the redesign has the accurate risk model (still worth fixing properly — “must fail identically twice” is still a real, if smaller, unwinnable-state risk, and the task is already about making the intended path reliable rather than relying on either chance-taking or the jump accident).


2. Soccer Shoes and Dance Shoes are single-chance items with no season-pair redundancy — currently soft-gated, but the redundancy that saves Tailored Clothes does not apply to them

What: soccerShoes (lidl_lunch_s2.ts) and danceShoes (tanzstunde_s2.ts) are each defined only in their level’s _s2.ts file — there is no Season 1 counterpart item at all (Season 1’s LIDL_LUNCH/TANZSTUNDE never mention them). Since Season 2 of any given level is only ever visited once in a full run (no backtracking, no Season 3), each of these items gets exactly one opportunity, not two. Contrast with finding #1: Tailored Clothes’ redundancy comes specifically from existing on a level (SOCCER/SOCCER_S2) that has a Season 1 twin sharing the same portal-to-secret pattern; these two equipment items don’t have that structural protection because their whole level variant is Season-2-exclusive.

Why it’s not currently a bug: both gates today are soft, not hard blockers:

  • soccerShoes gates only whether a kicked/walked-into footballGoal scores (main.ts lines ~1106, ~1208) — def.kind === 'food', never added to moveSolids. Missing it just means the goal stays inert; boss.checkReached() (the only thing that actually ends a level, per boss.ts) has zero awareness of footballGoal at all.
  • danceShoes gates only whether a dancePartner’s downbeat dip becomes a bonus (player.danceMove()) instead of a hit (player.takeHit()) — never a blocker either.

Missing either one just means “take the hit / miss the bonus points,” never “cannot proceed.”

Why it’s still worth flagging: the game’s own established pattern (see items.ts’s comment on ItemKind: 'equipment': “Soccer Shoes now, Dance Shoes/tailored clothes expected to follow the same pattern”) is exactly the pattern that produced the real hard-block case (dressCodeGuard). If either of these two ever gets escalated from a soft bonus-gate into a real moveSolids hard block — the same escalation that apparently already happened once for Tailored Clothes — it would be a worse version of PF-4, not an equal one: no season-pair redundancy exists to fall back on, so a single missed pickup would be a true, un-mitigated softlock from the moment it shipped, not one accidentally saved by a jump exploit or a second season-2 chance.

Fix direction: no code change needed today (nothing is broken). If either soccerShoes or danceShoes is ever turned into a hard gate later, budget for either (a) a second, independent pickup location, or (b) explicitly accepting single-chance risk with a clearly reliable (not accidental) path to it, the way task #13 is already doing for the guard. Worth a one-line note wherever the “equipment gates a mechanic” pattern gets extended (items.ts’s own comment is the natural place).


3. Minigolf’s healable pit-4 fill-rect overlaps the secret crawlspace floor it sits above — geometry oddity, not a lock (escape route always exists)

What: Minigolf’s hidden crawlspace under the “cup” (pit 3, x=700-730, heals:false, the permanent secret entrance) has a floor (TUNNEL_FLOOR_Y=213, TUNNEL_FLOOR_W=140 → spans x=640-780) with no roof past x=700 (the thin TUNNEL_ROOF_H crust only covers the approach side, 640-700, by design — see the file’s own comment). Pit 4 sits at x=762-792, heals:true, and its own hazard fill-rect is the full ordinary ground thickness: {x:762, y:GROUND_Y(192), w:30, h:40} — i.e. it spans y:192-232.

The crawlspace floor (y:213-216) at x:762-780 sits entirely inside that fill-rect’s vertical span (192-232). Pit 4 reveals (opens) purely by player.x proximity — the reveal check in main.ts (player.x + hazardRevealDistance >= hz.x) has no y component, so it fires the same whether the player is walking the surface or already underground. In ordinary forward-walking play, both pit 3 and pit 4’s reveal thresholds (700-130=570/762-130=632 in Season 1; tighter in Season 2) get crossed together well before the player reaches either pit, so pit 4 is already open by the time anyone falls into pit 3’s hole.

The actual issue: if the player dawdles underground (there’s food to collect at x=648-689, plenty of reason to linger) for more than HAZARD_HEAL_DELAY=2.0s past pit 4’s reveal, pit 4 re-heals — and its solid fill-rect reappears exactly where the crawlspace floor’s own footprint (762-780) is. Two sub-cases:

  • Player already moved on, not standing there when it re-seals: the crawlspace simply becomes impassable further east from that point on — a dead end. Harmless: nothing of value is placed east of x=689 underground (the only underground loot, the hidden food cluster, sits west of pit 3’s own opening), and pit 3’s hole (heals:false, permanent) is always available to climb back up and out to the west.
  • Player is standing exactly in that footprint when it re-seals: moveAndCollide (engine/collision.ts) resolves collisions by re-checking overlap after applying the frame’s own dx/dy, with no “was this already overlapping before this frame” check. Vertically this is self-correcting in practice — vy is essentially always slightly positive from gravity even when “standing still,” so the dy>0 branch fires and snaps the player up to stand on the newly-solid rect’s top (y = s.y - box.h), the same “land safely” behavior the dutyCycle hazards’ code comment describes (that comment is written for the Season 2 rhythmic-pit case specifically, but the underlying moveAndCollide mechanics happen to produce the same safe outcome here too). Horizontally it’s less clean — if dx≠0 at that exact instant, the player gets snapped to just outside the rect’s edge in the direction implied by dx’s sign, which can read as a sudden, disorienting teleport rather than a smooth collision, though never a stuck-forever state.

Severity: Low. No path to an unwinnable state exists — pit 3’s permanent hole is always a way out, and there is nothing to miss by not going further east. Worst case is a startling snap/reposition if the timing lines up exactly, or a dead-end wall with nothing behind it.

Fix direction: either exempt hazards whose fill-rect footprint overlaps a portal’s own destination-adjacent geometry from healing (cheapest: give pit 4 heals:false too, matching pit 3/5/6/7’s already-established “hazards near a secret stay open forever” convention — pits 1/2 are the only ones far from the tunnel and are the only ones that still make sense as healable), or narrow TUNNEL_FLOOR_W so the crawlspace floor never extends under a healable pit’s own x-range in the first place. Either is a one-line change; not attempted here per this doc’s read-only scope.


4. Tanzstunde S2’s teach-portal and Clouds S2’s Upperworld portal are walk-in on the only ground path — structurally “optional secret,” behaviorally near-mandatory

What: Both TANZSTUNDE_S2.portal (trigger x=1795, y=176, w=16, h=16, no requiresSneak) and CLOUDS_S2.portal (trigger x=1560, y=164, w=20, h=16, no requiresSneak) sit directly on their level’s sole continuous ground/platform path, with no crouch-gate distinguishing “walking past” from “entering.” Compare with LIDL_LUNCH.portal (requiresSneak: true — walking upright through the trigger zone never fires it) and SOCCER.portal (only reachable by a deliberate jump onto an elevated crate well off the main ground line) — both genuinely skippable by ordinary forward movement. A player would have to deliberately time a jump so their entire hitbox clears y:176 (Tanzstunde) or y:164 (Clouds) while horizontally inside a 16-20px window, with no in-game reason to attempt that jump at all, to avoid either portal. In practice, walking normally triggers both every time.

Why it’s not a bug: neither portal gates anything required for progression — the teach room (underworld_tanzstunde.ts) and the Upperworld recall room (upperworld_clouds.ts) are both reward/lesson content, not equipment sources, and boss.checkReached() never checks anything from either. If anything, the teach-portal being effectively mandatory is a small, accidental mitigant for PF-3 (dance lesson not looping/being missable) — a player is unlikely to skip the lesson by accident, unlike missing a genuinely optional secret.

Severity: Informational/Low. Worth naming because it reads structurally identical to the Lidl Lunch/Soccer secrets (a PortalDef on a level) while behaving completely differently (unavoidable vs. genuinely optional) — a future author reusing the “secret portal” pattern for something with real stakes (equipment, a hard gate) should know requiresSneak or off-the-ground-path placement is what actually makes a portal skippable, not merely being a PortalDef.

Fix direction: none needed for correctness. If PF-2’s “portals too close to the boss” repositioning work happens, worth deciding at the same time whether these two should stay walk-in-mandatory (arguably correct for the teach portal specifically) or gain requiresSneak/off-path placement to genuinely match “secret to find” framing.


5. Underworld Mushroom House’s lava pool can produce a slow multi-hit knockback grind at low speed tier — by design, but worth naming as a friction risk

What: lava (w:105, h:10) is deliberately sized, per its own items.ts comment, “close to the minimum that still blocks a max-speed-tier jump (~93px)” rather than padded wider. The same comment already flags the tradeoff: takeHit()’s knockback (HURT_KNOCKBACK=90px/s, opposing the direction just traveled) “fights forward progress through any hazard wider than one hit’s recovery distance,” and a decisive full-speed run is what avoids “a slow multi-hit grind… at walk speed instead.” A player who reaches this secret room early (Soccer, level 3 of 5) without ever picking up River Cola speed-tier boosts is running at RUN_BASE=80px/s at best, well under the max-tier speed the pool’s width assumes, and can get knocked back into the pool’s own start repeatedly.

Why it’s not a lock: every hit just costs energy and knockback; running out of energy triggers player.respawn(true) (full energy, same spot in the level, hazards reset) rather than any permanent block. Worst case is a frustrating multi-attempt grind, not an unwinnable state — and the room already places a River Cola pickup (x:60) before the lava specifically so a fresh attempt can rebuild some speed first.

Severity: Low, informational. Flagged only because it’s exactly the shape the physics-audit brief asked to check (obstacle width sized against the jump/speed envelope) and the developer’s own comment already names the tradeoff as a known compromise rather than an oversight — recorded here so it’s not rediscovered as new.

Fix direction: none needed; if it ever becomes a real complaint, the standard levers are a shorter knockback distance for this specific hazard (ItemDef.knockback override, already supported) or moving the River Cola pickup closer to the pool’s entrance.


Summary table

#FindingCategorySeverity
1Tailored Clothes has 2 independent chances (Season 1 + Season 2 Soccer), not 1 — corrects PF-4’s framingHard-gate reachabilityContext correction (informational)
2Soccer Shoes / Dance Shoes are single-chance, soft-gated today, would be a worse-than-PF-4 lock if ever hard-gatedHard-gate reachability / latent riskMedium (latent)
3Minigolf pit-4 heal-rect overlaps the secret crawlspace floor’s footprintJump/obstacle-height physicsLow
4Two Season 2 portals are walk-in-mandatory despite reading as optional secretsHard-gate reachabilityLow / informational
5Mushroom House lava pool can grind at low speed tierJump/obstacle-height physicsLow / informational

No second genuine hard-block softlock (the dressCodeGuard/PF-4 class) exists anywhere else in the game — the moveSolids sweep was exhaustive and found only the one already known and dispatched.