mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: career stats
This commit is contained in:
1 parent
8c041d4eef
commit
d801447e5d
6 files changed
+252
-121
No files matched your search
+246
-115
@@ -8,7 +8,7 @@ import { Button } from '../../../components/Button';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { SearchBar } from '../../../components/SearchBar';
|
||||
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 calculateWN8 from '../../../core/blitz/calculateWN8';
|
||||
import { diffNormalizedTankStats } from '../../../core/blitz/diffNormalizedTankStats';
|
||||
@@ -28,6 +28,9 @@ import {
|
||||
} from '../../../types/tanksStats';
|
||||
import * as styles from './page.css';
|
||||
|
||||
// 1 once per 5 seconds
|
||||
const REFRESH_RATE = 1 / 5;
|
||||
|
||||
export default function Page() {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
@@ -45,63 +48,90 @@ export default function Page() {
|
||||
500,
|
||||
);
|
||||
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<
|
||||
| {
|
||||
stats: IndividualTankStats;
|
||||
tankopedia?: TankopediaEntry;
|
||||
career: IndividualTankStats;
|
||||
currentWN8?: number;
|
||||
careerWN8?: number;
|
||||
}[]
|
||||
careerWN8: number;
|
||||
|
||||
list: {
|
||||
stats: IndividualTankStats;
|
||||
tankopedia?: TankopediaEntry;
|
||||
career: IndividualTankStats;
|
||||
currentWN8?: number;
|
||||
careerWN8?: number;
|
||||
}[];
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (session.isTracking) {
|
||||
const { id, region } = session;
|
||||
const careerRaw = await getWargamingResponse<TanksStats>(
|
||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
async function recalculateDiff() {
|
||||
if (session.isTracking) {
|
||||
const { id, region } = session;
|
||||
const careerRaw = await getWargamingResponse<TanksStats>(
|
||||
`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(
|
||||
(
|
||||
Object.values(
|
||||
diffNormalizedTankStats(session.tankStats, career),
|
||||
) as IndividualTankStats[]
|
||||
).map((stats) => {
|
||||
return {
|
||||
stats,
|
||||
tankopedia: awaitedTankopedia[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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
})();
|
||||
setDiff({
|
||||
careerWN8,
|
||||
|
||||
list: (
|
||||
Object.values(
|
||||
diffNormalizedTankStats(session.tankStats, career),
|
||||
) as IndividualTankStats[]
|
||||
).map((stats) => {
|
||||
return {
|
||||
stats,
|
||||
tankopedia: awaitedTankopedia[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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
async function setSession(region: Region, id: number, nickname: string) {}
|
||||
|
||||
useEffect(() => {
|
||||
recalculateDiff();
|
||||
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -135,7 +165,6 @@ export default function Page() {
|
||||
marginTop: 8,
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.colors.componentNonInteractive,
|
||||
border: theme.borderStyles.nonInteractive,
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
boxSizing: 'border-box',
|
||||
@@ -244,69 +273,171 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<Breakdown.Root>
|
||||
{diff &&
|
||||
diff
|
||||
.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;
|
||||
})()}
|
||||
rows={[
|
||||
{
|
||||
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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
{session.isTracking && since !== undefined && diff !== undefined && (
|
||||
<Breakdown.Row
|
||||
title={`Since ${since.toLocaleString(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
day: 'numeric',
|
||||
})}`}
|
||||
stats={[
|
||||
{
|
||||
title: 'Battles',
|
||||
current: diff.list
|
||||
.reduce(
|
||||
(accumulator, { stats }) => accumulator + stats.all.battles,
|
||||
0,
|
||||
)
|
||||
.toLocaleString(),
|
||||
career: tankStatsArray
|
||||
.reduce(
|
||||
(accumulator, stats) => accumulator + stats.all.battles,
|
||||
0,
|
||||
)
|
||||
.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'Winrate',
|
||||
current: `${(
|
||||
100 *
|
||||
(diff.list.reduce(
|
||||
(accumulator, { stats }) => accumulator + stats.all.wins,
|
||||
0,
|
||||
) /
|
||||
diff.list.reduce(
|
||||
(accumulator, { stats }) =>
|
||||
accumulator + stats.all.battles,
|
||||
0,
|
||||
))
|
||||
).toFixed(2)}%`,
|
||||
career: `${(
|
||||
100 *
|
||||
(tankStatsArray.reduce(
|
||||
(accumulator, stats) => accumulator + stats.all.wins,
|
||||
0,
|
||||
) /
|
||||
tankStatsArray.reduce(
|
||||
(accumulator, stats) => accumulator + stats.all.battles,
|
||||
0,
|
||||
))
|
||||
).toFixed(2)}%`,
|
||||
},
|
||||
(() => {
|
||||
const currentWN8 =
|
||||
diff.list.reduce(
|
||||
(accumulator, { currentWN8, stats }) =>
|
||||
stats.all.battles * (currentWN8 ?? 0) + accumulator,
|
||||
0,
|
||||
) /
|
||||
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>
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -5,13 +5,13 @@ import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
||||
import { RowStat } from './RowStat';
|
||||
|
||||
interface RowProps {
|
||||
type: 'tank' | 'summary';
|
||||
type?: 'tank' | 'summary';
|
||||
title: string;
|
||||
minimized?: boolean;
|
||||
treeType?: TreeTypeEnum;
|
||||
tankType?: TankType;
|
||||
|
||||
rows: {
|
||||
stats: {
|
||||
title: string;
|
||||
current?: number | string;
|
||||
career?: number | string;
|
||||
@@ -77,7 +77,7 @@ export function Row(props: RowProps) {
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
{props.rows.map((row, index) => (
|
||||
{props.stats.map((row, index) => (
|
||||
<RowStat
|
||||
minimized={props.minimized}
|
||||
key={index}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Button({ color, style, ...props }: ButtonProps) {
|
||||
backgroundColor:
|
||||
theme.colors[`componentInteractive${BUTTON_COLORS[color]}`],
|
||||
borderRadius: 4,
|
||||
border: theme.borderStyles[`interactive${BUTTON_COLORS[color]}`],
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 8,
|
||||
display: 'flex',
|
||||
|
||||
@@ -14,7 +14,6 @@ export const SearchBar = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
border: theme.borderStyles.interactive,
|
||||
backgroundColor: theme.colors.componentInteractive,
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
|
||||
@@ -138,7 +138,7 @@ export default async function today(
|
||||
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||
title={showTotal && id === 0 ? 'Total' : await resolveTankName(id)}
|
||||
minimized={showTotal ? index > maximized : index + 1 > maximized}
|
||||
rows={[
|
||||
stats={[
|
||||
{
|
||||
title: 'Battles',
|
||||
current: tankStats.battles.toLocaleString(),
|
||||
|
||||
@@ -14,6 +14,7 @@ type Session =
|
||||
id: number;
|
||||
nickname: string;
|
||||
tankStats: NormalizedTankStats;
|
||||
time: number;
|
||||
};
|
||||
|
||||
export const useSession = create<Session>()(
|
||||
|
||||
Reference in new issue
Block a user