feat: simpler subcommands

This commit is contained in:
TrèsAbhi committed 2023-10-11 18:26:35 -05:00
1 parent f965452c5d
commit 78e4c82f20
28 files changed
+397 -461

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
// BIG TODO: reorganize code folder
import { TreeTypeString } from '../../components/Tanks';
import { DiffedTankStats } from '../blitzstars/getDiffedTankStats';
import { tankAverages } from '../blitzstars/tankAverages';
import calculateWN8 from './calculateWN8';
import sumStats from './sumStats';
import { tankopedia } from './tankopedia';
export interface StatFilters {
nation?: string;
tier?: number;
tankType?: string;
treeType?: TreeTypeString;
tank?: number;
}
export async function filterStats(
{ diff, order }: DiffedTankStats,
filters: StatFilters,
) {
// tank, if provided, takes priority over all other filters
if (filters.tank) filters = { tank: filters.tank };
const awaitedTankopedia = await tankopedia;
const awaitedTankAverages = await tankAverages;
const filteredOrder = order.filter((id) => {
const entry = awaitedTankopedia[id];
return (
entry &&
(filters.nation === undefined || entry.nation === filters.nation) &&
(filters.tier === undefined || entry.tier === filters.tier) &&
(filters.tankType === undefined || entry.type === filters.tankType) &&
(filters.treeType === undefined ||
(filters.treeType === 'collector' && entry.is_collectible) ||
(filters.treeType === 'premium' &&
entry.is_premium &&
!entry.is_collectible) ||
(filters.treeType === 'tech-tree' &&
!entry.is_collectible &&
!entry.is_premium)) &&
(filters.tank === undefined || entry.tank_id === filters.tank)
);
});
const stats = sumStats(filteredOrder.map((id) => diff[id]));
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id] ? accumulator + diff[id].battles : accumulator,
0,
);
const battlesOfTanksWithTankopediaEntry = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankopedia[id] ? accumulator + diff[id].battles : accumulator,
0,
);
const supplementary = {
WN8:
filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id]
? accumulator +
calculateWN8(awaitedTankAverages[id].all, diff[id]) *
diff[id].battles
: accumulator,
0,
) / battlesOfTanksWithAverages,
tier:
filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankopedia[id]
? accumulator + awaitedTankopedia[id]!.tier * diff[id].battles
: accumulator,
0,
) / battlesOfTanksWithTankopediaEntry,
};
return { stats, supplementary };
}
+6 -7
View File
@@ -2,8 +2,7 @@ import { deburr } from 'lodash';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { context } from '../blitzkrieg/context';
import getWargamingResponse from './getWargamingResponse';
export type TankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
import { TreeTypeString } from '../../components/Tanks';
export interface TankopediaEntry {
name: string;
@@ -14,10 +13,10 @@ export interface TankopediaEntry {
cost: { price_credit: number; price_gold: number };
images: { preview: string; normal: string };
tank_id: number;
type: TankType;
type: string;
description: string;
}
type asd = TreeTypeString
export interface Tankopedia {
[id: number]: TankopediaEntry | undefined;
}
@@ -77,21 +76,21 @@ export const tankopediaInfo = getWargamingResponse<TankopediaInfo>(
);
console.log('Cached tankopedia info');
export const TANK_ICONS: Record<TankType, string> = {
export const TANK_ICONS: Record<string, string> = {
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
lightTank: 'https://i.imgur.com/CSNha5V.png',
mediumTank: 'https://i.imgur.com/wvf3ltm.png',
heavyTank: 'https://i.imgur.com/ECeqlZa.png',
};
export const TANK_ICONS_PREMIUM: Record<TankType, string> = {
export const TANK_ICONS_PREMIUM: Record<string, string> = {
'AT-SPG': 'https://i.imgur.com/TCu3EdR.png',
lightTank: 'https://i.imgur.com/zdkpTRb.png',
mediumTank: 'https://i.imgur.com/3z7eHX6.png',
heavyTank: 'https://i.imgur.com/P3vbmyA.png',
};
export const TANK_ICONS_COLLECTOR: Record<TankType, string> = {
export const TANK_ICONS_COLLECTOR: Record<string, string> = {
'AT-SPG': 'https://i.imgur.com/WTjeirB.png',
lightTank: 'https://i.imgur.com/EwhtKkU.png',
mediumTank: 'https://i.imgur.com/u8YDMBh.png',
@@ -0,0 +1,23 @@
import { TREE_TYPE_NAMES } from '../../components/Tanks';
import { StatFilters } from '../blitz/filterStats';
import resolveTankName from '../blitz/resolveTankName';
import { TIER_ROMAN_NUMERALS, Tier, tankopediaInfo } from '../blitz/tankopedia';
export async function filtersToDescription({
nation,
tier,
tankType,
treeType,
tank,
}: StatFilters) {
const awaitedTankopediaInfo = await tankopediaInfo;
const info: string[] = [];
if (tank) return await resolveTankName(tank);
if (nation) info.push(awaitedTankopediaInfo.vehicle_nations[nation]);
if (tier) info.push(`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]}`);
if (tankType) info.push(awaitedTankopediaInfo.vehicle_types[tankType]);
if (treeType) info.push(TREE_TYPE_NAMES[treeType]);
return info.length === 0 ? 'No filters' : info.join(', ');
}
@@ -0,0 +1,18 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { AccountInfo } from '../../types/accountInfo';
import getWargamingResponse from '../blitz/getWargamingResponse';
import linkButton from '../discord/linkButton';
export async function getBlitzStarsLinkButton(region: Region, id: number) {
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
)
)[id];
return linkButton(
`https://www.blitzstars.com/player/${region}/${nickname}`,
'BlitzStars',
);
}
+25 -20
View File
@@ -34,6 +34,11 @@ export const emptyTankHistoryNode: TankHistoryRaw = {
all: emptyAllStats,
};
export interface DiffedTankStats {
diff: Record<number, AllStats>;
order: number[];
}
// BIG TODO: merge this neatly with diffNormalizedTankStats
export default async function getDiffedTankStats(
server: Region,
@@ -66,7 +71,7 @@ export default async function getDiffedTankStats(
const playedTanks: number[] = [];
// fetch history node right before "start" and right before "end"
const diffed: Record<number, AllStats> = {};
const diff: Record<number, AllStats> = {};
inRangeTanks.forEach((tankId) => {
const sortedHistory = tankSortedHistory[tankId];
const previousIndex =
@@ -77,30 +82,30 @@ export default async function getDiffedTankStats(
const latest =
sortedHistory[latestIndex] ?? sortedHistory[sortedHistory.length - 1];
function diff(get: (allStats: AllStats) => number) {
function d(get: (allStats: AllStats) => number) {
return get(latest.all) - get(previous.all);
}
// check if there was a change in battles as games in ratings do update last_battle_time
if (diff((a) => a.battles) > 0) {
diffed[tankId] = {
battles: diff((a) => a.battles),
capture_points: diff((a) => a.capture_points),
damage_dealt: diff((a) => a.damage_dealt),
damage_received: diff((a) => a.damage_received),
dropped_capture_points: diff((a) => a.dropped_capture_points),
frags: diff((a) => a.frags),
frags8p: diff((a) => a.frags8p),
hits: diff((a) => a.hits),
losses: diff((a) => a.losses),
if (d((a) => a.battles) > 0) {
diff[tankId] = {
battles: d((a) => a.battles),
capture_points: d((a) => a.capture_points),
damage_dealt: d((a) => a.damage_dealt),
damage_received: d((a) => a.damage_received),
dropped_capture_points: d((a) => a.dropped_capture_points),
frags: d((a) => a.frags),
frags8p: d((a) => a.frags8p),
hits: d((a) => a.hits),
losses: d((a) => a.losses),
max_frags: latest.all.max_frags,
max_xp: latest.all.max_xp,
shots: diff((a) => a.shots),
spotted: diff((a) => a.spotted),
survived_battles: diff((a) => a.survived_battles),
win_and_survived: diff((a) => a.win_and_survived),
wins: diff((a) => a.wins),
xp: diff((a) => a.xp),
shots: d((a) => a.shots),
spotted: d((a) => a.spotted),
survived_battles: d((a) => a.survived_battles),
win_and_survived: d((a) => a.win_and_survived),
wins: d((a) => a.wins),
xp: d((a) => a.xp),
};
playedTanks.push(tankId);
@@ -116,5 +121,5 @@ export default async function getDiffedTankStats(
return bLatest.last_battle_time - aLatest.last_battle_time;
});
return { diffed, order };
return { diff, order } satisfies DiffedTankStats;
}
+3 -1
View File
@@ -3,5 +3,7 @@ import { PeriodSize } from '../discord/addPeriodSubCommands';
import getTimeDaysAgo from './getTimeDaysAgo';
export default function getPeriodStart(region: Region, period: PeriodSize) {
return period === 'career' ? 0 : getTimeDaysAgo(region, parseInt(period));
if (period === 'career') return 0;
if (period === 'today') return getTimeDaysAgo(region, 1);
return getTimeDaysAgo(region, parseInt(period));
}
+19 -32
View File
@@ -1,55 +1,42 @@
import {
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
export type PeriodType = 'period' | 'custom';
export type PeriodSize = `${number}` | 'career';
export type PeriodType = 'today' | '30' | '60' | '90' | 'custom';
export type PeriodSize = 'today' | `${number}` | 'career';
export default function addPeriodSubCommands(
option: SlashCommandSubcommandGroupBuilder,
option: SlashCommandBuilder,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
) {
return option
option
.addSubcommand((option) =>
extra(
option
.setName('period' satisfies PeriodType)
.setDescription("Preset period's statistics")
.addStringOption((option) =>
option
.setName('period')
.setDescription('A preset period')
.setChoices(
{ name: 'Today', value: '1' satisfies PeriodSize },
{ name: '30 days', value: '30' satisfies PeriodSize },
{ name: '60 days', value: '60' satisfies PeriodSize },
{ name: '90 days', value: '90' satisfies PeriodSize },
{ name: 'Career', value: 'career' satisfies PeriodSize },
)
.setRequired(true),
),
),
extra(option.setName('today').setDescription('Today')),
)
.addSubcommand((option) =>
extra(option.setName('30').setDescription('30 days')),
)
.addSubcommand((option) =>
extra(option.setName('60').setDescription('60 days')),
)
.addSubcommand((option) =>
extra(option.setName('90').setDescription('90 days')),
)
.addSubcommand((option) =>
extra(
option
.setName('custom' satisfies PeriodType)
.setDescription("Custom period's statistics")
.setName('custom')
.setDescription('Custom range')
.addIntegerOption((option) =>
option
.setName('start')
.setDescription('Days ago from today')
.setMinValue(0)
.setDescription('Start in number of days ago from today')
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('end')
.setDescription('Days ago from today')
.setMinValue(0)
.setDescription('End in number of days')
.setRequired(true),
),
),
+53 -74
View File
@@ -1,91 +1,70 @@
import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../../components/Tanks';
import { StatType } from '../../renderers/stats';
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
import { tankopediaInfo } from '../blitz/tankopedia';
import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices';
import addUsernameChoices from './addUsernameChoices';
export default async function addStatTypeSubCommandGroups(
option: SlashCommandBuilder,
addMultiTank = true,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
) {
const awaitedTankopediaInfo = await tankopediaInfo;
option
.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices),
)
.setName('player' satisfies StatType)
.setDescription('Player statistics'),
)
.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameChoices),
)
.setName('tank' satisfies StatType)
.setDescription('Tank statistics'),
);
addPeriodSubCommands(option, (option) => {
extra(option);
if (addMultiTank)
option.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
return option
.addStringOption((option) =>
option
.addStringOption(addUsernameChoices)
.addStringOption((option) => {
option
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
);
return option;
})
.addStringOption((option) =>
option
.setName('tank-type')
.setDescription('Type')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
)
.addStringOption((option) =>
addTierChoices(option).setRequired(false),
)
.addStringOption((option) =>
option
.setName('tree-type')
.setDescription('Tech tree type')
.addChoices(
{
name: 'Tech Tree',
value: 'techtree' satisfies TreeTypeString,
},
{ name: 'Premium', value: 'premium' satisfies TreeTypeString },
{
name: 'Collector',
value: 'collector' satisfies TreeTypeString,
},
)
.setRequired(false),
),
.setRequired(false),
)
.setName('multi-tank' satisfies StatType)
.setDescription('Multiple tanks'),
);
.addStringOption((option) => addTierChoices(option).setRequired(false))
.addStringOption((option) =>
option
.setName('tank-type')
.setDescription('Tank type')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
)
.addStringOption((option) =>
option
.setName('tree-type')
.setDescription('Tech tree type')
.addChoices(
{
name: TREE_TYPE_NAMES['tech-tree'],
value: 'tech-tree' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.premium,
value: 'premium' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.collector,
value: 'collector' satisfies TreeTypeString,
},
)
.setRequired(false),
)
.addStringOption((option) => addTankChoices(option).setRequired(false));
});
return option;
}
+2 -6
View File
@@ -1,11 +1,7 @@
import {
CacheType,
ChatInputCommandInteraction,
CommandInteractionOption,
} from 'discord.js';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
export interface Parameters {
[key: string]: CommandInteractionOption['value'] | undefined | null;
[key: string]: string | number | boolean | undefined | null;
}
export default function interactionToURL(
+16
View File
@@ -0,0 +1,16 @@
import { ChatInputCommandInteraction } from 'discord.js';
import primaryButton from './primaryButton';
export function refreshButton(
interaction: ChatInputCommandInteraction,
path: string,
) {
if (
interaction.appPermissions?.has('AttachFiles') &&
interaction.appPermissions?.has('ViewChannel')
) {
return primaryButton(path, 'Refresh');
}
return null;
}
+4 -2
View File
@@ -1,5 +1,4 @@
import { ButtonInteraction, CacheType } from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic';
import { Region } from '../../constants/regions';
import resolvePeriodFromURL from './resolvePeriodFromURL';
@@ -7,5 +6,8 @@ export default function resolvePeriodFromButton(
server: Region,
interaction: ButtonInteraction<CacheType>,
) {
return resolvePeriodFromURL(server, `${CYCLIC_API}/${interaction.customId}`);
return resolvePeriodFromURL(
server,
`https://example.com/${interaction.customId}`,
);
}
+13 -17
View File
@@ -6,12 +6,13 @@ import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
export function getPeriodOptionName(period: PeriodSize) {
return period === 'career' ? 'Career' : `${period}-day`;
if (period === 'career') return 'Career';
if (period === 'today') return 'Today';
return `${period} days`;
}
export interface ResolvedPeriod {
statsName: string;
evolutionName: string;
name: string;
start: number;
end: number;
}
@@ -20,33 +21,28 @@ export default function resolvePeriodFromCommand(
region: Region,
interaction: ChatInputCommandInteraction<CacheType>,
) {
let statsName: string;
let evolutionName: string;
let name: string;
let start: number;
let end: number;
const periodSubcommand = interaction.options.getSubcommand() as PeriodType;
const periodOption = interaction.options.getString('period') as PeriodSize;
if (periodSubcommand === 'custom') {
const startRaw = interaction.options.getInteger('start', true);
const endRaw = interaction.options.getInteger('end', true);
const startDaysAgoMin = Math.min(startRaw, endRaw);
const endDaysAgoMax = Math.max(startRaw, endRaw);
const startOption = interaction.options.getInteger('start', true);
const endOption = interaction.options.getInteger('end', true);
const startDaysAgoMin = Math.min(startOption, endOption);
const endDaysAgoMax = Math.max(startOption, endOption);
statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
name = `${startDaysAgoMin} - ${endDaysAgoMax} days`;
start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin);
} else {
statsName = `${getPeriodOptionName(periodOption)} Statistics`;
evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
start = getPeriodStart(region, periodOption);
name = getPeriodOptionName(periodSubcommand);
start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow();
}
return {
statsName,
evolutionName,
name,
start,
end,
} satisfies ResolvedPeriod;
+15 -15
View File
@@ -2,24 +2,22 @@ import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzstars/getPeriodNow';
import getPeriodStart from '../blitzstars/getPeriodStart';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { PeriodSize, PeriodType } from './addPeriodSubCommands';
import { PeriodType } from './addPeriodSubCommands';
import {
ResolvedPeriod,
getPeriodOptionName,
} from './resolvePeriodFromCommand';
export default function resolvePeriodFromURL(
server: Region,
region: Region,
urlString: string,
) {
let statsName: string;
let evolutionName: string;
let name: string;
let start: number;
let end: number;
const url = new URL(urlString);
const path = url.pathname.split('/').filter(Boolean);
const periodSubcommand = path[path.length - 1] as PeriodType;
const periodOption = url.searchParams.get('period') as PeriodSize;
const path = url.pathname.split('/');
const periodSubcommand = path[2] as PeriodType;
if (periodSubcommand === 'custom') {
const startRaw = parseInt(url!.searchParams.get('start')!);
@@ -27,16 +25,18 @@ export default function resolvePeriodFromURL(
const startDaysAgoMin = Math.min(startRaw, endRaw);
const endDaysAgoMax = Math.max(startRaw, endRaw);
statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
start = getTimeDaysAgo(server, endDaysAgoMax);
end = getTimeDaysAgo(server, startDaysAgoMin);
name = `${startDaysAgoMin} - ${endDaysAgoMax} days`;
start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin);
} else {
statsName = `${getPeriodOptionName(periodOption)} Statistics`;
evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
start = getPeriodStart(server, periodOption);
name = getPeriodOptionName(periodSubcommand);
start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow();
}
return { statsName, evolutionName, start, end } satisfies ResolvedPeriod;
return {
name,
start,
end,
} satisfies ResolvedPeriod;
}
+1 -2
View File
@@ -1,9 +1,8 @@
import { ButtonInteraction, CacheType } from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic';
import resolvePlayerFromURL from './resolvePlayerFromURL';
export default async function resolvePlayerFromButton(
interaction: ButtonInteraction<CacheType>,
) {
return resolvePlayerFromURL(`${CYCLIC_API}/${interaction.customId}`);
return resolvePlayerFromURL(`https://exmaple.com/${interaction.customId}`);
}