feat: mostly complete rating command

This commit is contained in:
TrèsAbhi committed 2024-04-24 16:22:48 -05:00
1 parent 983b96fd99
commit 1f30d1d2c3
11 files changed
+384 -32

No files matched your search

+2 -2
View File
@@ -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<CommandRegistry>((resolve) => {
height: 64,
objectFit: 'cover',
}}
src={await webpToPng(
src={await iconPng(
reward.type === 'vehicle'
? reward.vehicle.preview_image_url
: reward.stuff.image_url,
+340 -9
View File
@@ -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 (
<div
style={{
flex,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
</div>
);
}
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 (
<div
key={player.entity_id}
style={{
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div
style={{
display: 'flex',
gap: 8,
flex: 1,
overflow: 'hidden',
alignItems: 'center',
}}
>
<img
src={await iconPng(tankIcon(tank.id))}
style={{
width: 32,
height: 32,
objectFit: 'contain',
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
// align
}}
>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<span
style={{
color: theme.colors[`textLowContrast${blockAccent}`],
fontSize: 12,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
<span
style={{
color: theme.colors[`textHighContrast${blockAccent}`],
fontSize: 16,
}}
>
{tank.name}
</span>
</div>
<span
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ''}
</span>
</div>
</div>
<div
style={{
display: 'flex',
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat((2830).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
</div>
<div
style={{
height: 4,
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: 'flex',
}}
>
<div
style={{
width: `${healthLeft * 100}%`,
height: '100%',
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
}}
/>
</div>
</div>
);
}
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
@@ -93,17 +356,85 @@ export const replayCommand = new Promise<CommandRegistry>((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 }),
<CommandWrapper>
<TitleBar
title={replayData.player_name}
description={`uwu <3 xoxo hugs and kicks`}
/>
<div
style={{
display: 'flex',
gap: 8,
}}
>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
{await Promise.all(
players
.filter((player) => player.team === protagonistTeamId)
.map((player) =>
playerListing(
player,
true,
player.dbid === replayData.protagonist,
),
),
)}
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
{await Promise.all(
players
.filter((player) => player.team === antagonistTeamId)
.map((player) => playerListing(player, false)),
)}
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, 'WoT Inspector'),
buttonLink(submissionData.data.download_url, 'Download'),
];
},
});
});
+8 -10
View File
@@ -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<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(asset('icons/currencies/xp.webp'))}
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
@@ -305,7 +305,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
@@ -330,7 +330,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
@@ -447,9 +447,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
asset('icons/currencies/xp.webp'),
)}
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
@@ -476,7 +474,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
src={await iconPng(
asset('icons/currencies/free-xp.webp'),
)}
width={16}
@@ -503,7 +501,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
@@ -531,7 +529,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
+1 -1
View File
@@ -14,7 +14,7 @@ export default function CommandWrapper({ children, fat }: CommandWrapperProps) {
flexDirection: 'column',
padding: `16px ${fat ? 0 : 16}px`,
gap: 16,
width: 480,
width: '100%',
color: theme.colors.textHighContrast,
position: 'relative',
background: theme.colors.appBackground1,
+1 -1
View File
@@ -2,7 +2,7 @@ import sharp from 'sharp';
const cache: Record<string, Buffer> = {};
export async function webpToPng(url: string) {
export async function iconPng(url: string) {
if (!url.endsWith('.webp')) return url;
if (!cache[url]) {
+6 -3
View File
@@ -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;
}
+6 -2
View File
@@ -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',
+11
View File
@@ -0,0 +1,11 @@
export interface RenderConfiguration {
width: number;
}
export class RenderConfiguration implements RenderConfiguration {
width = 480;
constructor(configuration?: Partial<RenderConfiguration>) {
Object.assign(this, configuration);
}
}
+2 -2
View File
@@ -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));
}
@@ -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]);
}
}),
+2
View File
@@ -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 =