feat: merged stats and tankstats

This commit is contained in:
TresAbhi committed 2023-06-19 17:35:28 +00:00
1 parent 0cf0e63fda
commit d9690460d5
14 files changed
+195 -338

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ build({
},
bundle: true,
sourcemap: isProd,
sourcemap: true,
minifyIdentifiers: false, // causes errors
minifySyntax: isProd,
minifyWhitespace: isProd,
+2 -2
View File
@@ -5,7 +5,7 @@ import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import sumStats from '../core/blitz/sumStats.js';
import { tankopedia } from '../core/blitz/tankopedia.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
import getPeriodStart from '../core/blitzstars/getPeriodStart.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import cleanTable, { TableInput } from '../core/interaction/cleanTable.js';
@@ -62,7 +62,7 @@ export default {
const tankStatsOverTime = await getTankStatsOverTime(
server,
id,
getPeriodicStart('30'),
getPeriodStart('30'),
getPeriodNow(),
);
const entries = Object.entries(tankStatsOverTime);
+1 -1
View File
@@ -4,7 +4,7 @@ import { CommandRegistry } from '../events/interactionCreate.js';
export default {
inProduction: true,
inDevelopment: true,
inDevelopment: false,
inPublic: true,
handlesInteraction: true,
+112 -122
View File
@@ -5,26 +5,26 @@ 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 tanksAutocomplete from '../core/autocomplete/tanks.js';
import usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getTankStats from '../core/blitz/getTankStats.js';
import getWN8 from '../core/blitz/getWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import resolveTankName from '../core/blitz/resolveTankName.js';
import sumStats from '../core/blitz/sumStats.js';
import { tankopedia } from '../core/blitz/tankopedia.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
import getPeriodStartFromDaysAgo from '../core/blitzstars/getPeriodStartFromDaysAgo.js';
import getPeriodStart from '../core/blitzstars/getPeriodStart.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import cmdName from '../core/interaction/cmdName.js';
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
import addCustomPeriodOption from '../core/options/addCustomPeriodOption.js';
import addPeriodChoices, {
StatPeriod,
statPeriodNames,
} from '../core/options/addPeriodChoices.js';
import addPeriodSubCommands, {
PERIOD_NAMES,
Period,
} from '../core/options/addPeriodSubCommands.js';
import addTankChoices from '../core/options/addTankChoices.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js';
@@ -41,152 +41,142 @@ export default {
inPublic: true,
command: new SlashCommandBuilder()
// TODO: embed cmdName into interaction create
.setName(cmdName('stats'))
.setDescription("Gets the user's in-game statistics")
.addSubcommand((option) =>
option
.setName('period')
.setDescription('Pick from a predetermined periods')
.addStringOption(addPeriodChoices)
.addStringOption(addUsernameOption),
.setDescription('In-game statistics')
.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameOption),
)
.setName('player')
.setDescription("Player's statistics"),
)
.addSubcommand((option) =>
addCustomPeriodOption(option)
.setName('customperiod')
.setDescription('Custom periods')
.addStringOption(addUsernameOption),
.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameOption),
)
.setName('tank')
.setDescription("Tank's statistics"),
),
async execute(interaction) {
const period = interaction.options.getString('period') as StatPeriod;
const commandGroup = interaction.options.getSubcommandGroup()!;
const { id, server } = await getBlitzAccount(interaction);
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
);
let stats: AllStats;
let supplementaryStats: SupplementaryStats | undefined;
let hasWN8AccumulatedAtAll = false;
const isCustomPeriod =
interaction.options.getSubcommand() === 'customperiod';
let periodName: string;
const startRaw = interaction.options.getInteger('start')!;
const endRaw = interaction.options.getInteger('end')!;
const start = Math.max(startRaw, endRaw);
const end = Math.min(startRaw, endRaw);
let nameDiscriminator: string | undefined;
let image: string | undefined;
const tankIdRaw = interaction.options.getString('tank')!;
if (isCustomPeriod) {
periodName = `${start} to ${end} days ago`;
} else {
periodName = statPeriodNames[period];
if (commandGroup === '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 (commandGroup === 'tank') {
const tankId = await resolveTankId(tankIdRaw);
nameDiscriminator = `(${resolveTankName(tankId)})`;
image = tankopedia[tankId].images.normal;
}
if (period === 'career') {
const tankStats = await getTankStats(server, id);
stats = accountInfo[id].statistics.all;
const totalBattles = tankStats.reduce(
(accumulator, { all }) => accumulator + all.battles,
0,
);
const period = interaction.options.getSubcommand(true) as Period;
let periodName: string;
let periodStart: number;
let periodEnd: number;
supplementaryStats = {
tier:
tankStats.reduce(
(accumulator, { tank_id, all }) =>
accumulator + tankopedia[tank_id].tier * all.battles,
0,
) / totalBattles,
WN8:
tankStats.reduce((accumulator, { tank_id, all }) => {
if (tankAverages[tank_id]) {
const tankWN8 = getWN8(tankAverages[tank_id].all, all);
if (period === 'custom') {
const startRaw = interaction.options.getInteger('start')!;
const endRaw = interaction.options.getInteger('end')!;
const start = Math.max(startRaw, endRaw);
const end = Math.min(startRaw, endRaw);
if (isNaN(tankWN8)) return accumulator;
hasWN8AccumulatedAtAll = true;
return accumulator + tankWN8 * all.battles;
} else return accumulator;
}, 0) / totalBattles,
};
periodName = `${start} to ${end} days ago`;
periodStart = getTimeDaysAgo(start);
periodEnd = getTimeDaysAgo(end);
} else {
const tankStatsOverTime = await getTankStatsOverTime(
server,
id,
isCustomPeriod
? getPeriodStartFromDaysAgo(start)
: getPeriodicStart(period),
isCustomPeriod ? getPeriodStartFromDaysAgo(end) : getPeriodNow(),
);
const entries = Object.entries(tankStatsOverTime);
const totalBattles = entries.reduce(
periodName = PERIOD_NAMES[period];
periodStart = getPeriodStart(period);
periodEnd = getPeriodNow();
}
const tankStats = await getTankStatsOverTime(
server,
id,
periodStart,
periodEnd,
);
let stats: AllStats;
let supplementaryStats: SupplementaryStats;
if (commandGroup === 'player') {
const entries = Object.entries(tankStats);
stats = sumStats(entries.map(([, stats]) => stats));
const battles = entries.reduce(
(accumulator, [, stats]) => accumulator + stats.battles,
0,
);
supplementaryStats = {
tier:
entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankTier = tankopedia[tankId].tier;
return accumulator + tankTier * stats.battles;
}, 0) / totalBattles,
WN8:
entries.reduce((accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
// edge case where new tanks don't have averages
if (tankAverages[tankId]) {
const tankWN8 = getWN8(tankAverages[tankId].all, stats);
if (isNaN(tankWN8)) return accumulator;
hasWN8AccumulatedAtAll = true;
return accumulator + tankWN8 * stats.battles;
} else return accumulator;
}, 0) / totalBattles,
entries.reduce(
(accumulator, [tankIdString, stats]) =>
accumulator +
getWN8(tankAverages[parseInt(tankIdString)].all, stats) *
stats.battles,
0,
) / battles,
tier:
entries.reduce(
(accumulator, [tankIdString, stats]) =>
accumulator +
tankopedia[parseInt(tankIdString)].tier * stats.battles,
0,
) / battles,
};
} else if (commandGroup === 'tank') {
const tankId = await resolveTankId(tankIdRaw);
stats = tankStats[tankId];
supplementaryStats = {
WN8: getWN8(tankAverages[tankId].all, tankStats[tankId]),
tier: tankopedia[tankId].tier,
};
stats = sumStats(entries.map(([, stats]) => stats));
}
if (!hasWN8AccumulatedAtAll) supplementaryStats.WN8 = undefined;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
return [
return (
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
}
image={
clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined
}
nameDiscriminator={nameDiscriminator}
image={image}
description={`${periodName} • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`}
/>
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{stats.battles > 0 && (
{stats!.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{stats!.battles > 0 && (
<GenericAllStats
stats={stats}
supplementaryStats={supplementaryStats}
stats={stats!}
supplementaryStats={supplementaryStats!}
/>
)}
<PoweredByBlitzStars />
</Wrapper>,
fullBlitzStarsStats(server, accountInfo[id].nickname),
supportBlitzStars,
];
</Wrapper>
);
},
autocomplete: usernameAutocomplete,
autocomplete: (interaction) => {
usernameAutocomplete(interaction);
tanksAutocomplete(interaction);
},
} satisfies CommandRegistry;
-140
View File
@@ -1,140 +0,0 @@
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 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 usernameAutocomplete from '../core/autocomplete/username.js';
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
import getTankStats from '../core/blitz/getTankStats.js';
import getWN8 from '../core/blitz/getWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import resolveTankName from '../core/blitz/resolveTankName.js';
import { tankopedia } from '../core/blitz/tankopedia.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
import getPeriodStartFromDaysAgo from '../core/blitzstars/getPeriodStartFromDaysAgo.js';
import getTankStatsOverTime, {
emptyAllStats,
} from '../core/blitzstars/getTankStatsOverTime.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import cmdName from '../core/interaction/cmdName.js';
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
import addCustomPeriodOption from '../core/options/addCustomPeriodOption.js';
import addPeriodChoices, {
StatPeriod,
statPeriodNames,
} from '../core/options/addPeriodChoices.js';
import addTankChoices from '../core/options/addTankChoices.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo, AllStats } from '../types/accountInfo.js';
export default {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName(cmdName('tankstats'))
.setDescription('Stats for a tank over a period')
.addSubcommand((option) =>
option
.setName('period')
.setDescription('Pick from a predetermined periods')
.addStringOption(addTankChoices)
.addStringOption(addPeriodChoices)
.addStringOption(addUsernameOption),
)
.addSubcommand((option) =>
addCustomPeriodOption(option)
.setName('customperiod')
.addStringOption(addTankChoices)
.setDescription('Custom periods')
.addStringOption(addUsernameOption),
),
async execute(interaction) {
const tank = await resolveTankId(
interaction,
interaction.options.getString('tank')!,
);
const period = interaction.options.getString('period') as StatPeriod;
const blitzAccount = await getBlitzAccount(interaction);
const { id, server } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const tankStats = await getTankStats(server, id);
let stats: AllStats;
const tankId = Number(tank);
const isCustomPeriod =
interaction.options.getSubcommand() === 'customperiod';
let periodName: string;
const startRaw = interaction.options.getInteger('start')!;
const endRaw = interaction.options.getInteger('end')!;
const start = Math.max(startRaw, endRaw);
const end = Math.min(startRaw, endRaw);
if (isCustomPeriod) {
periodName = `${start} to ${end} days ago`;
} else {
periodName = statPeriodNames[period];
}
if (period === 'career') {
stats =
tankStats.find((stats) => stats.tank_id === tankId)?.all ??
emptyAllStats;
} else {
const tankStatsOverTime = await getTankStatsOverTime(
server,
id,
isCustomPeriod
? getPeriodStartFromDaysAgo(start)
: getPeriodicStart(period),
isCustomPeriod ? getPeriodStartFromDaysAgo(end) : getPeriodNow(),
);
stats = tankStatsOverTime[tankId] ?? emptyAllStats;
}
return [
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={`(${resolveTankName(tankId)})`}
image={tankopedia[tankId].images.normal}
description={`${periodName} • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`}
/>
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{stats.battles > 0 && (
<GenericAllStats
stats={stats}
supplementaryStats={{
tier: tankopedia[tankId].tier,
WN8: getWN8(tankAverages[tankId].all, stats),
}}
/>
)}
<PoweredByBlitzStars />
</Wrapper>,
fullBlitzStarsStats(server, accountInfo[id].nickname),
supportBlitzStars,
];
},
autocomplete: (interaction) => {
tanksAutocomplete(interaction);
usernameAutocomplete(interaction);
},
} satisfies CommandRegistry;
+1 -1
View File
@@ -27,7 +27,7 @@ import { PlayerClanData } from '../types/playerClanData.js';
export default {
inProduction: true,
inDevelopment: true,
inDevelopment: false,
inPublic: true,
command: new SlashCommandBuilder()
+1 -5
View File
@@ -1,12 +1,8 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { go } from 'fuzzysort';
import errorWithCause from '../process/errorWithCause.js';
import { TANKS, tankopedia } from './tankopedia.js';
export default async function resolveTankId(
interaction: ChatInputCommandInteraction<CacheType>,
tank: string,
) {
export default async function resolveTankId(tank: string) {
const number = Number(tank);
if (Number.isNaN(number)) {
+5 -11
View File
@@ -1,18 +1,12 @@
import { StatPeriod } from '../options/addPeriodChoices.js';
import getPeriodStartFromDaysAgo from './getPeriodStartFromDaysAgo.js';
import { Period } from '../options/addPeriodSubCommands.js';
import getTimeDaysAgo from './getTimeDaysAgo.js';
export default function getPeriodicStart(period: StatPeriod) {
export default function getPeriodStart(period: Exclude<Period, 'custom'>) {
if (period === 'career') {
return -Infinity;
} else if (period === 'today') {
const now = new Date();
now.setHours(5, 0, 0, 0);
// If it's after 5:00 AM today, subtract one day
if (now.getTime() > Date.now()) now.setDate(now.getDate() - 1);
return now.getTime() / 1000;
return getTimeDaysAgo(0);
} else {
return getPeriodStartFromDaysAgo(Number(period));
return getTimeDaysAgo(Number(period));
}
}
@@ -1,9 +0,0 @@
export default function getPeriodStartFromDaysAgo(period: number) {
const daysAgo = Number(period) - 1;
const periodStart = new Date();
periodStart.setDate(periodStart.getDate() - daysAgo);
periodStart.setHours(5, 0, 0, 0);
return periodStart.getTime() / 1000;
}
+12
View File
@@ -0,0 +1,12 @@
export default function getTimeDaysAgo(period: number) {
const now = new Date();
now.setHours(5, 0, 0, 0);
// if it's after 5:00 AM today, subtract one day
if (now.getTime() > Date.now()) now.setDate(now.getDate() - 1);
// subtract the period number of days from now
now.setDate(now.getDate() - period);
return now.getTime() / 1000;
}
-21
View File
@@ -1,21 +0,0 @@
import { SlashCommandSubcommandBuilder } from 'discord.js';
export default function addCustomPeriodOption(
option: SlashCommandSubcommandBuilder,
) {
return option
.addIntegerOption((option) =>
option
.setName('start')
.setDescription('Start of period in days ago from today')
.setMinValue(0)
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('end')
.setDescription('End of period in days ago from today')
.setMinValue(0)
.setRequired(true),
);
}
-23
View File
@@ -1,23 +0,0 @@
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 addPeriodChoices(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);
}
+60
View File
@@ -0,0 +1,60 @@
import {
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom';
export const PERIOD_NAMES: Record<Period, string> = {
today: "Today's statistics",
'30': "30 days' statistics",
'60': "60 days' statistics",
'90': "90 days' statistics",
career: 'Career statistics',
custom: 'Custom period',
};
export default function addPeriodSubCommands(
option: SlashCommandSubcommandGroupBuilder,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
) {
return option
.addSubcommand((option) =>
extra(option.setName('today').setDescription(PERIOD_NAMES.today)),
)
.addSubcommand((option) =>
extra(option.setName('30').setDescription(PERIOD_NAMES['30'])),
)
.addSubcommand((option) =>
extra(option.setName('60').setDescription(PERIOD_NAMES['60'])),
)
.addSubcommand((option) =>
extra(option.setName('90').setDescription(PERIOD_NAMES['90'])),
)
.addSubcommand((option) =>
extra(option.setName('career').setDescription(PERIOD_NAMES.career)),
)
.addSubcommand((option) =>
extra(
option
.setName('custom')
.setDescription(PERIOD_NAMES.custom)
.addIntegerOption((option) =>
option
.setName('start')
.setDescription('Start of period in days ago from today')
.setMinValue(0)
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('end')
.setDescription('End of period in days ago from today')
.setMinValue(0)
.setRequired(true),
),
),
);
}
-2
View File
@@ -27,7 +27,6 @@ import searchclans from '../commands/searchclans.js';
import searchplayers from '../commands/searchplayers.js';
import searchtanks from '../commands/searchtanks.js';
import stats from '../commands/stats.js';
import tankstats from '../commands/tankstats.js';
import today from '../commands/today.js';
import verify from '../commands/verify.js';
import negativeEmbed from '../core/interaction/negativeEmbed.js';
@@ -77,7 +76,6 @@ const commands = (
searchplayers,
searchtanks,
stats,
tankstats,
today,
verify,
ping,