diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 113998e3f..db8d805a1 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -1022,7 +1022,6 @@ export async function definitions(production: boolean) { writeFile('test.json', JSON.stringify(tankDefinitions, null, 2)); - return; await commitAssets( 'definitions', [ diff --git a/src/commands/ownedTanks.tsx b/src/commands/ownedTanks.tsx index c5faee701..9873f01c1 100644 --- a/src/commands/ownedTanks.tsx +++ b/src/commands/ownedTanks.tsx @@ -32,10 +32,9 @@ export const ownedTanksCommand = new Promise((resolve) => { async handler(interaction) { const { translate } = translator(interaction.locale); const tier = Number(interaction.options.getString('tier')); - const account = await resolvePlayerFromCommand(interaction); - const { id, region: server } = account; - const accountInfo = await getAccountInfo(server, id); - const tankStats = await getTankStats(server, id, interaction.locale); + const { id, region } = await resolvePlayerFromCommand(interaction); + const accountInfo = await getAccountInfo(region, id); + const tankStats = await getTankStats(region, id, interaction.locale); const filteredTanks = ( await Promise.all( tankStats.map(async (tankData) => ({ @@ -44,7 +43,7 @@ export const ownedTanksCommand = new Promise((resolve) => { })), ) ).filter((tank) => tank.tankDefinitions?.tier === tier); - const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']); + const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); const groupedTanks: Record = {}; const nations: string[] = []; diff --git a/src/commands/research.tsx b/src/commands/research.tsx new file mode 100644 index 000000000..8c540c1d9 --- /dev/null +++ b/src/commands/research.tsx @@ -0,0 +1,72 @@ +import { escapeMarkdown } from 'discord.js'; +import CommandWrapper from '../components/CommandWrapper'; +import TitleBar from '../components/TitleBar'; +import { getAccountInfo } from '../core/blitz/getAccountInfo'; +import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; +import getTankStats from '../core/blitz/getTankStats'; +import resolveTankId from '../core/blitz/resolveTankId'; +import { buildTechTreeLine } from '../core/blitzkrieg/buildTechTreeLine'; +import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; +import { resolveAncestry } from '../core/blitzkrieg/resolveAncestry'; +import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions'; +import addTankChoices from '../core/discord/addTankChoices'; +import autocompleteTanks from '../core/discord/autocompleteTanks'; +import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; +import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; +import { CommandRegistry } from '../events/interactionCreate'; + +export const researchCommand = new Promise((resolve) => { + resolve({ + inProduction: true, + inPublic: true, + + command: createLocalizedCommand('research').addStringOption(addTankChoices), + + async handler(interaction) { + const awaitedTankDefinitions = await tankDefinitions; + const tankId = await resolveTankId( + interaction.options.getString('tank', true), + interaction.locale, + ); + 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 { id, region } = await resolvePlayerFromCommand(interaction); + const tankStats = await getTankStats(region, id, interaction.locale); + const ancestry = await resolveAncestry(tankId); + const firstOwnedTechTree = ancestry.find((id) => + tankStats.some(({ tank_id }) => tank_id === id), + ); + + if (firstOwnedTechTree === undefined) { + return `idk what to call this error lol`; + } + + const line = await buildTechTreeLine(firstOwnedTechTree, tankId); + const { nickname } = await getAccountInfo(region, id); + const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; + const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined; + + return ( + + + + ); + }, + + autocomplete(interaction) { + autocompleteTanks(interaction, true); + }, + }); +}); diff --git a/src/core/blitzkrieg/buildTechTreeLine.ts b/src/core/blitzkrieg/buildTechTreeLine.ts new file mode 100644 index 000000000..d3d83bcfb --- /dev/null +++ b/src/core/blitzkrieg/buildTechTreeLine.ts @@ -0,0 +1,19 @@ +import { resolveAncestry } from './resolveAncestry'; +import { tankDefinitions } from './tankDefinitions'; + +export async function buildTechTreeLine(start: number, end: number) { + const awaitedTankDefinitions = await tankDefinitions; + const endTank = awaitedTankDefinitions[end]; + if (!endTank.ancestors) throw new Error('End tank has no ancestors'); + const line = [end]; + + if (start === end) return line; + for (const ancestorId of endTank.ancestors) { + if ((await resolveAncestry(ancestorId)).includes(start)) { + line.push(...(await buildTechTreeLine(start, ancestorId))); + break; + } + } + + return line; +} diff --git a/src/core/blitzkrieg/resolveAncestry.ts b/src/core/blitzkrieg/resolveAncestry.ts new file mode 100644 index 000000000..980c67894 --- /dev/null +++ b/src/core/blitzkrieg/resolveAncestry.ts @@ -0,0 +1,25 @@ +import { tankDefinitions } from './tankDefinitions'; + +export async function resolveAncestry(id: number) { + const awaitedTankDefinitions = await tankDefinitions; + const tank = awaitedTankDefinitions[id]; + const ancestors: number[] = []; + + if (tank.ancestors) { + for (const ancestor of tank.ancestors) { + if (!ancestors.includes(ancestor)) { + ancestors.push(ancestor); + + for (const ancestorLayer2 of await resolveAncestry(ancestor)) { + if (!ancestors.includes(ancestorLayer2)) { + ancestors.push(ancestorLayer2); + } + } + } + } + } + + return ancestors.sort( + (a, b) => awaitedTankDefinitions[b].tier - awaitedTankDefinitions[a].tier, + ); +} diff --git a/src/core/blitzkrieg/tankDefinitions/index.ts b/src/core/blitzkrieg/tankDefinitions/index.ts index 93061395a..8d84e81a6 100644 --- a/src/core/blitzkrieg/tankDefinitions/index.ts +++ b/src/core/blitzkrieg/tankDefinitions/index.ts @@ -1,8 +1,8 @@ +import { readFile } from 'fs/promises'; import { deburr } from 'lodash'; import { Vector2Tuple } from 'three'; import { TankClass, TreeType } from '../../../components/Tanks'; -import { asset } from '../asset'; -import { fetchCdonLz4 } from '../fetchCdonLz4'; +import { superDecompress } from '../superDecompress'; import { TIERS } from './constants'; export type ShellType = 'ap' | 'ap_cr' | 'hc' | 'he'; @@ -137,8 +137,12 @@ export type ShellDefinition = { explosionRadius?: number; }; -export const tankDefinitions = fetchCdonLz4( - asset('definitions/tanks.cdon.lz4'), +// export const tankDefinitions = fetchCdonLz4( +// asset('definitions/tanks.cdon.lz4'), +// ); + +export const tankDefinitions = readFile('test.tanks.cdon.lz4').then((data) => + superDecompress(data.buffer), ); const entries = new Promise(async (resolve) => { @@ -158,6 +162,7 @@ export const tankNames = tanksDefinitionsArray.then((tanks) => id, original: tank.name, combined: `${tank.name}${deburr(tank.name)}${tank.nameFull ? `${tank.nameFull}${deburr(tank.nameFull)}` : ''}`, + treeType: tank.treeType, }; }), ), diff --git a/src/core/discord/autocompleteTanks.ts b/src/core/discord/autocompleteTanks.ts index ac05a3b84..1e15ec13f 100644 --- a/src/core/discord/autocompleteTanks.ts +++ b/src/core/discord/autocompleteTanks.ts @@ -10,8 +10,13 @@ import { OVERFLOW_SUFFIX, } from './autocompleteClan/constants'; +const tankNamesTechTreeOnly = tankNames.then((names) => + names.filter((tank) => tank.treeType === 'researchable'), +); + export default async function autocompleteTanks( interaction: AutocompleteInteraction, + techTreeOnly = false, ) { const focusedOption = interaction.options.getFocused(true); if (focusedOption.name !== 'tank') return; @@ -19,10 +24,14 @@ export default async function autocompleteTanks( await interaction.respond( focusedOption.value ? await Promise.all( - go(focusedOption.value, await tankNames, { - keys: ['combined'], - limit: 10, - }).map(async (item) => { + go( + focusedOption.value, + await (techTreeOnly ? tankNamesTechTreeOnly : tankNames), + { + keys: ['combined'], + limit: 10, + }, + ).map(async (item) => { let name = item.obj.original; if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index 5e18ebfd6..25a277c53 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -26,6 +26,7 @@ import { permissionsCommand } from '../../commands/permissions'; import { pingCommand } from '../../commands/ping'; import { playerInfoCommand } from '../../commands/playerInfo'; import { ratingsCommand } from '../../commands/ratings'; +import { researchCommand } from '../../commands/research'; import { searchClansCommand } from '../../commands/searchClans'; import { searchPlayersCommand } from '../../commands/searchPlayers'; import { statsCommand } from '../../commands/stats'; @@ -92,6 +93,7 @@ export const COMMANDS_RAW: Promise[] = [ statsCommand, ratingsCommand, todayCommand, + researchCommand, ]; export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => { diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 25fe514a7..3d236abd6 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -195,6 +195,11 @@ }, "commands": { + "research": { + "$": "research", + "description": "Lists the expenses in researching a line" + }, + "about": { "$": "about", "description": "All the info you need about the bot",