From 752f9b339b49c715bfe9ac6495e115131c20ff40 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Fri, 23 Jun 2023 01:35:15 +0000 Subject: [PATCH] feat: extracted renderer from `/stats` --- src/commands/eligible.ts | 4 +- src/commands/evolution.tsx | 11 +- src/commands/ownedtanks.tsx | 4 +- src/commands/playerachievements.ts | 4 +- src/commands/playerinfo.ts | 4 +- src/commands/stats.tsx | 145 +------------ src/commands/today.tsx | 13 +- src/commands/verify.ts | 4 +- src/core/blitz/getBlitzClan.ts | 2 +- src/core/blitzstars/getPeriodNow.ts | 2 +- src/core/blitzstars/getPeriodStart.ts | 2 +- src/core/blitzstars/getPlayerHistories.ts | 2 +- src/core/blitzstars/getTankHistories.ts | 2 +- src/core/blitzstars/getTankStatsOverTime.ts | 4 +- src/core/interaction/generateCustomId.ts | 9 + src/core/options/addPeriodSubCommands.ts | 14 +- ...DataFromSubcommand.ts => resolvePeriod.ts} | 23 ++- .../resolvePlayer.ts} | 21 +- src/events/interactionCreate.ts | 190 ++++++++++-------- src/renderers/stats.tsx | 140 +++++++++++++ tsconfig.json | 2 +- 21 files changed, 329 insertions(+), 273 deletions(-) create mode 100644 src/core/interaction/generateCustomId.ts rename src/core/options/{getPeriodDataFromSubcommand.ts => resolvePeriod.ts} (78%) rename src/core/{blitz/getBlitzAccount.ts => options/resolvePlayer.ts} (78%) create mode 100644 src/renderers/stats.tsx diff --git a/src/commands/eligible.ts b/src/commands/eligible.ts index 92bcc56fb..9804867be 100644 --- a/src/commands/eligible.ts +++ b/src/commands/eligible.ts @@ -1,5 +1,4 @@ import { SlashCommandBuilder } from 'discord.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getWN8 from '../core/blitz/getWN8.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import sumStats from '../core/blitz/sumStats.js'; @@ -12,6 +11,7 @@ import cleanTable, { TableInput } from '../core/interaction/cleanTable.js'; import negativeEmbed from '../core/interaction/negativeEmbed.js'; import positiveEmbed from '../core/interaction/positiveEmbed.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo } from '../types/accountInfo.js'; @@ -48,7 +48,7 @@ export default { async execute(interaction) { const clan = interaction.options.getString('clan') as SkilledClan; - const { id, server } = await getBlitzAccount(interaction); + const { id, server } = await resolvePlayer(interaction); const accountInfo = await getWargamingResponse( `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, ); diff --git a/src/commands/evolution.tsx b/src/commands/evolution.tsx index 6a46cddbc..8abf4b152 100644 --- a/src/commands/evolution.tsx +++ b/src/commands/evolution.tsx @@ -7,7 +7,6 @@ import Wrapper from '../components/Wrapper.js'; import { BLITZ_SERVERS } from '../constants/servers.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 resolveTankName from '../core/blitz/resolveTankName.js'; @@ -20,7 +19,8 @@ import { RELATIVE_PERIOD_NAMES, } from '../core/options/addPeriodSubCommands.js'; import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js'; -import getPeriodDataFromSubcommand from '../core/options/getPeriodDataFromSubcommand.js'; +import resolvePeriod from '../core/options/resolvePeriod.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo } from '../types/accountInfo.js'; @@ -29,7 +29,7 @@ import { PlayerClanData } from '../types/playerClanData.js'; export default { inProduction: true, - inDevelopment: true, + inDevelopment: false, inPublic: true, command: addStatsSubCommandGroups( @@ -40,7 +40,7 @@ export default { async execute(interaction) { const commandGroup = interaction.options.getSubcommandGroup()!; - const { id, server } = await getBlitzAccount(interaction); + const { id, server } = await resolvePlayer(interaction); let nameDiscriminator: string | undefined; let image: string | undefined; const tankIdRaw = interaction.options.getString('tank')!; @@ -65,8 +65,7 @@ export default { let histories: Histories; - const { evolutionName, start, end } = - getPeriodDataFromSubcommand(interaction); + const { evolutionName, start, end } = resolvePeriod(interaction); if (commandGroup === 'player') { histories = await getPlayerHistories(server, id, { diff --git a/src/commands/ownedtanks.tsx b/src/commands/ownedtanks.tsx index d92d5e1c3..d73a14a70 100644 --- a/src/commands/ownedtanks.tsx +++ b/src/commands/ownedtanks.tsx @@ -6,7 +6,6 @@ import TitleBar from '../components/TitleBar.js'; import Wrapper from '../components/Wrapper.js'; import { BLITZ_SERVERS } from '../constants/servers.js'; import usernameAutocomplete from '../core/autocomplete/username.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getTankStats from '../core/blitz/getTankStats.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import { @@ -15,6 +14,7 @@ import { tankopedia, } from '../core/blitz/tankopedia.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo } from '../types/accountInfo.js'; @@ -71,7 +71,7 @@ export default { async execute(interaction) { const tier = Number(interaction.options.getString('tier')); - const account = await getBlitzAccount(interaction); + const account = await resolvePlayer(interaction); const { id, server } = account; const accountInfo = await getWargamingResponse( `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, diff --git a/src/commands/playerachievements.ts b/src/commands/playerachievements.ts index d230d712f..fb52b3a50 100644 --- a/src/commands/playerachievements.ts +++ b/src/commands/playerachievements.ts @@ -2,11 +2,11 @@ import { SlashCommandBuilder } from 'discord.js'; import { startCase } from 'lodash'; import markdownEscape from 'markdown-escape'; import usernameAutocomplete from '../core/autocomplete/username.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import cleanTable, { TableInputEntry } from '../core/interaction/cleanTable.js'; import infoEmbed from '../core/interaction/infoEmbed.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountAchievements } from '../types/accountAchievements.js'; @@ -35,7 +35,7 @@ export default { async execute(interaction) { const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy; - const account = await getBlitzAccount(interaction); + const account = await resolvePlayer(interaction); const { id, server } = account; const accounts = await getWargamingResponse( `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, diff --git a/src/commands/playerinfo.ts b/src/commands/playerinfo.ts index 9aae9269d..e34426edc 100644 --- a/src/commands/playerinfo.ts +++ b/src/commands/playerinfo.ts @@ -1,11 +1,11 @@ import { SlashCommandBuilder } from 'discord.js'; import markdownEscape from 'markdown-escape'; import usernameAutocomplete from '../core/autocomplete/username.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import cleanTable from '../core/interaction/cleanTable.js'; import infoEmbed from '../core/interaction/infoEmbed.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo } from '../types/accountInfo.js'; @@ -21,7 +21,7 @@ export default { .addStringOption(addUsernameOption), async execute(interaction) { - const account = await getBlitzAccount(interaction); + const account = await resolvePlayer(interaction); const { id, server } = account; const accounts = await getWargamingResponse( `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index ea98a9706..c83d7fce4 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -1,32 +1,12 @@ import { SlashCommandBuilder } from 'discord.js'; -import GenericAllStats from '../components/GenericAllStats.js'; -import NoData, { NoDataType } from '../components/NoData.js'; -import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js'; -import TierWeights, { TierWeightsRecord } from '../components/TierWeights.js'; -import TitleBar from '../components/TitleBar.js'; -import Wrapper from '../components/Wrapper.js'; -import { BLITZ_SERVERS } from '../constants/servers.js'; import tanksAutocomplete from '../core/autocomplete/tanks.js'; import usernameAutocomplete from '../core/autocomplete/username.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; -import getWN8 from '../core/blitz/getWN8.js'; -import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import resolveTankId from '../core/blitz/resolveTankId.js'; -import resolveTankName from '../core/blitz/resolveTankName.js'; -import sumStats from '../core/blitz/sumStats.js'; -import { Tier, tankopedia } from '../core/blitz/tankopedia.js'; -import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; -import { tankAverages } from '../core/blitzstars/tankAverages.js'; import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js'; -import getPeriodDataFromSubcommand from '../core/options/getPeriodDataFromSubcommand.js'; -import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; +import resolvePeriod from '../core/options/resolvePeriod.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { CommandRegistry } from '../events/interactionCreate.js'; -import { - AccountInfo, - AllStats, - SupplementaryStats, -} from '../types/accountInfo.js'; -import { PlayerClanData } from '../types/playerClanData.js'; +import stats, { StatType } from '../renderers/stats.js'; export default { inProduction: true, @@ -40,120 +20,15 @@ export default { ), async execute(interaction) { - const commandGroup = interaction.options.getSubcommandGroup()!; - const { id, server } = await getBlitzAccount(interaction); - let nameDiscriminator: string | undefined; - let image: string | undefined; + const commandGroup = interaction.options.getSubcommandGroup( + true, + ) as StatType; + const player = await resolvePlayer(interaction); + const period = resolvePeriod(interaction); const tankIdRaw = interaction.options.getString('tank')!; + const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : 0; - if (commandGroup === 'player') { - const clan = ( - await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, - ) - )[id]?.clan; - - if (clan) nameDiscriminator = `[${clan.tag}]`; - image = clan - ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` - : undefined; - } else if (commandGroup === 'tank') { - const tankId = resolveTankId(tankIdRaw); - nameDiscriminator = `(${resolveTankName(tankId)})`; - image = tankopedia[tankId].images.normal; - } - - const { statsName, start, end } = getPeriodDataFromSubcommand(interaction); - - const tankStats = await getTankStatsOverTime(server, id, start, end); - let stats: AllStats; - let supplementaryStats: SupplementaryStats; - let tierWeights: TierWeightsRecord; - - if (commandGroup === 'player') { - const entries = Object.entries(tankStats); - stats = sumStats(entries.map(([, stats]) => stats)); - const battles = entries.reduce((accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - const tankAverage = tankAverages[tankId]; - - return tankAverage ? accumulator + stats.battles : accumulator; - }, 0); - - supplementaryStats = { - WN8: - entries.reduce((accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - const tankAverage = tankAverages[tankId]; - - return tankAverage - ? accumulator + getWN8(tankAverage.all, stats) * stats.battles - : accumulator; - }, 0) / battles, - tier: - entries.reduce((accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - const tankAverage = tankAverages[tankId]; - - return tankAverage - ? accumulator + tankopedia[tankId].tier * stats.battles - : accumulator; - }, 0) / battles, - }; - tierWeights = entries.reduce( - (accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - const tier = tankopedia[tankId].tier as Tier; - - if (accumulator[tier]) { - accumulator[tier]! += stats.battles; - } else { - accumulator[tier] = stats.battles; - } - - return accumulator; - }, - {}, - ); - } else { - const tankId = resolveTankId(tankIdRaw); - stats = tankStats[tankId]; - - supplementaryStats = { - WN8: getWN8(tankAverages[tankId].all, tankStats[tankId]), - tier: tankopedia[tankId].tier, - }; - } - - const accountInfo = await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, - ); - - return ( - - - - {stats.battles === 0 && } - {stats.battles > 0 && commandGroup === 'player' && ( - - )} - {stats.battles > 0 && ( - - )} - - - - ); + return await stats(commandGroup, period, player, tankId); }, autocomplete: (interaction) => { diff --git a/src/commands/today.tsx b/src/commands/today.tsx index 9ea6bf384..0029ace74 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -6,7 +6,6 @@ import TitleBar from '../components/TitleBar.js'; import Wrapper from '../components/Wrapper.js'; import { BLITZ_SERVERS } from '../constants/servers.js'; import usernameAutocomplete from '../core/autocomplete/username.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getTankStats from '../core/blitz/getTankStats.js'; import getWN8 from '../core/blitz/getWN8.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; @@ -14,19 +13,21 @@ import resolveTankName from '../core/blitz/resolveTankName.js'; import sumStats from '../core/blitz/sumStats.js'; import { tankopedia } from '../core/blitz/tankopedia.js'; import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; -import last5AM from '../core/blitzstars/last5AM.js'; +import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo.js'; import { tankAverages } from '../core/blitzstars/tankAverages.js'; import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js'; import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo, AllStats } from '../types/accountInfo.js'; import { PlayerClanData } from '../types/playerClanData.js'; +import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; export default { inProduction: true, - inDevelopment: false, + inDevelopment: true, inPublic: true, command: new SlashCommandBuilder() @@ -35,12 +36,12 @@ export default { .addStringOption(addUsernameOption), async execute(interaction) { - const { id, server } = await getBlitzAccount(interaction); + const { id, server } = await resolvePlayer(interaction); const tankStatsOverTime = await getTankStatsOverTime( server, id, - last5AM().getTime() / 1000, - new Date().getTime() / 1000, + getTimeDaysAgo(0), + getPeriodNow(), ); const accountInfo = await getWargamingResponse( `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 2afe3fdb0..d05b64d89 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -2,11 +2,11 @@ import { SlashCommandBuilder } from 'discord.js'; import markdownEscape from 'markdown-escape'; import discord from '../../discord.json' assert { type: 'json' }; import usernameAutocomplete from '../core/autocomplete/username.js'; -import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import negativeEmbed from '../core/interaction/negativeEmbed.js'; import positiveEmbed from '../core/interaction/positiveEmbed.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; +import resolvePlayer from '../core/options/resolvePlayer.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo } from '../types/accountInfo.js'; @@ -23,7 +23,7 @@ export default { .addStringOption((option) => addUsernameOption(option).setRequired(true)), async execute(interaction) { - const blitzAccount = await getBlitzAccount(interaction); + const blitzAccount = await resolvePlayer(interaction); const { id, server } = blitzAccount; const accountInfo = await getWargamingResponse( `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, diff --git a/src/core/blitz/getBlitzClan.ts b/src/core/blitz/getBlitzClan.ts index 7d5c847c1..d511584fc 100644 --- a/src/core/blitz/getBlitzClan.ts +++ b/src/core/blitz/getBlitzClan.ts @@ -1,7 +1,7 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { BlitzServer } from '../../constants/servers.js'; +import { serverAndIdPattern } from '../options/resolvePlayer.js'; import errorWithCause from '../process/errorWithCause.js'; -import { serverAndIdPattern } from './getBlitzAccount.js'; import listClansPanServer from './listClansPanServer.js'; export default async function getBlitzClan( diff --git a/src/core/blitzstars/getPeriodNow.ts b/src/core/blitzstars/getPeriodNow.ts index 2be20c889..1f9a6734e 100644 --- a/src/core/blitzstars/getPeriodNow.ts +++ b/src/core/blitzstars/getPeriodNow.ts @@ -1,3 +1,3 @@ export default function getPeriodNow() { - return new Date().getTime() / 1000; + return Date.now() / 1000; } diff --git a/src/core/blitzstars/getPeriodStart.ts b/src/core/blitzstars/getPeriodStart.ts index 8bd3d7060..abff335a7 100644 --- a/src/core/blitzstars/getPeriodStart.ts +++ b/src/core/blitzstars/getPeriodStart.ts @@ -7,6 +7,6 @@ export default function getPeriodStart(period: Exclude) { } else if (period === 'today') { return getTimeDaysAgo(0); } else { - return getTimeDaysAgo(Number(period)); + return getTimeDaysAgo(parseInt(period)); } } diff --git a/src/core/blitzstars/getPlayerHistories.ts b/src/core/blitzstars/getPlayerHistories.ts index d52cfd151..3d2e0ed2e 100644 --- a/src/core/blitzstars/getPlayerHistories.ts +++ b/src/core/blitzstars/getPlayerHistories.ts @@ -88,5 +88,5 @@ export default async function getPlayerHistories( } } - return [...latestNodes, ...filtered, ...previousNodes] satisfies Histories; + return [...previousNodes, ...filtered, ...latestNodes] satisfies Histories; } diff --git a/src/core/blitzstars/getTankHistories.ts b/src/core/blitzstars/getTankHistories.ts index 6f4bca99c..21c324974 100644 --- a/src/core/blitzstars/getTankHistories.ts +++ b/src/core/blitzstars/getTankHistories.ts @@ -109,5 +109,5 @@ export default async function getTankHistories( }); } - return [...latestNodes, ...filtered, ...previousNodes] satisfies Histories; + return [...previousNodes, ...filtered, ...latestNodes] satisfies Histories; } diff --git a/src/core/blitzstars/getTankStatsOverTime.ts b/src/core/blitzstars/getTankStatsOverTime.ts index d69fc4f80..c1c789822 100644 --- a/src/core/blitzstars/getTankStatsOverTime.ts +++ b/src/core/blitzstars/getTankStatsOverTime.ts @@ -1,7 +1,7 @@ import { BlitzServer } from '../../constants/servers.js'; import { AllStats } from '../../types/accountInfo.js'; import getTankHistories, { - TankHistoriesRaw, + TankHistories, TankHistoryRaw, } from './getTankHistories.js'; @@ -45,7 +45,7 @@ export default async function getTankStatsOverTime( }); // sort them even though most won't be used - const tankSortedHistory: Record = {}; + const tankSortedHistory: Record = {}; const tankIds: number[] = []; history.forEach((node) => { if (!tankSortedHistory[node.tank_id]) { diff --git a/src/core/interaction/generateCustomId.ts b/src/core/interaction/generateCustomId.ts new file mode 100644 index 000000000..380573365 --- /dev/null +++ b/src/core/interaction/generateCustomId.ts @@ -0,0 +1,9 @@ +import { CacheType, ChatInputCommandInteraction } from 'discord.js'; + +export default function generateCustomId( + interaction: ChatInputCommandInteraction, +) { + return `${interaction.commandName}/${ + interaction.options.getSubcommandGroup() ?? '' + }/${interaction.options.getSubcommand() ?? ''}`; +} diff --git a/src/core/options/addPeriodSubCommands.ts b/src/core/options/addPeriodSubCommands.ts index b250c61fa..13cd63a2d 100644 --- a/src/core/options/addPeriodSubCommands.ts +++ b/src/core/options/addPeriodSubCommands.ts @@ -2,15 +2,15 @@ import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder, } from 'discord.js'; -import { PERIOD_NAMES } from './getPeriodDataFromSubcommand.js'; +import { PERIOD_NAMES } from './resolvePeriod.js'; export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom'; export const RELATIVE_PERIOD_NAMES: Record = { today: 'A day ago', - '30': '30 days ago', - '60': '60 days ago', - '90': '90 days ago', + 30: '30 days ago', + 60: '60 days ago', + 90: '90 days ago', career: 'Extent of data', custom: 'Custom period', }; @@ -26,13 +26,13 @@ export default function addPeriodSubCommands( extra(option.setName('today').setDescription(PERIOD_NAMES.today)), ) .addSubcommand((option) => - extra(option.setName('30').setDescription(PERIOD_NAMES['30'])), + extra(option.setName('30').setDescription(PERIOD_NAMES[30])), ) .addSubcommand((option) => - extra(option.setName('60').setDescription(PERIOD_NAMES['60'])), + extra(option.setName('60').setDescription(PERIOD_NAMES[60])), ) .addSubcommand((option) => - extra(option.setName('90').setDescription(PERIOD_NAMES['90'])), + extra(option.setName('90').setDescription(PERIOD_NAMES[90])), ) .addSubcommand((option) => extra(option.setName('career').setDescription(PERIOD_NAMES.career)), diff --git a/src/core/options/getPeriodDataFromSubcommand.ts b/src/core/options/resolvePeriod.ts similarity index 78% rename from src/core/options/getPeriodDataFromSubcommand.ts rename to src/core/options/resolvePeriod.ts index 65b9879c2..793d5e86f 100644 --- a/src/core/options/getPeriodDataFromSubcommand.ts +++ b/src/core/options/resolvePeriod.ts @@ -6,23 +6,30 @@ import { Period } from './addPeriodSubCommands.js'; export const PERIOD_NAMES: Record = { today: "Today's statistics", - '30': "30 days' statistics", - '60': "60 days' statistics", - '90': "90 days' statistics", + 30: "30 days' statistics", + 60: "60 days' statistics", + 90: "90 days' statistics", career: 'Career statistics', custom: 'Custom period', }; export const EVOLUTION_PERIOD_NAMES: Record = { today: "Today's evolution", - '30': "30 days' evolution", - '60': "60 days' evolution", - '90': "90 days' evolution", + 30: "30 days' evolution", + 60: "60 days' evolution", + 90: "90 days' evolution", career: 'Career evolution', custom: 'Custom period', }; -export default function getPeriodDataFromSubcommand( +export interface ResolvedPeriod { + statsName: string; + evolutionName: string; + start: number; + end: number; +} + +export default function resolvePeriod( interaction: ChatInputCommandInteraction, ) { const period = interaction.options.getSubcommand(true) as Period; @@ -48,5 +55,5 @@ export default function getPeriodDataFromSubcommand( end = getPeriodNow(); } - return { statsName, evolutionName, start, end }; + return { statsName, evolutionName, start, end } satisfies ResolvedPeriod; } diff --git a/src/core/blitz/getBlitzAccount.ts b/src/core/options/resolvePlayer.ts similarity index 78% rename from src/core/blitz/getBlitzAccount.ts rename to src/core/options/resolvePlayer.ts index e25cfda1a..487a24f07 100644 --- a/src/core/blitz/getBlitzAccount.ts +++ b/src/core/options/resolvePlayer.ts @@ -1,13 +1,18 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { BlitzServer } from '../../constants/servers.js'; -import errorWithCause from '../process/errorWithCause.js'; import listAccountsPanServer, { usernamePatternWithoutPosition, -} from './listAccountsPanServer.js'; +} from '../blitz/listAccountsPanServer.js'; +import errorWithCause from '../process/errorWithCause.js'; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; -export default async function getBlitzAccount( +export interface ResolvedPlayer { + server: BlitzServer; + id: number; +} + +export default async function resolvePlayer( interaction: ChatInputCommandInteraction, ) { const commandUsername = interaction.options.getString('username'); @@ -25,12 +30,18 @@ export default async function getBlitzAccount( if (serverAndIdPattern.test(username)) { const [server, accountId] = username.split('/'); - return { server: server as BlitzServer, id: Number(accountId) }; + return { + server: server as BlitzServer, + id: Number(accountId), + } satisfies ResolvedPlayer; } else { const accounts = await listAccountsPanServer(username); if (accounts[0]) { - return { server: accounts[0].server, id: accounts[0].account_id }; + return { + server: accounts[0].server, + id: accounts[0].account_id, + } satisfies ResolvedPlayer; } else { throw errorWithCause( 'Could not find user', diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index fb46359bf..dda1cb625 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -2,6 +2,7 @@ import { ActionRowBuilder, AutocompleteInteraction, ButtonBuilder, + ButtonInteraction, ButtonStyle, CacheType, ChatInputCommandInteraction, @@ -140,98 +141,111 @@ try { console.error(error); } +//#region Event type handlers +function handleAutocomplete(interaction: AutocompleteInteraction) { + const command = commands[interaction.commandName]; + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + + if (command.autocomplete) command.autocomplete(interaction); +} + +async function handleChatInputCommand( + interaction: ChatInputCommandInteraction, +) { + const command = commands[interaction.commandName]; + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + + const metadata = [ + `${interaction.user.id} (${interaction.user.username})`, + `${interaction.guildId}/${interaction.channelId} (${interaction.guild?.name})`, + new Date(), + ]; + + console.log( + `${interaction.toString()}\n${metadata + .map((item) => ` ${item}`) + .join('\n')}`, + ); + await interaction.deferReply(); + + try { + const result = await command.execute(interaction); + + if (command.handlesInteraction) return; + + const normalizedResult = Array.isArray(result) ? result : [result]; + const reply: InteractionEditReplyOptions = {}; + + if (psa.data) { + normalizedResult.push(warningEmbed(psa.data.title, psa.data.description)); + } + + await Promise.all( + normalizedResult.map(async (item) => { + if (item instanceof EmbedBuilder) { + if (!reply.embeds) reply.embeds = []; + reply.embeds.push(item); + } else if (item instanceof ButtonBuilder) { + if (!reply.components) + reply.components = [new ActionRowBuilder()]; + ( + reply.components[0] as ActionRowBuilder + ).addComponents(item); + } else { + const image = await render(item!); + if (!reply.files) reply.files = []; + reply.files.push(image); + } + }), + ); + + await interaction.editReply(reply); + } catch (error) { + const actionRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('Get Help on Discord Server') + .setURL('https://discord.gg/nDt7AjGJQH') + .setStyle(ButtonStyle.Link), + ); + + await interaction.editReply({ + embeds: [ + negativeEmbed( + (error as Error).message, + `${(error as Error).cause ?? 'No further information is available.'}`, + ), + ], + components: [actionRow], + }); + + handleError(error as Error, interaction.commandName); + } +} + +function handleButton(interaction: ButtonInteraction) { + console.log(interaction.customId); + interaction.message.edit({ + files: [], + }); +} +//#endregion + export default async function interactionCreate( interaction: Interaction, ) { if (interaction.isAutocomplete()) { - const command = commands[interaction.commandName]; - - if (!command) { - console.error( - `No command matching ${interaction.commandName} was found.`, - ); - return; - } - - if (command.autocomplete) command.autocomplete(interaction); + handleAutocomplete(interaction); } else if (interaction.isChatInputCommand()) { - const command = commands[interaction.commandName]; - - if (!command) { - console.error( - `No command matching ${interaction.commandName} was found.`, - ); - return; - } - - const metadata = [ - `${interaction.user.id} (${interaction.user.username})`, - `${interaction.guildId}/${interaction.channelId} (${interaction.guild?.name})`, - new Date(), - ]; - - console.log( - `${interaction.toString()}\n${metadata - .map((item) => ` ${item}`) - .join('\n')}`, - ); - await interaction.deferReply(); - - try { - const result = await command.execute(interaction); - - if (command.handlesInteraction) return; - - const normalizedResult = Array.isArray(result) ? result : [result]; - const reply: InteractionEditReplyOptions = {}; - - if (psa.data) { - normalizedResult.push( - warningEmbed(psa.data.title, psa.data.description), - ); - } - - await Promise.all( - normalizedResult.map(async (item) => { - if (item instanceof EmbedBuilder) { - if (!reply.embeds) reply.embeds = []; - reply.embeds.push(item); - } else if (item instanceof ButtonBuilder) { - if (!reply.components) - reply.components = [new ActionRowBuilder()]; - ( - reply.components[0] as ActionRowBuilder - ).addComponents(item); - } else { - const image = await render(item!); - if (!reply.files) reply.files = []; - reply.files.push(image); - } - }), - ); - - await interaction.editReply(reply); - } catch (error) { - const actionRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel('Get Help on Discord Server') - .setURL('https://discord.gg/nDt7AjGJQH') - .setStyle(ButtonStyle.Link), - ); - - await interaction.editReply({ - embeds: [ - negativeEmbed( - (error as Error).message, - `${ - (error as Error).cause ?? 'No further information is available.' - }`, - ), - ], - components: [actionRow], - }); - - handleError(error as Error, interaction.commandName); - } + handleChatInputCommand(interaction); + } else if (interaction.isButton()) { + handleButton(interaction); } } diff --git a/src/renderers/stats.tsx b/src/renderers/stats.tsx new file mode 100644 index 000000000..ee9493bee --- /dev/null +++ b/src/renderers/stats.tsx @@ -0,0 +1,140 @@ +import GenericAllStats from '../components/GenericAllStats.js'; +import NoData, { NoDataType } from '../components/NoData.js'; +import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js'; +import TierWeights, { TierWeightsRecord } from '../components/TierWeights.js'; +import TitleBar from '../components/TitleBar.js'; +import Wrapper from '../components/Wrapper.js'; +import { BLITZ_SERVERS } from '../constants/servers.js'; +import getWN8 from '../core/blitz/getWN8.js'; +import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; +import resolveTankName from '../core/blitz/resolveTankName.js'; +import sumStats from '../core/blitz/sumStats.js'; +import { Tier, tankopedia } from '../core/blitz/tankopedia.js'; +import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; +import { tankAverages } from '../core/blitzstars/tankAverages.js'; +import { ResolvedPeriod } from '../core/options/resolvePeriod.js'; +import { ResolvedPlayer } from '../core/options/resolvePlayer.js'; +import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; +import { + AccountInfo, + AllStats, + SupplementaryStats, +} from '../types/accountInfo.js'; +import { PlayerClanData } from '../types/playerClanData.js'; + +export type StatType = 'player' | 'tank'; + +export default async function stats( + type: Type, + { start, end, statsName }: ResolvedPeriod, + { server, id }: ResolvedPlayer, + tankId: Type extends 'tank' ? number : never, +) { + let nameDiscriminator: string | undefined; + let image: string | undefined; + + if (type === 'player') { + const clan = ( + await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, + ) + )[id]?.clan; + + if (clan) nameDiscriminator = `[${clan.tag}]`; + image = clan + ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` + : undefined; + } else if (type === 'tank') { + nameDiscriminator = `(${resolveTankName(tankId)})`; + image = tankopedia[tankId].images.normal; + } + + const tankStats = await getTankStatsOverTime(server, id, start, end); + let stats: AllStats; + let supplementaryStats: SupplementaryStats; + let tierWeights: TierWeightsRecord; + + if (type === 'player') { + const entries = Object.entries(tankStats); + stats = sumStats(entries.map(([, stats]) => stats)); + const battles = entries.reduce((accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + const tankAverage = tankAverages[tankId]; + + return tankAverage ? accumulator + stats.battles : accumulator; + }, 0); + + supplementaryStats = { + WN8: + entries.reduce((accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + const tankAverage = tankAverages[tankId]; + + return tankAverage + ? accumulator + getWN8(tankAverage.all, stats) * stats.battles + : accumulator; + }, 0) / battles, + tier: + entries.reduce((accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + const tankAverage = tankAverages[tankId]; + + return tankAverage + ? accumulator + tankopedia[tankId].tier * stats.battles + : accumulator; + }, 0) / battles, + }; + tierWeights = entries.reduce( + (accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + const tier = tankopedia[tankId].tier as Tier; + + if (accumulator[tier]) { + accumulator[tier]! += stats.battles; + } else { + accumulator[tier] = stats.battles; + } + + return accumulator; + }, + {}, + ); + } else { + stats = tankStats[tankId]; + + supplementaryStats = { + WN8: getWN8(tankAverages[tankId].all, tankStats[tankId]), + tier: tankopedia[tankId].tier, + }; + } + + const accountInfo = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, + ); + + return ( + + + + {stats.battles === 0 && } + {stats.battles > 0 && type === 'player' && ( + + )} + {stats.battles > 0 && ( + + )} + + + + ); +} diff --git a/tsconfig.json b/tsconfig.json index 8d1a8a7e4..fa21483dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "resolveJsonModule": true, "jsx": "react-jsx" }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "src/renderers/stats.tsx"] }