feat: composite stats

This commit is contained in:
tresabhi committed 2024-11-08 10:32:44 -06:00
1 parent f1767bf5d7
commit d910ed9f5e
5 files changed
+314 -60

No files matched your search

+2 -2
View File
@@ -113,12 +113,12 @@
"cumulative_accuracy": "Accuracy", "cumulative_accuracy": "Accuracy",
"cumulative_battles": "Battles", "cumulative_battles": "Battles",
"cumulative_capture_points": "Total encounter points", "cumulative_capture_points": "Total capture points",
"cumulative_damage_dealt": "Total damage", "cumulative_damage_dealt": "Total damage",
"cumulative_damage_ratio": "Damage ratio", "cumulative_damage_ratio": "Damage ratio",
"cumulative_damage_received": "Total damage received", "cumulative_damage_received": "Total damage received",
"cumulative_deaths": "Total deaths", "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_frags": "Total kills",
"cumulative_hits": "Total hits", "cumulative_hits": "Total hits",
"cumulative_inaccuracy": "Inaccuracy", "cumulative_inaccuracy": "Inaccuracy",
@@ -1,3 +1,5 @@
import { calculateWN8 } from '../calculateWN8';
export const blitzStatsKeys = [ export const blitzStatsKeys = [
'battles', 'battles',
'capture_points', 'capture_points',
@@ -131,70 +133,256 @@ export type CompositeStatsKey =
| `cumulative_${(typeof cumulativeCompositeStatsKeys)[number]}`; | `cumulative_${(typeof cumulativeCompositeStatsKeys)[number]}`;
export type CompositeStats = Record<CompositeStatsKey, number>; export type CompositeStats = Record<CompositeStatsKey, number>;
enum CumulationType {
Sum,
WeightedSum,
}
export const compositeStatsFormatting: { export const compositeStatsFormatting: {
[Key in CompositeStatsKey]: { [Key in CompositeStatsKey]: {
unit?: string; unit?: string;
fixed: number; fixed: number;
coefficient?: number; coefficient?: number;
preview: number;
localeFormat?: boolean; localeFormat?: boolean;
cumulate: CumulationType;
}; };
} = { } = {
normalized_damage_dealt: { fixed: 0, preview: 3246, localeFormat: true }, normalized_damage_dealt: {
normalized_damage_received: { fixed: 0, preview: 966, localeFormat: true }, fixed: 0,
normalized_deaths: { fixed: 0, unit: '%', coefficient: 100, preview: 0.09 }, localeFormat: true,
normalized_frags: { fixed: 2, preview: 2.1 }, cumulate: CumulationType.WeightedSum,
normalized_hits: { fixed: 2, preview: 12.33 }, },
normalized_losses: { fixed: 0, unit: '%', coefficient: 100, preview: 0.3 }, normalized_damage_received: {
normalized_misses: { fixed: 2, preview: 1.02 }, fixed: 0,
normalized_shots: { fixed: 2, preview: 14.54 }, localeFormat: true,
normalized_spotted: { fixed: 2, preview: 2.02 }, 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: { normalized_survived_battles: {
fixed: 0, fixed: 0,
unit: '%', unit: '%',
coefficient: 100, coefficient: 100,
preview: 0.65, cumulate: CumulationType.WeightedSum,
}, },
normalized_win_and_survived: { normalized_win_and_survived: {
fixed: 0, fixed: 0,
unit: '%', unit: '%',
coefficient: 100, 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_accuracy: {
cumulative_battles: { fixed: 0, preview: 12 }, fixed: 0,
cumulative_capture_points: { fixed: 2, preview: 0.12 }, unit: '%',
cumulative_damage_dealt: { fixed: 0, preview: 45023, localeFormat: true }, coefficient: 100,
cumulative_damage_ratio: { fixed: 2, preview: 2.83 }, cumulate: CumulationType.WeightedSum,
cumulative_damage_received: { fixed: 0, preview: 10093, localeFormat: true }, },
cumulative_deaths: { fixed: 0, preview: 2 }, cumulative_battles: {
cumulative_dropped_capture_points: { fixed: 2, preview: 15 }, fixed: 0,
cumulative_frags: { fixed: 0, preview: 26 }, cumulate: CumulationType.Sum,
cumulative_hits: { fixed: 0, preview: 480 }, },
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: { cumulative_inaccuracy: {
fixed: 0, fixed: 0,
unit: '%', unit: '%',
coefficient: 100, 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: { cumulative_battle_life_time: {
fixed: 0, fixed: 0,
preview: 109,
coefficient: 1 / 60, coefficient: 1 / 60,
localeFormat: true, 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<CompositeStats> = {};
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;
}
@@ -49,19 +49,20 @@ export function compositeStats(stats: BlitzkitStats, average: BlitzkitStats) {
} satisfies CompositeStats; } 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 formatting = compositeStatsFormatting[key];
const resolvedValue = (formatting.coefficient ?? 1) * value; const resolvedValue = (formatting.coefficient ?? 1) * value;
return `${ return `${
formatting.localeFormat formatting.localeFormat
? resolvedValue.toLocaleString() ? Math.round(resolvedValue).toLocaleString()
: resolvedValue.toFixed(formatting.fixed) : resolvedValue.toFixed(formatting.fixed)
}${formatting.unit ?? ''}`; }${formatting.unit ?? ''}`;
} }
export function previewCompositeStat(key: CompositeStatsKey) {
const { preview } = compositeStatsFormatting[key];
return formatCompositeStat(preview, key);
}
export * from './constants'; export * from './constants';
@@ -1,9 +1,13 @@
import { import {
calculateCompositeStats,
compositeStats,
compositeStatsKeys, compositeStatsKeys,
deltaTankStats, deltaTankStats,
fetchAverageDefinitions,
fetchTankDefinitions, fetchTankDefinitions,
getTankStats, getTankStats,
idToRegion, idToRegion,
sumCompositeStats,
type IndividualTankStats, type IndividualTankStats,
} from '@blitzkit/core'; } from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json'; import strings from '@blitzkit/core/lang/en-US.json';
@@ -21,6 +25,31 @@ export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({
})); }));
const tankDefinitions = await fetchTankDefinitions(); 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() { export function BreakdownPreview() {
const tanks = Object.values(tankDefinitions.tanks); const tanks = Object.values(tankDefinitions.tanks);
@@ -28,10 +57,16 @@ export function BreakdownPreview() {
return ( return (
<BreakdownEmbedWrapper> <BreakdownEmbedWrapper>
{useEmbedState('showTotal') && <BreakdownEmbedCard tank={null} />} {useEmbedState('showTotal') && (
<BreakdownEmbedCard composite={fakeCompositeStats} tank={null} />
)}
{tanks.slice(0, useEmbedState('listMaxTanks')).map((tank) => ( {tanks.slice(0, useEmbedState('listMaxTanks')).map((tank) => (
<BreakdownEmbedCard key={tank.id} tank={tank} /> <BreakdownEmbedCard
composite={fakeCompositeStats}
key={tank.id}
tank={tank}
/>
))} ))}
</BreakdownEmbedWrapper> </BreakdownEmbedWrapper>
); );
@@ -59,6 +94,23 @@ function BreakdownRendererContent() {
), ),
[tankStatsB], [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(() => { useEffect(() => {
const interval = setInterval(async () => { const interval = setInterval(async () => {
@@ -76,13 +128,23 @@ function BreakdownRendererContent() {
<ContextMenu.Root> <ContextMenu.Root>
<ContextMenu.Trigger> <ContextMenu.Trigger>
<BreakdownEmbedWrapper> <BreakdownEmbedWrapper>
{useEmbedState('showTotal') && <BreakdownEmbedCard tank={null} />} {useEmbedState('showTotal') && (
<BreakdownEmbedCard composite={sum} tank={null} />
)}
{diff.slice(0, useEmbedState('listMaxTanks')).map((diff) => { {compositeStats
const tank = tankDefinitions.tanks[diff.tank_id]; .slice(0, useEmbedState('listMaxTanks'))
.map(({ id, compositeStats }) => {
const tank = tankDefinitions.tanks[id];
return <BreakdownEmbedCard key={tank.id} tank={tank} />; return (
})} <BreakdownEmbedCard
key={tank.id}
tank={tank}
composite={compositeStats}
/>
);
})}
</BreakdownEmbedWrapper> </BreakdownEmbedWrapper>
</ContextMenu.Trigger> </ContextMenu.Trigger>
+10 -7
View File
@@ -1,8 +1,9 @@
import { import {
previewCompositeStat, formatCompositeStat,
TankClass, TankClass,
TankDefinition, TankDefinition,
TankType, TankType,
type CompositeStats,
type CompositeStatsKey, type CompositeStatsKey,
} from '@blitzkit/core'; } from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json'; import strings from '@blitzkit/core/lang/en-US.json';
@@ -42,9 +43,13 @@ export function BreakdownEmbedWrapper({
interface BreakdownEmbedCardProps { interface BreakdownEmbedCardProps {
tank: TankDefinition | null | undefined; tank: TankDefinition | null | undefined;
composite: CompositeStats;
} }
export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) { export function BreakdownEmbedCard({
tank,
composite,
}: BreakdownEmbedCardProps) {
const { useEmbedState, useRichText } = const { useEmbedState, useRichText } =
useEmbedStateCurry<typeof breakdownConfig>(); useEmbedStateCurry<typeof breakdownConfig>();
@@ -114,9 +119,7 @@ export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) {
<Flex p={useEmbedState('cardBodyPadding')}> <Flex p={useEmbedState('cardBodyPadding')}>
{times(4, (index) => { {times(4, (index) => {
const column = (index + 1) as 1 | 2 | 3 | 4; const column = (index + 1) as 1 | 2 | 3 | 4;
const columnState = useEmbedState( const statKey = useEmbedState(`column${column}`) as CompositeStatsKey;
`column${column}`,
) as CompositeStatsKey;
const customLabel = useEmbedState(`column${column}CustomLabel`); const customLabel = useEmbedState(`column${column}CustomLabel`);
return ( return (
@@ -128,11 +131,11 @@ export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) {
flexGrow="1" flexGrow="1"
> >
<Text {...useRichText('columnValue')}> <Text {...useRichText('columnValue')}>
{previewCompositeStat(columnState)} {formatCompositeStat(composite[statKey], statKey, composite)}
</Text> </Text>
<Text {...useRichText('columnLabel')}> <Text {...useRichText('columnLabel')}>
{customLabel.length === 0 {customLabel.length === 0
? strings.common.composite_stats[columnState] ? strings.common.composite_stats[statKey]
: customLabel} : customLabel}
</Text> </Text>
</Flex> </Flex>