diff --git a/src/commands/rating.tsx b/src/commands/rating.tsx index 971a5dce0..48ea5d0e0 100644 --- a/src/commands/rating.tsx +++ b/src/commands/rating.tsx @@ -11,7 +11,7 @@ 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 { webpToPng } from '../core/blitzkit/iconPng'; +import { iconPng } from '../core/blitzkit/iconPng'; import addUsernameChoices from '../core/discord/addUsernameChoices'; import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; @@ -295,7 +295,7 @@ export const ratingCommand = new Promise((resolve) => { height: 64, objectFit: 'cover', }} - src={await webpToPng( + src={await iconPng( reward.type === 'vehicle' ? reward.vehicle.preview_image_url : reward.stuff.image_url, diff --git a/src/commands/replay.tsx b/src/commands/replay.tsx index e6a5009e7..e684254da 100644 --- a/src/commands/replay.tsx +++ b/src/commands/replay.tsx @@ -1,7 +1,17 @@ +import { clamp } from 'lodash'; +import CommandWrapper from '../components/CommandWrapper'; +import TitleBar from '../components/TitleBar'; +import { iconPng } from '../core/blitzkit/iconPng'; +import { RenderConfiguration } from '../core/blitzkit/renderConfiguration'; +import { tankDefinitions } from '../core/blitzkit/tankDefinitions'; +import { TIER_ROMAN_NUMERALS } from '../core/blitzkit/tankDefinitions/constants'; +import { tankIcon } from '../core/blitzkit/tankIcon'; +import buttonLink from '../core/discord/buttonLink'; import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { CommandRegistry } from '../events/interactionCreate'; +import { theme } from '../stitches.config'; -type WotInspectorReplayResponse = +type WotInspectorReplaySubmission = | { status: 'error'; data: unknown; error: { message: string } } | { status: 'ok'; @@ -83,6 +93,259 @@ type WotInspectorReplayResponse = error: {}; }; +interface WotInspectorReplay { + id: string; + map_id: number; + battle_duration: number; + title: null; + 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; + game_version: WotInspectorReplayGameVersion; + 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 WotInspectorReplayGameVersion { + name: string; + package: string; +} + +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 ( +
+ {value} +
+ ); +} + +async function playerListing( + player: WotInspectorReplayPlayerData, + protagonistTeam: boolean, + recordingPlayer = false, +) { + const awaitedTankDefinitions = await tankDefinitions; + const tank = awaitedTankDefinitions[player.vehicle_descr]; + 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); + + return ( +
+
+
+ + +
+
+ + {TIER_ROMAN_NUMERALS[tank.tier]} + + + {tank.name} + +
+ + + {player.name} + {player.clan_tag ? ` [${player.clan_tag}]` : ''} + +
+
+ +
+ {stat(player.damage_made.toLocaleString())} + {stat((2830).toLocaleString())} + {stat(player.exp.toLocaleString())} + {stat(player.enemies_destroyed, 1)} +
+
+ +
+
+
+
+ ); +} + export const replayCommand = new Promise((resolve) => { resolve({ inProduction: true, @@ -93,17 +356,85 @@ export const replayCommand = new Promise((resolve) => { async handler(interaction) { const file = interaction.options.getAttachment('file')!; - const url = `https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent( - file.url, - )}`; - const response = await fetch(url); - const replay = (await response.json()) as WotInspectorReplayResponse; + // 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; - if (replay.status === 'error') { - return `# Uh oh! WoTInspoec` + 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).`; } - return await response.text(); + 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; + const protagonistTeamId = replayData.players_data.find( + (player) => player.dbid === replayData.protagonist, + )!.team; + const antagonistTeamId = 3 - protagonistTeamId; + const players = replayData.players_data.sort( + (a, b) => b.damage_made - a.damage_made, + ); + + return [ + new RenderConfiguration({ width: 960 }), + + + +
+
+ {await Promise.all( + players + .filter((player) => player.team === protagonistTeamId) + .map((player) => + playerListing( + player, + true, + player.dbid === replayData.protagonist, + ), + ), + )} +
+ +
+ {await Promise.all( + players + .filter((player) => player.team === antagonistTeamId) + .map((player) => playerListing(player, false)), + )} +
+
+
, + buttonLink(submissionData.data.view_url, 'WoT Inspector'), + buttonLink(submissionData.data.download_url, 'Download'), + ]; }, }); }); diff --git a/src/commands/research.tsx b/src/commands/research.tsx index ff66b5a0d..5fcdeb1cd 100644 --- a/src/commands/research.tsx +++ b/src/commands/research.tsx @@ -9,7 +9,7 @@ import resolveTankId from '../core/blitz/resolveTankId'; import { asset } from '../core/blitzkit/asset'; import { buildTechTreeLine } from '../core/blitzkit/buildTechTreeLine'; import { emblemURL } from '../core/blitzkit/emblemURL'; -import { webpToPng } from '../core/blitzkit/iconPng'; +import { iconPng } from '../core/blitzkit/iconPng'; import { resolveAncestry } from '../core/blitzkit/resolveAncestry'; import { tankDefinitions } from '../core/blitzkit/tankDefinitions'; import { @@ -280,7 +280,7 @@ export const researchCommand = new Promise((resolve) => { }} > @@ -305,7 +305,7 @@ export const researchCommand = new Promise((resolve) => { }} > @@ -330,7 +330,7 @@ export const researchCommand = new Promise((resolve) => { }} > @@ -447,9 +447,7 @@ export const researchCommand = new Promise((resolve) => { }} > @@ -476,7 +474,7 @@ export const researchCommand = new Promise((resolve) => { }} > ((resolve) => { }} > ((resolve) => { }} > = {}; -export async function webpToPng(url: string) { +export async function iconPng(url: string) { if (!url.endsWith('.webp')) return url; if (!cache[url]) { diff --git a/src/core/blitzkit/jsxToPngThreaded.ts b/src/core/blitzkit/jsxToPngThreaded.ts index 5a68fb629..0e8b08991 100644 --- a/src/core/blitzkit/jsxToPngThreaded.ts +++ b/src/core/blitzkit/jsxToPngThreaded.ts @@ -1,9 +1,12 @@ import jsxToSvg from './jsxToSvg'; +import { RenderConfiguration } from './renderConfiguration'; import { svgToPngThreaded } from './svgToPngThreaded'; -export default async function jsxToPngThreaded(jsx: JSX.Element) { - const svg = await jsxToSvg(jsx); +export default async function jsxToPngThreaded( + jsx: JSX.Element, + renderConfiguration: RenderConfiguration, +) { + const svg = await jsxToSvg(jsx, renderConfiguration); const png = await svgToPngThreaded(svg); - return png; } diff --git a/src/core/blitzkit/jsxToSvg/index.ts b/src/core/blitzkit/jsxToSvg/index.ts index 66f0b5b07..6092b5157 100644 --- a/src/core/blitzkit/jsxToSvg/index.ts +++ b/src/core/blitzkit/jsxToSvg/index.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'fs'; import satori from 'satori'; +import { RenderConfiguration } from '../renderConfiguration'; import robotoBlack from './Roboto-Black.ttf'; import robotoBold from './Roboto-Bold.ttf'; import roboto from './Roboto.ttf'; @@ -10,9 +11,12 @@ const fonts = [ { file: robotoBlack, weight: 900 as const }, ]; -export default async function jsxToSvg(jsx: JSX.Element) { +export default async function jsxToSvg( + jsx: JSX.Element, + renderConfiguration: RenderConfiguration, +) { return await satori(jsx, { - width: 480, + width: renderConfiguration.width, fonts: fonts.map(({ file, weight }) => ({ weight, name: 'Roboto', diff --git a/src/core/blitzkit/renderConfiguration.ts b/src/core/blitzkit/renderConfiguration.ts new file mode 100644 index 000000000..508e20591 --- /dev/null +++ b/src/core/blitzkit/renderConfiguration.ts @@ -0,0 +1,11 @@ +export interface RenderConfiguration { + width: number; +} + +export class RenderConfiguration implements RenderConfiguration { + width = 480; + + constructor(configuration?: Partial) { + Object.assign(this, configuration); + } +} diff --git a/src/core/blitzkit/tankIconPng.ts b/src/core/blitzkit/tankIconPng.ts index 21b5d3c21..97b076bf9 100644 --- a/src/core/blitzkit/tankIconPng.ts +++ b/src/core/blitzkit/tankIconPng.ts @@ -1,6 +1,6 @@ -import { webpToPng } from './iconPng'; +import { iconPng } from './iconPng'; import { tankIcon } from './tankIcon'; export function tankIconPng(id: number) { - return webpToPng(tankIcon(id)); + return iconPng(tankIcon(id)); } diff --git a/src/core/discord/normalizeInteractionReturnable.ts b/src/core/discord/normalizeInteractionReturnable.ts index 66b793b20..2ae4ce76a 100644 --- a/src/core/discord/normalizeInteractionReturnable.ts +++ b/src/core/discord/normalizeInteractionReturnable.ts @@ -9,6 +9,7 @@ import { } from 'discord.js'; import { InteractionReturnable } from '../../events/interactionCreate'; import jsxToPngThreaded from '../blitzkit/jsxToPngThreaded'; +import { RenderConfiguration } from '../blitzkit/renderConfiguration'; export default async function normalizeInteractionReturnable( returnable: InteractionReturnable, @@ -17,11 +18,11 @@ export default async function normalizeInteractionReturnable( const reply: InteractionEditReplyOptions & InteractionReplyOptions & MessageEditOptions = {}; - const awaitedReturnable = await returnable; const normalizedReturnable = Array.isArray(awaitedReturnable) ? awaitedReturnable : [awaitedReturnable]; + let renderConfiguration = new RenderConfiguration(); await Promise.all( normalizedReturnable.map(async (item, index) => { @@ -39,10 +40,12 @@ export default async function normalizeInteractionReturnable( } else if (item instanceof AttachmentBuilder) { if (!reply.files) reply.files = []; reply.files.push(item); + } else if (item instanceof RenderConfiguration) { + renderConfiguration = item; } else if (item === null) { return; } else { - const image = await jsxToPngThreaded(item); + const image = await jsxToPngThreaded(item, renderConfiguration); images.push([index, image]); } }), diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index cc650e8de..9732ffdf7 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -35,6 +35,7 @@ import { statsCommand } from '../../commands/stats'; import { todayCommand } from '../../commands/today'; import getClientId from '../../core/blitzkit/getClientId'; import isDev from '../../core/blitzkit/isDev'; +import { RenderConfiguration } from '../../core/blitzkit/renderConfiguration'; import { secrets } from '../../core/blitzkit/secrets'; import handleAutocomplete from './handlers/autocomplete'; import handleButton from './handlers/button'; @@ -45,6 +46,7 @@ export type InteractionRawReturnable = | EmbedBuilder | ButtonBuilder | AttachmentBuilder + | RenderConfiguration | JSX.Element | null; export type InteractionIterableReturnable =