feat: extracted renderer from /stats

This commit is contained in:
TresAbhi committed 2023-06-23 01:35:15 +00:00
1 parent bcbc1e259f
commit 752f9b339b
21 files changed
+329 -273

No files matched your search

+2 -2
View File
@@ -1,5 +1,4 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWN8 from '../core/blitz/getWN8.js'; import getWN8 from '../core/blitz/getWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import sumStats from '../core/blitz/sumStats.js'; import sumStats from '../core/blitz/sumStats.js';
@@ -12,6 +11,7 @@ import cleanTable, { TableInput } from '../core/interaction/cleanTable.js';
import negativeEmbed from '../core/interaction/negativeEmbed.js'; import negativeEmbed from '../core/interaction/negativeEmbed.js';
import positiveEmbed from '../core/interaction/positiveEmbed.js'; import positiveEmbed from '../core/interaction/positiveEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js'; import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo } from '../types/accountInfo.js'; import { AccountInfo } from '../types/accountInfo.js';
@@ -48,7 +48,7 @@ export default {
async execute(interaction) { async execute(interaction) {
const clan = interaction.options.getString('clan') as SkilledClan; const clan = interaction.options.getString('clan') as SkilledClan;
const { id, server } = await getBlitzAccount(interaction); const { id, server } = await resolvePlayer(interaction);
const accountInfo = await getWargamingResponse<AccountInfo>( const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
); );
+5 -6
View File
@@ -7,7 +7,6 @@ import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js'; import { BLITZ_SERVERS } from '../constants/servers.js';
import tanksAutocomplete from '../core/autocomplete/tanks.js'; import tanksAutocomplete from '../core/autocomplete/tanks.js';
import usernameAutocomplete from '../core/autocomplete/username.js'; import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js'; import resolveTankId from '../core/blitz/resolveTankId.js';
import resolveTankName from '../core/blitz/resolveTankName.js'; import resolveTankName from '../core/blitz/resolveTankName.js';
@@ -20,7 +19,8 @@ import {
RELATIVE_PERIOD_NAMES, RELATIVE_PERIOD_NAMES,
} from '../core/options/addPeriodSubCommands.js'; } from '../core/options/addPeriodSubCommands.js';
import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js'; import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js';
import getPeriodDataFromSubcommand from '../core/options/getPeriodDataFromSubcommand.js'; import resolvePeriod from '../core/options/resolvePeriod.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo } from '../types/accountInfo.js'; import { AccountInfo } from '../types/accountInfo.js';
@@ -29,7 +29,7 @@ import { PlayerClanData } from '../types/playerClanData.js';
export default { export default {
inProduction: true, inProduction: true,
inDevelopment: true, inDevelopment: false,
inPublic: true, inPublic: true,
command: addStatsSubCommandGroups( command: addStatsSubCommandGroups(
@@ -40,7 +40,7 @@ export default {
async execute(interaction) { async execute(interaction) {
const commandGroup = interaction.options.getSubcommandGroup()!; const commandGroup = interaction.options.getSubcommandGroup()!;
const { id, server } = await getBlitzAccount(interaction); const { id, server } = await resolvePlayer(interaction);
let nameDiscriminator: string | undefined; let nameDiscriminator: string | undefined;
let image: string | undefined; let image: string | undefined;
const tankIdRaw = interaction.options.getString('tank')!; const tankIdRaw = interaction.options.getString('tank')!;
@@ -65,8 +65,7 @@ export default {
let histories: Histories; let histories: Histories;
const { evolutionName, start, end } = const { evolutionName, start, end } = resolvePeriod(interaction);
getPeriodDataFromSubcommand(interaction);
if (commandGroup === 'player') { if (commandGroup === 'player') {
histories = await getPlayerHistories(server, id, { histories = await getPlayerHistories(server, id, {
+2 -2
View File
@@ -6,7 +6,6 @@ import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js'; import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js'; import { BLITZ_SERVERS } from '../constants/servers.js';
import usernameAutocomplete from '../core/autocomplete/username.js'; import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getTankStats from '../core/blitz/getTankStats.js'; import getTankStats from '../core/blitz/getTankStats.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import { import {
@@ -15,6 +14,7 @@ import {
tankopedia, tankopedia,
} from '../core/blitz/tankopedia.js'; } from '../core/blitz/tankopedia.js';
import addUsernameOption from '../core/options/addUsernameOption.js'; import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo } from '../types/accountInfo.js'; import { AccountInfo } from '../types/accountInfo.js';
@@ -71,7 +71,7 @@ export default {
async execute(interaction) { async execute(interaction) {
const tier = Number(interaction.options.getString('tier')); const tier = Number(interaction.options.getString('tier'));
const account = await getBlitzAccount(interaction); const account = await resolvePlayer(interaction);
const { id, server } = account; const { id, server } = account;
const accountInfo = await getWargamingResponse<AccountInfo>( const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
+2 -2
View File
@@ -2,11 +2,11 @@ import { SlashCommandBuilder } from 'discord.js';
import { startCase } from 'lodash'; import { startCase } from 'lodash';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import usernameAutocomplete from '../core/autocomplete/username.js'; import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import cleanTable, { TableInputEntry } from '../core/interaction/cleanTable.js'; import cleanTable, { TableInputEntry } from '../core/interaction/cleanTable.js';
import infoEmbed from '../core/interaction/infoEmbed.js'; import infoEmbed from '../core/interaction/infoEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js'; import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountAchievements } from '../types/accountAchievements.js'; import { AccountAchievements } from '../types/accountAchievements.js';
@@ -35,7 +35,7 @@ export default {
async execute(interaction) { async execute(interaction) {
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy; const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
const account = await getBlitzAccount(interaction); const account = await resolvePlayer(interaction);
const { id, server } = account; const { id, server } = account;
const accounts = await getWargamingResponse<AccountInfo>( const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
+2 -2
View File
@@ -1,11 +1,11 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import usernameAutocomplete from '../core/autocomplete/username.js'; import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import cleanTable from '../core/interaction/cleanTable.js'; import cleanTable from '../core/interaction/cleanTable.js';
import infoEmbed from '../core/interaction/infoEmbed.js'; import infoEmbed from '../core/interaction/infoEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js'; import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo } from '../types/accountInfo.js'; import { AccountInfo } from '../types/accountInfo.js';
@@ -21,7 +21,7 @@ export default {
.addStringOption(addUsernameOption), .addStringOption(addUsernameOption),
async execute(interaction) { async execute(interaction) {
const account = await getBlitzAccount(interaction); const account = await resolvePlayer(interaction);
const { id, server } = account; const { id, server } = account;
const accounts = await getWargamingResponse<AccountInfo>( const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
+10 -135
View File
@@ -1,32 +1,12 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import GenericAllStats from '../components/GenericAllStats.js';
import NoData, { NoDataType } from '../components/NoData.js';
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
import TierWeights, { TierWeightsRecord } from '../components/TierWeights.js';
import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js';
import tanksAutocomplete from '../core/autocomplete/tanks.js'; import tanksAutocomplete from '../core/autocomplete/tanks.js';
import usernameAutocomplete from '../core/autocomplete/username.js'; import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWN8 from '../core/blitz/getWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js'; import resolveTankId from '../core/blitz/resolveTankId.js';
import resolveTankName from '../core/blitz/resolveTankName.js';
import sumStats from '../core/blitz/sumStats.js';
import { Tier, tankopedia } from '../core/blitz/tankopedia.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js'; import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js';
import getPeriodDataFromSubcommand from '../core/options/getPeriodDataFromSubcommand.js'; import resolvePeriod from '../core/options/resolvePeriod.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import resolvePlayer from '../core/options/resolvePlayer.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { import stats, { StatType } from '../renderers/stats.js';
AccountInfo,
AllStats,
SupplementaryStats,
} from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
export default { export default {
inProduction: true, inProduction: true,
@@ -40,120 +20,15 @@ export default {
), ),
async execute(interaction) { async execute(interaction) {
const commandGroup = interaction.options.getSubcommandGroup()!; const commandGroup = interaction.options.getSubcommandGroup(
const { id, server } = await getBlitzAccount(interaction); true,
let nameDiscriminator: string | undefined; ) as StatType;
let image: string | undefined; const player = await resolvePlayer(interaction);
const period = resolvePeriod(interaction);
const tankIdRaw = interaction.options.getString('tank')!; const tankIdRaw = interaction.options.getString('tank')!;
const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : 0;
if (commandGroup === 'player') { return await stats(commandGroup, period, player, tankId);
const clan = (
await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
)
)[id]?.clan;
if (clan) nameDiscriminator = `[${clan.tag}]`;
image = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
} else if (commandGroup === 'tank') {
const tankId = resolveTankId(tankIdRaw);
nameDiscriminator = `(${resolveTankName(tankId)})`;
image = tankopedia[tankId].images.normal;
}
const { statsName, start, end } = getPeriodDataFromSubcommand(interaction);
const tankStats = await getTankStatsOverTime(server, id, start, end);
let stats: AllStats;
let supplementaryStats: SupplementaryStats;
let tierWeights: TierWeightsRecord;
if (commandGroup === 'player') {
const entries = Object.entries(tankStats);
stats = sumStats(entries.map(([, stats]) => stats));
const battles = entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankAverage = tankAverages[tankId];
return tankAverage ? accumulator + stats.battles : accumulator;
}, 0);
supplementaryStats = {
WN8:
entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankAverage = tankAverages[tankId];
return tankAverage
? accumulator + getWN8(tankAverage.all, stats) * stats.battles
: accumulator;
}, 0) / battles,
tier:
entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankAverage = tankAverages[tankId];
return tankAverage
? accumulator + tankopedia[tankId].tier * stats.battles
: accumulator;
}, 0) / battles,
};
tierWeights = entries.reduce<TierWeightsRecord>(
(accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tier = tankopedia[tankId].tier as Tier;
if (accumulator[tier]) {
accumulator[tier]! += stats.battles;
} else {
accumulator[tier] = stats.battles;
}
return accumulator;
},
{},
);
} else {
const tankId = resolveTankId(tankIdRaw);
stats = tankStats[tankId];
supplementaryStats = {
WN8: getWN8(tankAverages[tankId].all, tankStats[tankId]),
tier: tankopedia[tankId].tier,
};
}
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
return (
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={nameDiscriminator}
image={image}
description={`${statsName} • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`}
/>
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{stats.battles > 0 && commandGroup === 'player' && (
<TierWeights weights={tierWeights!} />
)}
{stats.battles > 0 && (
<GenericAllStats
stats={stats}
supplementaryStats={supplementaryStats}
/>
)}
<PoweredByBlitzStars />
</Wrapper>
);
}, },
autocomplete: (interaction) => { autocomplete: (interaction) => {
+7 -6
View File
@@ -6,7 +6,6 @@ import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js'; import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js'; import { BLITZ_SERVERS } from '../constants/servers.js';
import usernameAutocomplete from '../core/autocomplete/username.js'; import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getTankStats from '../core/blitz/getTankStats.js'; import getTankStats from '../core/blitz/getTankStats.js';
import getWN8 from '../core/blitz/getWN8.js'; import getWN8 from '../core/blitz/getWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
@@ -14,19 +13,21 @@ import resolveTankName from '../core/blitz/resolveTankName.js';
import sumStats from '../core/blitz/sumStats.js'; import sumStats from '../core/blitz/sumStats.js';
import { tankopedia } from '../core/blitz/tankopedia.js'; import { tankopedia } from '../core/blitz/tankopedia.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js'; import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import last5AM from '../core/blitzstars/last5AM.js'; import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js'; import { tankAverages } from '../core/blitzstars/tankAverages.js';
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js'; import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js'; import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
import addUsernameOption from '../core/options/addUsernameOption.js'; import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo, AllStats } from '../types/accountInfo.js'; import { AccountInfo, AllStats } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js'; import { PlayerClanData } from '../types/playerClanData.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
export default { export default {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -35,12 +36,12 @@ export default {
.addStringOption(addUsernameOption), .addStringOption(addUsernameOption),
async execute(interaction) { async execute(interaction) {
const { id, server } = await getBlitzAccount(interaction); const { id, server } = await resolvePlayer(interaction);
const tankStatsOverTime = await getTankStatsOverTime( const tankStatsOverTime = await getTankStatsOverTime(
server, server,
id, id,
last5AM().getTime() / 1000, getTimeDaysAgo(0),
new Date().getTime() / 1000, getPeriodNow(),
); );
const accountInfo = await getWargamingResponse<AccountInfo>( const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
+2 -2
View File
@@ -2,11 +2,11 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' }; import discord from '../../discord.json' assert { type: 'json' };
import usernameAutocomplete from '../core/autocomplete/username.js'; import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import negativeEmbed from '../core/interaction/negativeEmbed.js'; import negativeEmbed from '../core/interaction/negativeEmbed.js';
import positiveEmbed from '../core/interaction/positiveEmbed.js'; import positiveEmbed from '../core/interaction/positiveEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js'; import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js'; import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo } from '../types/accountInfo.js'; import { AccountInfo } from '../types/accountInfo.js';
@@ -23,7 +23,7 @@ export default {
.addStringOption((option) => addUsernameOption(option).setRequired(true)), .addStringOption((option) => addUsernameOption(option).setRequired(true)),
async execute(interaction) { async execute(interaction) {
const blitzAccount = await getBlitzAccount(interaction); const blitzAccount = await resolvePlayer(interaction);
const { id, server } = blitzAccount; const { id, server } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>( const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
+1 -1
View File
@@ -1,7 +1,7 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { BlitzServer } from '../../constants/servers.js'; import { BlitzServer } from '../../constants/servers.js';
import { serverAndIdPattern } from '../options/resolvePlayer.js';
import errorWithCause from '../process/errorWithCause.js'; import errorWithCause from '../process/errorWithCause.js';
import { serverAndIdPattern } from './getBlitzAccount.js';
import listClansPanServer from './listClansPanServer.js'; import listClansPanServer from './listClansPanServer.js';
export default async function getBlitzClan( export default async function getBlitzClan(
+1 -1
View File
@@ -1,3 +1,3 @@
export default function getPeriodNow() { export default function getPeriodNow() {
return new Date().getTime() / 1000; return Date.now() / 1000;
} }
+1 -1
View File
@@ -7,6 +7,6 @@ export default function getPeriodStart(period: Exclude<Period, 'custom'>) {
} else if (period === 'today') { } else if (period === 'today') {
return getTimeDaysAgo(0); return getTimeDaysAgo(0);
} else { } else {
return getTimeDaysAgo(Number(period)); return getTimeDaysAgo(parseInt(period));
} }
} }
+1 -1
View File
@@ -88,5 +88,5 @@ export default async function getPlayerHistories(
} }
} }
return [...latestNodes, ...filtered, ...previousNodes] satisfies Histories; return [...previousNodes, ...filtered, ...latestNodes] satisfies Histories;
} }
+1 -1
View File
@@ -109,5 +109,5 @@ export default async function getTankHistories(
}); });
} }
return [...latestNodes, ...filtered, ...previousNodes] satisfies Histories; return [...previousNodes, ...filtered, ...latestNodes] satisfies Histories;
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { BlitzServer } from '../../constants/servers.js'; import { BlitzServer } from '../../constants/servers.js';
import { AllStats } from '../../types/accountInfo.js'; import { AllStats } from '../../types/accountInfo.js';
import getTankHistories, { import getTankHistories, {
TankHistoriesRaw, TankHistories,
TankHistoryRaw, TankHistoryRaw,
} from './getTankHistories.js'; } from './getTankHistories.js';
@@ -45,7 +45,7 @@ export default async function getTankStatsOverTime(
}); });
// sort them even though most won't be used // sort them even though most won't be used
const tankSortedHistory: Record<number, TankHistoriesRaw> = {}; const tankSortedHistory: Record<number, TankHistories> = {};
const tankIds: number[] = []; const tankIds: number[] = [];
history.forEach((node) => { history.forEach((node) => {
if (!tankSortedHistory[node.tank_id]) { if (!tankSortedHistory[node.tank_id]) {
+9
View File
@@ -0,0 +1,9 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
export default function generateCustomId(
interaction: ChatInputCommandInteraction<CacheType>,
) {
return `${interaction.commandName}/${
interaction.options.getSubcommandGroup() ?? ''
}/${interaction.options.getSubcommand() ?? ''}`;
}
+7 -7
View File
@@ -2,15 +2,15 @@ import {
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder, SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { PERIOD_NAMES } from './getPeriodDataFromSubcommand.js'; import { PERIOD_NAMES } from './resolvePeriod.js';
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom'; export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom';
export const RELATIVE_PERIOD_NAMES: Record<Period, string> = { export const RELATIVE_PERIOD_NAMES: Record<Period, string> = {
today: 'A day ago', today: 'A day ago',
'30': '30 days ago', 30: '30 days ago',
'60': '60 days ago', 60: '60 days ago',
'90': '90 days ago', 90: '90 days ago',
career: 'Extent of data', career: 'Extent of data',
custom: 'Custom period', custom: 'Custom period',
}; };
@@ -26,13 +26,13 @@ export default function addPeriodSubCommands(
extra(option.setName('today').setDescription(PERIOD_NAMES.today)), extra(option.setName('today').setDescription(PERIOD_NAMES.today)),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('30').setDescription(PERIOD_NAMES['30'])), extra(option.setName('30').setDescription(PERIOD_NAMES[30])),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('60').setDescription(PERIOD_NAMES['60'])), extra(option.setName('60').setDescription(PERIOD_NAMES[60])),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('90').setDescription(PERIOD_NAMES['90'])), extra(option.setName('90').setDescription(PERIOD_NAMES[90])),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('career').setDescription(PERIOD_NAMES.career)), extra(option.setName('career').setDescription(PERIOD_NAMES.career)),
@@ -6,23 +6,30 @@ import { Period } from './addPeriodSubCommands.js';
export const PERIOD_NAMES: Record<Period, string> = { export const PERIOD_NAMES: Record<Period, string> = {
today: "Today's statistics", today: "Today's statistics",
'30': "30 days' statistics", 30: "30 days' statistics",
'60': "60 days' statistics", 60: "60 days' statistics",
'90': "90 days' statistics", 90: "90 days' statistics",
career: 'Career statistics', career: 'Career statistics',
custom: 'Custom period', custom: 'Custom period',
}; };
export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = { export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = {
today: "Today's evolution", today: "Today's evolution",
'30': "30 days' evolution", 30: "30 days' evolution",
'60': "60 days' evolution", 60: "60 days' evolution",
'90': "90 days' evolution", 90: "90 days' evolution",
career: 'Career evolution', career: 'Career evolution',
custom: 'Custom period', custom: 'Custom period',
}; };
export default function getPeriodDataFromSubcommand( export interface ResolvedPeriod {
statsName: string;
evolutionName: string;
start: number;
end: number;
}
export default function resolvePeriod(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) { ) {
const period = interaction.options.getSubcommand(true) as Period; const period = interaction.options.getSubcommand(true) as Period;
@@ -48,5 +55,5 @@ export default function getPeriodDataFromSubcommand(
end = getPeriodNow(); end = getPeriodNow();
} }
return { statsName, evolutionName, start, end }; return { statsName, evolutionName, start, end } satisfies ResolvedPeriod;
} }
@@ -1,13 +1,18 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { BlitzServer } from '../../constants/servers.js'; import { BlitzServer } from '../../constants/servers.js';
import errorWithCause from '../process/errorWithCause.js';
import listAccountsPanServer, { import listAccountsPanServer, {
usernamePatternWithoutPosition, usernamePatternWithoutPosition,
} from './listAccountsPanServer.js'; } from '../blitz/listAccountsPanServer.js';
import errorWithCause from '../process/errorWithCause.js';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export default async function getBlitzAccount( export interface ResolvedPlayer {
server: BlitzServer;
id: number;
}
export default async function resolvePlayer(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) { ) {
const commandUsername = interaction.options.getString('username'); const commandUsername = interaction.options.getString('username');
@@ -25,12 +30,18 @@ export default async function getBlitzAccount(
if (serverAndIdPattern.test(username)) { if (serverAndIdPattern.test(username)) {
const [server, accountId] = username.split('/'); const [server, accountId] = username.split('/');
return { server: server as BlitzServer, id: Number(accountId) }; return {
server: server as BlitzServer,
id: Number(accountId),
} satisfies ResolvedPlayer;
} else { } else {
const accounts = await listAccountsPanServer(username); const accounts = await listAccountsPanServer(username);
if (accounts[0]) { if (accounts[0]) {
return { server: accounts[0].server, id: accounts[0].account_id }; return {
server: accounts[0].server,
id: accounts[0].account_id,
} satisfies ResolvedPlayer;
} else { } else {
throw errorWithCause( throw errorWithCause(
'Could not find user', 'Could not find user',
+102 -88
View File
@@ -2,6 +2,7 @@ import {
ActionRowBuilder, ActionRowBuilder,
AutocompleteInteraction, AutocompleteInteraction,
ButtonBuilder, ButtonBuilder,
ButtonInteraction,
ButtonStyle, ButtonStyle,
CacheType, CacheType,
ChatInputCommandInteraction, ChatInputCommandInteraction,
@@ -140,98 +141,111 @@ try {
console.error(error); console.error(error);
} }
//#region Event type handlers
function handleAutocomplete(interaction: AutocompleteInteraction<CacheType>) {
const command = commands[interaction.commandName];
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
if (command.autocomplete) command.autocomplete(interaction);
}
async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const command = commands[interaction.commandName];
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
const metadata = [
`${interaction.user.id} (${interaction.user.username})`,
`${interaction.guildId}/${interaction.channelId} (${interaction.guild?.name})`,
new Date(),
];
console.log(
`${interaction.toString()}\n${metadata
.map((item) => ` ${item}`)
.join('\n')}`,
);
await interaction.deferReply();
try {
const result = await command.execute(interaction);
if (command.handlesInteraction) return;
const normalizedResult = Array.isArray(result) ? result : [result];
const reply: InteractionEditReplyOptions = {};
if (psa.data) {
normalizedResult.push(warningEmbed(psa.data.title, psa.data.description));
}
await Promise.all(
normalizedResult.map(async (item) => {
if (item instanceof EmbedBuilder) {
if (!reply.embeds) reply.embeds = [];
reply.embeds.push(item);
} else if (item instanceof ButtonBuilder) {
if (!reply.components)
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
(
reply.components[0] as ActionRowBuilder<ButtonBuilder>
).addComponents(item);
} else {
const image = await render(item!);
if (!reply.files) reply.files = [];
reply.files.push(image);
}
}),
);
await interaction.editReply(reply);
} catch (error) {
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setLabel('Get Help on Discord Server')
.setURL('https://discord.gg/nDt7AjGJQH')
.setStyle(ButtonStyle.Link),
);
await interaction.editReply({
embeds: [
negativeEmbed(
(error as Error).message,
`${(error as Error).cause ?? 'No further information is available.'}`,
),
],
components: [actionRow],
});
handleError(error as Error, interaction.commandName);
}
}
function handleButton(interaction: ButtonInteraction<CacheType>) {
console.log(interaction.customId);
interaction.message.edit({
files: [],
});
}
//#endregion
export default async function interactionCreate( export default async function interactionCreate(
interaction: Interaction<CacheType>, interaction: Interaction<CacheType>,
) { ) {
if (interaction.isAutocomplete()) { if (interaction.isAutocomplete()) {
const command = commands[interaction.commandName]; handleAutocomplete(interaction);
if (!command) {
console.error(
`No command matching ${interaction.commandName} was found.`,
);
return;
}
if (command.autocomplete) command.autocomplete(interaction);
} else if (interaction.isChatInputCommand()) { } else if (interaction.isChatInputCommand()) {
const command = commands[interaction.commandName]; handleChatInputCommand(interaction);
} else if (interaction.isButton()) {
if (!command) { handleButton(interaction);
console.error(
`No command matching ${interaction.commandName} was found.`,
);
return;
}
const metadata = [
`${interaction.user.id} (${interaction.user.username})`,
`${interaction.guildId}/${interaction.channelId} (${interaction.guild?.name})`,
new Date(),
];
console.log(
`${interaction.toString()}\n${metadata
.map((item) => ` ${item}`)
.join('\n')}`,
);
await interaction.deferReply();
try {
const result = await command.execute(interaction);
if (command.handlesInteraction) return;
const normalizedResult = Array.isArray(result) ? result : [result];
const reply: InteractionEditReplyOptions = {};
if (psa.data) {
normalizedResult.push(
warningEmbed(psa.data.title, psa.data.description),
);
}
await Promise.all(
normalizedResult.map(async (item) => {
if (item instanceof EmbedBuilder) {
if (!reply.embeds) reply.embeds = [];
reply.embeds.push(item);
} else if (item instanceof ButtonBuilder) {
if (!reply.components)
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
(
reply.components[0] as ActionRowBuilder<ButtonBuilder>
).addComponents(item);
} else {
const image = await render(item!);
if (!reply.files) reply.files = [];
reply.files.push(image);
}
}),
);
await interaction.editReply(reply);
} catch (error) {
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setLabel('Get Help on Discord Server')
.setURL('https://discord.gg/nDt7AjGJQH')
.setStyle(ButtonStyle.Link),
);
await interaction.editReply({
embeds: [
negativeEmbed(
(error as Error).message,
`${
(error as Error).cause ?? 'No further information is available.'
}`,
),
],
components: [actionRow],
});
handleError(error as Error, interaction.commandName);
}
} }
} }
+140
View File
@@ -0,0 +1,140 @@
import GenericAllStats from '../components/GenericAllStats.js';
import NoData, { NoDataType } from '../components/NoData.js';
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
import TierWeights, { TierWeightsRecord } from '../components/TierWeights.js';
import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js';
import getWN8 from '../core/blitz/getWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankName from '../core/blitz/resolveTankName.js';
import sumStats from '../core/blitz/sumStats.js';
import { Tier, tankopedia } from '../core/blitz/tankopedia.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import { ResolvedPeriod } from '../core/options/resolvePeriod.js';
import { ResolvedPlayer } from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import {
AccountInfo,
AllStats,
SupplementaryStats,
} from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
export type StatType = 'player' | 'tank';
export default async function stats<Type extends StatType>(
type: Type,
{ start, end, statsName }: ResolvedPeriod,
{ server, id }: ResolvedPlayer,
tankId: Type extends 'tank' ? number : never,
) {
let nameDiscriminator: string | undefined;
let image: string | undefined;
if (type === 'player') {
const clan = (
await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
)
)[id]?.clan;
if (clan) nameDiscriminator = `[${clan.tag}]`;
image = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
} else if (type === 'tank') {
nameDiscriminator = `(${resolveTankName(tankId)})`;
image = tankopedia[tankId].images.normal;
}
const tankStats = await getTankStatsOverTime(server, id, start, end);
let stats: AllStats;
let supplementaryStats: SupplementaryStats;
let tierWeights: TierWeightsRecord;
if (type === 'player') {
const entries = Object.entries(tankStats);
stats = sumStats(entries.map(([, stats]) => stats));
const battles = entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankAverage = tankAverages[tankId];
return tankAverage ? accumulator + stats.battles : accumulator;
}, 0);
supplementaryStats = {
WN8:
entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankAverage = tankAverages[tankId];
return tankAverage
? accumulator + getWN8(tankAverage.all, stats) * stats.battles
: accumulator;
}, 0) / battles,
tier:
entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankAverage = tankAverages[tankId];
return tankAverage
? accumulator + tankopedia[tankId].tier * stats.battles
: accumulator;
}, 0) / battles,
};
tierWeights = entries.reduce<TierWeightsRecord>(
(accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tier = tankopedia[tankId].tier as Tier;
if (accumulator[tier]) {
accumulator[tier]! += stats.battles;
} else {
accumulator[tier] = stats.battles;
}
return accumulator;
},
{},
);
} else {
stats = tankStats[tankId];
supplementaryStats = {
WN8: getWN8(tankAverages[tankId].all, tankStats[tankId]),
tier: tankopedia[tankId].tier,
};
}
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
return (
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={nameDiscriminator}
image={image}
description={`${statsName} • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`}
/>
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{stats.battles > 0 && type === 'player' && (
<TierWeights weights={tierWeights!} />
)}
{stats.battles > 0 && (
<GenericAllStats
stats={stats}
supplementaryStats={supplementaryStats}
/>
)}
<PoweredByBlitzStars />
</Wrapper>
);
}
+1 -1
View File
@@ -11,5 +11,5 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts", "src/renderers/stats.tsx"]
} }