Skip to content

Show level badges on the ranked leaderboard and clan member lists - #5764

Merged
evanpelle merged 35 commits into
mainfrom
leaderboard-levels
Oct 12, 2026
Merged

evanpelle merged 35 commits into
mainfrom
leaderboard-levels

Conversation

@iiamlewis

@iiamlewis iiamlewis commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Resolves #5762

Stacked on the lobby badges PR (#5763), which is stacked on #5726. The base is that PR's branch, so this diff shows only this change. Part of #5760.

Description:

Level badges on the ranked leaderboard and clan member lists, designed so the leaderboard doesn't get cluttered.

The data

The API adds optional level, prestige and legend to each player row of /leaderboard/ranked and the clan member lists. Players without a level, or any response while progression is off, have no fields. They're one shared optional field set (RowLevelFields) on RankedLeaderboardEntrySchema, PlayerLeaderboardEntrySchema and ClanMemberSchema, so older responses still parse. toPlayerEntry passes them through.

On screen

  • Ranked leaderboard (1v1 and 2v2, LeaderboardPlayerList.ts): a 24px <level-badge> in front of the name, only for players who have prestiged or are Legends. Every other row is unchanged, so the badge stays rare enough to mean something. The pinned "your ranking" row follows the same rule.
  • Clan member lists (renderMemberRow in ClanShared.ts): a 24px badge in front of every member's name. At 24px the badge shows its prestige tab ("P3"); at 20px it was hidden.
  • Names line up: rows without a badge get an empty, aria-hidden badge-sized space (LevelBadgeSlot.ts).
    • This only happens when at least one row in the list has a badge, so both lists look exactly as they do today until levels launch.
    • Each ladder decides for itself, and the clan list decides from the members actually shown after a search.
  • Not changed: the tribe and clan leaderboards, and the clan manage and transfer views, which draw their own member rows.
  • No new text: the badge has its own accessible label.

Screenshots

These are screenshots of the code on this branch at 2x, with sample names and levels in the same shape the API sends them.

Ranked leaderboard. Badges only for players who have prestiged (P3 Kestrel, P1 Hollis_7, P2 Emberline) or are Legends (Marrow). Everyone else's name sits in the same column, so nothing zigzags.

Ranked leaderboard with badges only for prestiged players and Legends

Clan members. A 24px badge in front of every member, with the prestige tab visible. The guest at the bottom gets an empty space and still lines up.

Clan member list with level badges on every member

Testing

  • New tests:
    • schema parsing with and without the fields; a malformed or out-of-range value is dropped (that row shows no badge) rather than failing the whole list;
    • the leaderboard: badges only for prestige and Legend, none for prestige 0, the empty-space rule per ladder, the pinned row;
    • clan rows: badge, empty space, and plain rows, end to end through ClanDetailView including search.
  • Results: 743 tests pass across the level-badge, lobby, panel, leaderboard and clan suites. tsc, oxlint, eslint and prettier are clean.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory

Please put your Discord username so you can be contacted if a bug or regression is found:

Iamlewis

🤖 Generated with Claude Code

iiamlewis and others added 11 commits October 1, 2026 00:16
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>
- 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>
…, 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>
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 game server already calls /users/@me for every signed-in joiner; it
now reads `progress` from that response and stamps a display-only
levelBadge { level, prestige, legend } onto the player's lobby roster
entry (ClientInfo), so the lobby list and in-game panel can show level
badges without the client knowing anyone's publicId.

- ClientInfo gains an optional `levelBadge` as its last field. It takes
  the last free bit of the one-byte header, so entries without a badge
  encode byte-identically and pre-badge frames still decode.
- Worker join: levelBadgeFromProgress() keeps only whole level >= 1 and
  prestige >= 0, so a bad API value drops the badge instead of throwing
  out of the lobby_info broadcast. No progress (guest, progression off,
  failed/malformed lookup) means no badge.
- NameVisibility omits it on anonymized entries; it shows beside real
  names only (self, host-granted reveals, pinned teammates).
- Trust: ClientInfo is built server-side only; the join schema strips
  and the join frame cannot carry level fields.
- Client keeps the last roster's badges for in-game use:
  lobbyLevelBadge(gameID, clientID) in src/client/LobbyRosterLevels.ts.
- No UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renders the roster's levelBadge as a <level-badge> in front of the name.

Lobby list (LobbyPlayerView), all four layouts — FFA pills, the team-mode
Players list, team cards and spectator pills: a 24px badge before the
display name, ahead of the verified check / friend mark / host label.
In the two column layouts (Players list, team cards) a player without a
badge gets an empty aria-hidden 24px slot so the names line up, but only
when someone in that list has a visible badge; a lobby with no badges
renders exactly as before. The wrapped pills never get a slot.

Streamer mode: with anonymous names on, other players' badges are hidden
(the viewer's own still shows), as the verified check already is. Hidden
badges count as none for the slot rule.

In-game player panel: flag, then a 28px badge, then the name, for human
players with a badge in the remembered lobby roster
(lobbyLevelBadge(gameID, clientID)). Hidden for anyone but the viewer's own
player while anonymous names are on, matching PlayerView.displayName().
The name keeps truncating beside the badge.

The lit-mocking PlayerPanel tests now mock the LevelBadge module, since it
draws with lit's svg tag at import time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API now stamps optional `level`, `prestige` and `legend` onto ranked
leaderboard entries and clan member rows. Show them as a 24px
<level-badge> in front of the player's name.

- Schemas: the three fields are optional on RankedLeaderboardEntrySchema,
  PlayerLeaderboardEntrySchema and ClanMemberSchema (shared as
  RowLevelFields), so responses without them still parse. toPlayerEntry
  passes them through.
- Ranked leaderboard (1v1 and 2v2): a badge only for prestiged players
  and Legends. Other rows get an empty aria-hidden 24px slot so names line
  up, but only when some loaded row in that ladder has a badge, so the
  board is unchanged until levels launch. The pinned "your ranking" row
  follows the same rule. Tribe and clan boards are untouched.
- Clan member list (renderMemberRow, via ClanDetailView): a badge for
  every member with a level; members without one get the slot when any
  member in the visible (search-filtered) list has a badge.
- Shared rendering in components/LevelBadgeSlot.ts. No new text: the
  badge carries its own accessible label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iiamlewis iiamlewis added this to the v35 milestone Oct 3, 2026
@iiamlewis iiamlewis added UI/UX UI/UX changes including assets, menus, QoL, etc. Feature area:client Best-guess area: client (rendering, UI, web components) labels Oct 3, 2026
@iiamlewis iiamlewis self-assigned this Oct 3, 2026
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4b7044fb-daa8-4466-9f34-5d8f7942e4ce

📥 Commits

Reviewing files that changed from the base of the PR and between 5b34dae and 1596c6b.


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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 41405278-7707-42d7-86b9-7c2d3f80d73e

📥 Commits

Reviewing files that changed from the base of the PR and between 021c072 and 5b34dae.


📒 Files selected for processing (3)
  • packages/shared/src/ApiSchemas.ts
  • src/client/components/leaderboard/LeaderboardPlayerList.ts
  • tests/client/LeaderboardModal.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.



Walkthrough

The change adds optional level, prestige, and legend fields to leaderboard and clan-member schemas. Leaderboard and clan rows use these fields to render badges for eligible players and aligned empty slots where needed.

Changes

Leaderboard and Clan Level Badges

Layer / File(s) Summary
Level fields in response schemas
packages/shared/src/ApiSchemas.ts, packages/shared/src/ClanApiSchemas.ts, tests/ApiSchemas.test.ts, tests/client/clan/ClanApiSchemas.test.ts
Leaderboard and clan-member schemas accept optional level, prestige, and legend fields. Invalid optional values are omitted, and tests cover valid values, boundaries, and malformed values.
Badge and name rendering helpers
src/client/components/LevelBadgeSlot.ts, src/client/components/ui/PlayerNameLink.ts
The shared badge helper checks for complete level data and renders a badge or an optional empty slot. playerNameLink accepts an optional CSS class.
Leaderboard and clan row rendering
src/client/components/leaderboard/LeaderboardPlayerList.ts, src/client/components/clan/ClanShared.ts, src/client/components/clan/ClanDetailView.ts, tests/client/LeaderboardModal.test.ts, tests/client/clan/ClanMemberLevels.test.ts
Leaderboard rows, including pinned rows, and clan member rows render badges and reserve space where applicable. Tests cover ladder-specific slots, filtered clan lists, malformed fields, and name layout.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant LeaderboardPlayerList
  participant ClanDetailView
  participant ClanShared
  participant levelBadgeSlot
  participant LevelBadge
  LeaderboardPlayerList->>levelBadgeSlot: Render badge or reserved slot
  ClanDetailView->>ClanShared: Render filtered member rows with level-slot setting
  ClanShared->>levelBadgeSlot: Render badge or reserved slot
  levelBadgeSlot->>LevelBadge: Render badge when level fields are complete
Loading

Suggested reviewers: evanpelle

Merge Risk: ⚪ Minimal · up to 5b34d

No actionable issue remains from this review; the change is mergeable after normal checks.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 45 functions across 34 files. 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 and concisely describes the primary change: adding level badges to ranked leaderboards and clan member lists.
Description check Passed The description directly explains the schema changes, badge display rules, alignment behavior, affected views, tests, and validation results.
Linked Issues check Passed Issue #5762 coding requirements are implemented. RowLevelFields adds optional validated fields to the ranked and clan-member schemas. Invalid values are omitted without rejecting the row. Ranked 1v1…
Out of Scope Changes check Passed The changes stay within issue #5762. Schema updates, badge rendering, PlayerNameLink layout support, and tests directly support the requested behavior. No unrelated feature change is demonstrated in…

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Level fields arrive in rows,
A badge appears where prestige shows.
Empty slots keep names aligned,
Filtered lists show what they find.
Pinned ranks join the same display,
Clan names wear badges on their way.

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

@iiamlewis
iiamlewis marked this pull request as ready for review October 3, 2026 00:11
@iiamlewis
iiamlewis requested a review from a team as a code owner October 3, 2026 00:11
iiamlewis and others added 4 commits October 3, 2026 19:35
On a phone the bar under the XP reveal is under 200px wide, so the
centred "+1,150 XP" counter ran straight into the truncated
"444 / 2,41…" at its right end. Below a 16rem bar (a container query on
the bar's column) the two now stack, centred, at a fixed height so
nothing moves during the reveal. Wider bars keep today's layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stats modal's compact reveal panel becomes a static card between the
game summary and the tabs: the total, the level change (with a Milestone
chip and glow at 10/25/50/75/100) or the level and progress, each source at
its base value in three columns (one on a phone), and each multiplier as its
own line with the XP it added.

The XP endpoint only returns the caller's own record, so any 200 is shown,
including why a game didn't earn XP and the coming before_progression
reason. A 404 shows "Calculating XP…" and polls only for the player's own
game (account history or own profile) that ended in the last 10 minutes;
otherwise it shows nothing, as before.

The API rounds once over subtotal x game x subscriber, so a multiplier's
amount is total - subtotal only when it is the only one; with both, the
lines show no amount rather than a guessed split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lobby roster went out with levelBadge as a nested { level, prestige,
legend } object: per badged entry its own header byte plus two varints and
an extra object encode/decode step. lobby_info is encoded once per
recipient every second, so a 150-player all-badged lobby paid that 22,500
times a broadcast (+25-41% broadcast time, +13% bytes).

ClientInfo.levelBadge is now a single optional zb.uint():

  bits 0-6 level (1..100), bits 7-10 prestige (0..10), bit 11 legend

One varint byte at prestige 0, two at most (P10 L100 legend = 3428). Still
the last field with its one header presence bit, so badge-less entries
stay byte-identical. The field is new in this PR and not on main, so no
deployed client or server speaks the old shape.

- src/core/LevelBadgeWire.ts: LevelBadge, packLevelBadge (undefined for
  anything it cannot represent) and unpackLevelBadge (undefined for an
  out-of-range level/prestige, reserved bits, or a non-integer — never
  throws). The schema has no range so one bad value cannot fail the whole
  roster parse; the decoder drops it instead.
- Server: Client keeps the readable badge and packs it once at
  construction (wireLevelBadge); NameVisibility sends that, so broadcasts
  do no packing. levelBadgeFromProgress now also drops level > 100 and
  prestige > 10.
- Client: lobbyLevelBadge and LobbyPlayerView unpack back to
  { level, prestige, legend }; the badge components are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lobby roster's levelBadge is now one packed integer; the leaderboard
and clan lists read levels from their own API payloads, so nothing here
changes. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@openfrontio
openfrontio Bot had a problem deploying to staging October 5, 2026 15:07 Failure
iiamlewis and others added 2 commits October 5, 2026 16:30
A 150-player lobby put 150 badges (300 in team mode, with the team
cards) on screen in one frame, and the browser's style and layout of
all those SVGs made the roster's first paint long. The badge drawing
itself is cheap now; the layout is what's left.

Lobby badges now carry a `stagger` attribute. When more than 8 of them
arrive in one render (a lobby opening, a big batch of joins, a switch
to team mode), each first renders an empty square the size of the
badge, and a shared requestAnimationFrame loop (LevelBadgeFill) draws
them a few per frame, nearest the viewport first. The rows frame lays
out only the rows. After that, each frame draws a batch and measures
its script plus forced layout, and steers the next batch to about
5 ms. A few badges arriving at once (a player joining, a badge
changing) are drawn straight away, as before, also while a fill is
running.

The square is the drawn badge's exact box inside the same host
element, so nothing moves when a badge is drawn. Badges taken off the
page leave the queue, and the loop cancels its frame once nothing is
waiting. Streamer mode and anonymised rosters are unchanged: those
rows never get a badge element.

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>
@openfrontio
openfrontio Bot had a problem deploying to staging October 8, 2026 14:02 Failure
@openfrontio openfrontio deleted a comment from github-actions Bot Oct 8, 2026
iiamlewis and others added 2 commits October 9, 2026 14:29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from roster-levels to main October 9, 2026 16:21
coderabbitai[bot]
coderabbitai Bot previously approved these changes Oct 9, 2026
Main now has #5763 (roster-levels, squashed) and #5734 (profile-card).

ApiSchemas.ts: kept this branch's RowLevelFields and main's TrackFlare
schema and progression-config comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@openfrontio openfrontio deleted a comment from github-actions Bot Oct 9, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Verdict: No issues found. 0 findings (0 high, 0 medium, 0 low).

Checked for bugs and CLAUDE.md compliance. Things I checked:

  • RowLevelFields and how it's spread into the schemas: .catch(undefined) drops a bad field without failing the whole row, and ClanApiSchemas → ApiSchemas → LevelBadgeWire is not a circular import.
  • levelBadgeSlot works with the existing <level-badge> properties (level, prestige, legend, size, and the stagger attribute).
  • toPlayerEntry passes the level fields through to both the ladder rows and the pinned user row.
  • Whether to reserve a slot is decided per ladder, and from the filtered clan member list.
  • No new user-visible text, so nothing is needed in en.json.

@Celant Celant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving: CI Claude review clean at 5b34daeb, required checks green, and every finding from the earlier review rounds closed and verified in code.

@github-project-automation github-project-automation Bot moved this from Triage to Final Review in OpenFront Release Management Oct 12, 2026
@evanpelle
evanpelle merged commit 54427ab into main Oct 12, 2026
15 checks passed
@evanpelle
evanpelle deleted the leaderboard-levels branch October 12, 2026 14:28
@github-project-automation github-project-automation Bot moved this from Final Review to Complete in OpenFront Release Management Oct 12, 2026
iiamlewis added a commit that referenced this pull request Oct 12, 2026
ApiSchemas.ts: kept this PR's XpRulesSchema next to #5764's RowLevelFields;
#5879's isRewardClaimable and fail-closed held came through untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
backport — 1596c6b0 Deployed Oct 12, 2026 by evanpelle via Cherry-pick #269
staging — 1596c6b0 Deployed Oct 12, 2026 by openfrontio[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:client Best-guess area: client (rendering, UI, web components) Feature UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

Show level badges on the ranked leaderboard and clan member lists

3 participants