From cfa40f2e5eda26e2009531ceea130d72ed5c6f62 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:57:46 +0000 Subject: [PATCH] mergestats (#2904) If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #2704 ## Description: Merges together easy + medium difficulties. Before: After: (dont have one to show oop) (btw that win ratio in the first screenshot is not mine.. :skull:) ## 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 --- .../baseComponents/stats/PlayerStatsTree.ts | 210 +++++++++++++----- 1 file changed, 158 insertions(+), 52 deletions(-) diff --git a/src/client/components/baseComponents/stats/PlayerStatsTree.ts b/src/client/components/baseComponents/stats/PlayerStatsTree.ts index 4ec48f70a..e703ee2a2 100644 --- a/src/client/components/baseComponents/stats/PlayerStatsTree.ts +++ b/src/client/components/baseComponents/stats/PlayerStatsTree.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from "lit"; +import { LitElement, PropertyValues, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { PlayerStatsLeaf, PlayerStatsTree } from "../../../../core/ApiSchemas"; import { @@ -21,22 +21,31 @@ export class PlayerStatsTreeView extends LitElement { @state() selectedMode: GameMode = GameMode.FFA; @state() selectedDifficulty: Difficulty = Difficulty.Medium; + private get typeNode() { + return this.statsTree?.[this.selectedType]; + } + + private get modeNode() { + return this.typeNode?.[this.selectedMode]; + } + + private get shouldMergeDifficulties() { + return this.selectedType === GameType.Public; + } + private get availableTypes(): GameType[] { if (!this.statsTree) return []; return Object.keys(this.statsTree).filter(isGameType); } private get availableModes(): GameMode[] { - const typeNode = this.statsTree?.[this.selectedType]; - if (!typeNode) return []; - return Object.keys(typeNode).filter(isGameMode); + if (!this.typeNode) return []; + return Object.keys(this.typeNode).filter(isGameMode); } private get availableDifficulties(): Difficulty[] { - const typeNode = this.statsTree?.[this.selectedType]; - const modeNode = typeNode?.[this.selectedMode]; - if (!modeNode) return []; - return Object.keys(modeNode).filter(isDifficulty); + if (!this.modeNode) return []; + return Object.keys(this.modeNode).filter(isDifficulty); } private labelForMode(m: GameMode) { @@ -50,52 +59,37 @@ export class PlayerStatsTreeView extends LitElement { } private getSelectedLeaf(): PlayerStatsLeaf | null { - const typeNode = this.statsTree?.[this.selectedType]; - if (!typeNode) return null; - const modeNode = typeNode[this.selectedMode]; + const modeNode = this.modeNode; if (!modeNode) return null; - const diffNode = modeNode[this.selectedDifficulty]; - if (!diffNode) return null; - return diffNode; - } - private getDisplayedStats(): PlayerStats | null { - const leaf = this.getSelectedLeaf(); - if (!leaf || !leaf.stats) return null; - return leaf.stats; - } - - private setGameType(t: GameType) { - if (this.selectedType === t) return; - this.selectedType = t; - const modes = this.availableModes; - if (!modes.includes(this.selectedMode)) { - this.selectedMode = modes[0] ?? this.selectedMode; + if (!this.shouldMergeDifficulties) { + return modeNode[this.selectedDifficulty] ?? null; } - const diffs = this.availableDifficulties; - if (!diffs.includes(this.selectedDifficulty)) { - this.selectedDifficulty = diffs[0] ?? this.selectedDifficulty; - } - this.requestUpdate(); + + const diffKeys = Object.keys(modeNode).filter(isDifficulty); + if (!diffKeys.length) return null; + + return diffKeys.reduce((merged, diffKey) => { + const leaf = modeNode[diffKey]; + if (!leaf) return merged; + if (!merged) { + return { + wins: leaf.wins, + losses: leaf.losses, + total: leaf.total, + stats: this.cloneStats(leaf.stats), + }; + } + return { + wins: merged.wins + leaf.wins, + losses: merged.losses + leaf.losses, + total: merged.total + leaf.total, + stats: this.mergeStats(merged.stats, leaf.stats), + }; + }, null); } - private setMode(m: GameMode) { - if (this.selectedMode === m) return; - this.selectedMode = m; - const diffs = this.availableDifficulties; - if (!diffs.includes(this.selectedDifficulty)) { - this.selectedDifficulty = diffs[0] ?? this.selectedDifficulty; - } - this.requestUpdate(); - } - - private setDifficulty(d: Difficulty) { - if (this.selectedDifficulty === d) return; - this.selectedDifficulty = d; - this.requestUpdate(); - } - - render() { + private syncSelection(): void { const types = this.availableTypes; if (types.length && !types.includes(this.selectedType)) { this.selectedType = types[0]; @@ -105,10 +99,122 @@ export class PlayerStatsTreeView extends LitElement { this.selectedMode = modes[0]; } const diffs = this.availableDifficulties; - if (diffs.length && !diffs.includes(this.selectedDifficulty)) { + if ( + !this.shouldMergeDifficulties && + diffs.length && + !diffs.includes(this.selectedDifficulty) + ) { this.selectedDifficulty = diffs[0]; } + } + protected willUpdate(changedProperties: PropertyValues) { + if ( + changedProperties.has("statsTree") || + changedProperties.has("selectedType") || + changedProperties.has("selectedMode") || + changedProperties.has("selectedDifficulty") + ) { + this.syncSelection(); + } + } + + private setGameType(t: GameType) { + if (this.selectedType === t) return; + this.selectedType = t; + this.requestUpdate(); + } + + private setMode(m: GameMode) { + if (this.selectedMode === m) return; + this.selectedMode = m; + this.requestUpdate(); + } + + private setDifficulty(d: Difficulty) { + if (this.selectedDifficulty === d) return; + this.selectedDifficulty = d; + this.requestUpdate(); + } + + private mergeStats( + base: PlayerStats | undefined, + next: PlayerStats | undefined, + ): PlayerStats | undefined { + if (!base && !next) return undefined; + if (!base) return this.cloneStats(next); + if (!next) return this.cloneStats(base); + + return { + attacks: this.mergeStatArrays(base.attacks, next.attacks), + betrayals: this.mergeStatValue(base.betrayals, next.betrayals), + killedAt: this.mergeStatValue(base.killedAt, next.killedAt), + conquests: this.mergeStatValue(base.conquests, next.conquests), + boats: this.mergeStatRecord(base.boats, next.boats), + bombs: this.mergeStatRecord(base.bombs, next.bombs), + gold: this.mergeStatArrays(base.gold, next.gold), + units: this.mergeStatRecord(base.units, next.units), + }; + } + + private mergeStatValue( + base: bigint | undefined, + next: bigint | undefined, + ): bigint | undefined { + if (base === undefined && next === undefined) return undefined; + return (base ?? 0n) + (next ?? 0n); + } + + private mergeStatArrays( + base: bigint[] | undefined, + next: bigint[] | undefined, + ): bigint[] | undefined { + if (!base && !next) return undefined; + const maxLen = Math.max(base?.length ?? 0, next?.length ?? 0); + const merged: bigint[] = []; + for (let i = 0; i < maxLen; i += 1) { + merged[i] = (base?.[i] ?? 0n) + (next?.[i] ?? 0n); + } + return merged; + } + + private mergeStatRecord( + base: Partial> | undefined, + next: Partial> | undefined, + ): Partial> | undefined { + if (!base && !next) return undefined; + const merged: Partial> = {}; + const keys = new Set([ + ...Object.keys(base ?? {}), + ...Object.keys(next ?? {}), + ]) as Set; + keys.forEach((key) => { + const mergedArray = this.mergeStatArrays(base?.[key], next?.[key]); + if (mergedArray) { + merged[key] = mergedArray; + } + }); + return Object.keys(merged).length ? merged : undefined; + } + + private cloneStats(stats: PlayerStats | undefined): PlayerStats | undefined { + if (!stats) return undefined; + return { + attacks: stats.attacks ? [...stats.attacks] : undefined, + betrayals: stats.betrayals, + killedAt: stats.killedAt, + conquests: stats.conquests, + boats: stats.boats ? { ...stats.boats } : undefined, + bombs: stats.bombs ? { ...stats.bombs } : undefined, + gold: stats.gold ? [...stats.gold] : undefined, + units: stats.units ? { ...stats.units } : undefined, + }; + } + + render() { + const types = this.availableTypes; + const modes = this.availableModes; + const diffs = this.availableDifficulties; const leaf = this.getSelectedLeaf(); const wlr = leaf ? leaf.losses === 0n @@ -167,7 +273,7 @@ export class PlayerStatsTreeView extends LitElement { : html``} - ${diffs.length + ${!this.shouldMergeDifficulties && diffs.length ? html` @@ -209,7 +315,7 @@ export class PlayerStatsTreeView extends LitElement {