From 2ec3f6d98d4048a96bbe17cf2526ae7724de3ce9 Mon Sep 17 00:00:00 2001 From: Amund Eggen Svandal Date: Mon, 6 Jul 2026 21:25:00 +0200 Subject: [PATCH 1/2] feat(stats): add 4v4 gamemode to core stats plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "4v4" to ALL_GAMEMODE_KEYS, gamemode labels, chart identity colors, the PlayerDataPIT types/mapper, and the MSW mock data (overall mock stays the sum of the five modes). Everything else — zod search params, gamemode selectors, chart series, stat helpers — is data-driven off ALL_GAMEMODE_KEYS and picks the new mode up automatically. PlayerDataPIT changed shape, so the persisted query cache buster is bumped to "2". The session-detail-page branch also uses "2"; it will be rebased on top of this later. Must not be deployed before flashlight serves the "4v4" block in its PIT responses — parsing crashes on payloads without it. Co-Authored-By: Claude Fable 5 --- src/App.tsx | 2 +- src/helpers/session.unit.test.ts | 1 + src/mocks/data.ts | 3 ++- src/queries/playerdata.ts | 3 +++ src/stats/keys.ts | 1 + src/stats/labels.ts | 3 +++ src/stats/progression.unit.test.ts | 2 ++ src/theme/tokens.ts | 3 ++- 8 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a3a7c5cf..5a72b763 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,7 +26,7 @@ export function App({ router, queryClient, persister }: AppProps) { persistOptions={{ persister, maxAge, - buster: "1", // API version + buster: "2", // API version }} > diff --git a/src/helpers/session.unit.test.ts b/src/helpers/session.unit.test.ts index d4651c97..c068acec 100644 --- a/src/helpers/session.unit.test.ts +++ b/src/helpers/session.unit.test.ts @@ -29,6 +29,7 @@ const makePlayerDataPIT = (gamesPlayed: number, experience: number): PlayerDataP doubles: makeStatsPIT(0), threes: makeStatsPIT(0), fours: makeStatsPIT(gamesPlayed), + "4v4": makeStatsPIT(0), }); describe(addExtrapolatedSessions, () => { diff --git a/src/mocks/data.ts b/src/mocks/data.ts index 25f27ecb..815e395c 100644 --- a/src/mocks/data.ts +++ b/src/mocks/data.ts @@ -94,7 +94,8 @@ export const makePlayerDataPIT = ( doubles: makeStatsPIT(multiplier), threes: makeStatsPIT(multiplier), fours: makeStatsPIT(multiplier), - overall: makeStatsPIT(multiplier * 4), + "4v4": makeStatsPIT(multiplier), + overall: makeStatsPIT(multiplier * 5), }); export const makeSession = ( diff --git a/src/queries/playerdata.ts b/src/queries/playerdata.ts index aa7577ec..fc7b3741 100644 --- a/src/queries/playerdata.ts +++ b/src/queries/playerdata.ts @@ -19,6 +19,7 @@ export interface APIPlayerDataPIT { readonly doubles: APIStatsPIT; readonly threes: APIStatsPIT; readonly fours: APIStatsPIT; + readonly "4v4": APIStatsPIT; readonly overall: APIStatsPIT; } @@ -43,6 +44,7 @@ export interface PlayerDataPIT { readonly doubles: StatsPIT; readonly threes: StatsPIT; readonly fours: StatsPIT; + readonly "4v4": StatsPIT; readonly overall: StatsPIT; } @@ -54,5 +56,6 @@ export const apiToPlayerDataPIT = (apiPlayerData: APIPlayerDataPIT): PlayerDataP doubles: apiPlayerData.doubles, threes: apiPlayerData.threes, fours: apiPlayerData.fours, + "4v4": apiPlayerData["4v4"], overall: apiPlayerData.overall, }); diff --git a/src/stats/keys.ts b/src/stats/keys.ts index b3eceb08..81ca5237 100644 --- a/src/stats/keys.ts +++ b/src/stats/keys.ts @@ -31,6 +31,7 @@ export const ALL_GAMEMODE_KEYS = [ "doubles", "threes", "fours", + "4v4", "overall", ] as const; export type GamemodeKey = (typeof ALL_GAMEMODE_KEYS)[number]; diff --git a/src/stats/labels.ts b/src/stats/labels.ts index 90d2938c..45dff2bc 100644 --- a/src/stats/labels.ts +++ b/src/stats/labels.ts @@ -117,6 +117,9 @@ export const getGamemodeLabel = (gamemode: GamemodeKey, capitalize = false): str case "fours": { return capitalize ? "Fours" : "fours"; } + case "4v4": { + return "4v4"; + } } }; diff --git a/src/stats/progression.unit.test.ts b/src/stats/progression.unit.test.ts index 8344147e..ad1b9c7b 100644 --- a/src/stats/progression.unit.test.ts +++ b/src/stats/progression.unit.test.ts @@ -137,6 +137,7 @@ class PlayerDataBuilder { doubles: { ...emptyStats }, threes: { ...emptyStats }, fours: { ...emptyStats }, + "4v4": { ...emptyStats }, overall: { ...emptyStats }, }; } @@ -159,6 +160,7 @@ class PlayerDataBuilder { doubles: { ...this.player.doubles }, threes: { ...this.player.threes }, fours: { ...this.player.fours }, + "4v4": { ...this.player["4v4"] }, overall: { ...this.player.overall }, }; } diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts index b430b3dc..1a172a1e 100644 --- a/src/theme/tokens.ts +++ b/src/theme/tokens.ts @@ -86,7 +86,7 @@ export const LIGHT_TOKENS: SchemeTokens = { }; /** - * Per-gamemode accent colours for the four real modes. Used for chart strokes + * Per-gamemode accent colours for the real modes. Used for chart strokes * and legend dots so a gamemode keeps a stable identity colour across the app. * Shared across light and dark. "overall" is intentionally absent — it resolves * to the scheme's `primary` colour (added in the theme), so the aggregate @@ -97,6 +97,7 @@ export const GAMEMODE_COLORS: Record, string> = doubles: "#06b6d4", threes: "#22c55e", fours: "#f59e0b", + "4v4": "#ef4444", }; /** From 390b686c475e54123a1722426d6a54eccfdefd49 Mon Sep 17 00:00:00 2001 From: Amund Eggen Svandal Date: Mon, 6 Jul 2026 21:26:56 +0200 Subject: [PATCH 2/2] feat(wrapped): show 4v4 streaks on the wrapped page Add "4v4" to both streak maps (winstreaks / finalKillStreaks) in the wrapped query types, to the mode list rendered by the Streaks section, and to the MSW wrapped mock and UI-test fixture. Mirrors the existing modes' handling: the StreakInfo fields stay non-optional in TS even though flashlight omits absent streaks on the wire, matching how solo/doubles/threes/fours are typed today. Requires flashlight to serve 4v4 wrapped streaks (plan step F4/F5) before this reaches production. Co-Authored-By: Claude Fable 5 --- src/mocks/data.ts | 2 ++ src/queries/wrapped.ts | 4 ++++ src/routes/wrapped/$uuid.tsx | 2 +- src/routes/wrapped/-uuid.ui.test.tsx | 24 ++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/mocks/data.ts b/src/mocks/data.ts index 815e395c..3657ca76 100644 --- a/src/mocks/data.ts +++ b/src/mocks/data.ts @@ -164,6 +164,7 @@ export const makeWrappedResponse = (uuid: string, year: number) => { doubles: { highest: 8, when: startTime }, threes: { highest: 6, when: startTime }, fours: { highest: 10, when: startTime }, + "4v4": { highest: 2, when: startTime }, }, finalKillStreaks: { overall: { highest: 20, when: startTime }, @@ -171,6 +172,7 @@ export const makeWrappedResponse = (uuid: string, year: number) => { doubles: { highest: 12, when: startTime }, threes: { highest: 10, when: startTime }, fours: { highest: 15, when: startTime }, + "4v4": { highest: 3, when: startTime }, }, sessionCoverage: { gamesPlayedPercentage: 0.85, diff --git a/src/queries/wrapped.ts b/src/queries/wrapped.ts index 63e6639c..752f1c67 100644 --- a/src/queries/wrapped.ts +++ b/src/queries/wrapped.ts @@ -50,6 +50,7 @@ interface APISessionStats { readonly doubles: StreakInfo; readonly threes: StreakInfo; readonly fours: StreakInfo; + readonly "4v4": StreakInfo; }; readonly finalKillStreaks: { readonly overall: StreakInfo; @@ -57,6 +58,7 @@ interface APISessionStats { readonly doubles: StreakInfo; readonly threes: StreakInfo; readonly fours: StreakInfo; + readonly "4v4": StreakInfo; }; readonly sessionCoverage: { readonly gamesPlayedPercentage: number; @@ -99,6 +101,7 @@ interface SessionStats { readonly doubles: StreakInfo; readonly threes: StreakInfo; readonly fours: StreakInfo; + readonly "4v4": StreakInfo; }; readonly finalKillStreaks: { readonly overall: StreakInfo; @@ -106,6 +109,7 @@ interface SessionStats { readonly doubles: StreakInfo; readonly threes: StreakInfo; readonly fours: StreakInfo; + readonly "4v4": StreakInfo; }; readonly sessionCoverage: { readonly gamesPlayedPercentage: number; diff --git a/src/routes/wrapped/$uuid.tsx b/src/routes/wrapped/$uuid.tsx index c5fc221f..b1d5b607 100644 --- a/src/routes/wrapped/$uuid.tsx +++ b/src/routes/wrapped/$uuid.tsx @@ -929,7 +929,7 @@ const Streaks: React.FC = ({ wrappedData }) => { const { sessionStats } = wrappedData; if (!sessionStats) return null; - const modes = ["overall", "solo", "doubles", "threes", "fours"] as const; + const modes = ["overall", "solo", "doubles", "threes", "fours", "4v4"] as const; return ( <> diff --git a/src/routes/wrapped/-uuid.ui.test.tsx b/src/routes/wrapped/-uuid.ui.test.tsx index 706a8ba7..aef7dc3a 100644 --- a/src/routes/wrapped/-uuid.ui.test.tsx +++ b/src/routes/wrapped/-uuid.ui.test.tsx @@ -417,6 +417,18 @@ describe("Wrapped detail page", () => { kills: 200, deaths: 100, }, + "4v4": { + winstreak: 3, + gamesPlayed: 100, + wins: 60, + losses: 40, + bedsBroken: 80, + bedsLost: 30, + finalKills: 150, + finalDeaths: 50, + kills: 200, + deaths: 100, + }, overall: { winstreak: 12, gamesPlayed: 400, @@ -482,6 +494,18 @@ describe("Wrapped detail page", () => { kills: 600, deaths: 300, }, + "4v4": { + winstreak: 9, + gamesPlayed: 300, + wins: 180, + losses: 120, + bedsBroken: 240, + bedsLost: 90, + finalKills: 450, + finalDeaths: 150, + kills: 600, + deaths: 300, + }, overall: { winstreak: 36, gamesPlayed: 1200,