Merge pull request #100 from tresabhi/dev

This commit is contained in:
TrèsAbhi authored and GitHub committed 2024-03-16 15:12:54 -05:00
commit 990ab1520c
25 files changed
+1084 -190

No files matched your search

+1 -1
View File
@@ -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' || '' }}
+13
View File
@@ -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.
+2 -2
View File
@@ -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",
@@ -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",
+39
View File
@@ -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);
}
+158 -40
View File
@@ -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
@@ -353,6 +368,8 @@ export async function definitions(production: boolean) {
`${DATA}/${POI.provisionsCommon}.dvpl`,
);
const tankXps = new Map<number, number>();
await Promise.all(
nations.map(async (nation) => {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
@@ -385,6 +402,10 @@ export async function definitions(production: boolean) {
);
for (const tankKey in tankList.root) {
const gunXps = new Map<number, number>();
const turretXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
const tank = tankList.root[tankKey];
const tankPrice: TankDefinitionPrice =
typeof tank.price === 'number'
@@ -427,6 +448,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 +554,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 +581,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 +619,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 +672,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 +685,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 +727,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 +749,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 +767,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 +833,100 @@ export async function definitions(production: boolean) {
});
},
);
totalUnlocks.forEach((unlocks) => {
if (unlocks === undefined) return;
Object.entries(unlocks).forEach(([key, value]) => {
(Array.isArray(value) ? value : [value]).forEach((vehicle) => {
switch (key as keyof Unlocks) {
case 'vehicle': {
const tankListEntry = tankList.root[vehicle['#text']];
const currentTank = tankDefinitions[tankId];
const successorId = toUniqueId(nation, tankListEntry.id);
tankXps.set(successorId, vehicle.cost);
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;
}
}
});
});
});
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(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 +1084,10 @@ export async function definitions(production: boolean) {
}
});
writeFile(
'test.tanks.cdon.lz4',
Buffer.from(superCompress(tankDefinitions), 'base64'),
);
await commitAssets(
'definitions',
[
+2
View File
@@ -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) {
+18
View File
@@ -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
}
}
+30 -74
View File
@@ -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<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
@@ -32,10 +33,9 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((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 +44,6 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
})),
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = [];
@@ -57,71 +56,28 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((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 (
<CommandWrapper>
<TitleBar
title={accountInfo.nickname}
image={
clanAccountInfo?.clan
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
: undefined
}
description={translate('bot.commands.owned_tanks.body.subtitle', [
return chunk(lines, TANKS_PER_MESSAGE).map((lines, index) =>
index === 0
? `${translate('bot.commands.owned_tanks.body.title', [
markdownEscape(accountInfo.nickname),
TIER_ROMAN_NUMERALS[tier as Tier],
])}
/>
{filteredTanks.length === 0 && (
<NoData type="tanks_found" locale={interaction.locale} />
)}
{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 (
<Tanks.Root key={nation}>
<Tanks.Title>
{translate(`common.nations.${nation}`)}
</Tanks.Title>
<Tanks.Row>
<Tanks.Column>
{await Promise.all(
leftColumn.map(async (tank) => (
<Tanks.Item
key={tank.id}
name={tank.name}
tankType={tank.class}
treeType={tank.treeType}
/>
)),
)}
</Tanks.Column>
<Tanks.Column>
{await Promise.all(
rightColumn.map(async (tank) => (
<Tanks.Item
key={tank.id}
name={tank.name}
tankType={tank.class}
treeType={tank.treeType}
/>
)),
)}
</Tanks.Column>
</Tanks.Row>
</Tanks.Root>
);
}),
))}
</CommandWrapper>
])}${lines.join('\n')}`
: lines.join('\n'),
);
},
+4
View File
@@ -27,6 +27,10 @@ export const permissionsCommand = new Promise<CommandRegistry>((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]) => {
+546
View File
@@ -0,0 +1,546 @@
import { Locale, 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 {
TANK_ICONS,
TIER_ROMAN_NUMERALS,
} from '../core/blitzkrieg/tankDefinitions/constants';
import { tankIconPng } from '../core/blitzkrieg/tankIconPng';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { t } = translator(Locale.EnglishUS);
resolve({
inProduction: true,
inPublic: true,
inPreview: true,
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 targetTankId = await resolveTankId(
interaction.options.getString('target-tank', true),
interaction.locale,
true,
);
const startingTankRaw = interaction.options.getString('starting-tank');
const targetTank = awaitedTankDefinitions[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
const targetTankAncestry = await resolveAncestry(targetTankId);
if (startingTankRaw) {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
);
if (targetTankId === startingTankId) {
return 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 techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
);
const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
const costs = await Promise.all(
line.map(async (id) => {
const tank = awaitedTankDefinitions[id];
const turretXps = new Map<number, number>();
const gunXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
tank.turrets.forEach((turret) => {
if (turret.xp !== undefined) {
turretXps.set(turret.id, turret.xp);
}
turret.guns.forEach((gun) => {
if (gun.xp !== undefined) {
gunXps.set(gun.id, gun.xp);
}
});
});
tank.engines.forEach((engine) => {
if (engine.xp !== undefined) {
engineXps.set(engine.id, engine.xp);
}
});
tank.tracks.forEach((track) => {
if (track.xp !== undefined) {
trackXps.set(track.id, track.xp);
}
});
let moduleXp = 0;
turretXps.forEach((value) => {
moduleXp += value;
});
gunXps.forEach((value) => {
moduleXp += value;
});
engineXps.forEach((value) => {
moduleXp += value;
});
trackXps.forEach((value) => {
moduleXp += value;
});
return {
research: tank.xp,
upgrades: moduleXp,
purchase: tank.price.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
),
};
}),
);
const image = (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${t`bot.commands.research.body.subtitle`} • ${targetTank.name}`}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<div
style={{
display: 'flex',
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div style={{ width: 48 }}></div>
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
right: 24,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{t`bot.commands.research.body.total`}
</span>
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
padding: '16px 0',
}}
>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{costs
.reduce((a, b) => a + b.purchase + b.equipment, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
</div>
</div>
{await Promise.all(
line.map(async (id, index) => {
const tank = awaitedTankDefinitions[id];
return (
<div
key={id}
style={{
display: 'flex',
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div
style={{
width: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
</div>
<div
style={{
flex: 1,
display: 'flex',
position: 'relative',
right: '5%',
}}
>
<div
style={{
height: 120,
width: 240,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
left: '25%',
top: '5%',
}}
>
<img
src={await tankIconPng(id)}
style={{
height: '100%',
position: 'absolute',
objectFit: 'contain',
}}
/>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 8,
left: '50%',
transform: 'translateX(-50%)',
}}
>
<img
src={TANK_ICONS[tank.class]}
width={16}
height={16}
/>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{tank.name}
</span>
</div>
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
padding: '16px 0',
}}
>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.research', [
`${costs[index].research?.toLocaleString(
interaction.locale,
)}`,
])}
</span>
</div>
{costs[index].upgrades > 0 && (
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(
asset('icons/currencies/free-xp.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.upgrades', [
costs[index].upgrades.toLocaleString(),
])}
</span>
</div>
)}
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.purchase', [
costs[index].purchase.toLocaleString(
interaction.locale,
),
])}
</span>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.equipment', [
costs[index].equipment.toLocaleString(
interaction.locale,
),
])}
</span>
</div>
</div>
</div>
);
}),
)}
</div>
</CommandWrapper>
);
return startingTankRaw
? image
: [t`bot.commands.research.body.estimation`, image];
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
},
});
});
+1 -1
View File
@@ -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)',
background: theme.colors.appBackground1,
}}
>
{children}
+15
View File
@@ -0,0 +1,15 @@
import { Vector3Tuple } from 'three';
import { Tier } from '../core/blitzkrieg/tankDefinitions';
export const equipmentPriceMatrix: Record<Tier, Vector3Tuple> = {
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],
};
+11 -8
View File
@@ -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(
+20
View File
@@ -0,0 +1,20 @@
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)) {
const ancestorTree = await buildTechTreeLine(start, ancestorId);
line.push(...ancestorTree);
break;
}
}
return line;
}
+14
View File
@@ -0,0 +1,14 @@
import sharp from 'sharp';
const cache: Record<string, string> = {};
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];
}
+25
View File
@@ -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,
);
}
@@ -22,6 +22,8 @@ export interface Crew {
export interface TankDefinition {
id: number;
fixedCamouflage?: boolean;
ancestors?: number[];
successors?: number[];
crew: Crew[];
health: number;
nation: string;
@@ -37,6 +39,7 @@ export interface TankDefinition {
engines: EngineDefinition[];
tracks: TrackDefinition[];
price: TankDefinitionPrice;
xp?: number;
speed: {
forwards: number;
backwards: number;
@@ -54,6 +57,7 @@ export interface TrackDefinition {
tier: Tier;
weight: number;
traverseSpeed: number;
xp?: number;
dispersion: {
move: number;
traverse: number;
@@ -67,6 +71,7 @@ export interface TrackDefinition {
export interface EngineDefinition {
id: number;
name: string;
xp?: number;
tier: Tier;
fireChance: number;
power: number;
@@ -79,6 +84,7 @@ export interface TurretDefinition {
health: number;
viewRange: number;
traverseSpeed: number;
xp?: number;
id: number;
name: string;
tier: Tier;
@@ -91,6 +97,7 @@ export type GunDefinition =
| GunDefinitionAutoReloader;
interface GunDefinitionBase {
rotationSpeed: number;
xp?: number;
weight: number;
id: number;
name: string;
@@ -139,6 +146,10 @@ export const tankDefinitions = fetchCdonLz4<TankDefinitions>(
asset('definitions/tanks.cdon.lz4'),
);
// export const tankDefinitions = readFile('test.tanks.cdon.lz4').then((data) =>
// superDecompress<TankDefinitions>(data.buffer),
// );
const entries = new Promise<TankDefinition[]>(async (resolve) => {
resolve(Object.entries(await tankDefinitions).map(([, entry]) => entry));
});
@@ -156,6 +167,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,
};
}),
),
@@ -173,3 +185,15 @@ export const NATIONS = tanksDefinitionsArray.then((tanks) => {
return nations;
});
export const flags: Record<string, string> = {
ussr: '<:ussr:1218421042033197197>',
germany: '🇩🇪',
usa: '🇺🇸',
china: '🇨🇳',
uk: '🇬🇧',
france: '🇫🇷',
japan: '🇯🇵',
european: '🇪🇺',
other: '<:other:1218421572243558482>',
};
+6
View File
@@ -0,0 +1,6 @@
import { iconPng } from './iconPng';
import { tankIcon } from './tankIcon';
export function tankIconPng(id: number) {
return iconPng(tankIcon(id));
}
+15 -5
View File
@@ -10,19 +10,29 @@ import {
OVERFLOW_SUFFIX,
} from './autocompleteClan/constants';
export const tankNamesTechTreeOnly = tankNames.then((names) =>
names.filter((tank) => tank.treeType === 'researchable'),
);
export default async function autocompleteTanks(
interaction: AutocompleteInteraction<CacheType>,
techTreeOnly = false,
fields = ['tank'],
) {
const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'tank') return;
if (!fields.includes(focusedOption.name)) return;
await interaction.respond(
focusedOption.value
? 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) {
@@ -4,6 +4,8 @@ import {
ButtonBuilder,
EmbedBuilder,
InteractionEditReplyOptions,
InteractionReplyOptions,
MessageEditOptions,
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate';
import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded';
@@ -12,7 +14,9 @@ export default async function normalizeInteractionReturnable(
returnable: InteractionReturnable,
) {
const images: [number, Buffer][] = [];
const reply: InteractionEditReplyOptions = {};
const reply: InteractionEditReplyOptions &
InteractionReplyOptions &
MessageEditOptions = {};
const awaitedReturnable = await returnable;
const normalizedReturnable = Array.isArray(awaitedReturnable)
@@ -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';
@@ -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<CacheType>,
) {
@@ -24,74 +29,105 @@ 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;
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<ButtonBuilder>()];
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<ButtonBuilder>).addComponents(
psa.data.links.map(({ title, url }) => buttonLink(url, title)),
);
if (psa.data.links) {
if (!reply.components?.[0]) {
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
}
(
reply.components[0] as ActionRowBuilder<ButtonBuilder>
).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<ButtonBuilder>()];
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<ButtonBuilder>
).addComponents(
psa.data.links.map(({ title, url }) => buttonLink(url, title)),
);
if (psa.data.links) {
if (!followUp.components?.[0]) {
followUp.components = [new ActionRowBuilder<ButtonBuilder>()];
}
(
followUp.components[0] as ActionRowBuilder<ButtonBuilder>
).addComponents(
psa.data.links.map(({ title, url }) => buttonLink(url, title)),
);
}
interaction.followUp(followUp);
}
interaction.followUp(followUp);
index++;
}
} catch (error) {
const { t } = translator(interaction.locale);
+3
View File
@@ -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';
@@ -54,6 +55,7 @@ export type InteractionReturnable =
interface CommandRegistryBase {
inProduction: boolean;
inPublic: boolean;
inPreview?: boolean;
command:
| SlashCommandBuilder
@@ -92,6 +94,7 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
statsCommand,
ratingsCommand,
todayCommand,
researchCommand,
];
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
+42 -5
View File
@@ -190,11 +190,47 @@
"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."
}
},
"commands": {
"research": {
"$": "research",
"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 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.",
"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": {
"$": "about",
"description": "All the info you need about the bot",
@@ -451,7 +487,7 @@
"description": "Shows all own tanks in a tier",
"body": {
"subtitle": "Tier %s tanks"
"title": "# %s's owned tier %s tanks\n\n"
}
},
@@ -461,9 +497,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 add national flags and tank class emojis"
}
},
+1
View File
@@ -29,6 +29,7 @@ export const theme = {
},
borderStyles: {
subtle: `1px solid ${slateDark.slate2}`,
...createBorderStyles(slateDark),
...createBorderStyles(blueDark, undefined, 'blue'),
},
+1 -1
View File
@@ -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==