feat(#115): add free-cam exploration to the world map - #864
Conversation
Replaces the node-locked react-spring rig with camera-controls: drag/touch truck, wheel/pinch dolly clamped to a tunable zoom range, rotation pinned to the isometric angle, and a soft rubber-band boundary at the lattice's encodable range. Tap-to-focus now glides the locked camera onto a selected node instead of re-deriving its position by hand. Adds the viewport plumbing this unlocks: a store slice tracking the camera's current cell-coordinate footprint (throttled to whole-cell changes), a pure builder projecting the frustum onto the ground plane, and a chunk-memoized graph builder that replaces the fixed-radius region slice. Scene now renders whatever the store's viewport-driven graph holds instead of culling a node-locked sphere.
registerZustandReset() ran from inside test-setup.ts, after its own statically-imported register-*-mock modules (registerWorldmapSceneMock among them) had already pulled in and created worldmap-client's zustand store — so that store was never tracked for reset and leaked state across every test file in the suite. Moving the call into its own earlier bunfig preload entry lets it wrap zustand's create before any store-importing module evaluates.
Replaces the fixed REGION_RADIUS ring slice with a viewport-derived one: useAvatarRegionGraph now rebuilds from the store's camera-reported viewport (falling back to a fixed box around the origin before the camera reports one), resetting the selection only on an actual avatar switch. A new read-only query keeps the avatar's revealed-nodes cache warm for the viewport the player is looking at, chunk-aligning it first so a pan that stays inside the same chunks reuses the cached response instead of re-fetching on every cell crossed.
Addresses reviewer findings on the free-cam PR: - isometric-camera: reapply the current selection's target when the camera-controls instance is rebuilt (a scene remount), and clear the store's camera ref on unmount so the rebuild never targets a disposed camera. - use-avatar-region-graph: build from the origin-centred initial viewport on an avatar switch, regardless of where the outgoing avatar had panned, so the incoming avatar's origin node is always selectable. - use-revealed-nodes-query: drop the issue-number reference from the doc comment. - add missing coverage: ViewportTracker's write-throttle, IsometricCamera's pinned angle/zoom and tap-to-focus glide, and the remount regression.
The ZOOM_MAX_DISTANCE cap test measured the raw camera footprint, but the reveal query actually sends buildChunkAlignedViewport's output, which is always larger. Move buildChunkAlignedViewport into worldmap-client so the test can compose it with buildViewportFromCamera and sweep camera offsets across a full chunk period at the widest supported aspect ratio, catching the true worst case instead of an optimistic one.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe PR replaces fixed-radius world graphs with camera-driven viewport graphs. It adds free camera controls, viewport state and tracking, avatar graph refresh behavior, and viewport-based revealed-node query prefetching. ChangesWorld map exploration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/lib/activity/build-revealed-nodes-query-options.test.ts`:
- Around line 8-11: Remove the unnecessary `as unknown` assertion from the
`options.queryKey` assertion in the test, passing `options.queryKey` directly to
`expect` while preserving the existing expected query key structure.
In `@apps/web/src/lib/activity/build-revealed-nodes-query-options.ts`:
- Around line 11-15: Update the JSDoc above the revealed-nodes query options to
state that the query key changes when either the chunk-aligned viewport or
avatarID changes, preserving the existing explanation that cell-granular
movement within a chunk does not change the key.
In `@apps/web/src/routes/-game/use-avatar-region-graph.ts`:
- Around line 36-44: Update the avatar-switch handling around isAvatarSwitch in
the effect to call setViewport(null) when the avatar changes, clearing the
stored Zustand viewport while retaining INITIAL_VIEWPORT for graph construction.
Add a regression assertion verifying useViewport() is null after the switch.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2dfa9861-bd52-4ba3-b10c-fb638efb848c
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock,!bun.lock
📒 Files selected for processing (42)
apps/web/bunfig.tomlapps/web/register-zustand-reset-early.tsapps/web/src/lib/activity/build-revealed-nodes-query-options.test.tsapps/web/src/lib/activity/build-revealed-nodes-query-options.tsapps/web/src/routes/-game/game-world.tsxapps/web/src/routes/-game/use-avatar-region-graph.test.tsxapps/web/src/routes/-game/use-avatar-region-graph.tsapps/web/src/routes/-game/use-revealed-nodes-query.test.tsxapps/web/src/routes/-game/use-revealed-nodes-query.tsapps/web/test-setup.tslibs/game/worldmap-client/package.jsonlibs/game/worldmap-client/src/build-region-graph.test.tslibs/game/worldmap-client/src/build-region-graph.tslibs/game/worldmap-client/src/build-viewport-graph.test.tslibs/game/worldmap-client/src/build-viewport-graph.tslibs/game/worldmap-client/src/components-three/isometric-camera.test.tsxlibs/game/worldmap-client/src/components-three/isometric-camera.tsxlibs/game/worldmap-client/src/components-three/scene.tsxlibs/game/worldmap-client/src/components-three/viewport-tracker.test.tsxlibs/game/worldmap-client/src/components-three/viewport-tracker.tsxlibs/game/worldmap-client/src/consts.tslibs/game/worldmap-client/src/index.tslibs/game/worldmap-client/src/state/create-viewport-slice.test.tslibs/game/worldmap-client/src/state/create-viewport-slice.tslibs/game/worldmap-client/src/state/set-viewport.test.tslibs/game/worldmap-client/src/state/set-viewport.tslibs/game/worldmap-client/src/state/set-world-region.test.tslibs/game/worldmap-client/src/state/set-world-region.tslibs/game/worldmap-client/src/state/use-viewport.test.tslibs/game/worldmap-client/src/state/use-viewport.tslibs/game/worldmap-client/src/state/use-worldmap-store.test.tslibs/game/worldmap-client/src/state/use-worldmap-store.tslibs/game/worldmap-client/src/utils/build-chunk-aligned-viewport.test.tslibs/game/worldmap-client/src/utils/build-chunk-aligned-viewport.tslibs/game/worldmap-client/src/utils/build-nearby-graph.test.tslibs/game/worldmap-client/src/utils/build-nearby-graph.tslibs/game/worldmap-client/src/utils/build-viewport-from-camera.test.tslibs/game/worldmap-client/src/utils/build-viewport-from-camera.tslibs/game/worldmap-client/src/utils/update-camera-focus.test.tslibs/game/worldmap-client/src/utils/update-camera-focus.tslibs/game/worldmap-core/src/index.tspackage.json
💤 Files with no reviewable changes (7)
- libs/game/worldmap-client/src/utils/update-camera-focus.test.ts
- libs/game/worldmap-client/src/utils/update-camera-focus.ts
- libs/game/worldmap-client/src/utils/build-nearby-graph.test.ts
- libs/game/worldmap-client/src/utils/build-nearby-graph.ts
- libs/game/worldmap-client/src/build-region-graph.ts
- libs/game/worldmap-client/src/build-region-graph.test.ts
- package.json
There was a problem hiding this comment.
All reported issues were addressed across 43 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- reset the stored viewport inside the avatar-switch region writer, so no consumer reads the outgoing avatar's camera footprint - shrink the reveal query's chunk-aligned viewport to the cell cap so no canvas aspect can exceed a single request's bound; geometry stays uncapped - bound the chunk cache with a hand-rolled LRU map (256 entries) and cache each chunk's edges beside its nodes, ending per-pan edge recomputation - derive the region graph from the chunk-aligned viewport and skip rebuilds while the alignment holds, so the instanced mesh only churns on chunk crossings - typecheck the zustand-reset preload via the app tsconfig include list - hoist the per-frame raycaster and NDC scratch vector to module scope - pad the world boundary by the jitter margin so rim nodes stay reachable - add a zoom-clamp test dollying far past the maximum distance - correct the revealed-nodes query-key and selected-node doc comments - scope viewport-tracker write counts to the viewport slice and poll single node keys in region-graph waits
The per-frame tracker hit the ground-plane invariant during the frames between scene mount and the camera controls' first orientation update, and the resulting exception storm in the frame loop blocked all camera input. A camera with no ground footprint is real transient state, not a broken invariant: the builder now reports it as null and the tracker skips those frames.
The camera tests re-derived their expected positions through a second camera-controls instance in a top-level helper, which the testing guidelines ban twice over: test files declare no baseline-builder helpers, and an expectation computed through the library under test asserts nothing a broken pin wouldn't also produce. The rest position is now asserted as the contract states it: spherical radius at the zoom limit, angles at the pinned rotation. The reveal query-key test swaps a hand-written tuple behind an 'as unknown' cast for an inline snapshot of oRPC's own key encoding.
The viewport-tracker suite inlines its camera arrangement in place of a top-level builder helper; the region-graph suite inlines its shared viewport fixture and asserts selection through toMatchObject instead of optional chaining inside expect; the world-region suite arranges state through the exported setters instead of raw setState; and the viewport-graph suite gains the frozen-golden opener its deterministic pipeline calls for, narrows with invariant, and asserts edge endpoints with toSatisfyAll.
Closes #115
Replaces the locked-node isometric camera with a free-roam
camera-controlsrig, and drives the world-map reveal query from the camera's live viewport instead of a fixed radius around the avatar.controls.moveTo(...)instead of hand-computed offsets@react-spring/threedependencyTesting
bun run typecheckpassesbun run testpassesbun run lintpasses