mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8edd16eb3 | ||
|
|
b96ff2e239 | ||
|
|
a575da3005 | ||
|
|
3ca2dece3e | ||
|
|
191d6133b2 | ||
|
|
b15e73e3a8 | ||
|
|
9a6b8ab979 | ||
|
|
87786d41a7 | ||
|
|
ee7ffe7bc8 | ||
|
|
379a33375d | ||
|
|
bd3f5dea05 | ||
|
|
146d36234e | ||
|
|
f3edcb8077 | ||
|
|
9e492015ff | ||
|
|
d2977182d9 | ||
|
|
7d2d65bab8 | ||
|
|
415c3bd066 | ||
|
|
990ab1520c | ||
|
|
81cb1afeff | ||
|
|
dba5a0a2cd | ||
|
|
04cd205fee | ||
|
|
ec93d8e9ab | ||
|
|
56321d9e33 | ||
|
|
d39ab43e74 | ||
|
|
787d3ae6ea | ||
|
|
035f751bf1 | ||
|
|
43442ac829 | ||
|
|
da02a089b9 | ||
|
|
476d8e804a | ||
|
|
43e1d53d39 | ||
|
|
5372068830 | ||
|
|
025cec3687 | ||
|
|
c47c629a83 | ||
|
|
2b7907f8f7 | ||
|
|
903d4e4d95 | ||
|
|
8857b2baae | ||
|
|
7169e0d91a | ||
|
|
1c9b7463fc | ||
|
|
80741d66fc |
No files matched your search
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -23,6 +23,12 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Add fixed sharp
|
||||
run: yarn add [email protected] --ignore-engines
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
||||
|
||||
@@ -33,6 +33,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Add fixed sharp
|
||||
run: yarn add [email protected] --ignore-engines
|
||||
|
||||
- name: Build
|
||||
run: yarn build:bot
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Blitzkrieg 1.17.1
|
||||
|
||||
Some stability fixes.
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- `sharp` is now included in the build instead of having being installed before runtime.
|
||||
- Png converter cache is now more memory efficient.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.17.2
|
||||
|
||||
Introducing the new `/research` command!
|
||||
|
||||
## Features
|
||||
|
||||
- Added the `/research` command
|
||||
- Type in the name of a tech tree tank you desire to research and this command will list all the expenses in researching all tanks in that line
|
||||
- The starting tank is inferred automatically by the tanks you own
|
||||
- This may be a bit inaccurate because of the lack of accurate information Wargaming provides about owned tanks
|
||||
- You are also manually provide a starting tank and a user to see the progress on a line for
|
||||
- Replaced Vercel Analytics (which collapsed under the insane amount of traffic on the website) with Google Analytics
|
||||
- Spaced armor can now be toggled in the Tankopedia armor sandbox
|
||||
+3
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.16.4",
|
||||
"version": "1.17.1",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
@@ -12,7 +12,7 @@
|
||||
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
|
||||
"build:test": "vite-node --script scripts/buildTest",
|
||||
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot",
|
||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||
"dev:bot": "yarn build:bot --dev && yarn start:bot",
|
||||
"dev:docs": "vitepress dev docs",
|
||||
"dev:website": "next dev",
|
||||
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
||||
@@ -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",
|
||||
@@ -43,7 +43,6 @@
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"bepaint": "^0.1.0-beta.3",
|
||||
"decompress": "^4.2.1",
|
||||
"discord.js": "^14.13.0",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { build } from 'esbuild';
|
||||
import { cp, readdir } from 'fs/promises';
|
||||
import { argv } from 'process';
|
||||
|
||||
const isDev = argv.includes('--dev');
|
||||
|
||||
readdir('src/workers').then((workers) => {
|
||||
const files = [
|
||||
@@ -25,6 +28,7 @@ readdir('src/workers').then((workers) => {
|
||||
'.js': '.cjs',
|
||||
},
|
||||
|
||||
external: ['sharp'],
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // causes goofy errors
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -92,7 +102,7 @@ interface VehicleDefinitions {
|
||||
};
|
||||
consumableSlots: number;
|
||||
provisionSlots: number;
|
||||
optDevicePreset: string;
|
||||
optDevicePreset: string | string[];
|
||||
hull: {
|
||||
armor: VehicleDefinitionArmor;
|
||||
turretPositions: { turret: string };
|
||||
@@ -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;
|
||||
@@ -473,7 +495,8 @@ export async function definitions(production: boolean) {
|
||||
forwards: tankDefinition.root.speedLimits.forward,
|
||||
backwards: tankDefinition.root.speedLimits.backward,
|
||||
},
|
||||
equipment,
|
||||
equipment:
|
||||
typeof equipment === 'string' ? equipment : equipment.at(-1)!,
|
||||
consumables: tankDefinition.root.consumableSlots,
|
||||
provisions: tankDefinition.root.provisionSlots,
|
||||
name:
|
||||
@@ -531,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,
|
||||
@@ -557,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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -593,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) => {
|
||||
@@ -645,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
|
||||
@@ -658,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];
|
||||
@@ -702,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 {
|
||||
@@ -724,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,
|
||||
@@ -742,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],
|
||||
@@ -808,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;
|
||||
@@ -969,6 +1084,10 @@ export async function definitions(production: boolean) {
|
||||
}
|
||||
});
|
||||
|
||||
writeFile(
|
||||
'test.tanks.cdon.lz4',
|
||||
Buffer.from(superCompress(tankDefinitions), 'base64'),
|
||||
);
|
||||
await commitAssets(
|
||||
'definitions',
|
||||
[
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -3,12 +3,12 @@
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { AlertDialog, Button, Flex, Text, Theme } from '@radix-ui/themes';
|
||||
import '@radix-ui/themes/styles.css';
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
import { config } from 'dotenv';
|
||||
import { Roboto_Flex } from 'next/font/google';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { SkyLine } from '../assets/art/SkyLine';
|
||||
import { Analytics } from '../components/Analytics';
|
||||
import Navbar from '../components/Navbar';
|
||||
import PageWrapper from '../components/PageWrapper';
|
||||
import isDev from '../core/blitzkrieg/isDev';
|
||||
@@ -51,6 +51,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
name="description"
|
||||
content="🎉 Tools for everything World of Tanks Blitz"
|
||||
/>
|
||||
<Analytics />
|
||||
</head>
|
||||
|
||||
<body
|
||||
@@ -60,8 +61,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
backgroundColor: isEmbed ? 'transparent' : undefined,
|
||||
}}
|
||||
>
|
||||
<Analytics />
|
||||
|
||||
<Theme
|
||||
appearance="dark"
|
||||
panelBackground="translucent"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
||||
import { RefObject } from 'react';
|
||||
import { Pose, poseEvent } from '../../../../../../../core/blitzkrieg/pose';
|
||||
import mutateTankopediaPersistent, {
|
||||
mutateTankopediaTemporary,
|
||||
useTankopediaPersistent,
|
||||
} from '../../../../../../../stores/tankopedia';
|
||||
import { ENVIRONMENTS } from '../../Lighting';
|
||||
@@ -30,6 +31,9 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
||||
const wireframe = useTankopediaPersistent(
|
||||
(state) => state.model.visual.wireframe,
|
||||
);
|
||||
const showSpacedArmor = useTankopediaPersistent(
|
||||
(state) => state.model.visual.showSpacedArmor,
|
||||
);
|
||||
const opaque = useTankopediaPersistent((state) => state.model.visual.opaque);
|
||||
const fullScreenAvailable =
|
||||
typeof document !== 'undefined' && document.fullscreenEnabled;
|
||||
@@ -81,6 +85,20 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Label>Armor</DropdownMenu.Label>
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={showSpacedArmor}
|
||||
onCheckedChange={(checked) => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.showSpacedArmor = checked;
|
||||
});
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
Spaced armor
|
||||
</DropdownMenu.CheckboxItem>
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={greenPenetration}
|
||||
onCheckedChange={(checked) => {
|
||||
|
||||
+30
-74
@@ -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'),
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
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 addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||
import { localizationObject } from '../core/discord/localizationObject';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { translator } from '../core/localization/translator';
|
||||
import { 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,
|
||||
|
||||
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),
|
||||
)
|
||||
.addStringOption(addUsernameChoices),
|
||||
|
||||
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,
|
||||
}}
|
||||
>
|
||||
Experience:{' '}
|
||||
{costs
|
||||
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 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,
|
||||
}}
|
||||
>
|
||||
Purchase:{' '}
|
||||
{costs
|
||||
.reduce((a, b) => a + b.purchase, 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,
|
||||
}}
|
||||
>
|
||||
Equipment:{' '}
|
||||
{costs
|
||||
.reduce((a, b) => a + b.equipment, 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']);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
export function Analytics() {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-98NTMRHRP9"
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-98NTMRHRP9');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export function CoreArmorSceneComponent({
|
||||
explosionRadius: { value: null },
|
||||
greenPenetration: { value: null },
|
||||
opaque: { value: null },
|
||||
useSpacedArmor: { value: null },
|
||||
|
||||
resolution: { value: new Vector2() },
|
||||
spacedArmorBuffer: { value: null },
|
||||
@@ -75,6 +76,7 @@ export function CoreArmorSceneComponent({
|
||||
) {
|
||||
material.uniforms.greenPenetration.value = visual.greenPenetration;
|
||||
material.uniforms.opaque.value = visual.opaque || visual.wireframe;
|
||||
material.uniforms.useSpacedArmor.value = visual.showSpacedArmor;
|
||||
material.wireframe = visual.wireframe;
|
||||
}
|
||||
async function handleProtagonistEquipmentChange(
|
||||
|
||||
@@ -14,6 +14,7 @@ uniform float damage;
|
||||
uniform float explosionRadius;
|
||||
uniform bool greenPenetration;
|
||||
uniform bool opaque;
|
||||
uniform bool useSpacedArmor;
|
||||
|
||||
uniform highp sampler2D spacedArmorBuffer;
|
||||
uniform highp sampler2D spacedArmorDepth;
|
||||
@@ -49,7 +50,7 @@ void main() {
|
||||
float coreArmorDistance = depthToDistance(gl_FragCoord.z);
|
||||
float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance;
|
||||
|
||||
if (isUnderSpacedArmor) {
|
||||
if (useSpacedArmor && isUnderSpacedArmor) {
|
||||
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
|
||||
remainingPenetration -= spacedArmorThickness;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useDuel } from '../../../../stores/duel';
|
||||
import {
|
||||
Shot,
|
||||
ShotLayerBase,
|
||||
useTankopediaPersistent,
|
||||
useTankopediaTemporary,
|
||||
} from '../../../../stores/tankopedia';
|
||||
import { ArmorType } from '../SpacedArmorScene';
|
||||
@@ -115,12 +116,20 @@ export function SpacedArmorSceneComponent({
|
||||
pushLogs(shellNormal, filterIntersections(event.intersections), 0);
|
||||
|
||||
function filterIntersections(intersectionsRaw: Intersection[]) {
|
||||
const intersectionsAll = intersectionsRaw.filter(
|
||||
const showSpacedArmor =
|
||||
useTankopediaPersistent.getState().model.visual.showSpacedArmor;
|
||||
const intersectionsTyped = intersectionsRaw.filter(
|
||||
({ object }) => object.userData.type !== undefined,
|
||||
) as ArmorMeshIntersection[];
|
||||
const intersectionsWithDuplicates = intersectionsAll.slice(
|
||||
const intersectionsFiltered = intersectionsTyped.filter(
|
||||
({ object }) =>
|
||||
showSpacedArmor
|
||||
? true
|
||||
: object.userData.type === ArmorType.Core,
|
||||
);
|
||||
const intersectionsWithDuplicates = intersectionsFiltered.slice(
|
||||
0,
|
||||
intersectionsAll.findIndex(
|
||||
intersectionsFiltered.findIndex(
|
||||
({ object }) => object.userData.type === ArmorType.Core,
|
||||
) + 1,
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
@@ -57,7 +57,7 @@ export async function extractModel(data: string, path: string) {
|
||||
material.setBaseColorTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType('image/png')
|
||||
.setMimeType('image/jpeg')
|
||||
.setImage(
|
||||
await readTexture(
|
||||
`${data}/3d/${dirname(path)}/${textures.albedo}`,
|
||||
@@ -70,7 +70,7 @@ export async function extractModel(data: string, path: string) {
|
||||
material.setMetallicRoughnessTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType('image/png')
|
||||
.setMimeType('image/jpeg')
|
||||
.setImage(
|
||||
await readTexture(
|
||||
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`,
|
||||
@@ -86,7 +86,7 @@ export async function extractModel(data: string, path: string) {
|
||||
material.setNormalTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType('image/png')
|
||||
.setMimeType('image/jpeg')
|
||||
.setImage(
|
||||
await readTexture(
|
||||
`${data}/3d/${dirname(path)}/${
|
||||
@@ -102,7 +102,7 @@ export async function extractModel(data: string, path: string) {
|
||||
material.setOcclusionTexture(
|
||||
document
|
||||
.createTexture(node.materialName)
|
||||
.setMimeType('image/png')
|
||||
.setMimeType('image/jpeg')
|
||||
.setImage(
|
||||
await readTexture(
|
||||
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
const cache: Record<string, Buffer> = {};
|
||||
|
||||
export async function iconPng(url: string) {
|
||||
if (!cache[url]) {
|
||||
const imageResponse = await fetch(url);
|
||||
const imageBufferWEBP = await imageResponse.arrayBuffer();
|
||||
const imageBufferPNG = await sharp(imageBufferWEBP).png().toBuffer();
|
||||
cache[url] = imageBufferPNG;
|
||||
}
|
||||
|
||||
return `data:image/png;base64,${cache[url].toString('base64')}`;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export async function readTexture(path: string, mutation?: TextureMutation) {
|
||||
raw.data[index + 3] = 255;
|
||||
}
|
||||
|
||||
return await sharp(raw.data, { raw }).png().toBuffer();
|
||||
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||
}
|
||||
|
||||
case TextureMutation.RoughnessMetallicness: {
|
||||
@@ -45,16 +45,16 @@ export async function readTexture(path: string, mutation?: TextureMutation) {
|
||||
});
|
||||
const metallicness = sharp(raw.data, { raw })
|
||||
.extractChannel('green')
|
||||
.png()
|
||||
.jpeg()
|
||||
.toBuffer();
|
||||
const roughness = sharp(raw.data, { raw })
|
||||
.extractChannel('alpha')
|
||||
.png()
|
||||
.jpeg()
|
||||
.toBuffer();
|
||||
|
||||
return await newImage
|
||||
.joinChannel(await Promise.all([roughness, metallicness]))
|
||||
.png()
|
||||
.jpeg()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
@@ -65,15 +65,15 @@ export async function readTexture(path: string, mutation?: TextureMutation) {
|
||||
*/
|
||||
return await sharp(raw.data, { raw })
|
||||
.extractChannel('alpha')
|
||||
.png()
|
||||
.jpeg()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
case TextureMutation.Albedo: {
|
||||
return await sharp(raw.data, { raw }).removeAlpha().png().toBuffer();
|
||||
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||
}
|
||||
|
||||
default:
|
||||
return await sharp(raw.data, { raw }).png().toBuffer();
|
||||
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||
}
|
||||
}
|
||||
@@ -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>',
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { iconPng } from './iconPng';
|
||||
import { tankIcon } from './tankIcon';
|
||||
|
||||
export function tankIconPng(id: number) {
|
||||
return iconPng(tankIcon(id));
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 to add national flags and tank class emojis"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+46
-13
@@ -137,10 +137,10 @@
|
||||
"description": "Región"
|
||||
},
|
||||
"tier": {
|
||||
"$": "tier",
|
||||
"description": "Tier",
|
||||
"$": "nivel",
|
||||
"description": "Nivel",
|
||||
"choices": {
|
||||
"tier": "Tier %s - %s"
|
||||
"tier": "Nivel %s - %s"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
},
|
||||
"filters": {
|
||||
"none": "Sin filtros",
|
||||
"tier": "Tier %s"
|
||||
"tier": "Nivel %s"
|
||||
},
|
||||
"no_data": {
|
||||
"battles_in_period": "No hay batallas jugadas en este período",
|
||||
@@ -170,10 +170,42 @@
|
||||
"tank_id_not_found": "# Tanque no encontrado\nNo se pudo encontrar al tanque del ID \"%s\". Prueba escribiendo el nombre del tanque en vez de un número.",
|
||||
"no_tank_stats": "# No hay estadísticas de tanque disponibles\nEste jugador no tiene estadísticas disponibles. Este podría no ser el jugador que estás buscando.",
|
||||
"get_help": "Servidor de ayuda de Discord",
|
||||
"uncaught_error": "# ¡Blitzkrieg sufrió un error!\nLo sentimos mucho. Siéntete libre de [unirte al servidor oficial de Discord](https://discord.gg/nDt7AjGJQH) para obtener ayuda."
|
||||
"uncaught_error": "# ¡Blitzkrieg sufrió un error!\nLo sentimos mucho. Siéntete libre de [unirte al servidor oficial de Discord](https://discord.gg/nDt7AjGJQH) para obtener ayuda.",
|
||||
"preview": "# Este comando es una vista previa\n\nSolo puedes acceder a este comando en [el servidor oficial de Discord](https://discord.gg/nDt7AjGJQH). Generalmente este comando estará disponible pronto."
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"research": {
|
||||
"$": "investigar",
|
||||
"description": "Enumera los gastos por investigar una línea",
|
||||
"options": {
|
||||
"target_tank": {
|
||||
"$": "tanque-objetivo",
|
||||
"description": "El último tanque a investigar en la línea"
|
||||
},
|
||||
"starting_tank": {
|
||||
"$": "tanque-inicial",
|
||||
"description": "El tanque inicial en la línea (se deduce automáticamente si no se proporciona)"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"subtitle": "Investigar",
|
||||
"total": "Total",
|
||||
"research": "Investigar: %s",
|
||||
"upgrades": "Mejoras: %s",
|
||||
"purchase": "Comprar: %s",
|
||||
"equipment": "Equipamiento: %s",
|
||||
"estimation": "# ¡Esto es una estimación!\n\nEsta lista podría incluir tanques ya investigados debido a la información errónea que proporciona Wargaming. Si puedes, prueba proporcionando un `tanque-inicial` de la línea al usar el comando."
|
||||
},
|
||||
"errors": {
|
||||
"non_tech_tree": "# %s no está en el árbol tecnológico\n\nNo puedes investigar un tanque prémium o coleccionista.",
|
||||
"tier_1": "# %s es un nivel I\n\nLos tanque de nivel I están siempre desbloqueados gratuitamente para todos los jugadores.",
|
||||
"already_researched": "# %s ya está investigado\n\nPrueba proporcionando un `tanque-inicial` al usar el comando para ver los totales para una línea que ya has investigado.",
|
||||
"start_end_equal": "# %s fue proporcionado dos veces\n\nAsegúrate de que `tanque-inicial` y `tanque-objetivo` son diferentes.",
|
||||
"tanks_not_on_line": "# %s y %s no están en la misma línea\n\nAsegúrate de que `tanque-inicial` y `tanque-objetivo` están en la misma línea del árbol tecnológico.",
|
||||
"unordered": "# %s de nivel superior a %s\n\nAsegúrate de proporcionar %s como el `tanque-inicial` en su lugar ya que es el tanque de nivel inferior."
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"$": "sobre",
|
||||
"description": "Toda la información que necesitas sobre el bot",
|
||||
@@ -278,13 +310,13 @@
|
||||
"lethality": {
|
||||
"$": "Letalidad",
|
||||
"average_damage": "Daño promedio",
|
||||
"damage_per_tier": "Daño por tier",
|
||||
"damage_per_tier": "Daño por nivel",
|
||||
"damage_ratio": "Relación de daño",
|
||||
"average_kills": "Promedio de bajas"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"$": "Misceláneo",
|
||||
"average_tier": "Tier promedio",
|
||||
"average_tier": "Nivel promedio",
|
||||
"average_spots": "Promedio de detecciones",
|
||||
"average_xp": "Promedio de XP",
|
||||
"kills_to_death_ratio": "Relación bajas-muertes"
|
||||
@@ -386,7 +418,7 @@
|
||||
"winrate": "Tasa de victorias",
|
||||
"damage": "Daño",
|
||||
"damage_ratio": "Relación de daño",
|
||||
"tier": "Tier",
|
||||
"tier": "Nivel",
|
||||
"survival": "Supervivencia",
|
||||
"accuracy": "Precisión",
|
||||
"kill_ratio": "Relación de bajas"
|
||||
@@ -394,9 +426,9 @@
|
||||
},
|
||||
"owned_tanks": {
|
||||
"$": "tanques-propios",
|
||||
"description": "Muestra todos los tanques propios en un tier",
|
||||
"description": "Muestra todos los tanques propios en un nivel",
|
||||
"body": {
|
||||
"subtitle": "Tanques de tier %s"
|
||||
"title": "# Tanques de %s de nivel %s\n\n"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
@@ -404,9 +436,10 @@
|
||||
"description": "Comprueba si el bot tiene los permisos requeridos para funcionar",
|
||||
"body": {
|
||||
"title": "# Permisos\n\n%s",
|
||||
"view_channels": "Ver canales: necesario para que funcione el botón de actualizar",
|
||||
"read_message_history": "Leer el historial de mensajes: necesario para que el botón de actualizar pueda encontrar el mensaje a actualizar",
|
||||
"attach_files": "Adjuntar archivos: necesario para que funcionen los comandos basados en imágenes"
|
||||
"view_channels": "Ver canales: necesario para encontrar el mensaje en el cual el botón \"actualizar\" fue usado",
|
||||
"read_message_history": "Leer historial de mensajes: necesario para encontrar el mensaje a actualizar",
|
||||
"attach_files": "Adjuntar archivos: necesario para añadir imágenes a los comandos basados en estas",
|
||||
"external_emojis": "Emoticonos externos: necesario para añadir emoticonos de banderas nacionales y clases de tanque"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
|
||||
+39
-6
@@ -170,10 +170,42 @@
|
||||
"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",
|
||||
@@ -371,7 +403,7 @@
|
||||
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running ratings season.",
|
||||
"deleted_player": "Deleted %s",
|
||||
"neighbors": "Ratings neighbors",
|
||||
"more": "+%s more"
|
||||
"more": "+ %s more"
|
||||
}
|
||||
},
|
||||
"today": {
|
||||
@@ -396,7 +428,7 @@
|
||||
"$": "owned-tanks",
|
||||
"description": "Shows all own tanks in a tier",
|
||||
"body": {
|
||||
"subtitle": "Tier %s tanks"
|
||||
"title": "# %s's owned tier %s tanks\n\n"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
@@ -404,9 +436,10 @@
|
||||
"description": "Checks wether the bot has the required permissions to function",
|
||||
"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 add national flags and tank class emojis"
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const theme = {
|
||||
},
|
||||
|
||||
borderStyles: {
|
||||
subtle: `1px solid ${slateDark.slate2}`,
|
||||
...createBorderStyles(slateDark),
|
||||
...createBorderStyles(blueDark, undefined, 'blue'),
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface TankopediaPersistent {
|
||||
showGrid: boolean;
|
||||
greenPenetration: boolean;
|
||||
showEnvironment: boolean;
|
||||
showSpacedArmor: boolean;
|
||||
};
|
||||
};
|
||||
sort: {
|
||||
@@ -123,6 +124,7 @@ export const useTankopediaPersistent = create<TankopediaPersistent>()(
|
||||
subscribeWithSelector<TankopediaPersistent>(() => ({
|
||||
model: {
|
||||
visual: {
|
||||
showSpacedArmor: true,
|
||||
wireframe: false,
|
||||
opaque: false,
|
||||
environment: ENVIRONMENTS[0],
|
||||
|
||||
@@ -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==
|
||||
@@ -2361,13 +2361,6 @@
|
||||
debug "^4.3.1"
|
||||
loader-utils "^2.0.0"
|
||||
|
||||
"@vercel/analytics@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-1.2.2.tgz#715d8f203a170c06ba36b363e03b048c03060d5d"
|
||||
integrity sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==
|
||||
dependencies:
|
||||
server-only "^0.0.1"
|
||||
|
||||
"@vitejs/plugin-vue@^5.0.4":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37"
|
||||
@@ -5049,11 +5042,6 @@ [email protected]:
|
||||
parseurl "~1.3.3"
|
||||
send "0.18.0"
|
||||
|
||||
server-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e"
|
||||
integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==
|
||||
|
||||
set-function-length@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425"
|
||||
|
||||
Reference in new issue
Block a user