Files
blitzkit/packages/website/src/components/Performance/Tanks.tsx
T

269 lines
8.3 KiB
TypeScript
Raw Normal View History

import { formatCompact, type BlitzkitStats } from "@blitzkit/core";
2025-09-01 19:13:08 -05:00
import { Table } from "@radix-ui/themes";
import { times } from "lodash-es";
import {
Fragment,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
2025-10-11 21:45:18 -05:00
import usePromise from "react-promise-suspense";
2025-09-01 19:13:08 -05:00
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
2026-08-04 20:29:54 -05:00
import { api } from "../../core/blitzkit/api";
2025-10-11 21:45:18 -05:00
import { filterTanks } from "../../core/blitzkit/filterTanks";
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from "../../hooks/useLocale";
2025-09-01 19:13:08 -05:00
import { Performance } from "../../stores/performance";
import { TankFilters } from "../../stores/tankFilters";
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
2025-09-01 19:13:08 -05:00
import { RowLoader } from "./RowLoader";
import { TankRow } from "./TankRow";
2024-06-30 19:59:32 -05:00
2024-07-06 20:32:41 -05:00
const PREVIEW_COUNT = 10;
const DEFAULT_LOADED_ROWS = 25;
2024-11-29 13:20:27 -06:00
const [tankDefinitions, averageDefinitions] = await Promise.all([
2026-08-04 20:29:54 -05:00
api.tankDefinitions(),
2024-11-29 13:20:27 -06:00
awaitableAverageDefinitions,
]);
2025-10-11 21:45:18 -05:00
const tankList = Object.values(tankDefinitions.tanks);
2024-10-07 17:31:13 -05:00
2024-10-18 10:24:39 -05:00
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
if (skeleton) {
return (
<Table.Body>
{times(16, (index) => (
<RowLoader key={index} />
))}
</Table.Body>
);
}
2024-07-08 19:59:15 -05:00
const sort = TankPerformanceSort.use();
2025-10-11 21:45:18 -05:00
const filters = TankFilters.use();
const filteredTanks = usePromise(
() =>
filterTanks(filters, tankList).then((tanks) =>
2026-03-31 14:11:45 -05:00
tanks.filter(({ id }) => id in averageDefinitions.averages),
2025-10-11 21:45:18 -05:00
),
// react-promise-suspense has awful type annotations
2026-03-31 14:11:45 -05:00
[filters] as any,
2025-10-11 21:45:18 -05:00
);
const tanksMapped = useMemo(
() =>
filteredTanks.map(({ id }) => ({
id,
...averageDefinitions.averages[id],
})),
2026-03-31 14:11:45 -05:00
[filters],
2025-10-11 21:45:18 -05:00
);
2024-07-02 19:19:15 -05:00
const tanksSorted = useMemo(() => {
2025-09-01 18:16:41 -05:00
const { playerCountPeriod } = Performance.state;
2024-07-13 14:20:07 -05:00
2024-07-01 20:18:09 -05:00
switch (sort.type) {
2025-09-01 19:13:08 -05:00
case "accuracy":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
2026-03-31 14:11:45 -05:00
sort.direction *
(a.mu!.hits / a.mu!.shots - b.mu!.hits / b.mu!.shots),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "capture_points":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.capture_points / a.mu!.battles -
b.mu!.capture_points / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "damage":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.damage_dealt / a.mu!.battles -
b.mu!.damage_dealt / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "damage_ratio":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.damage_dealt / a.mu!.damage_received -
b.mu!.damage_dealt / b.mu!.damage_received),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "damage_taken":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.damage_received / a.mu!.battles -
b.mu!.damage_received / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "hits":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.hits / a.mu!.battles - b.mu!.hits / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "kills":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.frags / a.mu!.battles - b.mu!.frags / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "shots":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.shots / a.mu!.battles - b.mu!.shots / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "players":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-13 14:20:07 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.samples![playerCountPeriod] - b.samples![playerCountPeriod]),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "spots":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.spotted / a.mu!.battles - b.mu!.spotted / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "survival":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.survived_battles / a.mu!.battles -
b.mu!.survived_battles / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "winrate":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
sort.direction *
2026-03-31 14:11:45 -05:00
(a.mu!.wins / a.mu!.battles - b.mu!.wins / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
2025-09-01 19:13:08 -05:00
case "xp":
2025-10-11 21:45:18 -05:00
return tanksMapped.sort(
2024-07-01 20:18:09 -05:00
(a, b) =>
2026-03-31 14:11:45 -05:00
sort.direction *
(a.mu!.xp / a.mu!.battles - b.mu!.xp / b.mu!.battles),
2024-07-01 20:18:09 -05:00
);
}
2024-07-06 20:32:41 -05:00
}, [filters, sort]);
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
useEffect(() => {
setLoadedRows(DEFAULT_LOADED_ROWS);
}, [filters, sort]);
2024-06-30 19:59:32 -05:00
const playerCountPeriod = Performance.use((state) => state.playerCountPeriod);
const { strings, locale } = useLocale();
const ratio = useAveragesExclusionRatio();
const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => {
return tanksSorted.reduce(
2026-03-31 14:11:45 -05:00
(acc, tank) => acc + tank.samples![playerCountPeriod] * slice(tank.mu!),
0,
);
},
2026-03-31 14:11:45 -05:00
[tanksSorted],
);
const battles = sum(({ battles }) => battles);
const winrate = sum(({ wins }) => wins) / battles;
const damage = sum(({ damage_dealt }) => damage_dealt) / battles;
const survival = sum(({ survived_battles }) => survived_battles) / battles;
const xp = sum(({ xp }) => xp) / battles;
const kills = sum(({ frags }) => frags) / battles;
const spots = sum(({ spotted }) => spotted) / battles;
const hits = sum(({ hits }) => hits) / battles;
const shots = sum(({ shots }) => shots) / battles;
const accuracy = hits / shots;
const damageReceived =
sum(({ damage_received }) => damage_received) / battles;
const damageRatio = damage / damageReceived;
const capturePoints = sum(({ capture_points }) => capture_points) / battles;
const average = {
battles,
winrate,
damage,
survival,
xp,
kills,
spots,
hits,
shots,
accuracy,
damageReceived,
damageRatio,
capturePoints,
};
2024-06-30 19:59:32 -05:00
return (
<Table.Body>
<Table.Row>
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
{strings.website.tools.performance.table.tanks.total}
</StickyRowHeaderCell>
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
<Table.Cell align="center">
{formatCompact(
locale,
2026-03-31 14:11:45 -05:00
ratio * averageDefinitions.samples![playerCountPeriod],
)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(damage).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(survival * 100).toFixed(1)}%
</Table.Cell>
<Table.Cell align="center">
{Math.round(xp).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{(accuracy * 100).toFixed(0)}%</Table.Cell>
<Table.Cell align="center">{shots.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
<Table.Cell align="center">
{Math.round(damageReceived).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
</Table.Row>
2024-07-07 18:56:49 -05:00
2025-10-11 21:45:18 -05:00
{tanksSorted.slice(0, loadedRows).map((averages) => {
2024-10-07 17:31:13 -05:00
const tank = tankDefinitions.tanks[averages.id];
2024-07-08 19:59:15 -05:00
if (tank === undefined) return null;
2024-06-30 19:59:32 -05:00
return (
2024-10-13 20:59:01 -05:00
<Fragment key={tank.id}>
<Suspense fallback={<RowLoader />}>
<TankRow average={average} tank={tank} />
2024-10-13 20:59:01 -05:00
</Suspense>
</Fragment>
2024-06-30 19:59:32 -05:00
);
})}
2024-07-06 20:32:41 -05:00
2025-10-11 21:45:18 -05:00
{times(
Math.min(PREVIEW_COUNT, tanksSorted.length - loadedRows),
(index) => {
return (
<RowLoader
key={index}
onIntersection={() => {
setLoadedRows((state) =>
2026-03-31 14:11:45 -05:00
Math.min(state + 2, tanksSorted.length),
2025-10-11 21:45:18 -05:00
);
}}
/>
);
2026-03-31 14:11:45 -05:00
},
2025-10-11 21:45:18 -05:00
)}
2024-06-30 19:59:32 -05:00
</Table.Body>
);
}