Merge branch 'main' into dev

This commit is contained in:
TrèsAbhi committed 2024-02-23 16:05:29 -06:00
commit 23a33c7722
45 files changed
+743 -504

No files matched your search

+20 -3
View File
@@ -13,14 +13,21 @@ import { tankAverages } from '../../../core/blitzstars/tankAverages';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import { resetSession, useSession } from '../../../stores/session';
import mutateSession, {
resetSession,
useSession,
} from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
const REFRESH_RATE = 1 / 5;
export default function SessionPage() {
interface SessionPageProps {
naked?: boolean;
}
export default function SessionPage({ naked = true }: SessionPageProps) {
const session = useSession();
const since = session.isTracking ? new Date(session.time) : undefined;
const [diff, setDiff] = useState<
@@ -71,6 +78,14 @@ export default function SessionPage() {
0,
) / careerBattles;
useEffect(() => {
if (session.title === 'Untitled session') {
mutateSession((draft) => {
if (draft.isTracking) draft.title = draft.nickname;
});
}
}, [session.title]);
useEffect(() => {
async function recalculateDiff() {
if (session.isTracking) {
@@ -113,7 +128,7 @@ export default function SessionPage() {
list: (
Object.values(
deltaTankStats(session.tankStats, career),
deltaTankStats(session.tankStats, career, session.time / 1000),
) as IndividualTankStats[]
).map((stats) => {
return {
@@ -206,6 +221,7 @@ export default function SessionPage() {
since !== undefined &&
diff !== undefined && (
<Breakdown.Row
naked={naked}
color={session.color}
minimized={!session.showCareer}
title={session.title}
@@ -287,6 +303,7 @@ export default function SessionPage() {
return (
<Breakdown.Row
naked={naked}
color={session.color}
key={stats.tank_id}
minimized={!session.showCareer}
+1 -1
View File
@@ -248,7 +248,7 @@ export default function Page() {
</div>
</div>
<SessionPage />
<SessionPage naked={false} />
</PageWrapper>
);
}
-2
View File
@@ -6,7 +6,6 @@ import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
const DOCS = {
permissions: 'guide/permissions',
embeds: 'guide/embeds',
introduction: 'guide/introduction',
invite: 'guide/invite',
@@ -14,7 +13,6 @@ const DOCS = {
creators: 'guide/creators',
};
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
permissions: 'The permissions needed for the bot to function',
embeds: 'How embeds work',
introduction: 'About the bot',
invite: 'How to invite the bot',
+7 -7
View File
@@ -13,7 +13,7 @@ import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions';
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,
+42
View File
@@ -0,0 +1,42 @@
import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate';
export const permissionsCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('permissions')
.setDescription('Checks wether the bot has the required permissions'),
async handler(interaction) {
// friday the 13th permissions easter egg
const isFridayThe13th =
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has('ViewChannel'),
'View channels: needed for the refresh button to work',
],
[
interaction.appPermissions?.has('ReadMessageHistory'),
'Read message history: also needed for the refresh button to work',
],
[
interaction.appPermissions?.has('AttachFiles'),
'Attach files: needed for image-based commands to work',
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
} else {
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
}
})
.join('\n');
return `# Permissions\n${body}`;
},
};
+164 -192
View File
@@ -2,9 +2,9 @@ import {
APIApplicationCommandOptionChoice,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { range } from 'lodash';
import markdownEscape from 'markdown-escape';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
@@ -17,16 +17,16 @@ 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 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';
import { UserError } from '../hooks/userError';
export interface RatingsPlayer {
@@ -116,18 +116,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 +139,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 +208,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 +234,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[player.id]?.account_name ??
`Unknown Player ${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 +308,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 +329,47 @@ 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(
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis 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>
);
}
+11 -3
View File
@@ -18,6 +18,7 @@ export interface RowStatItem {
interface RowProps {
type?: 'tank' | 'summary';
naked?: boolean;
title: string;
minimized?: boolean;
treeType?: TreeType;
@@ -33,18 +34,23 @@ export function Row({
minimized,
tankType,
treeType,
naked = false,
type,
}: RowProps) {
const theme = createColors(PALETTES[`${color}Dark`]);
const palette = PALETTES[`${color}Dark`];
const theme = createColors(palette);
return (
<div
className="session-tracker-row"
style={{
maxWidth: naked ? 600 : undefined,
display: 'flex',
flexDirection: 'column',
borderRadius: 4,
backgroundColor: theme.appBackground2,
backgroundColor: naked
? `${theme.appBackground2}c0`
: theme.appBackground2,
overflow: 'hidden',
}}
>
@@ -55,7 +61,9 @@ export function Row({
justifyContent: 'center',
padding: 8,
gap: 4,
backgroundColor: theme.componentInteractive,
backgroundColor: naked
? `${theme.componentInteractive}c0`
: theme.componentInteractive,
}}
>
{type === 'tank' && tankType !== undefined && (
+2 -2
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,7 +20,7 @@ export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9,
[Percentile.Bad]: red.red9,
[Percentile.BelowAverage]: orange.orange9,
[Percentile.Average]: yellow.yellow9,
[Percentile.Average]: amber.amber10,
[Percentile.AboveAverage]: lime.lime8,
[Percentile.Good]: green.green9,
[Percentile.VeryGood]: teal.teal9,
-1
View File
@@ -12,7 +12,6 @@ export const TANK_TYPES = [
'tankDestroyer',
] 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
@@ -16,6 +16,7 @@ export const TOOLS: Tool[] = [
id: 'tankopedia',
title: 'Tankopedia',
description: 'Blitz tank encyclopedia',
disabled: true,
},
{
id: 'ratings',
+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;
}
+3 -4
View File
@@ -10,10 +10,9 @@ 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\nThis 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;
}
+7 -6
View File
@@ -1,5 +1,6 @@
import { go } from 'fuzzysort';
import { UserError } from '../../hooks/userError';
import markdownEscape from 'markdown-escape';
import {
tankDefinitions,
tanksDefinitionsArray,
@@ -15,9 +16,9 @@ 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\nCould not find tank by the name "${markdownEscape(`${tank}`)}".`,
);
} else {
return searchResult[0].obj.id;
}
@@ -25,9 +26,9 @@ 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\nCould not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`,
);
}
}
}
@@ -1,17 +1,28 @@
import getRatingsInfo from '../blitz/getRatingsInfo';
interface GitHubTrees {
tree: { path: string; url: string }[];
}
let cachedLatestArchivedSeasonNumber: number | null = null;
export async function getArchivedLatestSeasonNumber() {
if (cachedLatestArchivedSeasonNumber !== null) {
return cachedLatestArchivedSeasonNumber;
}
const ratingsInfo = await getRatingsInfo('com');
if (ratingsInfo.detail) {
const comURL = (
const regionsURL = (
(await fetch(
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main/regions',
).then((response) => response.json())) as {
tree: { path: string; url: string }[];
}
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main',
).then((response) => response.json())) as GitHubTrees
).tree.find(({ path }) => path === 'regions')!.url;
const comURL = (
(await fetch(regionsURL).then((response) =>
response.json(),
)) as GitHubTrees
).tree.find(({ path }) => path === 'com')!.url;
const ratingsURL = (
(await fetch(comURL).then((response) => response.json())) as {
@@ -20,14 +31,16 @@ export async function getArchivedLatestSeasonNumber() {
).tree[0].url;
const latestSeason = Math.max(
...(
(await fetch(ratingsURL).then((response) => {
response.json();
})) as { tree: { path: string }[] }
).tree.map(({ path }) => parseInt(path)),
(await fetch(ratingsURL).then((response) => response.json())) as {
tree: { path: string }[];
}
).tree.map(({ path }) => Number(path)),
);
return latestSeason;
cachedLatestArchivedSeasonNumber = latestSeason;
} else {
return ratingsInfo.current_season - 1;
cachedLatestArchivedSeasonNumber = ratingsInfo.current_season - 1;
}
return cachedLatestArchivedSeasonNumber;
}
@@ -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
@@ -1,4 +1,5 @@
import { ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { Region } from '../../constants/regions';
import { UserError } from '../../hooks/userError';
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
@@ -18,9 +19,9 @@ 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\nCouldn't find "${markdownEscape(clan)}". 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);
+6 -6
View File
@@ -34,11 +34,11 @@ 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\nI 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 +47,9 @@ 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(
"# I don't know who you are\nUse the `/link` command to get started.",
);
}
}
}
+13 -3
View File
@@ -4,12 +4,19 @@ import {
emptyIndividualTankStats,
} from '../../types/tanksStats';
export function deltaTankStats(a: NormalizedTankStats, b: NormalizedTankStats) {
export function deltaTankStats(
a: NormalizedTankStats,
b: NormalizedTankStats,
time?: number,
) {
const bTanks = Object.keys(b);
const changedTanks = bTanks.filter((id) => {
return (
a[id as unknown as number]?.all.battles !==
b[id as unknown as number].all.battles
b[id as unknown as number].all.battles &&
(time === undefined
? true
: b[id as unknown as number].last_battle_time > time)
);
});
const diff = changedTanks.map((id) => {
@@ -24,7 +31,10 @@ export function deltaTankStats(a: NormalizedTankStats, b: NormalizedTankStats) {
in_garage: bTank.in_garage,
max_xp: Math.max(aTank.max_xp, bTank.max_xp),
in_garage_updated: bTank.in_garage_updated,
last_battle_time: bTank.last_battle_time - aTank.last_battle_time,
last_battle_time: Math.max(
bTank.last_battle_time,
aTank.last_battle_time,
),
mark_of_mastery: bTank.mark_of_mastery,
tank_id: bTank.tank_id,
@@ -10,7 +10,6 @@ import {
import { commands } from '..';
import discord from '../../../../discord.json' assert { type: 'json' };
import buttonLink from '../../../core/discord/buttonLink';
import embedNegative from '../../../core/discord/embedNegative';
import embedWarning from '../../../core/discord/embedWarning';
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
import { psa } from '../../../core/discord/psa';
@@ -19,7 +18,8 @@ import { UserError } from '../../../hooks/userError';
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();
@@ -118,25 +118,15 @@ export default async function handleChatInputCommand(
if (error instanceof UserError) {
interaction.editReply({
embeds: [
embedNegative(
error.message,
(error.cause as string | undefined) ??
'No further information is available.',
),
],
content: error.message,
components,
});
} else {
console.error(interaction.commandName, error);
interaction.editReply({
embeds: [
embedNegative(
'Blitzkrieg ran into an error!',
"We're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
),
],
content:
"# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
components,
});
}
+32 -23
View File
@@ -23,6 +23,7 @@ import { fullStatsCommand } from '../../commands/fullStats';
import { inactiveCommand } from '../../commands/inactive';
import { verifyCommand } from '../../commands/link';
import { ownedTanksCommand } from '../../commands/ownedTanks';
import { permissionsCommand } from '../../commands/permissions';
import { pingCommand } from '../../commands/ping';
import { playerAchievementsCommand } from '../../commands/playerAchievements';
import { playerInfoCommand } from '../../commands/playerInfo';
@@ -31,6 +32,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,38 +54,44 @@ 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);
export const COMMANDS_RAW: CommandRegistry[] = [
permissionsCommand,
debugCommand,
eligibleCommand,
aboutCommand,
@@ -101,10 +109,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 +132,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) {
+3 -1
View File
@@ -1,4 +1,6 @@
/**
* Discriminator for user caused errors to not report in logs
*/
export class UserError extends Error {}
export class UserError {
constructor(public message: string) {}
}