Skip to content

feat(#115): add free-cam exploration to the world map - #864

Merged
zgeoff merged 10 commits into
mainfrom
feat/115-worldmap-free-cam
Aug 11, 2026
Merged

feat(#115): add free-cam exploration to the world map#864
zgeoff merged 10 commits into
mainfrom
feat/115-worldmap-free-cam

Conversation

@zgeoff

@zgeoff zgeoff commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Closes #115

Replaces the locked-node isometric camera with a free-roam camera-controls rig, and drives the world-map reveal query from the camera's live viewport instead of a fixed radius around the avatar.

  • Drag/touch truck and wheel/pinch dolly, clamped so every viewport stays under the reveal cell cap; rotation locked; rubber-band boundary at the world bounds
  • Tap-to-focus calls controls.moveTo(...) instead of hand-computed offsets
  • New viewport store slice tracks the camera's cell footprint, updated per-frame via frustum-to-ground raycasting
  • New chunk-memoized viewport-bounded graph builder replaces the fixed-radius region graph and sphere-cull nearby graph
  • app-web keeps the reveal-nodes query cache warm for the chunk-aligned viewport and derives the avatar region graph from it
  • Fixes a test-infra bug where app-web's zustand reset registered after stores were already created, leaking state across test files
  • Removes the now-unused @react-spring/three dependency

Testing

  • bun run typecheck passes
  • bun run test passes
  • bun run lint passes
  • New tests added for new functionality

zgeoff added 5 commits August 11, 2026 16:27
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.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 662ad089-f17e-4be7-9584-e9ec5f634dbc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The 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.

Changes

World map exploration

Layer / File(s) Summary
Viewport geometry and graph generation
libs/game/worldmap-core/src/index.ts, libs/game/worldmap-client/src/utils/*, libs/game/worldmap-client/src/build-viewport-graph.ts, libs/game/worldmap-client/src/build-viewport-graph.test.ts, libs/game/worldmap-client/src/index.ts
Adds camera viewport projection, chunk alignment, and viewport-scoped graph generation with chunk caching. Removes the fixed-radius graph and nearby-graph utilities.
Viewport state and graph refresh
libs/game/worldmap-client/src/state/*
Adds nullable viewport state, store accessors, and viewport updates. Existing region keys now refresh their graph while preserving selection.
Free camera and viewport tracking
libs/game/worldmap-client/src/components-three/isometric-camera.tsx, libs/game/worldmap-client/src/components-three/viewport-tracker.tsx, libs/game/worldmap-client/src/components-three/scene.tsx, libs/game/worldmap-client/src/consts.ts
Replaces spring camera movement with camera-controls, renders the full world graph, and tracks quantized camera viewports.
Avatar graph integration
apps/web/src/routes/-game/use-avatar-region-graph.ts, apps/web/src/routes/-game/use-avatar-region-graph.test.tsx
Builds graphs from the current viewport, refreshes after panning, and resets the viewport and origin selection when the avatar changes.
Revealed-node query and test preload
apps/web/src/routes/-game/use-revealed-nodes-query.ts, apps/web/src/lib/activity/build-revealed-nodes-query-options.ts, apps/web/src/routes/-game/game-world.tsx, apps/web/bunfig.toml, apps/web/register-zustand-reset-early.ts, apps/web/test-setup.ts
Adds viewport-aligned revealed-node prefetching and installs Zustand reset handling before test setup imports execute. Removes the obsolete camera dependency and adds the activity contract as a development dependency.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • zgeoff/vers#862: Provides the revealed-node activity endpoint and viewport types consumed by this PR.
  • zgeoff/vers#832: Established the local world-map generation that this PR replaces with viewport-based graph generation.
  • zgeoff/vers#826: Provides the chunk generation and edge collection utilities used by viewport graph construction.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: adding free-camera exploration to the world map.
Description check ✅ Passed The description directly explains the free-camera, viewport, reveal-query, testing, and dependency changes.
Linked Issues check ✅ Passed The changes satisfy issue #115 with free camera controls, viewport-bounded rendering and queries, soft boundaries, tap-to-focus, and supporting tests.
Out of Scope Changes check ✅ Passed The changes remain within issue #115, including related test-infrastructure and dependency cleanup required by the implementation.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/115-worldmap-free-cam

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b1895b7 and 449820d.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock, !bun.lock
📒 Files selected for processing (42)
  • apps/web/bunfig.toml
  • apps/web/register-zustand-reset-early.ts
  • apps/web/src/lib/activity/build-revealed-nodes-query-options.test.ts
  • apps/web/src/lib/activity/build-revealed-nodes-query-options.ts
  • apps/web/src/routes/-game/game-world.tsx
  • apps/web/src/routes/-game/use-avatar-region-graph.test.tsx
  • apps/web/src/routes/-game/use-avatar-region-graph.ts
  • apps/web/src/routes/-game/use-revealed-nodes-query.test.tsx
  • apps/web/src/routes/-game/use-revealed-nodes-query.ts
  • apps/web/test-setup.ts
  • libs/game/worldmap-client/package.json
  • libs/game/worldmap-client/src/build-region-graph.test.ts
  • libs/game/worldmap-client/src/build-region-graph.ts
  • libs/game/worldmap-client/src/build-viewport-graph.test.ts
  • libs/game/worldmap-client/src/build-viewport-graph.ts
  • libs/game/worldmap-client/src/components-three/isometric-camera.test.tsx
  • libs/game/worldmap-client/src/components-three/isometric-camera.tsx
  • libs/game/worldmap-client/src/components-three/scene.tsx
  • libs/game/worldmap-client/src/components-three/viewport-tracker.test.tsx
  • libs/game/worldmap-client/src/components-three/viewport-tracker.tsx
  • libs/game/worldmap-client/src/consts.ts
  • libs/game/worldmap-client/src/index.ts
  • libs/game/worldmap-client/src/state/create-viewport-slice.test.ts
  • libs/game/worldmap-client/src/state/create-viewport-slice.ts
  • libs/game/worldmap-client/src/state/set-viewport.test.ts
  • libs/game/worldmap-client/src/state/set-viewport.ts
  • libs/game/worldmap-client/src/state/set-world-region.test.ts
  • libs/game/worldmap-client/src/state/set-world-region.ts
  • libs/game/worldmap-client/src/state/use-viewport.test.ts
  • libs/game/worldmap-client/src/state/use-viewport.ts
  • libs/game/worldmap-client/src/state/use-worldmap-store.test.ts
  • libs/game/worldmap-client/src/state/use-worldmap-store.ts
  • libs/game/worldmap-client/src/utils/build-chunk-aligned-viewport.test.ts
  • libs/game/worldmap-client/src/utils/build-chunk-aligned-viewport.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/utils/build-viewport-from-camera.test.ts
  • libs/game/worldmap-client/src/utils/build-viewport-from-camera.ts
  • libs/game/worldmap-client/src/utils/update-camera-focus.test.ts
  • libs/game/worldmap-client/src/utils/update-camera-focus.ts
  • libs/game/worldmap-core/src/index.ts
  • package.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

Comment thread apps/web/src/lib/activity/build-revealed-nodes-query-options.test.ts Outdated
Comment thread apps/web/src/lib/activity/build-revealed-nodes-query-options.ts
Comment thread apps/web/src/routes/-game/use-avatar-region-graph.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 43 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread libs/game/worldmap-client/src/consts.ts
Comment thread libs/game/worldmap-client/src/build-viewport-graph.ts Outdated
Comment thread libs/game/worldmap-client/src/build-viewport-graph.ts Outdated
Comment thread libs/game/worldmap-client/src/components-three/scene.tsx
Comment thread apps/web/register-zustand-reset-early.ts
Comment thread libs/game/worldmap-client/src/components-three/viewport-tracker.test.tsx Outdated
Comment thread apps/web/src/routes/-game/use-avatar-region-graph.test.tsx
Comment thread libs/game/worldmap-client/src/components-three/isometric-camera.tsx
Comment thread libs/game/worldmap-client/src/components-three/viewport-tracker.tsx
Comment thread libs/game/worldmap-client/src/utils/build-viewport-from-camera.ts Outdated
zgeoff added 5 commits August 11, 2026 19:02
- 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.
@zgeoff
zgeoff merged commit 940e2ed into main Aug 11, 2026
6 checks passed
@zgeoff
zgeoff deleted the feat/115-worldmap-free-cam branch August 11, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

enable free cam exploration in the world map

1 participant