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
This commit is contained in:
Ryan
2026-03-27 02:04:33 +00:00
committed by GitHub
parent c213a1deda
commit 14a5128e87
3 changed files with 96 additions and 18 deletions
+2
View File
@@ -1017,6 +1017,8 @@
"public": "Public",
"private": "Private",
"solo": "Solo",
"ranked": "Ranked",
"ranked_1v1": "1v1",
"mode": "Mode",
"stats_wins": "Wins",
"stats_losses": "Losses",
@@ -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")}
</button>
`,
)}
</div>
<div class="flex gap-2">
<!-- Ranked type selector -->
${this.selectedType === "Ranked" && rankedTypes.length
? html`<div
class="flex gap-1 bg-black/20 rounded-md p-1 border border-white/5"
>
${rankedTypes.map(
(r) => html`
<button
class="text-xs px-3 py-1 rounded-sm transition-colors ${this
.selectedRankedType === r
? "bg-white/20 text-white font-bold"
: "text-gray-400 hover:text-white"}"
@click=${() => this.setRankedType(r)}
>
${this.labelForRankedType(r)}
</button>
`,
)}
</div>`
: html``}
<!-- Mode selector -->
${modes.length
? html`<div
+10 -6
View File
@@ -98,13 +98,17 @@ export const PlayerStatsLeafSchema = z.object({
});
export type PlayerStatsLeaf = z.infer<typeof PlayerStatsLeafSchema>;
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<typeof PlayerStatsTreeSchema>;
export const PlayerGameSchema = z.object({