Compare commits

...
12 Commits
29 changed files with 645 additions and 528 deletions

No files matched your search

+1
View File
@@ -39,6 +39,7 @@
"ownedtanks",
"playerachievements",
"playerinfo",
"Promisable",
"ratingsleaderboard",
"ratingsplot",
"researchable",
+23
View File
@@ -0,0 +1,23 @@
# Blitzkrieg 1.15.10
The 10th patch for the `1.15` update is here, breaking the record of the most patches for a single update!
## Features
- Added `/today`
- This is a direct alias for `/breakdown today`
- Drastically simplified ratings command
- `/ratings current` and `/ratings archive` have been now merged
- Where the data is fetched from is now decided directly from the chosen season number
- Simplified many error messages
- Title bar description text is now centered when there is no image
- It always felt awkward when there was no image on the left but the left still aligned to the right
- Removed `/link`'s requirement to select a username explicitly from the search results
- Full names for regions are now used in the `/link` command
- So "NA" turns into "North America"
## Fixes
- Fixed contrast issues with WN8 percentile colors behind white text
- Archived ratings neighbors now functions once again
- Disabled tankopedia; this feature isn't finished yet and the changes were accidentally leaked
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkrieg",
"version": "1.15.9",
"version": "1.15.10",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
+7 -7
View File
@@ -13,7 +13,7 @@ import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../core/blitzstars/tankAverages';
import addFilterOptions from '../core/discord/addFilterOptions';
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -33,12 +33,12 @@ import resolvePlayerFromCommand, {
import calculateWN8 from '../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryRaw } from '../events/interactionCreate';
import { CommandRegistryPromisable } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 10;
async function render(
export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
@@ -290,9 +290,9 @@ async function render(
}
}
export const breakdownCommand = new Promise<CommandRegistryRaw>(
export const breakdownCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const command = await addFilterOptions(
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('breakdown')
.setDescription("A period's breakdown by tanks played"),
@@ -316,7 +316,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
});
return [
...(await render(player, period, filters)),
...(await renderBreakdown(player, period, filters)),
buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id),
];
@@ -332,7 +332,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters);
return await renderBreakdown(player, period, filters);
},
});
},
+1 -1
View File
@@ -13,7 +13,7 @@ export const debugCommand: CommandRegistry = {
.setName('debug')
.setDescription('Debug information about the bot'),
async handler(interaction) {
async handler() {
const currentTime = new Date().getTime();
const uptime = currentTime - executionStart;
const list = [
+2 -2
View File
@@ -29,7 +29,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryRaw } from '../events/interactionCreate';
import { CommandRegistryPromisable } from '../events/interactionCreate';
type EvolutionStatType = 'player' | 'tank';
@@ -107,7 +107,7 @@ async function render(
);
}
export const evolutionCommand = new Promise<CommandRegistryRaw>(
export const evolutionCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
let playerGroup: SlashCommandSubcommandGroupBuilder;
let tankGroup: SlashCommandSubcommandGroupBuilder;
+4 -4
View File
@@ -10,7 +10,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import addFilterOptions from '../core/discord/addFilterOptions';
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -29,7 +29,7 @@ import resolvePlayerFromCommand, {
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { getTierWeights } from '../core/statistics/getTierWeights';
import { CommandRegistryRaw } from '../events/interactionCreate';
import { CommandRegistryPromisable } from '../events/interactionCreate';
async function render(
{ region, id }: ResolvedPlayer,
@@ -64,9 +64,9 @@ async function render(
);
}
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const command = await addFilterOptions(
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('full-stats')
.setDescription('Full in-game statistics'),
+140 -154
View File
@@ -1,25 +1,21 @@
import { teal } from '@radix-ui/colors';
import {
AttachmentBuilder,
GuildMemberRoleManager,
SlashCommandBuilder,
} from 'discord.js';
import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' };
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
import Wrapper from '../components/Wrapper';
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
import { REGION_NAMES } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo';
import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export const verifyCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
@@ -30,193 +26,183 @@ export const verifyCommand: CommandRegistry = {
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
async handler(interaction) {
const username = interaction.options.getString('username', true);
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']);
if (serverAndIdPattern.test(username)) {
const [region, idString] = username.split('/');
const id = parseInt(idString);
const discordId = parseInt(interaction.user.id);
const accountInfo = await getAccountInfo(region as Region, id);
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [
'clan',
]);
await linkBlitzAndDiscord(discordId, region, id);
await linkBlitzAndDiscord(discordId, region as 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.",
);
}
await (interaction.member!.roles as GuildMemberRoleManager).remove(
discord.sklld_verify_role,
);
await (interaction.member!.roles as GuildMemberRoleManager).add(
discord.sklld_peasant_role,
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.",
);
}
return (
<Wrapper fat>
await (interaction.member!.roles as GuildMemberRoleManager).remove(
discord.sklld_verify_role,
);
await (interaction.member!.roles as GuildMemberRoleManager).add(
discord.sklld_peasant_role,
);
}
return [
<Wrapper fat>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
justifyContent: 'center',
gap: 16,
}}
>
<Glow color={teal.teal9} rotation={90} />
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
/>
<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
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<div
<img
src="https://i.imgur.com/jIcRgog.png"
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
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
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
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`}
/>
<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>
) : null}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<img
src="https://i.imgur.com/jIcRgog.png"
<span
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
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,
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{accountInfo.nickname}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] - `
: ''}
{REGION_NAMES_SHORT[region as Region]}
</span>
</div>
{accountInfo.nickname}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] - `
: ''}
{REGION_NAMES[region]}
</span>
</div>
</div>
<Glow color={teal.teal9} rotation={-90} />
</div>
</Wrapper>
);
} else {
return [
embedNegative(
'Please select from the options',
'Follow the procedure like the video above. The search results may take a while to show up.',
),
new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif'),
];
}
<Glow color={teal.teal9} rotation={-90} />
</div>
</Wrapper>,
embedInfo(
'Incorrect account?',
'Use the command again and select a username from the search results.',
),
];
},
autocomplete: autocompleteUsername,
+164 -192
View File
@@ -2,7 +2,6 @@ import {
APIApplicationCommandOptionChoice,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { range } from 'lodash';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
@@ -17,17 +16,17 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo';
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason';
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
import { UserError } from '../core/blitzkrieg/userError';
import addRegionChoices from '../core/discord/addRegionChoices';
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 { CommandRegistryRaw } from '../events/interactionCreate';
import { CommandRegistryPromisable } from '../events/interactionCreate';
export interface RatingsPlayer {
spa_id: number;
@@ -116,18 +115,22 @@ export interface RatingsNeighbors {
neighbors: RatingsPlayer[];
}
function addSubcommands(
option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option,
) {
return option
.addSubcommand((option) =>
addOptions(
option
.setName('neighbors')
.setDescription('Your position and neighbors'),
)
.addStringOption(addUsernameChoices)
const noOngoingSeason = embedNegative(
'No ongoing season',
"Wargaming didn't provide any data for this season.",
);
export const ratingsCommand = new Promise<CommandRegistryPromisable>(
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
.addIntegerOption((option) =>
option
.setName('limit')
@@ -135,77 +138,57 @@ function addSubcommands(
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
)
.addSubcommand((option) =>
addOptions(option.setName('league').setDescription('Top league players'))
)
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
value: `${value}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setName('season')
.setDescription('Season number')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
);
}
const noOngoingSeason = embedNegative(
'No ongoing season',
"Wargaming didn't provide any data for this season.",
);
export const ratingsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const seasonChoices = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + 1,
).map(
(number) =>
({
name: `Season ${number}`,
value: `${number}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
.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')
.addSubcommandGroup((option) =>
addSubcommands(
option.setName('archive').setDescription('Any previous season'),
(option) =>
option.addStringOption((option) =>
option
.setName('season')
.setDescription('The season number')
.setChoices(...seasonChoices)
.setRequired(true),
),
),
)
.addSubcommandGroup((option) =>
.addSubcommand((option) =>
addSubcommands(
option
.setName('current')
.setDescription('The current running Ratings season'),
.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),
),
),
);
@@ -224,18 +207,19 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
nickname: string;
}
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
| 'archive'
| 'current';
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) : undefined;
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string;
let titleImage: string | undefined;
let titleNameDiscriminator: string | undefined;
let titleDescription: string;
let playersBefore: number;
let playersAfter: number;
@@ -249,69 +233,66 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
interaction.options.getString('league')!,
);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (subcommandGroup === 'current'
regionRatingsInfo = await (isCurrentSeason
? getRatingsInfo(region)
: getArchivedRatingsInfo(region, season!));
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const result =
subcommandGroup === 'current'
? 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'],
);
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) =>
({
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,
);
},
);
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 =
subcommandGroup === 'current'
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
players = result;
playersBefore = result ? result[0].position - 1 : 0;
@@ -326,22 +307,17 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
titleDescription = `Top ${limit} players`;
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo =
subcommandGroup === 'current'
? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season!);
regionRatingsInfo = isCurrentSeason
? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season!);
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors =
subcommandGroup === 'current'
? await getRatingsNeighbors(
region,
id,
Math.round(limit / 2),
).then((players) =>
const neighbors = isCurrentSeason
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
players.neighbors.map(
(player) =>
({
@@ -352,52 +328,48 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.findIndex(
(player) => player.id === id,
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw new UserError(
`${accountInfo.nickname} didn't play ratings in season ${season}`,
'This player did not participate in this season or did not get past calibration.',
);
}
if (playerIndex === -1) {
throw new UserError(
`${accountInfo.nickname} didn't player ratings in season ${season}`,
{
cause:
'This 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'],
);
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[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}));
},
);
midnightLeaderboard =
subcommandGroup === 'current'
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
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
+42 -40
View File
@@ -9,7 +9,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import addFilterOptions from '../core/discord/addFilterOptions';
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -27,7 +27,7 @@ import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { CommandRegistryRaw } from '../events/interactionCreate';
import { CommandRegistryPromisable } from '../events/interactionCreate';
async function render(
{ region, id }: ResolvedPlayer,
@@ -57,48 +57,50 @@ async function render(
);
}
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
const command = await addFilterOptions(
new SlashCommandBuilder()
.setName('stats')
.setDescription('Regular battles statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
export const statsCommand = new Promise<CommandRegistryPromisable>(
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);
},
});
},
);
+71
View File
@@ -0,0 +1,71 @@
import { SlashCommandBuilder } from 'discord.js';
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import { addFilterOptions } from '../core/discord/addFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
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 { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
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 { renderBreakdown } from './breakdown';
export const todayCommand = new Promise<CommandRegistryPromisable>(
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,
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,
});
return [
...(await renderBreakdown(player, period, filters)),
buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(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);
},
});
},
);
@@ -37,7 +37,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
gap: -16,
}}
>
<Glow color={PERCENTILE_COLORS[percentile]} />
<Glow color={color} />
<div
style={{
@@ -71,7 +71,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
</span>
</div>
<Glow color={PERCENTILE_COLORS[percentile]} direction="reverse" />
<Glow color={color} direction="reverse" />
</div>
);
}
+3 -3
View File
@@ -1,4 +1,5 @@
import {
amber,
blue,
green,
lime,
@@ -8,7 +9,6 @@ import {
red,
slate,
teal,
yellow,
} from '@radix-ui/colors';
import { Percentile } from '../constants/percentiles';
@@ -20,8 +20,8 @@ export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9,
[Percentile.Bad]: red.red9,
[Percentile.BelowAverage]: orange.orange9,
[Percentile.Average]: yellow.yellow9,
[Percentile.AboveAverage]: lime.lime9,
[Percentile.Average]: amber.amber10,
[Percentile.AboveAverage]: lime.lime8,
[Percentile.Good]: green.green9,
[Percentile.VeryGood]: teal.teal9,
[Percentile.Great]: blue.blue9,
-1
View File
@@ -18,7 +18,6 @@ export const TANK_TYPES = [
'heavy',
] as const;
// TODO: remove hyphen for underscore
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
export type TankType = (typeof TANK_TYPES)[number];
+17 -3
View File
@@ -36,6 +36,8 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
@@ -51,9 +53,21 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
</span>
</div>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{description}
</span>
<div
style={{
display: 'flex',
justifyContent: image ? 'flex-start' : 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{description}
</span>
</div>
</div>
</div>
</div>
+1
View File
@@ -20,6 +20,7 @@ export const TOOLS: Tool[] = [
id: 'tankopedia',
title: 'Tankopedia',
description: 'Blitz tank encyclopedia',
disabled: true,
},
{
id: 'inactive',
+8 -1
View File
@@ -4,7 +4,11 @@ import { context } from '../blitzkrieg/context';
import { patientFetch } from '../blitzkrieg/patientFetch';
import regionToRegionSubdomain from './regionToRegionSubdomain';
const ratingsInfoCache: Partial<Record<Region, RatingsInfo>> = {};
export default async function getRatingsInfo(region: Region) {
if (ratingsInfoCache[region]) return ratingsInfoCache[region]!;
const response = await patientFetch(
context === 'website'
? `/api/${region}/ratings/current/info`
@@ -14,6 +18,9 @@ export default async function getRatingsInfo(region: Region) {
undefined,
{ cache: 'no-store' },
);
const data = (await response.json()) as RatingsInfo;
return (await response.json()) as Promise<RatingsInfo>;
ratingsInfoCache[region] = data;
return data;
}
+4 -4
View File
@@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) {
);
if (tankStats[id] === null) {
throw new UserError('No tank stats available', {
cause:
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
});
throw new UserError(
'No tank stats available',
"This player doesn't have any stats available. This may not be the player you are looking for.",
);
}
return tankStats[id];
+6
View File
@@ -0,0 +1,6 @@
import getRatingsInfo from './getRatingsInfo';
export async function isOnGoingRatingsSeason() {
const ratingsInfo = await getRatingsInfo('com');
return ratingsInfo.detail === undefined;
}
+8 -6
View File
@@ -15,9 +15,10 @@ export default async function resolveTankId(tank: string | number) {
});
if (searchResult.length === 0) {
throw new UserError('Tank not found', {
cause: `Could not find tank by the name "${tank}".`,
});
throw new UserError(
'Tank not found',
`Could not find tank by the name "${tank}".`,
);
} else {
return searchResult[0].obj.id;
}
@@ -25,9 +26,10 @@ export default async function resolveTankId(tank: string | number) {
if ((await tankDefinitions)[number]) {
return number;
} else {
throw new UserError('Tank not found', {
cause: `Could not find tank by the ID "${number}".`,
});
throw new UserError(
'Tank not found',
`Could not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`,
);
}
}
}
+6 -1
View File
@@ -1,4 +1,9 @@
/**
* Discriminator for user caused errors to not report in logs
*/
export class UserError extends Error {}
export class UserError {
constructor(
public message: string,
public cause: string,
) {}
}
+55 -71
View File
@@ -1,76 +1,60 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
import { encyclopediaInfo } from '../blitz/encyclopediaInfo';
import addPeriodSubCommands from './addPeriodSubCommands';
import { EncyclopediaInfo } from '../blitz/encyclopediaInfo';
import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices';
export default async function addFilterOptions<
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
>(
option: OptionType,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
): Promise<OptionType> {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
addPeriodSubCommands(option, (option) => {
extra(option);
return option
.addStringOption((option) =>
option
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(awaitedEncyclopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
)
.setRequired(false),
)
.addStringOption((option) => addTierChoices(option).setRequired(false))
.addStringOption((option) =>
option
.setName('tank-type')
.setDescription('Tank type')
.addChoices(
...Object.entries(awaitedEncyclopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
)
.addStringOption((option) =>
option
.setName('tree-type')
.setDescription('Tech tree type')
.addChoices(
{
name: TREE_TYPE_NAMES['researchable'],
value: 'researchable' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.premium,
value: 'premium' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.collector,
value: 'collector' satisfies TreeTypeString,
},
)
.setRequired(false),
)
.addStringOption((option) => addTankChoices(option).setRequired(false));
});
return option;
export function addFilterOptions<
Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder,
>(option: Option, encyclopediaInfo: EncyclopediaInfo) {
return option
.addStringOption((option) =>
option
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(encyclopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
)
.setRequired(false),
)
.addStringOption((option) => addTierChoices(option).setRequired(false))
.addStringOption((option) =>
option
.setName('tank-type')
.setDescription('Tank type')
.addChoices(
...Object.entries(encyclopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
)
.addStringOption((option) =>
option
.setName('tree-type')
.setDescription('Tech tree type')
.addChoices(
{
name: TREE_TYPE_NAMES['researchable'],
value: 'researchable' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.premium,
value: 'premium' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.collector,
value: 'collector' satisfies TreeTypeString,
},
)
.setRequired(false),
)
.addStringOption((option) =>
addTankChoices(option).setRequired(false),
) as Option;
}
@@ -0,0 +1,25 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { encyclopediaInfo } from '../blitz/encyclopediaInfo';
import { addFilterOptions } from './addFilterOptions';
import addPeriodSubCommands from './addPeriodSubCommands';
export default async function addPeriodicFilterOptions<
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
>(
option: OptionType,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
): Promise<OptionType> {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
addPeriodSubCommands(option, (option) => {
return addFilterOptions(extra(option), awaitedEncyclopediaInfo);
});
return option;
}
+6 -1
View File
@@ -3,12 +3,17 @@ import { PeriodType } from './addPeriodSubCommands';
export function getCustomPeriodParams(
interaction: ChatInputCommandInteraction,
forcedNonCustomPeriod = false,
) {
if ((interaction.options.getSubcommand() as PeriodType) === 'custom')
if (
!forcedNonCustomPeriod &&
(interaction.options.getSubcommand() as PeriodType) === 'custom'
) {
return {
start: interaction.options.getInteger('start', true),
end: interaction.options.getInteger('end', true),
};
}
return undefined;
}
+4 -3
View File
@@ -18,9 +18,10 @@ export default async function resolveClanFromCommand(
if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id };
} else {
throw new UserError('Could not find clan', {
cause: `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
});
throw new UserError(
'Could not find clan',
`\`${clan}\` was not found. Try selecting a clan from the search result.`,
);
}
}
}
+4 -1
View File
@@ -20,11 +20,14 @@ export interface ResolvedPeriod {
export default function resolvePeriodFromCommand(
region: Region,
interaction: ChatInputCommandInteraction<CacheType>,
forcedPeriod?: PeriodType,
) {
let name: string;
let start: number;
let end: number;
const periodSubcommand = interaction.options.getSubcommand() as PeriodType;
const periodSubcommand = forcedPeriod
? forcedPeriod
: (interaction.options.getSubcommand() as PeriodType);
if (periodSubcommand === 'custom') {
const startOption = interaction.options.getInteger('start', true);
+8 -6
View File
@@ -34,11 +34,12 @@ export default async function resolvePlayerFromCommand(
id: accounts[0].account_id,
} satisfies ResolvedPlayer;
} else {
throw new UserError('Could not find user', {
cause: `I couldn't find user "${markdownEscape(
throw new UserError(
'Could not find user',
`I couldn't find user "${markdownEscape(
commandUsername,
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`,
});
);
}
}
} else {
@@ -47,9 +48,10 @@ export default async function resolvePlayerFromCommand(
if (account) {
return { region: account.region, id: account.blitz };
} else {
throw new UserError('Use the `/link` command', {
cause: 'Link your Blitz and Discord accounts to get started.',
});
throw new UserError(
"You're account isn't linked with Blitzkrieg yet",
'Use the `/link` command to get started.',
);
}
}
}
@@ -19,7 +19,8 @@ import { psa } from '../../../core/discord/psa';
export default async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const registry = (await commands)[interaction.commandName];
const awaitedCommands = await commands;
const registry = awaitedCommands[interaction.commandName];
await interaction.deferReply();
+30 -23
View File
@@ -31,6 +31,7 @@ import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers';
import { searchTanksCommand } from '../../commands/searchTanks';
import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today';
import getClientId from '../../core/blitzkrieg/getClientId';
import isDev from '../../core/blitzkrieg/isDev';
import { secrets } from '../../core/blitzkrieg/secrets';
@@ -52,34 +53,39 @@ export type InteractionReturnable =
| InteractionIterableReturnable
| Promise<InteractionIterableReturnable>;
export interface Registry {
inProduction: boolean;
}
export type CommandRegistryRaw = Registry & {
inPublic: boolean;
inPreview?: boolean;
type CommandRegistryBase = {
command:
| SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
};
type CommandRegistryDefinitionBase = {
inProduction: boolean;
inPublic: boolean;
inPreview?: boolean;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
} & (
| {
handlesInteraction: true;
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
}
| {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable;
}
);
export type CommandRegistry = CommandRegistryRaw | Promise<CommandRegistryRaw>;
};
type CommandRegistryDefinitionHandlesInteraction = {
handlesInteraction: true;
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
};
type CommandRegistryDefinitionDoesHandleInteraction = {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable;
};
export type CommandRegistryPromisable = CommandRegistryBase &
(CommandRegistryDefinitionBase &
(
| CommandRegistryDefinitionHandlesInteraction
| CommandRegistryDefinitionDoesHandleInteraction
));
export type CommandRegistry =
| CommandRegistryPromisable
| Promise<CommandRegistryPromisable>;
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
@@ -101,10 +107,11 @@ export const COMMANDS_RAW: CommandRegistry[] = [
evolutionCommand,
statsCommand,
ratingsCommand,
todayCommand,
];
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
rawCommands.reduce<Record<string, CommandRegistryRaw>>(
rawCommands.reduce<Record<string, CommandRegistryPromisable>>(
(accumulator, registry) => {
if (isDev()) registry.command.setDefaultMemberPermissions(0);
@@ -123,7 +130,7 @@ export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
[];
commands.then((awaitedCommands) => {
Object.entries(awaitedCommands).forEach(([, registry]) => {
Object.values(awaitedCommands).forEach((registry) => {
const json = registry.command.toJSON();
if (registry.inPublic) {