feat: per-town leaderboard popover + explore/instructions polish#35
Merged
Merged
Conversation
…umns Row now reads as a single Town cell (name + aura bar + @owner) plus one Score column — the separate Aura and Visits numbers were redundant with the composite score. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a trophy HudButton next to the Population pill on the town view. Clicking it opens a right-anchored popover with the town's top visitors ranked by items + tags earned inside that town. Backed by a new GET /api/towns/[slug]/leaderboard, gated through resolveViewer (same policy as the items endpoint). Names + character come from the latest TownActivity row per subjectKey. Also: - /explore subtitle drops the aura + visitors formula for a friendlier one-liner; metadata description tightened to match. - TownInstructionsModal adds a brown-path hint underneath the door row. - Both popovers now use a fixed height (440) with a 70vh guard so the frame no longer grows with content. - InfoPageShell "Back to city" wording -> "Back to town" (already staged on branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five wins, all in the game module. Together they cut per-frame allocation
churn and per-frame work on the overworld — the surface where the lag
was most visible.
1. Guard boot() with a `cancelled` flag flipped by onSceneLeave. Every
await in the async boot pipeline (loadPlot, loadManifest,
loadPlotSprites) checks it before proceeding, so a stale in-flight
boot from a previous scene entry doesn't land its k.onUpdate handlers
or k.add game objects on the next scene. This was the accumulator —
each round-trip could double the camera-follow tick + proximity tick
+ duplicate every building sprite. Only a full page reload reset it.
2. Cache the extras manifest at module scope + drop `cache: "no-store"`.
Fetched once per page instead of every interior->overworld transition.
Silent-fail path also flips worldReady=true so TransitionLoading can't
get stuck if the fetch 5xx's; boot() is wrapped in .catch() with the
same rescue.
3. Delete three hot-path console.log calls in realtime.ts (per-remote
position update, publishLocal-queued, publish-ok success). Removed
string-interpolation garbage every publication + heartbeat.
4. Mutate `entry.parent.pos.x/.y` in place in the remote tween callback
instead of allocating a fresh k.vec2() per frame per remote. The
camera-follow pattern in kaplay's own docs uses direct mutation for
the same reason.
5. Add a per-scene RemotePlayer[] cache in realtime.ts, rebuilt inside
notify(). getRemotePlayersForScene() is now O(1) instead of O(N)
per call — the proximity tick (60Hz on the overworld) is the main
beneficiary.
6. Migrate drawPaths, drawPonds, and building signs from N x k.add() to
a single container + container.onDraw per surface. Positions are
pre-baked as Vec2 so the draw callback is allocation-free. Kaplay's
optimization guide explicitly calls this out ("Game objects have a
cost. Static visuals should render via onDraw calls, not as objects.")
On a dense plot this trims thousands of game objects off the per-frame
update list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ons to the HIGH pass. Neither is user-visible on its own; both strip per-frame CPU/GC from hot paths. 1. remotePlayer.ts reconcile() no longer allocates a throwaway Map<string, RemotePlayer> on every Centrifugo publication. Rewritten as a two-pass diff over a Set<string> reused across calls: first pass spawns / updates / marks, second pass destroys entries the current roster didn't mention. Fires on every remote position update. 2. interior.ts nearestInteract() is memoized on the player's tile key. Result is a pure function of player.tile, but ran a linear scan over spec.interacts every frame — collapses from 60Hz down to whatever the actual tile-change cadence is (a few Hz during movement, 0 while standing still). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HudButtonnext to the Population pill opens a right-anchored popover ranking visitors by items + tags earned in that town. Backed by newGET /api/towns/[slug]/leaderboard(gated viaresolveViewer, same policy as the items endpoint).aura + 10 × visitorsscoring formula in the subtitle for a friendlier one-liner. Metadata description tightened to match.PopulationPopover+ newLeaderboardPopoverboth use a fixedheight: 440(with70vhguard) so the frame stays constant regardless of list size.Test plan
TownActivity./api/towns/<slug>/leaderboardgets a 403./explore→ verify the new subtitle text renders and the towns table is unchanged.🤖 Generated with Claude Code