feat: /tankstats

This commit is contained in:
TresAbhi committed 2023-05-13 21:42:40 +00:00
1 parent d62ca080e9
commit 5b918d6e3e
16 files changed
+317 -33

No files matched your search

+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@types/node": "^18.15.11",
"discord.js": "^14.9.0",
"fuzzysort": "^2.0.4",
"markdown-escape": "^2.0.0",
"node-fetch": "^3.3.1",
"typescript": "^5.0.4",
+40 -20
View File
@@ -1,4 +1,5 @@
import {
AutocompleteInteraction,
CacheType,
ChatInputCommandInteraction,
Collection,
@@ -21,6 +22,7 @@ export interface CommandRegistry {
command: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
execute: (interaction: ChatInputCommandInteraction<CacheType>) => void;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
}
const rest = new REST().setToken(args['discord-token']);
@@ -68,28 +70,46 @@ try {
export default async function interactionCreate(
interaction: Interaction<CacheType>,
) {
if (!interaction.isChatInputCommand()) return;
const command = commandCollection.get(interaction.commandName);
if (interaction.isAutocomplete()) {
const command = commandCollection.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
if (!command) {
console.error(
`No command matching ${interaction.commandName} was found.`,
);
return;
}
try {
await interaction.deferReply();
command.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: 'There was an error while executing this command!',
ephemeral: true,
});
} else {
await interaction.editReply({
content: 'There was an error while executing this command!',
});
try {
if (command.autocomplete) command.autocomplete(interaction);
} catch (error) {
console.error(error);
}
} else if (interaction.isChatInputCommand()) {
const command = commandCollection.get(interaction.commandName);
if (!command) {
console.error(
`No command matching ${interaction.commandName} was found.`,
);
return;
}
try {
await interaction.deferReply();
command.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: 'There was an error while executing this command!',
ephemeral: true,
});
} else {
await interaction.editReply({
content: 'There was an error while executing this command!',
});
}
}
}
}
+3 -10
View File
@@ -9,6 +9,7 @@ import addIGNOption from '../utilities/addIGNOption.js';
import addServerChoices from '../utilities/addServerChoices.js';
import { args } from '../utilities/args.js';
import blitzLinks from '../utilities/blitzLinks.js';
import cleanTable from '../utilities/cleanTable.js';
import cmdName from '../utilities/cmdName.js';
import getBlitzAccount from '../utilities/getBlitzAccount.js';
import getBlitzStarsAccount from '../utilities/getBlitzStarsAccount.js';
@@ -134,7 +135,7 @@ export default {
period === 'today'
? '**⚠ "Today" stats are in development. Some stats are not available/wrong.**\n\n'
: ''
}${[
}${cleanTable([
[
'Winrate',
`${(100 * (stats.all.wins / stats.all.battles)).toFixed(
@@ -200,15 +201,7 @@ export default {
(stats.all.battles - stats.all.survived_battles)
).toFixed(2),
],
]
.filter((array) => array[1] !== `${-Infinity}`)
.map((array) =>
array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`,
)
.join('\n')}\n\n${blitzLinks(
server,
blitzAccount.nickname,
)}`
])}\n\n${blitzLinks(server, blitzAccount.nickname)}`
: 'No battles played in this period.',
),
),
+169
View File
@@ -0,0 +1,169 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { CommandRegistry } from '../behaviors/interactionCreate.js';
import { BlitzServer } from '../constants/servers.js';
import tanksAutocomplete from '../core/autocomplete/tanks.js';
import getPeriodicStats, {
Period,
} from '../core/blitztankstats/getPeriodicStats.js';
import errorEmbed from '../core/embeds/errorEmbed.js';
import sklldEmbed from '../core/embeds/sklldEmbed.js';
import addIGNOption from '../utilities/addIGNOption.js';
import addServerChoices from '../utilities/addServerChoices.js';
import addTankChoices from '../utilities/addTankChoices.js';
import cleanTable from '../utilities/cleanTable.js';
import cmdName from '../utilities/cmdName.js';
import getBlitzAccount from '../utilities/getBlitzAccount.js';
import { tankopedia } from '../utilities/tankopedia.js';
type Periods = '30' | '60' | '90';
export default {
inProduction: true,
inDevelopment: false,
inPublic: true,
command: new SlashCommandBuilder()
.setName(cmdName('tankstats'))
.setDescription('Stats for a tank over a period')
.addStringOption(addTankChoices)
.addStringOption((option) =>
option
.setName('period')
.setDescription('The period to get stats for')
.setChoices(
{ name: '30 Days', value: '30' satisfies Periods },
{ name: '60 Days', value: '60' satisfies Periods },
{ name: '90 Days', value: '90' satisfies Periods },
)
.setRequired(true),
)
.addStringOption(addServerChoices)
.addStringOption(addIGNOption),
async execute(interaction) {
const server = interaction.options.getString('server') as BlitzServer;
const name = interaction.options.getString('name')!;
const tankId = interaction.options.getString('tank')!;
const period = interaction.options.getString('period') as Periods;
const blitzAccount = await getBlitzAccount(interaction, name, server);
if (!blitzAccount) return;
const tank = tankopedia.data[tankId as unknown as number];
if (tank) {
const periodicStats = (
await getPeriodicStats(blitzAccount.account_id, Number(period))
).filter((stats) => stats.tank_id === tank.tank_id);
function sum(summer: (period: Period) => number) {
return periodicStats.reduce(
(accumulative, period) => accumulative + summer(period),
0,
);
}
function weighted(summer: (period: Period) => number) {
return (
periodicStats.reduce(
(accumulative, period) =>
accumulative + summer(period) * period.battles,
0,
) / sum((period) => period.battles)
);
}
interaction.editReply({
embeds: [
sklldEmbed(
`${period} day stats for ${markdownEscape(
blitzAccount.nickname,
)}'s ${
tank.name ? markdownEscape(tank.name) : `Unknown Tank ${tankId}`
}`,
periodicStats.length > 0
? cleanTable([
[
'Winrate',
`${(
100 *
(sum((a) => a.wins) / sum((a) => a.battles))
).toFixed(2)}%`,
],
[
'Survival',
`${(
100 *
(sum((a) => a.survived_battles) / sum((a) => a.battles))
).toFixed(2)}%`,
],
[
'Accuracy',
`${(
100 *
(sum((a) => a.hits) / sum((a) => a.shots))
).toFixed(2)}%`,
],
['WN8', weighted((a) => a.wn8).toFixed(0)],
[],
['Battles', sum((a) => a.battles)],
['Wins', sum((a) => a.wins)],
['Losses', sum((a) => a.losses)],
[],
[
'Shots per battle',
(sum((a) => a.shots) / sum((a) => a.battles)).toFixed(2),
],
[
'Hits per battle',
(sum((a) => a.hits) / sum((a) => a.battles)).toFixed(2),
],
[
'Damage per battle',
(
sum((a) => a.damage_dealt) / sum((a) => a.battles)
).toFixed(0),
],
[
'Kills per battle',
(sum((a) => a.frags) / sum((a) => a.battles)).toFixed(2),
],
[
'Spots per battle',
(sum((a) => a.spotted) / sum((a) => a.battles)).toFixed(2),
],
[
'XP per battle',
(sum((a) => a.xp) / sum((a) => a.battles)).toFixed(0),
],
[],
[
'Damage ratio',
(
sum((a) => a.damage_dealt) / sum((a) => a.damage_received)
).toFixed(2),
],
[
'Kills to death ratio',
(
sum((a) => a.frags) /
sum((a) => a.battles - a.survived_battles)
).toFixed(2),
],
])
: 'No battles were played or recorded during this period.',
),
],
});
} else {
interaction.editReply({
embeds: [
errorEmbed(
'Tank not found',
`"${markdownEscape(tankId)}" does not exist in the game.`,
),
],
});
}
},
autocomplete: tanksAutocomplete,
} satisfies CommandRegistry;
+27
View File
@@ -0,0 +1,27 @@
import {
ApplicationCommandOptionChoiceData,
AutocompleteInteraction,
CacheType,
} from 'discord.js';
import { go } from 'fuzzysort';
import { tanks } from '../../utilities/tankopedia.js';
export default async function tanksAutocomplete(
interaction: AutocompleteInteraction<CacheType>,
) {
const focusedValue = interaction.options.getFocused();
await interaction.respond(
focusedValue
? go(focusedValue, tanks, { keys: ['name'], limit: 10 }).map(
(item) =>
({
name: item.obj.name
? item.obj.name
: `Unknown Tank ${item.obj.tank_id}`,
value: `${item.obj.tank_id}`,
} satisfies ApplicationCommandOptionChoiceData<string>),
)
: [],
);
}
@@ -0,0 +1,36 @@
export interface Period {
statsId: number;
account_id: number;
tank_id: number;
recordDate: string;
last_battle_time: number;
fromDate: string;
prior_battle_time: number;
battles: number;
wins: number;
losses: number;
spotted: number;
shots: number;
hits: number;
xp: number;
battle_life_time: number;
frags: number;
max_frags: number;
damage_dealt: number;
damage_received: number;
survived_battles: number;
win_and_survived: number;
capture_points: number;
dropped_capture_points: number;
killStealerValue: number;
isRating: boolean;
wn8: number;
region: string;
}
export default async function getPeriodicStats(id: number, period: number) {
const response = await fetch(
`https://blitztankstats.com/api/Stats/period/${id}/${period}`,
);
return (await response.json()) as Period[];
}
File renamed without changes.
@@ -1,5 +1,5 @@
import { EmbedBuilder } from 'discord.js';
import { NEGATIVE_COLOR } from '../constants/colors.js';
import { NEGATIVE_COLOR } from '../../constants/colors.js';
export default function errorEmbed(title: string, description: string) {
return new EmbedBuilder()
File renamed without changes.
+9
View File
@@ -0,0 +1,9 @@
import { EmbedBuilder } from 'discord.js';
import { SKILLED_COLOR } from '../../constants/colors.js';
export default function sklldEmbed(title: string, description: string) {
return new EmbedBuilder()
.setColor(SKILLED_COLOR)
.setTitle(title)
.setDescription(description);
}
+9
View File
@@ -0,0 +1,9 @@
import { SlashCommandStringOption } from 'discord.js';
export default function addTankChoices(option: SlashCommandStringOption) {
return option
.setName('tank')
.setDescription('The tank to get stats for')
.setAutocomplete(true)
.setRequired(true);
}
+8
View File
@@ -0,0 +1,8 @@
export type TableInput = ([] | [string, string | number])[];
export default function cleanTable(input: TableInput) {
return input
.filter((array) => array[1] !== `${-Infinity}`)
.map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`))
.join('\n');
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import fetch from 'node-fetch';
import { notTrackedByBlitzStars } from '../embeds/notTrackedByBlitzStars.js';
import { notTrackedByBlitzStars } from '../core/embeds/notTrackedByBlitzStars.js';
import { PlayerPeriodicStatsCollection } from '../types/statistics.js';
export default async function getBlitzStarsAccount(
+8 -1
View File
@@ -9,7 +9,7 @@ export interface Tankopedia {
};
data: {
[tankId: number]: {
name: string;
name?: string;
nation: string;
is_premium: boolean;
tier: number;
@@ -32,12 +32,19 @@ export const tankopedia = (await (
await fetch('https://www.blitzstars.com/bs-tankopedia.json')
).json()) as Tankopedia;
export const tankIds = Object.keys(tankopedia.data) as unknown as number[];
export const tankNames = tankIds.map((id) => tankopedia.data[id].name);
export const tanks = tankIds.map((id) => tankopedia.data[id]);
export const TANK_TYPE_NAMES: Record<TankType, string> = {
'AT-SPG': 'Tank Destroyer',
heavyTank: 'Heavy Tank',
mediumTank: 'Medium Tank',
lightTank: 'Light Tank',
};
export const TANK_TYPE_EMOJIS: Record<TankType, string> = {
'AT-SPG': '🔽',
heavyTank: '🇭',
+5
View File
@@ -311,6 +311,11 @@ fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
fuzzysort@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-2.0.4.tgz#a21d1ce8947eaf2797dc3b7c28c36db9d1165f84"
integrity sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw==
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"