Files
blitzkit/packages/bot/src/commands/research.tsx
T

612 lines
20 KiB
TypeScript
Raw Normal View History

2024-09-07 21:45:00 -05:00
import {
asset,
2024-09-08 10:42:34 -05:00
emblemURL,
2024-09-07 21:45:00 -05:00
equipmentPriceMatrix,
2024-09-08 10:17:40 -05:00
getAccountInfo,
getClanAccountInfo,
getTankStats,
2024-09-07 21:45:00 -05:00
TANK_ICONS,
2024-10-05 23:20:04 -05:00
TankType,
2024-09-07 21:45:00 -05:00
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { escapeMarkdown, Locale } from 'discord.js';
2024-09-08 10:11:24 -05:00
import { CommandWrapper } from '../components/CommandWrapper';
import { TitleBar } from '../components/TitleBar';
import { resolveTankId } from '../core/blitz/resolveTankId';
2024-09-08 10:42:34 -05:00
import { buildTechTreeLine } from '../core/blitzkit/buildTechTreeLine';
2024-09-08 10:28:54 -05:00
import { iconPng } from '../core/blitzkit/iconPng';
2024-10-05 23:20:04 -05:00
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises';
2024-09-08 10:42:34 -05:00
import { resolveAncestry } from '../core/blitzkit/resolveAncestry';
import { tankIconPng } from '../core/blitzkit/tankIconPng';
2024-09-08 10:11:24 -05:00
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteTanks } from '../core/discord/autocompleteTanks';
2024-03-14 16:43:28 -05:00
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
2024-03-15 21:50:19 -05:00
import { localizationObject } from '../core/discord/localizationObject';
2024-09-08 10:11:24 -05:00
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
2024-03-15 21:50:19 -05:00
import { translator } from '../core/localization/translator';
2024-03-14 16:43:28 -05:00
import { CommandRegistry } from '../events/interactionCreate';
2024-03-14 17:31:11 -05:00
import { theme } from '../stitches.config';
2024-03-14 16:43:28 -05:00
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
2024-03-15 21:50:19 -05:00
const { t } = translator(Locale.EnglishUS);
2024-03-14 16:43:28 -05:00
resolve({
2024-03-15 21:50:19 -05:00
command: createLocalizedCommand('research')
.addStringOption((option) =>
option
.setName(t`bot.commands.research.options.target_tank`)
.setNameLocalizations(
2024-12-19 11:04:48 -06:00
localizationObject(
'bot.commands.research.options.target_tank',
undefined,
true,
),
2024-03-15 21:50:19 -05:00
)
.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(
2024-12-19 11:04:48 -06:00
localizationObject(
'bot.commands.research.options.starting_tank',
undefined,
true,
),
2024-03-15 21:50:19 -05:00
)
.setDescription(
t`bot.commands.research.options.starting_tank.description`,
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.research.options.starting_tank.description',
),
)
.setAutocomplete(true)
.setRequired(false),
2024-03-18 16:47:53 -05:00
)
.addStringOption(addUsernameChoices),
2024-03-14 16:43:28 -05:00
async handler(interaction) {
2025-01-14 19:22:41 -06:00
const { t, translate, unwrap } = translator(interaction.locale);
2024-03-14 16:43:28 -05:00
const awaitedTankDefinitions = await tankDefinitions;
2024-03-15 21:50:19 -05:00
const targetTankId = await resolveTankId(
interaction.options.getString('target-tank', true),
2024-03-14 16:43:28 -05:00
interaction.locale,
2024-03-15 21:50:19 -05:00
true,
2024-03-14 16:43:28 -05:00
);
2024-03-15 21:50:19 -05:00
const startingTankRaw = interaction.options.getString('starting-tank');
2024-10-05 23:20:04 -05:00
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
2024-03-14 16:43:28 -05:00
const { id, region } = await resolvePlayerFromCommand(interaction);
2024-03-15 21:50:19 -05:00
let startingTankId: number;
const targetTankAncestry = await resolveAncestry(targetTankId);
2024-03-14 16:43:28 -05:00
2024-03-15 21:50:19 -05:00
if (startingTankRaw) {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
);
if (targetTankId === startingTankId) {
return translate('bot.commands.research.errors.start_end_equal', [
2025-01-14 19:22:41 -06:00
escapeMarkdown(unwrap(targetTank.name)),
2024-03-15 21:50:19 -05:00
]);
}
2024-10-05 23:20:04 -05:00
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
2024-03-15 21:50:19 -05:00
if (startingTank.tier > targetTank.tier) {
return translate('bot.commands.research.errors.unordered', [
2025-01-14 19:22:41 -06:00
escapeMarkdown(unwrap(startingTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
2024-03-15 21:50:19 -05:00
]);
}
if (!targetTankAncestry.includes(startingTankId)) {
return translate('bot.commands.research.errors.tanks_not_on_line', [
2025-01-14 19:22:41 -06:00
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(startingTank.name)),
2024-03-15 21:50:19 -05:00
]);
}
} else {
2024-10-05 23:20:04 -05:00
if (targetTank.type !== TankType.RESEARCHABLE) {
2024-03-15 21:50:19 -05:00
return translate('bot.commands.research.errors.non_tech_tree', [
2025-01-14 19:22:41 -06:00
escapeMarkdown(unwrap(targetTank.name)),
2024-03-15 21:50:19 -05:00
]);
}
if (targetTank.tier === 1) {
return translate('bot.commands.research.errors.tier_1', [
2025-01-14 19:22:41 -06:00
escapeMarkdown(unwrap(targetTank.name)),
2024-03-15 21:50:19 -05:00
]);
}
2024-04-15 13:25:35 -05:00
const tankStats = await getTankStats(region, id);
if (tankStats === null) {
return t`bot.common.errors.no_tank_stats`;
}
2024-03-15 21:50:19 -05:00
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return translate('bot.commands.research.errors.already_researched', [
2025-01-14 19:22:41 -06:00
escapeMarkdown(unwrap(targetTank.name)),
2024-03-15 21:50:19 -05:00
]);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
2024-10-05 23:20:04 -05:00
awaitedTankDefinitions.tanks[id].tier === 1 ||
2024-03-15 21:50:19 -05:00
tankStats.some(({ tank_id }) => tank_id === id),
);
if (foundAncestor === undefined) {
throw new Error('No first owned tech tree found.');
}
startingTankId = foundAncestor;
2024-03-14 16:43:28 -05:00
}
2024-03-15 21:50:19 -05:00
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
);
const line = [...techTreeLine, startingTankId].reverse();
2024-03-14 16:43:28 -05:00
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
2024-03-24 22:06:48 -05:00
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
2024-03-15 21:50:19 -05:00
const costs = await Promise.all(
line.map(async (id) => {
2024-10-05 23:20:04 -05:00
const tank = awaitedTankDefinitions.tanks[id];
2024-03-15 21:50:19 -05:00
const turretXps = new Map<number, number>();
const gunXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
2024-03-14 16:43:28 -05:00
2024-03-15 21:50:19 -05:00
tank.turrets.forEach((turret) => {
2024-11-18 10:56:24 -06:00
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,
);
2024-03-15 21:50:19 -05:00
}
turret.guns.forEach((gun) => {
2024-11-18 10:56:24 -06:00
if (
gun.gun_type!.value.base.research_cost !== undefined &&
typeof gun.gun_type!.value.base.research_cost
.research_cost_type!.value === 'number'
) {
2024-10-05 23:20:04 -05:00
gunXps.set(
2024-11-18 10:56:24 -06:00
gun.gun_type!.value.base.id,
gun.gun_type!.value.base.research_cost.research_cost_type!
.value,
2024-10-05 23:20:04 -05:00
);
2024-03-15 21:50:19 -05:00
}
});
});
tank.engines.forEach((engine) => {
2024-11-18 10:56:24 -06:00
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,
);
2024-03-15 21:50:19 -05:00
}
});
tank.tracks.forEach((track) => {
2024-11-18 10:56:24 -06:00
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,
);
2024-03-15 21:50:19 -05:00
}
});
let moduleXp = 0;
turretXps.forEach((value) => {
moduleXp += value;
});
gunXps.forEach((value) => {
moduleXp += value;
});
engineXps.forEach((value) => {
moduleXp += value;
});
trackXps.forEach((value) => {
moduleXp += value;
});
return {
2024-11-18 10:56:24 -06:00
research: tank.research_cost!.research_cost_type!.value as number,
2024-03-15 21:50:19 -05:00
upgrades: moduleXp,
purchase: tank.price.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
),
};
}),
);
const image = (
2024-03-14 16:43:28 -05:00
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
2024-09-07 21:45:00 -05:00
description={`${t`bot.commands.research.body.subtitle`} • ${
targetTank.name
}`}
2024-03-14 16:43:28 -05:00
/>
2024-03-14 17:31:11 -05:00
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
2024-03-15 21:50:19 -05:00
<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
2024-05-04 16:28:12 -05:00
alt="XP icon"
2024-04-24 16:22:48 -05:00
src={await iconPng(asset('icons/currencies/xp.webp'))}
2024-03-15 21:50:19 -05:00
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
2024-03-17 21:40:21 -05:00
Experience:{' '}
2024-03-15 21:50:19 -05:00
{costs
2024-03-17 21:40:21 -05:00
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
2024-03-15 21:50:19 -05:00
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
2024-05-04 16:28:12 -05:00
alt="Silver icon"
2024-04-24 16:22:48 -05:00
src={await iconPng(asset('icons/currencies/silver.webp'))}
2024-03-15 21:50:19 -05:00
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
2024-03-17 21:40:21 -05:00
Purchase:{' '}
2024-03-15 21:50:19 -05:00
{costs
2024-03-17 21:40:21 -05:00
.reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
2024-05-04 16:28:12 -05:00
alt="Silver icon"
2024-04-24 16:22:48 -05:00
src={await iconPng(asset('icons/currencies/silver.webp'))}
2024-03-17 21:40:21 -05:00
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Equipment:{' '}
{costs
.reduce((a, b) => a + b.equipment, 0)
2024-03-15 21:50:19 -05:00
.toLocaleString(interaction.locale)}
</span>
</div>
</div>
</div>
2024-03-14 17:31:11 -05:00
{await Promise.all(
2024-03-15 21:50:19 -05:00
line.map(async (id, index) => {
2024-10-05 23:20:04 -05:00
const tank = awaitedTankDefinitions.tanks[id];
2024-03-14 17:31:11 -05:00
return (
<div
key={id}
style={{
display: 'flex',
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
2024-03-15 21:50:19 -05:00
border: theme.borderStyles.subtle,
2024-03-14 17:31:11 -05:00
}}
>
<div
style={{
width: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
</div>
<div
style={{
flex: 1,
display: 'flex',
position: 'relative',
right: '5%',
}}
>
2024-04-12 09:17:05 -05:00
<img
2024-05-04 16:28:12 -05:00
alt="Tank icon"
2024-04-12 09:17:05 -05:00
src={await tankIconPng(id)}
2024-03-14 17:31:11 -05:00
style={{
2024-04-12 09:17:05 -05:00
height: 100,
width: 100,
objectFit: 'contain',
position: 'absolute',
2024-03-14 17:31:11 -05:00
left: '25%',
2024-04-12 09:17:05 -05:00
top: 0,
2024-03-14 17:31:11 -05:00
}}
2024-04-12 09:17:05 -05:00
/>
2024-03-14 17:31:11 -05:00
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 8,
left: '50%',
transform: 'translateX(-50%)',
}}
>
<img
2025-01-14 19:22:41 -06:00
alt={unwrap(tank.name)}
2024-03-14 17:31:11 -05:00
src={TANK_ICONS[tank.class]}
width={16}
height={16}
/>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
2025-01-14 19:22:41 -06:00
{unwrap(tank.name)}
2024-03-14 17:31:11 -05:00
</span>
</div>
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
2024-03-15 12:28:29 -05:00
justifyContent: 'center',
2024-03-14 17:31:11 -05:00
gap: 8,
padding: '16px 0',
}}
>
2024-03-15 12:28:29 -05:00
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
2024-05-04 16:28:12 -05:00
alt="XP icon"
2024-04-24 16:22:48 -05:00
src={await iconPng(asset('icons/currencies/xp.webp'))}
2024-03-15 12:28:29 -05:00
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
2024-03-15 21:50:19 -05:00
{translate('bot.commands.research.body.research', [
`${costs[index].research?.toLocaleString(
interaction.locale,
)}`,
])}
2024-03-15 12:28:29 -05:00
</span>
</div>
2024-03-15 21:50:19 -05:00
{costs[index].upgrades > 0 && (
2024-03-15 12:28:29 -05:00
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
2024-05-04 16:28:12 -05:00
alt="Free XP icon"
2024-04-24 16:22:48 -05:00
src={await iconPng(
2024-03-15 12:28:29 -05:00
asset('icons/currencies/free-xp.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
2024-03-15 21:50:19 -05:00
{translate('bot.commands.research.body.upgrades', [
costs[index].upgrades.toLocaleString(),
])}
2024-03-15 12:28:29 -05:00
</span>
</div>
)}
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
2024-05-04 16:28:12 -05:00
alt="Silver icon"
2024-04-24 16:22:48 -05:00
src={await iconPng(
2024-03-15 12:28:29 -05:00
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
2024-03-15 21:50:19 -05:00
{translate('bot.commands.research.body.purchase', [
costs[index].purchase.toLocaleString(
interaction.locale,
),
])}
2024-03-15 12:28:29 -05:00
</span>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
2024-05-04 16:28:12 -05:00
alt="Silver icon"
2024-04-24 16:22:48 -05:00
src={await iconPng(
2024-03-15 12:28:29 -05:00
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
2024-03-15 21:50:19 -05:00
{translate('bot.commands.research.body.equipment', [
costs[index].equipment.toLocaleString(
interaction.locale,
),
])}
2024-03-15 12:28:29 -05:00
</span>
</div>
2024-03-14 17:31:11 -05:00
</div>
</div>
);
}),
)}
</div>
2024-03-14 16:43:28 -05:00
</CommandWrapper>
);
2024-03-15 21:50:19 -05:00
return startingTankRaw
? image
: [t`bot.commands.research.body.estimation`, image];
2024-03-14 16:43:28 -05:00
},
autocomplete(interaction) {
2024-03-15 21:50:19 -05:00
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
2024-03-14 16:43:28 -05:00
},
});
});