diff --git a/src/commands/breakdown.tsx b/src/commands/breakdown.tsx index 6e7c3a90e..e635142b8 100644 --- a/src/commands/breakdown.tsx +++ b/src/commands/breakdown.tsx @@ -35,7 +35,7 @@ import resolvePlayerFromCommand, { import calculateWN8 from '../core/statistics/calculateWN8'; import { StatFilters, filterStats } from '../core/statistics/filterStats'; import getWN8Percentile from '../core/statistics/getWN8Percentile'; -import { CommandRegistryPromisable } from '../events/interactionCreate'; +import { CommandRegistry } from '../events/interactionCreate'; const ROWS_PER_PAGE = 8; const MAX_PAGES = 9; @@ -290,7 +290,7 @@ export async function renderBreakdown( } } -export const breakdownCommand = new Promise( +export const breakdownCommand = new Promise( async (resolve) => { resolve({ inProduction: true, diff --git a/src/commands/debug.ts b/src/commands/debug.ts index f0a10993d..57452999b 100644 --- a/src/commands/debug.ts +++ b/src/commands/debug.ts @@ -6,33 +6,35 @@ import { CommandRegistry } from '../events/interactionCreate'; const executionStart = new Date().getTime(); -export const debugCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const debugCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: createLocalizedCommand('debug'), + command: createLocalizedCommand('debug'), - async handler(interaction) { - const { t } = translator(interaction.locale); - const currentTime = new Date().getTime(); - const uptime = currentTime - executionStart; - const list = [ - [t`bot.commands.debug.body.version`, packageJSON.version], - [ - t`bot.commands.debug.body.shard`, - client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`, - ], - [ - t`bot.commands.debug.body.uptime`, - `${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor( - (uptime / 1000 / 60) % 60, - )}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor( - uptime % 1000, - )}ms`, - ], - ]; - return t`bot.commands.debug.body.title${list - .map(([key, value]) => `- ${key}: ${value}`) - .join('\n')}`; - }, -}; + async handler(interaction) { + const { t } = translator(interaction.locale); + const currentTime = new Date().getTime(); + const uptime = currentTime - executionStart; + const list = [ + [t`bot.commands.debug.body.version`, packageJSON.version], + [ + t`bot.commands.debug.body.shard`, + client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`, + ], + [ + t`bot.commands.debug.body.uptime`, + `${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor( + (uptime / 1000 / 60) % 60, + )}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor( + uptime % 1000, + )}ms`, + ], + ]; + return t`bot.commands.debug.body.title${list + .map(([key, value]) => `- ${key}: ${value}`) + .join('\n')}`; + }, + }); +}); diff --git a/src/commands/eligible.ts b/src/commands/eligible.ts index 8225885e6..33fcc5812 100644 --- a/src/commands/eligible.ts +++ b/src/commands/eligible.ts @@ -1,198 +1,205 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { getAccountInfo } from '../core/blitz/getAccountInfo'; -import getPeriodNow from '../core/blitzkrieg/getPeriodNow'; -import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo'; -import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions'; -import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; -import { tankAverages } from '../core/blitzstars/tankAverages'; -import addUsernameChoices from '../core/discord/addUsernameChoices'; -import embedNegative from '../core/discord/embedNegative'; -import embedPositive from '../core/discord/embedPositive'; -import markdownTable, { TableInput } from '../core/discord/markdownTable'; -import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; -import calculateWN8 from '../core/statistics/calculateWN8'; -import sumStats from '../core/statistics/sumStats'; -import { CommandRegistry } from '../events/interactionCreate'; -import { PossiblyPromise } from '../types/possiblyPromise'; +// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __ +// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ / +// | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' / +// | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | < +// _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \ +// |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\ -export type SkilledClan = 'SKLLD' | 'SMRI'; +// import { SlashCommandBuilder } from 'discord.js'; +// import { getAccountInfo } from '../core/blitz/getAccountInfo'; +// import getPeriodNow from '../core/blitzkrieg/getPeriodNow'; +// import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo'; +// import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions'; +// import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; +// import { tankAverages } from '../core/blitzstars/tankAverages'; +// import addUsernameChoices from '../core/discord/addUsernameChoices'; +// import embedNegative from '../core/discord/embedNegative'; +// import embedPositive from '../core/discord/embedPositive'; +// import markdownTable, { TableInput } from '../core/discord/markdownTable'; +// import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; +// import calculateWN8 from '../core/statistics/calculateWN8'; +// import sumStats from '../core/statistics/sumStats'; +// import { CommandRegistry } from '../events/interactionCreate'; +// import { PossiblyPromise } from '../types/possiblyPromise'; -export const SKILLED_CLANS: Record = { - SKLLD: 'Skilled', - SMRI: 'Samurai', -}; +// export type SkilledClan = 'SKLLD' | 'SMRI'; -export const eligibleCommand: CommandRegistry = { - inProduction: true, - inPublic: false, +// export const SKILLED_CLANS: Record = { +// SKLLD: 'Skilled', +// SMRI: 'Samurai', +// }; - command: new SlashCommandBuilder() - .setName('eligible') - .setDescription('Checks eligibility for any Skilled clan') - .addStringOption((option) => - option - .setName('clan') - .setDescription('The clan to check') - .setChoices( - { - name: SKILLED_CLANS.SKLLD, - value: 'SKLLD' satisfies SkilledClan, - }, - { name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan }, - ) - .setRequired(true), - ) - .addStringOption(addUsernameChoices), +// export const eligibleCommand: CommandRegistry = { +// inProduction: true, +// inPublic: false, - async handler(interaction) { - const clan = interaction.options.getString('clan') as SkilledClan; - const { id, region: region } = await resolvePlayerFromCommand(interaction); - const accountInfo = await getAccountInfo(region, id); - const problems: TableInput = []; - const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`; - let isEligible = false; - let body = ''; +// command: new SlashCommandBuilder() +// .setName('eligible') +// .setDescription('Checks eligibility for any Skilled clan') +// .addStringOption((option) => +// option +// .setName('clan') +// .setDescription('The clan to check') +// .setChoices( +// { +// name: SKILLED_CLANS.SKLLD, +// value: 'SKLLD' satisfies SkilledClan, +// }, +// { name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan }, +// ) +// .setRequired(true), +// ) +// .addStringOption(addUsernameChoices), - if (clan === 'SKLLD') { - const { diff: diffed } = await getStatsInPeriod( - region, - id, - getTimeDaysAgo(region, 30), - getPeriodNow(), - ); - const entries = Object.entries(diffed); - const stats = sumStats(entries.map(([, stats]) => stats)); - const averageTier = - (await entries.reduce>( - async (accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - const tankDefinition = (await tankDefinitions)[tankId]; +// async handler(interaction) { +// const clan = interaction.options.getString('clan') as SkilledClan; +// const { id, region: region } = await resolvePlayerFromCommand(interaction); +// const accountInfo = await getAccountInfo(region, id); +// const problems: TableInput = []; +// const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`; +// let isEligible = false; +// let body = ''; - if (!tankDefinition) return accumulator; +// if (clan === 'SKLLD') { +// const { diff: diffed } = await getStatsInPeriod( +// region, +// id, +// getTimeDaysAgo(region, 30), +// getPeriodNow(), +// ); +// const entries = Object.entries(diffed); +// const stats = sumStats(entries.map(([, stats]) => stats)); +// const averageTier = +// (await entries.reduce>( +// async (accumulator, [tankIdString, stats]) => { +// const tankId = parseInt(tankIdString); +// const tankDefinition = (await tankDefinitions)[tankId]; - const tankTier = tankDefinition.tier; +// if (!tankDefinition) return accumulator; - return (await accumulator) + tankTier * stats.battles; - }, - 0, - )) / - (await entries.reduce>( - async (accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); - const tankDefinition = (await tankDefinitions)[tankId]; +// const tankTier = tankDefinition.tier; - if (!tankDefinition) return accumulator; +// return (await accumulator) + tankTier * stats.battles; +// }, +// 0, +// )) / +// (await entries.reduce>( +// async (accumulator, [tankIdString, stats]) => { +// const tankId = parseInt(tankIdString); +// const tankDefinition = (await tankDefinitions)[tankId]; - return stats.battles; - }, - 0, - )); - const WN8 = - (await entries.reduce>( - async (accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); +// if (!tankDefinition) return accumulator; - // edge case where new tanks don't have averages - if ((await tankAverages)[tankId]) { - const tankWN8 = calculateWN8( - (await tankAverages)[tankId].all, - stats, - ); +// return stats.battles; +// }, +// 0, +// )); +// const WN8 = +// (await entries.reduce>( +// async (accumulator, [tankIdString, stats]) => { +// const tankId = parseInt(tankIdString); - if (isNaN(tankWN8)) return accumulator; - return (await accumulator) + tankWN8 * stats.battles; - } else return accumulator; - }, - 0, - )) / stats.battles; - const tier10Damage = - (await entries.reduce>( - async (accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); +// // edge case where new tanks don't have averages +// if ((await tankAverages)[tankId]) { +// const tankWN8 = calculateWN8( +// (await tankAverages)[tankId].all, +// stats, +// ); - if ((await tankDefinitions)[tankId]?.tier === 10) { - const tankStats = diffed[tankId]; - return (await accumulator) + tankStats.damage_dealt; - } else return accumulator; - }, - 0, - )) / - (await entries.reduce>( - async (accumulator, [tankIdString, stats]) => { - const tankId = parseInt(tankIdString); +// if (isNaN(tankWN8)) return accumulator; +// return (await accumulator) + tankWN8 * stats.battles; +// } else return accumulator; +// }, +// 0, +// )) / stats.battles; +// const tier10Damage = +// (await entries.reduce>( +// async (accumulator, [tankIdString, stats]) => { +// const tankId = parseInt(tankIdString); - if ((await tankDefinitions)[tankId]?.tier === 10) { - const tankStats = diffed[tankId]; - return (await accumulator) + tankStats.battles; - } else return accumulator; - }, - 0, - )); +// if ((await tankDefinitions)[tankId]?.tier === 10) { +// const tankStats = diffed[tankId]; +// return (await accumulator) + tankStats.damage_dealt; +// } else return accumulator; +// }, +// 0, +// )) / +// (await entries.reduce>( +// async (accumulator, [tankIdString, stats]) => { +// const tankId = parseInt(tankIdString); - if (stats.battles < 126) { - problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]); - } - if (averageTier < 8) { - problems.push([ - 'Average tier', - `${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`, - ]); - } - if (stats.wins / stats.battles < 0.6) { - problems.push([ - 'Winrate', - `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${( - 100 * - (stats.wins / stats.battles - 0.6) - ).toFixed(2)}%)`, - ]); - } - if (WN8 < 2450) { - problems.push([ - 'WN8', - `${WN8.toFixed(0)} (${(WN8 - 2450).toFixed(0)})`, - ]); - } - if (tier10Damage < 1250) { - problems.push([ - 'Tier 10 damage', - `${tier10Damage.toFixed(0)} (${tier10Damage - 1250})`, - ]); - } +// if ((await tankDefinitions)[tankId]?.tier === 10) { +// const tankStats = diffed[tankId]; +// return (await accumulator) + tankStats.battles; +// } else return accumulator; +// }, +// 0, +// )); - isEligible = problems.length === 0; - body = isEligible - ? 'Player is fully eligible.' - : `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable( - problems, - )}`; - } else if (clan === 'SMRI') { - const stats = accountInfo.statistics.all; +// if (stats.battles < 126) { +// problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]); +// } +// if (averageTier < 8) { +// problems.push([ +// 'Average tier', +// `${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`, +// ]); +// } +// if (stats.wins / stats.battles < 0.6) { +// problems.push([ +// 'Winrate', +// `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${( +// 100 * +// (stats.wins / stats.battles - 0.6) +// ).toFixed(2)}%)`, +// ]); +// } +// if (WN8 < 2450) { +// problems.push([ +// 'WN8', +// `${WN8.toFixed(0)} (${(WN8 - 2450).toFixed(0)})`, +// ]); +// } +// if (tier10Damage < 1250) { +// problems.push([ +// 'Tier 10 damage', +// `${tier10Damage.toFixed(0)} (${tier10Damage - 1250})`, +// ]); +// } - if (stats.battles < 1500) { - problems.push([ - 'Battles', - `${stats.battles} (${stats.battles - 1500})`, - ]); - } - if (stats.wins / stats.battles <= 0.53) { - problems.push([ - 'Winrate', - `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${( - 100 * - (stats.wins / stats.battles - 0.53) - ).toFixed(2)}%)`, - ]); - } +// isEligible = problems.length === 0; +// body = isEligible +// ? 'Player is fully eligible.' +// : `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable( +// problems, +// )}`; +// } else if (clan === 'SMRI') { +// const stats = accountInfo.statistics.all; - isEligible = problems.length === 0; - body = isEligible - ? 'Player is fully eligible.' - : `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable( - problems, - )}`; - } +// if (stats.battles < 1500) { +// problems.push([ +// 'Battles', +// `${stats.battles} (${stats.battles - 1500})`, +// ]); +// } +// if (stats.wins / stats.battles <= 0.53) { +// problems.push([ +// 'Winrate', +// `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${( +// 100 * +// (stats.wins / stats.battles - 0.53) +// ).toFixed(2)}%)`, +// ]); +// } - return (isEligible ? embedPositive : embedNegative)(title, body); - }, -}; +// isEligible = problems.length === 0; +// body = isEligible +// ? 'Player is fully eligible.' +// : `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable( +// problems, +// )}`; +// } + +// return (isEligible ? embedPositive : embedNegative)(title, body); +// }, +// }; diff --git a/src/commands/evolution.tsx b/src/commands/evolution.tsx index e2bb7bd99..8bc87cfe7 100644 --- a/src/commands/evolution.tsx +++ b/src/commands/evolution.tsx @@ -1,7 +1,4 @@ -import { - SlashCommandBuilder, - SlashCommandSubcommandGroupBuilder, -} from 'discord.js'; +import { SlashCommandSubcommandGroupBuilder } from 'discord.js'; import CommandWrapper from '../components/CommandWrapper'; import * as Graph from '../components/Graph'; import NoData, { NoDataType } from '../components/NoData'; @@ -20,6 +17,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteUsername from '../core/discord/autocompleteUsername'; import buttonPrimary from '../core/discord/buttonPrimary'; import commandToURL from '../core/discord/commandToURL'; +import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton'; import resolvePeriodFromCommand, { @@ -29,7 +27,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromCommand, { ResolvedPlayer, } from '../core/discord/resolvePlayerFromCommand'; -import { CommandRegistryPromisable } from '../events/interactionCreate'; +import { CommandRegistry } from '../events/interactionCreate'; type EvolutionStatType = 'player' | 'tank'; @@ -107,40 +105,32 @@ async function render( ); } -export const evolutionCommand = new Promise( +export const evolutionCommand = new Promise( async (resolve) => { - let playerGroup: SlashCommandSubcommandGroupBuilder; - let tankGroup: SlashCommandSubcommandGroupBuilder; - const command = new SlashCommandBuilder() - .setName('evolution') - .setDescription('Evolution of statistics') - .addSubcommandGroup((option) => { - playerGroup = option - .setName('player') - .setDescription('Player statistics'); - - return playerGroup; - }) - .addSubcommandGroup((option) => { - tankGroup = option.setName('tank').setDescription('Tank statistics'); - - return tankGroup; - }); - - addPeriodSubCommands(playerGroup!, (option) => - option.addStringOption(addUsernameChoices), - ); - addPeriodSubCommands(tankGroup!, (option) => - option - .addStringOption(addTankChoices) - .addStringOption(addUsernameChoices), - ); - resolve({ inProduction: true, inPublic: true, - command, + command: createLocalizedCommand('evolution', [ + { + group: 'player', + modify(option: SlashCommandSubcommandGroupBuilder) { + addPeriodSubCommands(option, (option) => + option.addStringOption(addUsernameChoices), + ); + }, + }, + { + group: 'tank', + modify(option: SlashCommandSubcommandGroupBuilder) { + addPeriodSubCommands(option, (option) => + option + .addStringOption(addTankChoices) + .addStringOption(addUsernameChoices), + ); + }, + }, + ]), async handler(interaction) { const commandGroup = @@ -156,8 +146,6 @@ export const evolutionCommand = new Promise( tankId, }); - getBlitzStarsLinkButton; - return [ await render(player, period, commandGroup, tankId), buttonPrimary(path, 'Refresh'), diff --git a/src/commands/fullStats.tsx b/src/commands/fullStats.tsx index fce3f7205..bb7e1f37f 100644 --- a/src/commands/fullStats.tsx +++ b/src/commands/fullStats.tsx @@ -29,7 +29,7 @@ import resolvePlayerFromCommand, { } from '../core/discord/resolvePlayerFromCommand'; import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { getTierWeights } from '../core/statistics/getTierWeights'; -import { CommandRegistryPromisable } from '../events/interactionCreate'; +import { CommandRegistry } from '../events/interactionCreate'; async function render( { region, id }: ResolvedPlayer, @@ -64,7 +64,7 @@ async function render( ); } -export const fullStatsCommand = new Promise( +export const fullStatsCommand = new Promise( async (resolve) => { const command = await addPeriodicFilterOptions( new SlashCommandBuilder() diff --git a/src/commands/inactive.tsx b/src/commands/inactive.tsx index 762ff3515..f5a4807f5 100644 --- a/src/commands/inactive.tsx +++ b/src/commands/inactive.tsx @@ -12,58 +12,61 @@ import { CommandRegistry } from '../events/interactionCreate'; const DEFAULT_THRESHOLD = 7; -export const inactiveCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const inactiveCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('inactive') - .setDescription('Lists all inactive players') - .addStringOption(addClanChoices) - .addNumberOption((option) => - option - .setName('threshold') - .setDescription( - `The number of days inactive (default: ${DEFAULT_THRESHOLD})`, + command: new SlashCommandBuilder() + .setName('inactive') + .setDescription('Lists all inactive players') + .addStringOption(addClanChoices) + .addNumberOption((option) => + option + .setName('threshold') + .setDescription( + `The number of days inactive (default: ${DEFAULT_THRESHOLD})`, + ) + .setMinValue(0), + ), + + async handler(interaction) { + const { region, id } = await resolveClanFromCommand(interaction); + const threshold = + interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD; + const time = new Date().getTime() / 1000; + const clanInfo = await getClanInfo(region, id); + const accountInfo = await getAccountInfo(region, clanInfo.members_ids); + const inactive = accountInfo + .map( + (account) => + [ + account.nickname, + (time - account.last_battle_time) / 60 / 60 / 24, + ] as [string, number], ) - .setMinValue(0), - ), + .filter(([, inactiveDays]) => inactiveDays >= threshold) + .sort((a, b) => b[1] - a[1]) + .map( + ([name, days]) => + [name, `${days.toFixed(0)} days`] as [string, string], + ); + const hasInactiveMembers = inactive.length > 0; - async handler(interaction) { - const { region, id } = await resolveClanFromCommand(interaction); - const threshold = - interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD; - const time = new Date().getTime() / 1000; - const clanInfo = await getClanInfo(region, id); - const accountInfo = await getAccountInfo(region, clanInfo.members_ids); - const inactive = accountInfo - .map( - (account) => - [ - account.nickname, - (time - account.last_battle_time) / 60 / 60 / 24, - ] as [string, number], - ) - .filter(([, inactiveDays]) => inactiveDays >= threshold) - .sort((a, b) => b[1] - a[1]) - .map( - ([name, days]) => [name, `${days.toFixed(0)} days`] as [string, string], + return ( + + + + {!hasInactiveMembers && } + {hasInactiveMembers && } + ); - const hasInactiveMembers = inactive.length > 0; + }, - return ( - - - - {!hasInactiveMembers && } - {hasInactiveMembers && } - - ); - }, - - autocomplete: autocompleteClan, -}; + autocomplete: autocompleteClan, + }); +}); diff --git a/src/commands/link.tsx b/src/commands/link.tsx index 27e744b34..5f81fa346 100644 --- a/src/commands/link.tsx +++ b/src/commands/link.tsx @@ -16,194 +16,198 @@ import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import { CommandRegistry } from '../events/interactionCreate'; import { theme } from '../stitches.config'; -export const verifyCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const verifyCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('link') - .setDescription('Links your Blitz and Discord account') - .addStringOption((option) => addUsernameChoices(option).setRequired(true)), + command: new SlashCommandBuilder() + .setName('link') + .setDescription('Links your Blitz and Discord account') + .addStringOption((option) => + addUsernameChoices(option).setRequired(true), + ), - async handler(interaction) { - const { id, region } = await resolvePlayerFromCommand(interaction); - const discordId = parseInt(interaction.user.id); - const accountInfo = await getAccountInfo(region, id); - const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); + async handler(interaction) { + const { id, region } = await resolvePlayerFromCommand(interaction); + const discordId = parseInt(interaction.user.id); + const accountInfo = await getAccountInfo(region, id); + const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); - await linkBlitzAndDiscord(discordId, region, id); + await linkBlitzAndDiscord(discordId, region, id); - if (interaction.guildId === discord.sklld_guild_id) { - if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { - return embedNegative( - `${markdownEscape(interaction.user.username)} failed to verify`, - "I don't have the permission to change your manage roles.", + if (interaction.guildId === discord.sklld_guild_id) { + if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { + return embedNegative( + `${markdownEscape(interaction.user.username)} failed to verify`, + "I don't have the permission to change your manage roles.", + ); + } + + await (interaction.member!.roles as GuildMemberRoleManager).remove( + discord.sklld_verify_role, + ); + await (interaction.member!.roles as GuildMemberRoleManager).add( + discord.sklld_peasant_role, ); } - await (interaction.member!.roles as GuildMemberRoleManager).remove( - discord.sklld_verify_role, - ); - await (interaction.member!.roles as GuildMemberRoleManager).add( - discord.sklld_peasant_role, - ); - } - - return [ - -
- - + return [ +
+ +
- -
- - {interaction.user.displayName} - - - @{interaction.user.username} - -
-
- -
- - Accounts linked -
- -
- {clanAccountInfo?.clan ? ( - ) : null} + +
+ + {interaction.user.displayName} + + + @{interaction.user.username} + +
+
+
- + Accounts linked +
+ +
+ {clanAccountInfo?.clan ? ( + + ) : null} +
- {accountInfo.nickname} - - - {clanAccountInfo?.clan - ? `[${clanAccountInfo?.clan?.tag}] • ` - : ''} - {REGION_NAMES[region]} - + + {accountInfo.nickname} + + + {clanAccountInfo?.clan + ? `[${clanAccountInfo?.clan?.tag}] • ` + : ''} + {REGION_NAMES[region]} + +
+ +
+
, - - - , + embedInfo( + 'Incorrect account?', + 'Use the command again and select a username from the search results.', + ), + ]; + }, - embedInfo( - 'Incorrect account?', - 'Use the command again and select a username from the search results.', - ), - ]; - }, - - autocomplete: autocompleteUsername, -}; + autocomplete: autocompleteUsername, + }); +}); diff --git a/src/commands/ownedTanks.tsx b/src/commands/ownedTanks.tsx index 0e61e4f31..4c8640c7a 100644 --- a/src/commands/ownedTanks.tsx +++ b/src/commands/ownedTanks.tsx @@ -21,108 +21,112 @@ import autocompleteUsername from '../core/discord/autocompleteUsername'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import { CommandRegistry } from '../events/interactionCreate'; -export const ownedTanksCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const ownedTanksCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('owned-tanks') - .setDescription("Shows a player's owned tanks") - .addStringOption(addTierChoices) - .addStringOption(addUsernameChoices), + command: new SlashCommandBuilder() + .setName('owned-tanks') + .setDescription("Shows a player's owned tanks") + .addStringOption(addTierChoices) + .addStringOption(addUsernameChoices), - async handler(interaction) { - const tier = Number(interaction.options.getString('tier')); - const account = await resolvePlayerFromCommand(interaction); - const { id, region: server } = account; - const accountInfo = await getAccountInfo(server, id); - const tankStats = await getTankStats(server, id); - const filteredTanks = ( - await Promise.all( - tankStats.map(async (tankData) => ({ - tankDefinitions: (await tankDefinitions)[tankData.tank_id]!, - id: tankData.tank_id, - })), - ) - ).filter((tank) => tank.tankDefinitions?.tier === tier); - const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']); - const groupedTanks: Record = {}; - const nations: string[] = []; + async handler(interaction) { + const tier = Number(interaction.options.getString('tier')); + const account = await resolvePlayerFromCommand(interaction); + const { id, region: server } = account; + const accountInfo = await getAccountInfo(server, id); + const tankStats = await getTankStats(server, id); + const filteredTanks = ( + await Promise.all( + tankStats.map(async (tankData) => ({ + tankDefinitions: (await tankDefinitions)[tankData.tank_id]!, + id: tankData.tank_id, + })), + ) + ).filter((tank) => tank.tankDefinitions?.tier === tier); + const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']); + const groupedTanks: Record = {}; + const nations: string[] = []; - filteredTanks.forEach((tank) => { - if (groupedTanks[tank.tankDefinitions.nation] === undefined) { - groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions]; - nations.push(tank.tankDefinitions.nation); - } else { - groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions); - } - }); + filteredTanks.forEach((tank) => { + if (groupedTanks[tank.tankDefinitions.nation] === undefined) { + groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions]; + nations.push(tank.tankDefinitions.nation); + } else { + groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions); + } + }); - nations.sort(); + nations.sort(); - return ( - - + return ( + + - {filteredTanks.length === 0 && } + {filteredTanks.length === 0 && ( + + )} - {filteredTanks.length > 0 && - (await Promise.all( - nations.map(async (nation) => { - const tanks = groupedTanks[nation]; - const leftColumnSize = Math.ceil(tanks.length / 2); - const leftColumn = tanks.slice(0, leftColumnSize); - const rightColumn = tanks.slice(leftColumnSize); + {filteredTanks.length > 0 && + (await Promise.all( + nations.map(async (nation) => { + const tanks = groupedTanks[nation]; + const leftColumnSize = Math.ceil(tanks.length / 2); + const leftColumn = tanks.slice(0, leftColumnSize); + const rightColumn = tanks.slice(leftColumnSize); - return ( - - - {(await encyclopediaInfo).vehicle_nations[nation]} - + return ( + + + {(await encyclopediaInfo).vehicle_nations[nation]} + - - - {await Promise.all( - leftColumn.map(async (tank) => ( - - )), - )} - - - {await Promise.all( - rightColumn.map(async (tank) => ( - - )), - )} - - - - ); - }), - ))} - - ); - }, + + + {await Promise.all( + leftColumn.map(async (tank) => ( + + )), + )} + + + {await Promise.all( + rightColumn.map(async (tank) => ( + + )), + )} + + + + ); + }), + ))} + + ); + }, - autocomplete: autocompleteUsername, -}; + autocomplete: autocompleteUsername, + }); +}); diff --git a/src/commands/permissions.ts b/src/commands/permissions.ts index b8f29f563..367364d8e 100644 --- a/src/commands/permissions.ts +++ b/src/commands/permissions.ts @@ -1,42 +1,44 @@ import { SlashCommandBuilder } from 'discord.js'; import { CommandRegistry } from '../events/interactionCreate'; -export const permissionsCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const permissionsCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('permissions') - .setDescription('Checks wether the bot has the required permissions'), + command: new SlashCommandBuilder() + .setName('permissions') + .setDescription('Checks wether the bot has the required permissions'), - async handler(interaction) { - // friday the 13th permissions easter egg - const isFridayThe13th = - new Date().getDay() === 5 && new Date().getDate() === 13; - const permissions = [ - [ - interaction.appPermissions?.has('ViewChannel'), - 'View channels: needed for the refresh button to work', - ], - [ - interaction.appPermissions?.has('ReadMessageHistory'), - 'Read message history: also needed for the refresh button to work', - ], - [ - interaction.appPermissions?.has('AttachFiles'), - 'Attach files: needed for image-based commands to work', - ], - ]; - const body = permissions - .map(([hasPermission, description]) => { - if (hasPermission) { - return `${isFridayThe13th ? '😇' : '✅'} ${description}`; - } else { - return `${isFridayThe13th ? '💀' : '❌'} ${description}`; - } - }) - .join('\n'); + async handler(interaction) { + // friday the 13th permissions easter egg + const isFridayThe13th = + new Date().getDay() === 5 && new Date().getDate() === 13; + const permissions = [ + [ + interaction.appPermissions?.has('ViewChannel'), + 'View channels: needed for the refresh button to work', + ], + [ + interaction.appPermissions?.has('ReadMessageHistory'), + 'Read message history: also needed for the refresh button to work', + ], + [ + interaction.appPermissions?.has('AttachFiles'), + 'Attach files: needed for image-based commands to work', + ], + ]; + const body = permissions + .map(([hasPermission, description]) => { + if (hasPermission) { + return `${isFridayThe13th ? '😇' : '✅'} ${description}`; + } else { + return `${isFridayThe13th ? '💀' : '❌'} ${description}`; + } + }) + .join('\n'); - return `# Permissions\n${body}`; - }, -}; + return `# Permissions\n${body}`; + }, + }); +}); diff --git a/src/commands/ping.ts b/src/commands/ping.ts index bb93c57dc..e02eabcd0 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -1,32 +1,34 @@ import { SlashCommandBuilder } from 'discord.js'; import { CommandRegistry } from '../events/interactionCreate'; -export const pingCommand: CommandRegistry = { - inProduction: true, - inPublic: true, - handlesInteraction: true, +export const pingCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, + handlesInteraction: true, - command: new SlashCommandBuilder() - .setName('ping') - .setDescription('Check if the bot is alive') - .addSubcommand((option) => - option.setName('blitzkrieg').setDescription('Ping Blitzkrieg'), - ) - .addSubcommand((option) => - option.setName('blitz').setDescription('Ping Blitz'), - ), + command: new SlashCommandBuilder() + .setName('ping') + .setDescription('Check if the bot is alive') + .addSubcommand((option) => + option.setName('blitzkrieg').setDescription('Ping Blitzkrieg'), + ) + .addSubcommand((option) => + option.setName('blitz').setDescription('Ping Blitz'), + ), - async handler(interaction) { - const subcommand = interaction.options.getSubcommand(); - const executionStart = Date.now(); + async handler(interaction) { + const subcommand = interaction.options.getSubcommand(); + const executionStart = Date.now(); - if (subcommand === 'blitzkrieg') { - await interaction.editReply('Pong 🏓'); - } else { - await fetch('https://api.wotblitz.com/'); - } + if (subcommand === 'blitzkrieg') { + await interaction.editReply('Pong 🏓'); + } else { + await fetch('https://api.wotblitz.com/'); + } - const executionTime = Date.now() - executionStart; - interaction.editReply(`Pong 🏓 - ${executionTime}ms`); - }, -}; + const executionTime = Date.now() - executionStart; + interaction.editReply(`Pong 🏓 - ${executionTime}ms`); + }, + }); +}); diff --git a/src/commands/playerAchievements.ts b/src/commands/playerAchievements.ts index 440b4d954..ec76f0a0f 100644 --- a/src/commands/playerAchievements.ts +++ b/src/commands/playerAchievements.ts @@ -12,57 +12,62 @@ import { CommandRegistry } from '../events/interactionCreate'; type SortBy = 'name' | 'count'; -export const playerAchievementsCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const playerAchievementsCommand = new Promise( + (resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('player-achievements') - .setDescription("All the player's achievements") - .addStringOption(addUsernameChoices) - .addStringOption((option) => - option - .setName('sort') - .setDescription('What to sort by (default: name)') - .addChoices( - { name: 'By name', value: 'name' satisfies SortBy }, - { name: 'By count', value: 'count' satisfies SortBy }, + command: new SlashCommandBuilder() + .setName('player-achievements') + .setDescription("All the player's achievements") + .addStringOption(addUsernameChoices) + .addStringOption((option) => + option + .setName('sort') + .setDescription('What to sort by (default: name)') + .addChoices( + { name: 'By name', value: 'name' satisfies SortBy }, + { name: 'By count', value: 'count' satisfies SortBy }, + ), ), - ), - async handler(interaction) { - const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy; - const account = await resolvePlayerFromCommand(interaction); - const { id, region: region } = account; - const accountInfo = await getAccountInfo(region, id); - const accountsAchievements = await getAccountAchievements(region, id); - const accountAchievements = accountsAchievements; - const compound = { - ...accountAchievements.achievements, - ...accountAchievements.max_series, - }; + async handler(interaction) { + const sortBy = (interaction.options.getString('sort') ?? + 'name') as SortBy; + const account = await resolvePlayerFromCommand(interaction); + const { id, region: region } = account; + const accountInfo = await getAccountInfo(region, id); + const accountsAchievements = await getAccountAchievements(region, id); + const accountAchievements = accountsAchievements; + const compound = { + ...accountAchievements.achievements, + ...accountAchievements.max_series, + }; - return embedInfo( - `${markdownEscape(accountInfo.nickname)}'s information`, + return embedInfo( + `${markdownEscape(accountInfo.nickname)}'s information`, - markdownTable( - Object.keys(compound) - .filter((achievement) => compound[achievement] > 0) - .sort( - sortBy === 'count' - ? (a, b) => compound[b] - compound[a] - : undefined, - ) - .map( - (achievement) => - [ - startCase(achievement), - compound[achievement], - ] as TableInputEntry, + markdownTable( + Object.keys(compound) + .filter((achievement) => compound[achievement] > 0) + .sort( + sortBy === 'count' + ? (a, b) => compound[b] - compound[a] + : undefined, + ) + .map( + (achievement) => + [ + startCase(achievement), + compound[achievement], + ] as TableInputEntry, + ), ), - ), - ); - }, + ); + }, - autocomplete: autocompleteUsername, -}; + autocomplete: autocompleteUsername, + }); + }, +); diff --git a/src/commands/playerInfo.ts b/src/commands/playerInfo.ts index 64342da2d..69a628be9 100644 --- a/src/commands/playerInfo.ts +++ b/src/commands/playerInfo.ts @@ -8,47 +8,49 @@ import markdownTable from '../core/discord/markdownTable'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import { CommandRegistry } from '../events/interactionCreate'; -export const playerInfoCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const playerInfoCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('player-info') - .setDescription('Basic information about a player') - .addStringOption(addUsernameChoices), + command: new SlashCommandBuilder() + .setName('player-info') + .setDescription('Basic information about a player') + .addStringOption(addUsernameChoices), - async handler(interaction) { - const account = await resolvePlayerFromCommand(interaction); - const { id, region: region } = account; - const accountInfo = await getAccountInfo(region, id); + async handler(interaction) { + const account = await resolvePlayerFromCommand(interaction); + const { id, region: region } = account; + const accountInfo = await getAccountInfo(region, id); - return embedInfo( - `${markdownEscape(accountInfo.nickname)}'s information`, + return embedInfo( + `${markdownEscape(accountInfo.nickname)}'s information`, - markdownTable([ - ['Nickname', `${accountInfo.nickname}`], - ['Battles', `${accountInfo.statistics.all.battles}`], - [ - 'Winrate', - `${( - 100 * - (accountInfo.statistics.all.wins / - accountInfo.statistics.all.battles) - ).toFixed(2)}%`, - ], - [], - ['Account ID', `${accountInfo.account_id}`], - [ - 'Created', - `${new Date(accountInfo.created_at * 1000).toDateString()}`, - ], - [ - 'Last battle', - `${new Date(accountInfo.last_battle_time * 1000).toDateString()}`, - ], - ]), - ); - }, + markdownTable([ + ['Nickname', `${accountInfo.nickname}`], + ['Battles', `${accountInfo.statistics.all.battles}`], + [ + 'Winrate', + `${( + 100 * + (accountInfo.statistics.all.wins / + accountInfo.statistics.all.battles) + ).toFixed(2)}%`, + ], + [], + ['Account ID', `${accountInfo.account_id}`], + [ + 'Created', + `${new Date(accountInfo.created_at * 1000).toDateString()}`, + ], + [ + 'Last battle', + `${new Date(accountInfo.last_battle_time * 1000).toDateString()}`, + ], + ]), + ); + }, - autocomplete: autocompleteUsername, -}; + autocomplete: autocompleteUsername, + }); +}); diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 91dc7d817..5fbe47c79 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -27,7 +27,7 @@ import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; import embedNegative from '../core/discord/embedNegative'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; -import { CommandRegistryPromisable } from '../events/interactionCreate'; +import { CommandRegistry } from '../events/interactionCreate'; import { UserError } from '../hooks/userError'; export interface RatingsPlayer { @@ -122,334 +122,329 @@ const noOngoingSeason = embedNegative( "Wargaming didn't provide any data for this season.", ); -export const ratingsCommand = new Promise( - async (resolve) => { - const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); - const onGoingSeason = await isOnGoingRatingsSeason(); - const seasonNumbers = range( - FIRST_ARCHIVED_RATINGS_SEASON, - latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1, +export const ratingsCommand = new Promise(async (resolve) => { + const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); + const onGoingSeason = await isOnGoingRatingsSeason(); + const seasonNumbers = range( + FIRST_ARCHIVED_RATINGS_SEASON, + latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1, + ); + + function addSubcommands(option: SlashCommandSubcommandBuilder) { + return option + .addStringOption(addUsernameChoices) + .addIntegerOption((option) => + option + .setName('limit') + .setDescription('How many neighbors to display (default: 10)') + .setRequired(false) + .setMinValue(5) + .setMaxValue(30), + ) + .addStringOption((option) => + option + .setName('season') + .setDescription('Season number') + .setRequired(false) + .addChoices( + ...seasonNumbers + .map( + (number, index) => + ({ + name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`, + value: `${number}`, + }) satisfies APIApplicationCommandOptionChoice, + ) + .reverse(), + ), + ); + } + + const command = new SlashCommandBuilder() + .setName('ratings') + .setDescription('Ratings battles statistics') + .addSubcommand((option) => + addSubcommands( + option + .setName('neighbors') + .setDescription('Shows neighboring players on the leaderboard'), + ), + ) + .addSubcommand((option) => + addSubcommands( + option + .setName('league') + .setDescription('Shows top players from a league') + .addStringOption(addRegionChoices) + .addStringOption((option) => + option + .setName('league') + .setDescription('The league to display') + .addChoices( + ...LEAGUES.map( + ({ name }, value) => + ({ + name, + value: `${value}`, + }) satisfies APIApplicationCommandOptionChoice, + ), + ) + .setRequired(true), + ), + ), ); - function addSubcommands(option: SlashCommandSubcommandBuilder) { - return option - .addStringOption(addUsernameChoices) - .addIntegerOption((option) => - option - .setName('limit') - .setDescription('How many neighbors to display (default: 10)') - .setRequired(false) - .setMinValue(5) - .setMaxValue(30), - ) - .addStringOption((option) => - option - .setName('season') - .setDescription('Season number') - .setRequired(false) - .addChoices( - ...seasonNumbers - .map( - (number, index) => + resolve({ + inProduction: true, + inPublic: true, + + command, + + async handler(interaction) { + interface SimplifiedPlayer { + id: number; + score: number; + position: number; + clan: string | undefined; + nickname: string; + } + + const subcommand = interaction.options.getSubcommand(true) as + | 'league' + | 'neighbors'; + const limit = interaction.options.getInteger('limit') ?? 10; + const seasonOption = interaction.options.getString('season'); + const season = seasonOption + ? parseInt(seasonOption) + : onGoingSeason + ? latestArchivedSeasonNumber + 1 + : latestArchivedSeasonNumber; + const isCurrentSeason = season === latestArchivedSeasonNumber + 1; + let titleName: string; + let titleImage: string | undefined; + let titleDescription: string; + let playersBefore: number; + let playersAfter: number; + let players: SimplifiedPlayer[] | undefined; + let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined; + let playerId: number | undefined; + let regionRatingsInfo: RatingsInfo; + + if (subcommand === 'league') { + const leagueIndex = parseInt(interaction.options.getString('league')!); + const region = interaction.options.getString('region') as Region; + regionRatingsInfo = await (isCurrentSeason + ? getRatingsInfo(region) + : getArchivedRatingsInfo(region, season!)); + + if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; + + const result = isCurrentSeason + ? await getRatingsLeague(region, leagueIndex).then(async (result) => + result.slice(0, limit).map( + (player) => + ({ + id: player.spa_id, + score: player.score, + position: player.number, + clan: player.clan_tag, + nickname: player.nickname, + }) satisfies SimplifiedPlayer, + ), + ) + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { + const firstPlayerIndex = + leagueIndex === 0 + ? 0 + : players.findIndex( + (player) => + player.score < LEAGUES[leagueIndex - 1].minScore, + ); + const lastPlayerIndex = firstPlayerIndex + limit - 1; + const trimmed = players.slice( + firstPlayerIndex, + lastPlayerIndex + 1, + ); + const clanData = await getClanAccountInfo( + region, + trimmed.map(({ id }) => id), + ['clan'], + ); + + return trimmed.map( + (player, index) => ({ - name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`, - value: `${number}`, - }) satisfies APIApplicationCommandOptionChoice, - ) - .reverse(), - ), - ); - } + id: player.id, + score: player.score, + position: firstPlayerIndex + index + 1, + clan: clanData[player.id]?.clan?.tag, + nickname: + clanData[index]?.account_name ?? `Deleted ${player.id}`, + }) satisfies SimplifiedPlayer, + ); + }, + ); + const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; + midnightLeaderboard = isCurrentSeason + ? await getArchivedRatingsMidnightLeaderboard( + region, + regionRatingsInfo.current_season, + ) + : await getArchivedRatingsMidnightLeaderboard(region, season!); - const command = new SlashCommandBuilder() - .setName('ratings') - .setDescription('Ratings battles statistics') - .addSubcommand((option) => - addSubcommands( - option - .setName('neighbors') - .setDescription('Shows neighboring players on the leaderboard'), - ), - ) - .addSubcommand((option) => - addSubcommands( - option - .setName('league') - .setDescription('Shows top players from a league') - .addStringOption(addRegionChoices) - .addStringOption((option) => - option - .setName('league') - .setDescription('The league to display') - .addChoices( - ...LEAGUES.map( - ({ name }, value) => - ({ - name, - value: `${value}`, - }) satisfies APIApplicationCommandOptionChoice, - ), - ) - .setRequired(true), - ), - ), - ); + players = result; + playersBefore = result ? result[0].position - 1 : 0; + playersAfter = + regionRatingsInfo.count - result[result.length - 1].position; + titleName = `${ + leagueInfo ? leagueInfo.title : 'No Ongoing Season' + } - ${REGION_NAMES_SHORT[region]}`; + titleImage = leagueInfo.big_icon.startsWith('http') + ? leagueInfo.big_icon + : `https:${leagueInfo.big_icon}`; + titleDescription = `Top ${limit} players`; + } else { + const { region, id } = await resolvePlayerFromCommand(interaction); + regionRatingsInfo = isCurrentSeason + ? await getRatingsInfo(region) + : await getArchivedRatingsInfo(region, season!); - resolve({ - inProduction: true, - inPublic: true, + if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; - command, - - async handler(interaction) { - interface SimplifiedPlayer { - id: number; - score: number; - position: number; - clan: string | undefined; - nickname: string; - } - - const subcommand = interaction.options.getSubcommand(true) as - | 'league' - | 'neighbors'; - const limit = interaction.options.getInteger('limit') ?? 10; - const seasonOption = interaction.options.getString('season'); - const season = seasonOption - ? parseInt(seasonOption) - : onGoingSeason - ? latestArchivedSeasonNumber + 1 - : latestArchivedSeasonNumber; - const isCurrentSeason = season === latestArchivedSeasonNumber + 1; - let titleName: string; - let titleImage: string | undefined; - let titleDescription: string; - let playersBefore: number; - let playersAfter: number; - let players: SimplifiedPlayer[] | undefined; - let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined; - let playerId: number | undefined; - let regionRatingsInfo: RatingsInfo; - - if (subcommand === 'league') { - const leagueIndex = parseInt( - interaction.options.getString('league')!, - ); - const region = interaction.options.getString('region') as Region; - regionRatingsInfo = await (isCurrentSeason - ? getRatingsInfo(region) - : getArchivedRatingsInfo(region, season!)); - - if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; - - const result = isCurrentSeason - ? await getRatingsLeague(region, leagueIndex).then(async (result) => - result.slice(0, limit).map( + const accountInfo = await getAccountInfo(region, id); + const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; + const neighbors = isCurrentSeason + ? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then( + (players) => + players.neighbors.map( (player) => ({ id: player.spa_id, - score: player.score, position: player.number, + score: player.score, clan: player.clan_tag, nickname: player.nickname, }) satisfies SimplifiedPlayer, ), - ) - : await getArchivedRatingsLeaderboard(region, season!).then( - async (players) => { - const firstPlayerIndex = - leagueIndex === 0 - ? 0 - : players.findIndex( - (player) => - player.score < LEAGUES[leagueIndex - 1].minScore, - ); - const lastPlayerIndex = firstPlayerIndex + limit - 1; - const trimmed = players.slice( - firstPlayerIndex, - lastPlayerIndex + 1, - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map(({ id }) => id), - ['clan'], + ) + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { + const playerIndex = players.findIndex( + (player) => player.id === id, + ); + + if (playerIndex === -1) { + throw new UserError( + `# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`, ); + } - return trimmed.map( - (player, index) => - ({ - id: player.id, - score: player.score, - position: firstPlayerIndex + index + 1, - clan: clanData[player.id]?.clan?.tag, - nickname: - clanData[index]?.account_name ?? - `Deleted ${player.id}`, - }) satisfies SimplifiedPlayer, - ); - }, - ); - const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; - midnightLeaderboard = isCurrentSeason - ? await getArchivedRatingsMidnightLeaderboard( - region, - regionRatingsInfo.current_season, - ) - : await getArchivedRatingsMidnightLeaderboard(region, season!); + const halfRange = Math.round(limit / 2); + const trimmed = players.slice( + playerIndex - halfRange, + playerIndex + halfRange + 1, + ); + const clanData = await getClanAccountInfo( + region, + trimmed.map((player) => player.id), + ['clan'], + ); - players = result; - playersBefore = result ? result[0].position - 1 : 0; - playersAfter = - regionRatingsInfo.count - result[result.length - 1].position; - titleName = `${ - leagueInfo ? leagueInfo.title : 'No Ongoing Season' - } - ${REGION_NAMES_SHORT[region]}`; - titleImage = leagueInfo.big_icon.startsWith('http') - ? leagueInfo.big_icon - : `https:${leagueInfo.big_icon}`; - titleDescription = `Top ${limit} players`; - } else { - const { region, id } = await resolvePlayerFromCommand(interaction); - regionRatingsInfo = isCurrentSeason - ? await getRatingsInfo(region) - : await getArchivedRatingsInfo(region, season!); + return trimmed.map((player, index) => ({ + id: player.id, + score: player.score, + position: playerIndex + index + 1, + clan: clanData[index]?.clan?.tag, + nickname: clanData[index] + ? clanData[index]!.account_name + : `Deleted ${player.id}`, + })); + }, + ); + midnightLeaderboard = isCurrentSeason + ? await getArchivedRatingsMidnightLeaderboard( + region, + regionRatingsInfo.current_season, + ) + : await getArchivedRatingsMidnightLeaderboard(region, season!); - if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; + players = neighbors; + titleImage = clan + ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` + : undefined; + titleName = neighbors + ? accountInfo.nickname + : `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`; + titleDescription = 'Ratings neighbors'; + playersBefore = neighbors ? neighbors[0].position - 1 : 0; + playersAfter = + regionRatingsInfo.count - neighbors[neighbors.length - 1].position; + playerId = id; + } - const accountInfo = await getAccountInfo(region, id); - const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; - const neighbors = isCurrentSeason - ? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then( - (players) => - players.neighbors.map( - (player) => - ({ - id: player.spa_id, - position: player.number, - score: player.score, - clan: player.clan_tag, - nickname: player.nickname, - }) satisfies SimplifiedPlayer, - ), - ) - : await getArchivedRatingsLeaderboard(region, season!).then( - async (players) => { - const playerIndex = players.findIndex( - (player) => player.id === id, - ); + const resolvedRegionRatingsInfo: typeof regionRatingsInfo & { + detail: undefined; + } = regionRatingsInfo; - if (playerIndex === -1) { - throw new UserError( - `# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`, - ); - } - - const halfRange = Math.round(limit / 2); - const trimmed = players.slice( - playerIndex - halfRange, - playerIndex + halfRange + 1, - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map((player) => player.id), - ['clan'], - ); - - return trimmed.map((player, index) => ({ - id: player.id, - score: player.score, - position: playerIndex + index + 1, - clan: clanData[index]?.clan?.tag, - nickname: clanData[index] - ? clanData[index]!.account_name - : `Deleted ${player.id}`, - })); - }, - ); - midnightLeaderboard = isCurrentSeason - ? await getArchivedRatingsMidnightLeaderboard( - region, - regionRatingsInfo.current_season, - ) - : await getArchivedRatingsMidnightLeaderboard(region, season!); - - players = neighbors; - titleImage = clan - ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` - : undefined; - titleName = neighbors - ? accountInfo.nickname - : `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`; - titleDescription = 'Ratings neighbors'; - playersBefore = neighbors ? neighbors[0].position - 1 : 0; - playersAfter = - regionRatingsInfo.count - neighbors[neighbors.length - 1].position; - playerId = id; - } - - const resolvedRegionRatingsInfo: typeof regionRatingsInfo & { - detail: undefined; - } = regionRatingsInfo; - - const items = players.map((player) => { - const reward = resolvedRegionRatingsInfo.rewards.find( - (reward) => - player.position >= reward.from_position && - player.position <= reward.to_position, - ); - const midnightIndex = midnightLeaderboard?.findIndex( - (item) => item.id === player.id, - ); - const midnightScore = - midnightIndex === undefined - ? player.score - : midnightLeaderboard![midnightIndex].score; - const midnightPosition = - midnightIndex === undefined ? player.position : midnightIndex + 1; - - return ( - - ); - }); + const items = players.map((player) => { + const reward = resolvedRegionRatingsInfo.rewards.find( + (reward) => + player.position >= reward.from_position && + player.position <= reward.to_position, + ); + const midnightIndex = midnightLeaderboard?.findIndex( + (item) => item.id === player.id, + ); + const midnightScore = + midnightIndex === undefined + ? player.score + : midnightLeaderboard![midnightIndex].score; + const midnightPosition = + midnightIndex === undefined ? player.position : midnightIndex + 1; return ( - - - - {items && ( - - {playersBefore > 0 && ( - - )} - {items} - {playersAfter > 0 && ( - - )} - - )} - + ); - }, + }); - autocomplete: autocompleteUsername, - }); - }, -); + return ( + + + + {items && ( + + {playersBefore > 0 && ( + + )} + {items} + {playersAfter > 0 && ( + + )} + + )} + + ); + }, + + autocomplete: autocompleteUsername, + }); +}); diff --git a/src/commands/searchClans.ts b/src/commands/searchClans.ts index 9b4860799..4b70afae5 100644 --- a/src/commands/searchClans.ts +++ b/src/commands/searchClans.ts @@ -9,38 +9,42 @@ import embedInfo from '../core/discord/embedInfo'; import { CommandRegistry } from '../events/interactionCreate'; import { ClanList } from '../types/clanList'; -export const searchClansCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const searchClansCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('search-clans') - .setDescription('Search clans') - .addStringOption(addRegionChoices) - .addStringOption((option) => addClanChoices(option).setAutocomplete(false)) - .addIntegerOption((option) => - option - .setName('limit') - .setDescription('The size of the search result (default: 25)') - .setMinValue(1) - .setMaxValue(100), - ), + command: new SlashCommandBuilder() + .setName('search-clans') + .setDescription('Search clans') + .addStringOption(addRegionChoices) + .addStringOption((option) => + addClanChoices(option).setAutocomplete(false), + ) + .addIntegerOption((option) => + option + .setName('limit') + .setDescription('The size of the search result (default: 25)') + .setMinValue(1) + .setMaxValue(100), + ), - async handler(interaction) { - const server = interaction.options.getString('region') as Region; - const clan = interaction.options.getString('clan')!; - const limit = interaction.options.getInteger('limit') ?? 25; - const clanList = await fetchBlitz( - `https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`, - ); + async handler(interaction) { + const server = interaction.options.getString('region') as Region; + const clan = interaction.options.getString('clan')!; + const limit = interaction.options.getInteger('limit') ?? 25; + const clanList = await fetchBlitz( + `https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`, + ); - return embedInfo( - `Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`, - clanList.length === 0 - ? 'No clans found.' - : `\`\`\`\n${clanList - .map((clan) => `${clan.name} [${clan.tag}]`) - .join('\n')}\n\`\`\``, - ); - }, -}; + return embedInfo( + `Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`, + clanList.length === 0 + ? 'No clans found.' + : `\`\`\`\n${clanList + .map((clan) => `${clan.name} [${clan.tag}]`) + .join('\n')}\n\`\`\``, + ); + }, + }); +}); diff --git a/src/commands/searchPlayers.ts b/src/commands/searchPlayers.ts index 3d6037bcc..11f642735 100644 --- a/src/commands/searchPlayers.ts +++ b/src/commands/searchPlayers.ts @@ -12,45 +12,47 @@ import addUsernameChoices from '../core/discord/addUsernameChoices'; import embedInfo from '../core/discord/embedInfo'; import { CommandRegistry } from '../events/interactionCreate'; -export const searchPlayersCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const searchPlayersCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('search-players') - .setDescription('Search players in a Blitz server') - .addStringOption(addRegionChoices) - .addStringOption((option) => - addUsernameChoices(option).setAutocomplete(false).setRequired(true), - ) - .addIntegerOption((option) => - option - .setName('limit') - .setDescription('The size of the search result (default: 25)') - .setMinValue(1) - .setMaxValue(100), - ), + command: new SlashCommandBuilder() + .setName('search-players') + .setDescription('Search players in a Blitz server') + .addStringOption(addRegionChoices) + .addStringOption((option) => + addUsernameChoices(option).setAutocomplete(false).setRequired(true), + ) + .addIntegerOption((option) => + option + .setName('limit') + .setDescription('The size of the search result (default: 25)') + .setMinValue(1) + .setMaxValue(100), + ), - async handler(interaction) { - const server = interaction.options.getString('region') as Region; - const name = interaction.options.getString('username')!; - const limit = interaction.options.getInteger('limit') ?? 25; - const trimmedSearch = name.trim(); - const players = usernamePattern.test(trimmedSearch) - ? await fetchBlitz( - `https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent( - trimmedSearch, - )}&limit=${limit}`, - ) - : []; + async handler(interaction) { + const server = interaction.options.getString('region') as Region; + const name = interaction.options.getString('username')!; + const limit = interaction.options.getInteger('limit') ?? 25; + const trimmedSearch = name.trim(); + const players = usernamePattern.test(trimmedSearch) + ? await fetchBlitz( + `https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent( + trimmedSearch, + )}&limit=${limit}`, + ) + : []; - return embedInfo( - `Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`, - `\`\`\`${ - players.length === 0 - ? 'No players found.' - : players.map((player) => player.nickname).join('\n') - }\`\`\``, - ); - }, -}; + return embedInfo( + `Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`, + `\`\`\`${ + players.length === 0 + ? 'No players found.' + : players.map((player) => player.nickname).join('\n') + }\`\`\``, + ); + }, + }); +}); diff --git a/src/commands/searchTanks.ts b/src/commands/searchTanks.ts index e06414c46..86dfa42ab 100644 --- a/src/commands/searchTanks.ts +++ b/src/commands/searchTanks.ts @@ -6,35 +6,39 @@ import addTankChoices from '../core/discord/addTankChoices'; import embedInfo from '../core/discord/embedInfo'; import { CommandRegistry } from '../events/interactionCreate'; -export const searchTanksCommand: CommandRegistry = { - inProduction: true, - inPublic: true, +export const searchTanksCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, - command: new SlashCommandBuilder() - .setName('search-tanks') - .setDescription('Search tanks') - .addStringOption((option) => addTankChoices(option).setAutocomplete(false)) - .addIntegerOption((option) => - option - .setName('limit') - .setDescription('The size of the search result (default: 25)') - .setMinValue(1) - .setMaxValue(100), - ), + command: new SlashCommandBuilder() + .setName('search-tanks') + .setDescription('Search tanks') + .addStringOption((option) => + addTankChoices(option).setAutocomplete(false), + ) + .addIntegerOption((option) => + option + .setName('limit') + .setDescription('The size of the search result (default: 25)') + .setMinValue(1) + .setMaxValue(100), + ), - async handler(interaction) { - const tank = interaction.options.getString('tank')!; - const limit = interaction.options.getInteger('limit') ?? 25; - const results = go(tank, await tankNames, { - limit, - keys: ['combined'], - }).map((result) => result.obj.original); + async handler(interaction) { + const tank = interaction.options.getString('tank')!; + const limit = interaction.options.getInteger('limit') ?? 25; + const results = go(tank, await tankNames, { + limit, + keys: ['combined'], + }).map((result) => result.obj.original); - return embedInfo( - `Tank search for "${markdownEscape(tank)}"`, - results.length === 0 - ? 'No tanks found.' - : `\`\`\`\n${results.join('\n')}\n\`\`\``, - ); - }, -}; + return embedInfo( + `Tank search for "${markdownEscape(tank)}"`, + results.length === 0 + ? 'No tanks found.' + : `\`\`\`\n${results.join('\n')}\n\`\`\``, + ); + }, + }); +}); diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index 27ca249c3..0e18962d7 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -27,7 +27,7 @@ import resolvePlayerFromCommand, { ResolvedPlayer, } from '../core/discord/resolvePlayerFromCommand'; import { StatFilters, filterStats } from '../core/statistics/filterStats'; -import { CommandRegistryPromisable } from '../events/interactionCreate'; +import { CommandRegistry } from '../events/interactionCreate'; async function render( { region, id }: ResolvedPlayer, @@ -57,50 +57,48 @@ async function render( ); } -export const statsCommand = new Promise( - async (resolve) => { - const command = await addPeriodicFilterOptions( - new SlashCommandBuilder() - .setName('stats') - .setDescription('Regular battles statistics'), - (option) => option.addStringOption(addUsernameChoices), - ); +export const statsCommand = new Promise(async (resolve) => { + const command = await addPeriodicFilterOptions( + new SlashCommandBuilder() + .setName('stats') + .setDescription('Regular battles statistics'), + (option) => option.addStringOption(addUsernameChoices), + ); - resolve({ - inProduction: true, - inPublic: true, + resolve({ + inProduction: true, + inPublic: true, - command, + command, - async handler(interaction) { - const player = await resolvePlayerFromCommand(interaction); - const period = resolvePeriodFromCommand(player.region, interaction); - const filters = await getFiltersFromCommand(interaction); - const path = commandToURL(interaction, { - ...player, - ...getCustomPeriodParams(interaction), - ...filters, - }); + async handler(interaction) { + const player = await resolvePlayerFromCommand(interaction); + const period = resolvePeriodFromCommand(player.region, interaction); + const filters = await getFiltersFromCommand(interaction); + const path = commandToURL(interaction, { + ...player, + ...getCustomPeriodParams(interaction), + ...filters, + }); - return Promise.all([ - render(player, period, filters), - buttonRefresh(interaction, path), - getBlitzStarsLinkButton(player.region, player.id), - ]); - }, + return Promise.all([ + render(player, period, filters), + buttonRefresh(interaction, path), + getBlitzStarsLinkButton(player.region, player.id), + ]); + }, - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, + autocomplete: (interaction) => { + autocompleteUsername(interaction); + autocompleteTanks(interaction); + }, - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - const filters = getFiltersFromButton(interaction); + async button(interaction) { + const player = await resolvePlayerFromButton(interaction); + const period = resolvePeriodFromButton(player.region, interaction); + const filters = getFiltersFromButton(interaction); - return await render(player, period, filters); - }, - }); - }, -); + return await render(player, period, filters); + }, + }); +}); diff --git a/src/commands/today.ts b/src/commands/today.ts index 81e82405b..cc7e0e30f 100644 --- a/src/commands/today.ts +++ b/src/commands/today.ts @@ -14,58 +14,56 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; -import { CommandRegistryPromisable } from '../events/interactionCreate'; +import { CommandRegistry } from '../events/interactionCreate'; import { renderBreakdown } from './breakdown'; -export const todayCommand = new Promise( - async (resolve) => { - const awaitedEncyclopediaInfo = await encyclopediaInfo; - const command = addFilterOptions( - new SlashCommandBuilder() - .setName('today') - .setDescription("Today's played tanks and statistics"), - awaitedEncyclopediaInfo, - ).addStringOption(addUsernameChoices); +export const todayCommand = new Promise(async (resolve) => { + const awaitedEncyclopediaInfo = await encyclopediaInfo; + const command = addFilterOptions( + new SlashCommandBuilder() + .setName('today') + .setDescription("Today's played tanks and statistics"), + awaitedEncyclopediaInfo, + ).addStringOption(addUsernameChoices); - resolve({ - inProduction: true, - inPublic: true, + resolve({ + inProduction: true, + inPublic: true, - command, + command, - async handler(interaction) { - const player = await resolvePlayerFromCommand(interaction); - const period = resolvePeriodFromCommand( - player.region, - interaction, - 'today', - ); - const filters = await getFiltersFromCommand(interaction); - const path = commandToURL(interaction, { - ...player, - ...getCustomPeriodParams(interaction, true), - ...filters, - }); + async handler(interaction) { + const player = await resolvePlayerFromCommand(interaction); + const period = resolvePeriodFromCommand( + player.region, + interaction, + 'today', + ); + const filters = await getFiltersFromCommand(interaction); + const path = commandToURL(interaction, { + ...player, + ...getCustomPeriodParams(interaction, true), + ...filters, + }); - return [ - ...(await renderBreakdown(player, period, filters)), - buttonPrimary(path, 'Refresh'), - await getBlitzStarsLinkButton(player.region, player.id), - ]; - }, + return [ + ...(await renderBreakdown(player, period, filters)), + buttonPrimary(path, 'Refresh'), + await getBlitzStarsLinkButton(player.region, player.id), + ]; + }, - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, + autocomplete: (interaction) => { + autocompleteUsername(interaction); + autocompleteTanks(interaction); + }, - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - const filters = getFiltersFromButton(interaction); + async button(interaction) { + const player = await resolvePlayerFromButton(interaction); + const period = resolvePeriodFromButton(player.region, interaction); + const filters = getFiltersFromButton(interaction); - return await renderBreakdown(player, period, filters); - }, - }); - }, -); + return await renderBreakdown(player, period, filters); + }, + }); +}); diff --git a/src/core/discord/createLocalizedCommand.ts b/src/core/discord/createLocalizedCommand.ts index 1efda3cf8..dbadcd776 100644 --- a/src/core/discord/createLocalizedCommand.ts +++ b/src/core/discord/createLocalizedCommand.ts @@ -1,14 +1,27 @@ -import { Locale, SlashCommandBuilder } from 'discord.js'; +import { + Locale, + SlashCommandBuilder, + SlashCommandSubcommandBuilder, + SlashCommandSubcommandGroupBuilder, +} from 'discord.js'; import { translator } from '../localization/translator'; import { localizationObject } from './localizationObject'; interface CreateLocalizedCommandSubcommand { subcommand: string; + modify?: (option: SlashCommandSubcommandBuilder) => void; +} + +interface CreateLocalizedCommandSubcommandGroup { + group: string; + modify?: (option: SlashCommandSubcommandGroupBuilder) => void; } export function createLocalizedCommand( command: string, - subcommands?: CreateLocalizedCommandSubcommand[], + sub?: + | CreateLocalizedCommandSubcommand[] + | CreateLocalizedCommandSubcommandGroup[], ) { const { translate } = translator(Locale.EnglishUS); const slashCommand = new SlashCommandBuilder() @@ -19,26 +32,51 @@ export function createLocalizedCommand( localizationObject(`bot.commands.${command}.description`), ); - if (subcommands) { - subcommands.map(({ subcommand }) => { - const subcommandNameLocalizations = localizationObject( - `bot.commands.${command}.subcommands.${subcommand}`, - ); - const subcommandDescriptionLocalizations = localizationObject( - `bot.commands.${command}.subcommands.${subcommand}.description`, - ); + if (sub) { + sub.map((subItem) => { + if ('subcommand' in subItem) { + const subcommandNameLocalizations = localizationObject( + `bot.commands.${command}.subcommands.${subItem.subcommand}`, + ); + const subcommandDescriptionLocalizations = localizationObject( + `bot.commands.${command}.subcommands.${subItem.subcommand}.description`, + ); - slashCommand.addSubcommand((option) => - option - .setName(subcommand) - .setNameLocalizations(subcommandNameLocalizations) - .setDescription( - translate( - `bot.commands.${command}.subcommands.${subcommand}.description`, - ), - ) - .setDescriptionLocalizations(subcommandDescriptionLocalizations), - ); + slashCommand.addSubcommand((option) => { + if (subItem.modify) subItem.modify(option); + + return option + .setName(subItem.subcommand) + .setNameLocalizations(subcommandNameLocalizations) + .setDescription( + translate( + `bot.commands.${command}.subcommands.${subItem.subcommand}.description`, + ), + ) + .setDescriptionLocalizations(subcommandDescriptionLocalizations); + }); + } else { + const subcommandNameLocalizations = localizationObject( + `bot.commands.${command}.groups.${subItem.group}`, + ); + const subcommandDescriptionLocalizations = localizationObject( + `bot.commands.${command}.groups.${subItem.group}.description`, + ); + + slashCommand.addSubcommandGroup((option) => { + if (subItem.modify) subItem.modify(option); + + return option + .setName(subItem.group) + .setNameLocalizations(subcommandNameLocalizations) + .setDescription( + translate( + `bot.commands.${command}.groups.${subItem.group}.description`, + ), + ) + .setDescriptionLocalizations(subcommandDescriptionLocalizations); + }); + } }); } diff --git a/src/core/discord/resolvePeriodFromButton.ts b/src/core/discord/resolvePeriodFromButton.ts index 5498b7553..ecaf7f155 100644 --- a/src/core/discord/resolvePeriodFromButton.ts +++ b/src/core/discord/resolvePeriodFromButton.ts @@ -30,7 +30,7 @@ export default function resolvePeriodFromButton( start = getTimeDaysAgo(region, endDaysAgoMax); end = getTimeDaysAgo(region, startDaysAgoMin); } else { - name = getPeriodOptionName(periodSubcommand); + name = getPeriodOptionName(periodSubcommand, interaction.locale); start = getPeriodStart(region, periodSubcommand); end = getPeriodNow(); } diff --git a/src/core/discord/resolvePeriodFromCommand.ts b/src/core/discord/resolvePeriodFromCommand.ts index fc09987aa..043c53140 100644 --- a/src/core/discord/resolvePeriodFromCommand.ts +++ b/src/core/discord/resolvePeriodFromCommand.ts @@ -1,14 +1,17 @@ -import { CacheType, ChatInputCommandInteraction } from 'discord.js'; +import { CacheType, ChatInputCommandInteraction, Locale } from 'discord.js'; import { Region } from '../../constants/regions'; import getPeriodNow from '../blitzkrieg/getPeriodNow'; import getPeriodStart from '../blitzkrieg/getPeriodStart'; import getTimeDaysAgo from '../blitzkrieg/getTimeDaysAgo'; +import { translator } from '../localization/translator'; import { PeriodSize, PeriodType } from './addPeriodSubCommands'; -export function getPeriodOptionName(period: PeriodSize) { - if (period === 'career') return 'Career'; - if (period === 'today') return 'Today'; - return `${period} days`; +export function getPeriodOptionName(period: PeriodSize, locale: Locale) { + const { translate } = translator(locale); + + if (period === 'career') return translate('bot.common.periods.career'); + if (period === 'today') return translate('bot.common.periods.today'); + return translate('bot.common.periods.days', [period]); } export interface ResolvedPeriod { @@ -39,7 +42,7 @@ export default function resolvePeriodFromCommand( start = getTimeDaysAgo(region, endDaysAgoMax); end = getTimeDaysAgo(region, startDaysAgoMin); } else { - name = getPeriodOptionName(periodSubcommand); + name = getPeriodOptionName(periodSubcommand, interaction.locale); start = getPeriodStart(region, periodSubcommand); end = getPeriodNow(); } diff --git a/src/core/localization/translator.ts b/src/core/localization/translator.ts index 0b55fb879..78714fbcd 100644 --- a/src/core/localization/translator.ts +++ b/src/core/localization/translator.ts @@ -5,9 +5,10 @@ export function translator(localeRaw: Locale) { const locale = locales.includes(localeRaw) ? localeRaw : Locale.EnglishUS; const strings = translations[locale]; - function translate(path: string): string { + function translate(path: string, literals?: string[]): string { const pathArray = path.split('.'); let fragment = strings; + let resolvedString: string; for (const pathItem of pathArray) { if (typeof fragment === 'string') { @@ -33,14 +34,26 @@ export function translator(localeRaw: Locale) { } if (typeof fragment === 'string') { - return fragment; + resolvedString = fragment; } else if (fragment.$) { - return fragment.$!; + resolvedString = fragment.$!; } else { throw new Error( `Unresolved tree ending for "${path}" in locale "${locale}"`, ); } + + if (literals) { + const chunks = resolvedString.split('%s'); + let embeddedString = ''; + + chunks.forEach((chunk, index) => { + if (index !== 0) embeddedString += literals[index - 1]; + embeddedString += chunk; + }); + } + + return resolvedString; } function t(paths: TemplateStringsArray, ...embeds: string[]) { diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index 81e6b9452..c90b78c6c 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -17,7 +17,6 @@ import discord from '../../../discord.json' assert { type: 'json' }; import { aboutCommand } from '../../commands/about'; import { breakdownCommand } from '../../commands/breakdown'; import { debugCommand } from '../../commands/debug'; -import { eligibleCommand } from '../../commands/eligible'; import { evolutionCommand } from '../../commands/evolution'; import { fullStatsCommand } from '../../commands/fullStats'; import { inactiveCommand } from '../../commands/inactive'; @@ -78,22 +77,18 @@ type CommandRegistryDefinitionDoesHandleInteraction = { interaction: ChatInputCommandInteraction, ) => InteractionReturnable; }; -export type CommandRegistryPromisable = CommandRegistryBase & +export type CommandRegistry = CommandRegistryBase & (CommandRegistryDefinitionBase & ( | CommandRegistryDefinitionHandlesInteraction | CommandRegistryDefinitionDoesHandleInteraction )); -export type CommandRegistry = - | CommandRegistryPromisable - | Promise; const rest = new REST().setToken(secrets.DISCORD_TOKEN); -export const COMMANDS_RAW: CommandRegistry[] = [ +export const COMMANDS_RAW: Promise[] = [ permissionsCommand, debugCommand, - eligibleCommand, aboutCommand, inactiveCommand, ownedTanksCommand, @@ -113,7 +108,7 @@ export const COMMANDS_RAW: CommandRegistry[] = [ ]; export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => { - return rawCommands.reduce>( + return rawCommands.reduce>( (commands, registry) => { if (registry.status === 'rejected') return commands; if (isDev()) registry.value.command.setDefaultMemberPermissions(0); diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 2fa78623d..fa80328a0 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -51,6 +51,13 @@ } } } + }, + + "periods": { + "career": "Career", + "today": "Today", + "days": "%s days", + "custom": "%s - %s days" } }, @@ -112,6 +119,23 @@ }, "uptime": "Uptime" } + }, + + "evolution": { + "$": "evolution", + "description": "Evolution of statistics", + + "groups": { + "player": { + "$": "player", + "description": "Player statistics" + }, + + "tank": { + "$": "tank", + "description": "Tank statistics" + } + } } } },