feat: career stats

This commit is contained in:
TrèsAbhi committed 2023-09-24 17:02:09 -05:00
1 parent 8c041d4eef
commit d801447e5d
6 files changed
+252 -121

No files matched your search

+246 -115
View File
@@ -8,7 +8,7 @@ import { Button } from '../../../components/Button';
import PageWrapper from '../../../components/PageWrapper'; import PageWrapper from '../../../components/PageWrapper';
import { SearchBar } from '../../../components/SearchBar'; import { SearchBar } from '../../../components/SearchBar';
import { TreeTypeEnum } from '../../../components/Tanks'; import { TreeTypeEnum } from '../../../components/Tanks';
import { REGION_NAMES } from '../../../constants/regions'; import { REGION_NAMES, Region } from '../../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import calculateWN8 from '../../../core/blitz/calculateWN8'; import calculateWN8 from '../../../core/blitz/calculateWN8';
import { diffNormalizedTankStats } from '../../../core/blitz/diffNormalizedTankStats'; import { diffNormalizedTankStats } from '../../../core/blitz/diffNormalizedTankStats';
@@ -28,6 +28,9 @@ import {
} from '../../../types/tanksStats'; } from '../../../types/tanksStats';
import * as styles from './page.css'; import * as styles from './page.css';
// 1 once per 5 seconds
const REFRESH_RATE = 1 / 5;
export default function Page() { export default function Page() {
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState< const [searchResults, setSearchResults] = useState<
@@ -45,63 +48,90 @@ export default function Page() {
500, 500,
); );
const session = useSession(); const session = useSession();
const tankStatsArray = session.isTracking
? (Object.values(session.tankStats) as IndividualTankStats[])
: [];
const since = session.isTracking ? new Date(session.time) : undefined;
const [diff, setDiff] = useState< const [diff, setDiff] = useState<
| { | {
stats: IndividualTankStats; careerWN8: number;
tankopedia?: TankopediaEntry;
career: IndividualTankStats; list: {
currentWN8?: number; stats: IndividualTankStats;
careerWN8?: number; tankopedia?: TankopediaEntry;
}[] career: IndividualTankStats;
currentWN8?: number;
careerWN8?: number;
}[];
}
| undefined | undefined
>(undefined); >(undefined);
useEffect(() => { async function recalculateDiff() {
(async () => { if (session.isTracking) {
if (session.isTracking) { const { id, region } = session;
const { id, region } = session; const careerRaw = await getWargamingResponse<TanksStats>(
const careerRaw = await getWargamingResponse<TanksStats>( `https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, );
const career = careerRaw[id].reduce<Record<number, IndividualTankStats>>(
(accumulator, curr) => ({
...accumulator,
[curr.tank_id]: curr,
}),
{},
);
const awaitedTankopedia = await tankopedia;
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,
); );
const career = careerRaw[id].reduce<
Record<number, IndividualTankStats>
>(
(accumulator, curr) => ({
...accumulator,
[curr.tank_id]: curr,
}),
{},
);
const awaitedTankopedia = await tankopedia;
const awaitedTankAverages = await tankAverages;
setDiff( setDiff({
( careerWN8,
Object.values(
diffNormalizedTankStats(session.tankStats, career), list: (
) as IndividualTankStats[] Object.values(
).map((stats) => { diffNormalizedTankStats(session.tankStats, career),
return { ) as IndividualTankStats[]
stats, ).map((stats) => {
tankopedia: awaitedTankopedia[stats.tank_id], return {
career: career[stats.tank_id], stats,
currentWN8: awaitedTankAverages[stats.tank_id] tankopedia: awaitedTankopedia[stats.tank_id],
? calculateWN8( career: career[stats.tank_id],
awaitedTankAverages[stats.tank_id].all, currentWN8: awaitedTankAverages[stats.tank_id]
stats.all, ? calculateWN8(awaitedTankAverages[stats.tank_id].all, stats.all)
) : undefined,
: undefined, careerWN8: awaitedTankAverages[stats.tank_id]
careerWN8: awaitedTankAverages[stats.tank_id] ? calculateWN8(
? calculateWN8( awaitedTankAverages[stats.tank_id].all,
awaitedTankAverages[stats.tank_id].all, career[stats.tank_id].all,
career[stats.tank_id].all, )
) : undefined,
: undefined, };
}; }),
}), });
); }
} }
})(); async function setSession(region: Region, id: number, nickname: string) {}
useEffect(() => {
recalculateDiff();
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
return () => clearInterval(interval);
}, []); }, []);
return ( return (
@@ -135,7 +165,6 @@ export default function Page() {
marginTop: 8, marginTop: 8,
flexDirection: 'column', flexDirection: 'column',
backgroundColor: theme.colors.componentNonInteractive, backgroundColor: theme.colors.componentNonInteractive,
border: theme.borderStyles.nonInteractive,
borderRadius: 4, borderRadius: 4,
padding: 8, padding: 8,
boxSizing: 'border-box', boxSizing: 'border-box',
@@ -244,69 +273,171 @@ export default function Page() {
</div> </div>
<Breakdown.Root> <Breakdown.Root>
{diff && {session.isTracking && since !== undefined && diff !== undefined && (
diff <Breakdown.Row
.sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time) title={`Since ${since.toLocaleString(undefined, {
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => ( hour: 'numeric',
<Breakdown.Row minute: 'numeric',
key={stats.tank_id} hour12: true,
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`} month: 'short',
type="tank" year: 'numeric',
tankType={tankopedia?.type} day: 'numeric',
treeType={(() => { })}`}
if (tankopedia?.is_collectible) return TreeTypeEnum.Collector; stats={[
if (tankopedia?.is_premium) return TreeTypeEnum.Premium; {
})()} title: 'Battles',
rows={[ current: diff.list
{ .reduce(
title: 'Battles', (accumulator, { stats }) => accumulator + stats.all.battles,
current: stats.all.battles.toLocaleString(), 0,
career: career.all.battles.toLocaleString(), )
}, .toLocaleString(),
{ career: tankStatsArray
title: 'Winrate', .reduce(
current: `${( (accumulator, stats) => accumulator + stats.all.battles,
100 * 0,
(stats.all.wins / stats.all.battles) )
).toFixed(2)}%`, .toLocaleString(),
career: `${( },
100 * {
(career.all.wins / career.all.battles) title: 'Winrate',
).toFixed(2)}%`, current: `${(
delta: 100 *
stats.all.wins / stats.all.battles - (diff.list.reduce(
career.all.wins / career.all.battles, (accumulator, { stats }) => accumulator + stats.all.wins,
}, 0,
{ ) /
title: 'WN8', diff.list.reduce(
current: (accumulator, { stats }) =>
currentWN8 === undefined accumulator + stats.all.battles,
? undefined 0,
: Math.round(currentWN8).toLocaleString(), ))
career: ).toFixed(2)}%`,
careerWN8 === undefined career: `${(
? undefined 100 *
: Math.round(careerWN8).toLocaleString(), (tankStatsArray.reduce(
percentile: (accumulator, stats) => accumulator + stats.all.wins,
currentWN8 === undefined 0,
? undefined ) /
: getWN8Percentile(currentWN8), tankStatsArray.reduce(
}, (accumulator, stats) => accumulator + stats.all.battles,
{ 0,
title: 'Damage', ))
current: Math.round( ).toFixed(2)}%`,
stats.all.damage_dealt / stats.all.battles, },
).toLocaleString(), (() => {
career: Math.round( const currentWN8 =
career.all.damage_dealt / career.all.battles, diff.list.reduce(
).toLocaleString(), (accumulator, { currentWN8, stats }) =>
delta: stats.all.battles * (currentWN8 ?? 0) + accumulator,
stats.all.damage_dealt / stats.all.battles - 0,
career.all.damage_dealt / career.all.battles, ) /
}, diff.list.reduce(
]} (accumulator, { currentWN8, stats }) =>
/> (currentWN8 === undefined ? 0 : stats.all.battles) +
))} accumulator,
0,
);
return {
title: 'WN8',
current: Math.round(currentWN8).toLocaleString(),
percentile: getWN8Percentile(currentWN8),
career: Math.round(diff.careerWN8).toLocaleString(),
};
})(),
{
title: 'Damage',
current: Math.round(
diff.list.reduce(
(accumulator, { stats }) =>
accumulator + stats.all.damage_dealt,
0,
) /
diff.list.reduce(
(accumulator, { stats }) =>
accumulator + stats.all.battles,
0,
),
).toLocaleString(),
career: Math.round(
tankStatsArray.reduce(
(accumulator, stats) =>
accumulator + stats.all.damage_dealt,
0,
) /
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.battles,
0,
),
).toLocaleString(),
},
]}
/>
)}
{diff?.list
.sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time)
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => (
<Breakdown.Row
key={stats.tank_id}
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
type="tank"
tankType={tankopedia?.type}
treeType={(() => {
if (tankopedia?.is_collectible) return TreeTypeEnum.Collector;
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
})()}
stats={[
{
title: 'Battles',
current: stats.all.battles.toLocaleString(),
career: career.all.battles.toLocaleString(),
},
{
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,
},
{
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
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,
},
]}
/>
))}
</Breakdown.Root> </Breakdown.Root>
</PageWrapper> </PageWrapper>
); );
+3 -3
View File
@@ -5,13 +5,13 @@ import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
import { RowStat } from './RowStat'; import { RowStat } from './RowStat';
interface RowProps { interface RowProps {
type: 'tank' | 'summary'; type?: 'tank' | 'summary';
title: string; title: string;
minimized?: boolean; minimized?: boolean;
treeType?: TreeTypeEnum; treeType?: TreeTypeEnum;
tankType?: TankType; tankType?: TankType;
rows: { stats: {
title: string; title: string;
current?: number | string; current?: number | string;
career?: number | string; career?: number | string;
@@ -77,7 +77,7 @@ export function Row(props: RowProps) {
paddingBottom: 8, paddingBottom: 8,
}} }}
> >
{props.rows.map((row, index) => ( {props.stats.map((row, index) => (
<RowStat <RowStat
minimized={props.minimized} minimized={props.minimized}
key={index} key={index}
+1 -1
View File
@@ -23,7 +23,7 @@ export function Button({ color, style, ...props }: ButtonProps) {
backgroundColor: backgroundColor:
theme.colors[`componentInteractive${BUTTON_COLORS[color]}`], theme.colors[`componentInteractive${BUTTON_COLORS[color]}`],
borderRadius: 4, borderRadius: 4,
border: theme.borderStyles[`interactive${BUTTON_COLORS[color]}`], border: 'none',
cursor: 'pointer', cursor: 'pointer',
padding: 8, padding: 8,
display: 'flex', display: 'flex',
-1
View File
@@ -14,7 +14,6 @@ export const SearchBar = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
display: 'flex', display: 'flex',
padding: 8, padding: 8,
borderRadius: 4, borderRadius: 4,
border: theme.borderStyles.interactive,
backgroundColor: theme.colors.componentInteractive, backgroundColor: theme.colors.componentInteractive,
color: theme.colors.textLowContrast, color: theme.colors.textLowContrast,
fontSize: 16, fontSize: 16,
+1 -1
View File
@@ -138,7 +138,7 @@ export default async function today(
treeType={tankopediaEntry ? await getTreeType(id) : undefined} treeType={tankopediaEntry ? await getTreeType(id) : undefined}
title={showTotal && id === 0 ? 'Total' : await resolveTankName(id)} title={showTotal && id === 0 ? 'Total' : await resolveTankName(id)}
minimized={showTotal ? index > maximized : index + 1 > maximized} minimized={showTotal ? index > maximized : index + 1 > maximized}
rows={[ stats={[
{ {
title: 'Battles', title: 'Battles',
current: tankStats.battles.toLocaleString(), current: tankStats.battles.toLocaleString(),
+1
View File
@@ -14,6 +14,7 @@ type Session =
id: number; id: number;
nickname: string; nickname: string;
tankStats: NormalizedTankStats; tankStats: NormalizedTankStats;
time: number;
}; };
export const useSession = create<Session>()( export const useSession = create<Session>()(