feat: deltas

This commit is contained in:
TrèsAbhi committed 2024-05-01 20:54:27 -05:00
1 parent cceebd1980
commit c24639dd0c
5 files changed
+128 -477

No files matched your search

-338
View File
@@ -1,338 +0,0 @@
'use client';
import { ContextMenu } from '@radix-ui/themes';
import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { resetSession } from '../../../core/blitzkit/resetSession';
import {
TankDefinition,
tankDefinitions,
} from '../../../core/blitzkit/tankDefinitions';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import mutateSession, { useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
const REFRESH_RATE = 1 / 5;
interface SessionPageProps {
naked?: boolean;
}
export default function SessionPage({ naked = true }: SessionPageProps) {
const session = useSession();
const since = session.isTracking ? new Date(session.time) : undefined;
const [diff, setDiff] = useState<
| {
careerWN8: number;
list: {
stats: IndividualTankStats;
tankDefinitions?: TankDefinition;
career: IndividualTankStats;
currentWN8?: number;
careerWN8?: number;
}[];
}
| undefined
>(undefined);
const sessionBattles = diff?.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.battles,
0,
);
const tankStatsArray = session.isTracking
? (Object.values(session.tankStats) as IndividualTankStats[])
: [];
const sessionWinrate = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.wins,
0,
) / sessionBattles!
: 0;
const careerBattles = tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.battles,
0,
);
const careerWinrate =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.wins,
0,
) / careerBattles;
const currentDamage = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.damage_dealt,
0,
) / sessionBattles!
: 0;
const careerDamage =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.damage_dealt,
0,
) / careerBattles;
useEffect(() => {
if (session.title === 'Untitled session') {
mutateSession((draft) => {
if (draft.isTracking) draft.title = draft.nickname;
});
}
}, [session.title]);
useEffect(() => {
async function recalculateDiff() {
if (session.isTracking) {
const { id, region } = session;
const careerRaw = await fetchBlitz<TanksStats>(region, 'tanks/stats', {
account_id: id,
});
const career = careerRaw[id]!.reduce<
Record<number, IndividualTankStats>
>(
(accumulator, curr) => ({
...accumulator,
[curr.tank_id]: curr,
}),
{},
);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await tankAverages;
const careerWN8 =
careerRaw[id]!.reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles *
calculateWN8(awaitedTankAverages[tank_id].all, all)
: 0),
0,
) /
careerRaw[id]!.reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles
: 0),
0,
);
setDiff({
careerWN8,
list: (
Object.values(
deltaTankStats(session.tankStats, career, session.time / 1000),
) as IndividualTankStats[]
).map((stats) => {
return {
stats,
tankDefinitions: awaitedTankDefinitions[stats.tank_id],
career: career[stats.tank_id],
currentWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
stats.all,
)
: undefined,
careerWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
career[stats.tank_id].all,
)
: undefined,
};
}),
});
}
}
recalculateDiff();
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
return () => clearInterval(interval);
}, [session]);
const totalRowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: sessionBattles?.toLocaleString(),
career: tankStatsArray
.reduce((accumulator, stats) => accumulator + stats.all.battles, 0)
.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: sessionBattles ? `${(100 * sessionWinrate).toFixed(2)}%` : '--',
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
},
wn8: (() => {
if (!diff) return undefined;
const battlesWithWN8 = diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
(currentWN8 === undefined ? 0 : stats.all.battles) + accumulator,
0,
);
const currentWN8 =
diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
stats.all.battles * (currentWN8 ?? 0) + accumulator,
0,
) / battlesWithWN8!;
return {
title: 'WN8',
current: battlesWithWN8
? Math.round(currentWN8!).toLocaleString()
: '--',
percentile: battlesWithWN8 ? getWN8Percentile(currentWN8!) : undefined,
career: Math.round(diff.careerWN8).toLocaleString(),
};
})(),
damage: {
title: 'Damage',
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
current: sessionBattles
? Math.round(currentDamage).toLocaleString()
: '--',
career: Math.round(careerDamage).toLocaleString(),
},
none: undefined,
};
return (
<ContextMenu.Root>
<ContextMenu.Trigger>
<div>
<Breakdown.Root>
{session.showTotal &&
session.isTracking &&
since !== undefined &&
diff !== undefined && (
<Breakdown.Row
naked={naked}
color={session.color}
minimized={!session.showCareer}
title={session.title}
stats={session.customColumns.map((customColumn) => {
const rowStat = totalRowStats[customColumn.display];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
)}
{diff?.list
.sort(
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
)
.map(
({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => {
const rowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: stats.all.battles.toLocaleString(),
career: career.all.battles.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: `${(
100 *
(stats.all.wins / stats.all.battles)
).toFixed(2)}%`,
career: `${(
100 *
(career.all.wins / career.all.battles)
).toFixed(2)}%`,
delta:
stats.all.wins / stats.all.battles -
career.all.wins / career.all.battles,
},
wn8: {
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
damage: {
title: 'Damage',
current: Math.round(
stats.all.damage_dealt / stats.all.battles,
).toLocaleString(),
career: Math.round(
career.all.damage_dealt / career.all.battles,
).toLocaleString(),
delta:
stats.all.damage_dealt / stats.all.battles -
career.all.damage_dealt / career.all.battles,
},
none: undefined,
};
return (
<Breakdown.Row
naked={naked}
color={session.color}
key={stats.tank_id}
minimized={!session.showCareer}
title={
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
}
type="tank"
tankType={tankDefinitions?.class}
treeType={tankDefinitions?.treeType}
stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
);
},
)}
</Breakdown.Root>
</div>
</ContextMenu.Trigger>
<Menu reset={resetSession} Builder={ContextMenu} />
</ContextMenu.Root>
);
}
+83 -70
View File
@@ -38,6 +38,7 @@ import searchPlayersAcrossRegions, {
import { tankDefinitions } from '../../../core/blitzkit/tankDefinitions';
import { tankIcon } from '../../../core/blitzkit/tankIcon';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import { theme } from '../../../stitches.config';
import { useApp } from '../../../stores/app';
import mutateSession, {
@@ -71,6 +72,15 @@ export default function Page() {
[session.tracking && session.player.id],
);
const accountInfo = use(accountInfoPromise);
const delta = useMemo(
() =>
session.tracking && tankStatsB
? deltaTankStats(session.player.stats, tankStatsB).sort(
(a, b) => b.last_battle_time - a.last_battle_time,
)
: undefined,
[session.tracking && session.player, tankStatsB],
);
const search = debounce(async () => {
if (!input.current) return;
@@ -248,7 +258,12 @@ export default function Page() {
<Button
key={player.account_id}
variant="ghost"
onClick={() => {
onClick={async () => {
const stats = (await getTankStats(
player.region,
player.account_id,
))!;
setShowSearch(false);
mutateSession((draft) => {
if (!input.current) return;
@@ -258,6 +273,7 @@ export default function Page() {
id: player.account_id,
region: player.region,
since: Date.now(),
stats,
};
input.current.value = player.nickname;
@@ -308,7 +324,7 @@ export default function Page() {
</Flex>
)}
{session.tracking && tankStatsB && tankStatsB.length > 0 && (
{session.tracking && delta && delta.length > 0 && (
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
@@ -322,84 +338,81 @@ export default function Page() {
</Table.Header>
<Table.Body>
{tankStatsB
.filter((stat) => stat.all.battles > 0)
.sort((a, b) => b.last_battle_time - a.last_battle_time)
.map((entry) => {
const tank = awaitedTankDefinitions[entry.tank_id];
const average = awaitedTankAvearges[tank.id];
const stats = prettifyStats(
generateStats(entry.all, average?.all),
);
{delta.map((entry) => {
const tank = awaitedTankDefinitions[entry.tank_id];
const average = awaitedTankAvearges[tank.id];
const stats = prettifyStats(
generateStats(entry.all, average?.all),
);
return (
<Table.Row
key={tank.id}
return (
<Table.Row
key={tank.id}
style={{
overflow: 'hidden',
}}
>
<Table.RowHeaderCell
style={{
overflow: 'hidden',
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
<Table.RowHeaderCell
style={{
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
<Link href={`/tools/tankopedia/${tank.id}`}>
<img
draggable={false}
src={tankIcon(tank.id)}
style={{
position: 'absolute',
width: 128 + 32,
height: '200%',
top: '-50%',
left: 0,
objectFit: 'contain',
objectPosition: '50% 50%',
overflow: 'hidden',
}}
/>
<Link href={`/tools/tankopedia/${tank.id}`}>
<img
draggable={false}
src={tankIcon(tank.id)}
style={{
position: 'absolute',
width: 128 + 32,
height: '200%',
top: '-50%',
left: 0,
objectFit: 'contain',
objectPosition: '50% 50%',
overflow: 'hidden',
}}
/>
<div
style={{
backgroundColor: 'red',
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: 128,
background:
'linear-gradient(90deg, #00000080, #00000000)',
}}
/>
<div
style={{
backgroundColor: 'red',
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: 128,
background:
'linear-gradient(90deg, #00000080, #00000000)',
}}
/>
<Text
style={{
color: theme.colors.textHighContrast,
position: 'relative',
textWrap: 'nowrap',
textShadow: 'black 0 0 4px',
}}
>
{tank.name}
</Text>
</Link>
</Table.RowHeaderCell>
{session.columns.map((column) => (
<Table.Cell key={column} align="right">
{stats[column]}
</Table.Cell>
))}
</Table.Row>
);
})}
<Text
style={{
color: theme.colors.textHighContrast,
position: 'relative',
textWrap: 'nowrap',
textShadow: 'black 0 0 4px',
}}
>
{tank.name}
</Text>
</Link>
</Table.RowHeaderCell>
{session.columns.map((column) => (
<Table.Cell key={column} align="right">
{stats[column]}
</Table.Cell>
))}
</Table.Row>
);
})}
</Table.Body>
</Table.Root>
)}
{session.tracking && tankStatsB && tankStatsB.length === 0 && (
{session.tracking && delta && delta.length === 0 && (
<Flex
align="center"
justify="center"
+2 -7
View File
@@ -15,12 +15,8 @@ import {
Spinner,
} from '@radix-ui/themes';
import Link from 'next/link';
import { Suspense, useEffect, useState } from 'react';
import {
Region,
REGIONS,
UNLOCALIZED_REGION_NAMES,
} from '../../constants/regions';
import { Suspense, useEffect } from 'react';
import { REGIONS, UNLOCALIZED_REGION_NAMES } from '../../constants/regions';
import { TOOLS } from '../../constants/tools';
import { authURL } from '../../core/blitz/authURL';
import { extendAuth } from '../../core/blitz/extendAuth';
@@ -37,7 +33,6 @@ export default function Navbar() {
const isFullScreen = useFullScreen();
const wideFormat = useWideFormat(480);
const login = useApp((state) => state.login);
const [region, setRegion] = useState<Region>('com');
useEffect(() => {
if (!login) return;
+41 -62
View File
@@ -1,68 +1,47 @@
import {
IndividualTankStats,
NormalizedTankStats,
emptyIndividualTankStats,
} from '../../types/tanksStats';
import { IndividualTankStats } from '../../types/tanksStats';
export function deltaTankStats(
aRaw: NormalizedTankStats,
bRaw: NormalizedTankStats,
time?: number,
tanksA: IndividualTankStats[],
tanksB: IndividualTankStats[],
) {
const bTanks = Object.keys(bRaw);
const changedTanks = bTanks.filter((id) => {
return (
aRaw[id as unknown as number]?.all.battles !==
bRaw[id as unknown as number].all.battles &&
(time === undefined
? true
: bRaw[id as unknown as number].last_battle_time > time)
);
});
const diff = changedTanks.map((id) => {
const a = aRaw[id as unknown as number] ?? emptyIndividualTankStats;
const b = bRaw[id as unknown as number];
return tanksB
.map((b) => {
const a = tanksA.find((a) => a.tank_id === b.tank_id);
if (!a) return null;
const tankDiff = {
account_id: b.account_id,
battle_life_time: b.battle_life_time,
frags: (b.frags ?? a.frags ?? 0) - (a.frags ?? 0),
max_frags: Math.max(a.max_frags, b.max_frags),
in_garage: b.in_garage,
max_xp: Math.max(a.max_xp, b.max_xp),
in_garage_updated: b.in_garage_updated,
last_battle_time: Math.max(b.last_battle_time, a.last_battle_time),
mark_of_mastery: b.mark_of_mastery,
tank_id: b.tank_id,
return {
account_id: b.account_id,
battle_life_time: b.battle_life_time - a.battle_life_time,
frags: b.frags === null || a.frags === null ? null : b.frags - a.frags,
in_garage: b.in_garage,
in_garage_updated: b.in_garage_updated,
last_battle_time: b.last_battle_time,
mark_of_mastery: b.mark_of_mastery,
max_frags: b.max_frags,
max_xp: b.max_xp,
tank_id: b.tank_id,
all: {
battles: b.all.battles - a.all.battles,
wins: b.all.wins - a.all.wins,
capture_points: b.all.capture_points - a.all.capture_points,
damage_dealt: b.all.damage_dealt - a.all.damage_dealt,
damage_received: b.all.damage_received - a.all.damage_received,
dropped_capture_points:
b.all.dropped_capture_points - a.all.dropped_capture_points,
frags: b.all.frags - a.all.frags,
frags8p: b.all.frags8p - a.all.frags8p,
hits: b.all.hits - a.all.hits,
losses: b.all.losses - a.all.losses,
max_frags: Math.max(a.max_frags, b.max_frags),
max_xp: Math.max(a.max_xp, b.max_xp),
shots: b.all.shots - a.all.shots,
spotted: b.all.spotted - a.all.spotted,
survived_battles: b.all.survived_battles - a.all.survived_battles,
win_and_survived: b.all.win_and_survived - a.all.win_and_survived,
xp: b.all.xp - a.all.xp,
},
} satisfies IndividualTankStats;
return tankDiff;
});
const normalized = diff.reduce<NormalizedTankStats>(
(accumulator, diff) => ({ ...accumulator, [diff.tank_id]: diff }),
{},
);
return normalized;
all: {
battles: b.all.battles - a.all.battles,
capture_points: b.all.capture_points - a.all.capture_points,
damage_dealt: b.all.damage_dealt - a.all.damage_dealt,
damage_received: b.all.damage_received - a.all.damage_received,
dropped_capture_points:
b.all.dropped_capture_points - a.all.dropped_capture_points,
frags: b.all.frags - a.all.frags,
frags8p: b.all.frags8p - a.all.frags8p,
hits: b.all.hits - a.all.hits,
losses: b.all.losses - a.all.losses,
max_frags: b.all.max_frags,
max_xp: b.all.max_xp,
shots: b.all.shots - a.all.shots,
spotted: b.all.spotted - a.all.spotted,
survived_battles: b.all.survived_battles - a.all.survived_battles,
xp: b.all.xp - a.all.xp,
win_and_survived: b.all.win_and_survived - a.all.win_and_survived,
wins: b.all.wins - a.all.wins,
},
} satisfies IndividualTankStats;
})
.filter((a) => a !== null && a.all.battles > 0) as IndividualTankStats[];
}
+2
View File
@@ -4,6 +4,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Region } from '../constants/regions';
import { Stat } from '../core/blitz/generateStats';
import { IndividualTankStats } from '../types/tanksStats';
interface SessionBase {
columns: Stat[];
@@ -15,6 +16,7 @@ export interface SessionTracking extends SessionBase {
id: number;
region: Region;
since: number;
stats: IndividualTankStats[];
};
}