From 890a4d142b3fd8f8bfad02e929e57bd8453b3d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:02:03 -0600 Subject: [PATCH] feat: simplified /ratings command --- src/commands/ratings.tsx | 359 +++++++++++------------ src/core/blitz/getRatingsInfo.ts | 9 +- src/core/blitz/isOnGoingRatingsSeason.ts | 6 + 3 files changed, 180 insertions(+), 194 deletions(-) create mode 100644 src/core/blitz/isOnGoingRatingsSeason.ts diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 197a55c21..68ab00559 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -2,7 +2,6 @@ import { APIApplicationCommandOptionChoice, SlashCommandBuilder, SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, } from 'discord.js'; import { range } from 'lodash'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; @@ -17,13 +16,13 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo'; import { getRatingsLeague } from '../core/blitz/getRatingsLeague'; import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors'; +import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason'; import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber'; import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo'; import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard'; import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard'; import { UserError } from '../core/blitzkrieg/userError'; import addRegionChoices from '../core/discord/addRegionChoices'; -import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; import embedNegative from '../core/discord/embedNegative'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; @@ -116,56 +115,6 @@ 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) - .addIntegerOption((option) => - option - .setName('limit') - .setDescription('How many neighbors to display (default: 10)') - .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, - ), - ) - .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.", @@ -174,38 +123,72 @@ const noOngoingSeason = embedNegative( export const ratingsCommand = new Promise( async (resolve) => { const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); - const seasonChoices = range( + const onGoingSeason = await isOnGoingRatingsSeason(); + const seasonNumbers = range( FIRST_ARCHIVED_RATINGS_SEASON, - latestArchivedSeasonNumber + 1, - ).map( - (number) => - ({ - name: `Season ${number}`, - value: `${number}`, - }) satisfies APIApplicationCommandOptionChoice, + latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1, ); + function addSubcommands(option: SlashCommandSubcommandBuilder) { + return option + .addIntegerOption((option) => + option + .setName('limit') + .setDescription('How many neighbors to display (default: 10)') + .setRequired(false) + .setMinValue(5) + .setMaxValue(30), + ) + .addStringOption((option) => + option + .setName('season') + .setDescription('Season number') + .setRequired(false) + .addChoices( + ...seasonNumbers + .map( + (number, index) => + ({ + name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`, + value: `${number}`, + }) satisfies APIApplicationCommandOptionChoice, + ) + .reverse(), + ), + ); + } + const command = new SlashCommandBuilder() .setName('ratings') .setDescription('Ratings battles statistics') - .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, + ), + ) + .setRequired(true), + ), ), ); @@ -224,18 +207,19 @@ export const ratingsCommand = new Promise( nickname: string; } - const subcommandGroup = interaction.options.getSubcommandGroup(true) as - | 'archive' - | 'current'; const subcommand = interaction.options.getSubcommand(true) as | 'league' | 'neighbors'; const limit = interaction.options.getInteger('limit') ?? 10; const seasonOption = interaction.options.getString('season'); - const season = seasonOption ? parseInt(seasonOption) : undefined; + const season = seasonOption + ? parseInt(seasonOption) + : onGoingSeason + ? latestArchivedSeasonNumber + 1 + : latestArchivedSeasonNumber; + const isCurrentSeason = season === latestArchivedSeasonNumber + 1; let titleName: string; let titleImage: string | undefined; - let titleNameDiscriminator: string | undefined; let titleDescription: string; let playersBefore: number; let playersAfter: number; @@ -249,69 +233,66 @@ export const ratingsCommand = new Promise( interaction.options.getString('league')!, ); const region = interaction.options.getString('region') as Region; - regionRatingsInfo = await (subcommandGroup === 'current' + regionRatingsInfo = await (isCurrentSeason ? getRatingsInfo(region) : getArchivedRatingsInfo(region, season!)); if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; - const result = - subcommandGroup === 'current' - ? await getRatingsLeague(region, leagueIndex).then( - async (result) => - result.slice(0, limit).map( - (player) => - ({ - id: player.spa_id, - score: player.score, - position: player.number, - clan: player.clan_tag, - nickname: player.nickname, - }) satisfies SimplifiedPlayer, - ), - ) - : await getArchivedRatingsLeaderboard(region, season!).then( - async (players) => { - const firstPlayerIndex = - leagueIndex === 0 - ? 0 - : players.findIndex( - (player) => - player.score < LEAGUES[leagueIndex - 1].minScore, - ); - const lastPlayerIndex = firstPlayerIndex + limit - 1; - const trimmed = players.slice( - firstPlayerIndex, - lastPlayerIndex + 1, - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map(({ id }) => id), - ['clan'], - ); + const result = isCurrentSeason + ? await getRatingsLeague(region, leagueIndex).then(async (result) => + result.slice(0, limit).map( + (player) => + ({ + id: player.spa_id, + score: player.score, + position: player.number, + clan: player.clan_tag, + nickname: player.nickname, + }) satisfies SimplifiedPlayer, + ), + ) + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { + const firstPlayerIndex = + leagueIndex === 0 + ? 0 + : players.findIndex( + (player) => + player.score < LEAGUES[leagueIndex - 1].minScore, + ); + const lastPlayerIndex = firstPlayerIndex + limit - 1; + const trimmed = players.slice( + firstPlayerIndex, + lastPlayerIndex + 1, + ); + const clanData = await getClanAccountInfo( + region, + trimmed.map(({ id }) => id), + ['clan'], + ); - return trimmed.map( - (player, index) => - ({ - id: player.id, - score: player.score, - position: firstPlayerIndex + index + 1, - clan: clanData[player.id]?.clan?.tag, - nickname: - clanData[index]?.account_name ?? - `Deleted ${player.id}`, - }) satisfies SimplifiedPlayer, - ); - }, - ); + return trimmed.map( + (player, index) => + ({ + id: player.id, + score: player.score, + position: firstPlayerIndex + index + 1, + clan: clanData[player.id]?.clan?.tag, + nickname: + clanData[index]?.account_name ?? + `Deleted ${player.id}`, + }) satisfies SimplifiedPlayer, + ); + }, + ); const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; - midnightLeaderboard = - subcommandGroup === 'current' - ? await getArchivedRatingsMidnightLeaderboard( - region, - regionRatingsInfo.current_season, - ) - : await getArchivedRatingsMidnightLeaderboard(region, season!); + midnightLeaderboard = isCurrentSeason + ? await getArchivedRatingsMidnightLeaderboard( + region, + regionRatingsInfo.current_season, + ) + : await getArchivedRatingsMidnightLeaderboard(region, season!); players = result; playersBefore = result ? result[0].position - 1 : 0; @@ -326,22 +307,17 @@ export const ratingsCommand = new Promise( 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,54 +328,51 @@ export const ratingsCommand = new Promise( nickname: player.nickname, }) satisfies SimplifiedPlayer, ), - ) - : await getArchivedRatingsLeaderboard(region, season!).then( - async (players) => { - const playerIndex = players.findIndex( - (player) => player.id === id, + ) + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { + const playerIndex = players.findIndex( + (player) => player.id === id, + ); + + if (playerIndex === -1) { + throw new UserError( + `${accountInfo.nickname} didn't player ratings in season ${season}`, + { + cause: + 'This player did not participate in this season or did not get past calibration.', + }, ); + } - if (playerIndex === -1) { - throw new UserError( - `${accountInfo.nickname} didn't player ratings in season ${season}`, - { - cause: - 'This player did not participate in this season or did not get past calibration.', - }, - ); - } + const halfRange = Math.round(limit / 2); + const trimmed = players.slice( + playerIndex - halfRange, + playerIndex + halfRange + 1, + ); + const clanData = await getClanAccountInfo( + region, + trimmed.map((player) => player.id), + ['clan'], + ); - const halfRange = Math.round(limit / 2); - const trimmed = players.slice( - playerIndex - halfRange, - playerIndex + halfRange + 1, - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map((player) => player.id), - ['clan'], - ); - - return trimmed.map((player, index) => ({ - id: player.id, - score: player.score, - position: playerIndex + index + 1, - clan: clanData[index]?.clan?.tag, - nickname: clanData[index] - ? clanData[index]!.account_name - : `Deleted ${player.id}`, - })); - }, - ); - 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 diff --git a/src/core/blitz/getRatingsInfo.ts b/src/core/blitz/getRatingsInfo.ts index 6c77df9f7..ef1616f12 100644 --- a/src/core/blitz/getRatingsInfo.ts +++ b/src/core/blitz/getRatingsInfo.ts @@ -4,7 +4,11 @@ import { context } from '../blitzkrieg/context'; import { patientFetch } from '../blitzkrieg/patientFetch'; import regionToRegionSubdomain from './regionToRegionSubdomain'; +const ratingsInfoCache: Partial> = {}; + 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; + ratingsInfoCache[region] = data; + + return data; } diff --git a/src/core/blitz/isOnGoingRatingsSeason.ts b/src/core/blitz/isOnGoingRatingsSeason.ts new file mode 100644 index 000000000..ef6f1e2d5 --- /dev/null +++ b/src/core/blitz/isOnGoingRatingsSeason.ts @@ -0,0 +1,6 @@ +import getRatingsInfo from './getRatingsInfo'; + +export async function isOnGoingRatingsSeason() { + const ratingsInfo = await getRatingsInfo('com'); + return ratingsInfo.detail === undefined; +}