From 804fa58d274169631f8fbc3bdbb0a29fe716d439 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Sun, 4 Jun 2023 00:01:10 +0000 Subject: [PATCH] feat: eligible command --- .vscode/settings.json | 1 + src/commands/eligible.ts | 179 ++++++++++++++++++ src/commands/stats.tsx | 3 +- src/core/blitz/getBlitzAccount.ts | 6 +- src/core/blitz/getBlitzClan.ts | 4 +- src/core/blitz/getTankStats.ts | 4 +- src/core/blitz/getWN8Percentile.ts | 2 +- src/core/blitz/resolveTankId.ts | 6 +- .../{errorEmbed.ts => negativeEmbed.ts} | 2 +- src/core/interaction/noMember.ts | 4 +- .../interaction/notTrackedByBlitzStars.ts | 4 +- src/core/interaction/positiveEmbed.ts | 9 + src/events/interactionCreate.ts | 18 +- 13 files changed, 219 insertions(+), 23 deletions(-) create mode 100644 src/commands/eligible.ts rename src/core/interaction/{errorEmbed.ts => negativeEmbed.ts} (74%) create mode 100644 src/core/interaction/positiveEmbed.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 862f08758..3c3d7a00f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "ownedtanks", "Resvg", "searchplayers", + "SKLLD", "Smri", "strvk", "tankopedia", diff --git a/src/commands/eligible.ts b/src/commands/eligible.ts new file mode 100644 index 000000000..56c16db5f --- /dev/null +++ b/src/commands/eligible.ts @@ -0,0 +1,179 @@ +import { SlashCommandBuilder } from 'discord.js'; +import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; +import getWN8 from '../core/blitz/getWN8.js'; +import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; +import sumStats from '../core/blitz/sumStats.js'; +import { tankopedia } from '../core/blitz/tankopedia.js'; +import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; +import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; +import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; +import { tankAverages } from '../core/blitzstars/tankAverages.js'; +import cleanTable, { TableInput } from '../core/interaction/cleanTable.js'; +import cmdName from '../core/interaction/cmdName.js'; +import negativeEmbed from '../core/interaction/negativeEmbed.js'; +import positiveEmbed from '../core/interaction/positiveEmbed.js'; +import addUsernameOption from '../core/options/addUsernameOption.js'; +import { args } from '../core/process/args.js'; +import { CommandRegistry } from '../events/interactionCreate.js'; +import { AccountInfo } from '../types/accountInfo.js'; + +export type SkilledClan = 'SKLLD' | 'SMRI'; + +export const SKILLED_CLANS: Record = { + SKLLD: 'Skilled', + SMRI: 'Samurai', +}; + +export default { + inProduction: true, + inDevelopment: true, + inPublic: false, + + command: new SlashCommandBuilder() + .setName(cmdName('eligible')) + .setDescription('Checks eligibility for any Skilled clan') + .addStringOption((option) => + option + .setName('clan') + .setDescription('The clan to check') + .setChoices( + { + name: SKILLED_CLANS.SKLLD, + value: 'SKLLD' satisfies SkilledClan, + }, + { name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan }, + ) + .setRequired(true), + ) + .addStringOption(addUsernameOption), + + async execute(interaction) { + const clan = interaction.options.getString('clan') as SkilledClan; + const { id, server } = await getBlitzAccount(interaction); + const accountInfo = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`, + ); + const problems: TableInput = []; + const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`; + let isEligible = false; + let body = ''; + + if (clan === 'SKLLD') { + const tankStatsOverTime = await getTankStatsOverTime( + interaction, + server, + id, + getPeriodicStart('30'), + getPeriodNow(), + ); + const entries = Object.entries(tankStatsOverTime); + const stats = sumStats(entries.map(([, stats]) => stats)); + const averageTier = + entries.reduce((accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + const tankTier = tankopedia[tankId].tier; + + return accumulator + tankTier * stats.battles; + }, 0) / stats.battles; + const WN8 = + entries.reduce((accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + + // edge case where new tanks don't have averages + if (tankAverages[tankId]) { + const tankWN8 = getWN8(tankAverages[tankId].all, stats); + + if (isNaN(tankWN8)) return accumulator; + return accumulator + tankWN8 * stats.battles; + } else return accumulator; + }, 0) / stats.battles; + const tier10Damage = + entries.reduce((accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + + if (tankopedia[tankId]?.tier === 10) { + const tankStats = tankStatsOverTime[tankId]; + return accumulator + tankStats.damage_dealt; + } else return accumulator; + }, 0) / + entries.reduce((accumulator, [tankIdString, stats]) => { + const tankId = parseInt(tankIdString); + + if (tankopedia[tankId]?.tier === 10) { + const tankStats = tankStatsOverTime[tankId]; + return accumulator + tankStats.battles; + } else return accumulator; + }, 0); + + if (stats.battles < 126) { + problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]); + } + if (averageTier < 8) { + problems.push([ + 'Average tier', + `${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`, + ]); + } + if (stats.wins / stats.battles < 0.6) { + problems.push([ + 'Winrate', + `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${( + 100 * + (stats.wins / stats.battles - 0.6) + ).toFixed(2)}%)`, + ]); + } + if (WN8 < 2450) { + problems.push(['WN8', `${WN8.toFixed(0)} (${WN8 - 2450})`]); + } + if (tier10Damage < 1250) { + problems.push([ + 'Tier 10 damage', + `${tier10Damage.toFixed(0)} (${tier10Damage - 1250})`, + ]); + } + + isEligible = problems.length === 0; + body = isEligible + ? 'Player is fully eligible.' + : `Player is not eligible. Shortcomings in 30-day statistics:\n\n${cleanTable( + problems, + )}`; + } else if (clan === 'SMRI') { + const stats = accountInfo[id].statistics.all; + + if (stats.battles < 1500) { + problems.push([ + 'Battles', + `${stats.battles} (${stats.battles - 1500})`, + ]); + } + if (stats.wins / stats.battles <= 0.53) { + problems.push([ + 'Winrate', + `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${( + 100 * + (stats.wins / stats.battles - 0.53) + ).toFixed(2)}%)`, + ]); + } + + isEligible = problems.length === 0; + body = isEligible + ? 'Player is fully eligible.' + : `Player is not eligible. Shortcomings in career statistics:\n\n${cleanTable( + problems, + )}`; + } + + await interaction.editReply({ + embeds: isEligible + ? [positiveEmbed(title, body)] + : [negativeEmbed(title, body)], + }); + + console.log( + `Displaying ${server}/${id}'s eligibility information for ${clan}`, + ); + }, +} satisfies CommandRegistry; diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index 7fec4bc15..866c6c8ce 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -51,8 +51,7 @@ export default { async execute(interaction) { const period = interaction.options.getString('period') as StatPeriod; - const blitzAccount = await getBlitzAccount(interaction); - const { id, server } = blitzAccount; + const { id, server } = await getBlitzAccount(interaction); const accountInfo = await getWargamingResponse( `https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`, ); diff --git a/src/core/blitz/getBlitzAccount.ts b/src/core/blitz/getBlitzAccount.ts index 5b3c8e72c..7a16d341b 100644 --- a/src/core/blitz/getBlitzAccount.ts +++ b/src/core/blitz/getBlitzAccount.ts @@ -1,6 +1,6 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { BlitzServer } from '../../constants/servers.js'; -import errorEmbed from '../interaction/errorEmbed.js'; +import negativeEmbed from '../interaction/negativeEmbed.js'; import listAccountsPanServer, { usernamePatternWithoutPosition, } from './listAccountsPanServer.js'; @@ -19,7 +19,7 @@ export default async function getBlitzAccount( if (username === undefined) { await interaction.editReply({ embeds: [ - errorEmbed( + negativeEmbed( 'Username could not be inferred', "You didn't provide me a username in the command nor was I able to infer your Blitz username from your Discord nickname. Try providing a username manually in the command or using the `/verify` command to set your Discord nickname to your Blitz username.", ), @@ -42,7 +42,7 @@ export default async function getBlitzAccount( } else { await interaction.editReply({ embeds: [ - errorEmbed( + negativeEmbed( 'Could not find user', `I couldn't find user \`${username}\`. Try providing a username manually or using the \`/verify\` command to set your Discord nickname to your Blitz username.`, ), diff --git a/src/core/blitz/getBlitzClan.ts b/src/core/blitz/getBlitzClan.ts index 343711c11..e15c103be 100644 --- a/src/core/blitz/getBlitzClan.ts +++ b/src/core/blitz/getBlitzClan.ts @@ -1,6 +1,6 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { BlitzServer } from '../../constants/servers.js'; -import errorEmbed from '../interaction/errorEmbed.js'; +import negativeEmbed from '../interaction/negativeEmbed.js'; import { serverAndIdPattern } from './getBlitzAccount.js'; import listClansPanServer from './listClansPanServer.js'; @@ -19,7 +19,7 @@ export default async function getBlitzClan( } else { await interaction.editReply({ embeds: [ - errorEmbed( + negativeEmbed( 'Could not find clan', `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`, ), diff --git a/src/core/blitz/getTankStats.ts b/src/core/blitz/getTankStats.ts index 6569c694c..d41b3a9c6 100644 --- a/src/core/blitz/getTankStats.ts +++ b/src/core/blitz/getTankStats.ts @@ -1,7 +1,7 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { BLITZ_SERVERS, BlitzServer } from '../../constants/servers.js'; import { TanksStats } from '../../types/tanksStats.js'; -import errorEmbed from '../interaction/errorEmbed.js'; +import negativeEmbed from '../interaction/negativeEmbed.js'; import { args } from '../process/args.js'; import getWargamingResponse from './getWargamingResponse.js'; @@ -17,7 +17,7 @@ export default async function getTankStats( if (tankStats[id] === null) { interaction.editReply({ embeds: [ - errorEmbed( + negativeEmbed( 'No tank stats available', `Wargaming says there is no tank stats for this account. This account may not have any battles/tanks or exist in the ${BLITZ_SERVERS[server]} server.`, ), diff --git a/src/core/blitz/getWN8Percentile.ts b/src/core/blitz/getWN8Percentile.ts index 870bf83d4..e153cad4e 100644 --- a/src/core/blitz/getWN8Percentile.ts +++ b/src/core/blitz/getWN8Percentile.ts @@ -1,6 +1,6 @@ import { Percentile } from '../../constants/percentiles.js'; -const WN8_PERCENTILES: [number, Percentile][] = [ +export const WN8_PERCENTILES: [number, Percentile][] = [ [0, Percentile.VeryBad], [300, Percentile.Bad], [450, Percentile.BelowAverage], diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index 95507f5d9..c480d0fee 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -1,6 +1,6 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { go } from 'fuzzysort'; -import errorEmbed from '../interaction/errorEmbed.js'; +import negativeEmbed from '../interaction/negativeEmbed.js'; import { TANKS, tankopedia } from './tankopedia.js'; export default async function resolveTankId( @@ -15,7 +15,7 @@ export default async function resolveTankId( if (searchResult.length === 0) { await interaction.editReply({ embeds: [ - errorEmbed( + negativeEmbed( 'Tank not found', `Could not find tank by the name "${tank}".`, ), @@ -32,7 +32,7 @@ export default async function resolveTankId( } else { await interaction.editReply({ embeds: [ - errorEmbed( + negativeEmbed( 'Tank not found', `Could not find tank by the ID "${number}".`, ), diff --git a/src/core/interaction/errorEmbed.ts b/src/core/interaction/negativeEmbed.ts similarity index 74% rename from src/core/interaction/errorEmbed.ts rename to src/core/interaction/negativeEmbed.ts index e4e6ed1e1..f01024c82 100644 --- a/src/core/interaction/errorEmbed.ts +++ b/src/core/interaction/negativeEmbed.ts @@ -1,7 +1,7 @@ import { EmbedBuilder } from 'discord.js'; import { NEGATIVE_COLOR } from '../../constants/colors.js'; -export default function errorEmbed(title: string, description: string) { +export default function negativeEmbed(title: string, description: string) { return new EmbedBuilder() .setColor(NEGATIVE_COLOR) .setTitle(title) diff --git a/src/core/interaction/noMember.ts b/src/core/interaction/noMember.ts index 9737ece2a..6b2071220 100644 --- a/src/core/interaction/noMember.ts +++ b/src/core/interaction/noMember.ts @@ -1,6 +1,6 @@ -import errorEmbed from './errorEmbed.js'; +import negativeEmbed from './negativeEmbed.js'; -export const noMember = errorEmbed( +export const noMember = negativeEmbed( 'User of command is not a member', 'Discord claims that the user of this command does not have an account... strange!', ); diff --git a/src/core/interaction/notTrackedByBlitzStars.ts b/src/core/interaction/notTrackedByBlitzStars.ts index 907f3342c..566551cf3 100644 --- a/src/core/interaction/notTrackedByBlitzStars.ts +++ b/src/core/interaction/notTrackedByBlitzStars.ts @@ -1,6 +1,6 @@ -import errorEmbed from './errorEmbed.js'; +import negativeEmbed from './negativeEmbed.js'; -export const notTrackedByBlitzStars = errorEmbed( +export const notTrackedByBlitzStars = negativeEmbed( 'No data to display', 'Player is not tracked by BlitzStars.', ); diff --git a/src/core/interaction/positiveEmbed.ts b/src/core/interaction/positiveEmbed.ts new file mode 100644 index 000000000..29a86adc5 --- /dev/null +++ b/src/core/interaction/positiveEmbed.ts @@ -0,0 +1,9 @@ +import { EmbedBuilder } from 'discord.js'; +import { POSITIVE_COLOR } from '../../constants/colors.js'; + +export default function positiveEmbed(title: string, description: string) { + return new EmbedBuilder() + .setColor(POSITIVE_COLOR) + .setTitle(title) + .setDescription(description); +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index b64e953ac..5fb7c2b75 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -58,11 +58,19 @@ try { console.log(`Successfully refreshed ${publicData.length} public command(s).`); console.log(`Refreshing ${guildCommands.length} guild command(s).`); - const guildData = (await rest.put( - Routes.applicationGuildCommands(getClientId(), discord.sklld_guild_id), - { body: guildCommands }, - )) as RESTPostAPIChatInputApplicationCommandsJSONBody[]; - console.log(`Successfully refreshed ${guildData.length} guild command(s).`); + const guildData = [ + ...((await rest.put( + Routes.applicationGuildCommands(getClientId(), discord.sklld_guild_id), + { body: guildCommands }, + )) as RESTPostAPIChatInputApplicationCommandsJSONBody[]), + ...((await rest.put( + Routes.applicationGuildCommands(getClientId(), discord.tres_guild_id), + { body: guildCommands }, + )) as RESTPostAPIChatInputApplicationCommandsJSONBody[]), + ]; + console.log( + `Successfully refreshed ${guildData.length / 2} guild command(s).`, + ); } catch (error) { console.error(error); }