mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
520 lines
17 KiB
TypeScript
520 lines
17 KiB
TypeScript
import { SlashCommandSubcommandBuilder } from 'discord.js';
|
|
import markdownEscape from 'markdown-escape';
|
|
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
|
|
import CommandWrapper from '../components/CommandWrapper';
|
|
import { DeltaCaret } from '../components/DeltaCaret';
|
|
import TitleBar from '../components/TitleBar';
|
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
|
import { getLeagueFromScore } from '../core/blitz/getLeagueFromScore';
|
|
import getRatingInfo from '../core/blitz/getRatingInfo';
|
|
import { getRatingNeighbors } from '../core/blitz/getRatingNeighbors';
|
|
import { normalizeLeagueIcon } from '../core/blitz/normalizeLeagueIcon';
|
|
import { emblemURL } from '../core/blitzkit/emblemURL';
|
|
import getArchivedRatingMidnightLeaderboard from '../core/blitzkit/getArchivedRatingMidnightLeaderboard';
|
|
import { iconPng } from '../core/blitzkit/iconPng';
|
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
|
import { translator } from '../core/localization/translator';
|
|
import { deltaBkrlBlitzStats } from '../core/statistics/deltaBkrlBlitzStats';
|
|
import { BkrlFormat } from '../core/streams/bkrl';
|
|
import { CommandRegistry } from '../events/interactionCreate';
|
|
import { theme } from '../stitches.config';
|
|
|
|
const LEAGUE_ACCENT = [
|
|
'_purple',
|
|
'_sand',
|
|
'_amber',
|
|
'_mauve',
|
|
'_brown',
|
|
] as const;
|
|
|
|
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
|
|
resolve({
|
|
command: createLocalizedCommand('rating', [
|
|
{
|
|
subcommand: 'today',
|
|
modify(option: SlashCommandSubcommandBuilder) {
|
|
option.addStringOption(addUsernameChoices);
|
|
},
|
|
},
|
|
{
|
|
subcommand: 'season',
|
|
modify(option: SlashCommandSubcommandBuilder) {
|
|
option.addStringOption(addUsernameChoices);
|
|
},
|
|
},
|
|
]),
|
|
|
|
async handler(interaction) {
|
|
const subcommand = interaction.options.getSubcommand(true) as
|
|
| 'season'
|
|
| 'today';
|
|
const { t, translate } = translator(interaction.locale);
|
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
|
|
|
const ratingInfo = await getRatingInfo(region);
|
|
|
|
if (ratingInfo.detail) {
|
|
throw new Error(`Rating info not available for ${region}`);
|
|
}
|
|
|
|
const leaderboard = await getArchivedRatingMidnightLeaderboard(
|
|
region,
|
|
ratingInfo.current_season - (subcommand === 'today' ? 0 : 1),
|
|
);
|
|
|
|
if (leaderboard?.format === BkrlFormat.Base) {
|
|
throw new Error(
|
|
'Encountered bkrl minimal format in latest season. Wait till next cron?',
|
|
);
|
|
}
|
|
|
|
const accountInfo = await getAccountInfo(region, id, [
|
|
'statistics.rating',
|
|
]);
|
|
const statsIndex = leaderboard?.entries.findIndex(
|
|
(entry) => entry.id === id,
|
|
);
|
|
const statsA = leaderboard ? leaderboard.entries[statsIndex!] : null;
|
|
const positionA = leaderboard ? statsIndex! + 1 : undefined;
|
|
const statsB1 = accountInfo.statistics.rating!;
|
|
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
|
|
|
|
if (statsB2Array === undefined) {
|
|
return translate('bot.commands.rating.errors.no_participation', [
|
|
markdownEscape(accountInfo.nickname),
|
|
]);
|
|
}
|
|
|
|
const [statsB2] = statsB2Array;
|
|
const league = getLeagueFromScore(statsB2.score);
|
|
|
|
if (!statsA) {
|
|
return t`bot.commands.rating.errors.unrecorded_stats`;
|
|
}
|
|
if (!statsB1) {
|
|
return translate('bot.commands.rating.errors.no_participation', [
|
|
markdownEscape(accountInfo.nickname),
|
|
]);
|
|
}
|
|
|
|
const reward = ratingInfo.rewards.find(
|
|
(reward) =>
|
|
statsB2.number >= reward.from_position &&
|
|
statsB2.number <= reward.to_position,
|
|
);
|
|
const positionDelta = leaderboard
|
|
? statsB2.number - positionA!
|
|
: undefined;
|
|
const delta = deltaBkrlBlitzStats(
|
|
statsA,
|
|
accountInfo.statistics.rating!,
|
|
statsB2,
|
|
);
|
|
const leagueTheme = LEAGUE_ACCENT[league.index];
|
|
|
|
return (
|
|
<CommandWrapper>
|
|
<TitleBar
|
|
title={accountInfo.nickname}
|
|
image={clanImage}
|
|
description={`${translate(`bot.commands.rating.body.subtitle.${subcommand}`)} • ${new Date().toLocaleDateString(
|
|
interaction.locale,
|
|
)}`}
|
|
/>
|
|
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
display: 'flex',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
padding: 16,
|
|
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
|
|
borderRadius: 4,
|
|
border: `1px solid ${theme.colors[`componentInteractive${leagueTheme}`]}`,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
{reward === undefined && (
|
|
<Glow
|
|
endOpacity={3 / 4}
|
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
|
style={{
|
|
transform: 'translateX(-16px) rotate(90deg)',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
gap: 8,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<img
|
|
alt={`League ${league.name}`}
|
|
style={{
|
|
width: 64,
|
|
height: 64,
|
|
objectFit: 'contain',
|
|
}}
|
|
src={normalizeLeagueIcon(
|
|
ratingInfo.leagues[league.index].big_icon,
|
|
)}
|
|
/>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-start',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
color: theme.colors[`textHighContrast${leagueTheme}`],
|
|
fontSize: 24,
|
|
fontWeight: 900,
|
|
}}
|
|
>
|
|
{statsB2.score.toLocaleString(interaction.locale)}
|
|
</span>
|
|
{delta.score !== 0 && (
|
|
<>
|
|
<DeltaCaret delta={delta.score} />
|
|
<span
|
|
style={{
|
|
color:
|
|
delta.score > 0
|
|
? theme.colors.textLowContrast_green
|
|
: theme.colors.textLowContrast_red,
|
|
fontSize: 16,
|
|
}}
|
|
>
|
|
{Math.abs(delta.score).toLocaleString(
|
|
interaction.locale,
|
|
)}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-start',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
color: theme.colors[`textLowContrast${leagueTheme}`],
|
|
fontSize: 16,
|
|
}}
|
|
>
|
|
#{statsB2.number.toLocaleString(interaction.locale)}
|
|
</span>
|
|
{positionDelta !== undefined && positionDelta !== 0 && (
|
|
<>
|
|
<DeltaCaret delta={-positionDelta} />
|
|
<span
|
|
style={{
|
|
color:
|
|
positionDelta > 0
|
|
? theme.colors.textLowContrast_red
|
|
: theme.colors.textLowContrast_green,
|
|
fontSize: 16,
|
|
}}
|
|
>
|
|
{Math.abs(positionDelta).toLocaleString(
|
|
interaction.locale,
|
|
)}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{reward && (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
gap: 8,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-end',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
color: theme.colors[`textHighContrast${leagueTheme}`],
|
|
fontSize: 16,
|
|
maxWidth: 136,
|
|
textAlign: 'right',
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{reward.type === 'vehicle'
|
|
? reward.vehicle.user_string
|
|
: reward.stuff.title}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-end',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
color: theme.colors[`textLowContrast${leagueTheme}`],
|
|
fontSize: 16,
|
|
}}
|
|
>
|
|
Reward
|
|
{reward.type === 'stuff' ? ` x${reward.count}` : ''}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<img
|
|
style={{
|
|
width: 64,
|
|
height: 64,
|
|
objectFit: 'cover',
|
|
}}
|
|
src={await iconPng(
|
|
reward.type === 'vehicle'
|
|
? reward.vehicle.preview_image_url
|
|
: reward.stuff.image_url,
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{reward === undefined && (
|
|
<Glow
|
|
endOpacity={3 / 4}
|
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
|
style={{
|
|
transform: 'translateX(16px) rotate(-90deg)',
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* separated because fragment middle man invalidates relative parent: https://github.com/vercel/satori/issues/605 */}
|
|
{reward && (
|
|
<Glow
|
|
direction="reverse"
|
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
|
style={{
|
|
position: 'absolute',
|
|
top: -40,
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
}}
|
|
/>
|
|
)}
|
|
{reward && (
|
|
<Glow
|
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: -40,
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{delta.battles > 0 && (
|
|
<div style={{ padding: '8px 0', display: 'flex' }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 4,
|
|
flex: 1,
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 24,
|
|
color: theme.colors.textHighContrast,
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
{delta.battles.toLocaleString(interaction.locale)}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 16,
|
|
color: theme.colors.textLowContrast,
|
|
}}
|
|
>
|
|
Battles
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 4,
|
|
flex: 1,
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 24,
|
|
color: theme.colors.textHighContrast,
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 16,
|
|
color: theme.colors.textLowContrast,
|
|
}}
|
|
>
|
|
Winrate
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 4,
|
|
flex: 1,
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 24,
|
|
color: theme.colors.textHighContrast,
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
{Math.round(delta.damage / delta.battles).toLocaleString(
|
|
interaction.locale,
|
|
)}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 16,
|
|
color: theme.colors.textLowContrast,
|
|
}}
|
|
>
|
|
Damage
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 4,
|
|
flex: 1,
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 24,
|
|
color: theme.colors.textHighContrast,
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 16,
|
|
color: theme.colors.textLowContrast,
|
|
}}
|
|
>
|
|
Survival
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 4,
|
|
flex: 1,
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 24,
|
|
color: theme.colors.textHighContrast,
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
{(delta.kills / delta.battles).toFixed(2)}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 16,
|
|
color: theme.colors.textLowContrast,
|
|
}}
|
|
>
|
|
Kills
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CommandWrapper>
|
|
);
|
|
},
|
|
});
|
|
});
|