mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0644c9a67f | ||
|
|
c91ab29a23 | ||
|
|
8f4e33fd5a | ||
|
|
8cf5012602 | ||
|
|
23a23e36bb | ||
|
|
0480445bae | ||
|
|
ae07b8de4c | ||
|
|
4c746cbb86 | ||
|
|
890a4d142b | ||
|
|
ff9bda29cf | ||
|
|
5ca9e29f3d | ||
|
|
c804a3d564 |
No files matched your search
Vendored
+1
@@ -39,6 +39,7 @@
|
|||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
"playerinfo",
|
"playerinfo",
|
||||||
|
"Promisable",
|
||||||
"ratingsleaderboard",
|
"ratingsleaderboard",
|
||||||
"ratingsplot",
|
"ratingsplot",
|
||||||
"researchable",
|
"researchable",
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
|||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
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 addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
@@ -33,12 +33,12 @@ import resolvePlayerFromCommand, {
|
|||||||
import calculateWN8 from '../core/statistics/calculateWN8';
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
const ROWS_PER_PAGE = 8;
|
const ROWS_PER_PAGE = 8;
|
||||||
const MAX_PAGES = 10;
|
const MAX_PAGES = 10;
|
||||||
|
|
||||||
async function render(
|
export async function renderBreakdown(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
{ start, end, name }: ResolvedPeriod,
|
{ start, end, name }: ResolvedPeriod,
|
||||||
filters: StatFilters,
|
filters: StatFilters,
|
||||||
@@ -290,9 +290,9 @@ async function render(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
export const breakdownCommand = new Promise<CommandRegistryPromisable>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addFilterOptions(
|
const command = await addPeriodicFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('breakdown')
|
.setName('breakdown')
|
||||||
.setDescription("A period's breakdown by tanks played"),
|
.setDescription("A period's breakdown by tanks played"),
|
||||||
@@ -316,7 +316,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(await render(player, period, filters)),
|
...(await renderBreakdown(player, period, filters)),
|
||||||
buttonPrimary(path, 'Refresh'),
|
buttonPrimary(path, 'Refresh'),
|
||||||
await getBlitzStarsLinkButton(player.region, player.id),
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
];
|
];
|
||||||
@@ -332,7 +332,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
|||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
const filters = getFiltersFromButton(interaction);
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await render(player, period, filters);
|
return await renderBreakdown(player, period, filters);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const debugCommand: CommandRegistry = {
|
|||||||
.setName('debug')
|
.setName('debug')
|
||||||
.setDescription('Debug information about the bot'),
|
.setDescription('Debug information about the bot'),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler() {
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
const uptime = currentTime - executionStart;
|
const uptime = currentTime - executionStart;
|
||||||
const list = [
|
const list = [
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
|||||||
import resolvePlayerFromCommand, {
|
import resolvePlayerFromCommand, {
|
||||||
ResolvedPlayer,
|
ResolvedPlayer,
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
type EvolutionStatType = 'player' | 'tank';
|
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) => {
|
async (resolve) => {
|
||||||
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
|||||||
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
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 addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
@@ -29,7 +29,7 @@ import resolvePlayerFromCommand, {
|
|||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { getTierWeights } from '../core/statistics/getTierWeights';
|
import { getTierWeights } from '../core/statistics/getTierWeights';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
async function render(
|
async function render(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
@@ -64,9 +64,9 @@ async function render(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addFilterOptions(
|
const command = await addPeriodicFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('full-stats')
|
.setName('full-stats')
|
||||||
.setDescription('Full in-game statistics'),
|
.setDescription('Full in-game statistics'),
|
||||||
|
|||||||
+15
-29
@@ -1,25 +1,21 @@
|
|||||||
import { teal } from '@radix-ui/colors';
|
import { teal } from '@radix-ui/colors';
|
||||||
import {
|
import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js';
|
||||||
AttachmentBuilder,
|
|
||||||
GuildMemberRoleManager,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import discord from '../../discord.json' assert { type: 'json' };
|
import discord from '../../discord.json' assert { type: 'json' };
|
||||||
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
||||||
import Wrapper from '../components/Wrapper';
|
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 { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
|
||||||
|
|
||||||
export const verifyCommand: CommandRegistry = {
|
export const verifyCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
@@ -30,18 +26,12 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const username = interaction.options.getString('username', true);
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
|
||||||
if (serverAndIdPattern.test(username)) {
|
|
||||||
const [region, idString] = username.split('/');
|
|
||||||
const id = parseInt(idString);
|
|
||||||
const discordId = parseInt(interaction.user.id);
|
const discordId = parseInt(interaction.user.id);
|
||||||
const accountInfo = await getAccountInfo(region as Region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [
|
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
|
||||||
'clan',
|
|
||||||
]);
|
|
||||||
|
|
||||||
await linkBlitzAndDiscord(discordId, region as Region, id);
|
await linkBlitzAndDiscord(discordId, region, id);
|
||||||
|
|
||||||
if (interaction.guildId === discord.sklld_guild_id) {
|
if (interaction.guildId === discord.sklld_guild_id) {
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
||||||
@@ -59,7 +49,7 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return [
|
||||||
<Wrapper fat>
|
<Wrapper fat>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -198,7 +188,7 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
{clanAccountInfo?.clan
|
{clanAccountInfo?.clan
|
||||||
? `[${clanAccountInfo?.clan?.tag}] - `
|
? `[${clanAccountInfo?.clan?.tag}] - `
|
||||||
: ''}
|
: ''}
|
||||||
{REGION_NAMES_SHORT[region as Region]}
|
{REGION_NAMES[region]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,17 +196,13 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
|
|
||||||
<Glow color={teal.teal9} rotation={-90} />
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
</Wrapper>,
|
||||||
);
|
|
||||||
} else {
|
embedInfo(
|
||||||
return [
|
'Incorrect account?',
|
||||||
embedNegative(
|
'Use the command again and select a username from the search results.',
|
||||||
'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'),
|
|
||||||
];
|
];
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
|
|||||||
+70
-98
@@ -2,7 +2,6 @@ import {
|
|||||||
APIApplicationCommandOptionChoice,
|
APIApplicationCommandOptionChoice,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
SlashCommandSubcommandBuilder,
|
SlashCommandSubcommandBuilder,
|
||||||
SlashCommandSubcommandGroupBuilder,
|
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash';
|
||||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||||
@@ -17,17 +16,17 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
|||||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||||
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
|
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
|
||||||
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
|
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
|
||||||
|
import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason';
|
||||||
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
||||||
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
||||||
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
||||||
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
|
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
|
||||||
import { UserError } from '../core/blitzkrieg/userError';
|
import { UserError } from '../core/blitzkrieg/userError';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
export interface RatingsPlayer {
|
export interface RatingsPlayer {
|
||||||
spa_id: number;
|
spa_id: number;
|
||||||
@@ -116,18 +115,22 @@ export interface RatingsNeighbors {
|
|||||||
neighbors: RatingsPlayer[];
|
neighbors: RatingsPlayer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSubcommands(
|
const noOngoingSeason = embedNegative(
|
||||||
option: SlashCommandSubcommandGroupBuilder,
|
'No ongoing season',
|
||||||
addOptions = (option: SlashCommandSubcommandBuilder) => option,
|
"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
|
return option
|
||||||
.addSubcommand((option) =>
|
|
||||||
addOptions(
|
|
||||||
option
|
|
||||||
.setName('neighbors')
|
|
||||||
.setDescription('Your position and neighbors'),
|
|
||||||
)
|
|
||||||
.addStringOption(addUsernameChoices)
|
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('limit')
|
.setName('limit')
|
||||||
@@ -135,10 +138,42 @@ function addSubcommands(
|
|||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
.setMinValue(5)
|
.setMinValue(5)
|
||||||
.setMaxValue(30),
|
.setMaxValue(30),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('season')
|
||||||
|
.setDescription('Season number')
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
...seasonNumbers
|
||||||
|
.map(
|
||||||
|
(number, index) =>
|
||||||
|
({
|
||||||
|
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
|
||||||
|
value: `${number}`,
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
)
|
||||||
|
.reverse(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = new SlashCommandBuilder()
|
||||||
|
.setName('ratings')
|
||||||
|
.setDescription('Ratings battles statistics')
|
||||||
|
.addSubcommand((option) =>
|
||||||
|
addSubcommands(
|
||||||
|
option
|
||||||
|
.setName('neighbors')
|
||||||
|
.setDescription('Shows neighboring players on the leaderboard'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.addSubcommand((option) =>
|
.addSubcommand((option) =>
|
||||||
addOptions(option.setName('league').setDescription('Top league players'))
|
addSubcommands(
|
||||||
|
option
|
||||||
|
.setName('league')
|
||||||
|
.setDescription('Shows top players from a league')
|
||||||
|
.addStringOption(addRegionChoices)
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('league')
|
.setName('league')
|
||||||
@@ -153,59 +188,7 @@ function addSubcommands(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.setRequired(true),
|
.setRequired(true),
|
||||||
)
|
|
||||||
.addStringOption(addRegionChoices)
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('limit')
|
|
||||||
.setDescription('How many rows to display (default: 10)')
|
|
||||||
.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>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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) =>
|
|
||||||
addSubcommands(
|
|
||||||
option
|
|
||||||
.setName('current')
|
|
||||||
.setDescription('The current running Ratings season'),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -224,18 +207,19 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
nickname: string;
|
nickname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
|
|
||||||
| 'archive'
|
|
||||||
| 'current';
|
|
||||||
const subcommand = interaction.options.getSubcommand(true) as
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
| 'league'
|
| 'league'
|
||||||
| 'neighbors';
|
| 'neighbors';
|
||||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||||
const seasonOption = interaction.options.getString('season');
|
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 titleName: string;
|
||||||
let titleImage: string | undefined;
|
let titleImage: string | undefined;
|
||||||
let titleNameDiscriminator: string | undefined;
|
|
||||||
let titleDescription: string;
|
let titleDescription: string;
|
||||||
let playersBefore: number;
|
let playersBefore: number;
|
||||||
let playersAfter: number;
|
let playersAfter: number;
|
||||||
@@ -249,16 +233,14 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
interaction.options.getString('league')!,
|
interaction.options.getString('league')!,
|
||||||
);
|
);
|
||||||
const region = interaction.options.getString('region') as Region;
|
const region = interaction.options.getString('region') as Region;
|
||||||
regionRatingsInfo = await (subcommandGroup === 'current'
|
regionRatingsInfo = await (isCurrentSeason
|
||||||
? getRatingsInfo(region)
|
? getRatingsInfo(region)
|
||||||
: getArchivedRatingsInfo(region, season!));
|
: getArchivedRatingsInfo(region, season!));
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
|
|
||||||
const result =
|
const result = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
|
||||||
? await getRatingsLeague(region, leagueIndex).then(
|
|
||||||
async (result) =>
|
|
||||||
result.slice(0, limit).map(
|
result.slice(0, limit).map(
|
||||||
(player) =>
|
(player) =>
|
||||||
({
|
({
|
||||||
@@ -305,8 +287,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||||
midnightLeaderboard =
|
midnightLeaderboard = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
? await getArchivedRatingsMidnightLeaderboard(
|
||||||
region,
|
region,
|
||||||
regionRatingsInfo.current_season,
|
regionRatingsInfo.current_season,
|
||||||
@@ -326,8 +307,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
titleDescription = `Top ${limit} players`;
|
titleDescription = `Top ${limit} players`;
|
||||||
} else {
|
} else {
|
||||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
regionRatingsInfo =
|
regionRatingsInfo = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
|
||||||
? await getRatingsInfo(region)
|
? await getRatingsInfo(region)
|
||||||
: await getArchivedRatingsInfo(region, season!);
|
: await getArchivedRatingsInfo(region, season!);
|
||||||
|
|
||||||
@@ -335,13 +315,9 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
|
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
const neighbors =
|
const neighbors = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
|
||||||
? await getRatingsNeighbors(
|
(players) =>
|
||||||
region,
|
|
||||||
id,
|
|
||||||
Math.round(limit / 2),
|
|
||||||
).then((players) =>
|
|
||||||
players.neighbors.map(
|
players.neighbors.map(
|
||||||
(player) =>
|
(player) =>
|
||||||
({
|
({
|
||||||
@@ -361,11 +337,8 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
|
|
||||||
if (playerIndex === -1) {
|
if (playerIndex === -1) {
|
||||||
throw new UserError(
|
throw new UserError(
|
||||||
`${accountInfo.nickname} didn't player ratings in season ${season}`,
|
`${accountInfo.nickname} didn't play ratings in season ${season}`,
|
||||||
{
|
|
||||||
cause:
|
|
||||||
'This player did not participate in this season or did not get past calibration.',
|
'This player did not participate in this season or did not get past calibration.',
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,21 +357,20 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
id: player.id,
|
id: player.id,
|
||||||
score: player.score,
|
score: player.score,
|
||||||
position: playerIndex + index + 1,
|
position: playerIndex + index + 1,
|
||||||
clan: clanData[player.id]!.clan?.tag,
|
clan: clanData[index]?.clan?.tag,
|
||||||
nickname: clanData[player.id]!.account_name,
|
nickname: clanData[index]
|
||||||
|
? clanData[index]!.account_name
|
||||||
|
: `Deleted ${player.id}`,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
midnightLeaderboard =
|
midnightLeaderboard = isCurrentSeason
|
||||||
subcommandGroup === 'current'
|
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
? await getArchivedRatingsMidnightLeaderboard(
|
||||||
region,
|
region,
|
||||||
regionRatingsInfo.current_season,
|
regionRatingsInfo.current_season,
|
||||||
)
|
)
|
||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
|
||||||
|
|
||||||
players = neighbors;
|
players = neighbors;
|
||||||
titleImage = clan
|
titleImage = clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
|||||||
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
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 addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
@@ -27,7 +27,7 @@ import resolvePlayerFromCommand, {
|
|||||||
ResolvedPlayer,
|
ResolvedPlayer,
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
||||||
|
|
||||||
async function render(
|
async function render(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
@@ -57,8 +57,9 @@ async function render(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
export const statsCommand = new Promise<CommandRegistryPromisable>(
|
||||||
const command = await addFilterOptions(
|
async (resolve) => {
|
||||||
|
const command = await addPeriodicFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('stats')
|
.setName('stats')
|
||||||
.setDescription('Regular battles statistics'),
|
.setDescription('Regular battles statistics'),
|
||||||
@@ -101,4 +102,5 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
|||||||
return await render(player, period, filters);
|
return await render(player, period, filters);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
@@ -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,
|
gap: -16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Glow color={PERCENTILE_COLORS[percentile]} />
|
<Glow color={color} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -71,7 +71,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Glow color={PERCENTILE_COLORS[percentile]} direction="reverse" />
|
<Glow color={color} direction="reverse" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
amber,
|
||||||
blue,
|
blue,
|
||||||
green,
|
green,
|
||||||
lime,
|
lime,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
red,
|
red,
|
||||||
slate,
|
slate,
|
||||||
teal,
|
teal,
|
||||||
yellow,
|
|
||||||
} from '@radix-ui/colors';
|
} from '@radix-ui/colors';
|
||||||
import { Percentile } from '../constants/percentiles';
|
import { Percentile } from '../constants/percentiles';
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ export const PERCENTILE_COLORS = {
|
|||||||
[Percentile.VeryBad]: slate.slate9,
|
[Percentile.VeryBad]: slate.slate9,
|
||||||
[Percentile.Bad]: red.red9,
|
[Percentile.Bad]: red.red9,
|
||||||
[Percentile.BelowAverage]: orange.orange9,
|
[Percentile.BelowAverage]: orange.orange9,
|
||||||
[Percentile.Average]: yellow.yellow9,
|
[Percentile.Average]: amber.amber10,
|
||||||
[Percentile.AboveAverage]: lime.lime9,
|
[Percentile.AboveAverage]: lime.lime8,
|
||||||
[Percentile.Good]: green.green9,
|
[Percentile.Good]: green.green9,
|
||||||
[Percentile.VeryGood]: teal.teal9,
|
[Percentile.VeryGood]: teal.teal9,
|
||||||
[Percentile.Great]: blue.blue9,
|
[Percentile.Great]: blue.blue9,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const TANK_TYPES = [
|
|||||||
'heavy',
|
'heavy',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// TODO: remove hyphen for underscore
|
|
||||||
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
|
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
|
||||||
|
|
||||||
export type TankType = (typeof TANK_TYPES)[number];
|
export type TankType = (typeof TANK_TYPES)[number];
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -51,11 +53,23 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: image ? 'flex-start' : 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ export const TOOLS: Tool[] = [
|
|||||||
id: 'tankopedia',
|
id: 'tankopedia',
|
||||||
title: 'Tankopedia',
|
title: 'Tankopedia',
|
||||||
description: 'Blitz tank encyclopedia',
|
description: 'Blitz tank encyclopedia',
|
||||||
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'inactive',
|
id: 'inactive',
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { context } from '../blitzkrieg/context';
|
|||||||
import { patientFetch } from '../blitzkrieg/patientFetch';
|
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||||
|
|
||||||
|
const ratingsInfoCache: Partial<Record<Region, RatingsInfo>> = {};
|
||||||
|
|
||||||
export default async function getRatingsInfo(region: Region) {
|
export default async function getRatingsInfo(region: Region) {
|
||||||
|
if (ratingsInfoCache[region]) return ratingsInfoCache[region]!;
|
||||||
|
|
||||||
const response = await patientFetch(
|
const response = await patientFetch(
|
||||||
context === 'website'
|
context === 'website'
|
||||||
? `/api/${region}/ratings/current/info`
|
? `/api/${region}/ratings/current/info`
|
||||||
@@ -14,6 +18,9 @@ export default async function getRatingsInfo(region: Region) {
|
|||||||
undefined,
|
undefined,
|
||||||
{ cache: 'no-store' },
|
{ cache: 'no-store' },
|
||||||
);
|
);
|
||||||
|
const data = (await response.json()) as RatingsInfo;
|
||||||
|
|
||||||
return (await response.json()) as Promise<RatingsInfo>;
|
ratingsInfoCache[region] = data;
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (tankStats[id] === null) {
|
if (tankStats[id] === null) {
|
||||||
throw new UserError('No tank stats available', {
|
throw new UserError(
|
||||||
cause:
|
'No tank stats available',
|
||||||
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
|
"This player doesn't have any stats available. This may not be the player you are looking for.",
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankStats[id];
|
return tankStats[id];
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import getRatingsInfo from './getRatingsInfo';
|
||||||
|
|
||||||
|
export async function isOnGoingRatingsSeason() {
|
||||||
|
const ratingsInfo = await getRatingsInfo('com');
|
||||||
|
return ratingsInfo.detail === undefined;
|
||||||
|
}
|
||||||
@@ -15,9 +15,10 @@ export default async function resolveTankId(tank: string | number) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (searchResult.length === 0) {
|
if (searchResult.length === 0) {
|
||||||
throw new UserError('Tank not found', {
|
throw new UserError(
|
||||||
cause: `Could not find tank by the name "${tank}".`,
|
'Tank not found',
|
||||||
});
|
`Could not find tank by the name "${tank}".`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return searchResult[0].obj.id;
|
return searchResult[0].obj.id;
|
||||||
}
|
}
|
||||||
@@ -25,9 +26,10 @@ export default async function resolveTankId(tank: string | number) {
|
|||||||
if ((await tankDefinitions)[number]) {
|
if ((await tankDefinitions)[number]) {
|
||||||
return number;
|
return number;
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Tank not found', {
|
throw new UserError(
|
||||||
cause: `Could not find tank by the ID "${number}".`,
|
'Tank not found',
|
||||||
});
|
`Could not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Discriminator for user caused errors to not report in logs
|
* 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,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -1,34 +1,19 @@
|
|||||||
import {
|
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandSubcommandBuilder,
|
|
||||||
SlashCommandSubcommandGroupBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
|
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
|
||||||
import { encyclopediaInfo } from '../blitz/encyclopediaInfo';
|
import { EncyclopediaInfo } from '../blitz/encyclopediaInfo';
|
||||||
import addPeriodSubCommands from './addPeriodSubCommands';
|
|
||||||
import addTankChoices from './addTankChoices';
|
import addTankChoices from './addTankChoices';
|
||||||
import addTierChoices from './addTierChoices';
|
import addTierChoices from './addTierChoices';
|
||||||
|
|
||||||
export default async function addFilterOptions<
|
export function addFilterOptions<
|
||||||
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
|
Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder,
|
||||||
>(
|
>(option: Option, encyclopediaInfo: EncyclopediaInfo) {
|
||||||
option: OptionType,
|
|
||||||
extra: (
|
|
||||||
option: SlashCommandSubcommandBuilder,
|
|
||||||
) => SlashCommandSubcommandBuilder = (option) => option,
|
|
||||||
): Promise<OptionType> {
|
|
||||||
const awaitedEncyclopediaInfo = await encyclopediaInfo;
|
|
||||||
|
|
||||||
addPeriodSubCommands(option, (option) => {
|
|
||||||
extra(option);
|
|
||||||
|
|
||||||
return option
|
return option
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('nation')
|
.setName('nation')
|
||||||
.setDescription('Nation')
|
.setDescription('Nation')
|
||||||
.addChoices(
|
.addChoices(
|
||||||
...Object.entries(awaitedEncyclopediaInfo.vehicle_nations).map(
|
...Object.entries(encyclopediaInfo.vehicle_nations).map(
|
||||||
([type, name]) => ({ name, value: type }),
|
([type, name]) => ({ name, value: type }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -40,7 +25,7 @@ export default async function addFilterOptions<
|
|||||||
.setName('tank-type')
|
.setName('tank-type')
|
||||||
.setDescription('Tank type')
|
.setDescription('Tank type')
|
||||||
.addChoices(
|
.addChoices(
|
||||||
...Object.entries(awaitedEncyclopediaInfo.vehicle_types).map(
|
...Object.entries(encyclopediaInfo.vehicle_types).map(
|
||||||
([type, name]) => ({
|
([type, name]) => ({
|
||||||
name: name.replace(' Tank', ''),
|
name: name.replace(' Tank', ''),
|
||||||
value: type,
|
value: type,
|
||||||
@@ -69,8 +54,7 @@ export default async function addFilterOptions<
|
|||||||
)
|
)
|
||||||
.setRequired(false),
|
.setRequired(false),
|
||||||
)
|
)
|
||||||
.addStringOption((option) => addTankChoices(option).setRequired(false));
|
.addStringOption((option) =>
|
||||||
});
|
addTankChoices(option).setRequired(false),
|
||||||
|
) as Option;
|
||||||
return 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;
|
||||||
|
}
|
||||||
@@ -3,12 +3,17 @@ import { PeriodType } from './addPeriodSubCommands';
|
|||||||
|
|
||||||
export function getCustomPeriodParams(
|
export function getCustomPeriodParams(
|
||||||
interaction: ChatInputCommandInteraction,
|
interaction: ChatInputCommandInteraction,
|
||||||
|
forcedNonCustomPeriod = false,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!forcedNonCustomPeriod &&
|
||||||
|
(interaction.options.getSubcommand() as PeriodType) === 'custom'
|
||||||
) {
|
) {
|
||||||
if ((interaction.options.getSubcommand() as PeriodType) === 'custom')
|
|
||||||
return {
|
return {
|
||||||
start: interaction.options.getInteger('start', true),
|
start: interaction.options.getInteger('start', true),
|
||||||
end: interaction.options.getInteger('end', true),
|
end: interaction.options.getInteger('end', true),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -18,9 +18,10 @@ export default async function resolveClanFromCommand(
|
|||||||
if (accounts[0]) {
|
if (accounts[0]) {
|
||||||
return { region: accounts[0].region, id: accounts[0].clan_id };
|
return { region: accounts[0].region, id: accounts[0].clan_id };
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Could not find clan', {
|
throw new UserError(
|
||||||
cause: `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
|
'Could not find clan',
|
||||||
});
|
`\`${clan}\` was not found. Try selecting a clan from the search result.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,11 +20,14 @@ export interface ResolvedPeriod {
|
|||||||
export default function resolvePeriodFromCommand(
|
export default function resolvePeriodFromCommand(
|
||||||
region: Region,
|
region: Region,
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
|
forcedPeriod?: PeriodType,
|
||||||
) {
|
) {
|
||||||
let name: string;
|
let name: string;
|
||||||
let start: number;
|
let start: number;
|
||||||
let end: number;
|
let end: number;
|
||||||
const periodSubcommand = interaction.options.getSubcommand() as PeriodType;
|
const periodSubcommand = forcedPeriod
|
||||||
|
? forcedPeriod
|
||||||
|
: (interaction.options.getSubcommand() as PeriodType);
|
||||||
|
|
||||||
if (periodSubcommand === 'custom') {
|
if (periodSubcommand === 'custom') {
|
||||||
const startOption = interaction.options.getInteger('start', true);
|
const startOption = interaction.options.getInteger('start', true);
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ export default async function resolvePlayerFromCommand(
|
|||||||
id: accounts[0].account_id,
|
id: accounts[0].account_id,
|
||||||
} satisfies ResolvedPlayer;
|
} satisfies ResolvedPlayer;
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Could not find user', {
|
throw new UserError(
|
||||||
cause: `I couldn't find user "${markdownEscape(
|
'Could not find user',
|
||||||
|
`I couldn't find user "${markdownEscape(
|
||||||
commandUsername,
|
commandUsername,
|
||||||
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`,
|
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -47,9 +48,10 @@ export default async function resolvePlayerFromCommand(
|
|||||||
if (account) {
|
if (account) {
|
||||||
return { region: account.region, id: account.blitz };
|
return { region: account.region, id: account.blitz };
|
||||||
} else {
|
} else {
|
||||||
throw new UserError('Use the `/link` command', {
|
throw new UserError(
|
||||||
cause: 'Link your Blitz and Discord accounts to get started.',
|
"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(
|
export default async function handleChatInputCommand(
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
) {
|
) {
|
||||||
const registry = (await commands)[interaction.commandName];
|
const awaitedCommands = await commands;
|
||||||
|
const registry = awaitedCommands[interaction.commandName];
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { searchClansCommand } from '../../commands/searchClans';
|
|||||||
import { searchPlayersCommand } from '../../commands/searchPlayers';
|
import { searchPlayersCommand } from '../../commands/searchPlayers';
|
||||||
import { searchTanksCommand } from '../../commands/searchTanks';
|
import { searchTanksCommand } from '../../commands/searchTanks';
|
||||||
import { statsCommand } from '../../commands/stats';
|
import { statsCommand } from '../../commands/stats';
|
||||||
|
import { todayCommand } from '../../commands/today';
|
||||||
import getClientId from '../../core/blitzkrieg/getClientId';
|
import getClientId from '../../core/blitzkrieg/getClientId';
|
||||||
import isDev from '../../core/blitzkrieg/isDev';
|
import isDev from '../../core/blitzkrieg/isDev';
|
||||||
import { secrets } from '../../core/blitzkrieg/secrets';
|
import { secrets } from '../../core/blitzkrieg/secrets';
|
||||||
@@ -52,34 +53,39 @@ export type InteractionReturnable =
|
|||||||
| InteractionIterableReturnable
|
| InteractionIterableReturnable
|
||||||
| Promise<InteractionIterableReturnable>;
|
| Promise<InteractionIterableReturnable>;
|
||||||
|
|
||||||
export interface Registry {
|
type CommandRegistryBase = {
|
||||||
inProduction: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommandRegistryRaw = Registry & {
|
|
||||||
inPublic: boolean;
|
|
||||||
inPreview?: boolean;
|
|
||||||
|
|
||||||
command:
|
command:
|
||||||
| SlashCommandBuilder
|
| SlashCommandBuilder
|
||||||
| SlashCommandSubcommandsOnlyBuilder
|
| SlashCommandSubcommandsOnlyBuilder
|
||||||
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
||||||
|
};
|
||||||
|
type CommandRegistryDefinitionBase = {
|
||||||
|
inProduction: boolean;
|
||||||
|
inPublic: boolean;
|
||||||
|
inPreview?: boolean;
|
||||||
|
|
||||||
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
||||||
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
||||||
} & (
|
};
|
||||||
| {
|
type CommandRegistryDefinitionHandlesInteraction = {
|
||||||
handlesInteraction: true;
|
handlesInteraction: true;
|
||||||
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
|
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
|
||||||
}
|
};
|
||||||
| {
|
type CommandRegistryDefinitionDoesHandleInteraction = {
|
||||||
handlesInteraction?: false;
|
handlesInteraction?: false;
|
||||||
handler: (
|
handler: (
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
) => InteractionReturnable;
|
) => InteractionReturnable;
|
||||||
}
|
};
|
||||||
);
|
export type CommandRegistryPromisable = CommandRegistryBase &
|
||||||
export type CommandRegistry = CommandRegistryRaw | Promise<CommandRegistryRaw>;
|
(CommandRegistryDefinitionBase &
|
||||||
|
(
|
||||||
|
| CommandRegistryDefinitionHandlesInteraction
|
||||||
|
| CommandRegistryDefinitionDoesHandleInteraction
|
||||||
|
));
|
||||||
|
export type CommandRegistry =
|
||||||
|
| CommandRegistryPromisable
|
||||||
|
| Promise<CommandRegistryPromisable>;
|
||||||
|
|
||||||
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
|
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
|
||||||
|
|
||||||
@@ -101,10 +107,11 @@ export const COMMANDS_RAW: CommandRegistry[] = [
|
|||||||
evolutionCommand,
|
evolutionCommand,
|
||||||
statsCommand,
|
statsCommand,
|
||||||
ratingsCommand,
|
ratingsCommand,
|
||||||
|
todayCommand,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
|
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
|
||||||
rawCommands.reduce<Record<string, CommandRegistryRaw>>(
|
rawCommands.reduce<Record<string, CommandRegistryPromisable>>(
|
||||||
(accumulator, registry) => {
|
(accumulator, registry) => {
|
||||||
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
||||||
|
|
||||||
@@ -123,7 +130,7 @@ export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
|||||||
[];
|
[];
|
||||||
|
|
||||||
commands.then((awaitedCommands) => {
|
commands.then((awaitedCommands) => {
|
||||||
Object.entries(awaitedCommands).forEach(([, registry]) => {
|
Object.values(awaitedCommands).forEach((registry) => {
|
||||||
const json = registry.command.toJSON();
|
const json = registry.command.toJSON();
|
||||||
|
|
||||||
if (registry.inPublic) {
|
if (registry.inPublic) {
|
||||||
|
|||||||
Reference in new issue
Block a user