From 14a5128e871fce663784b6e6ce74b70f4327fc3c Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:04:33 +0000 Subject: [PATCH] playerstats to go with infra (#3520) ## Description: https://github.com/openfrontio/infra/pull/279 to go with this, splits out 1v1 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --- resources/lang/en.json | 2 + .../baseComponents/stats/PlayerStatsTree.ts | 96 ++++++++++++++++--- src/core/ApiSchemas.ts | 16 ++-- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index dd9fc61b6..119dde218 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1017,6 +1017,8 @@ "public": "Public", "private": "Private", "solo": "Solo", + "ranked": "Ranked", + "ranked_1v1": "1v1", "mode": "Mode", "stats_wins": "Wins", "stats_losses": "Losses", diff --git a/src/client/components/baseComponents/stats/PlayerStatsTree.ts b/src/client/components/baseComponents/stats/PlayerStatsTree.ts index 37166997e..809e482c0 100644 --- a/src/client/components/baseComponents/stats/PlayerStatsTree.ts +++ b/src/client/components/baseComponents/stats/PlayerStatsTree.ts @@ -5,6 +5,7 @@ import { Difficulty, GameMode, GameType, + RankedType, isDifficulty, isGameMode, isGameType, @@ -17,11 +18,12 @@ import "./PlayerStatsTable"; @customElement("player-stats-tree-view") export class PlayerStatsTreeView extends LitElement { @property({ type: Object }) statsTree?: PlayerStatsTree; - @state() selectedType: GameType = GameType.Public; + @state() selectedType: GameType | "Ranked" = GameType.Public; @state() selectedMode: GameMode = GameMode.FFA; @state() selectedDifficulty: Difficulty = Difficulty.Medium; - + @state() selectedRankedType: RankedType = RankedType.OneVOne; private get typeNode() { + if (this.selectedType === "Ranked") return undefined; return this.statsTree?.[this.selectedType]; } @@ -33,9 +35,20 @@ export class PlayerStatsTreeView extends LitElement { return this.selectedType === GameType.Public; } - private get availableTypes(): GameType[] { + private get availableTypes(): (GameType | "Ranked")[] { if (!this.statsTree) return []; - return Object.keys(this.statsTree).filter(isGameType); + const types: (GameType | "Ranked")[] = Object.keys(this.statsTree).filter( + (k): k is GameType => + isGameType(k) && + Object.keys(this.statsTree![k as GameType] ?? {}).length > 0, + ); + if ( + this.statsTree.Ranked && + Object.keys(this.statsTree.Ranked).length > 0 + ) { + types.push("Ranked"); + } + return types; } private get availableModes(): GameMode[] { @@ -43,6 +56,13 @@ export class PlayerStatsTreeView extends LitElement { return Object.keys(this.typeNode).filter(isGameMode); } + private get availableRankedTypes(): RankedType[] { + if (!this.statsTree?.Ranked) return []; + return Object.keys(this.statsTree.Ranked).filter((k): k is RankedType => + Object.values(RankedType).includes(k as RankedType), + ); + } + private get availableDifficulties(): Difficulty[] { if (!this.modeNode) return []; return Object.keys(this.modeNode).filter(isDifficulty); @@ -54,11 +74,22 @@ export class PlayerStatsTreeView extends LitElement { : translateText("game_mode.teams"); } + private labelForRankedType(r: RankedType) { + switch (r) { + case RankedType.OneVOne: + return translateText("player_stats_tree.ranked_1v1"); + } + } + createRenderRoot() { return this; } private getSelectedLeaf(): PlayerStatsLeaf | null { + if (this.selectedType === "Ranked") { + return this.statsTree?.Ranked?.[this.selectedRankedType] ?? null; + } + const modeNode = this.modeNode; if (!modeNode) return null; @@ -91,9 +122,19 @@ export class PlayerStatsTreeView extends LitElement { private syncSelection(): void { const types = this.availableTypes; - if (types.length && !types.includes(this.selectedType)) { + if (types.length && !types.includes(this.selectedType as GameType)) { this.selectedType = types[0]; } + if (this.selectedType === "Ranked") { + const rankedTypes = this.availableRankedTypes; + if ( + rankedTypes.length && + !rankedTypes.includes(this.selectedRankedType) + ) { + this.selectedRankedType = rankedTypes[0]; + } + return; + } const modes = this.availableModes; if (modes.length && !modes.includes(this.selectedMode)) { this.selectedMode = modes[0]; @@ -113,13 +154,14 @@ export class PlayerStatsTreeView extends LitElement { changedProperties.has("statsTree") || changedProperties.has("selectedType") || changedProperties.has("selectedMode") || - changedProperties.has("selectedDifficulty") + changedProperties.has("selectedDifficulty") || + changedProperties.has("selectedRankedType") ) { this.syncSelection(); } } - private setGameType(t: GameType) { + private setGameType(t: GameType | "Ranked") { if (this.selectedType === t) return; this.selectedType = t; this.requestUpdate(); @@ -131,6 +173,12 @@ export class PlayerStatsTreeView extends LitElement { this.requestUpdate(); } + private setRankedType(r: RankedType) { + if (this.selectedRankedType === r) return; + this.selectedRankedType = r; + this.requestUpdate(); + } + private setDifficulty(d: Difficulty) { if (this.selectedDifficulty === d) return; this.selectedDifficulty = d; @@ -215,6 +263,7 @@ export class PlayerStatsTreeView extends LitElement { const types = this.availableTypes; const modes = this.availableModes; const diffs = this.availableDifficulties; + const rankedTypes = this.availableRankedTypes; const leaf = this.getSelectedLeaf(); const wlr = leaf ? leaf.losses === 0n @@ -239,17 +288,40 @@ export class PlayerStatsTreeView extends LitElement { : "bg-white/5 border-white/10 text-gray-400 hover:bg-white/10 hover:text-white"}" @click=${() => this.setGameType(t)} > - ${t === GameType.Public - ? translateText("player_stats_tree.public") - : t === GameType.Private - ? translateText("player_stats_tree.private") - : translateText("player_stats_tree.solo")} + ${t === "Ranked" + ? translateText("player_stats_tree.ranked") + : t === GameType.Public + ? translateText("player_stats_tree.public") + : t === GameType.Private + ? translateText("player_stats_tree.private") + : translateText("player_stats_tree.solo")} `, )}
+ + ${this.selectedType === "Ranked" && rankedTypes.length + ? html`
+ ${rankedTypes.map( + (r) => html` + + `, + )} +
` + : html``} + ${modes.length ? html`
; -export const PlayerStatsTreeSchema = z.partialRecord( - z.enum(GameType), - z.partialRecord( - z.enum(GameMode), - z.partialRecord(z.enum(Difficulty), PlayerStatsLeafSchema), - ), +const GameModeStatsSchema = z.partialRecord( + z.enum(GameMode), + z.partialRecord(z.enum(Difficulty), PlayerStatsLeafSchema), ); + +export const PlayerStatsTreeSchema = z.object({ + Singleplayer: GameModeStatsSchema.optional(), + Public: GameModeStatsSchema.optional(), + Private: GameModeStatsSchema.optional(), + Ranked: z.partialRecord(z.enum(RankedType), PlayerStatsLeafSchema).optional(), +}); export type PlayerStatsTree = z.infer; export const PlayerGameSchema = z.object({