From 2b7907f8f75f300aea081b386a5dd62e96b3def1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:29:28 -0500 Subject: [PATCH 01/14] fix: use main --- .github/workflows/cd-assets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-assets.yml b/.github/workflows/cd-assets.yml index 4d840d1fd..3b7757240 100644 --- a/.github/workflows/cd-assets.yml +++ b/.github/workflows/cd-assets.yml @@ -61,4 +61,4 @@ jobs: $env:GH_TOKEN = "${{ secrets.GH_TOKEN }}" $env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}" $env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}" - yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/production' && '--production' || '' }} + yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }} From 537206883055e209ae1135058c215a8b892db7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 13 Mar 2024 23:23:06 -0500 Subject: [PATCH 02/14] feat: added trees --- scripts/buildAssets/definitions.ts | 133 +++++++++++++------ scripts/tsconfig.json | 18 +++ src/core/blitzkrieg/tankDefinitions/index.ts | 2 + 3 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 scripts/tsconfig.json diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 52452b722..113998e3f 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -1,4 +1,4 @@ -import { readdir } from 'fs/promises'; +import { readdir, writeFile } from 'fs/promises'; import { parse as parsePath } from 'path'; import { Vector3Tuple } from 'three'; import { parse as parseYaml } from 'yaml'; @@ -79,6 +79,16 @@ type VehicleDefinitionArmor = Record< string, number | { vehicleDamageFactor: 0; '#text': number } >; +interface UnlocksInner { + cost: number; + '#text': number; +} +type Unlocks = { + [key in 'vehicle' | 'engine' | 'chassis' | 'turret' | 'gun']: + | UnlocksInner + | UnlocksInner[]; +}; +type UnlocksListing = Unlocks | undefined; interface VehicleDefinitions { invisibility: { moving: number; @@ -102,6 +112,7 @@ interface VehicleDefinitions { }; chassis: { [key: string]: { + unlocks: UnlocksListing; weight: number; terrainResistance: string; rotationSpeed: number; @@ -130,10 +141,13 @@ interface VehicleDefinitions { }; }; engines: { - [key: string]: unknown; + [key: string]: { + unlocks: UnlocksListing; + }; }; turrets0: { [key: string]: { + unlocks: UnlocksListing; rotationSpeed: number; weight: number; circularVisionRadius: number; @@ -149,6 +163,7 @@ interface VehicleDefinitions { }; guns: { [key: string]: { + unlocks: UnlocksListing; armor: VehicleDefinitionArmor & { gun?: | number @@ -427,6 +442,7 @@ export async function definitions(production: boolean) { }); const crew: Crew[] = []; const fixedCamouflage = tankTags.includes('eventCamouflage_user'); + const totalUnlocks: UnlocksListing[] = []; Object.entries(tankDefinition.root.crew).forEach(([key, value]) => { let entry: Crew; @@ -532,6 +548,7 @@ export async function definitions(production: boolean) { .split(' ') .map(Number) as Vector3Tuple; + totalUnlocks.push(track.unlocks); tankDefinitions[tankId].tracks.push({ id: trackId, weight: track.weight, @@ -558,16 +575,18 @@ export async function definitions(production: boolean) { }); Object.keys(tankDefinition.root.engines).forEach((engineKey) => { - const engine = enginesList.root.shared[engineKey]; + const engine = tankDefinition.root.engines[engineKey]; + const engineListEntry = enginesList.root.shared[engineKey]; const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]); + totalUnlocks.push(engine.unlocks); tankDefinitions[tankId].engines.push({ id: engineId, - name: strings[engine.userString], - fireChance: engine.fireStartingChance, - tier: engine.level as Tier, - weight: engine.weight, - power: engine.power, + name: strings[engineListEntry.userString], + fireChance: engineListEntry.fireStartingChance, + tier: engineListEntry.level as Tier, + weight: engineListEntry.weight, + power: engineListEntry.power, }); }); @@ -594,6 +613,7 @@ export async function definitions(production: boolean) { .map(Number) as Vector3Tuple; const turretArmor: ModelArmor = { thickness: {} }; + totalUnlocks.push(turret.unlocks); Object.keys(turret.armor) .filter((name) => name.startsWith('armor_')) .forEach((name) => { @@ -646,11 +666,11 @@ export async function definitions(production: boolean) { }; Object.keys(turret.guns).forEach((gunKey, gunIndex) => { - const turretGunEntry = turret.guns[gunKey]; + const gun = turret.guns[gunKey]; const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); const gunListEntry = gunList.root.shared[gunKey]; const pitchLimitsRaw = - turretGunEntry.pitchLimits ?? gunListEntry.pitchLimits; + gun.pitchLimits ?? gunListEntry.pitchLimits; const gunPitch = ( typeof pitchLimitsRaw === 'string' ? pitchLimitsRaw @@ -659,43 +679,41 @@ export async function definitions(production: boolean) { .split(' ') .map(Number) as [number, number]; const gunModel = Number( - parsePath(turretGunEntry.models.undamaged).name.split('_')[1], + parsePath(gun.models.undamaged).name.split('_')[1], ); const gunName = strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' '); const gunType = - 'clip' in turretGunEntry - ? turretGunEntry.pumpGunMode + 'clip' in gun + ? gun.pumpGunMode ? 'autoReloader' : 'autoLoader' : 'regular'; const gunReload = gunType === 'autoReloader' - ? turretGunEntry.pumpGunReloadTimes!.split(' ').map(Number) - : turretGunEntry.reloadTime; + ? gun.pumpGunReloadTimes!.split(' ').map(Number) + : gun.reloadTime; const gunClipCount = - gunType === 'regular' ? undefined : turretGunEntry.clip!.count; + gunType === 'regular' ? undefined : gun.clip!.count; const gunIntraClip = - gunType === 'regular' - ? undefined - : 60 / turretGunEntry.clip!.rate; - const front = turretGunEntry.extraPitchLimits?.front - ? turretGunEntry.extraPitchLimits.front.split(' ').map(Number) + gunType === 'regular' ? undefined : 60 / gun.clip!.rate; + const front = gun.extraPitchLimits?.front + ? gun.extraPitchLimits.front.split(' ').map(Number) : undefined; - const back = turretGunEntry.extraPitchLimits?.back - ? turretGunEntry.extraPitchLimits.back.split(' ').map(Number) + const back = gun.extraPitchLimits?.back + ? gun.extraPitchLimits.back.split(' ').map(Number) : undefined; - const transition = turretGunEntry.extraPitchLimits?.transition - ? typeof turretGunEntry.extraPitchLimits.transition === 'number' - ? turretGunEntry.extraPitchLimits.transition - : turretGunEntry.extraPitchLimits.transition.at(-1)! + const transition = gun.extraPitchLimits?.transition + ? typeof gun.extraPitchLimits.transition === 'number' + ? gun.extraPitchLimits.transition + : gun.extraPitchLimits.transition.at(-1)! : undefined; const gunArmor: ModelArmor = { thickness: {} }; const shotDispersionFactors = - turretGunEntry.shotDispersionFactors ?? - gunListEntry.shotDispersionFactors; + gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors; - Object.keys(turretGunEntry.armor) + totalUnlocks.push(gun.unlocks); + Object.keys(gun.armor) .filter((name) => name.startsWith('armor_')) .forEach((name) => { const armorIdString = name.match(/armor_(\d+)/)?.[1]; @@ -703,7 +721,7 @@ export async function definitions(production: boolean) { throw new SyntaxError(`Invalid armor id: ${name}`); } const armorId = parseInt(armorIdString); - const armorRaw = turretGunEntry.armor[name]; + const armorRaw = gun.armor[name]; if (typeof armorRaw === 'number') { gunArmor.thickness[armorId] = armorRaw; } else { @@ -725,13 +743,13 @@ export async function definitions(production: boolean) { count: gunClipCount, intraClip: gunIntraClip, camouflageLoss: - typeof turretGunEntry.invisibilityFactorAtShot === 'number' - ? turretGunEntry.invisibilityFactorAtShot - : turretGunEntry.invisibilityFactorAtShot.at(-1)!, - aimTime: turretGunEntry.aimingTime ?? gunListEntry.aimingTime, + typeof gun.invisibilityFactorAtShot === 'number' + ? gun.invisibilityFactorAtShot + : gun.invisibilityFactorAtShot.at(-1)!, + aimTime: gun.aimingTime ?? gunListEntry.aimingTime, dispersion: { base: - turretGunEntry.shotDispersionRadius ?? + gun.shotDispersionRadius ?? gunListEntry.shotDispersionRadius, damaged: shotDispersionFactors.whileGunDamaged, shot: shotDispersionFactors.afterShot, @@ -743,11 +761,11 @@ export async function definitions(production: boolean) { armor: gunArmor, model: gunModel, thickness: - turretGunEntry.armor.gun === undefined + gun.armor.gun === undefined ? 0 - : typeof turretGunEntry.armor.gun === 'number' - ? turretGunEntry.armor.gun - : turretGunEntry.armor.gun['#text'], + : typeof gun.armor.gun === 'number' + ? gun.armor.gun + : gun.armor.gun['#text'], pitch: { min: gunPitch[0], max: gunPitch[1], @@ -809,10 +827,42 @@ export async function definitions(production: boolean) { }); }, ); + + totalUnlocks.forEach((unlocks) => { + if (unlocks === undefined) return; + Object.entries(unlocks).forEach(([key, value]) => { + if ((key as keyof Unlocks) !== 'vehicle') return; + + (Array.isArray(value) ? value : [value]).forEach((vehicle) => { + const tankListEntry = tankList.root[vehicle['#text']]; + const successorId = toUniqueId(nation, tankListEntry.id); + + if (tankDefinitions[tankId].successors === undefined) { + tankDefinitions[tankId].successors = []; + } + + if (!tankDefinitions[tankId].successors!.includes(successorId)) { + tankDefinitions[tankId].successors!.push(successorId); + } + }); + }); + }); } }), ); + Object.values(tankDefinitions).forEach((tank) => { + tank.successors?.forEach((predecessorId) => { + if (tankDefinitions[predecessorId].ancestors === undefined) { + tankDefinitions[predecessorId].ancestors = []; + } + + if (!tankDefinitions[predecessorId].ancestors?.includes(tank.id)) { + tankDefinitions[predecessorId].ancestors?.push(tank.id); + } + }); + }); + Object.entries(optionalDevices.root).forEach( ([optionalDeviceKey, optionalDeviceEntry]) => { if (optionalDeviceKey === 'nextAvailableId') return; @@ -970,6 +1020,9 @@ export async function definitions(production: boolean) { } }); + writeFile('test.json', JSON.stringify(tankDefinitions, null, 2)); + + return; await commitAssets( 'definitions', [ diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000..e84103846 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "jsx": "preserve", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "incremental": true, + "isolatedModules": true + } +} diff --git a/src/core/blitzkrieg/tankDefinitions/index.ts b/src/core/blitzkrieg/tankDefinitions/index.ts index 7edca84ff..93061395a 100644 --- a/src/core/blitzkrieg/tankDefinitions/index.ts +++ b/src/core/blitzkrieg/tankDefinitions/index.ts @@ -22,6 +22,8 @@ export interface Crew { export interface TankDefinition { id: number; fixedCamouflage?: boolean; + ancestors?: number[]; + successors?: number[]; crew: Crew[]; health: number; nation: string; From 43e1d53d39f002b189c94e3aeaf46de2a0868253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 14 Mar 2024 16:43:28 -0500 Subject: [PATCH 03/14] feat: resolving lines --- scripts/buildAssets/definitions.ts | 1 - src/commands/ownedTanks.tsx | 9 ++- src/commands/research.tsx | 72 ++++++++++++++++++++ src/core/blitzkrieg/buildTechTreeLine.ts | 19 ++++++ src/core/blitzkrieg/resolveAncestry.ts | 25 +++++++ src/core/blitzkrieg/tankDefinitions/index.ts | 13 ++-- src/core/discord/autocompleteTanks.ts | 17 +++-- src/events/interactionCreate/index.ts | 2 + src/lang/en-US.json | 5 ++ 9 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 src/commands/research.tsx create mode 100644 src/core/blitzkrieg/buildTechTreeLine.ts create mode 100644 src/core/blitzkrieg/resolveAncestry.ts 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", From 476d8e804acf57e1e8a18801a30e98a1330b7389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:31:11 -0500 Subject: [PATCH 04/14] feat: some ui --- package.json | 2 +- src/commands/research.tsx | 132 ++++++++++++++++++++++++++++- src/components/CommandWrapper.tsx | 2 +- src/core/blitzkrieg/tankIconPng.ts | 9 ++ yarn.lock | 2 +- 5 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/core/blitzkrieg/tankIconPng.ts diff --git a/package.json b/package.json index 3546cc0e0..110e4ffdc 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@octokit/plugin-throttling": "^8.1.3", "@octokit/rest": "^20.0.1", "@prisma/client": "^5.4.1", - "@radix-ui/colors": "^3.0.0-rc.4", + "@radix-ui/colors": "^3.0.0", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/themes": "^2.0.1", "@react-three/drei": "^9.93.0", diff --git a/src/commands/research.tsx b/src/commands/research.tsx index 8c540c1d9..ef66a62ca 100644 --- a/src/commands/research.tsx +++ b/src/commands/research.tsx @@ -9,11 +9,17 @@ 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 { + TANK_ICONS, + 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 resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import { CommandRegistry } from '../events/interactionCreate'; +import { theme } from '../stitches.config'; export const researchCommand = new Promise((resolve) => { resolve({ @@ -41,15 +47,19 @@ export const researchCommand = new Promise((resolve) => { 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), + const firstOwnedTechTree = ancestry.find( + (id) => + awaitedTankDefinitions[id].tier === 1 || + 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 line = ( + await buildTechTreeLine(firstOwnedTechTree, tankId) + ).reverse(); const { nickname } = await getAccountInfo(region, id); const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined; @@ -61,6 +71,122 @@ export const researchCommand = new Promise((resolve) => { image={clanImage} description={`Research β€’ ${tank.name}`} /> + +
+ {await Promise.all( + line.map(async (id) => { + const tank = awaitedTankDefinitions[id]; + + return ( +
+
+ + {TIER_ROMAN_NUMERALS[tank.tier]} + +
+ +
+
+ +
+ +
+ + + + {tank.name} + +
+
+ +
+ Research: 144,000 + Upgrades: 62,000 + Purchase: 1,500,000 + Equipment: 450,000 +
+
+ ); + }), + )} +
); }, diff --git a/src/components/CommandWrapper.tsx b/src/components/CommandWrapper.tsx index bf9b99f49..bc75f1756 100644 --- a/src/components/CommandWrapper.tsx +++ b/src/components/CommandWrapper.tsx @@ -16,7 +16,7 @@ export default function CommandWrapper({ children, fat }: CommandWrapperProps) { gap: 16, width: 480, color: theme.colors.textHighContrast, - background: 'url(https://i.imgur.com/PhS06NJ.png)', + backgroundColor: theme.colors.appBackground1, }} > {children} diff --git a/src/core/blitzkrieg/tankIconPng.ts b/src/core/blitzkrieg/tankIconPng.ts new file mode 100644 index 000000000..1fb197cfb --- /dev/null +++ b/src/core/blitzkrieg/tankIconPng.ts @@ -0,0 +1,9 @@ +import sharp from 'sharp'; +import { tankIcon } from './tankIcon'; + +export async function tankIconPng(id: number) { + const imageResponse = await fetch(tankIcon(id)); + const imageBufferWEBP = await imageResponse.arrayBuffer(); + const imageBufferPNG = await sharp(imageBufferWEBP).png().toBuffer(); + return `data:image/png;base64,${imageBufferPNG.toString('base64')}`; +} diff --git a/yarn.lock b/yarn.lock index 7029eaa43..f02ec5e07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1156,7 +1156,7 @@ dependencies: "@prisma/debug" "5.9.1" -"@radix-ui/colors@^3.0.0", "@radix-ui/colors@^3.0.0-rc.4": +"@radix-ui/colors@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-3.0.0.tgz#e8a591a303c44e503bd1212cacf40a09511165e0" integrity sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg== From da02a089b96ac5d6c66510cdb8a20e43ab0f7a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 14 Mar 2024 23:31:50 -0500 Subject: [PATCH 05/14] feat: xp definitions --- scripts/buildAssets/definitions.ts | 86 +++++++++++++++++--- src/components/CommandWrapper.tsx | 2 +- src/core/blitzkrieg/tankDefinitions/index.ts | 21 +++-- 3 files changed, 88 insertions(+), 21 deletions(-) diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index db8d805a1..8ff6eb0b9 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -1,4 +1,4 @@ -import { readdir, writeFile } from 'fs/promises'; +import { readdir } from 'fs/promises'; import { parse as parsePath } from 'path'; import { Vector3Tuple } from 'three'; import { parse as parseYaml } from 'yaml'; @@ -368,6 +368,12 @@ export async function definitions(production: boolean) { `${DATA}/${POI.provisionsCommon}.dvpl`, ); + const tankXps = new Map(); + const gunXps = new Map(); + const turretXps = new Map(); + const engineXps = new Map(); + const trackXps = new Map(); + await Promise.all( nations.map(async (nation) => { const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>( @@ -830,19 +836,57 @@ export async function definitions(production: boolean) { totalUnlocks.forEach((unlocks) => { if (unlocks === undefined) return; + Object.entries(unlocks).forEach(([key, value]) => { - if ((key as keyof Unlocks) !== 'vehicle') return; - (Array.isArray(value) ? value : [value]).forEach((vehicle) => { - const tankListEntry = tankList.root[vehicle['#text']]; - const successorId = toUniqueId(nation, tankListEntry.id); + switch (key as keyof Unlocks) { + case 'vehicle': { + const tankListEntry = tankList.root[vehicle['#text']]; + const currentTank = tankDefinitions[tankId]; + const successorId = toUniqueId(nation, tankListEntry.id); - if (tankDefinitions[tankId].successors === undefined) { - tankDefinitions[tankId].successors = []; - } + tankXps.set(successorId, vehicle.cost); - if (!tankDefinitions[tankId].successors!.includes(successorId)) { - tankDefinitions[tankId].successors!.push(successorId); + if (currentTank.successors === undefined) { + currentTank.successors = []; + } + if (!currentTank.successors!.includes(successorId)) { + currentTank.successors!.push(successorId); + } + break; + } + + case 'gun': { + gunXps.set( + toUniqueId(nation, gunList.root.ids[vehicle['#text']]), + vehicle.cost, + ); + break; + } + + case 'turret': { + turretXps.set( + toUniqueId(nation, turretList.root.ids[vehicle['#text']]), + vehicle.cost, + ); + break; + } + + case 'engine': { + engineXps.set( + toUniqueId(nation, enginesList.root.ids[vehicle['#text']]), + vehicle.cost, + ); + break; + } + + case 'chassis': { + trackXps.set( + toUniqueId(nation, chassisList.root.ids[vehicle['#text']]), + vehicle.cost, + ); + break; + } } }); }); @@ -851,6 +895,26 @@ export async function definitions(production: boolean) { }), ); + Object.values(tankDefinitions).forEach((tank) => { + tank.xp = tankXps.get(tank.id); + + Object.values(tank.turrets).forEach((turret) => { + turret.xp = turretXps.get(turret.id); + + Object.values(turret.guns).forEach((gun) => { + gun.xp = gunXps.get(gun.id); + }); + }); + + Object.values(tank.engines).forEach((engine) => { + engine.xp = engineXps.get(engine.id); + }); + + Object.values(tank.tracks).forEach((track) => { + track.xp = trackXps.get(track.id); + }); + }); + Object.values(tankDefinitions).forEach((tank) => { tank.successors?.forEach((predecessorId) => { if (tankDefinitions[predecessorId].ancestors === undefined) { @@ -1020,8 +1084,6 @@ export async function definitions(production: boolean) { } }); - writeFile('test.json', JSON.stringify(tankDefinitions, null, 2)); - await commitAssets( 'definitions', [ diff --git a/src/components/CommandWrapper.tsx b/src/components/CommandWrapper.tsx index bc75f1756..5a953ef3f 100644 --- a/src/components/CommandWrapper.tsx +++ b/src/components/CommandWrapper.tsx @@ -16,7 +16,7 @@ export default function CommandWrapper({ children, fat }: CommandWrapperProps) { gap: 16, width: 480, color: theme.colors.textHighContrast, - backgroundColor: theme.colors.appBackground1, + background: theme.colors.appBackground1, }} > {children} diff --git a/src/core/blitzkrieg/tankDefinitions/index.ts b/src/core/blitzkrieg/tankDefinitions/index.ts index 8d84e81a6..5891d2ba0 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 { superDecompress } from '../superDecompress'; +import { asset } from '../asset'; +import { fetchCdonLz4 } from '../fetchCdonLz4'; import { TIERS } from './constants'; export type ShellType = 'ap' | 'ap_cr' | 'hc' | 'he'; @@ -39,6 +39,7 @@ export interface TankDefinition { engines: EngineDefinition[]; tracks: TrackDefinition[]; price: TankDefinitionPrice; + xp?: number; speed: { forwards: number; backwards: number; @@ -56,6 +57,7 @@ export interface TrackDefinition { tier: Tier; weight: number; traverseSpeed: number; + xp?: number; dispersion: { move: number; traverse: number; @@ -69,6 +71,7 @@ export interface TrackDefinition { export interface EngineDefinition { id: number; name: string; + xp?: number; tier: Tier; fireChance: number; power: number; @@ -81,6 +84,7 @@ export interface TurretDefinition { health: number; viewRange: number; traverseSpeed: number; + xp?: number; id: number; name: string; tier: Tier; @@ -93,6 +97,7 @@ export type GunDefinition = | GunDefinitionAutoReloader; interface GunDefinitionBase { rotationSpeed: number; + xp?: number; weight: number; id: number; name: string; @@ -137,14 +142,14 @@ export type ShellDefinition = { explosionRadius?: number; }; -// export const tankDefinitions = fetchCdonLz4( -// asset('definitions/tanks.cdon.lz4'), -// ); - -export const tankDefinitions = readFile('test.tanks.cdon.lz4').then((data) => - superDecompress(data.buffer), +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) => { resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry)); }); From 43442ac82940126ae8087037ebd1fb4025d4f0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 15 Mar 2024 00:21:58 -0500 Subject: [PATCH 06/14] feat: start of protobuf --- .../blitzkrieg/tankDefinitions/index.proto | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/core/blitzkrieg/tankDefinitions/index.proto diff --git a/src/core/blitzkrieg/tankDefinitions/index.proto b/src/core/blitzkrieg/tankDefinitions/index.proto new file mode 100644 index 000000000..38300fabb --- /dev/null +++ b/src/core/blitzkrieg/tankDefinitions/index.proto @@ -0,0 +1,110 @@ +syntax = "proto3"; + +message TankDefinitions { + map tanks = 1; +} + +message TankDefinition { + enum Nation { + USA = 0; + GERMANY = 1; + USSR = 2; + UK = 3; + JAPAN = 4; + CHINA = 5; + FRANCE = 6; + EUROPEAN = 7; + OTHER = 8; + } + + enum TreeType { + RESEARCHABLE = 0; + PREMIUM = 1; + COLLECTOR = 2; + } + + enum Class { + LIGHT = 0; + MEDIUM = 1; + HEAVY = 2; + TANK_DESTROYER = 3; + } + + message Price { + enum PriceType { + CREDITS = 0; + GOLD = 1; + } + + PriceType type = 1; + uint32 value = 2; + } + + uint32 id = 1; + string name = 2; + optional string nameFull = 3; + uint32 tier = 4; + Nation nation = 5; + TreeType treeType = 6; + Class class = 7; + Price price = 8; + bool testing = 9; + + message Speed { + uint32 forwards = 1; + uint32 backwards = 2; + } + + message Camouflage { + uint32 still = 1; + uint32 moving = 2; + uint32 onFire = 3; + } + + uint32 health = 10; + repeated Crew crew = 11; + uint32 consumables = 12; + uint32 provisions = 13; + Speed speed = 14; + Camouflage camouflage = 15; + uint32 weight = 16; + string equipment = 17; + optional bool fixedCamouflage = 18; + + optional uint32 xp = 19; + repeated uint32 ancestors = 20; + repeated uint32 successors = 21; + repeated Turret turrets = 22; + repeated Engine engines = 23; + repeated Track tracks = 24; +} + +message Turret { + uint32 id = 1; + string name = 2; + uint32 tier = 3; + + uint32 health = 4; + uint32 viewRange = 5; + uint32 traverseSpeed = 6; + uint32 weight = 7; + + optional uint32 xp = 8; + repeated Gun guns = 9; +} + +message Gun {} + +message Crew { + enum CrewType { + COMMANDER = 0; + RADIOMAN = 1; + GUNNER = 2; + DRIVER = 3; + LOADER = 4; + } + + CrewType type = 1; + uint32 count = 2; + repeated CrewType substitute = 3; +} From 035f751bf1a893fa552fea80d2545b0a31cb0e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:47:05 -0500 Subject: [PATCH 07/14] feat: remove protobuf --- .../blitzkrieg/tankDefinitions/index.proto | 110 ------------------ 1 file changed, 110 deletions(-) delete mode 100644 src/core/blitzkrieg/tankDefinitions/index.proto diff --git a/src/core/blitzkrieg/tankDefinitions/index.proto b/src/core/blitzkrieg/tankDefinitions/index.proto deleted file mode 100644 index 38300fabb..000000000 --- a/src/core/blitzkrieg/tankDefinitions/index.proto +++ /dev/null @@ -1,110 +0,0 @@ -syntax = "proto3"; - -message TankDefinitions { - map tanks = 1; -} - -message TankDefinition { - enum Nation { - USA = 0; - GERMANY = 1; - USSR = 2; - UK = 3; - JAPAN = 4; - CHINA = 5; - FRANCE = 6; - EUROPEAN = 7; - OTHER = 8; - } - - enum TreeType { - RESEARCHABLE = 0; - PREMIUM = 1; - COLLECTOR = 2; - } - - enum Class { - LIGHT = 0; - MEDIUM = 1; - HEAVY = 2; - TANK_DESTROYER = 3; - } - - message Price { - enum PriceType { - CREDITS = 0; - GOLD = 1; - } - - PriceType type = 1; - uint32 value = 2; - } - - uint32 id = 1; - string name = 2; - optional string nameFull = 3; - uint32 tier = 4; - Nation nation = 5; - TreeType treeType = 6; - Class class = 7; - Price price = 8; - bool testing = 9; - - message Speed { - uint32 forwards = 1; - uint32 backwards = 2; - } - - message Camouflage { - uint32 still = 1; - uint32 moving = 2; - uint32 onFire = 3; - } - - uint32 health = 10; - repeated Crew crew = 11; - uint32 consumables = 12; - uint32 provisions = 13; - Speed speed = 14; - Camouflage camouflage = 15; - uint32 weight = 16; - string equipment = 17; - optional bool fixedCamouflage = 18; - - optional uint32 xp = 19; - repeated uint32 ancestors = 20; - repeated uint32 successors = 21; - repeated Turret turrets = 22; - repeated Engine engines = 23; - repeated Track tracks = 24; -} - -message Turret { - uint32 id = 1; - string name = 2; - uint32 tier = 3; - - uint32 health = 4; - uint32 viewRange = 5; - uint32 traverseSpeed = 6; - uint32 weight = 7; - - optional uint32 xp = 8; - repeated Gun guns = 9; -} - -message Gun {} - -message Crew { - enum CrewType { - COMMANDER = 0; - RADIOMAN = 1; - GUNNER = 2; - DRIVER = 3; - LOADER = 4; - } - - CrewType type = 1; - uint32 count = 2; - repeated CrewType substitute = 3; -} From 787d3ae6eaec66b8f3d03e635c5ed7a6213bb1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:28:29 -0500 Subject: [PATCH 08/14] feat: functionality and icons --- scripts/buildAssets/currencies.ts | 39 +++++ scripts/buildAssets/definitions.ts | 46 +++--- scripts/buildAssets/index.ts | 2 + src/commands/research.tsx | 152 ++++++++++++++++++- src/constants/equipmentPrice.ts | 15 ++ src/core/blitzkrieg/iconPng.ts | 14 ++ src/core/blitzkrieg/tankDefinitions/index.ts | 16 +- src/core/blitzkrieg/tankIconPng.ts | 9 +- 8 files changed, 253 insertions(+), 40 deletions(-) create mode 100644 scripts/buildAssets/currencies.ts create mode 100644 src/constants/equipmentPrice.ts create mode 100644 src/core/blitzkrieg/iconPng.ts diff --git a/scripts/buildAssets/currencies.ts b/scripts/buildAssets/currencies.ts new file mode 100644 index 000000000..8764410e8 --- /dev/null +++ b/scripts/buildAssets/currencies.ts @@ -0,0 +1,39 @@ +import sharp from 'sharp'; +import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; +import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; +import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA } from './constants'; + +const ICONS = [ + ['currency_silver_m.packed.webp', 'silver'], + ['currency_premium_xl.packed.webp', 'premium'], + ['currency_gold_m.packed.webp', 'gold'], + ['currency_free-xp_xl.packed.webp', 'free-xp'], + ['currency_elite-xp_xl.packed.webp', 'elite-xp'], + ['currency_crew-xp_xl.packed.webp', 'crew-xp'], + ['currency_battle-xp_xl.packed.webp', 'xp'], +]; + +export async function currencies(production: boolean) { + console.log('Building currency icons...'); + + const changes = await Promise.all( + ICONS.map(async ([file, name]) => { + const content = ( + await sharp( + await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}.dvpl`), + ) + .trim() + .toBuffer() + ).toString('base64'); + + return { + content, + encoding: 'base64', + path: `icons/currencies/${name}.webp`, + } satisfies FileChange; + }), + ); + + await commitAssets('currency icons', changes, production); +} diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 8ff6eb0b9..d6dd7b58f 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -1,4 +1,4 @@ -import { readdir } from 'fs/promises'; +import { readdir, writeFile } from 'fs/promises'; import { parse as parsePath } from 'path'; import { Vector3Tuple } from 'three'; import { parse as parseYaml } from 'yaml'; @@ -369,10 +369,6 @@ export async function definitions(production: boolean) { ); const tankXps = new Map(); - const gunXps = new Map(); - const turretXps = new Map(); - const engineXps = new Map(); - const trackXps = new Map(); await Promise.all( nations.map(async (nation) => { @@ -406,6 +402,10 @@ export async function definitions(production: boolean) { ); for (const tankKey in tankList.root) { + const gunXps = new Map(); + const turretXps = new Map(); + const engineXps = new Map(); + const trackXps = new Map(); const tank = tankList.root[tankKey]; const tankPrice: TankDefinitionPrice = typeof tank.price === 'number' @@ -891,28 +891,28 @@ export async function definitions(production: boolean) { }); }); }); + + Object.values(tankDefinitions[tankId].turrets).forEach((turret) => { + turret.xp = turretXps.get(turret.id); + + Object.values(turret.guns).forEach((gun) => { + gun.xp = gunXps.get(gun.id); + }); + }); + + Object.values(tankDefinitions[tankId].engines).forEach((engine) => { + engine.xp = engineXps.get(engine.id); + }); + + Object.values(tankDefinitions[tankId].tracks).forEach((track) => { + track.xp = trackXps.get(track.id); + }); } }), ); Object.values(tankDefinitions).forEach((tank) => { tank.xp = tankXps.get(tank.id); - - Object.values(tank.turrets).forEach((turret) => { - turret.xp = turretXps.get(turret.id); - - Object.values(turret.guns).forEach((gun) => { - gun.xp = gunXps.get(gun.id); - }); - }); - - Object.values(tank.engines).forEach((engine) => { - engine.xp = engineXps.get(engine.id); - }); - - Object.values(tank.tracks).forEach((track) => { - track.xp = trackXps.get(track.id); - }); }); Object.values(tankDefinitions).forEach((tank) => { @@ -1084,6 +1084,10 @@ export async function definitions(production: boolean) { } }); + writeFile( + 'test.tanks.cdon.lz4', + Buffer.from(superCompress(tankDefinitions), 'base64'), + ); await commitAssets( 'definitions', [ diff --git a/scripts/buildAssets/index.ts b/scripts/buildAssets/index.ts index b864c754f..643ee2b6b 100644 --- a/scripts/buildAssets/index.ts +++ b/scripts/buildAssets/index.ts @@ -14,6 +14,7 @@ import { boosterIcons } from './boosterIcons'; import { camouflageIcons } from './camouflageIcons'; import { circleFlags } from './circleFlags'; import { consumableProvisionIcons } from './consumableProvisionIcons'; +import { currencies } from './currencies'; import { definitions } from './definitions'; import { equipmentIcons } from './equipmentIcons'; import { moduleIcons } from './moduleIcons'; @@ -38,6 +39,7 @@ const methods = [ boosterIcons, tankArmors, tankModels, + currencies, ]; for (const method of methods) { diff --git a/src/commands/research.tsx b/src/commands/research.tsx index ef66a62ca..cc479c280 100644 --- a/src/commands/research.tsx +++ b/src/commands/research.tsx @@ -1,12 +1,15 @@ import { escapeMarkdown } from 'discord.js'; import CommandWrapper from '../components/CommandWrapper'; import TitleBar from '../components/TitleBar'; +import { equipmentPriceMatrix } from '../constants/equipmentPrice'; 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 { asset } from '../core/blitzkrieg/asset'; import { buildTechTreeLine } from '../core/blitzkrieg/buildTechTreeLine'; import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; +import { iconPng } from '../core/blitzkrieg/iconPng'; import { resolveAncestry } from '../core/blitzkrieg/resolveAncestry'; import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions'; import { @@ -54,7 +57,7 @@ export const researchCommand = new Promise((resolve) => { ); if (firstOwnedTechTree === undefined) { - return `idk what to call this error lol`; + throw new Error('No first owned tech tree found.'); } const line = ( @@ -82,6 +85,48 @@ export const researchCommand = new Promise((resolve) => { {await Promise.all( line.map(async (id) => { const tank = awaitedTankDefinitions[id]; + const turretXps = new Map(); + const gunXps = new Map(); + const engineXps = new Map(); + const trackXps = new Map(); + + 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 (
((resolve) => { flex: 1, display: 'flex', flexDirection: 'column', + justifyContent: 'center', gap: 8, padding: '16px 0', }} > - Research: 144,000 - Upgrades: 62,000 - Purchase: 1,500,000 - Equipment: 450,000 +
+ + + Research:{' '} + {tank.xp?.toLocaleString(interaction.locale)} + +
+ + {moduleXp > 0 && ( +
+ + + Upgrades: {moduleXp.toLocaleString()} + +
+ )} + +
+ + + Purchase:{' '} + {tank.price.value.toLocaleString(interaction.locale)} + +
+ +
+ + + Equipment:{' '} + {equipmentPriceMatrix[tank.tier] + .reduce((a, b) => a + 3 * b, 0) + .toLocaleString(interaction.locale)} + +
); diff --git a/src/constants/equipmentPrice.ts b/src/constants/equipmentPrice.ts new file mode 100644 index 000000000..115d05fd8 --- /dev/null +++ b/src/constants/equipmentPrice.ts @@ -0,0 +1,15 @@ +import { Vector3Tuple } from 'three'; +import { Tier } from '../core/blitzkrieg/tankDefinitions'; + +export const equipmentPriceMatrix: Record = { + 1: [100, 200, 300], + 2: [200, 300, 400], + 3: [400, 500, 600], + 4: [4000, 5000, 6000], + 5: [40_000, 50_000, 60_000], + 6: [100_000, 120_000, 140_000], + 7: [150_000, 170_000, 190_000], + 8: [200_000, 225_000, 250_000], + 9: [250_000, 275_000, 300_000], + 10: [300_000, 350_000, 400_000], +}; diff --git a/src/core/blitzkrieg/iconPng.ts b/src/core/blitzkrieg/iconPng.ts new file mode 100644 index 000000000..7a42c16e9 --- /dev/null +++ b/src/core/blitzkrieg/iconPng.ts @@ -0,0 +1,14 @@ +import sharp from 'sharp'; + +const cache: Record = {}; + +export async function iconPng(url: string) { + if (cache[url]) return cache[url]; + + const imageResponse = await fetch(url); + const imageBufferWEBP = await imageResponse.arrayBuffer(); + const imageBufferPNG = await sharp(imageBufferWEBP).png().toBuffer(); + cache[url] = `data:image/png;base64,${imageBufferPNG.toString('base64')}`; + + return cache[url]; +} diff --git a/src/core/blitzkrieg/tankDefinitions/index.ts b/src/core/blitzkrieg/tankDefinitions/index.ts index 5891d2ba0..8d3afa5b8 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'; @@ -142,14 +142,14 @@ export type ShellDefinition = { explosionRadius?: number; }; -export const tankDefinitions = fetchCdonLz4( - asset('definitions/tanks.cdon.lz4'), -); - -// export const tankDefinitions = readFile('test.tanks.cdon.lz4').then((data) => -// superDecompress(data.buffer), +// 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) => { resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry)); }); diff --git a/src/core/blitzkrieg/tankIconPng.ts b/src/core/blitzkrieg/tankIconPng.ts index 1fb197cfb..97b076bf9 100644 --- a/src/core/blitzkrieg/tankIconPng.ts +++ b/src/core/blitzkrieg/tankIconPng.ts @@ -1,9 +1,6 @@ -import sharp from 'sharp'; +import { iconPng } from './iconPng'; import { tankIcon } from './tankIcon'; -export async function tankIconPng(id: number) { - const imageResponse = await fetch(tankIcon(id)); - const imageBufferWEBP = await imageResponse.arrayBuffer(); - const imageBufferPNG = await sharp(imageBufferWEBP).png().toBuffer(); - return `data:image/png;base64,${imageBufferPNG.toString('base64')}`; +export function tankIconPng(id: number) { + return iconPng(tankIcon(id)); } From d39ab43e74070b62e2265d44ded312aa8179b28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 15 Mar 2024 21:50:19 -0500 Subject: [PATCH 09/14] feat: error handling for /research --- src/commands/research.tsx | 373 ++++++++++++++++++----- src/core/blitz/resolveTankId.ts | 19 +- src/core/blitzkrieg/buildTechTreeLine.ts | 3 +- src/core/discord/autocompleteTanks.ts | 5 +- src/lang/en-US.json | 32 +- src/stitches.config.ts | 1 + 6 files changed, 337 insertions(+), 96 deletions(-) diff --git a/src/commands/research.tsx b/src/commands/research.tsx index cc479c280..246a28d60 100644 --- a/src/commands/research.tsx +++ b/src/commands/research.tsx @@ -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((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(); + const gunXps = new Map(); + const engineXps = new Map(); + const trackXps = new Map(); - 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 = (
((resolve) => { gap: 8, }} > +
+
+ +
+ + {t`bot.commands.research.body.total`} + +
+ +
+
+ + + {costs + .reduce((a, b) => a + b.purchase + b.equipment, 0) + .toLocaleString(interaction.locale)} + +
+ +
+ + + {costs + .reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0) + .toLocaleString(interaction.locale)} + +
+
+
+ {await Promise.all( - line.map(async (id) => { + line.map(async (id, index) => { const tank = awaitedTankDefinitions[id]; - const turretXps = new Map(); - const gunXps = new Map(); - const engineXps = new Map(); - const trackXps = new Map(); - - 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 (
((resolve) => { display: 'flex', borderRadius: 4, backgroundColor: theme.colors.appBackground2, - border: `1px solid ${theme.colors.componentInteractive}`, + border: theme.borderStyles.subtle, }} >
((resolve) => { color: theme.colors.textHighContrast, }} > - Research:{' '} - {tank.xp?.toLocaleString(interaction.locale)} + {translate('bot.commands.research.body.research', [ + `${costs[index].research?.toLocaleString( + interaction.locale, + )}`, + ])}
- {moduleXp > 0 && ( + {costs[index].upgrades > 0 && (
((resolve) => { color: theme.colors.textHighContrast, }} > - Upgrades: {moduleXp.toLocaleString()} + {translate('bot.commands.research.body.upgrades', [ + costs[index].upgrades.toLocaleString(), + ])}
)} @@ -292,8 +489,11 @@ export const researchCommand = new Promise((resolve) => { color: theme.colors.textHighContrast, }} > - Purchase:{' '} - {tank.price.value.toLocaleString(interaction.locale)} + {translate('bot.commands.research.body.purchase', [ + costs[index].purchase.toLocaleString( + interaction.locale, + ), + ])}
@@ -317,10 +517,11 @@ export const researchCommand = new Promise((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, + ), + ])}
@@ -331,10 +532,14 @@ export const researchCommand = new Promise((resolve) => {
); + + return startingTankRaw + ? image + : [t`bot.commands.research.body.estimation`, image]; }, autocomplete(interaction) { - autocompleteTanks(interaction, true); + autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']); }, }); }); diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index 6db757940..1de2635a7 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -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( diff --git a/src/core/blitzkrieg/buildTechTreeLine.ts b/src/core/blitzkrieg/buildTechTreeLine.ts index d3d83bcfb..5f7a56aea 100644 --- a/src/core/blitzkrieg/buildTechTreeLine.ts +++ b/src/core/blitzkrieg/buildTechTreeLine.ts @@ -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; } } diff --git a/src/core/discord/autocompleteTanks.ts b/src/core/discord/autocompleteTanks.ts index 1e15ec13f..312ab7c91 100644 --- a/src/core/discord/autocompleteTanks.ts +++ b/src/core/discord/autocompleteTanks.ts @@ -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, 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 diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 3d236abd6..9fbb91c0d 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -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": { diff --git a/src/stitches.config.ts b/src/stitches.config.ts index 413f706be..5e769f749 100644 --- a/src/stitches.config.ts +++ b/src/stitches.config.ts @@ -29,6 +29,7 @@ export const theme = { }, borderStyles: { + subtle: `1px solid ${slateDark.slate2}`, ...createBorderStyles(slateDark), ...createBorderStyles(blueDark, undefined, 'blue'), }, From 56321d9e339c024df31a861cd3f7030358f5e1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 16 Mar 2024 00:06:44 -0500 Subject: [PATCH 10/14] feat: simplified owned tanks command --- src/commands/ownedTanks.tsx | 97 ++++---------- src/commands/permissions.ts | 4 + src/core/blitzkrieg/tankDefinitions/index.ts | 12 ++ .../discord/normalizeInteractionReturnable.ts | 3 +- .../handlers/chatInputCommand.ts | 125 ++++++++++-------- src/lang/en-US.json | 9 +- 6 files changed, 123 insertions(+), 127 deletions(-) diff --git a/src/commands/ownedTanks.tsx b/src/commands/ownedTanks.tsx index 9873f01c1..29f081c67 100644 --- a/src/commands/ownedTanks.tsx +++ b/src/commands/ownedTanks.tsx @@ -1,14 +1,13 @@ -import CommandWrapper from '../components/CommandWrapper'; -import NoData from '../components/NoData'; -import * as Tanks from '../components/Tanks'; -import TitleBar from '../components/TitleBar'; +import { chunk } from 'lodash'; +import markdownEscape from 'markdown-escape'; +import { TANK_CLASSES } from '../components/Tanks/components/Item/constants'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; -import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getTankStats from '../core/blitz/getTankStats'; -import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { + NATIONS, TankDefinition, Tier, + flags, tankDefinitions, } from '../core/blitzkrieg/tankDefinitions'; import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants'; @@ -20,6 +19,8 @@ import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import { translator } from '../core/localization/translator'; import { CommandRegistry } from '../events/interactionCreate'; +const TANKS_PER_MESSAGE = 64; + export const ownedTanksCommand = new Promise((resolve) => { resolve({ inProduction: true, @@ -43,7 +44,6 @@ export const ownedTanksCommand = new Promise((resolve) => { })), ) ).filter((tank) => tank.tankDefinitions?.tier === tier); - const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); const groupedTanks: Record = {}; const nations: string[] = []; @@ -56,71 +56,28 @@ export const ownedTanksCommand = new Promise((resolve) => { } }); - nations.sort(); + const awaitedTankDefinitions = await tankDefinitions; + const awaitedNations = await NATIONS; + const lines = tankStats + .map(({ tank_id }) => awaitedTankDefinitions[tank_id]) + .filter((tank) => tank.tier === tier) + .sort( + (a, b) => + TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class), + ) + .sort( + (a, b) => + awaitedNations.indexOf(a.nation) - awaitedNations.indexOf(b.nation), + ) + .map((tank) => `${flags[tank.nation]} ${markdownEscape(tank.name)}`); - return ( - - + index === 0 + ? `${translate('bot.commands.owned_tanks.body.title', [ + markdownEscape(accountInfo.nickname), TIER_ROMAN_NUMERALS[tier as Tier], - ])} - /> - - {filteredTanks.length === 0 && ( - - )} - - {filteredTanks.length > 0 && - (await Promise.all( - nations.map(async (nation) => { - const tanks = groupedTanks[nation]; - const leftColumnSize = Math.ceil(tanks.length / 2); - const leftColumn = tanks.slice(0, leftColumnSize); - const rightColumn = tanks.slice(leftColumnSize); - - return ( - - - {translate(`common.nations.${nation}`)} - - - - - {await Promise.all( - leftColumn.map(async (tank) => ( - - )), - )} - - - {await Promise.all( - rightColumn.map(async (tank) => ( - - )), - )} - - - - ); - }), - ))} - + ])}${lines.join('\n')}` + : lines.join('\n'), ); }, diff --git a/src/commands/permissions.ts b/src/commands/permissions.ts index c2916f13f..ab5052cf1 100644 --- a/src/commands/permissions.ts +++ b/src/commands/permissions.ts @@ -27,6 +27,10 @@ export const permissionsCommand = new Promise((resolve) => { interaction.appPermissions?.has('AttachFiles'), t`bot.commands.permissions.body.attach_files`, ], + [ + interaction.appPermissions?.has('UseExternalEmojis'), + t`bot.commands.permissions.body.external_emojis`, + ], ]; const body = permissions .map(([hasPermission, description]) => { diff --git a/src/core/blitzkrieg/tankDefinitions/index.ts b/src/core/blitzkrieg/tankDefinitions/index.ts index 8d3afa5b8..63e6cac4b 100644 --- a/src/core/blitzkrieg/tankDefinitions/index.ts +++ b/src/core/blitzkrieg/tankDefinitions/index.ts @@ -185,3 +185,15 @@ export const NATIONS = tanksDefinitionsArray.then((tanks) => { return nations; }); + +export const flags: Record = { + ussr: '<:ussr:1218421042033197197>', + germany: 'πŸ‡©πŸ‡ͺ', + usa: 'πŸ‡ΊπŸ‡Έ', + china: 'πŸ‡¨πŸ‡³', + uk: 'πŸ‡¬πŸ‡§', + france: 'πŸ‡«πŸ‡·', + japan: 'πŸ‡―πŸ‡΅', + european: 'πŸ‡ͺπŸ‡Ί', + other: '<:other:1218421572243558482>', +}; diff --git a/src/core/discord/normalizeInteractionReturnable.ts b/src/core/discord/normalizeInteractionReturnable.ts index 5642f5564..c555bd67d 100644 --- a/src/core/discord/normalizeInteractionReturnable.ts +++ b/src/core/discord/normalizeInteractionReturnable.ts @@ -4,6 +4,7 @@ import { ButtonBuilder, EmbedBuilder, InteractionEditReplyOptions, + InteractionReplyOptions, } from 'discord.js'; import { InteractionReturnable } from '../../events/interactionCreate'; import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded'; @@ -12,7 +13,7 @@ export default async function normalizeInteractionReturnable( returnable: InteractionReturnable, ) { const images: [number, Buffer][] = []; - const reply: InteractionEditReplyOptions = {}; + const reply: InteractionEditReplyOptions & InteractionReplyOptions = {}; const awaitedReturnable = await returnable; const normalizedReturnable = Array.isArray(awaitedReturnable) diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index a5842327b..9d796176b 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -7,7 +7,7 @@ import { ChatInputCommandInteraction, InteractionReplyOptions, } from 'discord.js'; -import { commands } from '..'; +import { InteractionRawReturnable, commands } from '..'; import buttonLink from '../../../core/discord/buttonLink'; import embedWarning from '../../../core/discord/embedWarning'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; @@ -28,70 +28,91 @@ export default async function handleChatInputCommand( if (registry.handlesInteraction) return; - const reply = await normalizeInteractionReturnable(returnable!); + const chunks: InteractionRawReturnable[][] = [[]]; - if ( - psa.data && - !psa.data.secret && - (psa.data.commands === undefined || - psa.data.commands.includes(interaction.commandName)) - ) { - if (psa.data.type === 'embed') { - if (!reply.embeds) reply.embeds = []; - reply.embeds.push(embedWarning(psa.data.title, psa.data.description)); - } else { - if (!reply.files) reply.files = []; - reply.files.push(new AttachmentBuilder(psa.data.image)); - reply.content = reply.content ?? psa.data.title; - } + (Array.isArray(returnable) ? returnable : [returnable]).forEach( + (object) => { + if (typeof object === 'string') { + chunks.push([object]); + } else chunks.at(-1)?.push(object!); + }, + ); - if (psa.data.links) { - if (!reply.components?.[0]) { - reply.components = [new ActionRowBuilder()]; + let index = 0; + for (const chunk of chunks.filter((chunk) => chunk.length > 0)) { + const reply = await normalizeInteractionReturnable(chunk); + + if ( + psa.data && + !psa.data.secret && + (psa.data.commands === undefined || + psa.data.commands.includes(interaction.commandName)) + ) { + if (psa.data.type === 'embed') { + if (!reply.embeds) reply.embeds = []; + reply.embeds.push(embedWarning(psa.data.title, psa.data.description)); + } else { + if (!reply.files) reply.files = []; + reply.files.push(new AttachmentBuilder(psa.data.image)); + reply.content = reply.content ?? psa.data.title; } - (reply.components[0] as ActionRowBuilder).addComponents( - psa.data.links.map(({ title, url }) => buttonLink(url, title)), - ); + if (psa.data.links) { + if (!reply.components?.[0]) { + reply.components = [new ActionRowBuilder()]; + } + + ( + reply.components[0] as ActionRowBuilder + ).addComponents( + psa.data.links.map(({ title, url }) => buttonLink(url, title)), + ); + } } - } - await interaction.editReply(reply); - - if ( - psa.data && - psa.data.secret && - (psa.data.commands === undefined || - psa.data.commands.includes(interaction.commandName)) - ) { - const followUp: InteractionReplyOptions = { - ephemeral: true, - }; - - if (psa.data.type === 'embed') { - if (!followUp.embeds) followUp.embeds = []; - followUp.embeds.push( - embedWarning(psa.data.title, psa.data.description), - ); + if (index === 0) { + await interaction.editReply(reply); } else { - if (!followUp.files) followUp.files = []; - followUp.files.push(new AttachmentBuilder(psa.data.image)); - followUp.content = followUp.content ?? psa.data.title; + await interaction.followUp(reply); } - if (psa.data.links) { - if (!followUp.components?.[0]) { - followUp.components = [new ActionRowBuilder()]; + if ( + psa.data && + psa.data.secret && + (psa.data.commands === undefined || + psa.data.commands.includes(interaction.commandName)) + ) { + const followUp: InteractionReplyOptions = { + ephemeral: true, + }; + + if (psa.data.type === 'embed') { + if (!followUp.embeds) followUp.embeds = []; + followUp.embeds.push( + embedWarning(psa.data.title, psa.data.description), + ); + } else { + if (!followUp.files) followUp.files = []; + followUp.files.push(new AttachmentBuilder(psa.data.image)); + followUp.content = followUp.content ?? psa.data.title; } - ( - followUp.components[0] as ActionRowBuilder - ).addComponents( - psa.data.links.map(({ title, url }) => buttonLink(url, title)), - ); + if (psa.data.links) { + if (!followUp.components?.[0]) { + followUp.components = [new ActionRowBuilder()]; + } + + ( + followUp.components[0] as ActionRowBuilder + ).addComponents( + psa.data.links.map(({ title, url }) => buttonLink(url, title)), + ); + } + + interaction.followUp(followUp); } - interaction.followUp(followUp); + index++; } } catch (error) { const { t } = translator(interaction.locale); diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 9fbb91c0d..ed9751a7a 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -486,7 +486,7 @@ "description": "Shows all own tanks in a tier", "body": { - "subtitle": "Tier %s tanks" + "title": "# %s's owned tier %s tanks\n\n" } }, @@ -496,9 +496,10 @@ "body": { "title": "# Permissions\n\n%s", - "view_channels": "View channels: needed for the refresh button to work", - "read_message_history": "Read message history: needed for the refresh button to find the message to refresh", - "attach_files": "Attach files: needed for image-based commands to work" + "view_channels": "View channels: needed to find the message the refresh button was used on", + "read_message_history": "Read message history: needed to find the message to refresh", + "attach_files": "Attach files: needed to add images to image-based commands", + "external_emojis": "External emojis: needed to fill add national flags and tank class emojis" } }, From ec93d8e9abf233f1323fe4719f5c2e75409d80ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 16 Mar 2024 10:41:25 -0500 Subject: [PATCH 11/14] feat: better words --- src/lang/en-US.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/en-US.json b/src/lang/en-US.json index ed9751a7a..fc270bf77 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -221,8 +221,8 @@ }, "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.", + "non_tech_tree": "# %s is not on the tech tree\n\nYou cannot research a premium/collector tank.", + "tier_1": "# %s is a tier I\n\nTier I tanks are always unlocked for free for all players.", "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.", @@ -499,7 +499,7 @@ "view_channels": "View channels: needed to find the message the refresh button was used on", "read_message_history": "Read message history: needed to find the message to refresh", "attach_files": "Attach files: needed to add images to image-based commands", - "external_emojis": "External emojis: needed to fill add national flags and tank class emojis" + "external_emojis": "External emojis: needed add national flags and tank class emojis" } }, From 04cd205fee04d5f732d219c6dd0e8203da69c87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 16 Mar 2024 15:07:05 -0500 Subject: [PATCH 12/14] feat: preview servers --- src/commands/research.tsx | 1 + .../handlers/chatInputCommand.ts | 15 +++++++++++++++ src/events/interactionCreate/index.ts | 1 + src/lang/en-US.json | 3 ++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/commands/research.tsx b/src/commands/research.tsx index 246a28d60..43dc84788 100644 --- a/src/commands/research.tsx +++ b/src/commands/research.tsx @@ -31,6 +31,7 @@ export const researchCommand = new Promise((resolve) => { resolve({ inProduction: true, inPublic: true, + inPreview: true, command: createLocalizedCommand('research') .addStringOption((option) => diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index 9d796176b..a3208c3a2 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -15,6 +15,11 @@ import { psa } from '../../../core/discord/psa'; import { translator } from '../../../core/localization/translator'; import { UserError } from '../../../hooks/userError'; +const previewServers = [ + '734786591205359697', // abhi + '460165161156870161', // optml +]; + export default async function handleChatInputCommand( interaction: ChatInputCommandInteraction, ) { @@ -24,6 +29,16 @@ export default async function handleChatInputCommand( await interaction.deferReply(); try { + if ( + registry.inPreview && + !previewServers.includes(`${interaction.guildId}`) + ) { + const { t } = translator(interaction.locale); + interaction.editReply(t`bot.common.errors.preview`); + + return; + } + const returnable = await registry.handler(interaction); if (registry.handlesInteraction) return; diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index 25a277c53..bcbdcfe95 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -55,6 +55,7 @@ export type InteractionReturnable = interface CommandRegistryBase { inProduction: boolean; inPublic: boolean; + inPreview?: boolean; command: | SlashCommandBuilder diff --git a/src/lang/en-US.json b/src/lang/en-US.json index fc270bf77..c07c3eff5 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -190,7 +190,8 @@ "tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s\". Try typing the name of the tank instead of a number.", "no_tank_stats": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.", "get_help": "Help Discord server", - "uncaught_error": "# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help." + "uncaught_error": "# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", + "preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon." } }, From dba5a0a2cda9a25810459918c3e1d205be9f6bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 16 Mar 2024 15:10:55 -0500 Subject: [PATCH 13/14] feat: logs --- docs/changelogs/1.17.0.md | 13 +++++++++++++ package.json | 2 +- src/core/blitzkrieg/tankDefinitions/index.ts | 16 ++++++++-------- 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 docs/changelogs/1.17.0.md diff --git a/docs/changelogs/1.17.0.md b/docs/changelogs/1.17.0.md new file mode 100644 index 000000000..41aba51cf --- /dev/null +++ b/docs/changelogs/1.17.0.md @@ -0,0 +1,13 @@ +# Blitzkrieg 1.17.0 + +A preview command and a touchup. + +## Features + +- Added `/research` in preview only accessible in the official Discord server. + - More details will be posted when the command is made public. + +## Changes + +- `/owned-tanks` now returns a simple text message listing all the tanks instead of an image. + - This is meant to improve the readability and ease of copying. diff --git a/package.json b/package.json index 110e4ffdc..9d13fd0fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkrieg", - "version": "1.16.4", + "version": "1.17.0", "description": "πŸŽ‰ Tools for everything World of Tanks Blitz", "main": "src/server.ts", "type": "module", diff --git a/src/core/blitzkrieg/tankDefinitions/index.ts b/src/core/blitzkrieg/tankDefinitions/index.ts index 63e6cac4b..47ac938d8 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 { superDecompress } from '../superDecompress'; +import { asset } from '../asset'; +import { fetchCdonLz4 } from '../fetchCdonLz4'; import { TIERS } from './constants'; export type ShellType = 'ap' | 'ap_cr' | 'hc' | 'he'; @@ -142,14 +142,14 @@ export type ShellDefinition = { explosionRadius?: number; }; -// export const tankDefinitions = fetchCdonLz4( -// asset('definitions/tanks.cdon.lz4'), -// ); - -export const tankDefinitions = readFile('test.tanks.cdon.lz4').then((data) => - superDecompress(data.buffer), +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) => { resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry)); }); From 81cb1afeff1dae24297ff1f7a8b9dfe4abec6c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 16 Mar 2024 15:12:47 -0500 Subject: [PATCH 14/14] fix: types --- src/core/discord/normalizeInteractionReturnable.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/discord/normalizeInteractionReturnable.ts b/src/core/discord/normalizeInteractionReturnable.ts index c555bd67d..cfaa7750c 100644 --- a/src/core/discord/normalizeInteractionReturnable.ts +++ b/src/core/discord/normalizeInteractionReturnable.ts @@ -5,6 +5,7 @@ import { EmbedBuilder, InteractionEditReplyOptions, InteractionReplyOptions, + MessageEditOptions, } from 'discord.js'; import { InteractionReturnable } from '../../events/interactionCreate'; import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded'; @@ -13,7 +14,9 @@ export default async function normalizeInteractionReturnable( returnable: InteractionReturnable, ) { const images: [number, Buffer][] = []; - const reply: InteractionEditReplyOptions & InteractionReplyOptions = {}; + const reply: InteractionEditReplyOptions & + InteractionReplyOptions & + MessageEditOptions = {}; const awaitedReturnable = await returnable; const normalizedReturnable = Array.isArray(awaitedReturnable)