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:
- Hard-gate reachability — every place player state conditionally blocks movement
(
moveSolids) or gates aPortalDef/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. - 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 atWALK_SPEED, ≈53px atRUN_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 onhasTailoredClothes). 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:
soccerShoesgates only whether a kicked/walked-intofootballGoalscores (main.tslines ~1106, ~1208) —def.kind === 'food', never added tomoveSolids. Missing it just means the goal stays inert;boss.checkReached()(the only thing that actually ends a level, perboss.ts) has zero awareness offootballGoalat all.danceShoesgates only whether adancePartner’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=689underground (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 owndx/dy, with no “was this already overlapping before this frame” check. Vertically this is self-correcting in practice —vyis essentially always slightly positive from gravity even when “standing still,” so thedy>0branch 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 underlyingmoveAndCollidemechanics happen to produce the same safe outcome here too). Horizontally it’s less clean — ifdx≠0at that exact instant, the player gets snapped to just outside the rect’s edge in the direction implied bydx’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
| # | Finding | Category | Severity |
|---|---|---|---|
| 1 | Tailored Clothes has 2 independent chances (Season 1 + Season 2 Soccer), not 1 — corrects PF-4’s framing | Hard-gate reachability | Context correction (informational) |
| 2 | Soccer Shoes / Dance Shoes are single-chance, soft-gated today, would be a worse-than-PF-4 lock if ever hard-gated | Hard-gate reachability / latent risk | Medium (latent) |
| 3 | Minigolf pit-4 heal-rect overlaps the secret crawlspace floor’s footprint | Jump/obstacle-height physics | Low |
| 4 | Two Season 2 portals are walk-in-mandatory despite reading as optional secrets | Hard-gate reachability | Low / informational |
| 5 | Mushroom House lava pool can grind at low speed tier | Jump/obstacle-height physics | Low / 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.