Files
blitzkit/packages/bot/src/commands/replay.tsx
T

577 lines
15 KiB
TypeScript
Raw Normal View History

2024-09-07 21:45:00 -05:00
import {
2024-09-08 10:11:24 -05:00
calculateWN8,
2024-09-08 10:42:34 -05:00
emblemURL,
2024-09-08 10:17:40 -05:00
getClanAccountInfo,
2024-09-07 21:45:00 -05:00
idToRegion,
2024-09-08 10:11:24 -05:00
tankIcon,
2024-09-07 21:45:00 -05:00
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
2025-01-14 19:22:41 -06:00
import { Locale } from 'discord.js';
2024-09-14 19:43:14 -05:00
import { clamp } from 'lodash-es';
2024-09-08 10:11:24 -05:00
import { CommandWrapper } from '../components/CommandWrapper';
import { TitleBar } from '../components/TitleBar';
2024-09-08 10:28:54 -05:00
import { iconPng } from '../core/blitzkit/iconPng';
2024-10-05 23:20:04 -05:00
import {
mapDefinitions,
tankDefinitions,
} from '../core/blitzkit/nonBlockingPromises';
2024-09-08 10:28:54 -05:00
import { RenderConfiguration } from '../core/blitzkit/renderConfiguration';
2024-10-05 23:20:04 -05:00
import { blitzStarsTankAverages } from '../core/blitzstars/tankAverages';
2024-09-08 10:11:24 -05:00
import { buttonLink } from '../core/discord/buttonLink';
2024-04-23 16:07:59 -05:00
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
2025-01-14 19:22:41 -06:00
import { translator } from '../core/localization/translator';
2024-04-23 16:07:59 -05:00
import { CommandRegistry } from '../events/interactionCreate';
2024-04-24 16:22:48 -05:00
import { theme } from '../stitches.config';
2024-04-23 16:07:59 -05:00
2024-04-24 16:22:48 -05:00
type WotInspectorReplaySubmission =
2024-04-23 16:07:59 -05:00
| { status: 'error'; data: unknown; error: { message: string } }
| {
status: 'ok';
data: {
download_url: string;
view_url: string;
summary: {
battle_start_time: string;
map_name: string;
battle_start_timestamp: number;
battle_duration: number;
title: string;
arena_unique_id: string;
allies: number[];
enemies: number[];
protagonist: number;
player_name: string;
vehicle: string;
exp_base: number;
exp_total: number;
credits_base: number;
credits_total: number;
room_type: number;
battle_type: number;
mastery_badge: number;
battle_result: number;
vehicle_tier: number;
vehicle_type: number;
protagonist_team: number;
winner_team: number;
details: {
achievements: unknown[];
base_capture_points: number;
base_defend_points: number;
chassis_id: unknown | null;
clan_tag: string;
clanid: number;
credits: number;
damage_assisted: number;
damage_assisted_track: number;
damage_blocked: unknown | null;
damage_made: number;
damage_received: number;
dbid: number;
death_reason: number;
distance_travelled: number;
enemies_damaged: number;
enemies_destroyed: number;
enemies_spotted: number;
entity_id: number;
exp: number;
exp_for_assist: number;
exp_for_damage: number;
exp_team_bonus: number;
gun_id: unknown | null;
hero_bonus_credits: number;
hero_bonus_exp: number;
hitpoints_left: number;
hits_bounced: number;
hits_pen: number;
hits_received: number;
hits_splash: number;
killed_by: number;
name: string;
shots_hit: number;
shots_made: number;
shots_pen: number;
shots_splash: number;
squad_index: number;
team: number;
time_alive: number;
turret_id: unknown | null;
vehicle_descr: number;
wp_points_earned: number;
wp_points_stolen: number;
};
};
};
error: {};
};
2024-04-24 16:22:48 -05:00
interface WotInspectorReplay {
id: string;
map_id: number;
battle_duration: number;
2024-04-26 12:51:35 -05:00
title: unknown | null;
2024-04-24 16:22:48 -05:00
player_name: string;
protagonist: number;
vehicle_descr: number;
mastery_badge: number;
exp_base: number;
enemies_spotted: number;
enemies_destroyed: number;
damage_assisted: number;
damage_made: number;
details_url: string;
download_url: string;
2024-04-26 12:51:35 -05:00
game_version: {
name: string;
package: string;
};
2024-04-24 16:22:48 -05:00
arena_unique_id: string;
download_count: number;
data_version: number;
private: boolean;
private_clan: boolean;
battle_start_time: Date;
upload_time: Date;
allies: number[];
enemies: number[];
protagonist_clan: number;
protagonist_team: number;
battle_result: number;
credits_base: number;
tags: number[];
battle_type: number;
room_type: number;
last_accessed_time: Date;
winner_team: number;
finish_reason: number;
players_data: WotInspectorReplayPlayerData[];
exp_total: number;
credits_total: number;
repair_cost: number;
exp_free: number;
exp_free_base: number;
exp_penalty: number;
credits_penalty: number;
credits_contribution_in: number;
credits_contribution_out: number;
camouflage_id: number;
}
export interface WotInspectorReplayPlayerData {
team: number;
name: string;
entity_id: number;
dbid: number;
clanid: number | null;
clan_tag: null | string;
hitpoints_left: number;
credits: number;
exp: number;
shots_made: number;
shots_hit: number;
shots_splash: number;
shots_pen: number;
damage_made: number;
damage_received: number;
damage_assisted: number;
damage_assisted_track: number;
hits_received: number;
hits_bounced: number;
hits_splash: number;
hits_pen: number;
enemies_spotted: number;
enemies_damaged: number;
enemies_destroyed: number;
time_alive: number;
distance_travelled: number;
killed_by: number;
base_capture_points: number;
base_defend_points: number;
exp_for_damage: number;
exp_for_assist: number;
exp_team_bonus: number;
wp_points_earned: number;
wp_points_stolen: number;
hero_bonus_credits: number;
hero_bonus_exp: number;
death_reason: number;
achievements: any[];
vehicle_descr: number;
turret_id: number;
gun_id: number;
chassis_id: number;
squad_index: number;
damage_blocked: number;
}
function stat(value: string | number, flex = 2) {
return (
<div
style={{
flex,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
</div>
);
}
async function playerListing(
2025-01-14 19:22:41 -06:00
locale: Locale,
2024-04-24 16:22:48 -05:00
player: WotInspectorReplayPlayerData,
protagonistTeam: boolean,
2024-04-26 12:34:59 -05:00
winningTeam: number,
2024-04-24 16:22:48 -05:00
recordingPlayer = false,
) {
2025-01-14 19:22:41 -06:00
const { unwrap } = translator(locale);
2024-09-07 23:00:56 -05:00
const awaitedTankAverages = await blitzStarsTankAverages;
2024-04-24 16:22:48 -05:00
const awaitedTankDefinitions = await tankDefinitions;
2024-10-05 23:20:04 -05:00
const tank = awaitedTankDefinitions.tanks[player.vehicle_descr];
2024-04-24 16:22:48 -05:00
const blockAccent = recordingPlayer ? '_amber' : '';
const healthAccent = protagonistTeam
? recordingPlayer
? '_amber'
: '_green'
: '_red';
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
// TODO: 💀
const turret = tank.turrets.sort(
(a, b) =>
Math.abs(a.id - player.turret_id) - Math.abs(b.id - player.turret_id),
)[0];
const totalHealth = turret.health + tank.health;
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
2024-04-26 12:34:59 -05:00
const tankAveragesEntry = awaitedTankAverages[player.vehicle_descr];
const wn8 =
tankAveragesEntry === undefined
? -1
: calculateWN8(tankAveragesEntry.all, {
battles: 1,
damage_dealt: player.damage_made,
dropped_capture_points: player.base_defend_points,
frags: player.enemies_destroyed,
spotted: player.enemies_spotted,
wins: winningTeam === player.team ? 1 : 0,
});
2024-04-24 16:22:48 -05:00
return (
<div
key={player.entity_id}
style={{
2024-04-30 22:20:31 -05:00
display: 'flex',
alignItems: 'center',
gap: 8,
2024-04-24 16:22:48 -05:00
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
}}
>
<div
style={{
display: 'flex',
gap: 8,
2024-04-30 22:20:31 -05:00
flex: 1,
overflow: 'hidden',
alignItems: 'center',
2024-04-24 16:22:48 -05:00
}}
>
2024-04-30 22:20:31 -05:00
<img
2025-01-14 19:22:41 -06:00
alt={unwrap(tank.name)}
2024-04-30 22:20:31 -05:00
src={await iconPng(tankIcon(tank.id))}
style={{
width: 32,
height: 32,
objectFit: 'contain',
}}
/>
2024-04-24 16:22:48 -05:00
<div
style={{
display: 'flex',
2024-04-30 22:20:31 -05:00
flexDirection: 'column',
// align
2024-04-24 16:22:48 -05:00
}}
>
<div
style={{
display: 'flex',
2024-04-30 22:20:31 -05:00
gap: 4,
alignItems: 'center',
2024-04-24 16:22:48 -05:00
}}
>
<span
style={{
color: theme.colors[`textLowContrast${blockAccent}`],
2024-04-30 22:20:31 -05:00
fontSize: 12,
2024-04-24 16:22:48 -05:00
}}
>
2024-04-30 22:20:31 -05:00
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
<span
style={{
color: theme.colors[`textHighContrast${blockAccent}`],
fontSize: 16,
}}
>
2025-01-14 19:22:41 -06:00
{unwrap(tank.name)}
2024-04-24 16:22:48 -05:00
</span>
</div>
2024-04-30 22:20:31 -05:00
<span
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
whiteSpace: 'nowrap',
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ''}
</span>
2024-04-24 16:22:48 -05:00
</div>
</div>
<div
style={{
2024-04-30 22:20:31 -05:00
display: 'flex',
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat(wn8 === -1 ? '--' : Math.round(wn8).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
<div
style={{
2024-04-24 16:22:48 -05:00
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: 'flex',
2024-04-30 22:20:31 -05:00
width: 4,
height: '100%',
transform: 'scaleY(-1)',
2024-04-24 16:22:48 -05:00
}}
>
<div
style={{
2024-04-30 22:20:31 -05:00
width: '100%',
height: `${healthLeft * 100}%`,
2024-04-24 16:22:48 -05:00
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
}}
/>
</div>
</div>
);
}
2024-04-26 12:34:59 -05:00
function Header() {
return (
<div
style={{
padding: 8,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ flex: 1 }} />
<div
style={{
display: 'flex',
flex: 1,
}}
>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
WN8
</span>
</div>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
XP
</span>
</div>
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
</div>
);
}
2024-04-23 16:07:59 -05:00
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('replay').addAttachmentOption((option) =>
option.setName('file').setDescription('Replay file').setRequired(true),
),
async handler(interaction) {
2024-04-27 11:07:58 -05:00
const awaitedMapDefinitions = await mapDefinitions;
2024-04-23 16:07:59 -05:00
const file = interaction.options.getAttachment('file')!;
2024-04-24 16:22:48 -05:00
// const file = {
// url: 'https://cdn.discordapp.com/ephemeral-attachments/1232434891652861972/1232781496016834581/20240328_1530__TresAbhi_M60_1157057448155406708.wotbreplay?ex=662ab4bc&is=6629633c&hm=c784cd10e80232350ddaf291fa987b879c4d0a8083b78298c9eee4fa0d507f6c&',
// };
const submissionData = (await fetch(
`https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent(
file.url,
)}`,
).then((response) => response.json())) as WotInspectorReplaySubmission;
2024-04-23 16:07:59 -05:00
2024-04-24 16:22:48 -05:00
if (submissionData.status === 'error') {
return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`;
2024-04-23 16:07:59 -05:00
}
2024-04-24 16:22:48 -05:00
const replayId = submissionData.data.download_url.split('/').at(-1)!;
const replayData = (await fetch(
`https://api.wotinspector.com/v2/blitz/replays/${replayId}/`,
).then((response) => response.json())) as WotInspectorReplay;
2024-04-27 11:07:58 -05:00
const antagonistTeamId = 3 - replayData.protagonist_team;
2024-04-24 16:22:48 -05:00
const players = replayData.players_data.sort(
(a, b) => b.damage_made - a.damage_made,
);
2024-04-27 11:07:58 -05:00
const clan = (
await getClanAccountInfo(
idToRegion(replayData.protagonist),
replayData.protagonist,
['clan'],
)
)?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
2024-04-24 16:22:48 -05:00
return [
new RenderConfiguration({ width: 960 }),
<CommandWrapper>
<TitleBar
title={replayData.player_name}
2024-04-27 11:07:58 -05:00
image={clanImage}
description={`${
replayData.protagonist_team === replayData.winner_team
? 'Victory'
: 'Defeat'
2024-10-05 23:20:04 -05:00
} • ${awaitedMapDefinitions.maps[replayData.map_id].name}`}
2024-04-24 16:22:48 -05:00
/>
<div
style={{
display: 'flex',
gap: 8,
}}
>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
2024-04-26 12:34:59 -05:00
<Header />
2024-04-24 16:22:48 -05:00
{await Promise.all(
players
2024-04-27 11:07:58 -05:00
.filter(
(player) => player.team === replayData.protagonist_team,
)
2024-04-24 16:22:48 -05:00
.map((player) =>
playerListing(
2025-01-14 19:22:41 -06:00
interaction.locale,
2024-04-24 16:22:48 -05:00
player,
true,
2024-04-26 12:34:59 -05:00
replayData.winner_team,
2024-04-24 16:22:48 -05:00
player.dbid === replayData.protagonist,
),
),
)}
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
2024-04-26 12:34:59 -05:00
<Header />
2024-04-24 16:22:48 -05:00
{await Promise.all(
players
.filter((player) => player.team === antagonistTeamId)
2024-04-26 12:34:59 -05:00
.map((player) =>
2025-01-14 19:22:41 -06:00
playerListing(
interaction.locale,
player,
false,
replayData.winner_team,
),
2024-04-26 12:34:59 -05:00
),
2024-04-24 16:22:48 -05:00
)}
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, 'WoT Inspector'),
buttonLink(submissionData.data.download_url, 'Download'),
];
2024-04-23 16:07:59 -05:00
},
});
});