Repository navigation
Player profile: Progression tab, reward track, profile links and sharing, and end-of-game rewards - #5774
Conversation
A level badge (level bands, prestige emblems, the Legend crown) and an XP bar for the account menu, player profiles and the game stats modal. On the end-of-game screen, the game's XP is revealed a source at a time: a caption per source rolls in over a segmented bar that climbs, pausing with an animation at each level reached, while each source's card lands below. Multipliers then wipe across the cards in the bonus's colours, a subscription tier's own (Sovereign, Warlord, Vanguard), and a milestone or Legend gets its own card. Tapping skips to the end. The modal's buttons get the store's texture and a hover. Every API state is handled (signed out, calculating, ineligible, result), and nothing shows while the API reports progression is off. Resolves #5725 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL flags the regex (incomplete multi-character sanitization). These tests only compare SVG markup, but removing the comment nodes through the DOM is simpler to read and leaves nothing to flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile card heads the Stats tab and public profiles, with a compact row on the Account tab. At level 100 your own card offers Prestige: a full-screen confirmation (the new emblem, the prestige track, what it unlocks), held down to confirm, then a ceremony on the menu background. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- End a hold when the button loses focus, the window blurs or the tab is hidden, so a key released elsewhere can't let it run to a prestige. - Ignore open() while the flow is up, focus the hold button on open, keep Tab inside the confirmation, and take Escape before the page behind does (cancel the confirmation; skip, then close, the ceremony). - Keep the idempotency key per rank until a prestige succeeds, so reopening after a lost answer gets that prestige back instead of a 409. A 409 now reloads the account; any other failure no longer claims nothing changed. - Give each profile card its own hex pattern id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Parse a malformed /users/@me progress object as absent instead of failing the whole profile (which made the player look signed out). - Decide "signed out" for the XP panel from auth state: a /users/@me failure with a live session now hides the panel rather than telling a signed-in player to sign in. The rule lives in ProgressionAccount and is shared by WinModal and GameStatsModal, so both accept a CrazyGames session. - Show a player who was already a Legend with the Legend frame for the whole reveal. - Remove each poll wait's abort listener when its timer fires. - Load the profile's level independently of the profile itself, still dropping a response for a superseded load. - Add a keyboard-reachable skip button for the reveal, and give the XP progress bars an accessible name and value text. - Translate the prestige tab's "P{prestige}" label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On this branch the public profile shows the level on <profile-card>, not the summary #5726's tests look for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Re-read /users/@me after a prestige and broadcast it, so the header's account menu shows the new rank (the purchase flows' pattern). A replayed answer's rewards are listed once. - Keep focus in the overlay through every stage: on the dialog while the request is out, on the ceremony while it plays, then on Continue. Tab is trapped throughout. - prestigeMe reports why it failed: refused (409), signed out (no session, 401/403) or failed. A signed-out player is told to sign in, and a 409 now says the account was refreshed (or asks for a reload). - Draw the hold's fill straight onto its element, so a hold no longer re-renders the whole overlay every frame. - celebrate() is documented as the preview entry point and ignored while the flow is up. - Direct tests for prestigeMe's header and status mapping, and for the account page's handling of prestiged and prestige-stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, abortable XP fetch - Explain games recorded ineligible as "unverified" (stats the votes did not agree on) instead of the generic line. - Only play the XP reveal while the win modal is on screen: a result that lands after it is dismissed shows in its final state, and dismissing it mid-reveal jumps to the end. A panel removed mid-reveal also ends it, so it never comes back half-revealed and aria-busy. - Pass pollGameXp's abort signal into the request itself (combined with its timeout via AbortSignal.any), so aborting cancels a request in flight. - Default the breakdown's per-source fields and leftEarly, so XP rows scored under the first XP rules (no `played`/`leftEarly`) still parse. - Read your own profile's level from the public endpoint, not the /users/@me copy cached at page load. - Put the level in the nav account button's accessible name, which its aria-label otherwise hid from screen readers. - Drop xpBar's unused animate/durationMs options and fix the milestone comment that mentioned a share button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/client/PlayerProfileModal.ts # tests/client/ProgressionModals.test.ts
The API now pays +100 XP for the first eligible public game each UTC day,
win or lose, instead of +200 for the first win, and reports it as
`firstGame` in the XP breakdown. Rename the schema field, the line key and
its label ("First game of the day"). Feats stay in the breakdown but are
always 0 for now; zero lines are already left off the cards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The subscription tiers' XP multipliers move from 1.2x / 1.3x / 1.5x to
1.25x / 1.5x / 2x, so the client now recognises Vanguard at 1250,
Warlord at 1500 and Sovereign at 2000 permille. The bonus line's percent
is still derived from the multiplier ("+100% XP (Sovereign bonus)"), and
any other value, the old ones included, shows as a plain subscriber bonus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The public progress response can now carry the player's prestige history (when each rank was entered) and milestones (the first time each of levels 10/25/50/75/100 was reached in each prestige run). Both are optional, and a malformed list reads as absent rather than failing the whole response. The profile gains a Progression tab after Clans, only for a player with a level: - Prestige history: one tile per rank entered with its badge and date, or "Hasn't prestiged yet". - Milestones: grouped by run, newest first. The current run shows all five milestones, the ones still ahead faded as "Not yet"; earlier runs show only what they reached. A Legend's run is named for it, and its level 100 wears the Legend badge. Dates use the viewer's locale in the same day/short-month/year format as the other dates in the client. A link (or the tab last left open) asking for Progression selects it once the progress arrives, and stays on Stats when there is none. The reward track will join the tab later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a fourth tab (Progression) the tab row wrapped onto a second line at phone width. It now stays on one row that scrolls sideways when it doesn't fit, still centred when it does, and the active tab is scrolled into view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canonical profile link is now https://<site>/player/<publicId>, the link the site unfurls into a profile card. Opening it, with or without a trailing slash, behind a /w<n> worker prefix, and with an optional ?tab=stats|games|clans|progression, opens that profile on load. The hash form (#modal=profile&publicID=...) keeps working. The modal router learns path routes: while a profile opened from its path stays open the URL keeps the path, with tab changes written to ?tab=; closing it (Back, Escape) returns to the home page and replaces the URL with / so a reload doesn't reopen it. Opening another modal hands the URL back to the hash. On the profile: - Signed-out viewers get a "New to OpenFront?" banner above the card, whose Play button closes the profile onto the home page. - Share buttons under the card: Copy link, Share on X, and Copy for Discord (a short line and the link, which Discord unfurls). Touch devices with a native share sheet get one Share button instead. Copies toast as elsewhere and are announced to screen readers. - Not found now explains itself, shows the link that was opened, and offers Play OpenFront and View leaderboard. The friends box accepts a pasted /player/<id> link. Game server workers serve the app shell at /player/<id> (the master's SPA fallback already did for the unprefixed form), and the dev server does the same for /w<n>/player/<id>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Play OpenFront and View leaderboard stacked on desktop because the not-found block was capped at max-w-md. The block is wider now (the text keeps its measure), and the buttons sit side by side from sm up and stack on a phone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
Priority: ⬇️ Low Change: Feature Merge Risk: ⚪ Minimal · up to The Progression tab, reward track, profile links, and sharing features have no open defects in this review. The earlier tab-selection and reward-track concerns appear addressed, so the change is ready to merge after normal checks. Pre-merge checks |
|
- The profile's search-visibility comment described an API that has since shipped; removed. - The reward track no longer hides itself for a config without per-level amounts (trackHasRewards), nor shows the owner the visitor view when level rewards lack level/prestige: the API always sends both now. A level reward missing them is simply left off the track. - The Claim all on the profile and the end-of-game Claim share one loop, claimRewards() in RewardClaims.ts: not_found counts as claimed, any other result than the success shape stops the run, and /users/@me is dropped once anything was claimed (the reveal now does so after a partial claim too). - One reward-amount parser (rewardAmount in Progression.ts) and one set of Progression-tab section classes (ProgressionSection.ts). - RewardTrack tests use the current Plutonium cadence (every 25 levels). - File-local types and constants are no longer exported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Celant heads-up: this PR now combines #5774, #5775 and #5786 (approved by the designer to cut down the PR count). All three were reviewed separately and their commits are unchanged; what's new since your last look is two commits on top: 67b202d drops #5786's per-rank prestige emblems (superseded by a new design; 🤖 Generated with Claude Code |
…el rewards yet The client can reach players before the API that sends the per-level amounts and stamps level rewards with their level and run. Until then: - trackHasRewards(): the profile leaves the track out when the config has no per-level Caps/Plutonium amounts (nor level flares). - statuses: when a level reward lacks level/prestige, the owner sees the track as a visitor does (no claim status, no Claim all, no key), with a one-time console warning, rather than every passed level reading as claimed. Restores the tests for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 Claude Code ReviewVerdict: No issues found. Findings: 0 critical, 0 major, 0 minor. No issues found. Checked for bugs and CLAUDE.md compliance. Scope covered: homepage-chunk and lazy-modal import rules ( |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Now that main has isRewardClaimable() and `held` (#5879): - claimRewards() takes the rewards themselves and never claims a held one; a `{ held }` answer still counts as not claimed. - Reward track: Claim all and its totals leave held rewards out, and a level whose only unclaimed rewards are held no longer reads as ready (it shows no claim status, as a passed level does for a visitor). - End-of-game rewards row: a held reward is listed apart with the rewards panel's "Trusted accounts only" label (or its generic note for another hold), is never claimed nor counted in what was claimed, and the row offers no Claim when every reward is held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Conflicts with
Verified with
Everything else on this PR is ready: the CI Claude review is clean at the current head, all required Worth being careful with |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When this client's copy of the account is stale, the API can refuse a
claim with { held } (held until the account is trusted). The claim run
treated that as a failure and stopped, so every retry stopped on the same
reward and the rest were never claimed until /users/@me was re-read.
claimRewards now notes such a reward in ClaimRun.held (id and hold),
moves on, and drops the cached /users/@me when anything was claimed or
refused as held. The end-of-game row shows a refused reward as held from
then on (same label, not claimed again, not counted), and the reward
track's Claim all marks it held and re-reads the account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged Two commits for #5879, now that it's on
One test from #5879 changed: Typecheck and lint pass. The full suite passes apart from the Windows-only shell-script tests and load timeouts, which pass when run alone. 🤖 Addressed by Claude Code |
Keep both reworks of the end-of-game XP panel: #5759's provisional and reconciled states, celebration mode and status/reconcile notes, and this branch's level rewards row, unlocks, glowing bar edge and moment share. Rewards and unlocks show (and are claimed) only for the server's confirmed result; a celebration that reaches no new level brings the rewards row in at its start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…evels A link to a profile's Progression tab, routed while another profile is open on Games or Clans, now holds on Stats and switches to Progression once that player's progress arrives, rather than staying put: an explicit tab request wins on a re-open, as it already did on a first open. On your own profile the reward track's progress can lag a game behind the freshly read /users/@me rewards the claim bar counts. A level this run has an unclaimed level reward for now counts as reached, so it shows ready rather than locked and the track and claim bar always agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged Merge: Nit 1, linked tab: a Nit 2, track vs claim bar: a level this run has an unclaimed level reward for (claimable or held) now counts as reached, so the track and the claim bar read the same rewards. A level just reached shows ready, not locked, and the claim bar counts it once. One side effect: the "You" marker can sit a level ahead of the rest of the profile until the public progress catches up. Typecheck and lint pass. Full suite: 8,750 passed, and the only failures are the Windows-only 🤖 Addressed by Claude Code |
Resolves #5772
Resolves #5773
Part of #5785 and #5760.
This PR now combines three that were stacked on each other, so they can be reviewed and merged as one: the Progression tab and reward track (was this PR), profile links and sharing (was #5775), and the end-of-game rewards and XP polish (was #5786, minus its prestige emblems; see below). Every part has been reviewed in its own PR already; the history is kept, with each part's commits as they were.
What was dropped while combining
LevelBadge.tsand its tests are identical tomain. The cached-template speed-up that only existed to offset those emblems' drawing cost went with them.claimRewards()(RewardClaims.ts) now does the claiming for both the reward track's Claim all and the end-of-game rewards row; one reward-amount parser; one set of section styles.Part 1: Progression tab and reward track
A Progression tab on the player profile: prestige history, and the date each milestone level was first reached in every prestige run.
The data
GET /public/player/:publicId/progressgains two optional fields on the backend:prestigeHistory: [{ rank, at }], ordered by rank;milestones: [{ prestige, level, at }], for levels 10/25/50/75/100, the first time reached per prestige run.Both are added to
PublicProgressSchemaas.optional().catch(undefined), so older responses still parse and a malformed list reads as absent rather than failing the whole profile.The tab (
<profile-progression>,PlayerProfileModal.ts)toLocaleDateString(undefined, { year, month: "short", day }). An unreadable date isn't shown.GameStatsModal(from Show player levels and the XP each game earns #5726), so nothing was needed there.Popup tabs on narrow screens (
baseComponents/Modal.ts)With a fourth tab, the shared tab row wrapped onto a second line on phones. Now:
justify-center, never push a tab off the left edge out of reach;This is shared, so every popup with tabs gets it; wide screens look the same as before.
Screenshots
From this branch's stack (the profile page PR on top), opened from a real
/player/<id>link at 2x. The profile is sample data given to the popup, because the dev API has no such player.Desktop, Progression tab: prestige history, then milestones per run (the current run shows "Not yet" for 50/75/100).
Phone: the tab row stays on one line and scrolls sideways, with Progression scrolled into view.
Testing
prestigeHistory/milestones.flex-nowrap overflow-x-auto, non-shrinking tabs), and the active tab scrolled into view on change.tsc,npm run lintand the translation tests pass. In the full suite the only failures were the knownInventoryModal/MainInitializetimeouts, which pass on their own.Added (2026-10-04): the reward track
A "Reward track · Prestige N" section now heads the Progression tab, above prestige history and milestones. It shows what each level of the current run pays. Approved from mockups ("Layout A", with the next rank's reward inside the end card).
flares[]: this run's level rewards, plus every-run ones./users/@meis refreshed andrewards-changedis sent./users/@merewards, which now say which level and prestige run they paid for (openfrontio/infra#907). A passed level with no reward row counts as claimed.RewardTrack.ts(the model functions and<reward-track>).RewardSchemagains optionallevel/prestige.FlareCosmetic.tscan show a flare's own name and image until the cosmetics catalog answers.New
en.jsonkeys: all underreward_track:after_level_100,caps_amount,claim_caps,claim_count,claim_plutonium;earlier,later;key_claimed,key_locked,key_ready;legend_reward,legend_sub,levels_label,plus,prestige_reward;strip_end_legend,strip_end_prestige,strip_label,strip_level,strip_start,strip_value,strip_you;title,title_first_run,you.Screenshots and clip (built UI at 2x, sample data Wonder / wonder01):
Your own profile at level 47, then scrolled to the end for P4 → P5 with the Solar Corona tag. The 34px Solar Corona preview is the catalog's own animated effect, caught between bursts:
A P10 run ending in Legend, then P3 → P4 with no exclusive:
Phone, then a visitor's view. The visitor still predates a last fix that removed an empty status row, so the live track is about 16px shorter:
The pop-in, then a switch to Stats and back, which shows the tab at rest:
7-progression-tab-pop-in-60fps.mp4
Testing for these additions:
RewardTrack.test.ts(39 tests):both, and no replay on the same opening.PlayerProfileProgressionTab.test.ts: the tab pops in once per opening, not when switching back.Results:
tests/client, plus the translation andApiSchemastests: 277 files, 3,726 passed.tscand lint are clean.Performance: load the levels screens on demand (2026-10-05)
Benchmarks against main showed the levels UI added 9.4% (67 KB gzipped) to the startup download. Screens most sessions never open now load when they're needed:
Ceremonycode): downloads as soon as the Prestige button appears. If the download fails, pressing it shows an alert (prestige.load_failed) and the next press retries.Two build fixes go with this:
FlareCosmetic.tsnow importsCosmetics/CosmeticPresentation/CosmeticPreviewstatically. They were in the startup bundle anyway, and this removes threeINEFFECTIVE_DYNAMIC_IMPORTwarnings.vite.config.tskeeps everything on the startup import chain in the entry chunk, so the bundler no longer splits extra startup files. On main it builds byte-identical chunks.Results:
Unthrottled page load is within noise. The prestige, Legend, level-up, Claim and reward-track scenarios pass on the lazy production build.
Tests:
WinModalXp: the ceremony fallback, and downloading only for possible Legends.AccountModalPrestige: the load on press, and the failure alert.RewardTrack: no track when it fails to load, and the parallel download.tsc, lint andtests/clientpass.Part 2: Profile links and sharing
openfront.io/player/<publicId>now opens that player's profile directly, so a shared link lands on a real page. The backend's link previews make the same URL unfurl in Discord, X and WhatsApp with the player's level and card.Opening from a link (
ModalRouter.ts,Main.ts)ModalRoutergainsregisterPath()androuteFromPath();Main.handleUrltries the path just before the hash./player/<id>, with or without a trailing slash, behind/w<n>/and/v/<commit>/, with an optional?tab=stats|games|clans|progression.[A-Za-z0-9_-]{1,22}; anything else is ignored. The hash form (#modal=profile&publicID=…) still works.?tab=./(or/v/<pin>/on a version-pinned page), so a reload doesn't reopen it. Opening another popup in-app hands the URL back to the hash form.Links (
utilities/PlayerProfileUrl.ts)playerProfileUrl()is nowshareOrigin()/player/<id>: the page's own origin on the web, and the site origin in the desktop shell, the same as lobby invite links./player/<id>link.Share row (
components/ProfileShare.ts)Under the profile card:
aria-livestatus, since the toast isn't a live region.https://x.com/intent/post?url=…&text=…withnoopener,noreferrer. The desktop shell already sends https links to the system browser.navigator.share: one Share button opens the native share sheet instead of the three. It's gated on touch because desktop Chrome on Windows also exposesnavigator.share. A dismissed sheet does nothing; any other failure falls back to copying the link."Player not found"
Replaces the old 📊 line with the approved design:
smup, stacked on a phone.The old
player_profile.not_foundkey is removed; the translation test fails on unused keys.Server (
src/server/PlayerProfileRoute.ts)/player/<id>./w<n>/player/<id>reaches a worker, which returned 404, soregisterPlayerProfileRouteserves the shell there, in theGamePreviewRoutestyle./w<n>/player/as well as/w<n>/game/.Screenshots
From this branch at 2x, opened from real
/player/<id>links. The profile is sample data given to the popup, because the dev API has no such player. The not-found shots are the real failure path.Desktop: the profile card and the share row. Signed-in and signed-out viewers see the same page (the "New to OpenFront?" banner was removed in a18d74e).
Desktop, Progression (from #5774), opened with
?tab=progression:Player not found (the real failure path, from
/player/aB3dE5fX):Phone: Stats (the three share buttons collapse to a native Share button on touch devices with share support; headless Chrome has none, so the three show here), Progression, and Player not found with the buttons stacked.
Added: sharing level-up moments
Players can now share a milestone, Legend or a new prestige rank, using the same Copy link / Share on X / Copy for Discord options as the profile. The link is the profile with the moment, e.g.
/player/wonder01?moment=level50. openfrontio/infra#910 unfurls it as that moment's celebration card.End-of-game popup:
When it shows:
With several milestones in one game it shares the highest, and Legend wins over any level.
Prestige ceremony: a share row with a dark backing sits between the title and Continue. Clicks on it don't close the ceremony.
Code:
<profile-share>defines its actions once and gains amenulayout. The profile's own row works as before.MomentShare.tsworks out what to share.GameXpPanelfiresxp-reveal-settledwhen the reveal ends.PrestigeFlowtakes apublicId.New
en.jsonkeys:progression.share_moment_level,share_moment_legend,share_text_level,share_text_legend,prestige.share_text.Screenshots from this branch at 2x. The XP results are sample data given to the popup. In dev the link starts with the local origin; in production it's
https://openfront.io.Level 50, closed and open, and Legend:
Phone, closed and open:
Prestige ceremony, desktop and phone:
Testing for these additions:
MomentShare.test.ts, plus 7 menu tests inProfileShare, 8 inWinModalXpand 3 inPrestigeFlow. They cover:tscand lint are clean.Added (2026-10-04): share ring on hover, and a share row on the Legend ceremony
:focus-visible). It's at full strength instantly, held for 80 ms, then fades over 320 ms while growing to 1.04×.(hover: hover) and (pointer: fine)only) or under reduced motion.ProfileSharerenders its own ring element and plays it withring.animate(...). It's not a pseudo-element on the trigger, because the trigger's.win-action::afteris already its hover streak.?moment=legendthe same way the prestige ceremony does.Built UI, sample data:
The share button in the end-of-game footer, hovered twice:
P6-share-ring-footer-hover.mp4
The Legend ceremony's final screen with the share row:
Tests:
ProfileShare.test.ts: the ring element, oneanimateper hover with the exact keyframes, a hover during playback ignored, focus-visible, touch and pen ignored, nothing under reduced motion or a coarse pointer.LegendCeremony.test.ts: the row only at the end, between the line and Continue; URL and text; clicks kept; no row without an ID; Tab cycling.WinModalXp.test.ts: the?moment=legendURL.Results:
tscand lint are clean, andtests/clientpasses (3,654).Testing
PlayerProfilePage.test.ts: path parsing in every form, invalid ids ignored, opening on load,?tab=sync, closing resets the URL, the not-found state;PlayerProfileUrl.test.ts;ProfileShare.test.ts: copy calls the clipboard, X URL encoding, the native share path and its fallback;tests/server/PlayerProfileRoute.test.ts.ShareBase,NavAccountMenu,FriendsListPublicIdInput, and the profile tests' mocks.tscandnpm run lintare clean. In the full suite the only failures were the knownInventoryModal/MainInitializetimeouts, which pass on their own.Part 3: End-of-game level rewards and XP polish
Approved from mockups and prototype clips.
1. The XP bar's leading edge glows while it fills
While the bar fills, its front edge glows and a sheen runs along the fill. It uses
::after/::before, moved by transform only. It shows only whilebarAnimateis on, and it's off under reduced motion.2. A game's level rewards, shown and claimed in the reveal
When the confirmed result (never the provisional one) levels you up, a row shows what those levels paid. It reads
rewards/flaresfromGET /users/@me/xp/:gameId(openfrontio/infra#907, #911):How the row behaves:
/users/@meis refreshed so the Locker shows it.GameRewards.ts(sums, claiming, unlock lookup) andGameXpRewardSchema/GameXpFlareSchema. The new fields are optional, so an older API still parses.3. The nav's XP bar catches up after a game
When a refreshed
/users/@meshows more XP, the account menu's bar fills to the end, the badge pops as the new level lands, the bar resets, then it grows to the new progress.4. Reduced motion on the "calculating" spinner
The reveal's spinner now has
motion-reduce:animate-none.New
en.jsonkeysAll under
progression:rewards_balancerewards_balance_capsrewards_claimedrewards_claimingrewards_eachrewards_level_manyrewards_level_onerewards_levelsunlock_in_lockerunlockedunlocked_cosmeticScreenshots
From the branch at 2x with sample data (Wonder / wonder01). These were taken before the prestige emblems were dropped, so any badge at prestige 1 or above in them shows the old per-rank emblem; the shipped badges are main's. Nothing else in them changed.
The bar's leading edge while it fills:
clip-bar-edge.mp4
Rewards: one level, several levels, a milestone with an unlock, and claimed:
The full reveal to level 50, the unlock, the rewards row and a Claim:
clip-rewards-claim.mp4
Phone, milestone:
Nav menu at rest, then catching up after a game:
clip-nav-catchup-v2.mp4
Testing
New:
GameXpRewards.test.ts(18 tests):NavAccountCatchUp.test.ts: when it animates or snaps, the fill → pop → reset → grow sequence, the fallback, and the layout (bar inside the badge-and-avatar row; the avatar node is kept).Updated:
GameXpPanelReveal.test.ts: the bar edge class and the spinner.ProgressionSchemas.test.ts.WinModalXp.test.ts.Results:
npx tsc --noEmitandnpm run lintare clean. The translation tests and all oftests/client(277 files, 3,574 tests) pass.Checks after combining
npm run typecheckandnpm run lintpass.npx vitest --run: 8,200 passed; the only failures are the Windows-onlyUpdateRegistertests, which fail onmaintoo.tests/client/RewardClaims.test.ts(6 tests) for the shared claim helper, including a reward the API refuses as held (Rewards: show level Plutonium held until the account is trusted #5879).Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
Iamlewis
🤖 Generated with Claude Code