Repository navigation
Show level badges in the lobby list and in-game player panel #5763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
53b26e0
Show player levels and the XP each game earns
iiamlewis 6ff9abf
Strip Lit's comment markers in the badge tests as nodes, not by regex
iiamlewis c91fed6
Merge branch 'main' into player-levels-ui
iiamlewis 0ef94ba
Harden player levels UI from PR review
iiamlewis 23be312
Merge branch 'main' into player-levels-ui
iiamlewis b899cb0
Address review on player levels: unverified games, off-screen reveals…
iiamlewis 216851d
Rename the first-win XP line to first game of the day
iiamlewis a9c27cb
Recognise the new subscriber XP boosts: +25% / +50% / +100%
iiamlewis 399acf7
feat: stamp player levels onto the lobby roster
iiamlewis 0e2d49c
feat: show level badges in the lobby list and player panel
iiamlewis 5f83874
Stack the XP reveal's counter and level progress on a narrow bar
iiamlewis 6a758f1
Show the XP a past game earned under its summary card
iiamlewis 2edf46b
perf: carry the roster's level badge as one packed integer
iiamlewis f6bf428
perf: draw a full lobby's level badges over a few frames
iiamlewis c93e4b0
Merge main: src/core split into engine, engine-api and shared packages
iiamlewis 0a960f1
Merge player-levels-ui: src/core split into packages
iiamlewis c1f80da
Merge remote-tracking branch 'origin/main' into player-levels-ui
iiamlewis d1ae378
Point the XP popup test at EventBus's new home in @openfront/shared
iiamlewis 88a6c74
Merge player-levels-ui: bring the branch up to date with main
iiamlewis 1271d1f
Merge main: lazy-loaded homepage modals
iiamlewis 1f01c85
Honour "hide my level" on the roster badge; own badge after a refresh
iiamlewis 812cc15
Address review on the player levels UI
iiamlewis c1b833f
Merge player-levels-ui (review fixes)
iiamlewis 0111c2c
Merge main (#5726 and #5778 landed)
iiamlewis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // A player's level as a name badge: the display subset of the API's | ||
| // /users/@me `progress`. Display-only — the simulation never reads it, so it | ||
| // cannot desync. | ||
| export interface LevelBadge { | ||
| level: number; | ||
| prestige: number; | ||
| legend: boolean; | ||
| } | ||
|
|
||
| // The lobby roster (ClientInfo.levelBadge) carries a badge as ONE small | ||
| // integer rather than a nested { level, prestige, legend } object. The roster | ||
| // goes to every client once a second, encoded once per recipient, so a | ||
| // 150-player lobby encodes 22,500 entries a second: a nested object cost each | ||
| // entry its own header byte plus two varints and a per-object encode/decode | ||
| // step; a packed varint is one or two bytes and a plain number. | ||
| // | ||
| // bits 0-6 level 1..100 | ||
| // bits 7-10 prestige 0..10 | ||
| // bit 11 legend | ||
| // | ||
| // Prestige 0 (most players) packs below 128, so one varint byte; anything | ||
| // else, up to legend at P10 L100 (3428), is two. | ||
| export const LEVEL_BADGE_MAX_LEVEL = 100; | ||
| export const LEVEL_BADGE_MAX_PRESTIGE = 10; | ||
| const LEVEL_BITS = 7; | ||
| const LEVEL_MASK = (1 << LEVEL_BITS) - 1; | ||
| const PRESTIGE_SHIFT = LEVEL_BITS; | ||
| const PRESTIGE_MASK = 0b1111; | ||
| const LEGEND_BIT = 1 << 11; | ||
| const ALL_BITS = (1 << 12) - 1; | ||
|
|
||
| function inRange(level: number, prestige: number): boolean { | ||
| return ( | ||
| Number.isInteger(level) && | ||
| level >= 1 && | ||
| level <= LEVEL_BADGE_MAX_LEVEL && | ||
| Number.isInteger(prestige) && | ||
| prestige >= 0 && | ||
| prestige <= LEVEL_BADGE_MAX_PRESTIGE | ||
| ); | ||
| } | ||
|
|
||
| // The wire form of a badge, or undefined when it holds anything the packing | ||
| // cannot represent (so no badge is sent rather than a wrong one). | ||
| export function packLevelBadge( | ||
| badge: LevelBadge | undefined, | ||
| ): number | undefined { | ||
| if (badge === undefined) return undefined; | ||
| const { level, prestige, legend } = badge; | ||
| if (!inRange(level, prestige) || typeof legend !== "boolean") { | ||
| return undefined; | ||
| } | ||
| return level | (prestige << PRESTIGE_SHIFT) | (legend ? LEGEND_BIT : 0); | ||
| } | ||
|
|
||
| // Back to { level, prestige, legend }. Undefined for no badge and for any | ||
| // value no server would send — out-of-range level or prestige, reserved bits | ||
| // set, not a whole number — so a bad roster entry shows no badge instead of | ||
| // a wrong one or an exception. | ||
| export function unpackLevelBadge( | ||
| packed: number | undefined, | ||
| ): LevelBadge | undefined { | ||
| if (packed === undefined) return undefined; | ||
| if (!Number.isInteger(packed) || packed < 0 || packed > ALL_BITS) { | ||
| return undefined; | ||
| } | ||
| const level = packed & LEVEL_MASK; | ||
| const prestige = (packed >> PRESTIGE_SHIFT) & PRESTIGE_MASK; | ||
| if (!inRange(level, prestige)) return undefined; | ||
| return { level, prestige, legend: (packed & LEGEND_BIT) !== 0 }; | ||
| } |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { ClientID, GameID } from "@openfront/engine-api/Schemas"; | ||
| import { LevelBadge, unpackLevelBadge } from "@openfront/shared/LevelBadgeWire"; | ||
| import { ClientInfo } from "@openfront/shared/WireSchemas"; | ||
|
|
||
| // Level badges from the last lobby roster this tab was sent, for in-game UI. | ||
| // The start message carries no levels, so a tab that never saw the lobby (a | ||
| // refresh or late join) has none. Kept in wire form, as a roster arrives every | ||
| // second and a badge is read only when a panel opens. | ||
|
|
||
| let rosterGameID: GameID | null = null; | ||
| let badges = new Map<ClientID, number>(); | ||
|
|
||
| export function rememberLobbyRoster( | ||
| gameID: GameID, | ||
| clients: readonly ClientInfo[] | undefined, | ||
| ): void { | ||
| const next = new Map<ClientID, number>(); | ||
| for (const c of clients ?? []) { | ||
| if (c.levelBadge !== undefined) next.set(c.clientID, c.levelBadge); | ||
| } | ||
| rosterGameID = gameID; | ||
| badges = next; | ||
| } | ||
|
|
||
| // The player's badge in game `gameID`, or undefined when they have none (a | ||
| // guest, an anonymized name, progression off, their level hidden, a value out | ||
| // of range) or this tab never saw them in that game's lobby. | ||
| export function lobbyLevelBadge( | ||
| gameID: GameID, | ||
| clientID: ClientID | null, | ||
| ): LevelBadge | undefined { | ||
| if (clientID === null || gameID !== rosterGameID) return undefined; | ||
| return unpackLevelBadge(badges.get(clientID)); | ||
| } | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.