diff --git a/scripts/build.js b/scripts/build.js index 70ee2d8f3..afeb9b893 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -20,7 +20,7 @@ build({ }, bundle: true, - sourcemap: isProd, + sourcemap: true, minifyIdentifiers: false, // causes errors minifySyntax: isProd, minifyWhitespace: isProd, diff --git a/src/commands/eligible.ts b/src/commands/eligible.ts index d4bd25ca8..13615f71c 100644 --- a/src/commands/eligible.ts +++ b/src/commands/eligible.ts @@ -5,7 +5,7 @@ import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import sumStats from '../core/blitz/sumStats.js'; import { tankopedia } from '../core/blitz/tankopedia.js'; import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; -import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; +import getPeriodStart from '../core/blitzstars/getPeriodStart.js'; import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; import { tankAverages } from '../core/blitzstars/tankAverages.js'; import cleanTable, { TableInput } from '../core/interaction/cleanTable.js'; @@ -62,7 +62,7 @@ export default { const tankStatsOverTime = await getTankStatsOverTime( server, id, - getPeriodicStart('30'), + getPeriodStart('30'), getPeriodNow(), ); const entries = Object.entries(tankStatsOverTime); diff --git a/src/commands/ping.ts b/src/commands/ping.ts index 30c975323..1493876ad 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -4,7 +4,7 @@ import { CommandRegistry } from '../events/interactionCreate.js'; export default { inProduction: true, - inDevelopment: true, + inDevelopment: false, inPublic: true, handlesInteraction: true, diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index 141c0c8af..bae68fd75 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -5,26 +5,26 @@ 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 tanksAutocomplete from '../core/autocomplete/tanks.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'; +import resolveTankId from '../core/blitz/resolveTankId.js'; +import resolveTankName from '../core/blitz/resolveTankName.js'; import sumStats from '../core/blitz/sumStats.js'; import { tankopedia } from '../core/blitz/tankopedia.js'; import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; -import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; -import getPeriodStartFromDaysAgo from '../core/blitzstars/getPeriodStartFromDaysAgo.js'; +import getPeriodStart from '../core/blitzstars/getPeriodStart.js'; import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; +import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo.js'; import { tankAverages } from '../core/blitzstars/tankAverages.js'; import cmdName from '../core/interaction/cmdName.js'; -import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js'; -import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js'; -import addCustomPeriodOption from '../core/options/addCustomPeriodOption.js'; -import addPeriodChoices, { - StatPeriod, - statPeriodNames, -} from '../core/options/addPeriodChoices.js'; +import addPeriodSubCommands, { + PERIOD_NAMES, + Period, +} from '../core/options/addPeriodSubCommands.js'; +import addTankChoices from '../core/options/addTankChoices.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; @@ -41,152 +41,142 @@ export default { inPublic: true, command: new SlashCommandBuilder() + // TODO: embed cmdName into interaction create .setName(cmdName('stats')) - .setDescription("Gets the user's in-game statistics") - .addSubcommand((option) => - option - .setName('period') - .setDescription('Pick from a predetermined periods') - .addStringOption(addPeriodChoices) - .addStringOption(addUsernameOption), + .setDescription('In-game statistics') + .addSubcommandGroup((option) => + addPeriodSubCommands(option, (option) => + option.addStringOption(addUsernameOption), + ) + .setName('player') + .setDescription("Player's statistics"), ) - .addSubcommand((option) => - addCustomPeriodOption(option) - .setName('customperiod') - .setDescription('Custom periods') - .addStringOption(addUsernameOption), + .addSubcommandGroup((option) => + addPeriodSubCommands(option, (option) => + option + .addStringOption(addTankChoices) + .addStringOption(addUsernameOption), + ) + .setName('tank') + .setDescription("Tank's statistics"), ), async execute(interaction) { - const period = interaction.options.getString('period') as StatPeriod; + const commandGroup = interaction.options.getSubcommandGroup()!; const { id, server } = await getBlitzAccount(interaction); - const accountInfo = await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, - ); - const clanData = await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, - ); - let stats: AllStats; - let supplementaryStats: SupplementaryStats | undefined; - let hasWN8AccumulatedAtAll = false; - const isCustomPeriod = - interaction.options.getSubcommand() === 'customperiod'; - let periodName: string; - const startRaw = interaction.options.getInteger('start')!; - const endRaw = interaction.options.getInteger('end')!; - const start = Math.max(startRaw, endRaw); - const end = Math.min(startRaw, endRaw); + let nameDiscriminator: string | undefined; + let image: string | undefined; + const tankIdRaw = interaction.options.getString('tank')!; - if (isCustomPeriod) { - periodName = `${start} to ${end} days ago`; - } else { - periodName = statPeriodNames[period]; + 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 = await resolveTankId(tankIdRaw); + nameDiscriminator = `(${resolveTankName(tankId)})`; + image = tankopedia[tankId].images.normal; } - if (period === 'career') { - const tankStats = await getTankStats(server, id); - stats = accountInfo[id].statistics.all; - const totalBattles = tankStats.reduce( - (accumulator, { all }) => accumulator + all.battles, - 0, - ); + const period = interaction.options.getSubcommand(true) as Period; + let periodName: string; + let periodStart: number; + let periodEnd: number; - supplementaryStats = { - tier: - tankStats.reduce( - (accumulator, { tank_id, all }) => - accumulator + tankopedia[tank_id].tier * all.battles, - 0, - ) / totalBattles, - WN8: - tankStats.reduce((accumulator, { tank_id, all }) => { - if (tankAverages[tank_id]) { - const tankWN8 = getWN8(tankAverages[tank_id].all, all); + if (period === 'custom') { + const startRaw = interaction.options.getInteger('start')!; + const endRaw = interaction.options.getInteger('end')!; + const start = Math.max(startRaw, endRaw); + const end = Math.min(startRaw, endRaw); - if (isNaN(tankWN8)) return accumulator; - - hasWN8AccumulatedAtAll = true; - - return accumulator + tankWN8 * all.battles; - } else return accumulator; - }, 0) / totalBattles, - }; + periodName = `${start} to ${end} days ago`; + periodStart = getTimeDaysAgo(start); + periodEnd = getTimeDaysAgo(end); } else { - const tankStatsOverTime = await getTankStatsOverTime( - server, - id, - isCustomPeriod - ? getPeriodStartFromDaysAgo(start) - : getPeriodicStart(period), - isCustomPeriod ? getPeriodStartFromDaysAgo(end) : getPeriodNow(), - ); - const entries = Object.entries(tankStatsOverTime); - const totalBattles = entries.reduce( + periodName = PERIOD_NAMES[period]; + periodStart = getPeriodStart(period); + periodEnd = getPeriodNow(); + } + + const tankStats = await getTankStatsOverTime( + server, + id, + periodStart, + periodEnd, + ); + let stats: AllStats; + let supplementaryStats: SupplementaryStats; + + if (commandGroup === 'player') { + const entries = Object.entries(tankStats); + stats = sumStats(entries.map(([, stats]) => stats)); + const battles = entries.reduce( (accumulator, [, stats]) => accumulator + stats.battles, 0, ); - supplementaryStats = { - tier: - entries.reduce((accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - const tankTier = tankopedia[tankId].tier; - - return accumulator + tankTier * stats.battles; - }, 0) / totalBattles, WN8: - entries.reduce((accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - - // edge case where new tanks don't have averages - if (tankAverages[tankId]) { - const tankWN8 = getWN8(tankAverages[tankId].all, stats); - - if (isNaN(tankWN8)) return accumulator; - - hasWN8AccumulatedAtAll = true; - - return accumulator + tankWN8 * stats.battles; - } else return accumulator; - }, 0) / totalBattles, + entries.reduce( + (accumulator, [tankIdString, stats]) => + accumulator + + getWN8(tankAverages[parseInt(tankIdString)].all, stats) * + stats.battles, + 0, + ) / battles, + tier: + entries.reduce( + (accumulator, [tankIdString, stats]) => + accumulator + + tankopedia[parseInt(tankIdString)].tier * stats.battles, + 0, + ) / battles, + }; + } else if (commandGroup === 'tank') { + const tankId = await resolveTankId(tankIdRaw); + stats = tankStats[tankId]; + supplementaryStats = { + WN8: getWN8(tankAverages[tankId].all, tankStats[tankId]), + tier: tankopedia[tankId].tier, }; - - stats = sumStats(entries.map(([, stats]) => stats)); } - if (!hasWN8AccumulatedAtAll) supplementaryStats.WN8 = undefined; + const accountInfo = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, + ); - return [ + return ( - {stats.battles === 0 && } - {stats.battles > 0 && ( + {stats!.battles === 0 && } + {stats!.battles > 0 && ( )} - , - fullBlitzStarsStats(server, accountInfo[id].nickname), - supportBlitzStars, - ]; + + ); }, - autocomplete: usernameAutocomplete, + autocomplete: (interaction) => { + usernameAutocomplete(interaction); + tanksAutocomplete(interaction); + }, } satisfies CommandRegistry; diff --git a/src/commands/tankstats.tsx b/src/commands/tankstats.tsx deleted file mode 100644 index 3c863e4ea..000000000 --- a/src/commands/tankstats.tsx +++ /dev/null @@ -1,140 +0,0 @@ -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 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 getTankStats from '../core/blitz/getTankStats.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 { tankopedia } from '../core/blitz/tankopedia.js'; -import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; -import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; -import getPeriodStartFromDaysAgo from '../core/blitzstars/getPeriodStartFromDaysAgo.js'; -import getTankStatsOverTime, { - emptyAllStats, -} from '../core/blitzstars/getTankStatsOverTime.js'; -import { tankAverages } from '../core/blitzstars/tankAverages.js'; -import cmdName from '../core/interaction/cmdName.js'; -import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js'; -import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js'; -import addCustomPeriodOption from '../core/options/addCustomPeriodOption.js'; -import addPeriodChoices, { - StatPeriod, - statPeriodNames, -} from '../core/options/addPeriodChoices.js'; -import addTankChoices from '../core/options/addTankChoices.js'; -import addUsernameOption from '../core/options/addUsernameOption.js'; -import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; -import { CommandRegistry } from '../events/interactionCreate.js'; -import { AccountInfo, AllStats } from '../types/accountInfo.js'; - -export default { - inProduction: true, - inDevelopment: true, - inPublic: true, - - command: new SlashCommandBuilder() - .setName(cmdName('tankstats')) - .setDescription('Stats for a tank over a period') - .addSubcommand((option) => - option - .setName('period') - .setDescription('Pick from a predetermined periods') - .addStringOption(addTankChoices) - .addStringOption(addPeriodChoices) - .addStringOption(addUsernameOption), - ) - .addSubcommand((option) => - addCustomPeriodOption(option) - .setName('customperiod') - .addStringOption(addTankChoices) - .setDescription('Custom periods') - .addStringOption(addUsernameOption), - ), - - async execute(interaction) { - const tank = await resolveTankId( - interaction, - interaction.options.getString('tank')!, - ); - const period = interaction.options.getString('period') as StatPeriod; - const blitzAccount = await getBlitzAccount(interaction); - const { id, server } = blitzAccount; - const accountInfo = await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, - ); - const tankStats = await getTankStats(server, id); - let stats: AllStats; - const tankId = Number(tank); - const isCustomPeriod = - interaction.options.getSubcommand() === 'customperiod'; - let periodName: string; - const startRaw = interaction.options.getInteger('start')!; - const endRaw = interaction.options.getInteger('end')!; - const start = Math.max(startRaw, endRaw); - const end = Math.min(startRaw, endRaw); - - if (isCustomPeriod) { - periodName = `${start} to ${end} days ago`; - } else { - periodName = statPeriodNames[period]; - } - - if (period === 'career') { - stats = - tankStats.find((stats) => stats.tank_id === tankId)?.all ?? - emptyAllStats; - } else { - const tankStatsOverTime = await getTankStatsOverTime( - server, - id, - isCustomPeriod - ? getPeriodStartFromDaysAgo(start) - : getPeriodicStart(period), - isCustomPeriod ? getPeriodStartFromDaysAgo(end) : getPeriodNow(), - ); - - stats = tankStatsOverTime[tankId] ?? emptyAllStats; - } - - return [ - - - - {stats.battles === 0 && } - {stats.battles > 0 && ( - - )} - - - , - fullBlitzStarsStats(server, accountInfo[id].nickname), - supportBlitzStars, - ]; - }, - - autocomplete: (interaction) => { - tanksAutocomplete(interaction); - usernameAutocomplete(interaction); - }, -} satisfies CommandRegistry; diff --git a/src/commands/today.tsx b/src/commands/today.tsx index da78dccaf..b30fd4a26 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -27,7 +27,7 @@ import { PlayerClanData } from '../types/playerClanData.js'; export default { inProduction: true, - inDevelopment: true, + inDevelopment: false, inPublic: true, command: new SlashCommandBuilder() diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index e71e6ff8f..097c41ac8 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -1,12 +1,8 @@ -import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { go } from 'fuzzysort'; import errorWithCause from '../process/errorWithCause.js'; import { TANKS, tankopedia } from './tankopedia.js'; -export default async function resolveTankId( - interaction: ChatInputCommandInteraction, - tank: string, -) { +export default async function resolveTankId(tank: string) { const number = Number(tank); if (Number.isNaN(number)) { diff --git a/src/core/blitzstars/getPeriodStart.ts b/src/core/blitzstars/getPeriodStart.ts index 845d94ba9..8bd3d7060 100644 --- a/src/core/blitzstars/getPeriodStart.ts +++ b/src/core/blitzstars/getPeriodStart.ts @@ -1,18 +1,12 @@ -import { StatPeriod } from '../options/addPeriodChoices.js'; -import getPeriodStartFromDaysAgo from './getPeriodStartFromDaysAgo.js'; +import { Period } from '../options/addPeriodSubCommands.js'; +import getTimeDaysAgo from './getTimeDaysAgo.js'; -export default function getPeriodicStart(period: StatPeriod) { +export default function getPeriodStart(period: Exclude) { if (period === 'career') { return -Infinity; } else if (period === 'today') { - const now = new Date(); - now.setHours(5, 0, 0, 0); - - // If it's after 5:00 AM today, subtract one day - if (now.getTime() > Date.now()) now.setDate(now.getDate() - 1); - - return now.getTime() / 1000; + return getTimeDaysAgo(0); } else { - return getPeriodStartFromDaysAgo(Number(period)); + return getTimeDaysAgo(Number(period)); } } diff --git a/src/core/blitzstars/getPeriodStartFromDaysAgo.ts b/src/core/blitzstars/getPeriodStartFromDaysAgo.ts deleted file mode 100644 index 909f87735..000000000 --- a/src/core/blitzstars/getPeriodStartFromDaysAgo.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default function getPeriodStartFromDaysAgo(period: number) { - const daysAgo = 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/blitzstars/getTimeDaysAgo.ts b/src/core/blitzstars/getTimeDaysAgo.ts new file mode 100644 index 000000000..fc66a9171 --- /dev/null +++ b/src/core/blitzstars/getTimeDaysAgo.ts @@ -0,0 +1,12 @@ +export default function getTimeDaysAgo(period: number) { + const now = new Date(); + now.setHours(5, 0, 0, 0); + + // if it's after 5:00 AM today, subtract one day + if (now.getTime() > Date.now()) now.setDate(now.getDate() - 1); + + // subtract the period number of days from now + now.setDate(now.getDate() - period); + + return now.getTime() / 1000; +} diff --git a/src/core/options/addCustomPeriodOption.ts b/src/core/options/addCustomPeriodOption.ts deleted file mode 100644 index 4ec04c6b8..000000000 --- a/src/core/options/addCustomPeriodOption.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SlashCommandSubcommandBuilder } from 'discord.js'; - -export default function addCustomPeriodOption( - option: SlashCommandSubcommandBuilder, -) { - return option - .addIntegerOption((option) => - option - .setName('start') - .setDescription('Start of period in days ago from today') - .setMinValue(0) - .setRequired(true), - ) - .addIntegerOption((option) => - option - .setName('end') - .setDescription('End of period in days ago from today') - .setMinValue(0) - .setRequired(true), - ); -} diff --git a/src/core/options/addPeriodChoices.ts b/src/core/options/addPeriodChoices.ts deleted file mode 100644 index a724276d5..000000000 --- a/src/core/options/addPeriodChoices.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 addPeriodChoices(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/core/options/addPeriodSubCommands.ts b/src/core/options/addPeriodSubCommands.ts new file mode 100644 index 000000000..1bd9efdfc --- /dev/null +++ b/src/core/options/addPeriodSubCommands.ts @@ -0,0 +1,60 @@ +import { + SlashCommandSubcommandBuilder, + SlashCommandSubcommandGroupBuilder, +} from 'discord.js'; + +export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom'; + +export const PERIOD_NAMES: Record = { + today: "Today's statistics", + '30': "30 days' statistics", + '60': "60 days' statistics", + '90': "90 days' statistics", + career: 'Career statistics', + custom: 'Custom period', +}; + +export default function addPeriodSubCommands( + option: SlashCommandSubcommandGroupBuilder, + extra: ( + option: SlashCommandSubcommandBuilder, + ) => SlashCommandSubcommandBuilder = (option) => option, +) { + return option + .addSubcommand((option) => + extra(option.setName('today').setDescription(PERIOD_NAMES.today)), + ) + .addSubcommand((option) => + extra(option.setName('30').setDescription(PERIOD_NAMES['30'])), + ) + .addSubcommand((option) => + extra(option.setName('60').setDescription(PERIOD_NAMES['60'])), + ) + .addSubcommand((option) => + extra(option.setName('90').setDescription(PERIOD_NAMES['90'])), + ) + .addSubcommand((option) => + extra(option.setName('career').setDescription(PERIOD_NAMES.career)), + ) + .addSubcommand((option) => + extra( + option + .setName('custom') + .setDescription(PERIOD_NAMES.custom) + .addIntegerOption((option) => + option + .setName('start') + .setDescription('Start of period in days ago from today') + .setMinValue(0) + .setRequired(true), + ) + .addIntegerOption((option) => + option + .setName('end') + .setDescription('End of period in days ago from today') + .setMinValue(0) + .setRequired(true), + ), + ), + ); +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 43585b7d8..233470bcc 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -27,7 +27,6 @@ import searchclans from '../commands/searchclans.js'; import searchplayers from '../commands/searchplayers.js'; import searchtanks from '../commands/searchtanks.js'; import stats from '../commands/stats.js'; -import tankstats from '../commands/tankstats.js'; import today from '../commands/today.js'; import verify from '../commands/verify.js'; import negativeEmbed from '../core/interaction/negativeEmbed.js'; @@ -77,7 +76,6 @@ const commands = ( searchplayers, searchtanks, stats, - tankstats, today, verify, ping,