mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: migrate performance
This commit is contained in:
1 parent
b31c7a6738
commit
8d51ce2bea
20 files changed
+122
-149
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { RowLoader } from './RowLoader';
|
||||
|
||||
export function BodyLoader() {
|
||||
return (
|
||||
<Table.Body>
|
||||
{times(10, (index) => (
|
||||
<RowLoader key={index} />
|
||||
))}
|
||||
</Table.Body>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { FilterControl } from '../TankSearch/components/FilterControl';
|
||||
import { PlayerCountControl } from './PlayerCountControl';
|
||||
|
||||
export function Controls() {
|
||||
return (
|
||||
<>
|
||||
<FilterControl />
|
||||
<PlayerCountControl />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CaretSortIcon,
|
||||
CaretUpIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Flex, IconButton, Table } from '@radix-ui/themes';
|
||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
||||
import {
|
||||
TankPerformanceSortTypeNames,
|
||||
TankPerformanceSortTypeNamesArray,
|
||||
} from '../../stores/tankPerformanceSort/constants';
|
||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
||||
|
||||
export function Header() {
|
||||
const sort = TankPerformanceSort.use();
|
||||
const store = TankPerformanceSort.useStore();
|
||||
|
||||
return (
|
||||
<Table.Header>
|
||||
<Table.Row align="center">
|
||||
<StickyColumnHeaderCell>Tank</StickyColumnHeaderCell>
|
||||
{TankPerformanceSortTypeNamesArray.map((type) => {
|
||||
const isSelected = sort.type === type;
|
||||
|
||||
return (
|
||||
<StickyColumnHeaderCell
|
||||
px="2"
|
||||
key={type}
|
||||
width="0"
|
||||
justify="center"
|
||||
>
|
||||
<Flex align="center" gap="1">
|
||||
{TankPerformanceSortTypeNames[type]}
|
||||
|
||||
<IconButton
|
||||
size={isSelected ? '1' : '2'}
|
||||
variant={isSelected ? 'soft' : 'ghost'}
|
||||
color={isSelected ? undefined : 'gray'}
|
||||
highContrast={!isSelected}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
store.setState({ direction: -sort.direction as 1 | -1 });
|
||||
} else {
|
||||
store.setState({ type, direction: -1 });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!isSelected && <CaretSortIcon />}
|
||||
{isSelected && (
|
||||
<>
|
||||
{sort.direction === 1 && <CaretUpIcon />}
|
||||
{sort.direction === -1 && <CaretDownIcon />}
|
||||
</>
|
||||
)}
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</StickyColumnHeaderCell>
|
||||
);
|
||||
})}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
fetchAverageDefinitions,
|
||||
fetchDiscoveredIdsDefinitions,
|
||||
formatCompact,
|
||||
} from '@blitzkit/core';
|
||||
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
||||
import { Callout, Flex } from '@radix-ui/themes';
|
||||
|
||||
const discoveredIdsDefinitions = await fetchDiscoveredIdsDefinitions();
|
||||
const averageDefinitions = await fetchAverageDefinitions();
|
||||
|
||||
export function PerformanceInfo() {
|
||||
const samples = discoveredIdsDefinitions.count;
|
||||
const minutesAgo = Math.floor(
|
||||
(Date.now() - averageDefinitions.time) / (1000 * 60),
|
||||
);
|
||||
const hoursAgo = Math.floor(minutesAgo / 60);
|
||||
|
||||
return (
|
||||
<Flex justify="center">
|
||||
<Callout.Root style={{ width: 'fit-content' }}>
|
||||
<Callout.Icon>
|
||||
<InfoCircledIcon />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>
|
||||
Career stats based on {formatCompact(Math.round(samples))} players.
|
||||
Updated {hoursAgo === 0 ? minutesAgo : hoursAgo}{' '}
|
||||
{hoursAgo === 0 ? 'minutes' : 'hours'} ago.
|
||||
</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Flex, Select, Text } from '@radix-ui/themes';
|
||||
import { TankPerformancePersistent } from '../../stores/tankPerformancePersistent';
|
||||
import { PlayerCountPeriod } from '../../stores/tankPerformancePersistent/constants';
|
||||
|
||||
export function PlayerCountControl() {
|
||||
const playerCountPeriod = TankPerformancePersistent.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const mutateTankPerformanceSort = TankPerformancePersistent.useMutation();
|
||||
|
||||
return (
|
||||
<Flex justify="center" align="center" gap="2">
|
||||
<Text>Player count</Text>
|
||||
<Select.Root
|
||||
value={playerCountPeriod}
|
||||
onValueChange={(period) => {
|
||||
mutateTankPerformanceSort((draft) => {
|
||||
draft.playerCountPeriod = period as PlayerCountPeriod;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Item value={PlayerCountPeriod.Past120Days}>
|
||||
Past 120 days
|
||||
</Select.Item>
|
||||
<Select.Item value={PlayerCountPeriod.Past90Days}>
|
||||
Past 90 days
|
||||
</Select.Item>
|
||||
<Select.Item value={PlayerCountPeriod.Past60Days}>
|
||||
Past 60 days
|
||||
</Select.Item>
|
||||
<Select.Item value={PlayerCountPeriod.ThisMonth}>
|
||||
This month
|
||||
</Select.Item>
|
||||
<Select.Item value={PlayerCountPeriod.ThisWeek}>
|
||||
This week
|
||||
</Select.Item>
|
||||
<Select.Item value={PlayerCountPeriod.Yesterday}>
|
||||
Yesterday
|
||||
</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Skeleton, Table } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useIntersection } from '../../hooks/useIntersection';
|
||||
import { TankPerformanceSortTypeNamesArray } from '../../stores/tankPerformanceSort/constants';
|
||||
|
||||
interface RowLoaderProps {
|
||||
onIntersection?: () => void;
|
||||
}
|
||||
|
||||
export const RowLoader = memo<RowLoaderProps>(({ onIntersection }) => {
|
||||
const row = useRef<HTMLTableRowElement>(null);
|
||||
|
||||
useIntersection(() => onIntersection?.(), row, {
|
||||
disabled: onIntersection === undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<Table.Row ref={row}>
|
||||
<Table.RowHeaderCell align="center">
|
||||
<Skeleton height="100%" width="128px" />
|
||||
</Table.RowHeaderCell>
|
||||
|
||||
{times(TankPerformanceSortTypeNamesArray.length, (index) => (
|
||||
<Table.Cell key={index} align="center">
|
||||
<Skeleton height="1em" width="32px" />
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import { Suspense } from 'react';
|
||||
import { StickyTableRoot } from '../StickyTableRoot';
|
||||
import { BodyLoader } from './BodyLoader';
|
||||
import { Header } from './Header';
|
||||
import { Tanks } from './Tanks';
|
||||
|
||||
export function TankTable() {
|
||||
return (
|
||||
<Flex justify="center">
|
||||
<StickyTableRoot
|
||||
size={{ initial: '1', sm: '2' }}
|
||||
variant="surface"
|
||||
style={{ maxWidth: '100%', maxHeight: 'calc(100vh - 256px)' }}
|
||||
>
|
||||
<Header />
|
||||
<Suspense fallback={<BodyLoader />}>
|
||||
<Tanks />
|
||||
</Suspense>
|
||||
</StickyTableRoot>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
TankDefinition,
|
||||
fetchAverageDefinitions,
|
||||
formatCompact,
|
||||
} from '@blitzkit/core';
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import { memo } from 'react';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { TankPerformancePersistent } from '../../stores/tankPerformancePersistent';
|
||||
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
||||
|
||||
interface TankRowProps {
|
||||
tank: TankDefinition;
|
||||
}
|
||||
|
||||
const averageDefinitions = await fetchAverageDefinitions();
|
||||
|
||||
export const TankRow = memo<TankRowProps>(
|
||||
({ tank }) => {
|
||||
const averages = averageDefinitions.averages[tank.id];
|
||||
const ratio = useAveragesExclusionRatio();
|
||||
const playerCountPeriod = TankPerformancePersistent.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
|
||||
return (
|
||||
<Table.Row>
|
||||
<TankRowHeaderCell tank={tank} />
|
||||
|
||||
<Table.Cell align="center">
|
||||
{((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{formatCompact(
|
||||
Math.round(ratio * averages.samples[playerCountPeriod]),
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(
|
||||
averages.mu.damage_dealt / averages.mu.battles,
|
||||
).toLocaleString()}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(
|
||||
(averages.mu.survived_battles / averages.mu.battles) * 100,
|
||||
)}
|
||||
%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString()}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.spotted / averages.mu.battles).toFixed(2)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.shots / averages.mu.battles).toFixed(1)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.hits / averages.mu.battles).toFixed(1)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(
|
||||
averages.mu.damage_received / averages.mu.battles,
|
||||
).toLocaleString()}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
},
|
||||
(prev, next) => prev.tank.id === next.tank.id,
|
||||
);
|
||||
@@ -0,0 +1,156 @@
|
||||
import { fetchAverageDefinitions, fetchTankDefinitions } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TankPerformancePersistent } from '../../stores/tankPerformancePersistent';
|
||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
||||
import { RowLoader } from './RowLoader';
|
||||
import { TankRow } from './TankRow';
|
||||
import { Total } from './Total';
|
||||
|
||||
const PREVIEW_COUNT = 10;
|
||||
const DEFAULT_LOADED_ROWS = 25;
|
||||
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
const averageDefinitionsArray = await fetchAverageDefinitions().then(
|
||||
({ averages }) =>
|
||||
Object.entries(averages).map(([id, average]) => ({
|
||||
id: Number(id),
|
||||
...average,
|
||||
})),
|
||||
);
|
||||
|
||||
export function Tanks() {
|
||||
const sort = TankPerformanceSort.use();
|
||||
const tankPerformancePersistentStore = TankPerformancePersistent.useStore();
|
||||
const filters = useStore($tankFilters);
|
||||
const tanksSorted = useMemo(() => {
|
||||
const { playerCountPeriod } = tankPerformancePersistentStore.getState();
|
||||
|
||||
switch (sort.type) {
|
||||
case 'accuracy':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots),
|
||||
);
|
||||
case 'capturePoints':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.capture_points / a.mu.battles -
|
||||
b.mu.capture_points / b.mu.battles),
|
||||
);
|
||||
case 'damage':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_dealt / a.mu.battles -
|
||||
b.mu.damage_dealt / b.mu.battles),
|
||||
);
|
||||
case 'damageRatio':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_dealt / a.mu.damage_received -
|
||||
b.mu.damage_dealt / b.mu.damage_received),
|
||||
);
|
||||
case 'damageTaken':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_received / a.mu.battles -
|
||||
b.mu.damage_received / b.mu.battles),
|
||||
);
|
||||
case 'hits':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles),
|
||||
);
|
||||
case 'kills':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles),
|
||||
);
|
||||
case 'shots':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles),
|
||||
);
|
||||
case 'players':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod]),
|
||||
);
|
||||
case 'spots':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles),
|
||||
);
|
||||
case 'survival':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.survived_battles / a.mu.battles -
|
||||
b.mu.survived_battles / b.mu.battles),
|
||||
);
|
||||
case 'winrate':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles),
|
||||
);
|
||||
case 'xp':
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles),
|
||||
);
|
||||
}
|
||||
}, [sort]);
|
||||
const tanks = useMemo(() => {
|
||||
return tanksSorted.filter((tank) =>
|
||||
filterTank(filters, tankDefinitions.tanks[tank.id]),
|
||||
);
|
||||
}, [filters, sort]);
|
||||
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadedRows(DEFAULT_LOADED_ROWS);
|
||||
}, [filters, sort]);
|
||||
|
||||
return (
|
||||
<Table.Body>
|
||||
<Total tanks={tanks} />
|
||||
|
||||
{tanks.slice(0, loadedRows).map((averages) => {
|
||||
const tank = tankDefinitions.tanks[averages.id];
|
||||
|
||||
if (tank === undefined) return null;
|
||||
|
||||
return (
|
||||
<Suspense key={tank.id} fallback={<RowLoader />}>
|
||||
<TankRow tank={tank} />
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
|
||||
{times(Math.min(PREVIEW_COUNT, tanks.length - loadedRows), (index) => {
|
||||
return (
|
||||
<RowLoader
|
||||
key={index}
|
||||
onIntersection={() => {
|
||||
setLoadedRows((state) => Math.min(state + 2, tanks.length));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
AverageDefinitionsEntry,
|
||||
fetchAverageDefinitions,
|
||||
formatCompact,
|
||||
type BlitzkitStats,
|
||||
} from '@blitzkit/core';
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { TankPerformancePersistent } from '../../stores/tankPerformancePersistent';
|
||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
||||
|
||||
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface TotalProps {
|
||||
tanks: AverageDefinitionsEntryWithId[];
|
||||
}
|
||||
|
||||
const averageDefinitions = await fetchAverageDefinitions();
|
||||
|
||||
export const Total = memo<TotalProps>(
|
||||
({ tanks }) => {
|
||||
const playerCountPeriod = TankPerformancePersistent.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const ratio = useAveragesExclusionRatio();
|
||||
const sum = useCallback(
|
||||
(slice: (tank: BlitzkitStats) => number) => {
|
||||
return tanks.reduce(
|
||||
(acc, tank) => acc + tank.samples.total * slice(tank.mu),
|
||||
0,
|
||||
);
|
||||
},
|
||||
[tanks],
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<Table.Row>
|
||||
<StickyRowHeaderCell style={{ overflow: 'hidden', display: 'flex' }}>
|
||||
Total
|
||||
</StickyRowHeaderCell>
|
||||
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{formatCompact(ratio * averageDefinitions.samples[playerCountPeriod])}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(damage).toLocaleString()}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(survival * 100).toFixed(1)}%
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">
|
||||
{Math.round(xp).toLocaleString()}
|
||||
</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()}
|
||||
</Table.Cell>
|
||||
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
},
|
||||
(prev, next) => prev.tanks === next.tanks,
|
||||
);
|
||||
Reference in new issue
Block a user