Tier KPI Overlay
A small KPI row that surfaces the player’s tier progression on the current planet: the highest tier they have ever reached, the next milestone tier that pays out gems, and a CLAIM button that lights up once the milestone has been met. It is driven by the per-planet tier_record cache on the client and the claim_tier_milestone RPC on the server.
What it shows
Three cells laid out in a single row:
- HIGHEST TIER — the personal best tier number on this planet, read from
useTierStore.getHighestTier(planetId). Renders an em-dash when the planet has never been played (highestTier === 0). - NEXT REWARD — the next unclaimed milestone tier (smallest multiple of 5 not yet claimed), read from
useTierStore.getNextMilestone(planetId). Walks up from 5 in steps of 5. - CLAIM button — shows the gem reward count (a magenta diamond icon plus the gem number) and the word CLAIM. It is enabled only when
canClaimMilestone(planetId)returns true (i.e.highestTier >= nextMilestoneand the milestone is not in the claims set). Otherwise it renders with alockedmodifier.
The gem reward formula is milestone * 2 (see getTierMilestoneGems in tierStore.ts), and the same formula is mirrored server-side inside the claim_tier_milestone RPC.
Where it lives in the UI
The Tier KPI row originally rendered as a standalone hub overlay (CSS class .hub-tier-kpi-overlay, positioned absolutely above the planet sprite at z-index: 4, left-aligned to the level-name underline). It has since been folded into the Challenge Popover as TierKpiSection, where it sits above the challenge card list inside the hub-popover-overlay modal. The standalone .hub-tier-kpi-overlay CSS class still exists in hub.css but is no longer mounted on the hub screen — the row only appears when the player opens the challenges popover.
Claim flow
When the player taps CLAIM:
- The button locks (
claiming = true) to prevent double-submit. invokeRpc('claim_tier_milestone', { p_planet_id, p_tier_milestone })fires against Supabase.- The server returns the new wallet snapshot plus the claimed
{ planet_id, tier_milestone, gems_awarded }. useWalletStore.replaceFromSnapshot(response.wallet)updates the gem/credit totals.useTierStore.addClaim(planetId, tier_milestone)records the milestone locally so the next-milestone walk advances to the following multiple of 5.
If the RPC throws, the error is logged and the button re-enables — no optimistic local mutation happens before the server confirms.
Authority and hydration
The store is a hydrated runtime cache, not the source of truth. Supabase owns the canonical tier_records and tier_claims rows; the client receives them in the bootstrap_player response on app open and pipes them through useTierStore.loadFromBootstrap(records, claims). After a run, finalize_run returns the new highest tier and updateFromRunResult(planetId, highestTier) advances the local record using Math.max so a worse run cannot regress it.
Files
src/metagame/components/ChallengePopover.tsx—TierKpiSectioncomponent (the live mount point).src/starship-survivors/stores/tierStore.ts—useTierStore,getTierMilestoneGems, hydration and claim mutators.src/metagame/screens/hub/hub.css—.hub-tier-kpi-overlaylegacy styles,.hub-challenge-tier-row/.hub-challenge-claim-btnlive styles.
Related
- Tier Progression — the 4-minute escalation counter that feeds
highest_tier. - Challenge Mode — the popover that now hosts this row.
- Gem Economy — where the milestone gem payout lands.
- Leaderboard System — uses the same
highest_tiervalue for ranking.