mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
feat: new /stats
This commit is contained in:
1 parent
5fb36352d2
commit
64fd15c0a5
11 files changed
+510
-3
No files matched your search
@@ -11,7 +11,7 @@ import { StatType } from '../renderers/statsfull.js';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
inDevelopment: false,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: addStatTypeSubCommandGroups(
|
command: addStatTypeSubCommandGroups(
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { CYCLIC_API } from '../constants/cyclic.js';
|
||||||
|
import resolveTankId from '../core/blitz/resolveTankId.js';
|
||||||
|
import { Period } from '../core/discord/addPeriodSubCommands.js';
|
||||||
|
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
|
||||||
|
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
||||||
|
import interactionToURL from '../core/discord/interactionToURL.js';
|
||||||
|
import linkButton from '../core/discord/linkButton.js';
|
||||||
|
import primaryButton from '../core/discord/primaryButton.js';
|
||||||
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
|
||||||
|
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
|
||||||
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
||||||
|
import stats from '../renderers/stats.js';
|
||||||
|
import statsfull, { StatType } from '../renderers/statsfull.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inProduction: true,
|
||||||
|
inDevelopment: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: addStatTypeSubCommandGroups(
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('stats')
|
||||||
|
.setDescription('In-game statistics'),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const commandGroup = interaction.options.getSubcommandGroup(
|
||||||
|
true,
|
||||||
|
) as StatType;
|
||||||
|
const subcommand = interaction.options.getSubcommand() as Period;
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(interaction);
|
||||||
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
|
const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : 0;
|
||||||
|
const start = interaction.options.getInteger('start');
|
||||||
|
const end = interaction.options.getInteger('end');
|
||||||
|
const path = `stats/${commandGroup}/${subcommand}?server=${
|
||||||
|
player.server
|
||||||
|
}&id=${player.id}&tankId=${tankId}&start=${start ?? 0}&end=${end ?? 0}`;
|
||||||
|
|
||||||
|
interactionToURL(interaction);
|
||||||
|
|
||||||
|
return [
|
||||||
|
await stats(commandGroup, period, player, tankId),
|
||||||
|
primaryButton(path, 'Refresh'),
|
||||||
|
linkButton(path, 'Embed'),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||||
|
const path = url.pathname.split('/').filter(Boolean);
|
||||||
|
const commandGroup = path[1] as StatType;
|
||||||
|
const period = resolvePeriodFromButton(interaction);
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
|
||||||
|
return await statsfull(
|
||||||
|
commandGroup,
|
||||||
|
period,
|
||||||
|
player,
|
||||||
|
parseInt(url.searchParams.get('tankId')!),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} satisfies CommandRegistry;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Percentile } from '../../../../../constants/percentiles.js';
|
||||||
|
import hslToHex from '../../../../../core/node/hslToHex.js';
|
||||||
|
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator.js';
|
||||||
|
|
||||||
|
export enum GlowSide {
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlowProps {
|
||||||
|
percentile: Percentile;
|
||||||
|
side: GlowSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Glow({ percentile, side }: GlowProps) {
|
||||||
|
const color = PERCENTILE_COLORS[percentile];
|
||||||
|
const color1 = hslToHex(color);
|
||||||
|
const color2 = `${color1}00`;
|
||||||
|
|
||||||
|
const background = `linear-gradient(${
|
||||||
|
side === GlowSide.Top ? 0 : 180
|
||||||
|
}deg, ${color1} 0%, ${color2} 100%)`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: side === GlowSide.Top ? 'flex-end' : 'flex-start',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ height: 54, width: 16, background }} />
|
||||||
|
<div style={{ height: 72, width: 16, background }} />
|
||||||
|
<div style={{ height: 80, width: 16, background }} />
|
||||||
|
<div style={{ height: 72, width: 16, background }} />
|
||||||
|
<div style={{ height: 54, width: 16, background }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Percentile } from '../../../../constants/percentiles.js';
|
||||||
|
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile.js';
|
||||||
|
import { theme } from '../../../../stitches.config.js';
|
||||||
|
import { PERCENTILE_COLORS } from '../../../PercentileIndicator.js';
|
||||||
|
import { Glow, GlowSide } from './components/Glow.js';
|
||||||
|
|
||||||
|
export interface WN8DisplayProps {
|
||||||
|
WN8?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PERCENTILE_NAMES = {
|
||||||
|
[Percentile.VeryBad]: 'Very Bad',
|
||||||
|
[Percentile.Bad]: 'Bad',
|
||||||
|
[Percentile.BelowAverage]: 'Below Average',
|
||||||
|
[Percentile.Average]: 'Average',
|
||||||
|
[Percentile.AboveAverage]: 'Above Average',
|
||||||
|
[Percentile.Good]: 'Good',
|
||||||
|
[Percentile.VeryGood]: 'Very Good',
|
||||||
|
[Percentile.Great]: 'Great',
|
||||||
|
[Percentile.Unicum]: 'Unicum',
|
||||||
|
[Percentile.SuperUnicum]: 'Super Unicum',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WN8Display({ WN8 }: WN8DisplayProps) {
|
||||||
|
const percentile =
|
||||||
|
WN8 === undefined ? Percentile.VeryBad : getWN8Percentile(WN8);
|
||||||
|
const color = PERCENTILE_COLORS[percentile];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 128 * 2, // idk why twice the width turns into the correct width
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: -16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glow percentile={percentile} side={GlowSide.Top} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 128,
|
||||||
|
height: 80,
|
||||||
|
backgroundColor: color,
|
||||||
|
borderRadius: 40,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: -4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: 900,
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{WN8 === undefined ? '--' : WN8.toFixed(0)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{PERCENTILE_NAMES[percentile]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Glow percentile={percentile} side={GlowSide.Bottom} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { theme } from '../../../stitches.config.js';
|
||||||
|
|
||||||
|
export enum WN8SurroundingStatAlign {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WN8SurroundingStatProps {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
align?: WN8SurroundingStatAlign;
|
||||||
|
padded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WN8SurroundingStat({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
align = WN8SurroundingStatAlign.Left,
|
||||||
|
padded = false,
|
||||||
|
}: WN8SurroundingStatProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems:
|
||||||
|
align === WN8SurroundingStatAlign.Left ? 'flex-start' : 'flex-end',
|
||||||
|
paddingLeft: padded && align === WN8SurroundingStatAlign.Left ? 32 : 0,
|
||||||
|
paddingRight:
|
||||||
|
padded && align === WN8SurroundingStatAlign.Right ? 32 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { AllStats, SupplementaryStats } from '../../types/accountInfo.js';
|
||||||
|
import { WN8Display } from './components/WN8Display/index.js';
|
||||||
|
import {
|
||||||
|
WN8SurroundingStat,
|
||||||
|
WN8SurroundingStatAlign,
|
||||||
|
} from './components/WN8SurroundingStat.js';
|
||||||
|
|
||||||
|
export interface AllStatsOverviewProps {
|
||||||
|
stats: AllStats;
|
||||||
|
supplementaryStats: SupplementaryStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AllStatsOverview({
|
||||||
|
stats,
|
||||||
|
supplementaryStats,
|
||||||
|
}: AllStatsOverviewProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: -64,
|
||||||
|
// alignItems: 'center',
|
||||||
|
// justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Battles"
|
||||||
|
value={stats.battles.toLocaleString()}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Winrate"
|
||||||
|
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Damage"
|
||||||
|
value={Math.round(
|
||||||
|
stats.damage_dealt / stats.battles,
|
||||||
|
).toLocaleString()}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Dmg. ratio"
|
||||||
|
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WN8Display WN8={supplementaryStats.WN8} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Tier"
|
||||||
|
value={supplementaryStats.tier.toFixed(2)}
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Survival"
|
||||||
|
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
||||||
|
2,
|
||||||
|
)}%`}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Accuracy"
|
||||||
|
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label="Kill ratio"
|
||||||
|
value={(
|
||||||
|
stats.frags /
|
||||||
|
(stats.battles - stats.survived_battles)
|
||||||
|
).toFixed(2)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const numberRegex = /\d+(\.\d+)?/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thanks to icl7126!
|
||||||
|
* https://stackoverflow.com/a/44134328/12294756
|
||||||
|
*/
|
||||||
|
export default function hslToHex(hsl: string) {
|
||||||
|
let [h, s, l] = Array.from(hsl.matchAll(numberRegex)).map((m) =>
|
||||||
|
parseFloat(m[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
l /= 100;
|
||||||
|
const a = (s * Math.min(l, 1 - l)) / 100;
|
||||||
|
const f = (n: number) => {
|
||||||
|
const k = (n + h / 30) % 12;
|
||||||
|
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||||
|
return Math.round(255 * color)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, '0'); // convert to Hex and prefix "0" if needed
|
||||||
|
};
|
||||||
|
return `#${f(0)}${f(8)}${f(4)}`;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import playerinfo from '../../commands/playerinfo.js';
|
|||||||
import searchclans from '../../commands/searchclans.js';
|
import searchclans from '../../commands/searchclans.js';
|
||||||
import searchplayers from '../../commands/searchplayers.js';
|
import searchplayers from '../../commands/searchplayers.js';
|
||||||
import searchtanks from '../../commands/searchtanks.js';
|
import searchtanks from '../../commands/searchtanks.js';
|
||||||
|
import stats from '../../commands/stats.js';
|
||||||
import statsfull from '../../commands/statsfull.js';
|
import statsfull from '../../commands/statsfull.js';
|
||||||
import today from '../../commands/today.js';
|
import today from '../../commands/today.js';
|
||||||
import verify from '../../commands/verify.js';
|
import verify from '../../commands/verify.js';
|
||||||
@@ -86,6 +87,7 @@ export const commands: Record<string, CommandRegistry> = (
|
|||||||
verify,
|
verify,
|
||||||
ping,
|
ping,
|
||||||
evolution,
|
evolution,
|
||||||
|
stats,
|
||||||
] as CommandRegistry[]
|
] as CommandRegistry[]
|
||||||
).reduce((accumulator, registry) => {
|
).reduce((accumulator, registry) => {
|
||||||
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import AllStatsOverview from '../components/AllStatsOverview/index.js';
|
||||||
|
import NoData, { NoDataType } from '../components/NoData.js';
|
||||||
|
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||||
|
import { TierWeightsRecord } from '../components/TierWeights.js';
|
||||||
|
import TitleBar from '../components/TitleBar.js';
|
||||||
|
import Wrapper from '../components/Wrapper.js';
|
||||||
|
import { BLITZ_SERVERS } from '../constants/servers.js';
|
||||||
|
import calculateWN8 from '../core/blitz/calculateWN8.js';
|
||||||
|
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||||
|
import resolveTankName from '../core/blitz/resolveTankName.js';
|
||||||
|
import sumStats from '../core/blitz/sumStats.js';
|
||||||
|
import { Tier, tankopedia } from '../core/blitz/tankopedia.js';
|
||||||
|
import getTankStatsDiffed from '../core/blitzstars/getTankStatsDiffed.js';
|
||||||
|
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||||
|
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand.js';
|
||||||
|
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand.js';
|
||||||
|
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
||||||
|
import {
|
||||||
|
AccountInfo,
|
||||||
|
AllStats,
|
||||||
|
SupplementaryStats,
|
||||||
|
} from '../types/accountInfo.js';
|
||||||
|
import { PlayerClanData } from '../types/playerClanData.js';
|
||||||
|
|
||||||
|
export type StatType = 'player' | 'tank';
|
||||||
|
|
||||||
|
export default async function stats<Type extends StatType>(
|
||||||
|
type: Type,
|
||||||
|
{ start, end, statsName }: ResolvedPeriod,
|
||||||
|
{ server, id }: ResolvedPlayer,
|
||||||
|
tankId: Type extends 'tank' ? number : never,
|
||||||
|
) {
|
||||||
|
let nameDiscriminator: string | undefined;
|
||||||
|
let image: string | undefined;
|
||||||
|
|
||||||
|
if (type === 'player') {
|
||||||
|
const clan = (
|
||||||
|
await getWargamingResponse<PlayerClanData>(
|
||||||
|
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||||
|
)
|
||||||
|
)[id]?.clan;
|
||||||
|
|
||||||
|
if (clan) nameDiscriminator = `[${clan.tag}]`;
|
||||||
|
image = clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
|
: undefined;
|
||||||
|
} else if (type === 'tank') {
|
||||||
|
nameDiscriminator = `(${resolveTankName(tankId)})`;
|
||||||
|
image = tankopedia[tankId].images.normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tankStats = await getTankStatsDiffed(server, id, start, end);
|
||||||
|
let stats: AllStats;
|
||||||
|
let supplementaryStats: SupplementaryStats;
|
||||||
|
let tierWeights: TierWeightsRecord;
|
||||||
|
|
||||||
|
if (type === 'player') {
|
||||||
|
const entries = Object.entries(tankStats);
|
||||||
|
stats = sumStats(entries.map(([, stats]) => stats));
|
||||||
|
const battlesOfTanksWithAverages = entries.reduce(
|
||||||
|
(accumulator, [tankIdString, stats]) => {
|
||||||
|
const tankId = parseInt(tankIdString);
|
||||||
|
const tankAverage = tankAverages[tankId];
|
||||||
|
|
||||||
|
return tankAverage ? accumulator + stats.battles : accumulator;
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const battlesOfTanksWithTankopediaEntry = entries.reduce(
|
||||||
|
(accumulator, [tankIdString, stats]) => {
|
||||||
|
const tankId = parseInt(tankIdString);
|
||||||
|
const tankopediaEntry = tankopedia[tankId];
|
||||||
|
|
||||||
|
return tankopediaEntry ? accumulator + stats.battles : accumulator;
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
supplementaryStats = {
|
||||||
|
WN8:
|
||||||
|
entries.reduce((accumulator, [tankIdString, stats]) => {
|
||||||
|
const tankId = parseInt(tankIdString);
|
||||||
|
const tankAverage = tankAverages[tankId];
|
||||||
|
|
||||||
|
return tankAverage
|
||||||
|
? accumulator + calculateWN8(tankAverage.all, stats) * stats.battles
|
||||||
|
: accumulator;
|
||||||
|
}, 0) / battlesOfTanksWithAverages,
|
||||||
|
tier:
|
||||||
|
entries.reduce((accumulator, [tankIdString, stats]) => {
|
||||||
|
const tankId = parseInt(tankIdString);
|
||||||
|
const tankopediaEntry = tankopedia[tankId];
|
||||||
|
|
||||||
|
return tankopediaEntry
|
||||||
|
? accumulator + tankopediaEntry.tier * stats.battles
|
||||||
|
: accumulator;
|
||||||
|
}, 0) / battlesOfTanksWithTankopediaEntry,
|
||||||
|
};
|
||||||
|
tierWeights = entries.reduce<TierWeightsRecord>(
|
||||||
|
(accumulator, [tankIdString, stats]) => {
|
||||||
|
const tankId = parseInt(tankIdString);
|
||||||
|
const tier = tankopedia[tankId].tier as Tier;
|
||||||
|
|
||||||
|
if (accumulator[tier]) {
|
||||||
|
accumulator[tier]! += stats.battles;
|
||||||
|
} else {
|
||||||
|
accumulator[tier] = stats.battles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accumulator;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
stats = tankStats[tankId];
|
||||||
|
|
||||||
|
supplementaryStats = {
|
||||||
|
WN8: calculateWN8(tankAverages[tankId].all, tankStats[tankId]),
|
||||||
|
tier: tankopedia[tankId].tier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||||
|
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={accountInfo[id].nickname}
|
||||||
|
nameDiscriminator={nameDiscriminator}
|
||||||
|
image={image}
|
||||||
|
description={`${statsName} • ${new Date().toDateString()} • ${
|
||||||
|
BLITZ_SERVERS[server]
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<AllStatsOverview
|
||||||
|
stats={stats}
|
||||||
|
supplementaryStats={supplementaryStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PoweredByBlitzStars />
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { RouterRegistry } from '../server.js';
|
|||||||
export default {
|
export default {
|
||||||
route: '/stats/:type/:period',
|
route: '/stats/:type/:period',
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
inDevelopment: false,
|
||||||
|
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
const type = req.params.type as StatType;
|
const type = req.params.type as StatType;
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
// "moduleResolution": "Node", TODO: implement Node module resolution
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "nodenext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
@@ -11,5 +12,5 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts", "src/components/AllStatsOverview/components/WN8SurroundingStat.tsx"]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user