Compare commits

...
63 Commits
Author SHA1 Message Date
TrèsAbhi c8edd16eb3 feat: logs 2024-03-18 16:47:53 -05:00
TrèsAbhi b96ff2e239 New Crowdin updates (#101) 2024-03-18 16:36:55 -05:00
TrèsAbhi a575da3005 fix: move script to head 2024-03-18 10:09:46 -05:00
TrèsAbhi 3ca2dece3e feat: google analytics 2024-03-18 10:02:09 -05:00
TrèsAbhi 191d6133b2 feat: togglable spaced armor 2024-03-17 22:11:56 -05:00
TrèsAbhi b15e73e3a8 Update build.ts 2024-03-17 21:53:09 -05:00
TrèsAbhi 9a6b8ab979 Update cd-host.yml 2024-03-17 21:51:48 -05:00
TrèsAbhi 87786d41a7 feat: logs 2024-03-17 21:48:23 -05:00
TrèsAbhi ee7ffe7bc8 feat: include sharp in build 2024-03-17 21:45:55 -05:00
TrèsAbhi 379a33375d feat: better totals 2024-03-17 21:40:21 -05:00
TrèsAbhi bd3f5dea05 feat: optimized png cache 2024-03-17 21:23:57 -05:00
TrèsAbhi 146d36234e New Crowdin updates (#99) 2024-03-17 21:19:24 -05:00
TrèsAbhi f3edcb8077 Update cd-assets.yml 2024-03-17 20:17:11 -05:00
TrèsAbhi 9e492015ff feat: jpg texture files 2024-03-16 15:34:48 -05:00
TrèsAbhi d2977182d9 fix: grammer 2024-03-16 15:24:25 -05:00
TrèsAbhi 7d2d65bab8 fix: install sharp 2024-03-16 15:18:23 -05:00
TrèsAbhi 415c3bd066 fix: fixed sharp 2024-03-16 15:15:34 -05:00
TrèsAbhi 990ab1520c Merge pull request #100 from tresabhi/dev 2024-03-16 15:12:54 -05:00
TrèsAbhi 81cb1afeff fix: types 2024-03-16 15:12:47 -05:00
TrèsAbhi dba5a0a2cd feat: logs 2024-03-16 15:10:55 -05:00
TrèsAbhi 04cd205fee feat: preview servers 2024-03-16 15:07:05 -05:00
TrèsAbhi ec93d8e9ab feat: better words 2024-03-16 10:41:25 -05:00
TrèsAbhi 56321d9e33 feat: simplified owned tanks command 2024-03-16 00:06:44 -05:00
TrèsAbhi d39ab43e74 feat: error handling for /research 2024-03-15 21:50:19 -05:00
TrèsAbhi 787d3ae6ea feat: functionality and icons 2024-03-15 12:28:29 -05:00
TrèsAbhi 035f751bf1 feat: remove protobuf 2024-03-15 10:47:05 -05:00
TrèsAbhi 43442ac829 feat: start of protobuf 2024-03-15 00:21:58 -05:00
TrèsAbhi da02a089b9 feat: xp definitions 2024-03-14 23:31:50 -05:00
TrèsAbhi 476d8e804a feat: some ui 2024-03-14 17:31:11 -05:00
TrèsAbhi 43e1d53d39 feat: resolving lines 2024-03-14 16:43:28 -05:00
TrèsAbhi 5372068830 feat: added trees 2024-03-13 23:23:06 -05:00
TrèsAbhi 025cec3687 Merge pull request #98 from tresabhi/main 2024-03-13 20:30:50 -05:00
TrèsAbhi c47c629a83 Merge pull request #95 from tresabhi/l10n 2024-03-13 20:30:09 -05:00
TrèsAbhi 2b7907f8f7 fix: use main 2024-03-13 20:29:28 -05:00
TrèsAbhi 903d4e4d95 fix: multiple equipments 2024-03-13 18:50:08 -05:00
TrèsAbhi 8857b2baae feat: production ready 2024-03-13 18:36:48 -05:00
TrèsAbhi f7bbb17f66 Merge pull request #96 from tresabhi/1.16.4 2024-03-13 18:28:59 -05:00
TrèsAbhi f407ffe4bb feat: ignore script types 2024-03-13 18:25:13 -05:00
TrèsAbhi 791a09daae feat: simplified imports 2024-03-13 18:15:13 -05:00
TrèsAbhi 9592c0b55f feat: logs 2024-03-13 18:00:25 -05:00
TrèsAbhi d9a34dc60e fix: use no-store 2024-03-13 17:52:06 -05:00
TrèsAbhi 30e4e4e9b3 fix: autoreloader dpm by crew stats 2024-03-13 17:46:48 -05:00
TrèsAbhi bd3ea9a8ec fix: crew stats for auto reloader dpm 2024-03-13 17:42:19 -05:00
TrèsAbhi 8131b69422 fix: use WOTB_DLC_CDN 2024-03-13 17:35:40 -05:00
TrèsAbhi 340d9c2666 feat: nicer layer penetration indices 2024-03-13 17:34:29 -05:00
TrèsAbhi 2a169fac34 fix: more accurate splash mechanics 2024-03-13 17:15:22 -05:00
TrèsAbhi 4819100ec6 feat: sharp-based texture manipulation 2024-03-13 17:00:40 -05:00
TrèsAbhi f39a94cbe0 fix: us depot 2024-03-13 16:52:52 -05:00
TrèsAbhi e61598f768 Update cd-assets.yml 2024-03-13 14:14:14 -05:00
TrèsAbhi 04ee3a0a44 fix: use correct gh token 2024-03-13 13:30:08 -05:00
TrèsAbhi 8fa613f1e2 fix: use github token 2024-03-13 13:29:14 -05:00
TrèsAbhi bdc240df08 feat: push on any 2024-03-13 13:27:15 -05:00
TrèsAbhi 7169e0d91a New translations en-us.json (Spanish) 2024-03-13 05:37:55 -05:00
TrèsAbhi 889ed7e191 feat: verbose 2024-03-13 00:22:54 -05:00
TrèsAbhi 1fb8dac805 feat: smarter host 2024-03-13 00:19:33 -05:00
TrèsAbhi 9f71ac68b9 feat: patches 2024-03-13 00:17:30 -05:00
TrèsAbhi c1f3517a79 Merge pull request #97 from tresabhi/main 2024-03-12 23:39:24 -05:00
TrèsAbhi adb0af0f18 Delete docs/changelogs/1.16.2 copy.md 2024-03-12 23:39:06 -05:00
TrèsAbhi f79f1ffdc3 feat: organize files 2024-03-12 23:36:56 -05:00
TrèsAbhi 1c9b7463fc New translations en-us.json (Croatian (disambiguation)) 2024-03-12 23:04:08 -05:00
TrèsAbhi 80741d66fc New translations en-us.json (Spanish) 2024-03-12 23:04:07 -05:00
TrèsAbhi f02fb00e8c fix: space after plus 2024-03-12 22:50:08 -05:00
TrèsAbhi 3d353cc3cf feat: updated workflow action versions 2024-03-12 22:48:41 -05:00
157 changed files with 2097 additions and 1001 deletions

No files matched your search

+12 -2
View File
@@ -2,10 +2,9 @@ name: CD Build Assets
on:
push:
branches: [dev, main]
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
- cron: '0 2 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -18,6 +17,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Use node
uses: actions/setup-node@v4
@@ -46,6 +48,14 @@ jobs:
- name: Run DepotDownloader
run: ./temp/depotDownloader/DepotDownloader.exe -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
- name: Apply patches
run: |
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
$env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}"
yarn setup:patches --depot
- name: Build assets
run: |
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
+3 -3
View File
@@ -18,10 +18,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -54,7 +54,7 @@ jobs:
uses: actions/deploy-pages@v1
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: dist
path: docs/.vitepress/dist
+9 -1
View File
@@ -6,6 +6,8 @@ on:
schedule:
- cron: '0 */5 * * *'
workflow_dispatch:
release:
types: [created, published, released, edited]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -17,10 +19,16 @@ jobs:
steps:
- name: Use node
uses: actions/setup-node@v2
uses: actions/setup-node@v4
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 \
+2 -2
View File
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
+2 -2
View File
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
+2 -2
View File
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
+6 -3
View File
@@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -33,11 +33,14 @@ jobs:
- name: Install dependencies
run: yarn
- name: Add fixed sharp
run: yarn add [email protected] --ignore-engines
- name: Build
run: yarn build:bot
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: bot
path: dist/bot
+3 -3
View File
@@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -37,7 +37,7 @@ jobs:
run: yarn build:docs
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: docs
path: dist/docs
+3 -3
View File
@@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -37,7 +37,7 @@ jobs:
run: yarn build:website
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: website
path: dist/website
+2 -1
View File
@@ -17,4 +17,5 @@ tsconfig.tsbuildinfo
temp/
/.yarn
/.VSCodeCounter
/.yarn
/.yarn
depots
+2
View File
@@ -7,6 +7,7 @@
"alphatest",
"APAC",
"apcr",
"applypatches",
"artefacts",
"atgm",
"AYUV",
@@ -21,6 +22,7 @@
"CDON",
"classname",
"cleandist",
"closedkrieg",
"cockroackdb",
"Coef",
"cooldown",
-36
View File
@@ -1,36 +0,0 @@
# Blitzkrieg 1.16.2
¡Hola desde el mundo del español!
## Features
- Added translations for Spanish to the bot
- Your language will be inferred automatically from your Discord settings
- Any untranslated languages and words we forgot to translate will be left in English
- If you have your language set to Spanish and see English in any places, please let us know on [the official Discord server](https://discord.gg/nDt7AjGJQH)
- All `/about` commands except for `commands` remain in English
- Tank names are still in English
- The website remains unchanged
- The title bar (featured as the big text on top of most commands) has now been made more compact
## Changes
- "Tank type" is now called "tank class"
- Removed `/player-achievements` and `search-tanks`
- They will be reintroduced in the future in a far better way
- Also removed a `SKLLD` server exclusive `/eligible` command
- This will be generalized for all clans in the future
## Fixes
- Fixed the red glowing floor underneath the tank sandbox
- This has been replaced with a static image
- The refresh button is now properly removed when lacking permissions
- Tank names that start with a number are now properly inferred from command inputs
## Technical Changes
- Command registration is now error tolerant meaning that if a single command fails to initiate, it will be discarded until the next restart
- We are currently looking into how we can allow the command to retry registration after a fixed amount of time
- Due to the implications of the code added which can be very easily exploited by hackers, a single file has been added to the repository as a closed source submodule and will remain closed unless a third party open-source implementation is made public
- Removed preview commands
+19
View File
@@ -0,0 +1,19 @@
# Blitzkrieg 1.16.4
A few issues here and there addressed for good measure.
## Changes
- Armor layer penetration indices are now much more compact.
## Fixes
- Reverted splash mechanics changes from `1.16.1` as it made them less accurate.
- Auto-reloaders now report their DPM stats correctly with crew training.
## Technical Changes
- All files in the project were reviewed and constant exports were isolated.
- The downloaded game in the workflow now also includes the latest patches.
- Asset building now uses sharp under the hood which should in theory be faster.
- Fetching CDON LZ4 files now uses `no-store` instead of `no-cache`
+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.
+8
View File
@@ -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.
+13
View File
@@ -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
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkrieg",
"version": "1.16.2",
"version": "1.17.1",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
@@ -12,13 +12,14 @@
"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",
"start:website": "next start",
"host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
"setup:patches": "vite-node --script scripts/applyPatches",
"lint": "npx tsc",
"lint:website": "next lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
@@ -34,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",
@@ -42,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",
+48
View File
@@ -0,0 +1,48 @@
import { mkdir, writeFile } from 'fs/promises';
import { parse as parsePath } from 'path';
import { parse as parseYaml } from 'yaml';
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
import { writeDVPL } from '../src/core/blitz/writeDVPL';
import { secrets } from '../src/core/blitzkrieg/secrets';
import { dvp } from '../submodules/closedkrieg/src/dvp';
import { DATA } from './buildAssets/constants';
const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`);
const currentVersion = versionTextFile
.split(' ')[0]
.split('.')
.slice(0, 3)
.join('.');
console.log(`Installing patches for ${currentVersion}...`);
let patchIndex = 1;
while (true) {
const response = await fetch(
`${secrets.WOTB_DLC_CDN}/dlc/s${currentVersion}_${patchIndex}.yaml`,
);
if (response.status === 200) {
console.log(`Applying patch ${patchIndex}...`);
const data = parseYaml(await response.text());
const dvpm = await fetch(`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11}`).then(
(response) => response.arrayBuffer(),
);
const dvpd = await fetch(
`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11.replace('.dvpm', '.dvpd')}`,
).then((response) => response.arrayBuffer());
const files = await dvp(dvpm, dvpd);
await Promise.all(
files.map(async ({ path, data }) => {
const { dir } = parsePath(path);
await mkdir(`${DATA}/${dir}`, { recursive: true });
await writeFile(`${DATA}/${path}.dvpl`, writeDVPL(Buffer.from(data)));
}),
);
patchIndex++;
} else break;
}
+4
View File
@@ -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
+1 -1
View File
@@ -24,7 +24,7 @@ export const POI = {
collisionMeshes: '3d/Tanks/CollisionMeshes',
optionalDevices: 'XML/item_defs/vehicles/common/optional_devices.xml',
optionalDeviceSlots:
'XML/item_defs/vehicles/common/optional_device_slots.xml',
'XML/item_defs/vehicles/common/optional_device_slots.xml',
optionalDeviceImageMappings:
'UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml',
optionalDevicesImage:
+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);
}
+161 -42
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;
@@ -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',
[
+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) {
-19
View File
@@ -1,21 +1,15 @@
import { NodeIO } from '@gltf-transform/core';
import { readdir } from 'fs/promises';
import { Vector3Tuple } from 'three';
import { extractModel } from '../../src/core/blitz/extractModel';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
// import { AssetCommit } from '../../src/core/github/assetCommit';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
import { DATA, POI } from './constants';
import { VehicleDefinitionList } from './definitions';
import { TankParameters } from './tankIcons';
interface VehicleCustomization {
armorColor: string;
}
export async function tankModels(production: boolean) {
console.log('Building tank models...');
@@ -27,18 +21,9 @@ export async function tankModels(production: boolean) {
for (const nationIndex in nations) {
const nation = nations[nationIndex];
const changes: FileChange[] = [];
// const assetCommit = new AssetCommit(`tank models ${nation}`, production);
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
);
const customization = await readXMLDVPL<{ root: VehicleCustomization }>(
`${DATA}/${POI.vehicleDefinitions}/${nation}/customization.xml.dvpl`,
);
const baseColor = customization.root.armorColor
.split(' ')
.slice(0, 3)
.map(Number)
.map((channel) => channel / 255) as Vector3Tuple;
console.log(`Building models for ${nation}`);
@@ -56,11 +41,8 @@ export async function tankModels(production: boolean) {
const model = await extractModel(
DATA,
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
baseColor,
);
// writeFile(`test.${tankKey}.glb`, await nodeIO.writeBinary(model));
changes.push({
path: `3d/tanks/models/${id}.glb`,
content: Buffer.from(await nodeIO.writeBinary(model)).toString(
@@ -71,7 +53,6 @@ export async function tankModels(production: boolean) {
}),
);
// assetCommit.push();
await commitAssets(`tank models ${nation}`, changes, production);
}
}
+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
}
}
+2 -4
View File
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { resetSession } from '../../../core/blitzkrieg/resetSession';
import {
TankDefinition,
tankDefinitions,
@@ -13,10 +14,7 @@ import { tankAverages } from '../../../core/blitzstars/tankAverages';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import mutateSession, {
resetSession,
useSession,
} from '../../../stores/session';
import mutateSession, { useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
+2 -3
View File
@@ -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"
+3 -1
View File
@@ -22,7 +22,9 @@ import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
import searchPlayersAcrossRegions, {
AccountListWithServer,
} from '../../../core/blitz/searchPlayersAcrossRegions';
import { resetSession, setSession, useSession } from '../../../stores/session';
import { resetSession } from '../../../core/blitzkrieg/resetSession';
import { setSession } from '../../../core/blitzkrieg/setSession';
import { useSession } from '../../../stores/session';
import SessionPage from '../../embeds/session/page';
import { Menu } from './components/Menu';
import * as styles from './page.css';
@@ -19,7 +19,7 @@ import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopedi
import {
SHELL_NAMES,
TIER_ROMAN_NUMERALS,
} from '../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
import { mutateDuel, useDuel } from '../../../../../stores/duel';
import {
mutateTankopediaTemporary,
@@ -1,6 +1,6 @@
import { Flex, Heading } from '@radix-ui/themes';
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
@@ -18,7 +18,7 @@ import {
CREW_MEMBER_NAMES,
GUN_TYPE_NAMES,
SHELL_NAMES,
} from '../../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
import { useConsumable } from '../../../../../../hooks/useConsumable';
import { useEquipment } from '../../../../../../hooks/useEquipment';
@@ -268,9 +268,13 @@ export function Characteristics() {
<>
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
{gun.reload.at(-1)! < gun.reload.at(-2)!
? (shell.damage.armor / (gun.reload.at(-1)! + gun.intraClip)) *
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
60
: (shell.damage.armor / gun.reload[0]) * 60}
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient)) *
60}
</InfoWithDelta>
<InfoWithDelta
decimals={0}
@@ -279,13 +283,26 @@ export function Characteristics() {
unit="hp / min"
>
{gun.reload.at(-1)! < gun.reload.at(-2)!
? (shell.damage.armor / (gun.reload.at(-1)! + gun.intraClip)) *
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(0, -1).length - 1) * gun.intraClip) +
shell.damage.armor * gun.reload.slice(0, -1).length
: (shell.damage.armor / (gun.reload[0] + gun.intraClip)) *
(60 - (gun.reload.slice(1).length - 1) * gun.intraClip) +
shell.damage.armor * gun.reload.slice(1).length}
(gun.reload.slice(0, -1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient *
shell.damage.armor *
gun.reload.slice(0, -1).length
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient *
shell.damage.armor *
gun.reload.slice(1).length}
</InfoWithDelta>
<Info
indent
@@ -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) => {
@@ -1,7 +1,7 @@
import { UpdateIcon } from '@radix-ui/react-icons';
import { Button, Dialog, Flex, Heading } from '@radix-ui/themes';
import { useState } from 'react';
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks';
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks/components/Item/constants';
import { assignDuelMember } from '../../../../../core/blitzkrieg/assignDuelMember';
import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath';
import { useDuel } from '../../../../../stores/duel';
@@ -1,9 +1,7 @@
import { Button, Flex, Text } from '@radix-ui/themes';
import { SmallTankIcon } from '../../../../../../components/SmallTankIcon';
import {
TIER_ROMAN_NUMERALS,
TankDefinition,
} from '../../../../../../core/blitzkrieg/tankDefinitions';
import { TankDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
interface CompactSearchResultRowProps {
tanks: TankDefinition[];
@@ -4,17 +4,18 @@ import { CaretDownIcon } from '@radix-ui/react-icons';
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
import { use } from 'react';
import { Flag } from '../../../../../../../components/Flag';
import {} from '../../../../../../../components/Tanks';
import {
TANK_CLASSES,
TREE_TYPES,
TREE_TYPE_ICONS,
TREE_TYPE_IMAGES,
} from '../../../../../../../components/Tanks';
} from '../../../../../../../components/Tanks/components/Item/constants';
import { NATIONS } from '../../../../../../../core/blitzkrieg/tankDefinitions';
import {
NATIONS,
TIERS,
TIER_ROMAN_NUMERALS,
} from '../../../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
import mutateTankopediaPersistent, {
TankopediaTestTankDisplay,
useTankopediaPersistent,
@@ -15,7 +15,8 @@ import {
} from '@radix-ui/themes';
import { go } from 'fuzzysort';
import { use, useEffect, useMemo, useRef, useState } from 'react';
import { TANK_CLASSES, TreeType } from '../../../../../components/Tanks';
import { TreeType } from '../../../../../components/Tanks';
import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants';
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
import { asset } from '../../../../../core/blitzkrieg/asset';
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
@@ -24,13 +25,15 @@ import { resolveDpm } from '../../../../../core/blitzkrieg/resolveDpm';
import { resolveReload } from '../../../../../core/blitzkrieg/resolveReload';
import {
NATIONS,
TankDefinition,
tanksDefinitionsArray,
} from '../../../../../core/blitzkrieg/tankDefinitions';
import {
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
TIER_ROMAN_NUMERALS,
TankDefinition,
tanksDefinitionsArray,
} from '../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
import { tankIcon } from '../../../../../core/blitzkrieg/tankIcon';
import { unionBoundingBox } from '../../../../../core/blitzkrieg/unionBoundingBox';
import { theme } from '../../../../../stitches.config';
+71
View File
@@ -0,0 +1,71 @@
import { getBlitzStarsLinkButton } from '../../core/blitzstars/getBlitzStarsLinkButton';
import addPeriodicFilterOptions from '../../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../../core/discord/addUsernameChoices';
import autocompleteTanks from '../../core/discord/autocompleteTanks';
import autocompleteUsername from '../../core/discord/autocompleteUsername';
import { buttonRefresh } from '../../core/discord/buttonRefresh';
import commandToURL from '../../core/discord/commandToURL';
import { createLocalizedCommand } from '../../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../../events/interactionCreate';
import { renderBreakdown } from './render';
export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: await addPeriodicFilterOptions(
createLocalizedCommand('breakdown'),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
return [
...(await renderBreakdown(
player,
period,
filters,
interaction.locale,
)),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(
player,
period,
filters,
interaction.locale,
);
},
});
},
);
@@ -1,43 +1,25 @@
import { Locale } from 'discord.js';
import { chunk } from 'lodash';
import * as Breakdown from '../components/Breakdown';
import CommandWrapper from '../components/CommandWrapper';
import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar';
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import * as Breakdown from '../../components/Breakdown';
import CommandWrapper from '../../components/CommandWrapper';
import NoData from '../../components/NoData';
import TitleBar from '../../components/TitleBar';
import { AllStats, getAccountInfo } from '../../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../../core/blitz/getClanAccountInfo';
import getTankStats from '../../core/blitz/getTankStats';
import { filtersToDescription } from '../../core/blitzkrieg/filtersToDescription';
import {
TankDefinition,
tankDefinitions,
} from '../core/blitzkrieg/tankDefinitions';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../core/blitzstars/tankAverages';
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand, {
ResolvedPeriod,
} from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import calculateWN8 from '../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistry } from '../events/interactionCreate';
} from '../../core/blitzkrieg/tankDefinitions';
import getStatsInPeriod from '../../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand';
import { translator } from '../../core/localization/translator';
import calculateWN8 from '../../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../../core/statistics/filterStats';
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
@@ -296,57 +278,3 @@ export async function renderBreakdown(
];
}
}
export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: await addPeriodicFilterOptions(
createLocalizedCommand('breakdown'),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
return [
...(await renderBreakdown(
player,
period,
filters,
interaction.locale,
)),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(
player,
period,
filters,
interaction.locale,
);
},
});
},
);
+2 -1
View File
@@ -1,6 +1,7 @@
import { Locale, SlashCommandSubcommandGroupBuilder } from 'discord.js';
import CommandWrapper from '../components/CommandWrapper';
import * as Graph from '../components/Graph';
import { LineColor } from '../components/Graph/components/Line/constants';
import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
@@ -93,7 +94,7 @@ async function render(
xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
>
{Graph.Line({
color: Graph.LineColor.Red,
color: LineColor.Red,
plot: plot,
minY,
maxY,
+31 -78
View File
@@ -1,18 +1,16 @@
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 {
TIER_ROMAN_NUMERALS,
NATIONS,
TankDefinition,
Tier,
flags,
tankDefinitions,
} from '../core/blitzkrieg/tankDefinitions';
import { tankIcon } from '../core/blitzkrieg/tankIcon';
import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants';
import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -21,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,
@@ -33,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) => ({
@@ -45,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[] = [];
@@ -58,73 +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}
image={tankIcon(tank.id)}
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}
image={tankIcon(tank.id)}
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]) => {
+574
View File
@@ -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']);
},
});
});
+2 -4
View File
@@ -3,10 +3,8 @@ import markdownEscape from 'markdown-escape';
import { Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchBlitz';
import {
AccountList,
usernamePattern,
} from '../core/blitz/searchPlayersAcrossRegions';
import { AccountList } from '../core/blitz/searchPlayersAcrossRegions';
import { usernamePattern } from '../core/blitz/searchPlayersAcrossRegions/constants';
import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
+1 -1
View File
@@ -15,7 +15,7 @@ import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown';
import { renderBreakdown } from './breakdown/render';
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
const nations = await NATIONS;
@@ -3,7 +3,7 @@ import { Percentile } from '../../../../constants/percentiles';
import { translator } from '../../../../core/localization/translator';
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
import { theme } from '../../../../stitches.config';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator/constants';
import { Glow } from './components/Glow';
export interface WN8DisplayProps {
+21
View File
@@ -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');
`,
}}
/>
</>
);
}
@@ -1,12 +1,6 @@
import { useFrame } from '@react-three/fiber';
import { useEffect } from 'react';
import {
MeshBasicMaterial,
Object3D,
ShaderMaterial,
Vector2,
WebGLRenderTarget,
} from 'three';
import { MeshBasicMaterial, Object3D, ShaderMaterial, Vector2 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { canSplash } from '../../../../core/blitz/canSplash';
import { isExplosive } from '../../../../core/blitz/isExplosive';
@@ -21,8 +15,7 @@ import {
} from '../../../../stores/tankopedia';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
export const spacedArmorRenderTarget = new WebGLRenderTarget();
import { spacedArmorRenderTarget } from './target';
interface CoreArmorSceneComponentProps {
thickness: number;
@@ -55,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 },
@@ -82,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,11 +50,11 @@ void main() {
float coreArmorDistance = depthToDistance(gl_FragCoord.z);
float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance;
if (isUnderSpacedArmor) {
if (useSpacedArmor && isUnderSpacedArmor) {
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
remainingPenetration -= spacedArmorThickness;
if (isExplosive && !canSplash && remainingPenetration > 0.0) {
if (isExplosive && remainingPenetration > 0.0) {
// there is a 50% penetration loss per meter for HEAT shells
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
}
@@ -0,0 +1,3 @@
import { WebGLRenderTarget } from 'three';
export const spacedArmorRenderTarget = new WebGLRenderTarget();
+26 -21
View File
@@ -1,4 +1,4 @@
import { Card, Flex, Inset, Text } from '@radix-ui/themes';
import { Card, Flex, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
@@ -182,33 +182,38 @@ export function ShotDisplay() {
pointerEvents: 'none',
}}
>
<Card
<div
style={{
backgroundColor:
trueStatus === 'penetration'
? '#00ff00c0'
? '#00ff0080'
: trueStatus === 'blocked'
? '#ff0000c0'
: '#ffff00c0',
? '#ff000080'
: '#ffff0080',
borderRadius: 8,
border: 'none',
padding: 4,
width: hasMultipleLayers ? undefined : 8,
height: hasMultipleLayers ? undefined : 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Inset>
{hasMultipleLayers && (
<Text
style={{
width: '100%',
display: 'block',
textAlign: 'center',
color:
trueStatus === 'ricochet' ? 'black' : undefined,
}}
>
<b>{layer.index + 1}</b>
</Text>
)}
</Inset>
</Card>
{hasMultipleLayers && (
<Text
size="2"
style={{
width: '100%',
display: 'block',
textAlign: 'center',
color: trueStatus === 'ricochet' ? 'black' : undefined,
}}
>
<b>{layer.index + 1}</b>
</Text>
)}
</div>
</Html>
</group>
</>
@@ -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,
);
@@ -170,7 +179,7 @@ export function SpacedArmorSceneComponent({
const distance = (
shot.layers.at(-1) as ShotLayerBase
).point.distanceTo(intersections[0].point);
if (explosive && !splashing)
if (explosive)
remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration < 0;
@@ -196,7 +205,7 @@ export function SpacedArmorSceneComponent({
const previousIntersection = intersections[layerIndex - 1];
const distance =
intersection.distance - previousIntersection.distance;
if (explosive && !splashing)
if (explosive)
remainingPenetration -=
0.5 * remainingPenetration * distance;
const wasted = remainingPenetration < 0;
+1 -1
View File
@@ -2,7 +2,7 @@ import { createPortal, useFrame } from '@react-three/fiber';
import { memo, useState } from 'react';
import { DepthTexture, Scene, Vector2 } from 'three';
import { CoreArmorScene } from './components/CoreArmorScene';
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent/target';
import { SpacedArmorScene } from './components/SpacedArmorScene';
export const Armor = memo(() => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface RootProps {
interface RootProps {
children: ReactNode;
}
+2 -1
View File
@@ -5,7 +5,8 @@ import {
GrayColor,
PALETTES,
} from '../../../constants/radixColors';
import { TREE_TYPE_ICONS, TankClass, TreeType } from '../../Tanks';
import { TankClass, TreeType } from '../../Tanks';
import { TREE_TYPE_ICONS } from '../../Tanks/components/Item/constants';
import { RowStat } from './RowStat';
export interface RowStatItem {
@@ -7,7 +7,7 @@ import {
} from '../../../constants/radixColors';
import PercentileIndicator from '../../PercentileIndicator';
export interface RowStatProps {
interface RowStatProps {
name: string;
value?: string | number;
delta?: number;
+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}
@@ -2,13 +2,13 @@ import { Percentile } from '../../../constants/percentiles';
import { theme } from '../../../stitches.config';
import PercentileIndicator from '../../PercentileIndicator';
export interface CardItem {
interface CardItem {
label: string;
value: string | number | undefined;
percentile?: Percentile;
}
export interface CardProps {
interface CardProps {
items: CardItem[];
title: string;
}
@@ -1,4 +1,4 @@
export interface RootProps {
interface RootProps {
children: React.ReactNode;
}
@@ -1,4 +1,4 @@
export interface RowProps {
interface RowProps {
children: React.ReactNode;
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { Card } from './components/Card';
import { Root } from './components/Root';
import { Row } from './components/Row';
export interface GenericAllStatsProps {
interface GenericAllStatsProps {
stats: AllStats;
supplementaryStats?: SupplementaryStats;
locale: Locale;
@@ -1,8 +1,8 @@
import { Stat } from '..';
import { GenericStat } from '..';
import GenericStatsRow from './GenericStatsRow';
export interface GenericStatsColumnProps {
stats: Stat[];
interface GenericStatsColumnProps {
stats: GenericStat[];
}
export default function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
@@ -1,9 +1,9 @@
import { Stat } from '..';
import { GenericStat } from '..';
import { theme } from '../../../stitches.config';
import PercentileIndicator from '../../PercentileIndicator';
export interface GenericStatsRowProps {
stat: Stat;
interface GenericStatsRowProps {
stat: GenericStat;
}
export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
+3 -3
View File
@@ -1,12 +1,12 @@
import { Percentile } from '../../constants/percentiles';
import GenericStatsColumn from './components/GenericStatsColumn';
export type Stat =
export type GenericStat =
| [string, string | number | undefined]
| [string, string | number | undefined, Percentile | undefined];
export interface GenericStatsProps {
stats: Stat[];
stats: GenericStat[];
columnCount?: number;
}
@@ -16,7 +16,7 @@ export default function GenericStats({
}: GenericStatsProps) {
const filteredStats = stats.filter(([, value]) => value !== undefined);
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
const columns: Stat[][] = [];
const columns: GenericStat[][] = [];
filteredStats.forEach((stat, index) => {
const column = Math.floor(index / itemsPerRow);
@@ -0,0 +1,11 @@
export enum LineColor {
Red,
Green,
Blue,
White,
}
export enum LineStyle {
Solid,
Dashes,
DashesAndDots,
}
@@ -1,12 +1,6 @@
import { blue, green, red, slateDark } from '@radix-ui/colors';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './Root';
export enum LineColor {
Red,
Green,
Blue,
White,
}
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
import { LineColor, LineStyle } from './constants';
const LINE_COLORS = {
[LineColor.Red]: red.red9,
@@ -15,12 +9,6 @@ const LINE_COLORS = {
[LineColor.White]: slateDark.slate11,
};
export enum LineStyle {
Solid,
Dashes,
DashesAndDots,
}
const LINE_STYLES = {
[LineStyle.Solid]: undefined,
[LineStyle.Dashes]: '8, 8',
@@ -28,9 +16,9 @@ const LINE_STYLES = {
};
export type PlotItem = [number, number];
export type Plot = PlotItem[];
type Plot = PlotItem[];
export interface LineProps {
interface LineProps {
plot: Plot;
minY?: number;
maxY?: number;
@@ -0,0 +1,4 @@
export enum MarginOrientation {
Vertical,
Horizontal,
}
@@ -1,13 +1,9 @@
import { times } from 'lodash';
import { theme } from '../../../stitches.config';
import { MarginInputProps } from './Root';
import { theme } from '../../../../stitches.config';
import { MarginInputProps } from '../Root';
import { MarginOrientation } from './constants';
export enum MarginOrientation {
Vertical,
Horizontal,
}
export interface MarginProps extends MarginInputProps {
interface MarginProps extends MarginInputProps {
verticalSeparations: number;
orientation?: MarginOrientation;
}
@@ -0,0 +1,2 @@
export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320;
@@ -1,10 +1,9 @@
import { times } from 'lodash';
import { ReactNode } from 'react';
import { theme } from '../../../stitches.config';
import { Margin, MarginOrientation } from './Margin';
export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320;
import { theme } from '../../../../stitches.config';
import { Margin } from '../Margin';
import { MarginOrientation } from '../Margin/constants';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './constants';
export interface MarginInputProps {
min: number;
@@ -13,7 +12,7 @@ export interface MarginInputProps {
precision?: number;
}
export interface RootProps {
interface RootProps {
children: ReactNode;
verticalMargin?: MarginInputProps;
horizontalMargin?: MarginInputProps;
@@ -1,6 +1,6 @@
import { theme } from '../../../stitches.config';
export interface GapProps {
interface GapProps {
message: string;
}
@@ -2,7 +2,7 @@ import { ComponentProps } from 'react';
import { RatingsReward } from '../../../commands/ratings';
import { theme } from '../../../stitches.config';
export interface ItemProps extends ComponentProps<'div'> {
interface ItemProps extends ComponentProps<'div'> {
position: number;
deltaPosition?: number;
score?: number;
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface RootProps {
interface RootProps {
children: ReactNode;
}
+1 -1
View File
@@ -1,8 +1,8 @@
import { DiscordLogoIcon } from '@radix-ui/react-icons';
import { Flex, Text, Theme } from '@radix-ui/themes';
import Link from 'next/link';
import { theme } from '../../stitches.config';
import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide';
import { theme } from '../../stitches.config';
export default function Navbar() {
return (
+5
View File
@@ -0,0 +1,5 @@
export enum NoDataType {
BattlesInPeriod,
PlayersInPeriod,
TanksFound,
}
@@ -1,14 +1,8 @@
import { Locale } from 'discord.js';
import { translator } from '../core/localization/translator';
import { theme } from '../stitches.config';
import { translator } from '../../core/localization/translator';
import { theme } from '../../stitches.config';
export enum NoDataType {
BattlesInPeriod,
PlayersInPeriod,
TanksFound,
}
export interface NoDataProps {
interface NoDataProps {
type: 'battles_in_period' | 'players_in_period' | 'tanks_found';
locale: Locale;
}
@@ -10,11 +10,7 @@ import {
slate,
teal,
} from '@radix-ui/colors';
import { Percentile } from '../constants/percentiles';
export interface PercentileIndicatorProps {
percentile: Percentile;
}
import { Percentile } from '../../constants/percentiles';
export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9,
@@ -28,18 +24,3 @@ export const PERCENTILE_COLORS = {
[Percentile.Unicum]: pink.pink9,
[Percentile.SuperUnicum]: purple.purple9,
};
export default function PercentileIndicator({
percentile,
}: PercentileIndicatorProps) {
return (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: PERCENTILE_COLORS[percentile],
}}
/>
);
}
@@ -0,0 +1,21 @@
import { Percentile } from '../../constants/percentiles';
import { PERCENTILE_COLORS } from './constants';
interface PercentileIndicatorProps {
percentile: Percentile;
}
export default function PercentileIndicator({
percentile,
}: PercentileIndicatorProps) {
return (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: PERCENTILE_COLORS[percentile],
}}
/>
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface ColumnProps {
interface ColumnProps {
children: ReactNode;
}
@@ -0,0 +1,31 @@
import { TankClass, TreeType } from '.';
import {
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
} from '../../../../core/blitzkrieg/tankDefinitions/constants';
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const; // BIG TODO: REMOVE THIS AFTER LOCALIZATION
export const TANK_CLASSES = [
'lightTank',
'mediumTank',
'heavyTank',
'AT-SPG',
] as const;
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
researchable: 'Tech tree',
premium: 'Premium',
collector: 'Collector',
};
export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
researchable: 'https://i.imgur.com/pJxO2XY.png',
premium: 'https://i.imgur.com/mZzSwOU.png',
collector: 'https://i.imgur.com/7A0RsG5.png',
};
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankClass, string>> = {
researchable: TANK_ICONS,
premium: TANK_ICONS_PREMIUM,
collector: TANK_ICONS_COLLECTOR,
};
@@ -1,37 +1,10 @@
import {
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
} from '../../../core/blitzkrieg/tankDefinitions';
import { theme } from '../../../stitches.config';
export const TANK_CLASSES = [
'lightTank',
'mediumTank',
'heavyTank',
'AT-SPG',
] as const;
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
import { theme } from '../../../../stitches.config';
import { TANK_CLASSES, TREE_TYPES, TREE_TYPE_ICONS } from './constants';
export type TankClass = (typeof TANK_CLASSES)[number];
export type TreeType = (typeof TREE_TYPES)[number];
// BIG TODO: REMOVE THIS AFTER LOCALIZATION
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
researchable: 'Tech tree',
premium: 'Premium',
collector: 'Collector',
};
export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
researchable: 'https://i.imgur.com/pJxO2XY.png',
premium: 'https://i.imgur.com/mZzSwOU.png',
collector: 'https://i.imgur.com/7A0RsG5.png',
};
export interface ItemProps {
image?: string;
tankType?: TankClass;
name: string;
treeType: TreeType;
@@ -43,13 +16,7 @@ const TREE_TYPE_COLOR = {
collector: '_blue',
} as const;
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankClass, string>> = {
researchable: TANK_ICONS,
premium: TANK_ICONS_PREMIUM,
collector: TANK_ICONS_COLLECTOR,
};
export function Item({ image, tankType, name, treeType }: ItemProps) {
export function Item({ tankType, name, treeType }: ItemProps) {
return (
<div
style={{
+1 -1
View File
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface RootProps {
interface RootProps {
children: ReactNode;
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface RowProps {
interface RowProps {
children: ReactNode;
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { theme } from '../../../stitches.config';
export interface TitleProps {
interface TitleProps {
children: string;
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { theme } from '../stitches.config';
export type TierWeightsRecord = Partial<Record<Tier, number>>;
export interface TierWeightsProps {
interface TierWeightsProps {
weights: TierWeightsRecord;
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { theme } from '../stitches.config';
export interface TitleBarProps {
interface TitleBarProps {
title: string;
image?: string;
description?: string;
+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],
};
-15
View File
@@ -1,15 +0,0 @@
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchBlitz';
export interface EncyclopediaInfo {
achievement_sections: Record<string, { name: string; order: number }>;
tanks_updated_at: number;
languages: Record<string, string>;
vehicle_types: Record<string, string>;
vehicle_nations: Record<string, string>;
game_version: string;
}
export const encyclopediaInfo = fetchBlitz<EncyclopediaInfo>(
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
);
+1 -5
View File
@@ -6,13 +6,9 @@ import { VertexAttribute } from '../streams/scpg';
import {
vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes,
} from './extractModel';
} from './extractModel/constants';
import { readDVPLFile } from './readDVPLFile';
Buffer.prototype.toJSON = function () {
return [...(this as Buffer)].map((int) => int.toString(16)).join('');
};
export async function extractArmor(data: string, fileName: string) {
const sc2Path = `${data}/3d/Tanks/CollisionMeshes/${fileName}.sc2.dvpl`;
const scgPath = `${data}/3d/Tanks/CollisionMeshes/${fileName}.scg.dvpl`;
+19
View File
@@ -0,0 +1,19 @@
import { vertexAttributeVectorSizes } from '../../streams/scg';
import { VertexAttribute } from '../../streams/scpg';
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
{
[VertexAttribute.VERTEX]: 'POSITION',
[VertexAttribute.NORMAL]: 'NORMAL',
[VertexAttribute.TEXCOORD0]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD1]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD2]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD3]: 'TEXCOORD_0',
[VertexAttribute.TANGENT]: 'TANGENT',
[VertexAttribute.JOINTINDEX]: 'JOINT_0',
[VertexAttribute.JOINTWEIGHT]: 'WEIGHT_0',
};
export const vertexAttributeGltfVectorSizes = {
...vertexAttributeVectorSizes,
[VertexAttribute.TANGENT]: 4,
} as const;
@@ -1,51 +1,26 @@
import { Document, Material, Node, Scene } from '@gltf-transform/core';
import { times } from 'lodash';
import { dirname } from 'path';
import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three';
import { readTexture } from '../../blitzkrieg/readTexture';
import { TextureMutation } from '../../blitzkrieg/readTexture/constants';
import { Hierarchy, Sc2ReadStream, Textures } from '../../streams/sc2';
import { ScgReadStream } from '../../streams/scg';
import { VertexAttribute } from '../../streams/scpg';
import { readDVPLFile } from '../readDVPLFile';
import {
Matrix4,
Quaternion,
Vector3,
Vector3Tuple,
Vector4Tuple,
} from 'three';
import { TextureMutation, readTexture } from '../blitzkrieg/readTexture';
import { Hierarchy, Sc2ReadStream, Textures } from '../streams/sc2';
import { ScgReadStream, vertexAttributeVectorSizes } from '../streams/scg';
import { VertexAttribute } from '../streams/scpg';
import { readDVPLFile } from './readDVPLFile';
vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes,
} from './constants';
const ERROR_ON_UNKNOWN_COMPONENT = false;
const MAX_FLOAT32 = 2 ** 127 * (2 - 2 ** -23);
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
{
[VertexAttribute.VERTEX]: 'POSITION',
[VertexAttribute.NORMAL]: 'NORMAL',
[VertexAttribute.TEXCOORD0]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD1]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD2]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD3]: 'TEXCOORD_0',
[VertexAttribute.TANGENT]: 'TANGENT',
[VertexAttribute.JOINTINDEX]: 'JOINT_0',
[VertexAttribute.JOINTWEIGHT]: 'WEIGHT_0',
};
export const vertexAttributeGltfVectorSizes = {
...vertexAttributeVectorSizes,
[VertexAttribute.TANGENT]: 4,
} as const;
const omitMeshNames = {
start: ['chassis_chassis_', 'chassis_track_crash_', 'HP_'],
end: ['_POINT'],
};
export async function extractModel(
data: string,
path: string,
baseColor?: Vector3Tuple,
) {
export async function extractModel(data: string, path: string) {
const sc2Path = `${data}/3d/${path}.sc2.dvpl`;
const scgPath = `${data}/3d/${path}.scg.dvpl`;
const sc2 = new Sc2ReadStream((await readDVPLFile(sc2Path)).buffer).sc2();
@@ -82,13 +57,11 @@ export async function extractModel(
material.setBaseColorTexture(
document
.createTexture(node.materialName)
.setMimeType('image/png')
.setMimeType('image/jpeg')
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${textures.albedo}`,
baseColor
? { mutation: TextureMutation.BaseColor, baseColor }
: undefined,
TextureMutation.Albedo,
),
),
);
@@ -97,11 +70,11 @@ export async function extractModel(
material.setMetallicRoughnessTexture(
document
.createTexture(node.materialName)
.setMimeType('image/png')
.setMimeType('image/jpeg')
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`,
{ mutation: TextureMutation.RoughnessMetallicness },
TextureMutation.RoughnessMetallicness,
),
),
);
@@ -113,13 +86,13 @@ export async function extractModel(
material.setNormalTexture(
document
.createTexture(node.materialName)
.setMimeType('image/png')
.setMimeType('image/jpeg')
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap
}`,
isBase ? { mutation: TextureMutation.Normal } : undefined,
isBase ? TextureMutation.Normal : undefined,
),
),
);
@@ -129,11 +102,11 @@ export async function extractModel(
material.setOcclusionTexture(
document
.createTexture(node.materialName)
.setMimeType('image/png')
.setMimeType('image/jpeg')
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
{ mutation: TextureMutation.Miscellaneous },
TextureMutation.Miscellaneous,
),
),
);
+1 -1
View File
@@ -1,4 +1,4 @@
export type BlitzResponse<Data extends object> =
type BlitzResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
+2 -2
View File
@@ -4,7 +4,7 @@ import fetchBlitz from './fetchBlitz';
import { normalizeExtra } from './normalizeExtra';
import { normalizeIds } from './normalizeIds';
export type IndividualClanAccountInfo = {
type IndividualClanAccountInfo = {
account_id: number;
joined_at: number;
clan_id: number;
@@ -21,7 +21,7 @@ export type IndividualClanAccountInfo = {
};
} | null;
export interface ClanAccountInfo {
interface ClanAccountInfo {
[accountId: number]: IndividualClanAccountInfo;
}
+2 -2
View File
@@ -3,7 +3,7 @@ import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID
import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
export interface IndividualClanInfo {
interface IndividualClanInfo {
recruiting_options: {
vehicles_level: number;
wins_ratio: number;
@@ -32,7 +32,7 @@ export interface IndividualClanInfo {
description: string;
}
export interface ClanInfo {
interface ClanInfo {
[clanId: number]: IndividualClanInfo;
}
+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(
+1 -5
View File
@@ -1,12 +1,8 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { Clan, ClanList } from '../../types/clanList';
import { ClanList } from '../../types/clanList';
import fetchBlitz from './fetchBlitz';
export type ClanListWithRegion = (Clan & {
region: 'com' | 'eu' | 'asia';
})[];
export default async function searchClansAcrossRegions(
search: string,
limit = 25,
@@ -0,0 +1,2 @@
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
@@ -1,6 +1,7 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchBlitz';
import { Region } from '../../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../fetchBlitz';
import { usernamePattern } from './constants';
export interface AccountListItem {
nickname: string;
@@ -11,9 +12,6 @@ export type AccountListWithServer = (AccountListItem & {
region: Region;
})[];
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
export default async function searchPlayersAcrossRegions(
search: string,
limit = 25,
+50
View File
@@ -0,0 +1,50 @@
import { crc32 } from 'crc';
import { encodeBlockHC } from 'lz4';
/**
* Thanks Maddoxkkm! Modernized heavily.
*
* https://github.com/Maddoxkkm/dvpl_converter/
*/
export function writeDVPL(buffer: Buffer) {
let output = Buffer.alloc(buffer.length);
const compressedBlockSize = encodeBlockHC(buffer, output);
if (compressedBlockSize === 0 || compressedBlockSize >= buffer.length) {
const footerBuffer = toDVPLFooter(
buffer.length,
buffer.length,
crc32(buffer),
0,
);
return Buffer.concat([buffer, footerBuffer], buffer.length + 20);
} else {
output = output.subarray(0, compressedBlockSize);
const footerBuffer = toDVPLFooter(
buffer.length,
compressedBlockSize,
crc32(output),
2,
);
return Buffer.concat([output, footerBuffer], compressedBlockSize + 20);
}
}
function toDVPLFooter(
inputSize: number,
compressedSize: number,
crc32: number,
type: number,
) {
const result = Buffer.alloc(20);
result.writeUInt32LE(inputSize, 0);
result.writeUInt32LE(compressedSize, 4);
result.writeUInt32LE(crc32, 8);
result.writeUInt32LE(type, 12);
result.write('DVPL', 16, 4, 'utf8');
return result;
}
+2 -5
View File
@@ -1,9 +1,6 @@
import { degToRad } from 'three/src/math/MathUtils';
import {
DuelMember,
genericDefaultEquipmentMatrix,
mutateDuel,
} from '../../stores/duel';
import { DuelMember, mutateDuel } from '../../stores/duel';
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
import { availableProvisions } from './availableProvisions';
import { modelDefinitions } from './modelDefinitions';
import { provisionDefinitions } from './provisionDefinitions';
+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;
}
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff. Show more