diff --git a/packages/core/lang/en-US.json b/packages/core/lang/en-US.json index 7a22c9011..f46678260 100644 --- a/packages/core/lang/en-US.json +++ b/packages/core/lang/en-US.json @@ -113,12 +113,12 @@ "cumulative_accuracy": "Accuracy", "cumulative_battles": "Battles", - "cumulative_capture_points": "Total encounter points", + "cumulative_capture_points": "Total capture points", "cumulative_damage_dealt": "Total damage", "cumulative_damage_ratio": "Damage ratio", "cumulative_damage_received": "Total damage received", "cumulative_deaths": "Total deaths", - "cumulative_dropped_capture_points": "Total dropped encounter points", + "cumulative_dropped_capture_points": "Total dropped capture points", "cumulative_frags": "Total kills", "cumulative_hits": "Total hits", "cumulative_inaccuracy": "Inaccuracy", diff --git a/packages/core/src/statistics/compositeStats/constants.ts b/packages/core/src/statistics/compositeStats/constants.ts index 8c7e03fda..7982a2d2f 100644 --- a/packages/core/src/statistics/compositeStats/constants.ts +++ b/packages/core/src/statistics/compositeStats/constants.ts @@ -1,3 +1,5 @@ +import { calculateWN8 } from '../calculateWN8'; + export const blitzStatsKeys = [ 'battles', 'capture_points', @@ -131,70 +133,256 @@ export type CompositeStatsKey = | `cumulative_${(typeof cumulativeCompositeStatsKeys)[number]}`; export type CompositeStats = Record; +enum CumulationType { + Sum, + WeightedSum, +} + export const compositeStatsFormatting: { [Key in CompositeStatsKey]: { unit?: string; fixed: number; coefficient?: number; - preview: number; localeFormat?: boolean; + cumulate: CumulationType; }; } = { - normalized_damage_dealt: { fixed: 0, preview: 3246, localeFormat: true }, - normalized_damage_received: { fixed: 0, preview: 966, localeFormat: true }, - normalized_deaths: { fixed: 0, unit: '%', coefficient: 100, preview: 0.09 }, - normalized_frags: { fixed: 2, preview: 2.1 }, - normalized_hits: { fixed: 2, preview: 12.33 }, - normalized_losses: { fixed: 0, unit: '%', coefficient: 100, preview: 0.3 }, - normalized_misses: { fixed: 2, preview: 1.02 }, - normalized_shots: { fixed: 2, preview: 14.54 }, - normalized_spotted: { fixed: 2, preview: 2.02 }, + normalized_damage_dealt: { + fixed: 0, + localeFormat: true, + cumulate: CumulationType.WeightedSum, + }, + normalized_damage_received: { + fixed: 0, + localeFormat: true, + cumulate: CumulationType.WeightedSum, + }, + normalized_deaths: { + fixed: 0, + unit: '%', + coefficient: 100, + cumulate: CumulationType.WeightedSum, + }, + normalized_frags: { + fixed: 2, + cumulate: CumulationType.WeightedSum, + }, + normalized_hits: { + fixed: 2, + cumulate: CumulationType.WeightedSum, + }, + normalized_losses: { + fixed: 0, + unit: '%', + coefficient: 100, + cumulate: CumulationType.WeightedSum, + }, + normalized_misses: { + fixed: 2, + cumulate: CumulationType.WeightedSum, + }, + normalized_shots: { + fixed: 2, + cumulate: CumulationType.WeightedSum, + }, + normalized_spotted: { + fixed: 2, + cumulate: CumulationType.WeightedSum, + }, normalized_survived_battles: { fixed: 0, unit: '%', coefficient: 100, - preview: 0.65, + cumulate: CumulationType.WeightedSum, }, normalized_win_and_survived: { fixed: 0, unit: '%', coefficient: 100, - preview: 0.59, + cumulate: CumulationType.WeightedSum, + }, + normalized_wins: { + fixed: 0, + unit: '%', + coefficient: 100, + cumulate: CumulationType.WeightedSum, + }, + normalized_xp: { + fixed: 0, + localeFormat: true, + cumulate: CumulationType.WeightedSum, + }, + normalized_battle_life_time: { + fixed: 1, + coefficient: 1 / 60, + cumulate: CumulationType.WeightedSum, }, - normalized_wins: { fixed: 0, unit: '%', coefficient: 100, preview: 0.72 }, - normalized_xp: { fixed: 0, preview: 982, localeFormat: true }, - normalized_battle_life_time: { fixed: 1, preview: 4.6, coefficient: 1 / 60 }, - cumulative_accuracy: { fixed: 0, unit: '%', coefficient: 100, preview: 0.91 }, - cumulative_battles: { fixed: 0, preview: 12 }, - cumulative_capture_points: { fixed: 2, preview: 0.12 }, - cumulative_damage_dealt: { fixed: 0, preview: 45023, localeFormat: true }, - cumulative_damage_ratio: { fixed: 2, preview: 2.83 }, - cumulative_damage_received: { fixed: 0, preview: 10093, localeFormat: true }, - cumulative_deaths: { fixed: 0, preview: 2 }, - cumulative_dropped_capture_points: { fixed: 2, preview: 15 }, - cumulative_frags: { fixed: 0, preview: 26 }, - cumulative_hits: { fixed: 0, preview: 480 }, + cumulative_accuracy: { + fixed: 0, + unit: '%', + coefficient: 100, + cumulate: CumulationType.WeightedSum, + }, + cumulative_battles: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_capture_points: { + fixed: 2, + cumulate: CumulationType.Sum, + }, + cumulative_damage_dealt: { + fixed: 0, + localeFormat: true, + cumulate: CumulationType.Sum, + }, + cumulative_damage_ratio: { + fixed: 2, + cumulate: CumulationType.WeightedSum, + }, + cumulative_damage_received: { + fixed: 0, + localeFormat: true, + cumulate: CumulationType.Sum, + }, + cumulative_deaths: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_dropped_capture_points: { + fixed: 2, + cumulate: CumulationType.Sum, + }, + cumulative_frags: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_hits: { + fixed: 0, + cumulate: CumulationType.Sum, + }, cumulative_inaccuracy: { fixed: 0, unit: '%', coefficient: 100, - preview: 0.08, + cumulate: CumulationType.WeightedSum, + }, + cumulative_kills_to_death_ratio: { + fixed: 2, + cumulate: CumulationType.WeightedSum, + }, + cumulative_losses: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_misses: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_shots: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_spotted: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_survived_battles: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_win_and_survived: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_wins: { + fixed: 0, + cumulate: CumulationType.Sum, + }, + cumulative_xp: { + fixed: 0, + localeFormat: true, + cumulate: CumulationType.Sum, + }, + cumulative_wn8: { + fixed: 0, + localeFormat: false, + cumulate: CumulationType.WeightedSum, }, - cumulative_kills_to_death_ratio: { fixed: 2, preview: 2.3 }, - cumulative_losses: { fixed: 0, preview: 3 }, - cumulative_misses: { fixed: 0, preview: 13 }, - cumulative_shots: { fixed: 0, preview: 309 }, - cumulative_spotted: { fixed: 0, preview: 99 }, - cumulative_survived_battles: { fixed: 0, preview: 10 }, - cumulative_win_and_survived: { fixed: 0, preview: 9 }, - cumulative_wins: { fixed: 0, preview: 8 }, - cumulative_xp: { fixed: 0, preview: 19403, localeFormat: true }, - cumulative_wn8: { fixed: 0, preview: 3103, localeFormat: true }, cumulative_battle_life_time: { fixed: 0, - preview: 109, coefficient: 1 / 60, localeFormat: true, + cumulate: CumulationType.Sum, }, }; + +export function calculateCompositeStats(s: BlitzkitStats, a: BlitzStats) { + return { + cumulative_accuracy: s.hits / s.shots, + cumulative_battle_life_time: s.battle_life_time, + cumulative_battles: s.battles, + cumulative_capture_points: s.capture_points, + cumulative_damage_dealt: s.damage_dealt, + cumulative_damage_ratio: s.damage_dealt / s.damage_received, + cumulative_damage_received: s.damage_received, + cumulative_deaths: s.battles - s.survived_battles, + cumulative_dropped_capture_points: s.dropped_capture_points, + cumulative_frags: s.frags, + cumulative_hits: s.hits, + cumulative_inaccuracy: (s.shots - s.hits) / s.shots, + cumulative_kills_to_death_ratio: s.frags / (s.battles - s.survived_battles), + cumulative_losses: s.losses, + cumulative_misses: s.shots - s.hits, + cumulative_shots: s.shots, + cumulative_spotted: s.spotted, + cumulative_survived_battles: s.survived_battles, + cumulative_win_and_survived: s.win_and_survived, + cumulative_wins: s.wins, + cumulative_wn8: calculateWN8(a, s), + cumulative_xp: s.xp, + normalized_battle_life_time: s.battle_life_time / s.battles, + normalized_damage_dealt: s.damage_dealt / s.battles, + normalized_damage_received: s.damage_received / s.battles, + normalized_deaths: (s.battles - s.survived_battles) / s.battles, + normalized_frags: s.frags / s.battles, + normalized_hits: s.hits / s.battles, + normalized_losses: s.losses / s.battles, + normalized_misses: (s.shots - s.hits) / s.battles, + normalized_shots: s.shots / s.battles, + normalized_spotted: s.spotted / s.battles, + normalized_survived_battles: s.survived_battles / s.battles, + normalized_win_and_survived: s.win_and_survived / s.battles, + normalized_wins: s.wins / s.battles, + normalized_xp: s.xp / s.battles, + } satisfies CompositeStats; +} + +export function sumCompositeStats(stats: CompositeStats[]) { + const battles = stats.reduce((a, b) => a + b.cumulative_battles, 0); + const summed: Partial = {}; + + Object.entries(compositeStatsFormatting).forEach(([key, value]) => { + switch (value.cumulate) { + case CumulationType.Sum: { + summed[key as keyof CompositeStats] = stats.reduce( + (a, b) => a + b[key as keyof CompositeStats], + 0, + ); + break; + } + + case CumulationType.WeightedSum: { + summed[key as keyof CompositeStats] = + stats.reduce( + (a, b) => a + b[key as keyof CompositeStats] * b.cumulative_battles, + 0, + ) / battles; + break; + } + } + }); + + return summed as CompositeStats; +} diff --git a/packages/core/src/statistics/compositeStats/index.ts b/packages/core/src/statistics/compositeStats/index.ts index a6b92e69d..6419531d6 100644 --- a/packages/core/src/statistics/compositeStats/index.ts +++ b/packages/core/src/statistics/compositeStats/index.ts @@ -49,19 +49,20 @@ export function compositeStats(stats: BlitzkitStats, average: BlitzkitStats) { } satisfies CompositeStats; } -export function formatCompositeStat(value: number, key: CompositeStatsKey) { +export function formatCompositeStat( + value: number, + key: CompositeStatsKey, + composite: CompositeStats, +) { + if (composite.cumulative_battles === 0) return '--'; + const formatting = compositeStatsFormatting[key]; const resolvedValue = (formatting.coefficient ?? 1) * value; return `${ formatting.localeFormat - ? resolvedValue.toLocaleString() + ? Math.round(resolvedValue).toLocaleString() : resolvedValue.toFixed(formatting.fixed) }${formatting.unit ?? ''}`; } -export function previewCompositeStat(key: CompositeStatsKey) { - const { preview } = compositeStatsFormatting[key]; - return formatCompositeStat(preview, key); -} - export * from './constants'; diff --git a/packages/website/src/components/Embeds/breakdown.tsx b/packages/website/src/components/Embeds/breakdown.tsx index 984c784fe..2beef5e25 100644 --- a/packages/website/src/components/Embeds/breakdown.tsx +++ b/packages/website/src/components/Embeds/breakdown.tsx @@ -1,9 +1,13 @@ import { + calculateCompositeStats, + compositeStats, compositeStatsKeys, deltaTankStats, + fetchAverageDefinitions, fetchTankDefinitions, getTankStats, idToRegion, + sumCompositeStats, type IndividualTankStats, } from '@blitzkit/core'; import strings from '@blitzkit/core/lang/en-US.json'; @@ -21,6 +25,31 @@ export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({ })); const tankDefinitions = await fetchTankDefinitions(); +const averageDefinitions = await fetchAverageDefinitions(); + +export const fakeCompositeStats = compositeStats( + { + battle_life_time: 5 * 50 * 60, + battles: 50, + capture_points: 5, + damage_dealt: 3201 * 50, + damage_received: 1093 * 50, + dropped_capture_points: 40, + frags: 99, + frags8p: 0, + hits: 50 * 13, + losses: 20, + max_frags: 7, + max_xp: 1203, + shots: 50 * 14, + spotted: 50 * 1.5, + survived_battles: 32, + win_and_survived: 28, + wins: 30, + xp: 1230 * 50, + }, + averageDefinitions.averages[7297].mu, +); export function BreakdownPreview() { const tanks = Object.values(tankDefinitions.tanks); @@ -28,10 +57,16 @@ export function BreakdownPreview() { return ( - {useEmbedState('showTotal') && } + {useEmbedState('showTotal') && ( + + )} {tanks.slice(0, useEmbedState('listMaxTanks')).map((tank) => ( - + ))} ); @@ -59,6 +94,23 @@ function BreakdownRendererContent() { ), [tankStatsB], ); + const compositeStats = diff + .map((diff) => { + const average = averageDefinitions.averages[diff.tank_id]; + + if (average === undefined) return null; + + const compositeStats = calculateCompositeStats( + { battle_life_time: diff.battle_life_time, ...diff.all }, + average.mu, + ); + + return { id: diff.tank_id, compositeStats }; + }) + .filter((stats) => stats !== null); + const sum = sumCompositeStats( + compositeStats.map(({ compositeStats }) => compositeStats), + ); useEffect(() => { const interval = setInterval(async () => { @@ -76,13 +128,23 @@ function BreakdownRendererContent() { - {useEmbedState('showTotal') && } + {useEmbedState('showTotal') && ( + + )} - {diff.slice(0, useEmbedState('listMaxTanks')).map((diff) => { - const tank = tankDefinitions.tanks[diff.tank_id]; + {compositeStats + .slice(0, useEmbedState('listMaxTanks')) + .map(({ id, compositeStats }) => { + const tank = tankDefinitions.tanks[id]; - return ; - })} + return ( + + ); + })} diff --git a/packages/website/src/components/TanksEmbed.tsx b/packages/website/src/components/TanksEmbed.tsx index 5c6103eaa..2da57e7f0 100644 --- a/packages/website/src/components/TanksEmbed.tsx +++ b/packages/website/src/components/TanksEmbed.tsx @@ -1,8 +1,9 @@ import { - previewCompositeStat, + formatCompositeStat, TankClass, TankDefinition, TankType, + type CompositeStats, type CompositeStatsKey, } from '@blitzkit/core'; import strings from '@blitzkit/core/lang/en-US.json'; @@ -42,9 +43,13 @@ export function BreakdownEmbedWrapper({ interface BreakdownEmbedCardProps { tank: TankDefinition | null | undefined; + composite: CompositeStats; } -export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) { +export function BreakdownEmbedCard({ + tank, + composite, +}: BreakdownEmbedCardProps) { const { useEmbedState, useRichText } = useEmbedStateCurry(); @@ -114,9 +119,7 @@ export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) { {times(4, (index) => { const column = (index + 1) as 1 | 2 | 3 | 4; - const columnState = useEmbedState( - `column${column}`, - ) as CompositeStatsKey; + const statKey = useEmbedState(`column${column}`) as CompositeStatsKey; const customLabel = useEmbedState(`column${column}CustomLabel`); return ( @@ -128,11 +131,11 @@ export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) { flexGrow="1" > - {previewCompositeStat(columnState)} + {formatCompositeStat(composite[statKey], statKey, composite)} {customLabel.length === 0 - ? strings.common.composite_stats[columnState] + ? strings.common.composite_stats[statKey] : customLabel}