mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-25 03:29:51 +01: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": {
|
"dependencies": {
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
|
"fuzzysort": "^2.0.4",
|
||||||
"markdown-escape": "^2.0.0",
|
"markdown-escape": "^2.0.0",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AutocompleteInteraction,
|
||||||
CacheType,
|
CacheType,
|
||||||
ChatInputCommandInteraction,
|
ChatInputCommandInteraction,
|
||||||
Collection,
|
Collection,
|
||||||
@@ -21,6 +22,7 @@ export interface CommandRegistry {
|
|||||||
|
|
||||||
command: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
command: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
||||||
execute: (interaction: ChatInputCommandInteraction<CacheType>) => void;
|
execute: (interaction: ChatInputCommandInteraction<CacheType>) => void;
|
||||||
|
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rest = new REST().setToken(args['discord-token']);
|
const rest = new REST().setToken(args['discord-token']);
|
||||||
@@ -68,28 +70,46 @@ try {
|
|||||||
export default async function interactionCreate(
|
export default async function interactionCreate(
|
||||||
interaction: Interaction<CacheType>,
|
interaction: Interaction<CacheType>,
|
||||||
) {
|
) {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (interaction.isAutocomplete()) {
|
||||||
const command = commandCollection.get(interaction.commandName);
|
const command = commandCollection.get(interaction.commandName);
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
console.error(
|
||||||
return;
|
`No command matching ${interaction.commandName} was found.`,
|
||||||
}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await interaction.deferReply();
|
if (command.autocomplete) command.autocomplete(interaction);
|
||||||
command.execute(interaction);
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(error);
|
||||||
console.error(error);
|
}
|
||||||
if (interaction.replied || interaction.deferred) {
|
} else if (interaction.isChatInputCommand()) {
|
||||||
await interaction.followUp({
|
const command = commandCollection.get(interaction.commandName);
|
||||||
content: 'There was an error while executing this command!',
|
|
||||||
ephemeral: true,
|
if (!command) {
|
||||||
});
|
console.error(
|
||||||
} else {
|
`No command matching ${interaction.commandName} was found.`,
|
||||||
await interaction.editReply({
|
);
|
||||||
content: 'There was an error while executing this command!',
|
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 addServerChoices from '../utilities/addServerChoices.js';
|
||||||
import { args } from '../utilities/args.js';
|
import { args } from '../utilities/args.js';
|
||||||
import blitzLinks from '../utilities/blitzLinks.js';
|
import blitzLinks from '../utilities/blitzLinks.js';
|
||||||
|
import cleanTable from '../utilities/cleanTable.js';
|
||||||
import cmdName from '../utilities/cmdName.js';
|
import cmdName from '../utilities/cmdName.js';
|
||||||
import getBlitzAccount from '../utilities/getBlitzAccount.js';
|
import getBlitzAccount from '../utilities/getBlitzAccount.js';
|
||||||
import getBlitzStarsAccount from '../utilities/getBlitzStarsAccount.js';
|
import getBlitzStarsAccount from '../utilities/getBlitzStarsAccount.js';
|
||||||
@@ -134,7 +135,7 @@ export default {
|
|||||||
period === 'today'
|
period === 'today'
|
||||||
? '**⚠ "Today" stats are in development. Some stats are not available/wrong.**\n\n'
|
? '**⚠ "Today" stats are in development. Some stats are not available/wrong.**\n\n'
|
||||||
: ''
|
: ''
|
||||||
}${[
|
}${cleanTable([
|
||||||
[
|
[
|
||||||
'Winrate',
|
'Winrate',
|
||||||
`${(100 * (stats.all.wins / stats.all.battles)).toFixed(
|
`${(100 * (stats.all.wins / stats.all.battles)).toFixed(
|
||||||
@@ -200,15 +201,7 @@ export default {
|
|||||||
(stats.all.battles - stats.all.survived_battles)
|
(stats.all.battles - stats.all.survived_battles)
|
||||||
).toFixed(2),
|
).toFixed(2),
|
||||||
],
|
],
|
||||||
]
|
])}\n\n${blitzLinks(server, blitzAccount.nickname)}`
|
||||||
.filter((array) => array[1] !== `${-Infinity}`)
|
|
||||||
.map((array) =>
|
|
||||||
array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`,
|
|
||||||
)
|
|
||||||
.join('\n')}\n\n${blitzLinks(
|
|
||||||
server,
|
|
||||||
blitzAccount.nickname,
|
|
||||||
)}`
|
|
||||||
: 'No battles played in this period.',
|
: '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 { 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) {
|
export default function errorEmbed(title: string, description: string) {
|
||||||
return new EmbedBuilder()
|
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 { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { notTrackedByBlitzStars } from '../embeds/notTrackedByBlitzStars.js';
|
import { notTrackedByBlitzStars } from '../core/embeds/notTrackedByBlitzStars.js';
|
||||||
import { PlayerPeriodicStatsCollection } from '../types/statistics.js';
|
import { PlayerPeriodicStatsCollection } from '../types/statistics.js';
|
||||||
|
|
||||||
export default async function getBlitzStarsAccount(
|
export default async function getBlitzStarsAccount(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface Tankopedia {
|
|||||||
};
|
};
|
||||||
data: {
|
data: {
|
||||||
[tankId: number]: {
|
[tankId: number]: {
|
||||||
name: string;
|
name?: string;
|
||||||
nation: string;
|
nation: string;
|
||||||
is_premium: boolean;
|
is_premium: boolean;
|
||||||
tier: number;
|
tier: number;
|
||||||
@@ -32,12 +32,19 @@ export const tankopedia = (await (
|
|||||||
await fetch('https://www.blitzstars.com/bs-tankopedia.json')
|
await fetch('https://www.blitzstars.com/bs-tankopedia.json')
|
||||||
).json()) as Tankopedia;
|
).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> = {
|
export const TANK_TYPE_NAMES: Record<TankType, string> = {
|
||||||
'AT-SPG': 'Tank Destroyer',
|
'AT-SPG': 'Tank Destroyer',
|
||||||
heavyTank: 'Heavy Tank',
|
heavyTank: 'Heavy Tank',
|
||||||
mediumTank: 'Medium Tank',
|
mediumTank: 'Medium Tank',
|
||||||
lightTank: 'Light Tank',
|
lightTank: 'Light Tank',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TANK_TYPE_EMOJIS: Record<TankType, string> = {
|
export const TANK_TYPE_EMOJIS: Record<TankType, string> = {
|
||||||
'AT-SPG': '🔽',
|
'AT-SPG': '🔽',
|
||||||
heavyTank: '🇭',
|
heavyTank: '🇭',
|
||||||
|
|||||||
@@ -311,6 +311,11 @@ fsevents@~2.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
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:
|
ieee754@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
|
||||||
|
|||||||
Reference in new issue
Block a user