diff --git a/src/commands/ownedtanks.ts b/src/commands/ownedtanks.ts index a7f628d85..c026bf993 100644 --- a/src/commands/ownedtanks.ts +++ b/src/commands/ownedtanks.ts @@ -79,8 +79,8 @@ export default { : tanks .map( (tank) => - `${TANK_TYPE_EMOJIS[tank.type]} ${resolveTankName( - tank, + `${TANK_TYPE_EMOJIS[tank.type]} ${markdownEscape( + resolveTankName(tank.tank_id), )} ${tank.is_premium ? '⭐' : ''}${ COMP_TANKS.includes(tank.tank_id) ? '🏆' : '' }`, diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index b517fc53d..3dc9bed2c 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -15,8 +15,14 @@ import usernameAutocomplete from '../core/autocomplete/username.js'; import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import sumStats from '../core/blitz/sumStats.js'; +import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; +import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; import cmdName from '../core/interaction/cmdName.js'; +import addStatPeriodChoices, { + StatPeriod, + statPeriodNames, +} from '../core/options/addStatPeriodChoices.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; import { args } from '../core/process/args.js'; import render from '../core/ui/render.js'; @@ -24,36 +30,15 @@ import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo, AllStats } from '../types/accountInfo.js'; import { PlayerClanData } from '../types/playerClanData.js'; -export type StatPeriod = 'today' | '30' | '60' | '90' | 'career'; -export const statPeriodNames: Record = { - today: "Today's statistics", - 30: '30-day statistics', - 60: '60-day statistics', - 90: '90-day statistics', - career: 'Career statistics', -}; - export default { inProduction: true, - inDevelopment: true, + inDevelopment: false, inPublic: true, command: new SlashCommandBuilder() .setName(cmdName('stats')) .setDescription("Gets the user's in-game statistics") - .addStringOption((option) => - option - .setName('period') - .setDescription('The last number of days of stats') - .setChoices( - { name: 'Today', value: 'today' satisfies StatPeriod }, - { name: '30 Days', value: '30' satisfies StatPeriod }, - { name: '60 Days', value: '60' satisfies StatPeriod }, - { name: '90 Days', value: '90' satisfies StatPeriod }, - { name: 'Career', value: 'career' satisfies StatPeriod }, - ) - .setRequired(true), - ) + .addStringOption(addStatPeriodChoices) .addStringOption(addUsernameOption), async execute(interaction) { @@ -72,19 +57,13 @@ export default { if (period === 'career') { stats = accountInfo[id].statistics.all; } else { - const daysAgo = period === 'today' ? 0 : Number(period) - 1; - const periodStart = new Date(); - - periodStart.setDate(periodStart.getDate() - daysAgo); - periodStart.setHours(5, 0, 0, 0); - stats = sumStats( Object.entries( await getTankStatsOverTime( server, id, - periodStart.getTime() / 1000, - new Date().getTime() / 1000, + getPeriodicStart(period), + getPeriodNow(), ), ).map(([, stats]) => stats), ); @@ -115,7 +94,6 @@ export default { 'Winrate', `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`, ], - // ['WN8', stats.wn8.toFixed(0)], [ 'Survival', `${(100 * (stats.survived_battles / stats.battles)).toFixed( @@ -135,7 +113,6 @@ export default { ['Average hits', (stats.hits / stats.battles).toFixed(2)], ['Average kills', (stats.frags / stats.battles).toFixed(2)], ['Average spots', (stats.spotted / stats.battles).toFixed(2)], - // ['Average tier', stats.avg_tier.toFixed(2)], [ 'Damage ratio', (stats.damage_dealt / stats.damage_received).toFixed(2), diff --git a/src/commands/tankstats.ts b/src/commands/tankstats.ts deleted file mode 100644 index 776c3969a..000000000 --- a/src/commands/tankstats.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import markdownEscape from 'markdown-escape'; -import tanksAutocomplete from '../core/autocomplete/tanks.js'; -import usernameAutocomplete from '../core/autocomplete/username.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; -import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; -import { tankopedia } from '../core/blitzstars/tankopedia.js'; -import getPeriodicStats, { - Period, -} from '../core/blitztankstats/getPeriodicStats.js'; -import cleanTable from '../core/interaction/cleanTable.js'; -import cmdName from '../core/interaction/cmdName.js'; -import errorEmbed from '../core/interaction/errorEmbed.js'; -import infoEmbed from '../core/interaction/infoEmbed.js'; -import addTankChoices from '../core/options/addTankChoices.js'; -import addUsernameOption from '../core/options/addUsernameOption.js'; -import { args } from '../core/process/args.js'; -import { CommandRegistry } from '../events/interactionCreate.js'; -import { AccountInfo } from '../types/accountInfo.js'; -import resolveTankName from '../utilities/resolveTankName.js'; - -type Periods = '30' | '60' | '90'; - -export default { - inProduction: true, - inDevelopment: false, - inPublic: true, - - command: new SlashCommandBuilder() - .setName(cmdName('tankstats')) - .setDescription('Stats for a tank over a period') - .addStringOption(addTankChoices) - .addStringOption((option) => - option - .setName('period') - .setDescription('The period to get stats for') - .setChoices( - { name: '30 Days', value: '30' satisfies Periods }, - { name: '60 Days', value: '60' satisfies Periods }, - { name: '90 Days', value: '90' satisfies Periods }, - ) - .setRequired(true), - ) - .addStringOption(addUsernameOption), - - async execute(interaction) { - const name = interaction.options.getString('username')!; - const tankId = interaction.options.getString('tank')!; - const period = interaction.options.getString('period') as Periods; - const blitzAccount = await getBlitzAccount(interaction, name); - const { server, id } = blitzAccount; - const accountInfo = await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`, - ); - const tank = tankopedia[tankId as unknown as number]; - - if (tank) { - const periodicStats = (await getPeriodicStats(id, Number(period))) - .filter((stats) => stats.tank_id === tank.tank_id) - .filter((stats) => !stats.isRating); - - function sum(summer: (period: Period) => number) { - return periodicStats.reduce( - (accumulative, period) => accumulative + summer(period), - 0, - ); - } - function weighted(summer: (period: Period) => number) { - return ( - periodicStats.reduce( - (accumulative, period) => - accumulative + summer(period) * period.battles, - 0, - ) / sum((period) => period.battles) - ); - } - - interaction.editReply({ - embeds: [ - infoEmbed( - `${period} day stats for ${markdownEscape( - accountInfo[id].nickname, - )}'s ${resolveTankName(tank)}`, - periodicStats.length > 0 - ? cleanTable([ - [ - 'Winrate', - `${( - 100 * - (sum((a) => a.wins) / sum((a) => a.battles)) - ).toFixed(2)}%`, - ], - [ - 'Survival', - `${( - 100 * - (sum((a) => a.survived_battles) / sum((a) => a.battles)) - ).toFixed(2)}%`, - ], - [ - 'Accuracy', - `${( - 100 * - (sum((a) => a.hits) / sum((a) => a.shots)) - ).toFixed(2)}%`, - ], - ['WN8', weighted((a) => a.wn8).toFixed(0)], - [], - ['Battles', sum((a) => a.battles)], - ['Wins', sum((a) => a.wins)], - ['Losses', sum((a) => a.losses)], - [], - [ - 'Shots per battle', - (sum((a) => a.shots) / sum((a) => a.battles)).toFixed(2), - ], - [ - 'Hits per battle', - (sum((a) => a.hits) / sum((a) => a.battles)).toFixed(2), - ], - [ - 'Damage per battle', - ( - sum((a) => a.damage_dealt) / sum((a) => a.battles) - ).toFixed(0), - ], - [ - 'Kills per battle', - (sum((a) => a.frags) / sum((a) => a.battles)).toFixed(2), - ], - [ - 'Spots per battle', - (sum((a) => a.spotted) / sum((a) => a.battles)).toFixed(2), - ], - [ - 'XP per battle', - (sum((a) => a.xp) / sum((a) => a.battles)).toFixed(0), - ], - [], - [ - 'Damage ratio', - ( - sum((a) => a.damage_dealt) / sum((a) => a.damage_received) - ).toFixed(2), - ], - [ - 'Kills to death ratio', - ( - sum((a) => a.frags) / - sum((a) => a.battles - a.survived_battles) - ).toFixed(2), - ], - ]) - : 'No battles were played or recorded during this period.', - ), - ], - }); - } else { - interaction.editReply({ - embeds: [ - errorEmbed( - 'Tank not found', - `"${markdownEscape(tankId)}" does not exist in the game.`, - ), - ], - }); - } - }, - - autocomplete: (interaction) => { - tanksAutocomplete(interaction); - usernameAutocomplete(interaction); - }, -} satisfies CommandRegistry; diff --git a/src/commands/tankstats.tsx b/src/commands/tankstats.tsx new file mode 100644 index 000000000..2e80d5266 --- /dev/null +++ b/src/commands/tankstats.tsx @@ -0,0 +1,156 @@ +import { + ActionRowBuilder, + ButtonBuilder, + SlashCommandBuilder, +} from 'discord.js'; +import GenericStats from '../components/GenericStats/index.js'; +import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js'; +import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js'; +import TitleBar from '../components/TitleBar.js'; +import Wrapper from '../components/Wrapper.js'; +import { BLITZ_SERVERS } from '../constants/servers.js'; +import fullBlitzStarsStats from '../core/actions/fullBlitzStarsStats.js'; +import { supportBlitzStars } from '../core/actions/supportBlitzStars.js'; +import tanksAutocomplete from '../core/autocomplete/tanks.js'; +import usernameAutocomplete from '../core/autocomplete/username.js'; +import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; +import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; +import resolveTankId from '../core/blitz/resolveTankId.js'; +import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; +import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; +import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; +import { tankopedia } from '../core/blitzstars/tankopedia.js'; +import cmdName from '../core/interaction/cmdName.js'; +import addStatPeriodChoices, { + StatPeriod, + statPeriodNames, +} from '../core/options/addStatPeriodChoices.js'; +import addTankChoices from '../core/options/addTankChoices.js'; +import addUsernameOption from '../core/options/addUsernameOption.js'; +import { args } from '../core/process/args.js'; +import render from '../core/ui/render.js'; +import { CommandRegistry } from '../events/interactionCreate.js'; +import { AccountInfo, AllStats } from '../types/accountInfo.js'; +import { TanksStats } from '../types/tanksStats.js'; +import resolveTankName from '../utilities/resolveTankName.js'; + +export default { + inProduction: true, + inDevelopment: true, + inPublic: true, + + command: new SlashCommandBuilder() + .setName(cmdName('tankstats')) + .setDescription('Stats for a tank over a period') + .addStringOption(addTankChoices) + .addStringOption(addStatPeriodChoices) + .addStringOption(addUsernameOption), + + async execute(interaction) { + const tank = await resolveTankId( + interaction, + interaction.options.getString('tank')!, + ); + const period = interaction.options.getString('period') as StatPeriod; + const username = interaction.options.getString('username')!; + const blitzAccount = await getBlitzAccount(interaction, username); + const { id, server } = blitzAccount; + const accountInfo = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`, + ); + const tankStats = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`, + ); + let stats: AllStats; + + if (period === 'career') { + stats = tankStats[id].find( + (stats) => stats.tank_id === Number(tank), + )!.all; + } else { + stats = ( + await getTankStatsOverTime( + server, + id, + getPeriodicStart(period), + getPeriodNow(), + ) + )[Number(tank)]; + } + + const image = await render( + + + + {stats.battles === 0 && } + {stats.battles > 0 && ( + // TODO: GenericStats generator for AllStats + + )} + + + , + ); + + const actionRow = new ActionRowBuilder().addComponents( + fullBlitzStarsStats(server, accountInfo[id].nickname), + supportBlitzStars, + ); + + await interaction.editReply({ + files: [image], + components: [actionRow], + }); + + console.log(`Showing stats for ${accountInfo[id].nickname}`); + }, + + autocomplete: (interaction) => { + tanksAutocomplete(interaction); + usernameAutocomplete(interaction); + }, +} satisfies CommandRegistry; diff --git a/src/commands/today.tsx b/src/commands/today.tsx index 821c3980b..7f756cc03 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -30,7 +30,7 @@ import resolveTankName from '../utilities/resolveTankName.js'; export default { inProduction: true, - inDevelopment: true, + inDevelopment: false, inPublic: true, command: new SlashCommandBuilder() @@ -82,14 +82,7 @@ export default { return ( -
- {image && } +
+ {image && ( + + )} -
- +
- {name} + + {name} + {nameDiscriminator && ( - + {nameDiscriminator} )} - +
{description} diff --git a/src/core/autocomplete/tanks.ts b/src/core/autocomplete/tanks.ts index 2fdd33d7d..e92edb117 100644 --- a/src/core/autocomplete/tanks.ts +++ b/src/core/autocomplete/tanks.ts @@ -18,7 +18,9 @@ export default async function tanksAutocomplete( ? go(focusedOption.value, tanks, { keys: ['name'], limit: 10 }).map( (item) => ({ - name: item.obj.name ? item.obj.name : resolveTankName(item.obj), + name: item.obj.name + ? item.obj.name + : resolveTankName(item.obj.tank_id), value: `${item.obj.tank_id}`, } satisfies ApplicationCommandOptionChoiceData), ) diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts new file mode 100644 index 000000000..fe3d71c94 --- /dev/null +++ b/src/core/blitz/resolveTankId.ts @@ -0,0 +1,45 @@ +import { CacheType, ChatInputCommandInteraction } from 'discord.js'; +import { go } from 'fuzzysort'; +import { tankopedia, tanks } from '../blitzstars/tankopedia.js'; +import errorEmbed from '../interaction/errorEmbed.js'; + +export default async function resolveTankId( + interaction: ChatInputCommandInteraction, + tank: string, +) { + const number = Number(tank); + + if (Number.isNaN(number)) { + const searchResult = go(tank, tanks, { keys: ['name'], limit: 1 }); + + if (searchResult.length === 0) { + await interaction.editReply({ + embeds: [ + errorEmbed( + 'Tank not found', + `Could not find tank by the name "${tank}".`, + ), + ], + }); + + throw new Error(`Tank not found by name "${tank}"`); + } else { + return searchResult[0].obj.tank_id; + } + } else { + if (tankopedia[number]) { + return number; + } else { + await interaction.editReply({ + embeds: [ + errorEmbed( + 'Tank not found', + `Could not find tank by the ID "${number}".`, + ), + ], + }); + + throw new Error(`Tank not found by ID "${number}"`); + } + } +} diff --git a/src/core/blitzstars/getPeriodNow.ts b/src/core/blitzstars/getPeriodNow.ts new file mode 100644 index 000000000..2be20c889 --- /dev/null +++ b/src/core/blitzstars/getPeriodNow.ts @@ -0,0 +1,3 @@ +export default function getPeriodNow() { + return new Date().getTime() / 1000; +} diff --git a/src/core/blitzstars/getPeriodStart.ts b/src/core/blitzstars/getPeriodStart.ts new file mode 100644 index 000000000..8cb9c3074 --- /dev/null +++ b/src/core/blitzstars/getPeriodStart.ts @@ -0,0 +1,15 @@ +import { StatPeriod } from '../options/addStatPeriodChoices.js'; + +export default function getPeriodicStart(period: StatPeriod) { + if (period === 'career') { + return -Infinity; + } else { + const daysAgo = period === 'today' ? 0 : Number(period) - 1; + const periodStart = new Date(); + + periodStart.setDate(periodStart.getDate() - daysAgo); + periodStart.setHours(5, 0, 0, 0); + + return periodStart.getTime() / 1000; + } +} diff --git a/src/core/blitztankstats/getPeriodicStats.ts b/src/core/blitztankstats/getPeriodicStats.ts deleted file mode 100644 index d8dcf3d06..000000000 --- a/src/core/blitztankstats/getPeriodicStats.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface Period { - statsId: number; - account_id: number; - tank_id: number; - recordDate: string; - last_battle_time: number; - fromDate: string; - prior_battle_time: number; - battles: number; - wins: number; - losses: number; - spotted: number; - shots: number; - hits: number; - xp: number; - battle_life_time: number; - frags: number; - max_frags: number; - damage_dealt: number; - damage_received: number; - survived_battles: number; - win_and_survived: number; - capture_points: number; - dropped_capture_points: number; - killStealerValue: number; - isRating: boolean; - wn8: number; - region: string; -} - -export default async function getPeriodicStats(id: number, period: number) { - const response = await fetch( - `https://blitztankstats.com/api/Stats/period/${id}/${period}`, - ); - return (await response.json()) as Period[]; -} diff --git a/src/core/options/addStatPeriodChoices.ts b/src/core/options/addStatPeriodChoices.ts new file mode 100644 index 000000000..b2a36c3f4 --- /dev/null +++ b/src/core/options/addStatPeriodChoices.ts @@ -0,0 +1,23 @@ +import { SlashCommandStringOption } from 'discord.js'; + +export type StatPeriod = 'today' | '30' | '60' | '90' | 'career'; +export const statPeriodNames: Record = { + today: "Today's statistics", + 30: '30-day statistics', + 60: '60-day statistics', + 90: '90-day statistics', + career: 'Career statistics', +}; +export default function addStatPeriodChoices(option: SlashCommandStringOption) { + return option + .setName('period') + .setDescription('The period to get stats for') + .setChoices( + { name: 'Today', value: 'today' satisfies StatPeriod }, + { name: '30 Days', value: '30' satisfies StatPeriod }, + { name: '60 Days', value: '60' satisfies StatPeriod }, + { name: '90 Days', value: '90' satisfies StatPeriod }, + { name: 'Career', value: 'career' satisfies StatPeriod }, + ) + .setRequired(true); +} diff --git a/src/utilities/resolveTankName.ts b/src/utilities/resolveTankName.ts index a3ab7855c..a17599908 100644 --- a/src/utilities/resolveTankName.ts +++ b/src/utilities/resolveTankName.ts @@ -1,8 +1,6 @@ -import markdownEscape from 'markdown-escape'; +import { tankopedia } from '../core/blitzstars/tankopedia.js'; -export default function resolveTankName(tank: { - tank_id: number; - name?: string; -}) { - return tank.name ? markdownEscape(tank.name) : `Unknown Tank ${tank.tank_id}`; +export default function resolveTankName(tankId: number) { + const tankopediaName = tankopedia[tankId].name; + return tankopediaName ? tankopediaName : `Unknown Tank ${tankId}`; }