mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: error handling for /research
This commit is contained in:
1 parent
787d3ae6ea
commit
d39ab43e74
6 files changed
+337
-96
No files matched your search
+289
-84
@@ -1,4 +1,4 @@
|
||||
import { escapeMarkdown } from 'discord.js';
|
||||
import { Locale, escapeMarkdown } from 'discord.js';
|
||||
import CommandWrapper from '../components/CommandWrapper';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import { equipmentPriceMatrix } from '../constants/equipmentPrice';
|
||||
@@ -17,62 +17,207 @@ import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '../core/blitzkrieg/tankDefinitions/constants';
|
||||
import { tankIconPng } from '../core/blitzkrieg/tankIconPng';
|
||||
import addTankChoices from '../core/discord/addTankChoices';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||
import { localizationObject } from '../core/discord/localizationObject';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { translator } from '../core/localization/translator';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
const { t } = translator(Locale.EnglishUS);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inPublic: true,
|
||||
|
||||
command: createLocalizedCommand('research').addStringOption(addTankChoices),
|
||||
command: createLocalizedCommand('research')
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(t`bot.commands.research.options.target_tank`)
|
||||
.setNameLocalizations(
|
||||
localizationObject('bot.commands.research.options.target_tank'),
|
||||
)
|
||||
.setDescription(
|
||||
t`bot.commands.research.options.target_tank.description`,
|
||||
)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject(
|
||||
'bot.commands.research.options.target_tank.description',
|
||||
),
|
||||
)
|
||||
.setAutocomplete(true)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(t`bot.commands.research.options.starting_tank`)
|
||||
.setNameLocalizations(
|
||||
localizationObject('bot.commands.research.options.starting_tank'),
|
||||
)
|
||||
.setDescription(
|
||||
t`bot.commands.research.options.starting_tank.description`,
|
||||
)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject(
|
||||
'bot.commands.research.options.starting_tank.description',
|
||||
),
|
||||
)
|
||||
.setAutocomplete(true)
|
||||
.setRequired(false),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const { t, translate } = translator(interaction.locale);
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const tankId = await resolveTankId(
|
||||
interaction.options.getString('tank', true),
|
||||
const targetTankId = await resolveTankId(
|
||||
interaction.options.getString('target-tank', true),
|
||||
interaction.locale,
|
||||
true,
|
||||
);
|
||||
const tank = awaitedTankDefinitions[tankId];
|
||||
|
||||
if (tank.treeType !== 'researchable') {
|
||||
return `# ${escapeMarkdown(tank.name)} is not researchable\n\nThis tank is not on the tech tree.`;
|
||||
}
|
||||
|
||||
if (tank.tier === 1) {
|
||||
return `# ${escapeMarkdown(tank.name)} is not researchable\n\nThis tank is a tier I and is always unlocked.`;
|
||||
}
|
||||
|
||||
const startingTankRaw = interaction.options.getString('starting-tank');
|
||||
const targetTank = awaitedTankDefinitions[targetTankId];
|
||||
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||
const tankStats = await getTankStats(region, id, interaction.locale);
|
||||
const ancestry = await resolveAncestry(tankId);
|
||||
const firstOwnedTechTree = ancestry.find(
|
||||
(id) =>
|
||||
awaitedTankDefinitions[id].tier === 1 ||
|
||||
tankStats.some(({ tank_id }) => tank_id === id),
|
||||
);
|
||||
let startingTankId: number;
|
||||
const targetTankAncestry = await resolveAncestry(targetTankId);
|
||||
|
||||
if (firstOwnedTechTree === undefined) {
|
||||
throw new Error('No first owned tech tree found.');
|
||||
if (startingTankRaw) {
|
||||
startingTankId = await resolveTankId(
|
||||
startingTankRaw,
|
||||
interaction.locale,
|
||||
true,
|
||||
);
|
||||
|
||||
if (targetTankId === startingTankId) {
|
||||
return translate('bot.commands.research.errors.start_end_equal', [
|
||||
escapeMarkdown(targetTank.name),
|
||||
]);
|
||||
}
|
||||
|
||||
const startingTank = awaitedTankDefinitions[startingTankId];
|
||||
|
||||
if (startingTank.tier > targetTank.tier) {
|
||||
return translate('bot.commands.research.errors.unordered', [
|
||||
escapeMarkdown(startingTank.name),
|
||||
escapeMarkdown(targetTank.name),
|
||||
escapeMarkdown(targetTank.name),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!targetTankAncestry.includes(startingTankId)) {
|
||||
return translate('bot.commands.research.errors.tanks_not_on_line', [
|
||||
escapeMarkdown(targetTank.name),
|
||||
escapeMarkdown(startingTank.name),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if (targetTank.treeType !== 'researchable') {
|
||||
return translate('bot.commands.research.errors.non_tech_tree', [
|
||||
escapeMarkdown(targetTank.name),
|
||||
]);
|
||||
}
|
||||
|
||||
if (targetTank.tier === 1) {
|
||||
return translate('bot.commands.research.errors.tier_1', [
|
||||
escapeMarkdown(targetTank.name),
|
||||
]);
|
||||
}
|
||||
|
||||
const tankStats = await getTankStats(region, id, interaction.locale);
|
||||
|
||||
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
|
||||
return translate('bot.commands.research.errors.already_researched', [
|
||||
escapeMarkdown(targetTank.name),
|
||||
]);
|
||||
}
|
||||
|
||||
const foundAncestor = targetTankAncestry.find(
|
||||
(id) =>
|
||||
awaitedTankDefinitions[id].tier === 1 ||
|
||||
tankStats.some(({ tank_id }) => tank_id === id),
|
||||
);
|
||||
|
||||
if (foundAncestor === undefined) {
|
||||
throw new Error('No first owned tech tree found.');
|
||||
}
|
||||
|
||||
startingTankId = foundAncestor;
|
||||
}
|
||||
|
||||
const line = (
|
||||
await buildTechTreeLine(firstOwnedTechTree, tankId)
|
||||
).reverse();
|
||||
const techTreeLine = await buildTechTreeLine(
|
||||
startingTankId,
|
||||
targetTankId,
|
||||
);
|
||||
|
||||
const line = [...techTreeLine, startingTankId].reverse();
|
||||
const { nickname } = await getAccountInfo(region, id);
|
||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||
const costs = await Promise.all(
|
||||
line.map(async (id) => {
|
||||
const tank = awaitedTankDefinitions[id];
|
||||
const turretXps = new Map<number, number>();
|
||||
const gunXps = new Map<number, number>();
|
||||
const engineXps = new Map<number, number>();
|
||||
const trackXps = new Map<number, number>();
|
||||
|
||||
return (
|
||||
tank.turrets.forEach((turret) => {
|
||||
if (turret.xp !== undefined) {
|
||||
turretXps.set(turret.id, turret.xp);
|
||||
}
|
||||
|
||||
turret.guns.forEach((gun) => {
|
||||
if (gun.xp !== undefined) {
|
||||
gunXps.set(gun.id, gun.xp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tank.engines.forEach((engine) => {
|
||||
if (engine.xp !== undefined) {
|
||||
engineXps.set(engine.id, engine.xp);
|
||||
}
|
||||
});
|
||||
|
||||
tank.tracks.forEach((track) => {
|
||||
if (track.xp !== undefined) {
|
||||
trackXps.set(track.id, track.xp);
|
||||
}
|
||||
});
|
||||
|
||||
let moduleXp = 0;
|
||||
turretXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
gunXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
engineXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
trackXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
|
||||
return {
|
||||
research: tank.xp,
|
||||
upgrades: moduleXp,
|
||||
purchase: tank.price.value,
|
||||
equipment: equipmentPriceMatrix[tank.tier].reduce(
|
||||
(a, b) => a + 3 * b,
|
||||
0,
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const image = (
|
||||
<CommandWrapper>
|
||||
<TitleBar
|
||||
title={nickname}
|
||||
image={clanImage}
|
||||
description={`Research • ${tank.name}`}
|
||||
description={`${t`bot.commands.research.body.subtitle`} • ${targetTank.name}`}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -82,51 +227,98 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.appBackground2,
|
||||
border: theme.borderStyles.subtle,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 48 }}></div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{t`bot.commands.research.body.total`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
padding: '16px 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={await iconPng(asset('icons/currencies/xp.webp'))}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{costs
|
||||
.reduce((a, b) => a + b.purchase + b.equipment, 0)
|
||||
.toLocaleString(interaction.locale)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={await iconPng(asset('icons/currencies/silver.webp'))}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{costs
|
||||
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
|
||||
.toLocaleString(interaction.locale)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{await Promise.all(
|
||||
line.map(async (id) => {
|
||||
line.map(async (id, index) => {
|
||||
const tank = awaitedTankDefinitions[id];
|
||||
const turretXps = new Map<number, number>();
|
||||
const gunXps = new Map<number, number>();
|
||||
const engineXps = new Map<number, number>();
|
||||
const trackXps = new Map<number, number>();
|
||||
|
||||
tank.turrets.forEach((turret) => {
|
||||
if (turret.xp !== undefined) {
|
||||
turretXps.set(turret.id, turret.xp);
|
||||
}
|
||||
|
||||
turret.guns.forEach((gun) => {
|
||||
if (gun.xp !== undefined) {
|
||||
gunXps.set(gun.id, gun.xp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tank.engines.forEach((engine) => {
|
||||
if (engine.xp !== undefined) {
|
||||
engineXps.set(engine.id, engine.xp);
|
||||
}
|
||||
});
|
||||
|
||||
tank.tracks.forEach((track) => {
|
||||
if (track.xp !== undefined) {
|
||||
trackXps.set(track.id, track.xp);
|
||||
}
|
||||
});
|
||||
|
||||
let moduleXp = 0;
|
||||
turretXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
gunXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
engineXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
trackXps.forEach((value) => {
|
||||
moduleXp += value;
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -135,7 +327,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.appBackground2,
|
||||
border: `1px solid ${theme.colors.componentInteractive}`,
|
||||
border: theme.borderStyles.subtle,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -241,12 +433,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
Research:{' '}
|
||||
{tank.xp?.toLocaleString(interaction.locale)}
|
||||
{translate('bot.commands.research.body.research', [
|
||||
`${costs[index].research?.toLocaleString(
|
||||
interaction.locale,
|
||||
)}`,
|
||||
])}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{moduleXp > 0 && (
|
||||
{costs[index].upgrades > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -267,7 +462,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
Upgrades: {moduleXp.toLocaleString()}
|
||||
{translate('bot.commands.research.body.upgrades', [
|
||||
costs[index].upgrades.toLocaleString(),
|
||||
])}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -292,8 +489,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
Purchase:{' '}
|
||||
{tank.price.value.toLocaleString(interaction.locale)}
|
||||
{translate('bot.commands.research.body.purchase', [
|
||||
costs[index].purchase.toLocaleString(
|
||||
interaction.locale,
|
||||
),
|
||||
])}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -317,10 +517,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
Equipment:{' '}
|
||||
{equipmentPriceMatrix[tank.tier]
|
||||
.reduce((a, b) => a + 3 * b, 0)
|
||||
.toLocaleString(interaction.locale)}
|
||||
{translate('bot.commands.research.body.equipment', [
|
||||
costs[index].equipment.toLocaleString(
|
||||
interaction.locale,
|
||||
),
|
||||
])}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,10 +532,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||
</div>
|
||||
</CommandWrapper>
|
||||
);
|
||||
|
||||
return startingTankRaw
|
||||
? image
|
||||
: [t`bot.commands.research.body.estimation`, image];
|
||||
},
|
||||
|
||||
autocomplete(interaction) {
|
||||
autocompleteTanks(interaction, true);
|
||||
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -2,24 +2,27 @@ import { Locale } from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { UserError } from '../../hooks/userError';
|
||||
import {
|
||||
tankDefinitions,
|
||||
tanksDefinitionsArray,
|
||||
} from '../blitzkrieg/tankDefinitions';
|
||||
import { tankDefinitions, tankNames } from '../blitzkrieg/tankDefinitions';
|
||||
import { tankNamesTechTreeOnly } from '../discord/autocompleteTanks';
|
||||
import { translator } from '../localization/translator';
|
||||
|
||||
export default async function resolveTankId(
|
||||
tank: string | number,
|
||||
locale: Locale,
|
||||
techTreeOnly = false,
|
||||
) {
|
||||
const { translate } = translator(locale);
|
||||
const number = typeof tank === 'string' ? Number(tank) : tank;
|
||||
|
||||
if (Number.isNaN(number)) {
|
||||
const searchResult = go(`${tank}`, await tanksDefinitionsArray, {
|
||||
keys: ['name'],
|
||||
limit: 1,
|
||||
});
|
||||
const searchResult = go(
|
||||
`${tank}`,
|
||||
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
|
||||
{
|
||||
keys: ['combined'],
|
||||
limit: 1,
|
||||
},
|
||||
);
|
||||
|
||||
if (searchResult.length === 0) {
|
||||
throw new UserError(
|
||||
|
||||
@@ -10,7 +10,8 @@ export async function buildTechTreeLine(start: number, end: number) {
|
||||
if (start === end) return line;
|
||||
for (const ancestorId of endTank.ancestors) {
|
||||
if ((await resolveAncestry(ancestorId)).includes(start)) {
|
||||
line.push(...(await buildTechTreeLine(start, ancestorId)));
|
||||
const ancestorTree = await buildTechTreeLine(start, ancestorId);
|
||||
line.push(...ancestorTree);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,17 @@ import {
|
||||
OVERFLOW_SUFFIX,
|
||||
} from './autocompleteClan/constants';
|
||||
|
||||
const tankNamesTechTreeOnly = tankNames.then((names) =>
|
||||
export const tankNamesTechTreeOnly = tankNames.then((names) =>
|
||||
names.filter((tank) => tank.treeType === 'researchable'),
|
||||
);
|
||||
|
||||
export default async function autocompleteTanks(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
techTreeOnly = false,
|
||||
fields = ['tank'],
|
||||
) {
|
||||
const focusedOption = interaction.options.getFocused(true);
|
||||
if (focusedOption.name !== 'tank') return;
|
||||
if (!fields.includes(focusedOption.name)) return;
|
||||
|
||||
await interaction.respond(
|
||||
focusedOption.value
|
||||
|
||||
+31
-1
@@ -197,7 +197,37 @@
|
||||
"commands": {
|
||||
"research": {
|
||||
"$": "research",
|
||||
"description": "Lists the expenses in researching a line"
|
||||
"description": "Lists the expenses in researching a line",
|
||||
|
||||
"options": {
|
||||
"target_tank": {
|
||||
"$": "target-tank",
|
||||
"description": "The final tank to research in the line"
|
||||
},
|
||||
"starting_tank": {
|
||||
"$": "starting-tank",
|
||||
"description": "The starting tank in the line (inferred automatically if not provided)"
|
||||
}
|
||||
},
|
||||
|
||||
"body": {
|
||||
"subtitle": "Research",
|
||||
"total": "Total",
|
||||
"research": "Research: %s",
|
||||
"upgrades": "Upgrades: %s",
|
||||
"purchase": "Purchase: %s",
|
||||
"equipment": "Equipment: %s",
|
||||
"estimation": "# This is an estimation!\n\nThis list may include already researched tanks because of the inaccurate information Wargaming provides. Try providing a `starting-tank` from the line when using the command if you can."
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"non_tech_tree": "# %s is not researchable\n\nThis tank is not on the tech tree.",
|
||||
"tier_1": "# %s is not researchable\n\nThis tank is a tier I and is always unlocked.",
|
||||
"already_researched": "# %s is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
|
||||
"start_end_equal": "# %s was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
|
||||
"tanks_not_on_line": "# %s and %s are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
|
||||
"unordered": "# %s higher tiered to %s\n\nMake sure to provide %s as the `starting-tank` instead as it is the lower tiered tank."
|
||||
}
|
||||
},
|
||||
|
||||
"about": {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const theme = {
|
||||
},
|
||||
|
||||
borderStyles: {
|
||||
subtle: `1px solid ${slateDark.slate2}`,
|
||||
...createBorderStyles(slateDark),
|
||||
...createBorderStyles(blueDark, undefined, 'blue'),
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user