feat: some period translations

This commit is contained in:
TrèsAbhi committed 2024-03-06 15:34:10 -06:00
1 parent 267f23cad8
commit adad668157
24 files changed
+1309 -1216

No files matched your search

+2 -2
View File
@@ -35,7 +35,7 @@ import resolvePlayerFromCommand, {
import calculateWN8 from '../core/statistics/calculateWN8'; import calculateWN8 from '../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile'; import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8; const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9; const MAX_PAGES = 9;
@@ -290,7 +290,7 @@ export async function renderBreakdown(
} }
} }
export const breakdownCommand = new Promise<CommandRegistryPromisable>( export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => { async (resolve) => {
resolve({ resolve({
inProduction: true, inProduction: true,
+30 -28
View File
@@ -6,33 +6,35 @@ import { CommandRegistry } from '../events/interactionCreate';
const executionStart = new Date().getTime(); const executionStart = new Date().getTime();
export const debugCommand: CommandRegistry = { export const debugCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: createLocalizedCommand('debug'), command: createLocalizedCommand('debug'),
async handler(interaction) { async handler(interaction) {
const { t } = translator(interaction.locale); const { t } = translator(interaction.locale);
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const uptime = currentTime - executionStart; const uptime = currentTime - executionStart;
const list = [ const list = [
[t`bot.commands.debug.body.version`, packageJSON.version], [t`bot.commands.debug.body.version`, packageJSON.version],
[ [
t`bot.commands.debug.body.shard`, t`bot.commands.debug.body.shard`,
client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`, client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`,
], ],
[ [
t`bot.commands.debug.body.uptime`, t`bot.commands.debug.body.uptime`,
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor( `${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
(uptime / 1000 / 60) % 60, (uptime / 1000 / 60) % 60,
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor( )}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
uptime % 1000, uptime % 1000,
)}ms`, )}ms`,
], ],
]; ];
return t`bot.commands.debug.body.title${list return t`bot.commands.debug.body.title${list
.map(([key, value]) => `- ${key}: ${value}`) .map(([key, value]) => `- ${key}: ${value}`)
.join('\n')}`; .join('\n')}`;
}, },
}; });
});
+185 -178
View File
@@ -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<SkilledClan, string> = { // export type SkilledClan = 'SKLLD' | 'SMRI';
SKLLD: 'Skilled',
SMRI: 'Samurai',
};
export const eligibleCommand: CommandRegistry = { // export const SKILLED_CLANS: Record<SkilledClan, string> = {
inProduction: true, // SKLLD: 'Skilled',
inPublic: false, // SMRI: 'Samurai',
// };
command: new SlashCommandBuilder() // export const eligibleCommand: CommandRegistry = {
.setName('eligible') // inProduction: true,
.setDescription('Checks eligibility for any Skilled clan') // inPublic: false,
.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),
async handler(interaction) { // command: new SlashCommandBuilder()
const clan = interaction.options.getString('clan') as SkilledClan; // .setName('eligible')
const { id, region: region } = await resolvePlayerFromCommand(interaction); // .setDescription('Checks eligibility for any Skilled clan')
const accountInfo = await getAccountInfo(region, id); // .addStringOption((option) =>
const problems: TableInput = []; // option
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`; // .setName('clan')
let isEligible = false; // .setDescription('The clan to check')
let body = ''; // .setChoices(
// {
// name: SKILLED_CLANS.SKLLD,
// value: 'SKLLD' satisfies SkilledClan,
// },
// { name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan },
// )
// .setRequired(true),
// )
// .addStringOption(addUsernameChoices),
if (clan === 'SKLLD') { // async handler(interaction) {
const { diff: diffed } = await getStatsInPeriod( // const clan = interaction.options.getString('clan') as SkilledClan;
region, // const { id, region: region } = await resolvePlayerFromCommand(interaction);
id, // const accountInfo = await getAccountInfo(region, id);
getTimeDaysAgo(region, 30), // const problems: TableInput = [];
getPeriodNow(), // const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
); // let isEligible = false;
const entries = Object.entries(diffed); // let body = '';
const stats = sumStats(entries.map(([, stats]) => stats));
const averageTier =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankDefinition = (await tankDefinitions)[tankId];
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<PossiblyPromise<number>>(
// 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; // const tankTier = tankDefinition.tier;
},
0,
)) /
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankDefinition = (await tankDefinitions)[tankId];
if (!tankDefinition) return accumulator; // return (await accumulator) + tankTier * stats.battles;
// },
// 0,
// )) /
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
// const tankDefinition = (await tankDefinitions)[tankId];
return stats.battles; // if (!tankDefinition) return accumulator;
},
0,
));
const WN8 =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
// edge case where new tanks don't have averages // return stats.battles;
if ((await tankAverages)[tankId]) { // },
const tankWN8 = calculateWN8( // 0,
(await tankAverages)[tankId].all, // ));
stats, // const WN8 =
); // (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
if (isNaN(tankWN8)) return accumulator; // // edge case where new tanks don't have averages
return (await accumulator) + tankWN8 * stats.battles; // if ((await tankAverages)[tankId]) {
} else return accumulator; // const tankWN8 = calculateWN8(
}, // (await tankAverages)[tankId].all,
0, // stats,
)) / stats.battles; // );
const tier10Damage =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
if ((await tankDefinitions)[tankId]?.tier === 10) { // if (isNaN(tankWN8)) return accumulator;
const tankStats = diffed[tankId]; // return (await accumulator) + tankWN8 * stats.battles;
return (await accumulator) + tankStats.damage_dealt; // } else return accumulator;
} else return accumulator; // },
}, // 0,
0, // )) / stats.battles;
)) / // const tier10Damage =
(await entries.reduce<PossiblyPromise<number>>( // (await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => { // async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString); // const tankId = parseInt(tankIdString);
if ((await tankDefinitions)[tankId]?.tier === 10) { // if ((await tankDefinitions)[tankId]?.tier === 10) {
const tankStats = diffed[tankId]; // const tankStats = diffed[tankId];
return (await accumulator) + tankStats.battles; // return (await accumulator) + tankStats.damage_dealt;
} else return accumulator; // } else return accumulator;
}, // },
0, // 0,
)); // )) /
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
if (stats.battles < 126) { // if ((await tankDefinitions)[tankId]?.tier === 10) {
problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]); // const tankStats = diffed[tankId];
} // return (await accumulator) + tankStats.battles;
if (averageTier < 8) { // } else return accumulator;
problems.push([ // },
'Average tier', // 0,
`${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})`,
]);
}
isEligible = problems.length === 0; // if (stats.battles < 126) {
body = isEligible // problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]);
? 'Player is fully eligible.' // }
: `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable( // if (averageTier < 8) {
problems, // problems.push([
)}`; // 'Average tier',
} else if (clan === 'SMRI') { // `${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`,
const stats = accountInfo.statistics.all; // ]);
// }
// 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) { // isEligible = problems.length === 0;
problems.push([ // body = isEligible
'Battles', // ? 'Player is fully eligible.'
`${stats.battles} (${stats.battles - 1500})`, // : `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable(
]); // problems,
} // )}`;
if (stats.wins / stats.battles <= 0.53) { // } else if (clan === 'SMRI') {
problems.push([ // const stats = accountInfo.statistics.all;
'Winrate',
`${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
100 *
(stats.wins / stats.battles - 0.53)
).toFixed(2)}%)`,
]);
}
isEligible = problems.length === 0; // if (stats.battles < 1500) {
body = isEligible // problems.push([
? 'Player is fully eligible.' // 'Battles',
: `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable( // `${stats.battles} (${stats.battles - 1500})`,
problems, // ]);
)}`; // }
} // 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);
// },
// };
+24 -36
View File
@@ -1,7 +1,4 @@
import { import { SlashCommandSubcommandGroupBuilder } from 'discord.js';
SlashCommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import CommandWrapper from '../components/CommandWrapper'; import CommandWrapper from '../components/CommandWrapper';
import * as Graph from '../components/Graph'; import * as Graph from '../components/Graph';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
@@ -20,6 +17,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary'; import buttonPrimary from '../core/discord/buttonPrimary';
import commandToURL from '../core/discord/commandToURL'; import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton'; import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand, { import resolvePeriodFromCommand, {
@@ -29,7 +27,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, { import resolvePlayerFromCommand, {
ResolvedPlayer, ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
type EvolutionStatType = 'player' | 'tank'; type EvolutionStatType = 'player' | 'tank';
@@ -107,40 +105,32 @@ async function render(
); );
} }
export const evolutionCommand = new Promise<CommandRegistryPromisable>( export const evolutionCommand = new Promise<CommandRegistry>(
async (resolve) => { 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({ resolve({
inProduction: true, inProduction: true,
inPublic: 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) { async handler(interaction) {
const commandGroup = const commandGroup =
@@ -156,8 +146,6 @@ export const evolutionCommand = new Promise<CommandRegistryPromisable>(
tankId, tankId,
}); });
getBlitzStarsLinkButton;
return [ return [
await render(player, period, commandGroup, tankId), await render(player, period, commandGroup, tankId),
buttonPrimary(path, 'Refresh'), buttonPrimary(path, 'Refresh'),
+2 -2
View File
@@ -29,7 +29,7 @@ import resolvePlayerFromCommand, {
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { getTierWeights } from '../core/statistics/getTierWeights'; import { getTierWeights } from '../core/statistics/getTierWeights';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
@@ -64,7 +64,7 @@ async function render(
); );
} }
export const fullStatsCommand = new Promise<CommandRegistryPromisable>( export const fullStatsCommand = new Promise<CommandRegistry>(
async (resolve) => { async (resolve) => {
const command = await addPeriodicFilterOptions( const command = await addPeriodicFilterOptions(
new SlashCommandBuilder() new SlashCommandBuilder()
+53 -50
View File
@@ -12,58 +12,61 @@ import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_THRESHOLD = 7; const DEFAULT_THRESHOLD = 7;
export const inactiveCommand: CommandRegistry = { export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('inactive') .setName('inactive')
.setDescription('Lists all inactive players') .setDescription('Lists all inactive players')
.addStringOption(addClanChoices) .addStringOption(addClanChoices)
.addNumberOption((option) => .addNumberOption((option) =>
option option
.setName('threshold') .setName('threshold')
.setDescription( .setDescription(
`The number of days inactive (default: ${DEFAULT_THRESHOLD})`, `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) { return (
const { region, id } = await resolveClanFromCommand(interaction); <CommandWrapper>
const threshold = <TitleBar
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD; title={clanInfo.name}
const time = new Date().getTime() / 1000; image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
const clanInfo = await getClanInfo(region, id); description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
const accountInfo = await getAccountInfo(region, clanInfo.members_ids); />
const inactive = accountInfo
.map( {!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
(account) => {hasInactiveMembers && <GenericStats stats={inactive} />}
[ </CommandWrapper>
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],
); );
const hasInactiveMembers = inactive.length > 0; },
return ( autocomplete: autocompleteClan,
<CommandWrapper> });
<TitleBar });
title={clanInfo.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
/>
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
{hasInactiveMembers && <GenericStats stats={inactive} />}
</CommandWrapper>
);
},
autocomplete: autocompleteClan,
};
+148 -144
View File
@@ -16,194 +16,198 @@ import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
export const verifyCommand: CommandRegistry = { export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('link') .setName('link')
.setDescription('Links your Blitz and Discord account') .setDescription('Links your Blitz and Discord account')
.addStringOption((option) => addUsernameChoices(option).setRequired(true)), .addStringOption((option) =>
addUsernameChoices(option).setRequired(true),
),
async handler(interaction) { async handler(interaction) {
const { id, region } = await resolvePlayerFromCommand(interaction); const { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = parseInt(interaction.user.id); const discordId = parseInt(interaction.user.id);
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); 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.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
return embedNegative( return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`, `${markdownEscape(interaction.user.username)} failed to verify`,
"I don't have the permission to change your manage roles.", "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( return [
discord.sklld_verify_role, <CommandWrapper fat>
);
await (interaction.member!.roles as GuildMemberRoleManager).add(
discord.sklld_peasant_role,
);
}
return [
<CommandWrapper fat>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'space-between',
gap: 16,
}} }}
> >
<Glow color={teal.teal9} rotation={90} />
<div <div
style={{ style={{
display: 'flex', display: 'flex',
gap: 8, flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 16,
}} }}
> >
<img
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
/>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', gap: 8,
gap: 4, alignItems: 'center',
justifyContent: 'center',
}} }}
> >
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<img
src="https://i.imgur.com/jIcRgog.png"
style={{
width: 16,
height: 16,
}}
/>
<span style={{ fontSize: 16 }}>Accounts linked</span>
</div>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img <img
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{ style={{
width: 64, width: 64,
height: 64, height: 64,
borderRadius: 32, borderRadius: 32,
objectFit: 'cover', objectFit: 'cover',
}} }}
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
/> />
) : null}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', alignItems: 'center',
justifyContent: 'center',
gap: 4, gap: 4,
color: theme.colors.textLowContrast_teal,
}} }}
> >
<span <img
src="https://i.imgur.com/jIcRgog.png"
style={{ style={{
fontSize: 32, width: 16,
color: theme.colors.textHighContrast, height: 16,
fontWeight: 900, }}
maxWidth: 240, />
overflow: 'hidden', <span style={{ fontSize: 16 }}>Accounts linked</span>
textOverflow: 'ellipsis', </div>
whiteSpace: 'nowrap',
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
/>
) : null}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}} }}
> >
{accountInfo.nickname} <span
</span> style={{
<span fontSize: 32,
style={{ color: theme.colors.textHighContrast,
fontSize: 16, fontWeight: 900,
color: theme.colors.textLowContrast, maxWidth: 240,
}} overflow: 'hidden',
> textOverflow: 'ellipsis',
{clanAccountInfo?.clan whiteSpace: 'nowrap',
? `[${clanAccountInfo?.clan?.tag}] • ` }}
: ''} >
{REGION_NAMES[region]} {accountInfo.nickname}
</span> </span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] • `
: ''}
{REGION_NAMES[region]}
</span>
</div>
</div> </div>
</div> </div>
<Glow color={teal.teal9} rotation={-90} />
</div> </div>
</CommandWrapper>,
<Glow color={teal.teal9} rotation={-90} /> embedInfo(
</div> 'Incorrect account?',
</CommandWrapper>, 'Use the command again and select a username from the search results.',
),
];
},
embedInfo( autocomplete: autocompleteUsername,
'Incorrect account?', });
'Use the command again and select a username from the search results.', });
),
];
},
autocomplete: autocompleteUsername,
};
+99 -95
View File
@@ -21,108 +21,112 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const ownedTanksCommand: CommandRegistry = { export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('owned-tanks') .setName('owned-tanks')
.setDescription("Shows a player's owned tanks") .setDescription("Shows a player's owned tanks")
.addStringOption(addTierChoices) .addStringOption(addTierChoices)
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const tier = Number(interaction.options.getString('tier')); const tier = Number(interaction.options.getString('tier'));
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, region: server } = account; const { id, region: server } = account;
const accountInfo = await getAccountInfo(server, id); const accountInfo = await getAccountInfo(server, id);
const tankStats = await getTankStats(server, id); const tankStats = await getTankStats(server, id);
const filteredTanks = ( const filteredTanks = (
await Promise.all( await Promise.all(
tankStats.map(async (tankData) => ({ tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!, tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
id: tankData.tank_id, id: tankData.tank_id,
})), })),
) )
).filter((tank) => tank.tankDefinitions?.tier === tier); ).filter((tank) => tank.tankDefinitions?.tier === tier);
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']); const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
const groupedTanks: Record<string, TankDefinition[]> = {}; const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = []; const nations: string[] = [];
filteredTanks.forEach((tank) => { filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankDefinitions.nation] === undefined) { if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions]; groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation); nations.push(tank.tankDefinitions.nation);
} else { } else {
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions); groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
} }
}); });
nations.sort(); nations.sort();
return ( return (
<CommandWrapper> <CommandWrapper>
<TitleBar <TitleBar
title={accountInfo.nickname} title={accountInfo.nickname}
image={ image={
clanAccountInfo?.clan clanAccountInfo?.clan
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id) ? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
: undefined : undefined
} }
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`} description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`}
/> />
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />} {filteredTanks.length === 0 && (
<NoData type={NoDataType.TanksFound} />
)}
{filteredTanks.length > 0 && {filteredTanks.length > 0 &&
(await Promise.all( (await Promise.all(
nations.map(async (nation) => { nations.map(async (nation) => {
const tanks = groupedTanks[nation]; const tanks = groupedTanks[nation];
const leftColumnSize = Math.ceil(tanks.length / 2); const leftColumnSize = Math.ceil(tanks.length / 2);
const leftColumn = tanks.slice(0, leftColumnSize); const leftColumn = tanks.slice(0, leftColumnSize);
const rightColumn = tanks.slice(leftColumnSize); const rightColumn = tanks.slice(leftColumnSize);
return ( return (
<Tanks.Root> <Tanks.Root>
<Tanks.Title> <Tanks.Title>
{(await encyclopediaInfo).vehicle_nations[nation]} {(await encyclopediaInfo).vehicle_nations[nation]}
</Tanks.Title> </Tanks.Title>
<Tanks.Row> <Tanks.Row>
<Tanks.Column> <Tanks.Column>
{await Promise.all( {await Promise.all(
leftColumn.map(async (tank) => ( leftColumn.map(async (tank) => (
<Tanks.Item <Tanks.Item
key={tank.id} key={tank.id}
name={tank.name} name={tank.name}
tankType={tank.type} tankType={tank.type}
image={tankIcon(tank.id)} image={tankIcon(tank.id)}
treeType={tank.treeType} treeType={tank.treeType}
/> />
)), )),
)} )}
</Tanks.Column> </Tanks.Column>
<Tanks.Column> <Tanks.Column>
{await Promise.all( {await Promise.all(
rightColumn.map(async (tank) => ( rightColumn.map(async (tank) => (
<Tanks.Item <Tanks.Item
key={tank.id} key={tank.id}
name={tank.name} name={tank.name}
tankType={tank.type} tankType={tank.type}
image={tankIcon(tank.id)} image={tankIcon(tank.id)}
treeType={tank.treeType} treeType={tank.treeType}
/> />
)), )),
)} )}
</Tanks.Column> </Tanks.Column>
</Tanks.Row> </Tanks.Row>
</Tanks.Root> </Tanks.Root>
); );
}), }),
))} ))}
</CommandWrapper> </CommandWrapper>
); );
}, },
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
}; });
});
+38 -36
View File
@@ -1,42 +1,44 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const permissionsCommand: CommandRegistry = { export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('permissions') .setName('permissions')
.setDescription('Checks wether the bot has the required permissions'), .setDescription('Checks wether the bot has the required permissions'),
async handler(interaction) { async handler(interaction) {
// friday the 13th permissions easter egg // friday the 13th permissions easter egg
const isFridayThe13th = const isFridayThe13th =
new Date().getDay() === 5 && new Date().getDate() === 13; new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [ const permissions = [
[ [
interaction.appPermissions?.has('ViewChannel'), interaction.appPermissions?.has('ViewChannel'),
'View channels: needed for the refresh button to work', 'View channels: needed for the refresh button to work',
], ],
[ [
interaction.appPermissions?.has('ReadMessageHistory'), interaction.appPermissions?.has('ReadMessageHistory'),
'Read message history: also needed for the refresh button to work', 'Read message history: also needed for the refresh button to work',
], ],
[ [
interaction.appPermissions?.has('AttachFiles'), interaction.appPermissions?.has('AttachFiles'),
'Attach files: needed for image-based commands to work', 'Attach files: needed for image-based commands to work',
], ],
]; ];
const body = permissions const body = permissions
.map(([hasPermission, description]) => { .map(([hasPermission, description]) => {
if (hasPermission) { if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`; return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
} else { } else {
return `${isFridayThe13th ? '💀' : '❌'} ${description}`; return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
} }
}) })
.join('\n'); .join('\n');
return `# Permissions\n${body}`; return `# Permissions\n${body}`;
}, },
}; });
});
+27 -25
View File
@@ -1,32 +1,34 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const pingCommand: CommandRegistry = { export const pingCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
handlesInteraction: true, inPublic: true,
handlesInteraction: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('ping') .setName('ping')
.setDescription('Check if the bot is alive') .setDescription('Check if the bot is alive')
.addSubcommand((option) => .addSubcommand((option) =>
option.setName('blitzkrieg').setDescription('Ping Blitzkrieg'), option.setName('blitzkrieg').setDescription('Ping Blitzkrieg'),
) )
.addSubcommand((option) => .addSubcommand((option) =>
option.setName('blitz').setDescription('Ping Blitz'), option.setName('blitz').setDescription('Ping Blitz'),
), ),
async handler(interaction) { async handler(interaction) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now(); const executionStart = Date.now();
if (subcommand === 'blitzkrieg') { if (subcommand === 'blitzkrieg') {
await interaction.editReply('Pong 🏓'); await interaction.editReply('Pong 🏓');
} else { } else {
await fetch('https://api.wotblitz.com/'); await fetch('https://api.wotblitz.com/');
} }
const executionTime = Date.now() - executionStart; const executionTime = Date.now() - executionStart;
interaction.editReply(`Pong 🏓 - ${executionTime}ms`); interaction.editReply(`Pong 🏓 - ${executionTime}ms`);
}, },
}; });
});
+52 -47
View File
@@ -12,57 +12,62 @@ import { CommandRegistry } from '../events/interactionCreate';
type SortBy = 'name' | 'count'; type SortBy = 'name' | 'count';
export const playerAchievementsCommand: CommandRegistry = { export const playerAchievementsCommand = new Promise<CommandRegistry>(
inProduction: true, (resolve) => {
inPublic: true, resolve({
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('player-achievements') .setName('player-achievements')
.setDescription("All the player's achievements") .setDescription("All the player's achievements")
.addStringOption(addUsernameChoices) .addStringOption(addUsernameChoices)
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('sort') .setName('sort')
.setDescription('What to sort by (default: name)') .setDescription('What to sort by (default: name)')
.addChoices( .addChoices(
{ name: 'By name', value: 'name' satisfies SortBy }, { name: 'By name', value: 'name' satisfies SortBy },
{ name: 'By count', value: 'count' satisfies SortBy }, { name: 'By count', value: 'count' satisfies SortBy },
),
), ),
),
async handler(interaction) { async handler(interaction) {
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy; const sortBy = (interaction.options.getString('sort') ??
const account = await resolvePlayerFromCommand(interaction); 'name') as SortBy;
const { id, region: region } = account; const account = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id); const { id, region: region } = account;
const accountsAchievements = await getAccountAchievements(region, id); const accountInfo = await getAccountInfo(region, id);
const accountAchievements = accountsAchievements; const accountsAchievements = await getAccountAchievements(region, id);
const compound = { const accountAchievements = accountsAchievements;
...accountAchievements.achievements, const compound = {
...accountAchievements.max_series, ...accountAchievements.achievements,
}; ...accountAchievements.max_series,
};
return embedInfo( return embedInfo(
`${markdownEscape(accountInfo.nickname)}'s information`, `${markdownEscape(accountInfo.nickname)}'s information`,
markdownTable( markdownTable(
Object.keys(compound) Object.keys(compound)
.filter((achievement) => compound[achievement] > 0) .filter((achievement) => compound[achievement] > 0)
.sort( .sort(
sortBy === 'count' sortBy === 'count'
? (a, b) => compound[b] - compound[a] ? (a, b) => compound[b] - compound[a]
: undefined, : undefined,
) )
.map( .map(
(achievement) => (achievement) =>
[ [
startCase(achievement), startCase(achievement),
compound[achievement], compound[achievement],
] as TableInputEntry, ] as TableInputEntry,
),
), ),
), );
); },
},
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
}; });
},
);
+41 -39
View File
@@ -8,47 +8,49 @@ import markdownTable from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const playerInfoCommand: CommandRegistry = { export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('player-info') .setName('player-info')
.setDescription('Basic information about a player') .setDescription('Basic information about a player')
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, region: region } = account; const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
return embedInfo( return embedInfo(
`${markdownEscape(accountInfo.nickname)}'s information`, `${markdownEscape(accountInfo.nickname)}'s information`,
markdownTable([ markdownTable([
['Nickname', `${accountInfo.nickname}`], ['Nickname', `${accountInfo.nickname}`],
['Battles', `${accountInfo.statistics.all.battles}`], ['Battles', `${accountInfo.statistics.all.battles}`],
[ [
'Winrate', 'Winrate',
`${( `${(
100 * 100 *
(accountInfo.statistics.all.wins / (accountInfo.statistics.all.wins /
accountInfo.statistics.all.battles) accountInfo.statistics.all.battles)
).toFixed(2)}%`, ).toFixed(2)}%`,
], ],
[], [],
['Account ID', `${accountInfo.account_id}`], ['Account ID', `${accountInfo.account_id}`],
[ [
'Created', 'Created',
`${new Date(accountInfo.created_at * 1000).toDateString()}`, `${new Date(accountInfo.created_at * 1000).toDateString()}`,
], ],
[ [
'Last battle', 'Last battle',
`${new Date(accountInfo.last_battle_time * 1000).toDateString()}`, `${new Date(accountInfo.last_battle_time * 1000).toDateString()}`,
], ],
]), ]),
); );
}, },
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
}; });
});
+303 -308
View File
@@ -27,7 +27,7 @@ import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative'; import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { UserError } from '../hooks/userError'; import { UserError } from '../hooks/userError';
export interface RatingsPlayer { export interface RatingsPlayer {
@@ -122,334 +122,329 @@ const noOngoingSeason = embedNegative(
"Wargaming didn't provide any data for this season.", "Wargaming didn't provide any data for this season.",
); );
export const ratingsCommand = new Promise<CommandRegistryPromisable>( export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
async (resolve) => { const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); const onGoingSeason = await isOnGoingRatingsSeason();
const onGoingSeason = await isOnGoingRatingsSeason(); const seasonNumbers = range(
const seasonNumbers = range( FIRST_ARCHIVED_RATINGS_SEASON,
FIRST_ARCHIVED_RATINGS_SEASON, latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
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<string>,
)
.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<string>,
),
)
.setRequired(true),
),
),
); );
function addSubcommands(option: SlashCommandSubcommandBuilder) { resolve({
return option inProduction: true,
.addStringOption(addUsernameChoices) inPublic: true,
.addIntegerOption((option) =>
option command,
.setName('limit')
.setDescription('How many neighbors to display (default: 10)') async handler(interaction) {
.setRequired(false) interface SimplifiedPlayer {
.setMinValue(5) id: number;
.setMaxValue(30), score: number;
) position: number;
.addStringOption((option) => clan: string | undefined;
option nickname: string;
.setName('season') }
.setDescription('Season number')
.setRequired(false) const subcommand = interaction.options.getSubcommand(true) as
.addChoices( | 'league'
...seasonNumbers | 'neighbors';
.map( const limit = interaction.options.getInteger('limit') ?? 10;
(number, index) => 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)' : ''}`, id: player.id,
value: `${number}`, score: player.score,
}) satisfies APIApplicationCommandOptionChoice<string>, position: firstPlayerIndex + index + 1,
) clan: clanData[player.id]?.clan?.tag,
.reverse(), 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() players = result;
.setName('ratings') playersBefore = result ? result[0].position - 1 : 0;
.setDescription('Ratings battles statistics') playersAfter =
.addSubcommand((option) => regionRatingsInfo.count - result[result.length - 1].position;
addSubcommands( titleName = `${
option leagueInfo ? leagueInfo.title : 'No Ongoing Season'
.setName('neighbors') } - ${REGION_NAMES_SHORT[region]}`;
.setDescription('Shows neighboring players on the leaderboard'), titleImage = leagueInfo.big_icon.startsWith('http')
), ? leagueInfo.big_icon
) : `https:${leagueInfo.big_icon}`;
.addSubcommand((option) => titleDescription = `Top ${limit} players`;
addSubcommands( } else {
option const { region, id } = await resolvePlayerFromCommand(interaction);
.setName('league') regionRatingsInfo = isCurrentSeason
.setDescription('Shows top players from a league') ? await getRatingsInfo(region)
.addStringOption(addRegionChoices) : await getArchivedRatingsInfo(region, season!);
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
value: `${value}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
),
),
);
resolve({ if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
inProduction: true,
inPublic: true,
command, const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
async handler(interaction) { const neighbors = isCurrentSeason
interface SimplifiedPlayer { ? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
id: number; (players) =>
score: number; players.neighbors.map(
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) => (player) =>
({ ({
id: player.spa_id, id: player.spa_id,
score: player.score,
position: player.number, position: player.number,
score: player.score,
clan: player.clan_tag, clan: player.clan_tag,
nickname: player.nickname, nickname: player.nickname,
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer,
), ),
) )
: await getArchivedRatingsLeaderboard(region, season!).then( : await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => { async (players) => {
const firstPlayerIndex = const playerIndex = players.findIndex(
leagueIndex === 0 (player) => player.id === id,
? 0 );
: players.findIndex(
(player) => if (playerIndex === -1) {
player.score < LEAGUES[leagueIndex - 1].minScore, 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 lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
); );
}
return trimmed.map( const halfRange = Math.round(limit / 2);
(player, index) => const trimmed = players.slice(
({ playerIndex - halfRange,
id: player.id, playerIndex + halfRange + 1,
score: player.score, );
position: firstPlayerIndex + index + 1, const clanData = await getClanAccountInfo(
clan: clanData[player.id]?.clan?.tag, region,
nickname: trimmed.map((player) => player.id),
clanData[index]?.account_name ?? ['clan'],
`Deleted ${player.id}`, );
}) satisfies SimplifiedPlayer,
);
},
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
players = result; return trimmed.map((player, index) => ({
playersBefore = result ? result[0].position - 1 : 0; id: player.id,
playersAfter = score: player.score,
regionRatingsInfo.count - result[result.length - 1].position; position: playerIndex + index + 1,
titleName = `${ clan: clanData[index]?.clan?.tag,
leagueInfo ? leagueInfo.title : 'No Ongoing Season' nickname: clanData[index]
} - ${REGION_NAMES_SHORT[region]}`; ? clanData[index]!.account_name
titleImage = leagueInfo.big_icon.startsWith('http') : `Deleted ${player.id}`,
? leagueInfo.big_icon }));
: `https:${leagueInfo.big_icon}`; },
titleDescription = `Top ${limit} players`; );
} else { midnightLeaderboard = isCurrentSeason
const { region, id } = await resolvePlayerFromCommand(interaction); ? await getArchivedRatingsMidnightLeaderboard(
regionRatingsInfo = isCurrentSeason region,
? await getRatingsInfo(region) regionRatingsInfo.current_season,
: await getArchivedRatingsInfo(region, 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 resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; detail: undefined;
const neighbors = isCurrentSeason } = regionRatingsInfo;
? 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,
);
if (playerIndex === -1) { const items = players.map((player) => {
throw new UserError( const reward = resolvedRegionRatingsInfo.rewards.find(
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`, (reward) =>
); player.position >= reward.from_position &&
} player.position <= reward.to_position,
);
const halfRange = Math.round(limit / 2); const midnightIndex = midnightLeaderboard?.findIndex(
const trimmed = players.slice( (item) => item.id === player.id,
playerIndex - halfRange, );
playerIndex + halfRange + 1, const midnightScore =
); midnightIndex === undefined
const clanData = await getClanAccountInfo( ? player.score
region, : midnightLeaderboard![midnightIndex].score;
trimmed.map((player) => player.id), const midnightPosition =
['clan'], midnightIndex === undefined ? player.position : midnightIndex + 1;
);
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 (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.position}
clan={player.clan}
reward={reward}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return ( return (
<CommandWrapper> <Leaderboard.Item
<TitleBar nickname={player.nickname}
title={titleName} score={player.score}
image={titleImage} position={player.position}
description={titleDescription} clan={player.clan}
/> reward={reward}
deltaScore={player.score - midnightScore}
{items && ( deltaPosition={midnightPosition - player.position}
<Leaderboard.Root> key={player.id}
{playersBefore > 0 && ( highlight={player.id === playerId}
<Leaderboard.Gap />
message={`+ ${playersBefore.toLocaleString()} more`}
/>
)}
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={`+ ${playersAfter.toLocaleString()} more`}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
); );
}, });
autocomplete: autocompleteUsername, return (
}); <CommandWrapper>
}, <TitleBar
); title={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{playersBefore > 0 && (
<Leaderboard.Gap
message={`+ ${playersBefore.toLocaleString()} more`}
/>
)}
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={`+ ${playersAfter.toLocaleString()} more`}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
);
},
autocomplete: autocompleteUsername,
});
});
+36 -32
View File
@@ -9,38 +9,42 @@ import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { ClanList } from '../types/clanList'; import { ClanList } from '../types/clanList';
export const searchClansCommand: CommandRegistry = { export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('search-clans') .setName('search-clans')
.setDescription('Search clans') .setDescription('Search clans')
.addStringOption(addRegionChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => addClanChoices(option).setAutocomplete(false)) .addStringOption((option) =>
.addIntegerOption((option) => addClanChoices(option).setAutocomplete(false),
option )
.setName('limit') .addIntegerOption((option) =>
.setDescription('The size of the search result (default: 25)') option
.setMinValue(1) .setName('limit')
.setMaxValue(100), .setDescription('The size of the search result (default: 25)')
), .setMinValue(1)
.setMaxValue(100),
),
async handler(interaction) { async handler(interaction) {
const server = interaction.options.getString('region') as Region; const server = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!; const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const clanList = await fetchBlitz<ClanList>( const clanList = await fetchBlitz<ClanList>(
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`, `https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
); );
return embedInfo( return embedInfo(
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`, `Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
clanList.length === 0 clanList.length === 0
? 'No clans found.' ? 'No clans found.'
: `\`\`\`\n${clanList : `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`) .map((clan) => `${clan.name} [${clan.tag}]`)
.join('\n')}\n\`\`\``, .join('\n')}\n\`\`\``,
); );
}, },
}; });
});
+41 -39
View File
@@ -12,45 +12,47 @@ import addUsernameChoices from '../core/discord/addUsernameChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const searchPlayersCommand: CommandRegistry = { export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('search-players') .setName('search-players')
.setDescription('Search players in a Blitz server') .setDescription('Search players in a Blitz server')
.addStringOption(addRegionChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true), addUsernameChoices(option).setAutocomplete(false).setRequired(true),
) )
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName('limit')
.setDescription('The size of the search result (default: 25)') .setDescription('The size of the search result (default: 25)')
.setMinValue(1) .setMinValue(1)
.setMaxValue(100), .setMaxValue(100),
), ),
async handler(interaction) { async handler(interaction) {
const server = interaction.options.getString('region') as Region; const server = interaction.options.getString('region') as Region;
const name = interaction.options.getString('username')!; const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim(); const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch) const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>( ? await fetchBlitz<AccountList>(
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent( `https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
trimmedSearch, trimmedSearch,
)}&limit=${limit}`, )}&limit=${limit}`,
) )
: []; : [];
return embedInfo( return embedInfo(
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`, `Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
`\`\`\`${ `\`\`\`${
players.length === 0 players.length === 0
? 'No players found.' ? 'No players found.'
: players.map((player) => player.nickname).join('\n') : players.map((player) => player.nickname).join('\n')
}\`\`\``, }\`\`\``,
); );
}, },
}; });
});
+33 -29
View File
@@ -6,35 +6,39 @@ import addTankChoices from '../core/discord/addTankChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const searchTanksCommand: CommandRegistry = { export const searchTanksCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('search-tanks') .setName('search-tanks')
.setDescription('Search tanks') .setDescription('Search tanks')
.addStringOption((option) => addTankChoices(option).setAutocomplete(false)) .addStringOption((option) =>
.addIntegerOption((option) => addTankChoices(option).setAutocomplete(false),
option )
.setName('limit') .addIntegerOption((option) =>
.setDescription('The size of the search result (default: 25)') option
.setMinValue(1) .setName('limit')
.setMaxValue(100), .setDescription('The size of the search result (default: 25)')
), .setMinValue(1)
.setMaxValue(100),
),
async handler(interaction) { async handler(interaction) {
const tank = interaction.options.getString('tank')!; const tank = interaction.options.getString('tank')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const results = go(tank, await tankNames, { const results = go(tank, await tankNames, {
limit, limit,
keys: ['combined'], keys: ['combined'],
}).map((result) => result.obj.original); }).map((result) => result.obj.original);
return embedInfo( return embedInfo(
`Tank search for "${markdownEscape(tank)}"`, `Tank search for "${markdownEscape(tank)}"`,
results.length === 0 results.length === 0
? 'No tanks found.' ? 'No tanks found.'
: `\`\`\`\n${results.join('\n')}\n\`\`\``, : `\`\`\`\n${results.join('\n')}\n\`\`\``,
); );
}, },
}; });
});
+39 -41
View File
@@ -27,7 +27,7 @@ import resolvePlayerFromCommand, {
ResolvedPlayer, ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
@@ -57,50 +57,48 @@ async function render(
); );
} }
export const statsCommand = new Promise<CommandRegistryPromisable>( export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
async (resolve) => { const command = await addPeriodicFilterOptions(
const command = await addPeriodicFilterOptions( new SlashCommandBuilder()
new SlashCommandBuilder() .setName('stats')
.setName('stats') .setDescription('Regular battles statistics'),
.setDescription('Regular battles statistics'), (option) => option.addStringOption(addUsernameChoices),
(option) => option.addStringOption(addUsernameChoices), );
);
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command, command,
async handler(interaction) { async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction); const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction); const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction); const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, { const path = commandToURL(interaction, {
...player, ...player,
...getCustomPeriodParams(interaction), ...getCustomPeriodParams(interaction),
...filters, ...filters,
}); });
return Promise.all([ return Promise.all([
render(player, period, filters), render(player, period, filters),
buttonRefresh(interaction, path), buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id), getBlitzStarsLinkButton(player.region, player.id),
]); ]);
}, },
autocomplete: (interaction) => { autocomplete: (interaction) => {
autocompleteUsername(interaction); autocompleteUsername(interaction);
autocompleteTanks(interaction); autocompleteTanks(interaction);
}, },
async button(interaction) { async button(interaction) {
const player = await resolvePlayerFromButton(interaction); const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction); const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction); const filters = getFiltersFromButton(interaction);
return await render(player, period, filters); return await render(player, period, filters);
}, },
}); });
}, });
);
+44 -46
View File
@@ -14,58 +14,56 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown'; import { renderBreakdown } from './breakdown';
export const todayCommand = new Promise<CommandRegistryPromisable>( export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
async (resolve) => { const awaitedEncyclopediaInfo = await encyclopediaInfo;
const awaitedEncyclopediaInfo = await encyclopediaInfo; const command = addFilterOptions(
const command = addFilterOptions( new SlashCommandBuilder()
new SlashCommandBuilder() .setName('today')
.setName('today') .setDescription("Today's played tanks and statistics"),
.setDescription("Today's played tanks and statistics"), awaitedEncyclopediaInfo,
awaitedEncyclopediaInfo, ).addStringOption(addUsernameChoices);
).addStringOption(addUsernameChoices);
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command, command,
async handler(interaction) { async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction); const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand( const period = resolvePeriodFromCommand(
player.region, player.region,
interaction, interaction,
'today', 'today',
); );
const filters = await getFiltersFromCommand(interaction); const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, { const path = commandToURL(interaction, {
...player, ...player,
...getCustomPeriodParams(interaction, true), ...getCustomPeriodParams(interaction, true),
...filters, ...filters,
}); });
return [ return [
...(await renderBreakdown(player, period, filters)), ...(await renderBreakdown(player, period, filters)),
buttonPrimary(path, 'Refresh'), buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id), await getBlitzStarsLinkButton(player.region, player.id),
]; ];
}, },
autocomplete: (interaction) => { autocomplete: (interaction) => {
autocompleteUsername(interaction); autocompleteUsername(interaction);
autocompleteTanks(interaction); autocompleteTanks(interaction);
}, },
async button(interaction) { async button(interaction) {
const player = await resolvePlayerFromButton(interaction); const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction); const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction); const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters); return await renderBreakdown(player, period, filters);
}, },
}); });
}, });
);
+59 -21
View File
@@ -1,14 +1,27 @@
import { Locale, SlashCommandBuilder } from 'discord.js'; import {
Locale,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { translator } from '../localization/translator'; import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject'; import { localizationObject } from './localizationObject';
interface CreateLocalizedCommandSubcommand { interface CreateLocalizedCommandSubcommand {
subcommand: string; subcommand: string;
modify?: (option: SlashCommandSubcommandBuilder) => void;
}
interface CreateLocalizedCommandSubcommandGroup {
group: string;
modify?: (option: SlashCommandSubcommandGroupBuilder) => void;
} }
export function createLocalizedCommand( export function createLocalizedCommand(
command: string, command: string,
subcommands?: CreateLocalizedCommandSubcommand[], sub?:
| CreateLocalizedCommandSubcommand[]
| CreateLocalizedCommandSubcommandGroup[],
) { ) {
const { translate } = translator(Locale.EnglishUS); const { translate } = translator(Locale.EnglishUS);
const slashCommand = new SlashCommandBuilder() const slashCommand = new SlashCommandBuilder()
@@ -19,26 +32,51 @@ export function createLocalizedCommand(
localizationObject(`bot.commands.${command}.description`), localizationObject(`bot.commands.${command}.description`),
); );
if (subcommands) { if (sub) {
subcommands.map(({ subcommand }) => { sub.map((subItem) => {
const subcommandNameLocalizations = localizationObject( if ('subcommand' in subItem) {
`bot.commands.${command}.subcommands.${subcommand}`, const subcommandNameLocalizations = localizationObject(
); `bot.commands.${command}.subcommands.${subItem.subcommand}`,
const subcommandDescriptionLocalizations = localizationObject( );
`bot.commands.${command}.subcommands.${subcommand}.description`, const subcommandDescriptionLocalizations = localizationObject(
); `bot.commands.${command}.subcommands.${subItem.subcommand}.description`,
);
slashCommand.addSubcommand((option) => slashCommand.addSubcommand((option) => {
option if (subItem.modify) subItem.modify(option);
.setName(subcommand)
.setNameLocalizations(subcommandNameLocalizations) return option
.setDescription( .setName(subItem.subcommand)
translate( .setNameLocalizations(subcommandNameLocalizations)
`bot.commands.${command}.subcommands.${subcommand}.description`, .setDescription(
), translate(
) `bot.commands.${command}.subcommands.${subItem.subcommand}.description`,
.setDescriptionLocalizations(subcommandDescriptionLocalizations), ),
); )
.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);
});
}
}); });
} }
+1 -1
View File
@@ -30,7 +30,7 @@ export default function resolvePeriodFromButton(
start = getTimeDaysAgo(region, endDaysAgoMax); start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin); end = getTimeDaysAgo(region, startDaysAgoMin);
} else { } else {
name = getPeriodOptionName(periodSubcommand); name = getPeriodOptionName(periodSubcommand, interaction.locale);
start = getPeriodStart(region, periodSubcommand); start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow(); end = getPeriodNow();
} }
+9 -6
View File
@@ -1,14 +1,17 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction, Locale } from 'discord.js';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzkrieg/getPeriodNow'; import getPeriodNow from '../blitzkrieg/getPeriodNow';
import getPeriodStart from '../blitzkrieg/getPeriodStart'; import getPeriodStart from '../blitzkrieg/getPeriodStart';
import getTimeDaysAgo from '../blitzkrieg/getTimeDaysAgo'; import getTimeDaysAgo from '../blitzkrieg/getTimeDaysAgo';
import { translator } from '../localization/translator';
import { PeriodSize, PeriodType } from './addPeriodSubCommands'; import { PeriodSize, PeriodType } from './addPeriodSubCommands';
export function getPeriodOptionName(period: PeriodSize) { export function getPeriodOptionName(period: PeriodSize, locale: Locale) {
if (period === 'career') return 'Career'; const { translate } = translator(locale);
if (period === 'today') return 'Today';
return `${period} days`; 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 { export interface ResolvedPeriod {
@@ -39,7 +42,7 @@ export default function resolvePeriodFromCommand(
start = getTimeDaysAgo(region, endDaysAgoMax); start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin); end = getTimeDaysAgo(region, startDaysAgoMin);
} else { } else {
name = getPeriodOptionName(periodSubcommand); name = getPeriodOptionName(periodSubcommand, interaction.locale);
start = getPeriodStart(region, periodSubcommand); start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow(); end = getPeriodNow();
} }
+16 -3
View File
@@ -5,9 +5,10 @@ export function translator(localeRaw: Locale) {
const locale = locales.includes(localeRaw) ? localeRaw : Locale.EnglishUS; const locale = locales.includes(localeRaw) ? localeRaw : Locale.EnglishUS;
const strings = translations[locale]; const strings = translations[locale];
function translate(path: string): string { function translate(path: string, literals?: string[]): string {
const pathArray = path.split('.'); const pathArray = path.split('.');
let fragment = strings; let fragment = strings;
let resolvedString: string;
for (const pathItem of pathArray) { for (const pathItem of pathArray) {
if (typeof fragment === 'string') { if (typeof fragment === 'string') {
@@ -33,14 +34,26 @@ export function translator(localeRaw: Locale) {
} }
if (typeof fragment === 'string') { if (typeof fragment === 'string') {
return fragment; resolvedString = fragment;
} else if (fragment.$) { } else if (fragment.$) {
return fragment.$!; resolvedString = fragment.$!;
} else { } else {
throw new Error( throw new Error(
`Unresolved tree ending for "${path}" in locale "${locale}"`, `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[]) { function t(paths: TemplateStringsArray, ...embeds: string[]) {
+3 -8
View File
@@ -17,7 +17,6 @@ import discord from '../../../discord.json' assert { type: 'json' };
import { aboutCommand } from '../../commands/about'; import { aboutCommand } from '../../commands/about';
import { breakdownCommand } from '../../commands/breakdown'; import { breakdownCommand } from '../../commands/breakdown';
import { debugCommand } from '../../commands/debug'; import { debugCommand } from '../../commands/debug';
import { eligibleCommand } from '../../commands/eligible';
import { evolutionCommand } from '../../commands/evolution'; import { evolutionCommand } from '../../commands/evolution';
import { fullStatsCommand } from '../../commands/fullStats'; import { fullStatsCommand } from '../../commands/fullStats';
import { inactiveCommand } from '../../commands/inactive'; import { inactiveCommand } from '../../commands/inactive';
@@ -78,22 +77,18 @@ type CommandRegistryDefinitionDoesHandleInteraction = {
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable; ) => InteractionReturnable;
}; };
export type CommandRegistryPromisable = CommandRegistryBase & export type CommandRegistry = CommandRegistryBase &
(CommandRegistryDefinitionBase & (CommandRegistryDefinitionBase &
( (
| CommandRegistryDefinitionHandlesInteraction | CommandRegistryDefinitionHandlesInteraction
| CommandRegistryDefinitionDoesHandleInteraction | CommandRegistryDefinitionDoesHandleInteraction
)); ));
export type CommandRegistry =
| CommandRegistryPromisable
| Promise<CommandRegistryPromisable>;
const rest = new REST().setToken(secrets.DISCORD_TOKEN); const rest = new REST().setToken(secrets.DISCORD_TOKEN);
export const COMMANDS_RAW: CommandRegistry[] = [ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
permissionsCommand, permissionsCommand,
debugCommand, debugCommand,
eligibleCommand,
aboutCommand, aboutCommand,
inactiveCommand, inactiveCommand,
ownedTanksCommand, ownedTanksCommand,
@@ -113,7 +108,7 @@ export const COMMANDS_RAW: CommandRegistry[] = [
]; ];
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => { export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistryPromisable>>( return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry) => { (commands, registry) => {
if (registry.status === 'rejected') return commands; if (registry.status === 'rejected') return commands;
if (isDev()) registry.value.command.setDefaultMemberPermissions(0); if (isDev()) registry.value.command.setDefaultMemberPermissions(0);
+24
View File
@@ -51,6 +51,13 @@
} }
} }
} }
},
"periods": {
"career": "Career",
"today": "Today",
"days": "%s days",
"custom": "%s - %s days"
} }
}, },
@@ -112,6 +119,23 @@
}, },
"uptime": "Uptime" "uptime": "Uptime"
} }
},
"evolution": {
"$": "evolution",
"description": "Evolution of statistics",
"groups": {
"player": {
"$": "player",
"description": "Player statistics"
},
"tank": {
"$": "tank",
"description": "Tank statistics"
}
}
} }
} }
}, },