feat: updated /tankstats

This commit is contained in:
TresAbhi committed 2023-05-30 17:24:24 +00:00
1 parent 50369e9225
commit 831300cf69
13 files changed
+300 -272

No files matched your search

+2 -2
View File
@@ -79,8 +79,8 @@ export default {
: tanks
.map(
(tank) =>
`${TANK_TYPE_EMOJIS[tank.type]} ${resolveTankName(
tank,
`${TANK_TYPE_EMOJIS[tank.type]} ${markdownEscape(
resolveTankName(tank.tank_id),
)} ${tank.is_premium ? '⭐' : ''}${
COMP_TANKS.includes(tank.tank_id) ? '🏆' : ''
}`,
+10 -33
View File
@@ -15,8 +15,14 @@ import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import sumStats from '../core/blitz/sumStats.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import cmdName from '../core/interaction/cmdName.js';
import addStatPeriodChoices, {
StatPeriod,
statPeriodNames,
} from '../core/options/addStatPeriodChoices.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import { args } from '../core/process/args.js';
import render from '../core/ui/render.js';
@@ -24,36 +30,15 @@ import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo, AllStats } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
export type StatPeriod = 'today' | '30' | '60' | '90' | 'career';
export const statPeriodNames: Record<StatPeriod, string> = {
today: "Today's statistics",
30: '30-day statistics',
60: '60-day statistics',
90: '90-day statistics',
career: 'Career statistics',
};
export default {
inProduction: true,
inDevelopment: true,
inDevelopment: false,
inPublic: true,
command: new SlashCommandBuilder()
.setName(cmdName('stats'))
.setDescription("Gets the user's in-game statistics")
.addStringOption((option) =>
option
.setName('period')
.setDescription('The last number of days of stats')
.setChoices(
{ name: 'Today', value: 'today' satisfies StatPeriod },
{ name: '30 Days', value: '30' satisfies StatPeriod },
{ name: '60 Days', value: '60' satisfies StatPeriod },
{ name: '90 Days', value: '90' satisfies StatPeriod },
{ name: 'Career', value: 'career' satisfies StatPeriod },
)
.setRequired(true),
)
.addStringOption(addStatPeriodChoices)
.addStringOption(addUsernameOption),
async execute(interaction) {
@@ -72,19 +57,13 @@ export default {
if (period === 'career') {
stats = accountInfo[id].statistics.all;
} else {
const daysAgo = period === 'today' ? 0 : Number(period) - 1;
const periodStart = new Date();
periodStart.setDate(periodStart.getDate() - daysAgo);
periodStart.setHours(5, 0, 0, 0);
stats = sumStats(
Object.entries(
await getTankStatsOverTime(
server,
id,
periodStart.getTime() / 1000,
new Date().getTime() / 1000,
getPeriodicStart(period),
getPeriodNow(),
),
).map(([, stats]) => stats),
);
@@ -115,7 +94,6 @@ export default {
'Winrate',
`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
],
// ['WN8', stats.wn8.toFixed(0)],
[
'Survival',
`${(100 * (stats.survived_battles / stats.battles)).toFixed(
@@ -135,7 +113,6 @@ export default {
['Average hits', (stats.hits / stats.battles).toFixed(2)],
['Average kills', (stats.frags / stats.battles).toFixed(2)],
['Average spots', (stats.spotted / stats.battles).toFixed(2)],
// ['Average tier', stats.avg_tier.toFixed(2)],
[
'Damage ratio',
(stats.damage_dealt / stats.damage_received).toFixed(2),
-174
View File
@@ -1,174 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import tanksAutocomplete from '../core/autocomplete/tanks.js';
import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import { tankopedia } from '../core/blitzstars/tankopedia.js';
import getPeriodicStats, {
Period,
} from '../core/blitztankstats/getPeriodicStats.js';
import cleanTable from '../core/interaction/cleanTable.js';
import cmdName from '../core/interaction/cmdName.js';
import errorEmbed from '../core/interaction/errorEmbed.js';
import infoEmbed from '../core/interaction/infoEmbed.js';
import addTankChoices from '../core/options/addTankChoices.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';
import resolveTankName from '../utilities/resolveTankName.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(addUsernameOption),
async execute(interaction) {
const name = interaction.options.getString('username')!;
const tankId = interaction.options.getString('tank')!;
const period = interaction.options.getString('period') as Periods;
const blitzAccount = await getBlitzAccount(interaction, name);
const { server, id } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
);
const tank = tankopedia[tankId as unknown as number];
if (tank) {
const periodicStats = (await getPeriodicStats(id, Number(period)))
.filter((stats) => stats.tank_id === tank.tank_id)
.filter((stats) => !stats.isRating);
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: [
infoEmbed(
`${period} day stats for ${markdownEscape(
accountInfo[id].nickname,
)}'s ${resolveTankName(tank)}`,
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: (interaction) => {
tanksAutocomplete(interaction);
usernameAutocomplete(interaction);
},
} satisfies CommandRegistry;
+156
View File
@@ -0,0 +1,156 @@
import {
ActionRowBuilder,
ButtonBuilder,
SlashCommandBuilder,
} from 'discord.js';
import GenericStats from '../components/GenericStats/index.js';
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js';
import fullBlitzStarsStats from '../core/actions/fullBlitzStarsStats.js';
import { supportBlitzStars } from '../core/actions/supportBlitzStars.js';
import tanksAutocomplete from '../core/autocomplete/tanks.js';
import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import { tankopedia } from '../core/blitzstars/tankopedia.js';
import cmdName from '../core/interaction/cmdName.js';
import addStatPeriodChoices, {
StatPeriod,
statPeriodNames,
} from '../core/options/addStatPeriodChoices.js';
import addTankChoices from '../core/options/addTankChoices.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import { args } from '../core/process/args.js';
import render from '../core/ui/render.js';
import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo, AllStats } from '../types/accountInfo.js';
import { TanksStats } from '../types/tanksStats.js';
import resolveTankName from '../utilities/resolveTankName.js';
export default {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName(cmdName('tankstats'))
.setDescription('Stats for a tank over a period')
.addStringOption(addTankChoices)
.addStringOption(addStatPeriodChoices)
.addStringOption(addUsernameOption),
async execute(interaction) {
const tank = await resolveTankId(
interaction,
interaction.options.getString('tank')!,
);
const period = interaction.options.getString('period') as StatPeriod;
const username = interaction.options.getString('username')!;
const blitzAccount = await getBlitzAccount(interaction, username);
const { id, server } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
);
const tankStats = await getWargamingResponse<TanksStats>(
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
);
let stats: AllStats;
if (period === 'career') {
stats = tankStats[id].find(
(stats) => stats.tank_id === Number(tank),
)!.all;
} else {
stats = (
await getTankStatsOverTime(
server,
id,
getPeriodicStart(period),
getPeriodNow(),
)
)[Number(tank)];
}
const image = await render(
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={`(${resolveTankName(Number(tank))})`}
image={tankopedia[Number(tank)].images.normal}
description={`${
statPeriodNames[period]
} • ${new Date().toDateString()} • ${BLITZ_SERVERS[server]}`}
/>
{stats.battles === 0 && <NoBattlesInPeriod />}
{stats.battles > 0 && (
// TODO: GenericStats generator for AllStats
<GenericStats
stats={[
[
'Winrate',
`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
],
[
'Survival',
`${(100 * (stats.survived_battles / stats.battles)).toFixed(
2,
)}%`,
],
['Accuracy', `${((stats.hits / stats.shots) * 100).toFixed(2)}%`],
['Battles', stats.battles],
['Wins', stats.wins],
['Losses', stats.losses],
[
'Average damage',
(stats.damage_dealt / stats.battles).toFixed(0),
],
['Average XP', (stats.xp / stats.battles).toFixed(0)],
['Average shots', (stats.shots / stats.battles).toFixed(2)],
['Average hits', (stats.hits / stats.battles).toFixed(2)],
['Average kills', (stats.frags / stats.battles).toFixed(2)],
['Average spots', (stats.spotted / stats.battles).toFixed(2)],
[
'Damage ratio',
(stats.damage_dealt / stats.damage_received).toFixed(2),
],
[
'Kills to death ratio',
(
stats.frags /
(stats.battles - stats.survived_battles)
).toFixed(2),
],
]}
/>
)}
<PoweredByBlitzStars />
</Wrapper>,
);
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
fullBlitzStarsStats(server, accountInfo[id].nickname),
supportBlitzStars,
);
await interaction.editReply({
files: [image],
components: [actionRow],
});
console.log(`Showing stats for ${accountInfo[id].nickname}`);
},
autocomplete: (interaction) => {
tanksAutocomplete(interaction);
usernameAutocomplete(interaction);
},
} satisfies CommandRegistry;
+2 -9
View File
@@ -30,7 +30,7 @@ import resolveTankName from '../utilities/resolveTankName.js';
export default {
inProduction: true,
inDevelopment: true,
inDevelopment: false,
inPublic: true,
command: new SlashCommandBuilder()
@@ -82,14 +82,7 @@ export default {
return (
<Breakdown.Row
key={tankId}
name={
tankId === '0'
? 'Today'
: resolveTankName({
tank_id: tankId as unknown as number,
name: tankopedia[tankId as unknown as number].name,
})
}
name={tankId === '0' ? 'Today' : resolveTankName(Number(tankId))}
winrate={tankStats.wins / tankStats.battles}
careerWinrate={career.wins / career.battles}
WN8={-Infinity}
+37 -11
View File
@@ -17,29 +17,55 @@ export default function TitleBar({
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ display: 'flex', gap: 8 }}>
{image && <img style={{ width: 64, height: 64 }} src={image} />}
<div style={{ display: 'flex', gap: 8, flex: 1 }}>
{image && (
<img
style={{ width: 64, height: 64, objectFit: 'contain' }}
src={image}
/>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<span
<div
style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}
>
<div
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
display: 'flex',
gap: 4,
}}
>
{name}
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{name}
</span>
{nameDiscriminator && (
<span style={{ color: theme.colors.textLowContrast }}>
<span
style={{
flex: 1,
fontSize: 32,
color: theme.colors.textLowContrast,
fontWeight: 900,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{nameDiscriminator}
</span>
)}
</span>
</div>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{description}
+3 -1
View File
@@ -18,7 +18,9 @@ export default async function tanksAutocomplete(
? go(focusedOption.value, tanks, { keys: ['name'], limit: 10 }).map(
(item) =>
({
name: item.obj.name ? item.obj.name : resolveTankName(item.obj),
name: item.obj.name
? item.obj.name
: resolveTankName(item.obj.tank_id),
value: `${item.obj.tank_id}`,
} satisfies ApplicationCommandOptionChoiceData<string>),
)
+45
View File
@@ -0,0 +1,45 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { go } from 'fuzzysort';
import { tankopedia, tanks } from '../blitzstars/tankopedia.js';
import errorEmbed from '../interaction/errorEmbed.js';
export default async function resolveTankId(
interaction: ChatInputCommandInteraction<CacheType>,
tank: string,
) {
const number = Number(tank);
if (Number.isNaN(number)) {
const searchResult = go(tank, tanks, { keys: ['name'], limit: 1 });
if (searchResult.length === 0) {
await interaction.editReply({
embeds: [
errorEmbed(
'Tank not found',
`Could not find tank by the name "${tank}".`,
),
],
});
throw new Error(`Tank not found by name "${tank}"`);
} else {
return searchResult[0].obj.tank_id;
}
} else {
if (tankopedia[number]) {
return number;
} else {
await interaction.editReply({
embeds: [
errorEmbed(
'Tank not found',
`Could not find tank by the ID "${number}".`,
),
],
});
throw new Error(`Tank not found by ID "${number}"`);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export default function getPeriodNow() {
return new Date().getTime() / 1000;
}
+15
View File
@@ -0,0 +1,15 @@
import { StatPeriod } from '../options/addStatPeriodChoices.js';
export default function getPeriodicStart(period: StatPeriod) {
if (period === 'career') {
return -Infinity;
} else {
const daysAgo = period === 'today' ? 0 : Number(period) - 1;
const periodStart = new Date();
periodStart.setDate(periodStart.getDate() - daysAgo);
periodStart.setHours(5, 0, 0, 0);
return periodStart.getTime() / 1000;
}
}
@@ -1,36 +0,0 @@
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[];
}
+23
View File
@@ -0,0 +1,23 @@
import { SlashCommandStringOption } from 'discord.js';
export type StatPeriod = 'today' | '30' | '60' | '90' | 'career';
export const statPeriodNames: Record<StatPeriod, string> = {
today: "Today's statistics",
30: '30-day statistics',
60: '60-day statistics',
90: '90-day statistics',
career: 'Career statistics',
};
export default function addStatPeriodChoices(option: SlashCommandStringOption) {
return option
.setName('period')
.setDescription('The period to get stats for')
.setChoices(
{ name: 'Today', value: 'today' satisfies StatPeriod },
{ name: '30 Days', value: '30' satisfies StatPeriod },
{ name: '60 Days', value: '60' satisfies StatPeriod },
{ name: '90 Days', value: '90' satisfies StatPeriod },
{ name: 'Career', value: 'career' satisfies StatPeriod },
)
.setRequired(true);
}
+4 -6
View File
@@ -1,8 +1,6 @@
import markdownEscape from 'markdown-escape';
import { tankopedia } from '../core/blitzstars/tankopedia.js';
export default function resolveTankName(tank: {
tank_id: number;
name?: string;
}) {
return tank.name ? markdownEscape(tank.name) : `Unknown Tank ${tank.tank_id}`;
export default function resolveTankName(tankId: number) {
const tankopediaName = tankopedia[tankId].name;
return tankopediaName ? tankopediaName : `Unknown Tank ${tankId}`;
}