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

+1 -1
View File
@@ -58,7 +58,7 @@ export const eligibleCommand: CommandRegistry = {
let body = '';
if (clan === 'SKLLD') {
const { diffed } = await getDiffedTankStats(
const { diff: diffed } = await getDiffedTankStats(
server,
id,
getTimeDaysAgo(server, 30),
+3 -4
View File
@@ -1,9 +1,9 @@
import { SlashCommandBuilder } from 'discord.js';
import { CYCLIC_API } from '../constants/cyclic';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import interactionToURL from '../core/discord/interactionToURL';
@@ -24,7 +24,7 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics'),
false,
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
@@ -59,7 +59,6 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
return [
await evolution(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}${
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
@@ -75,7 +74,7 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const url = new URL(`https://example.com/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const player = await resolvePlayerFromButton(interaction);
+3 -3
View File
@@ -1,10 +1,10 @@
import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import interactionToURL from '../core/discord/interactionToURL';
@@ -25,6 +25,7 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
new SlashCommandBuilder()
.setName('full-stats')
.setDescription('Full in-game statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
@@ -82,7 +83,6 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
: null,
),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars',
@@ -96,7 +96,7 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const url = new URL(`https://example.com/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const player = await resolvePlayerFromButton(interaction);
+91 -71
View File
@@ -1,28 +1,77 @@
import { SlashCommandBuilder } from 'discord.js';
import AllStatsOverview from '../components/AllStatsOverview';
import NoData, { NoDataType } from '../components/NoData';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import { StatFilters, filterStats } from '../core/blitz/filterStats';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
import { PeriodType } from '../core/discord/addPeriodSubCommands';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import interactionToURL from '../core/discord/interactionToURL';
import linkButton from '../core/discord/linkButton';
import primaryButton from '../core/discord/primaryButton';
import { refreshButton } from '../core/discord/refreshButton';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePeriodFromCommand, {
ResolvedPeriod,
} from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryRaw } from '../events/interactionCreate';
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
export default async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name: statsName }: ResolvedPeriod,
filters: StatFilters,
) {
const nickname = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
)
)[id].nickname;
const clan = (
await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
)
)[id]?.clan;
const clanImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
const diffedTankStats = await getDiffedTankStats(region, id, start, end);
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
return (
<Wrapper>
<TitleBar
name={nickname}
image={clanImage}
description={`${statsName} • ${await filtersToDescription(filters)}`}
/>
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
{stats?.battles > 0 && (
<AllStatsOverview stats={stats} supplementaryStats={supplementary} />
)}
</Wrapper>
);
}
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('stats')
.setDescription('Regular battles statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
@@ -32,59 +81,38 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
command,
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const resolvedPlayer = await resolvePlayerFromCommand(interaction);
const resolvedPeriod = resolvePeriodFromCommand(
player.region,
resolvedPlayer.region,
interaction,
);
const period = interaction.options.getString('period');
const tankIdRaw = interaction.options.getString('tank')!;
const tankId =
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const filters = {
nation: interaction.options.getString('nation') ?? undefined,
tier:
parseInt(interaction.options.getString('tier') ?? '0') || undefined,
tankType: interaction.options.getString('tank-type') ?? undefined,
treeType:
(interaction.options.getString('tree-type') as
| TreeTypeString
| undefined) ?? undefined,
tank:
parseInt(interaction.options.getString('tank') ?? '0') || undefined,
} satisfies StatFilters;
const path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
period,
...resolvedPlayer,
...((interaction.options.getSubcommand() as PeriodType) === 'custom'
? {
start: interaction.options.getInteger('start', true),
end: interaction.options.getInteger('end', true),
}
: undefined),
...filters,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const tierOption = interaction.options.getString('tier');
return [
await stats(
commandGroup,
resolvedPeriod,
player,
commandGroup === 'tank'
? tankId
: commandGroup === 'multi-tank'
? ({
'tank-type':
interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined,
tier: tierOption ? parseInt(tierOption) : undefined,
'tree-type': (interaction.options.getString('tree-type') ??
undefined) as TreeTypeString | undefined,
} satisfies Partial<MultiTankFilters>)
: null,
),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars',
),
await render(resolvedPlayer, resolvedPeriod, filters),
refreshButton(interaction, path),
await getBlitzStarsLinkButton(resolvedPlayer.region, resolvedPlayer.id),
];
},
@@ -94,27 +122,19 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const url = new URL(`https://example.com${interaction.customId}`);
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
return await stats(
commandGroup,
period,
player,
commandGroup === 'tank'
? parseInt(url.searchParams.get('tankId')!)
: commandGroup === 'multi-tank'
? ({
'tank-type': url.searchParams.get('tank-type')!,
nation: url.searchParams.get('nation')!,
tier: parseInt(url.searchParams.get('tier')!),
'tree-type': url.searchParams.get('tree-type') as TreeTypeString,
} satisfies MultiTankFilters)
: null,
);
return await render(player, period, {
nation: url.searchParams.get('nation') ?? undefined,
tank: parseInt(url.searchParams.get('tank') ?? '0') || undefined,
tankType: url.searchParams.get('tank-type') ?? undefined,
treeType:
(url.searchParams.get('tree-type') as TreeTypeString | undefined) ??
undefined,
tier: parseInt(url.searchParams.get('tier') ?? '0') || undefined,
});
},
});
});
+1 -1
View File
@@ -65,7 +65,7 @@ export const todayCommand: CommandRegistry = {
return [
await today(player, cutoff, maximized, showTotal, false),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
// linkButton(`https://example.com/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars',
+8 -3
View File
@@ -2,7 +2,6 @@ import {
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
TankType,
} from '../../../core/blitz/tankopedia';
import { theme } from '../../../stitches.config';
@@ -12,11 +11,17 @@ export enum TreeTypeEnum {
Collector,
}
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
export type TreeTypeString = 'tech-tree' | 'premium' | 'collector';
export const TREE_TYPE_NAMES: Record<TreeTypeString, string> = {
'tech-tree': 'Tech tree',
premium: 'Premium',
collector: 'Collector',
};
export interface ItemProps {
image?: string;
tankType?: TankType;
tankType?: string;
name: string;
treeType: TreeTypeEnum;
}
+2 -22
View File
@@ -2,17 +2,11 @@ import { theme } from '../stitches.config';
export interface TitleBarProps {
name: string;
nameDiscriminator?: string;
image?: string;
description?: string;
}
export default function TitleBar({
name,
nameDiscriminator,
image,
description,
}: TitleBarProps) {
export default function TitleBar({ name, image, description }: TitleBarProps) {
return (
<div
style={{
@@ -33,9 +27,8 @@ export default function TitleBar({
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
justifyContent: 'center',
gap: 4,
}}
>
@@ -51,24 +44,11 @@ export default function TitleBar({
color: theme.colors.textHighContrast,
fontWeight: 900,
whiteSpace: 'nowrap',
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
overflow: 'hidden',
}}
>
{name}
</span>
{nameDiscriminator && (
<span
style={{
fontSize: 32,
color: theme.colors.textLowContrast,
fontWeight: 900,
whiteSpace: 'nowrap',
}}
>
{nameDiscriminator}
</span>
)}
</div>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
-1
View File
@@ -1 +0,0 @@
export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
+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}`);
}
@@ -1,6 +1,5 @@
import { ButtonInteraction, CacheType } from 'discord.js';
import { commands } from '..';
import { CYCLIC_API } from '../../../constants/cyclic';
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
import throwError from '../../../core/node/throwError';
@@ -9,7 +8,7 @@ export default async function handleButton(
) {
await interaction.deferUpdate();
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const url = new URL(`https://example.com/${interaction.customId}`);
const commandName = url.pathname.split('/')[1];
const button = (await commands)[commandName]?.button;
@@ -17,7 +17,6 @@ export default async function handleChatInputCommand(
) {
const registry = (await commands)[interaction.commandName];
console.log(interaction.toString());
await interaction.deferReply();
try {
+2 -1
View File
@@ -44,7 +44,8 @@ export type InteractionRawReturnable =
| EmbedBuilder
| ButtonBuilder
| AttachmentBuilder
| JSX.Element;
| JSX.Element
| null;
export type InteractionIterableReturnable =
| InteractionRawReturnable
| InteractionRawReturnable[];
+7 -2
View File
@@ -23,7 +23,7 @@ import { MultiTankFilters, StatType } from './stats';
export default async function fullStats<Type extends StatType>(
type: Type,
{ start, end, statsName }: ResolvedPeriod,
{ start, end, name: statsName }: ResolvedPeriod,
{ region: server, id }: ResolvedPlayer,
filters: Type extends 'tank'
? number
@@ -50,7 +50,12 @@ export default async function fullStats<Type extends StatType>(
image = (await tankopedia)[filters as number]?.images.normal;
}
const { diffed, order } = await getDiffedTankStats(server, id, start, end);
const { diff: diffed, order } = await getDiffedTankStats(
server,
id,
start,
end,
);
const awaitedTankopedia = await tankopedia;
let stats: AllStats | undefined;
let supplementaryStats: SupplementaryStats;
-172
View File
@@ -1,172 +0,0 @@
import AllStatsOverview from '../components/AllStatsOverview';
import NoData, { NoDataType } from '../components/NoData';
import { TreeTypeString } from '../components/Tanks';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import calculateWN8 from '../core/blitz/calculateWN8';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName';
import sumStats from '../core/blitz/sumStats';
import { tankopedia } from '../core/blitz/tankopedia';
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
import { tankAverages } from '../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand';
import { theme } from '../stitches.config';
import {
AccountInfo,
AllStats,
SupplementaryStats,
} from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
export type StatType = 'player' | 'tank' | 'multi-tank';
export interface MultiTankFilters {
nation: string;
'tank-type': string;
tier: number;
'tree-type': TreeTypeString;
}
export default async function stats<Type extends StatType>(
type: Type,
{ start, end, statsName }: ResolvedPeriod,
{ region: server, id }: ResolvedPlayer,
filters: Type extends 'tank'
? number
: Type extends 'multi-tank'
? Partial<MultiTankFilters>
: null,
naked = false,
) {
let nameDiscriminator: string | undefined;
let image: string | undefined;
if (type === 'player' || type === 'multi-tank') {
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 = `(${await resolveTankName(filters as number)})`;
image = (await tankopedia)[filters as number]?.images.normal;
} else {
}
const { diffed, order } = await getDiffedTankStats(server, id, start, end);
const awaitedTankopedia = await tankopedia;
let stats: AllStats | undefined;
let supplementaryStats: SupplementaryStats;
if (type === 'player' || type === 'multi-tank') {
const typedFilters = filters as Partial<MultiTankFilters> | null;
const filteredOrder = order.filter(
(id) =>
typedFilters === null ||
((typedFilters.nation === undefined ||
awaitedTankopedia[id]?.nation === typedFilters.nation) &&
(typedFilters['tank-type'] === undefined ||
awaitedTankopedia[id]?.type === typedFilters['tank-type']) &&
(typedFilters.tier === undefined ||
awaitedTankopedia[id]?.tier === typedFilters.tier) &&
(typedFilters['tree-type'] === undefined ||
(typedFilters['tree-type'] === 'collector' &&
awaitedTankopedia[id]?.is_collectible) ||
(typedFilters['tree-type'] === 'premium' &&
awaitedTankopedia[id]?.is_premium) ||
(typedFilters['tree-type'] === 'techtree' &&
!awaitedTankopedia[id]?.is_collectible &&
!awaitedTankopedia[id]?.is_premium))),
);
const awaitedTankAverages = await tankAverages;
stats = sumStats(filteredOrder.map((id) => diffed[id]));
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id]
? accumulator + diffed[id].battles
: accumulator,
0,
);
const battlesOfTanksWithTankopediaEntry = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankopedia[id] ? accumulator + diffed[id].battles : accumulator,
0,
);
supplementaryStats = {
WN8:
filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id]
? accumulator +
calculateWN8(awaitedTankAverages[id].all, diffed[id]) *
diffed[id].battles
: accumulator,
0,
) / battlesOfTanksWithAverages,
tier:
filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankopedia[id]
? accumulator + awaitedTankopedia[id]!.tier * diffed[id].battles
: accumulator,
0,
) / battlesOfTanksWithTankopediaEntry,
};
} else {
stats = diffed[(filters as number)!];
supplementaryStats = {
WN8: stats
? calculateWN8(
(await tankAverages)[(filters as number)!].all,
diffed[(filters as number)!],
)
: undefined,
tier: (await tankopedia)[(filters as number)!]?.tier,
};
}
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const overview = (
<AllStatsOverview stats={stats} supplementaryStats={supplementaryStats} />
);
const footer = (
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
display: 'flex',
gap: 4,
}}
>
<u>/full-stats</u> for all statistics
</span>
);
return naked ? (
<Wrapper naked>{overview}</Wrapper>
) : (
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
image={image}
nameDiscriminator={nameDiscriminator}
description={`${statsName} • ${new Date().toDateString()}`}
/>
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
{stats?.battles > 0 && overview}
</Wrapper>
);
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default async function today(
showTotal = true,
naked?: boolean,
) {
const { diffed, order } = await getDiffedTankStats(
const { diff: diffed, order } = await getDiffedTankStats(
server,
id,
getTimeDaysAgo(server, 1),