Skip to content

Walk mode is unreachable: the #718 gate asked its own output whether to open #723

Description

@TortoiseWolfe

Production. /chatt?diorama&walk — the navbar "Play" link — never enters first-person mode.
The camera stays in orbit, which idle-drifts (src/stage/Rig.ts:681), so the site sits in a
slowly rotating overhead shot of the city forever. Shipped in ba474e0e (#718, PR #720),
deployed 2026-08-13 18:50Z.

The cycle

#718 made the collision-BVH build lazy — a real ~780 ms win — but gated it on the state the
build is required to produce. In src/twin/TwinCanvas.client.tsx:

hop line needs
if (walkParam && walkReady && !didAutoWalk.current) setMode('walk') 1192 walkReady
if (walkCtrl) onWalkReady?.() 789 walkCtrl
setWalkCtrl(ctrl) — last line of tryBuildWalk 712 the build to run
if (!wantsWalkWorldRef.current) return; ← added by #718 620 wantsWalkWorldRef
wantsWalkWorldRef.current = mode === 'walk' — the only writer 729 mode === 'walk'

Hop 5 closes onto hop 1. The gate asked its own output for permission to open.

  • ?at= deep links died with itparseAtParam runs inside the gated function (676-685),
    so marker return links (Location HUD, spot markers and building inspection — make in-world reports reproducible #706) could not work either.
  • Manual entry still worked — digit 3 and the HUD ⋯ → Walk call setMode directly. That
    was the workaround.
  • Why chatt specifically spins: the wide atlasBox drops the tour (hasTour false, :1072), so
    the initial mode is orbit, which idle-drifts after 3 s.

The fix

The gate must ask "does this session want a walk world?" — a question with two sources that
are not downstream of the build: the ?walk URL intent at mount, and manual entry.
wantsWalkWorldRef is now seeded during render from a walkIntent prop (it must be render
time: React runs child effects before parent effects, so the mesh handovers reach the gate
before any SceneInner effect has), and the mode effect became a latch — monotonic
false→true. The old assignment also tore the gate down on the way out of walk, discarding
meshes that arrived while you were back in orbit.

#718's win is untouched: ?ortho, plain /chatt, the atlas and /twins/* carry no walk, so
no BVH is built. #676's handshake is untouched: seeding changes when the controller is built,
not when the camera enters walk, so the 1200 m/s glide-under-the-city cannot return.

Why nothing caught it

Both guards were shaped so they could not fail — see #725 for the CI-coverage half.
tests/unit/walk-world-lazy.test.ts was a regex over the source whose own comment anticipated
this exact bug ("the gate would be a permanent off-switch") and then asserted only that the
unlock line had been typed. It is replaced by tests/unit/walk-world-lazy.test.tsx, which
renders the real component with only the environment mocked and asserts walk is reachable
proven red before the fix, and re-proven red by reverting the seed alone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions