import { asset, emblemURL, equipmentPriceMatrix, getAccountInfo, getClanAccountInfo, getTankStats, TANK_ICONS, TankType, TIER_ROMAN_NUMERALS, } from "@blitzkit/core"; import { literals } from "@blitzkit/i18n"; import { escapeMarkdown, Locale } from "discord.js"; import { CommandWrapper } from "../components/CommandWrapper"; import { TitleBar } from "../components/TitleBar"; import { resolveTankId } from "../core/blitz/resolveTankId"; import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine"; import { iconPng } from "../core/blitzkit/iconPng"; import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises"; import { resolveAncestry } from "../core/blitzkit/resolveAncestry"; import { tankIconPng } from "../core/blitzkit/tankIconPng"; import { addUsernameChoices } from "../core/discord/addUsernameChoices"; 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 { type CommandRegistry } from "../events/interactionCreate"; import { theme } from "../stitches.config"; export const researchCommand = new Promise((resolve) => { const { strings } = translator(Locale.EnglishUS); resolve({ command: createLocalizedCommand("research") .addStringOption((option) => option .setName(strings.bot.commands.research.options.target_tank.name) .setNameLocalizations( localizationObject( (strings) => strings.bot.commands.research.options.target_tank.name, undefined, true ) ) .setDescription( strings.bot.commands.research.options.target_tank.description ) .setDescriptionLocalizations( localizationObject( (strings) => strings.bot.commands.research.options.target_tank.description ) ) .setAutocomplete(true) .setRequired(true) ) .addStringOption((option) => option .setName(strings.bot.commands.research.options.starting_tank.name) .setNameLocalizations( localizationObject( (strings) => strings.bot.commands.research.options.starting_tank.name, undefined, true ) ) .setDescription( strings.bot.commands.research.options.starting_tank.description ) .setDescriptionLocalizations( localizationObject( (strings) => strings.bot.commands.research.options.starting_tank.description ) ) .setAutocomplete(true) .setRequired(false) ) .addStringOption(addUsernameChoices), async handler(interaction) { const { strings, unwrap } = translator(interaction.locale); const awaitedTankDefinitions = await tankDefinitions; const targetTankId = await resolveTankId( interaction.options.getString("target-tank", true), interaction.locale, true ); const startingTankRaw = interaction.options.getString("starting-tank"); const targetTank = awaitedTankDefinitions.tanks[targetTankId]; const { id, region } = await resolvePlayerFromCommand(interaction); let startingTankId: number; const targetTankAncestry = await resolveAncestry(targetTankId); if (startingTankRaw) { startingTankId = await resolveTankId( startingTankRaw, interaction.locale, true ); if (targetTankId === startingTankId) { return literals( strings.bot.commands.research.errors.start_end_equal, { tank: escapeMarkdown(unwrap(targetTank.name)) } ); } const startingTank = awaitedTankDefinitions.tanks[startingTankId]; if (startingTank.tier > targetTank.tier) { return literals(strings.bot.commands.research.errors.unordered, { tank_1: escapeMarkdown(unwrap(startingTank.name)), tank_2: escapeMarkdown(unwrap(targetTank.name)), }); } if (!targetTankAncestry.includes(startingTankId)) { return literals( strings.bot.commands.research.errors.tanks_not_on_line, { tank_1: escapeMarkdown(unwrap(targetTank.name)), tank_2: escapeMarkdown(unwrap(startingTank.name)), } ); } } else { if (targetTank.type !== TankType.RESEARCHABLE) { return literals(strings.bot.commands.research.errors.non_tech_tree, { tank: escapeMarkdown(unwrap(targetTank.name)), }); } if (targetTank.tier === 1) { return literals(strings.bot.commands.research.errors.tier_1, { tank: escapeMarkdown(unwrap(targetTank.name)), }); } const tankStats = await getTankStats(region, id); if (tankStats === null) { return strings.bot.common.errors.no_tank_stats; } if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) { return literals( strings.bot.commands.research.errors.already_researched, { tank: escapeMarkdown(unwrap(targetTank.name)) } ); } const foundAncestor = targetTankAncestry.find( (id) => awaitedTankDefinitions.tanks[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 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 ? emblemURL(clan.emblem_set_id) : undefined; const costs = await Promise.all( line.map(async (id) => { const tank = awaitedTankDefinitions.tanks[id]; const turretXps = new Map(); const gunXps = new Map(); const engineXps = new Map(); const trackXps = new Map(); tank.turrets.forEach((turret) => { if ( turret.research_cost !== undefined && typeof turret.research_cost.research_cost_type!.value === "number" ) { turretXps.set( turret.id, turret.research_cost.research_cost_type!.value ); } turret.guns.forEach((gun) => { if ( gun.research_cost !== undefined && typeof gun.research_cost.research_cost_type!.value === "number" ) { gunXps.set(gun.id, gun.research_cost.research_cost_type!.value); } }); }); tank.engines.forEach((engine) => { if ( engine.research_cost !== undefined && typeof engine.research_cost.research_cost_type!.value === "number" ) { engineXps.set( engine.id, engine.research_cost.research_cost_type!.value ); } }); tank.tracks.forEach((track) => { if ( track.research_cost !== undefined && typeof track.research_cost.research_cost_type!.value === "number" ) { trackXps.set( track.id, track.research_cost.research_cost_type!.value ); } }); 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.research_cost!.research_cost_type!.value as number, upgrades: moduleXp, purchase: tank.price.value, equipment: equipmentPriceMatrix[tank.tier].reduce( (a, b) => a + 3 * b, 0 ), }; }) ); const image = (
{strings.bot.commands.research.body.total}
XP icon Experience:{" "} {costs .reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0) .toLocaleString(interaction.locale)}
Silver icon Purchase:{" "} {costs .reduce((a, b) => a + b.purchase, 0) .toLocaleString(interaction.locale)}
Silver icon Equipment:{" "} {costs .reduce((a, b) => a + b.equipment, 0) .toLocaleString(interaction.locale)}
{await Promise.all( line.map(async (id, index) => { const tank = awaitedTankDefinitions.tanks[id]; return (
{TIER_ROMAN_NUMERALS[tank.tier]}
Tank icon
{unwrap(tank.name)} {unwrap(tank.name)}
XP icon {literals( strings.bot.commands.research.body.research, { cost: costs[index].research?.toLocaleString( interaction.locale ), } )}
{costs[index].upgrades > 0 && (
Free XP icon {literals( strings.bot.commands.research.body.upgrades, { cost: costs[index].upgrades.toLocaleString() } )}
)}
Silver icon {literals( strings.bot.commands.research.body.purchase, { cost: costs[index].purchase.toLocaleString( interaction.locale ), } )}
Silver icon {literals( strings.bot.commands.research.body.equipment, { cost: costs[index].equipment.toLocaleString( interaction.locale ), } )}
); }) )}
); return startingTankRaw ? image : [strings.bot.commands.research.body.estimation, image]; }, autocomplete(interaction) { autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]); }, }); });