mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: /tankstats
This commit is contained in:
1 parent
d62ca080e9
commit
5b918d6e3e
16 files changed
+317
-33
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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
@@ -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.',
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
@@ -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.
File renamed without changes.
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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: '🇭',
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user