Compare commits

...
60 Commits
Author SHA1 Message Date
TrèsAbhi eed9d23646 fix: use root node modules 2025-12-01 16:58:18 -06:00
TrèsAbhi 2101dba7c6 New Crowdin updates (#184)
* New translations en.json (Russian)

* New translations en.json (Russian)

* New translations en.json (Russian)
2025-11-28 18:12:19 -06:00
TrèsAbhi 1b398a5f66 fix: initialize Tankopedia with model definitions for improved data handling 2025-11-28 12:55:55 -06:00
TrèsAbhi d8f9a2633d New Crowdin updates (#183)
* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)
2025-11-27 21:53:42 -06:00
TrèsAbhi 2e79ccf7eb feat: update ts-proto executable handling to support multiple locations 2025-11-27 17:59:34 -06:00
TrèsAbhi be778acf39 feat: logs 2025-11-27 17:56:28 -06:00
TrèsAbhi 8d10d7cffa feat: integrate CompareCell component into TankRow and Tanks for improved performance metrics display 2025-11-27 17:48:58 -06:00
TrèsAbhi 5b9041a972 feat: performance tank table occupy more real estate 2025-11-27 17:34:29 -06:00
TrèsAbhi 1e60b1f61f fix: remove unused PerformanceInfo component from performance page 2025-11-27 17:33:44 -06:00
TrèsAbhi 13a636c023 fix: update playerCountPeriod to use "total" value 2025-11-27 17:33:05 -06:00
TrèsAbhi 8bc0693f3b fix: compare table occupy minimum width 2025-11-27 17:31:28 -06:00
TrèsAbhi 9b842c501c feat: update guessing game text for clarity 2025-11-27 17:26:10 -06:00
TrèsAbhi 8aadda4fb8 feat: add ToolOpenGraph component to guess page 2025-11-27 17:25:54 -06:00
TrèsAbhi 58c70f98e1 refactor: remove 'naked' prop from Controls and TankSandbox components 2025-11-27 17:24:12 -06:00
TrèsAbhi 54b73d11ce Merge branch 'main' into dev 2025-11-27 17:16:12 -06:00
TrèsAbhi d1ac5644a4 feat: logs 2025-11-27 14:14:18 -06:00
TrèsAbhi ae35c1516c Merge branch 'main' into fix-lighting 2025-11-27 14:12:38 -06:00
TrèsAbhi fd364d434f feat: hemispehre 2025-11-27 14:10:57 -06:00
TrèsAbhi fd37f72f8c feat: some simplification 2025-11-27 13:54:36 -06:00
TrèsAbhi 1427f61c52 feat: complete transition 2025-11-27 13:42:37 -06:00
TrèsAbhi cd4fa366a0 feat: requestedDisplay 2025-11-27 13:20:06 -06:00
TrèsAbhi 344e7caa81 feat: overhead lighting 2025-11-27 13:05:08 -06:00
TrèsAbhi fe7b2aea02 Add files via upload 2025-11-27 15:57:34 +00:00
TrèsAbhi fced1e96ef feat: remove transition skeleton 2025-11-26 21:10:54 -06:00
TrèsAbhi 778efb2372 feat: transiton and reveal 2025-11-26 21:10:03 -06:00
TrèsAbhi b67fd5b58c feat: new lighting 2025-11-26 20:48:25 -06:00
TrèsAbhi 2559782a28 fix: ground phasing through 2025-11-26 20:25:45 -06:00
TrèsAbhi efb479fece fix: vfs in core 2025-11-26 19:27:58 -06:00
TrèsAbhi fb829d4b81 Merge branch 'main' into dev 2025-11-26 19:22:48 -06:00
TrèsAbhi 91f4f2edcf Comment out unchanged change log statement
Comment out the log statement for unchanged changes.
2025-11-26 18:23:25 -06:00
TrèsAbhi a692d1ee0b Merge branch 'main' into dev 2025-11-26 13:58:44 -06:00
TrèsAbhi 31d2ab4746 feat: workflow 2025-11-26 13:44:45 -06:00
TrèsAbhi e4e2c30748 feat: logs 2025-11-26 13:42:43 -06:00
TrèsAbhi d633874d90 fix: path normalization 2025-11-26 13:38:40 -06:00
TrèsAbhi 9cb1d0cb3f fix: read dvpl files 2025-11-26 13:25:46 -06:00
TrèsAbhi a0bccc7973 feat: update all 2025-11-26 13:18:44 -06:00
TrèsAbhi 3efe807e35 feat: migrate definitiosn 2025-11-26 13:07:53 -06:00
TrèsAbhi 1cc15f64b0 feat: augment 2025-11-26 12:58:19 -06:00
TrèsAbhi f70864a0db feat: vfs file 2025-11-26 12:46:03 -06:00
TrèsAbhi 5de40b220f feat: rename 2025-11-26 12:34:26 -06:00
TrèsAbhi bd19c9aac9 Merge branch 'main' into fix-mangler 2025-11-26 12:28:32 -06:00
TrèsAbhi 6bc5b7dcb6 feat: .well-known dir 2025-11-26 12:22:15 -06:00
TrèsAbhi 9df09c9a53 rm nm 2025-11-24 13:01:07 -06:00
TrèsAbhi a9f1d9ebec disable patches 2025-11-24 12:58:36 -06:00
TrèsAbhi 1b834eb848 just remove it 2025-11-24 12:46:32 -06:00
TrèsAbhi c5518ec9bc hide others 2025-11-24 12:45:39 -06:00
TrèsAbhi 8c705d7e2e root reserve 2025-11-24 12:45:05 -06:00
TrèsAbhi efe9f91a2d hide lvm 2025-11-24 12:44:33 -06:00
TrèsAbhi e84bf6b31c str 2025-11-24 12:44:01 -06:00
TrèsAbhi 46234b6cbb disable all 2025-11-24 12:43:02 -06:00
TrèsAbhi 0bda743a91 feat: reintroduce 2025-11-24 12:38:50 -06:00
TrèsAbhi b19866a1f5 feat: remove maximize-build-space 2025-11-24 12:35:45 -06:00
TrèsAbhi 6c8fbf7245 fix: use package node modules 2025-11-13 12:53:19 -06:00
TrèsAbhi 2ea22512d4 feat: new protos 2025-11-13 12:47:51 -06:00
TrèsAbhi 697d1fa1b4 Merge branch 'main' into dev 2025-11-13 12:40:59 -06:00
TrèsAbhi 2c03b530d2 feat: group array 2025-11-06 09:37:14 -06:00
TrèsAbhi 182471c4d6 feat: buildCatalogItemAccessor 2025-11-02 18:10:11 -06:00
TrèsAbhi aecc2adb07 feat: generalize colored delta cells 2025-10-26 22:01:42 -05:00
TrèsAbhi f8ee2b64a6 feat: simplify ownership and test filters 2025-10-26 21:49:48 -05:00
TrèsAbhi 7c4e28bd09 fix: show remove played even when logged out 2025-10-26 21:43:23 -05:00
93 changed files with 1160 additions and 1252 deletions

No files matched your search

+2 -121
View File
@@ -55,128 +55,15 @@ env:
PUBLIC_ASSET_FAST_REPO: assets
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
jobs:
download_client:
name: Download client
runs-on: ubuntu-latest
steps:
- uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 512
swap-size-mb: 1024
remove-dotnet: "true"
remove-haskell: "true"
remove-android: "true"
remove-codeql: "true"
remove-docker-images: "true"
- if: github.event.inputs.archive_url != '' && endsWith(github.event.inputs.archive_url, '.wgpkg')
run: |
sudo apt update
sudo apt install -y p7zip-full
- uses: actions/checkout@v4
if: github.event.inputs.archive_url == ''
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: arduino/setup-protoc@v3
if: github.event.inputs.archive_url == ''
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v1
if: github.event.inputs.archive_url == ''
- run: bun run setup
if: github.event.inputs.archive_url == ''
- name: (Steam) Setup DepotDownloader
if: github.event.inputs.archive_url == ''
run: bun run setup:depotdownloader
- name: (Steam) Download client
if: github.event.inputs.archive_url == ''
run: |
chmod +x ./temp/depotDownloader/DepotDownloader
./temp/depotDownloader/DepotDownloader -q -os windows -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
mkdir -p temp/game
mv depots/444202/*/* temp/game
rm -rf depots
- name: (Steam) Apply patches
if: github.event.inputs.archive_url == ''
run: bun setup:patches --manual
- name: (ZIP) Download client
if: github.event.inputs.archive_url != '' && endsWith(github.event.inputs.archive_url, '.zip')
env:
ARCHIVE_URL: ${{ github.event.inputs.archive_url }}
run: |
mkdir -p temp/game
wget -q -O temp/archive.zip $ARCHIVE_URL
unzip -q temp/archive.zip -d temp/archive
rm temp/archive.zip
mv temp/archive/app/* temp/game
- name: (WGPKG) Download client
if: github.event.inputs.archive_url != '' && endsWith(github.event.inputs.archive_url, '.wgpkg')
env:
ARCHIVE_URL: ${{ github.event.inputs.archive_url }}
run: |
mkdir -p temp/archives
mkdir -p temp/game
i=1
urls=()
while :; do
part=$(printf "%03d" $i)
url="${ARCHIVE_URL}.${part}"
if wget --spider -q "$url"; then
urls+=("$url")
((i++))
else
break
fi
done
for url in "${urls[@]}"; do
dest="temp/archives/$(basename "$url")"
wget -q -O "$dest" "$url" &
done
wait
first=$(ls temp/archives/*.001 | head -n 1)
7z x "$first" -otemp/game
rm -rf temp/archives
- name: Archive game
uses: actions/upload-artifact@v4
with:
name: game
path: temp/game
build_assets:
name: Build assets
runs-on: ubuntu-latest
needs: download_client
steps:
- uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 512
swap-size-mb: 1024
remove-dotnet: "true"
remove-haskell: "true"
remove-android: "true"
remove-codeql: "true"
remove-docker-images: "true"
- uses: actions/checkout@v4
with:
submodules: recursive
@@ -187,12 +74,6 @@ jobs:
- uses: oven-sh/setup-bun@v1
- run: bun run setup
- name: Download game
uses: actions/download-artifact@v4
with:
name: game
path: temp/game
- name: AppArmor hack
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
+4 -7
View File
@@ -69,16 +69,15 @@
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
"fast-xml-parser": "^5.3.2",
"lodash-es": "^4.17.21",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"steam-user": "^5.2.3",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1",
@@ -106,6 +105,7 @@
"@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1",
"@types/progress": "^2.0.7",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0",
"crc": "^4.3.2",
"csv-parse": "^6.1.0",
@@ -119,6 +119,7 @@
"progress": "^2.0.3",
"puppeteer": "^24.22.3",
"sharp": "^0.34.4",
"steam-user": "^5.2.3",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1",
@@ -1438,7 +1439,7 @@
"fast-uri": ["[email protected]", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
"fast-xml-parser": ["fast-xml-parser@5.3.2", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-n8v8b6p4Z1sMgqRmqLJm3awW4NX7NkaKPfb3uJIBTSH7Pdvufi3PQ3/lJLQrvxcMYl7JI2jnDO90siPEpD8JBA=="],
"fastq": ["[email protected]", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
@@ -2612,8 +2613,6 @@
"@blitzkit/bot/prisma": ["[email protected]", "", { "dependencies": { "@prisma/config": "6.17.1", "@prisma/engines": "6.17.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g=="],
"@blitzkit/scripts/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
"@blitzkit/website/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
"@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
@@ -2782,8 +2781,6 @@
"@blitzkit/bot/prisma/@prisma/engines": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "6.17.1", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "@prisma/fetch-engine": "6.17.1", "@prisma/get-platform": "6.17.1" } }, "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw=="],
"@blitzkit/scripts/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.29.4
## Technical Changes
- BlitzKit now partially downloads game files instead of the entire game, allowing for faster updates
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.29.5
## Features
- Added new lighting to be a bit softer on tanks
- Added a button with the sun icon to toggle high graphics
- Added a new reveal and transition animation between tabs
## Fixes
- Track wheels on some tanks no longer look translucent
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.29.6
## Features
- Simplified the ownership and test tank filters in the tank search
- Renamed "Guess the tank" to simply "Guess"
- The player count value in the Performance tool is now set to Total by default
- Added Compare-like red/green highlighting to the Performance tool
## Changes
- Removed the last updated info bar in Performance for some more screen real estate
## Fixes
- The remove played tanks in the Playlist tool now shows even when logged out
- The Guess game now has a page title
- The compare table now occupies the minimum width possible instead of expanding all columns excessively
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkit",
"version": "1.29.3",
"version": "1.29.6",
"license": "BlitzKit License",
"private": true,
"scripts": {
@@ -10,6 +10,7 @@
"build:website": "cd packages/website && bun run build",
"build:prisma": "prisma generate",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"build:catalogitemaccesssor": "cd packages/scripts && bun run build:catalogitemaccesssor",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"build:app": "cd packages/app && bun run build",
"dev:bot": "bun run build:bot --dev && bun start:bot",
+1 -2
View File
@@ -13,16 +13,15 @@
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
"fast-xml-parser": "^5.3.2",
"lodash-es": "^4.17.21",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"steam-user": "^5.2.3",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
+33 -32
View File
@@ -1,32 +1,33 @@
export * from './canSplash';
export * from './clans';
export * from './classes';
export * from './constants';
export * from './equipmentPrice';
export * from './fetchBlitz';
export * from './fetchGlossary';
export * from './generateStats';
export * from './getAccountAchievements';
export * from './getAccountInfo';
export * from './getClanAccountInfo';
export * from './getClanInfo';
export * from './getLeagueFromScore';
export * from './getRatingInfo';
export * from './getRatingLeague';
export * from './getRatingNeighbors';
export * from './getTankStats';
export * from './idToRegion';
export * from './isOnGoingRatingSeason';
export * from './isValidBlitzId';
export * from './nations';
export * from './normalizeExtra';
export * from './normalizeIds';
export * from './permanentSkills';
export * from './rating';
export * from './regions';
export * from './regionSubdomainToRegion';
export * from './regionToRegionSubdomain';
export * from './searchClansAcrossRegions';
export * from './searchCurrentRatingPlayers';
export * from './searchPlayersAcrossRegions';
export * from './toUniqueId';
export * from "./canSplash";
export * from "./clans";
export * from "./classes";
export * from "./constants";
export * from "./equipmentPrice";
export * from "./fetchBlitz";
export * from "./fetchGlossary";
export * from "./generateStats";
export * from "./getAccountAchievements";
export * from "./getAccountInfo";
export * from "./getClanAccountInfo";
export * from "./getClanInfo";
export * from "./getLeagueFromScore";
export * from "./getRatingInfo";
export * from "./getRatingLeague";
export * from "./getRatingNeighbors";
export * from "./getTankStats";
export * from "./idToRegion";
export * from "./isOnGoingRatingSeason";
export * from "./isValidBlitzId";
export * from "./nations";
export * from "./normalizeExtra";
export * from "./normalizeIds";
export * from "./permanentSkills";
export * from "./rating";
export * from "./readDVPL";
export * from "./regions";
export * from "./regionSubdomainToRegion";
export * from "./regionToRegionSubdomain";
export * from "./searchClansAcrossRegions";
export * from "./searchCurrentRatingPlayers";
export * from "./searchPlayersAcrossRegions";
export * from "./toUniqueId";
@@ -1,5 +1,5 @@
import { crc32 } from 'crc';
import { decompressBlock } from 'lz4js';
import { crc32 } from "crc";
import { decompressBlock } from "lz4js";
/**
* Thanks Maddoxkkm! Modified heavily to be modern.
@@ -9,10 +9,10 @@ export function readDVPL(buffer: Buffer) {
const footerBuffer = buffer.subarray(buffer.length - 20, buffer.length);
if (
footerBuffer.toString('utf8', 16, 20) !== 'DVPL' ||
footerBuffer.toString("utf8", 16, 20) !== "DVPL" ||
footerBuffer.length !== 20
) {
throw new SyntaxError('Invalid DVPL footer');
throw new SyntaxError("Invalid DVPL footer");
}
const footerData = {
@@ -24,15 +24,15 @@ export function readDVPL(buffer: Buffer) {
const targetBlock = buffer.subarray(0, buffer.length - 20);
if (targetBlock.length !== footerData.cSize) {
throw new RangeError('DVPL size mismatch');
throw new RangeError("DVPL size mismatch");
}
if (crc32(targetBlock) !== footerData.crc32) {
throw new TypeError('DVPL CRC32 mismatch');
throw new TypeError("DVPL CRC32 mismatch");
}
if (footerData.type === 0) {
if (!(footerData.oSize === footerData.cSize && footerData.type === 0)) {
throw new RangeError('DVPL type and size mismatch');
throw new RangeError("DVPL type and size mismatch");
}
return targetBlock;
@@ -44,17 +44,17 @@ export function readDVPL(buffer: Buffer) {
destination,
0,
source.length,
0,
0
);
if (decompressedSize !== footerData.oSize) {
throw new RangeError(
`Decompressed DVPL size mismatch (${decompressedSize} vs ${footerData.oSize})`,
`Decompressed DVPL size mismatch (${decompressedSize} vs ${footerData.oSize})`
);
}
return Buffer.from(destination);
} else {
throw new SyntaxError('Unknown DVPL format');
throw new SyntaxError("Unknown DVPL format");
}
}
-1
View File
@@ -6,6 +6,5 @@ export * from "./patreon";
export * from "./protobuf";
export * from "./protos";
export * from "./statistics";
export * from "./steam";
export * from "./streams";
export * from "./types";
-1
View File
@@ -1 +0,0 @@
export * from "./steam";
-78
View File
@@ -1,78 +0,0 @@
import { writeFile } from "fs/promises";
import SteamUser, { EConnectionProtocol } from "steam-user";
interface SteamManifestFile {
chunks: {
sha: string;
crc: number;
offset: string;
cb_original: number;
cb_compressed: number;
}[];
filename: string;
size: string;
flags: number;
sha_filename: string;
sha_content: string;
linktarget: unknown;
}
interface SteamManifest {
files: SteamManifestFile[];
depot_id: number;
gid_manifest: string;
creation_time: number;
filenames_encrypted: boolean;
cb_disk_original: string;
cb_disk_compressed: string;
unique_chunks: number;
crc_encrypted: number;
crc_clear: number;
}
export class SteamVFS {
private steam = new SteamUser({ protocol: EConnectionProtocol.TCP });
private manifest: Map<string, SteamManifestFile> = new Map();
constructor(
private username: string,
private password: string,
private app: number,
private depot: number
) {}
async init() {
this.steam.logOn({
accountName: this.username,
password: this.password,
});
await new Promise((resolve) => this.steam.once("loggedOn", resolve));
const productInfo = await this.steam.getProductInfo([this.app], []);
const manifest: SteamManifest = await new Promise((resolve) => {
this.steam.getManifest(
this.app,
this.depot,
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
.gid,
"public",
(_, response) => {
resolve(response);
}
);
});
const paths: string[] = [];
for (const file of manifest.files) {
const path = file.filename.replaceAll("\\", "/");
paths.push(path);
this.manifest.set(path, file);
}
writeFile("test.txt", paths.sort().join("\n"));
console.log(JSON.stringify(manifest.files[0], null, 2));
}
}
+2 -2
View File
@@ -906,8 +906,8 @@
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
"name": "Guess",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
"name": "Guess",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
"name": "Guess",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
"name": "Guess",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
"name": "Guess",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
"name": "Guess",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "Случайно"
},
"guess": {
"name": "Угадай танк",
"button": "Угадать",
"name": "Угадать",
"button": "Что это за танк?",
"description": "Мини-игра «Силуэт»",
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
"search": {
+42 -42
View File
@@ -1,19 +1,19 @@
{
"test": {
"default": "тест",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "{fruit}",
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}m",
"mm": "{value}mm",
"m": "{value}м",
"mm": "{value}мм",
"deg": "{value}°",
"hp": "{value}hp",
"hp": "{value}од",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"deg_s": "{value}град/c"
},
"gun_types": {
"auto_loader": "Магазинна система заряджання",
@@ -80,7 +80,7 @@
"nations": {
"ussr": "СРСР",
"germany": "Німеччина",
"usa": "США",
"usa": "ЗША",
"china": "Китай",
"uk": "Велика Британія",
"france": "Франція",
@@ -186,11 +186,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти \"{search}\". Спробуйте обрати клан з результату пошуку.",
"player_not_found": "# Не вдалося знайти користувача\nНе вдалося знайти користувача \"{search}\". Спробуйте вибрати користувача із результатів пошуку, ввівши правильне ім'я користувача, або скористайтеся командою `/link`.",
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# Танк не знайдено\nНе вдалося знайти танк із назвою «{search}».",
"tank_id_not_found": "# Танк не знайдено\nНе вдалося знайти танк з ID «{search}». Спробуйте ввести назву танка замість номера.",
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
"get_help": "Допомогти Discord серверу",
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
@@ -284,19 +284,19 @@
"name": "рівень",
"description": "Рівень",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Рівень {tier_1} - {tier_2}"
}
}
},
"periods": {
"career": "Кар'єра",
"today": "Сьогодні",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "{days} днів",
"custom": "{day_1} - {day_2} днів"
},
"filters": {
"none": "Немає фільтрів",
"tier": "Tier {tier}"
"tier": "Рівень {tier}"
},
"no_data": {
"battles_in_period": "За цей період боїв не було",
@@ -339,10 +339,10 @@
"body": {
"subtitle": "Дослідження",
"total": "Загалом",
"research": "Research: {cost}",
"research": "Дослідження: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"purchase": "Вартість: {cost}",
"equipment": "Спорядження: {cost}",
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
},
"errors": {
@@ -435,7 +435,7 @@
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"listing": "{days} днів"
}
},
"full_stats": {
@@ -549,7 +549,7 @@
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted {id}",
"deleted_player": "",
"neighbors": "Rating neighbors",
"more": "+ {count} more"
}
@@ -643,9 +643,9 @@
"body": "This page has moved. Redirecting..."
},
"settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode",
"title": "Налаштування",
"description": "Змінити налаштування BlitzKit ",
"dev_mode": "Режим розробника",
"language": "Мова"
},
"common": {
@@ -658,16 +658,16 @@
"copied": "Скопійовано!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"select_dialog_title": "Знайти танк",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"clear_filters": "Очистити",
"login": "Увійдіть, щоб скористатися",
"search_bar_hint": "Шукайте техніку...",
"recent": "Недавно переглянуті",
"select_all": "Вибрати все",
"count_singular": "1 танк",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "Танків: {count} ",
"sorting_by": "Відсортувати за {name}",
"no_tanks_found": {
"body": "Нічого не знайдено.",
"search_again": "Спробуйте шукати заново.",
@@ -731,7 +731,7 @@
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"new": "",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
@@ -763,20 +763,20 @@
},
"tools": {
"playlist": {
"reset": "Reset",
"shuffle": "Shuffle",
"remove": "Remove played",
"reset": "Скинути",
"shuffle": "Перемішати",
"remove": "Видалити зіграні",
"tracking": "BlitzKit is tracking your battles",
"sort": {
"button": "Сортувати за",
"last_played": "Last played",
"least_played": "Least played"
"last_played": "Остання гра",
"least_played": "Найменше зіграно"
},
"table": {
"index": "#",
"never": "Never",
"never": "Ніколи",
"tank": "Танк",
"last_played": "Last played",
"last_played": "Остання гра",
"battles": "Career battles",
"unrecorded": "--",
"wins": "Перемоги"
@@ -784,14 +784,14 @@
"large_warning": {
"title": "That's a lot of tanks!",
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
"cancel": "Cancel",
"proceed": "Proceed"
"cancel": "Скасувати",
"proceed": "Продовжити"
},
"generate": "Generate playlist",
"name": "Playlist",
"generate": "Згенерувати плейлист",
"name": "Плейлист",
"description": "Tank playlist generator",
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
"button": "Generate",
"button": "Створити",
"pool_size": "{count} possible tanks"
},
"mixer": {
@@ -803,8 +803,8 @@
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
"name": "Guess",
"button": "What could it be?",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
+2 -2
View File
@@ -803,8 +803,8 @@
"randomize": "随机搭配"
},
"guess": {
"name": "猜坦克",
"button": "猜猜看",
"name": "猜测",
"button": "What could it be?",
"description": "坦克剪影小游戏",
"seo_description": "看剪影猜坦克",
"search": {
+3
View File
@@ -14,6 +14,7 @@
"@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1",
"@types/progress": "^2.0.7",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0",
"crc": "^4.3.2",
"csv-parse": "^6.1.0",
@@ -27,6 +28,7 @@
"progress": "^2.0.3",
"puppeteer": "^24.22.3",
"sharp": "^0.34.4",
"steam-user": "^5.2.3",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
@@ -42,6 +44,7 @@
"build:bot": "bun src/buildBot.ts",
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
"build:proxyclient": "bun src/buildProxyClient.ts",
"build:catalogitemaccesssor": "bun src/buildCatalogItemAccessor.ts",
"prunestrings": "bun src/pruneStrings.ts",
"build:protos": "bun src/protos.ts",
"patchnotes": "bun src/patchNotes.ts"
+7 -9
View File
@@ -4,12 +4,10 @@ import { mkdir, writeFile } from "fs/promises";
import { parse as parsePath } from "path";
import ProgressBar from "progress";
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
import { readStringDVPL } from "../src/core/blitz/readStringDVPL";
import { DATA } from "./buildAssets/constants";
import { readYAMLDVPL } from "./core/blitz/readYAMLDVPL";
import { vfs } from "./buildAssets/constants";
import { writeDVPL } from "./core/blitz/writeDVPL";
const versionTextFile = await readStringDVPL(`${DATA}/version.txt`);
const versionTextFile = await vfs.text(`Data/version.txt`);
const currentVersion = versionTextFile
.split(" ")[0]
.split(".")
@@ -51,7 +49,7 @@ while (true) {
const { dir } = parsePath(path);
try {
await mkdir(`${DATA}/${dir}`, { recursive: true });
await mkdir(`Data/${dir}`, { recursive: true });
} catch (error) {
console.warn(`Failed to make directory "${dir}"`);
}
@@ -60,7 +58,7 @@ while (true) {
const buffer = Buffer.from(data);
await writeFile(
`${DATA}/${path}${isDvpl ? "" : ".dvpl"}`,
`Data/${path}${isDvpl ? "" : ".dvpl"}`,
new Uint8Array(isDvpl ? buffer : writeDVPL(buffer))
);
@@ -76,14 +74,14 @@ while (true) {
}/en.yaml`
);
const newStrings = parseYaml(await localizationsResponse.text());
const oldStrings = await readYAMLDVPL<Record<string, string>>(
`${DATA}/Strings/en.yaml`
const oldStrings = await vfs.yaml<Record<string, string>>(
`Data/Strings/en.yaml`
);
const patchedStrings = { ...oldStrings, ...newStrings };
const patchedContent = stringifyYaml(patchedStrings);
await writeFile(
`${DATA}/Strings/en.yaml`,
`Data/Strings/en.yaml`,
new Uint8Array(writeDVPL(Buffer.from(patchedContent)))
);
}
@@ -1,19 +1,16 @@
import { readdir } from "fs/promises";
import sharp from "sharp";
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { readStringDVPL } from "../../src/core/blitz/readStringDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
export async function boosterIcons() {
console.log("Building booster icons...");
using uploader = new AssetUploader("booster icons");
const boosterFiles = (await readdir(`${DATA}/Gfx/Shared/boosters`)).filter(
const boosterFiles = (vfs.dir(`Data/Gfx/Shared/boosters`)).filter(
(file) => !file.includes("@2x.txt") && !file.startsWith("texture0")
);
const image = sharp(
await readDVPLFile(`${DATA}/Gfx/Shared/boosters/texture0.packed.webp`)
await vfs.file(`Data/Gfx/Shared/boosters/texture0.packed.webp`)
);
for (const file of boosterFiles) {
@@ -21,7 +18,7 @@ export async function boosterIcons() {
if (name === undefined) continue;
const sizes = (await readStringDVPL(`${DATA}/Gfx/Shared/boosters/${file}`))
const sizes = (await vfs.text(`Data/Gfx/Shared/boosters/${file}`))
.split("\n")[4]
.split(" ")
.map(Number);
@@ -1,13 +1,12 @@
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
export async function camouflageIcons() {
console.log("Building camouflage icons...");
using uploader = new AssetUploader("camo icons");
const content = await readDVPLFile(
`${DATA}/Gfx/UI/Hangar/IconCamouflage.packed.webp`
const content = await vfs.file(
`Data/Gfx/UI/Hangar/IconCamouflage.packed.webp`
);
await uploader.add({ content, path: "icons/camo.webp" });
+10 -30
View File
@@ -1,32 +1,12 @@
import { readdir } from "fs/promises";
import { argv } from "process";
import { assertSecret } from "@blitzkit/core";
import { SteamVFS } from "../core/steam/vfs";
const WINDOWS_APPS = "C:/Program Files/WindowsApps";
const wotbPackageRegex = /7458BE2C\..+_\d+(\.\d+)+_x64__.+/;
const APP = 444200;
const DEPOT = 444202;
const isManual = argv.includes("--manual");
let data: string;
let compressed: boolean;
if (isManual) {
data = "../../temp/game/Data";
compressed = true;
} else {
const directories = await readdir(WINDOWS_APPS).then((files) =>
files.filter((file) => wotbPackageRegex.test(file))
);
if (directories.length !== 1) {
throw new Error(
`Expected 1 directory for WoTB Windows App, got ${directories.length}.`
);
}
const [windowsApp] = directories;
data = `${WINDOWS_APPS}/${windowsApp}/Data`;
compressed = false;
}
export const DATA = data;
export const DATA_COMPRESSED = compressed;
export const vfs = await new SteamVFS(
assertSecret(import.meta.env.STEAM_USERNAME),
assertSecret(import.meta.env.STEAM_PASSWORD),
APP,
DEPOT
).init();
@@ -1,11 +1,6 @@
import { existsSync } from "fs";
import sharp from "sharp";
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { readStringDVPL } from "../../src/core/blitz/readStringDVPL";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import { ConsumablesCommon, ProvisionsCommon } from "./definitions";
interface Mappings {
@@ -33,20 +28,20 @@ export async function consumableProvisionIcons() {
styles.map(async (path) =>
Object.values(
(
await readYAMLDVPL<Mappings>(`${DATA}/${path}`)
await vfs.yaml<Mappings>(`Data/${path}`)
).StyleSheets
)
)
).then((array) => array.flat());
for (const match of (
await readStringDVPL(
`${DATA}/XML/item_defs/vehicles/common/consumables/list.xml`
await vfs.text(
`Data/XML/item_defs/vehicles/common/consumables/list.xml`
)
).matchAll(listItemsPattern)) {
const consumablesCommon = await readXMLDVPL<{
const consumablesCommon = await vfs.xml<{
root: ConsumablesCommon;
}>(`${DATA}/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`);
}>(`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`);
for (const consumable of Object.values(consumablesCommon.root)) {
const styleSheet = styleSheets.find((styleSheet) =>
@@ -67,9 +62,9 @@ export async function consumableProvisionIcons() {
.replace(".psd", "")
.replace(".txt", "");
if (existsSync(`${DATA}/${configPath}.packed.webp`)) {
if (vfs.has(`Data/${configPath}.packed.webp`)) {
const image = sharp(
await readDVPLFile(`${DATA}/${configPath}.packed.webp`)
await vfs.file(`Data/${configPath}.packed.webp`)
);
const content = await image.trim({ threshold: 100 }).toBuffer();
@@ -79,14 +74,14 @@ export async function consumableProvisionIcons() {
});
} else {
const consumablesTexture = sharp(
await readDVPLFile(
`${`${DATA}/${configPath}`
await vfs.file(
`${`Data/${configPath}`
.split("/")
.slice(0, -1)
.join("/")}/texture0.packed.webp`
)
);
const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt`))
const sizes = (await vfs.text(`Data/${configPath}.txt`))
.split("\n")[4]
.split(" ")
.map(Number);
@@ -109,13 +104,13 @@ export async function consumableProvisionIcons() {
}
for (const match of (
await readStringDVPL(
`${DATA}/XML/item_defs/vehicles/common/provisions/list.xml`
await vfs.text(
`Data/XML/item_defs/vehicles/common/provisions/list.xml`
)
).matchAll(listItemsPattern)) {
const provisionsCommon = await readXMLDVPL<{
const provisionsCommon = await vfs.xml<{
root: ProvisionsCommon;
}>(`${DATA}/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`);
}>(`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`);
for (const provision of Object.values(provisionsCommon.root)) {
const styleSheet = styleSheets.find((styleSheet) =>
@@ -136,9 +131,9 @@ export async function consumableProvisionIcons() {
.replace(".psd", "")
.replace(".txt", "");
if (existsSync(`${DATA}/${configPath}.packed.webp`)) {
if (vfs.has(`Data/${configPath}.packed.webp`)) {
const image = sharp(
await readDVPLFile(`${DATA}/${configPath}.packed.webp`)
await vfs.file(`Data/${configPath}.packed.webp`)
);
const content = await image.trim({ threshold: 100 }).toBuffer();
@@ -148,14 +143,14 @@ export async function consumableProvisionIcons() {
});
} else {
const provisionsTexture = sharp(
await readDVPLFile(
`${`${DATA}/${configPath}`
await vfs.file(
`${`Data/${configPath}`
.split("/")
.slice(0, -1)
.join("/")}/texture0.packed.webp`
)
);
const sizes = (await readStringDVPL(`${DATA}/${configPath}.txt`))
const sizes = (await vfs.text(`Data/${configPath}.txt`))
.split("\n")[4]
.split(" ")
.map(Number);
@@ -1,8 +1,7 @@
import { assertSecret, BlitzGlossary } from "@blitzkit/core";
import sharp from "sharp";
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
const ICONS = [
["currency_silver_m.packed.webp", "silver"],
@@ -21,7 +20,7 @@ export async function currencies() {
for (const [file, name] of ICONS) {
const content = await sharp(
await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}`)
await vfs.file(`Data/Gfx/Lobby/currency/${file}`)
)
.trim()
.toBuffer();
+49 -55
View File
@@ -32,16 +32,12 @@ import {
} from "@blitzkit/core";
import { SUPPORTED_LOCALE_BLITZ_MAP } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import { readdir } from "fs/promises";
import { deburr } from "lodash-es";
import { parse as parsePath } from "path";
import type { Vector3Tuple } from "three";
import { parse as parseYaml } from "yaml";
import { readStringDVPL } from "../core/blitz/readStringDVPL";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import type { Avatar } from "./skillIcons";
import type { TankParameters } from "./tankIcons";
@@ -540,9 +536,9 @@ export async function definitions() {
using uploader = new AssetUploader("definitions");
const gameDefinitions: GameDefinitions = {
version: (await readStringDVPL(`${DATA}/version.txt`)).split(" ")[0],
version: (await vfs.text('Data/version.txt')).split(" ")[0],
nations: (
await readYAMLDVPL<AvailableNationsYaml>(`${DATA}/available_nations.yaml`)
await vfs.yaml<AvailableNationsYaml>('Data/available_nations.yaml')
).available_nations,
gameModes: {},
roles: {},
@@ -565,12 +561,10 @@ export async function definitions() {
[TankClass.TANK_DESTROYER]: { skills: [] },
},
};
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
(nations) => nations.filter((nation) => nation !== "common")
);
const nations = vfs.dir('Data/XML/item_defs/vehicles').filter((nation) => nation !== "common")
const tankStringIdMap: Record<string, number> = {};
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
`${DATA}/XML/item_defs/vehicles/common/optional_devices.xml`
const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
'Data/XML/item_defs/vehicles/common/optional_devices.xml'
);
const stringsI18n: Record<string, Record<string, string>> = {};
@@ -582,8 +576,8 @@ export async function definitions() {
)
.then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings);
const preInstalled = await readYAMLDVPL<BlitzStrings>(
`${DATA}/Strings/${blitzLocale}.yaml`
const preInstalled = await vfs.yaml<BlitzStrings>(
`Data/Strings/${blitzLocale}.yaml`
);
stringsI18n[locale] = {
@@ -616,15 +610,15 @@ export async function definitions() {
return { locales: collection } satisfies I18nString;
}
const optionalDeviceSlots = await readXMLDVPL<{
const optionalDeviceSlots = await vfs.xml<{
root: OptionalDeviceSlots;
}>(`${DATA}/XML/item_defs/vehicles/common/optional_device_slots.xml`);
}>(`Data/XML/item_defs/vehicles/common/optional_device_slots.xml`);
const consumables: ConsumablesCommon = {};
const provisions: ProvisionsCommon = {};
for (const match of (
await readStringDVPL(
`${DATA}/XML/item_defs/vehicles/common/consumables/list.xml`
await vfs.text(
`Data/XML/item_defs/vehicles/common/consumables/list.xml`
)
).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue;
@@ -632,16 +626,16 @@ export async function definitions() {
Object.assign(
consumables,
(
await readXMLDVPL<{ root: ConsumablesCommon }>(
`${DATA}/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`
await vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`
)
).root
);
}
for (const match of (
await readStringDVPL(
`${DATA}/XML/item_defs/vehicles/common/provisions/list.xml`
await vfs.text(
`Data/XML/item_defs/vehicles/common/provisions/list.xml`
)
).matchAll(/<items path="(.+)\.xml"\/>/g)) {
if (match[1] === "prototypes") continue;
@@ -649,34 +643,34 @@ export async function definitions() {
Object.assign(
provisions,
(
await readXMLDVPL<{ root: ConsumablesCommon }>(
`${DATA}/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`
await vfs.xml<{ root: ConsumablesCommon }>(
`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`
)
).root
);
}
const avatar = await readXMLDVPL<{ root: Avatar }>(
`${DATA}/XML/item_defs/tankmen/avatar.xml`
const avatar = await vfs.xml<{ root: Avatar }>(
`Data/XML/item_defs/tankmen/avatar.xml`
);
const maps = await readYAMLDVPL<Maps>(`${DATA}/maps.yaml`);
const maps = await vfs.yaml<Maps>(`Data/maps.yaml`);
const tankXps = new Map<number, ResearchCost>();
const camouflagesXml = await readXMLDVPL<{ root: CamouflagesXML }>(
`${DATA}/XML/item_defs/vehicles/common/camouflages.xml`
const camouflagesXml = await vfs.xml<{ root: CamouflagesXML }>(
`Data/XML/item_defs/vehicles/common/camouflages.xml`
);
const camouflagesYaml = await readYAMLDVPL<CamouflagesYaml>(
`${DATA}/camouflages.yaml`
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
`Data/camouflages.yaml`
);
const camouflagesXmlEntries = Object.entries(camouflagesXml.root.camouflages);
const squadBattleTypeStyles = await readYAMLDVPL<SquadBattleTypeStyles>(
`${DATA}/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
);
const gameTypeSelectorStyles = await readYAMLDVPL<SquadBattleTypeStyles>(
`${DATA}/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
);
const gameModeNativeNames: Record<string, number> = {};
const combatRoles = await readYAMLDVPL<CombatRolesYaml>(
`${DATA}/XML/item_defs/vehicles/common/combat_roles.yaml`
const combatRoles = await vfs.yaml<CombatRolesYaml>(
`Data/XML/item_defs/vehicles/common/combat_roles.yaml`
);
const consumableNativeNames: Record<string, number> = {};
const provisionNativeNames: Record<string, number> = {};
@@ -727,8 +721,8 @@ export async function definitions() {
const idToNation: Record<number, string> = {};
for (const nation of nations) {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`
);
for (const tankKey in tankList.root) {
@@ -811,24 +805,24 @@ export async function definitions() {
});
for (const nation of nations) {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`
);
const turretList = await readXMLDVPL<{
const turretList = await vfs.xml<{
root: TurretDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/turrets.xml`);
const gunList = await readXMLDVPL<{
}>(`Data/XML/item_defs/vehicles/${nation}/components/turrets.xml`);
const gunList = await vfs.xml<{
root: GunDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/guns.xml`);
const shellList = await readXMLDVPL<{
}>(`Data/XML/item_defs/vehicles/${nation}/components/guns.xml`);
const shellList = await vfs.xml<{
root: ShellDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/shells.xml`);
const enginesList = await readXMLDVPL<{
}>(`Data/XML/item_defs/vehicles/${nation}/components/shells.xml`);
const enginesList = await vfs.xml<{
root: EngineDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/engines.xml`);
const chassisList = await readXMLDVPL<{
}>(`Data/XML/item_defs/vehicles/${nation}/components/engines.xml`);
const chassisList = await vfs.xml<{
root: ChassisDefinitionsList;
}>(`${DATA}/XML/item_defs/vehicles/${nation}/components/chassis.xml`);
}>(`Data/XML/item_defs/vehicles/${nation}/components/chassis.xml`);
function resolveUnlocks(unlocks?: BlitzModuleType) {
if (!unlocks) return [];
@@ -879,11 +873,11 @@ export async function definitions() {
const trackXps = new Map<number, ResearchCost>();
const tank = tankList.root[tankKey];
let tankPrice: TankPrice;
const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>(
`${DATA}/XML/item_defs/vehicles/${nation}/${tankKey}.xml`
const tankDefinition = await vfs.xml<{ root: VehicleDefinitions }>(
`Data/XML/item_defs/vehicles/${nation}/${tankKey}.xml`
);
const tankParameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
const tankParameters = await vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
);
const turretOrigin = tankDefinition.root.hull.turretPositions.turret
.split(" ")
@@ -1,10 +1,6 @@
import sharp from "sharp";
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { readStringDVPL } from "../../src/core/blitz/readStringDVPL";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import { OptionalDevices } from "./definitions";
interface Mappings {
@@ -20,15 +16,15 @@ export async function equipmentIcons() {
console.log("Building equipment icons...");
using uploader = new AssetUploader("equipment icons");
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
`${DATA}/XML/item_defs/vehicles/common/optional_devices.xml`
const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
`Data/XML/item_defs/vehicles/common/optional_devices.xml`
);
const mappings = await readYAMLDVPL<Mappings>(
`${DATA}/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`
const mappings = await vfs.yaml<Mappings>(
`Data/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`
);
const image = sharp(
await readDVPLFile(
`${DATA}/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`
await vfs.file(
`Data/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`
)
);
@@ -54,7 +50,7 @@ export async function equipmentIcons() {
if (configPath.startsWith("Gfx/Lobby")) {
const configPathWebp = configPath.replace(".txt", "");
const image = sharp(
await readDVPLFile(`${DATA}/${configPathWebp}.packed.webp`)
await vfs.file(`Data/${configPathWebp}.packed.webp`)
);
const content = await image.trim().toBuffer();
@@ -63,7 +59,7 @@ export async function equipmentIcons() {
content,
});
} else {
const config = await readStringDVPL(`${DATA}/${configPath}`);
const config = await vfs.text(`Data/${configPath}`);
const sizes = config.split("\n")[4].split(" ").map(Number);
const content = await image
.clone()
+8 -17
View File
@@ -1,37 +1,28 @@
import { readdir } from "fs/promises";
import sharp from "sharp";
import { readDVPLFile } from "../core/blitz/readDVPLFile";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
export async function flags() {
console.log("Building flags...");
using uploader = new AssetUploader("flags");
const circleFiles = await readdir(`${DATA}/Gfx/Lobby/flags`).then((files) =>
files.filter(
const circleFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
(flag) =>
flag.startsWith("flag_profile-stat_") &&
!flag.endsWith("@2x.packed.webp")
)
);
const scratchedFiles = await readdir(`${DATA}/Gfx/Lobby/flags`).then(
(files) =>
files.filter(
);
const scratchedFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
(flag) =>
flag.startsWith("flag_tutor-tank_") &&
!flag.endsWith("@2x.packed.webp")
)
);
const fadedFiles = await readdir(`${DATA}/Gfx/Lobby/flags`).then((files) =>
files.filter(
const fadedFiles = vfs.dir(`Data/Gfx/Lobby/flags`).filter(
(flag) =>
flag.startsWith("flag_filter_") && flag.endsWith("@2x.packed.webp")
)
);
for (const flag of circleFiles) {
const image = sharp(await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`));
const image = sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`));
const content = await image.trim({ threshold: 100 }).toBuffer();
const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1];
@@ -42,7 +33,7 @@ export async function flags() {
}
for (const flag of scratchedFiles) {
const content = await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`);
const content = await vfs.file(`Data/Gfx/Lobby/flags/${flag}`);
const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
uploader.add({
@@ -53,7 +44,7 @@ export async function flags() {
for (const flag of fadedFiles) {
const content = await sharp(
await readDVPLFile(`${DATA}/Gfx/Lobby/flags/${flag}`)
await vfs.file(`Data/Gfx/Lobby/flags/${flag}`)
)
.trim({
threshold: 100,
@@ -1,19 +1,17 @@
import sharp from "sharp";
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import { SquadBattleTypeStyles } from "./definitions";
export async function gameModeBanners() {
console.log("Building game mode banners...");
using uploader = new AssetUploader("game mode banners");
const gameTypeSelectorStyles = await readYAMLDVPL<SquadBattleTypeStyles>(
`${DATA}/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`
);
const squadBattleTypeStyles = await readYAMLDVPL<SquadBattleTypeStyles>(
`${DATA}/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`
);
const bannerMatches: { name: string; path: string }[] = [];
@@ -37,7 +35,7 @@ export async function gameModeBanners() {
}
const content = await sharp(
await readDVPLFile(`${DATA}${path}.packed.webp`)
await vfs.file(`Data${path}.packed.webp`)
)
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 } })
.toBuffer();
+12 -9
View File
@@ -1,16 +1,8 @@
import { config } from "dotenv";
import { argv } from "process";
config();
const allTargets = argv.includes("--all-targets");
const targets = argv
.find((argument) => argument.startsWith("--target"))
?.split("=")[1]
.split(",");
import { boosterIcons } from "./boosterIcons";
import { camouflageIcons } from "./camouflageIcons";
import { vfs } from "./constants";
import { consumableProvisionIcons } from "./consumableProvisionIcons";
import { currencies } from "./currencies";
import { definitions } from "./definitions";
@@ -27,6 +19,14 @@ import { tankArmors } from "./tankArmors";
import { tankIcons } from "./tankIcons";
import { tankModels } from "./tankModels";
config();
const allTargets = argv.includes("--all-targets");
const targets = argv
.find((argument) => argument.startsWith("--target"))
?.split("=")[1]
.split(",");
if (!targets && !allTargets) throw new Error("No target(s) specified");
const methods = [
@@ -59,3 +59,6 @@ for (const method of methods) {
}
}
}
vfs.dispose();
process.exit(0);
@@ -1,8 +1,6 @@
import { toUniqueId } from "@blitzkit/core";
import { readdir } from "fs/promises";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import { VehicleDefinitionList } from "./definitions";
export async function migration() {
@@ -10,13 +8,11 @@ export async function migration() {
using uploader = new AssetUploader("migration map");
const map: Record<string, number> = {};
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
(nations) => nations.filter((nation) => nation !== "common")
);
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common");
for (const nation of nations) {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`
);
for (const tankKey in tankList.root) {
@@ -1,24 +1,20 @@
import { readdir } from "fs/promises";
import sharp from "sharp";
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
export async function moduleIcons() {
console.log("Building module icons...");
using uploader = new AssetUploader("module icons");
const files = await readdir(`${DATA}/Gfx/UI/ModulesTechTree`).then((dir) =>
dir.filter(
const files = vfs.dir(`Data/Gfx/UI/ModulesTechTree`).filter(
(file) => !file.endsWith("@2x.packed.webp") && file.startsWith("vehicle")
)
);
for (const file of files) {
const nameRaw = file.match(/vehicle(.+)\.packed\.webp/)![1];
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
const content = await sharp(
await readDVPLFile(`${DATA}/Gfx/UI/ModulesTechTree/${file}`)
await vfs.file(`Data/Gfx/UI/ModulesTechTree/${file}`)
)
.trim()
.toBuffer();
+8 -11
View File
@@ -1,28 +1,25 @@
import { readdir } from "fs/promises";
import sharp from "sharp";
import { readDVPLFile } from "../../src/core/blitz/readDVPLFile";
import { readStringDVPL } from "../../src/core/blitz/readStringDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
export async function shellIcons() {
console.log("Building shell icons...");
using uploader = new AssetUploader("shell icons");
const image = sharp(
await readDVPLFile(
`${DATA}/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`
await vfs.file(
`Data/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`
)
);
const files = await readdir(
`${DATA}/Gfx/Shared/tank-supply/ammunition/big`
).then((files) => files.filter((file) => file.endsWith("_l.txt")));
const files = vfs.dir(
`Data/Gfx/Shared/tank-supply/ammunition/big`
).filter((file) => file.endsWith("_l.txt"))
for (const file of files) {
const name = file.match(/(.+)_l\.txt/)![1];
const sizes = (
await readStringDVPL(
`${DATA}/Gfx/Shared/tank-supply/ammunition/big/${file}`
await vfs.text(
`Data/Gfx/Shared/tank-supply/ammunition/big/${file}`
)
)
.split("\n")[4]
@@ -1,9 +1,7 @@
import { TankClass } from "@blitzkit/core";
import sharp from "sharp";
import { readDVPLFile } from "../core/blitz/readDVPLFile";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
interface SkillIcon {
name: string;
@@ -27,8 +25,8 @@ export interface Avatar {
export async function skillIcons() {
console.log("Building skill icons...");
const avatar = await readXMLDVPL<{ root: Avatar }>(
`${DATA}/XML/item_defs/tankmen/avatar.xml`
const avatar = await vfs.xml<{ root: Avatar }>(
`Data/XML/item_defs/tankmen/avatar.xml`
);
using uploader = new AssetUploader("skill icons");
@@ -36,8 +34,8 @@ export async function skillIcons() {
const skill = avatar.root.skills[key];
const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon;
const name = icon.name.split("/").at(-1)!.replace(/_\d$/, "");
const path = `${DATA}${icon.name.replace("~res:", "")}.packed.webp`;
const image = sharp(await readDVPLFile(path)).trim();
const path = `Data${icon.name.replace("~res:", "")}.packed.webp`;
const image = sharp(await vfs.file(path)).trim();
const content = await image.toBuffer();
await uploader.add({ content, path: `icons/skills/${name}.webp` });
@@ -1,10 +1,8 @@
import { toUniqueId } from "@blitzkit/core";
import { NodeIO } from "@gltf-transform/core";
import { readdir } from "fs/promises";
import { extractArmor } from "../core/blitz/extractArmor";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import { VehicleDefinitionList } from "./definitions";
export async function tankArmors() {
@@ -12,13 +10,11 @@ export async function tankArmors() {
using uploader = new AssetUploader("tank armors");
const nodeIO = new NodeIO();
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
(nations) => nations.filter((nation) => nation !== "common")
);
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common");
for (const nation of nations) {
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`
);
for (const tankKey in tanks.root) {
@@ -27,7 +23,7 @@ export async function tankArmors() {
if (tankKey.includes("tutorial_bot")) continue;
const id = toUniqueId(nation, tank.id);
const model = await extractArmor(DATA, `${nation}-${tankKey}`);
const model = await extractArmor(vfs, `${nation}-${tankKey}`);
const content = await nodeIO.writeBinary(model);
await uploader.add({ path: `3d/tanks/armor/${id}.glb`, content });
+10 -16
View File
@@ -1,12 +1,8 @@
import { NATION_IDS } from "@blitzkit/core";
import { readdir } from "fs/promises";
import sharp from "sharp";
import type { Vector3Tuple } from "three";
import { readDVPLFile } from "../core/blitz/readDVPLFile";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import type { VehicleDefinitionList } from "./definitions";
export interface TankParameters {
@@ -45,13 +41,11 @@ export async function tankIcons() {
console.log("Building tank icons...");
using uploader = new AssetUploader("tank icons");
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
(nations) => nations.filter((nation) => nation !== "common")
);
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common")
for (const nation of nations) {
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`
);
for (const tankKey in tanks.root) {
@@ -62,19 +56,19 @@ export async function tankIcons() {
const nationVehicleId = tank.id;
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
const parameters = await vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
);
const smallPath = `${DATA}/${parameters.resourcesPath.smallIconPath
const smallPath = `Data/${parameters.resourcesPath.smallIconPath
.replace(/~res:\//, "")
.replace(/\..+/, "")}.packed.webp`;
const bigPath = `${DATA}/${parameters.resourcesPath.bigIconPath
const bigPath = `Data/${parameters.resourcesPath.bigIconPath
.replace(/~res:\//, "")
.replace(/\..+/, "")}.packed.webp`;
const big = await sharp(await readDVPLFile(bigPath))
const big = await sharp(await vfs.file(bigPath))
.trim()
.toBuffer();
const small = await sharp(await readDVPLFile(smallPath))
const small = await sharp(await vfs.file(smallPath))
.trim()
.toBuffer();
+7 -12
View File
@@ -1,12 +1,9 @@
import { toUniqueId } from "@blitzkit/core";
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { readdir } from "fs/promises";
import { extractModel } from "../core/blitz/extractModel";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { AssetUploader } from "../core/github/assetUploader";
import { DATA } from "./constants";
import { vfs } from "./constants";
import { VehicleDefinitionList } from "./definitions";
import { TankParameters } from "./tankIcons";
@@ -15,14 +12,12 @@ export async function tankModels() {
using uploader = new AssetUploader("tank models");
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
(nations) => nations.filter((nation) => nation !== "common")
);
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common")
for (const nationIndex in nations) {
const nation = nations[nationIndex];
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`
);
const entries = Object.entries(tanks.root);
@@ -30,11 +25,11 @@ export async function tankModels() {
if (tankKey.includes("tutorial_bot")) continue;
const id = toUniqueId(nation, tank.id);
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
const parameters = await vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
);
const model = await extractModel(
DATA,
vfs,
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "")
);
const content = Buffer.from(await nodeIO.writeBinary(model));
+1 -1
View File
@@ -5,7 +5,7 @@ import { argv } from "process";
const projectDir = "../..";
const srcRoot = `${projectDir}/packages/bot`;
const distRoot = `${projectDir}/dist/bot`;
const modulesRoot = `${projectDir}/packages/bot/node_modules`;
const modulesRoot = "../../node_modules";
const workers = await readdir(`${srcRoot}/src/workers`);
const dev = argv.includes("--dev");
@@ -0,0 +1,62 @@
import { readdir, readFile, writeFile } from "fs/promises";
const SOURCES = "../../packages/closed/protos";
const TARGET = "../../packages/closed/src/unreal/catalogItemAccessor.ts";
const componentPattern = /^message (\w+)Component {/gm;
const files = await readdir(SOURCES).then((files) =>
files.filter(
(file) =>
file.startsWith("blitz_static_") && file.endsWith("_component.proto")
)
);
const imports: { name: string; file: string }[] = [];
for (const file of files) {
const content = await readFile(`${SOURCES}/${file}`).then((buffer) =>
buffer.toString()
);
const components = content.matchAll(componentPattern);
for (const match of components) {
const name = match[1];
const duplicate = imports.find((_import) => _import.name === name);
if (duplicate) {
console.warn(
`Duplicate component ${name} across ${duplicate.file}.proto and ${file}`
);
continue;
}
const importFile = file.replace(".proto", "");
imports.push({ name, file: importFile });
}
}
let content = "";
for (const _import of imports) {
content += `import { ${_import.name}Component } from "@protos/${_import.file}";\n`;
}
content +=
'import { BaseCatalogItemAccessor } from "./baseCatalogItemAccessor";\n\n';
content +=
"export class CatalogItemAccessor extends BaseCatalogItemAccessor {\n";
for (const _import of imports) {
const strippedName = _import.name.replace("BlitzStatic", "");
const defaultName = strippedName[0].toLowerCase() + strippedName.slice(1);
content += ` ${strippedName}(name = "${defaultName}Component") {\n`;
content += ` return this.component(name, ${_import.name}Component);\n`;
content += " }\n";
}
content += "}\n";
await writeFile(TARGET, content);
+6 -9
View File
@@ -4,21 +4,18 @@ const IMPORT = "@protos/auto_legacy_proxy_client";
const SOURCE = "../../packages/closed/protos/auto_legacy_proxy_client.proto";
const TARGET = "../../packages/closed/src/unreal/methodPopulatedProxyClient.ts";
const requestPattern = /message (\w+)Request {/g;
const requestPattern = /^message (\w+)Request {/gm;
const source = await readFile(SOURCE).then((buffer) => buffer.toString());
const requests = source.matchAll(requestPattern);
const imports: string[] = [];
const methods: { name: string; method: string }[] = [];
const names: string[] = [];
for (const match of requests) {
const name = match[1];
// const method =
// name.charAt(0).toLowerCase() + name.slice(1).replace(/Async$/, "");
const method = name;
imports.push(`${name}Request`, `${name}Response`);
methods.push({ name, method });
names.push(name);
}
let content = "";
@@ -31,9 +28,9 @@ content += 'import { BaseProxyClient } from "./baseProxyClient";\n\n';
content +=
"export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n";
content += methods
.map(({ name, method }) => {
return ` ${method}(data: ${name}Request) {
content += names
.map((name) => {
return ` ${name}(data: ${name}Request) {
return this.request("${name}Request", ${name}Request, data, ${name}Response);
}\n`;
})
+29 -29
View File
@@ -3,23 +3,23 @@ import {
Sc2ReadStream,
ScgReadStream,
VertexAttribute,
} from '@blitzkit/core';
import { Accessor, Document, Node, Scene } from '@gltf-transform/core';
import { times } from 'lodash-es';
} from "@blitzkit/core";
import { Accessor, Document, Node, Scene } from "@gltf-transform/core";
import { times } from "lodash-es";
import { SteamVFS } from "../steam/vfs";
import {
vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes,
} from './extractModel/constants';
import { readDVPLFile } from './readDVPLFile';
} from "./extractModel/constants";
export async function extractArmor(data: string, fileName: string) {
const sc2Path = `${data}/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
const scgPath = `${data}/3d/Tanks/CollisionMeshes/${fileName}.scg`;
export async function extractArmor(vfs: SteamVFS, fileName: string) {
const sc2Path = `Data/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
const sc2 = new Sc2ReadStream(
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer,
(await vfs.file(sc2Path)).buffer as ArrayBuffer
).sc2();
const scg = new ScgReadStream(
(await readDVPLFile(scgPath)).buffer as ArrayBuffer,
(await vfs.file(scgPath)).buffer as ArrayBuffer
).scg();
const document = new Document();
const scene = document.createScene();
@@ -29,21 +29,21 @@ export async function extractArmor(data: string, fileName: string) {
hierarchies.forEach((hierarchy) => {
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, '0')],
(index) => hierarchy.components[index.toString().padStart(4, "0")]
);
components.forEach((component) => {
switch (component['comp.typename']) {
case 'TransformComponent':
switch (component["comp.typename"]) {
case "TransformComponent":
break;
case 'RenderComponent': {
const batch = component['rc.renderObj']['ro.batches']['0000'];
const polygonGroup = scg.get(batch['rb.datasource']);
case "RenderComponent": {
const batch = component["rc.renderObj"]["ro.batches"]["0000"];
const polygonGroup = scg.get(batch["rb.datasource"]);
if (!polygonGroup) {
console.warn(
`Missing polygon group ${batch['rb.datasource']} (${hierarchy.name}); skipping...`,
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`
);
break;
@@ -73,7 +73,7 @@ export async function extractArmor(data: string, fileName: string) {
VertexAttribute.TANGENT,
attributes
.get(VertexAttribute.TANGENT)!
.map((tangent) => [...tangent, 1]),
.map((tangent) => [...tangent, 1])
);
}
@@ -85,7 +85,7 @@ export async function extractArmor(data: string, fileName: string) {
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
const accessor = document
.createAccessor(name)
.setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`)
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
.setArray(new Float32Array(value.flat()))
.setBuffer(buffer);
@@ -94,19 +94,19 @@ export async function extractArmor(data: string, fileName: string) {
hardJointIndices.forEach((hardJointIndex) => {
const node = document.createNode(
`${hierarchy.name}_armor_${hardJointIndex}`,
`${hierarchy.name}_armor_${hardJointIndex}`
);
const mesh = document.createMesh(batch['##name']);
const mesh = document.createMesh(batch["##name"]);
const indicesAccessor = document
.createAccessor()
.setType('SCALAR')
.setType("SCALAR")
.setArray(
new Uint16Array(
polygonGroup.indices.filter(
(index) =>
vertexHardJointIndices.get(index) === hardJointIndex,
),
),
vertexHardJointIndices.get(index) === hardJointIndex
)
)
)
.setBuffer(buffer);
const primitive = document
@@ -127,18 +127,18 @@ export async function extractArmor(data: string, fileName: string) {
default:
throw new TypeError(
`Unhandled component type: ${component['comp.typename']}`,
`Unhandled component type: ${component["comp.typename"]}`
);
}
});
if (hierarchy['#hierarchy']) {
parseHierarchies(hierarchy['#hierarchy'], parent);
if (hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy["#hierarchy"], parent);
}
});
}
parseHierarchies(sc2['#hierarchy'], scene);
parseHierarchies(sc2["#hierarchy"], scene);
return document;
}
@@ -9,8 +9,8 @@ import { Document, Material, Node, Scene } from "@gltf-transform/core";
import { dedup, prune } from "@gltf-transform/functions";
import { times } from "lodash-es";
import { dirname } from "path";
import { SteamVFS } from "../../steam/vfs";
import { readBaseColor } from "../readBaseColor";
import { readDVPLFile } from "../readDVPLFile";
import { readNormal } from "../readNormal";
import { readRoughnessMetallic } from "../readRoughnessMetallic";
import {
@@ -25,14 +25,14 @@ const omitMeshNames = {
end: ["_POINT"],
};
export async function extractModel(data: string, path: string) {
const sc2Path = `${data}/3d/${path}.sc2`;
const scgPath = `${data}/3d/${path}.scg`;
export async function extractModel(vfs: SteamVFS, path: string) {
const sc2Path = `Data/3d/${path}.sc2`;
const scgPath = `Data/3d/${path}.scg`;
const sc2 = new Sc2ReadStream(
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer
(await vfs.file(sc2Path)).buffer as ArrayBuffer
).sc2();
const scg = new ScgReadStream(
(await readDVPLFile(scgPath)).buffer as ArrayBuffer
(await vfs.file(scgPath)).buffer as ArrayBuffer
).scg();
const document = new Document();
const scene = document.createScene();
@@ -67,11 +67,11 @@ export async function extractModel(data: string, path: string) {
.setMimeType("image/webp")
.setImage(
await readBaseColor(
`${data}/3d/${dirname(path)}/${
`Data/3d/${dirname(path)}/${
textures.baseColorMap ?? textures.albedo
}`,
textures.miscMap
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
? `Data/3d/${dirname(path)}/${textures.miscMap}`
: undefined
)
)
@@ -103,7 +103,7 @@ export async function extractModel(data: string, path: string) {
.setMimeType("image/webp")
.setImage(
await readRoughnessMetallic(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`
`Data/3d/${dirname(path)}/${textures.baseRMMap}`
)
)
);
@@ -118,7 +118,7 @@ export async function extractModel(data: string, path: string) {
.setMimeType("image/webp")
.setImage(
await readNormal(
`${data}/3d/${dirname(path)}/${
`Data/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap
}`,
isBase
@@ -1,6 +0,0 @@
import { readDVPLFile } from './readDVPLFile';
export async function readBase64DVPL(file: string) {
const buffer = await readDVPLFile(file);
return buffer.toString('base64');
}
@@ -1,21 +0,0 @@
import { readFile } from 'fs/promises';
import { DATA_COMPRESSED } from '../../buildAssets/constants';
import { readDVPL } from './readDVPL';
export async function readDVPLFile(file: string) {
let path: string;
if (DATA_COMPRESSED) {
path = file.endsWith('.dvpl') ? file : `${file}.dvpl`;
} else {
path = file.replace('.dvpl', '');
}
const content = await readFile(path);
if (!DATA_COMPRESSED) return content;
const dvpl = readDVPL(content);
return dvpl;
}
@@ -1,6 +0,0 @@
import { readDVPLFile } from './readDVPLFile';
export async function readStringDVPL(file: string) {
const buffer = await readDVPLFile(file);
return buffer.toString();
}
@@ -1,81 +1,17 @@
import { DdsReadStream, PvrReadStream } from "@blitzkit/core";
import { existsSync } from "fs";
import { DATA_COMPRESSED } from "../../../buildAssets/constants";
import { readDVPLFile } from "../readDVPLFile";
import { vfs } from "../../../buildAssets/constants";
export async function readTexture(path: string) {
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
const isDds = existsSync(
`${ddsTexturePath}${DATA_COMPRESSED ? ".dvpl" : ""}`
);
const isDds = vfs.has(ddsTexturePath);
const resolvedTexturePath = isDds
? ddsTexturePath
: ddsTexturePath.replace(".dds", ".pvr");
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`);
const file = await vfs.file(resolvedTexturePath);
const raw = isDds
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds()
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr();
? await new DdsReadStream(file.buffer as ArrayBuffer).dds()
: new PvrReadStream(file.buffer as ArrayBuffer).pvr();
return raw;
// switch (mutation) {
// case TextureMutation.Normal: {
// const bytes = 4 * raw.width * raw.height;
// for (let index = 0; index < bytes; index += 4) {
// /**
// * Red is always 255 and blue is always 0. Only alpha and green contain any
// * sort of information.
// */
// let x = raw.data[index + 3] * (2 / 255) - 1;
// let y = raw.data[index + 1] * (2 / 255) - 1;
// const underSqrt = 1 - x ** 2 - y ** 2;
// let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
// raw.data[index] = Math.round((x + 1) * (255 / 2));
// raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
// raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
// raw.data[index + 3] = 255;
// }
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
// }
// case TextureMutation.RoughnessMetallicness: {
// const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
// raw: { ...raw, channels: 1 },
// });
// const metallicness = sharp(raw.data, { raw })
// .extractChannel("green")
// .jpeg()
// .toBuffer();
// const roughness = sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
// return await newImage
// .joinChannel(await Promise.all([roughness, metallicness]))
// .jpeg()
// .toBuffer();
// }
// case TextureMutation.Miscellaneous: {
// /**
// * Alpha is ambient occlusion and green is emissive. But very few tanks
// * use emissive so I am ignoring it for now.
// */
// return await sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
// }
// case TextureMutation.Albedo: {
// return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
// }
// default:
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
// }
}
@@ -1,9 +0,0 @@
import { XMLParser } from 'fast-xml-parser';
import { readStringDVPL } from './readStringDVPL';
export async function readXMLDVPL<Type>(file: string) {
const parser = new XMLParser();
const xml = await readStringDVPL(file);
return parser.parse(xml) as Type;
}
@@ -1,7 +0,0 @@
import { readFile } from 'fs/promises';
import { parse } from 'yaml';
export async function readYAML<Type>(file: string) {
const yaml = (await readFile(file)).toString();
return parse(yaml) as Type;
}
@@ -1,8 +0,0 @@
import { parse } from 'yaml';
import { readStringDVPL } from './readStringDVPL';
export async function readYAMLDVPL<Type>(file: string) {
const yaml = await readStringDVPL(file);
return parse(yaml) as Type;
}
+155
View File
@@ -0,0 +1,155 @@
import { readDVPL } from "@blitzkit/core/src/blitz";
import { XMLParser } from "fast-xml-parser";
import { normalize } from "path/posix";
import SteamUser, { EConnectionProtocol } from "steam-user";
import { parse } from "yaml";
interface SteamManifestFile {
chunks: {
sha: string;
crc: number;
offset: string;
cb_original: number;
cb_compressed: number;
}[];
filename: string;
size: string;
flags: number;
sha_filename: string;
sha_content: string;
linktarget: unknown;
}
interface SteamManifest {
files: SteamManifestFile[];
depot_id: number;
gid_manifest: string;
creation_time: number;
filenames_encrypted: boolean;
cb_disk_original: string;
cb_disk_compressed: string;
unique_chunks: number;
crc_encrypted: number;
crc_clear: number;
}
export class SteamVFS {
private steam = new SteamUser({ protocol: EConnectionProtocol.TCP });
private textDecoder = new TextDecoder();
private xmlParser = new XMLParser();
private manifest: Map<string, SteamManifestFile> = new Map();
constructor(
private username: string,
private password: string,
private app: number,
private depot: number
) {}
async init() {
this.steam.logOn({
accountName: this.username,
password: this.password,
});
await new Promise((resolve) => this.steam.once("loggedOn", resolve));
const productInfo = await this.steam.getProductInfo([this.app], []);
const manifest: SteamManifest = await new Promise((resolve) => {
// @ts-expect-error
this.steam.getManifest(
this.app,
this.depot,
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
.gid,
"public",
// @ts-expect-error
(_, response) => {
resolve(response);
}
);
});
const paths: string[] = [];
for (const file of manifest.files) {
const path = file.filename.replaceAll("\\", "/");
paths.push(path);
this.manifest.set(path, file);
}
return this;
}
has(path: string) {
const normalized = normalize(path);
const dvplPath = `${normalized}.dvpl`;
return (
(this.manifest.has(dvplPath) && dvplPath) ||
(this.manifest.has(normalized) && normalized)
);
}
assert(requested: string) {
const path = this.has(requested);
if (!path) throw new Error(`File not found: ${requested}`);
return path;
}
dir(path: string) {
const parentSegments = path.split("/").length;
const children: string[] = [];
for (const child of this.manifest.keys()) {
const childSegments = child.split("/");
if (
child.startsWith(path) &&
parentSegments + 1 === childSegments.length
) {
children.push(childSegments.at(-1)!);
}
}
return children;
}
async file(requested: string) {
const path = this.assert(requested);
const fileManifest = this.manifest.get(path)!;
const downloaded: { type: "complete"; file: Buffer } =
// @ts-expect-error
await this.steam.downloadFile(this.app, this.depot, fileManifest);
let buffer = downloaded.file;
if (path.endsWith(".dvpl")) buffer = readDVPL(downloaded.file);
return new Uint8Array(buffer);
}
async text(path: string) {
const file = await this.file(path);
return this.textDecoder.decode(file);
}
async yaml<Type>(path: string) {
const file = await this.text(path);
return parse(file) as Type;
}
async xml<Type>(path: string) {
const file = await this.text(path);
return this.xmlParser.parse(file) as Type;
}
dispose() {
this.steam.logOff();
}
[Symbol.dispose]() {
this.dispose();
}
}
+22 -6
View File
@@ -4,6 +4,10 @@ import { readdir, rm } from "fs/promises";
import { promisify } from "util";
const MAX_COMMAND_LENGTH = 2 ** 11;
const TS_PROTO_EXECUTABLE_LOCATIONS = [
"./node_modules/.bin/protoc-gen-ts_proto",
"../../node_modules/.bin/protoc-gen-ts_proto",
];
const roots = [
// "../../packages/core/src/protos",
@@ -12,12 +16,24 @@ const roots = [
const exec = promisify(execSync);
let executableFileExtension: string;
let tsProtoExecutableLocation: string | undefined = undefined;
if (existsSync("../../node_modules/.bin/protoc-gen-ts_proto.exe")) {
executableFileExtension = ".exe";
} else {
executableFileExtension = "";
for (const location of TS_PROTO_EXECUTABLE_LOCATIONS) {
if (existsSync(location)) {
tsProtoExecutableLocation = location;
break;
} else if (existsSync(`${location}.exe`)) {
tsProtoExecutableLocation = `${location}.exe`;
break;
}
}
if (!tsProtoExecutableLocation) {
throw new Error(
`Could not find ts-proto executable. Checked locations: ${TS_PROTO_EXECUTABLE_LOCATIONS.join(
", "
)}`
);
}
for (const root of roots) {
@@ -32,7 +48,7 @@ for (const root of roots) {
while (files.length > 0) {
let command = [
"protoc",
`--plugin=../../node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`,
`--plugin=${tsProtoExecutableLocation}`,
"--ts_proto_opt=esModuleInterop=true",
"--ts_proto_opt=oneof=unions-value",
// "--ts_proto_opt=removeEnumPrefix=true",
+17 -17
View File
@@ -1,39 +1,39 @@
import decompress from 'decompress';
import { existsSync } from 'fs';
import { mkdir, readdir, writeFile } from 'fs/promises';
import decompress from "decompress";
import { existsSync } from "fs";
import { mkdir, readdir, writeFile } from "fs/promises";
if (!existsSync('../../temp')) {
console.log('Creating temp directory...');
await mkdir('../../temp');
if (!existsSync("../../temp")) {
console.log("Creating temp directory...");
await mkdir("../../temp");
}
console.log('Downloading depot downloader...');
console.log("Downloading depot downloader...");
await fetch(
'https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest',
"https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest"
)
.then(
(response) =>
response.json() as Promise<{
assets: { browser_download_url: string; name: string }[];
}>,
}>
)
.then(({ assets }) => {
const asset = assets.find(({ name }) => name.includes('linux-x64'));
if (!asset) throw new Error('No asset found');
const asset = assets.find(({ name }) => name.includes("linux-x64"));
if (!asset) throw new Error("No asset found");
return fetch(asset.browser_download_url);
})
.then((response) => response.arrayBuffer())
.then((arrayBuffer) =>
writeFile('../../temp/depotDownloader.zip', new Uint8Array(arrayBuffer)),
writeFile("../../temp/depotDownloader.zip", new Uint8Array(arrayBuffer))
);
console.log('Unzipping...');
console.log("Unzipping...");
await decompress(
'../../temp/depotDownloader.zip',
'../../temp/depotDownloader',
"../../temp/depotDownloader.zip",
"../../temp/depotDownloader"
);
console.log(
'Download complete; files:',
await readdir('../../temp/depotDownloader'),
"Download complete; files:",
await readdir("../../temp/depotDownloader")
);
+1 -1
View File
@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*"]
"include": ["src/**/*", "../core/src/blitz/readDVPL.ts"]
}
Binary file not shown.
@@ -67,7 +67,9 @@ export const PrimaryArmorScene = memo(() => {
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
`turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
@@ -4,6 +4,7 @@ import {
resolvePenetrationCoefficient,
} from "@blitzkit/core";
import { invalidate, useFrame } from "@react-three/fiber";
import type { QuicklimeEvent } from "quicklime";
import { useEffect } from "react";
import {
MeshBasicMaterial,
@@ -21,6 +22,7 @@ import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw";
import { spacedArmorRenderTarget } from "./target";
@@ -72,6 +74,8 @@ export function PrimaryArmorSceneComponent({
resolution: { value: new Vector2() },
spacedArmorBuffer: { value: null },
spacedArmorDepth: { value: null },
opacity: { value: 0 },
},
]),
});
@@ -154,6 +158,10 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate();
}
function handleTransitionEvent(event: QuicklimeEvent<number>) {
material.uniforms.opacity.value = event.data;
}
handleShellChange();
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange(
@@ -164,6 +172,8 @@ export function PrimaryArmorSceneComponent({
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
transitionEvent.on(handleTransitionEvent);
const unsubscribes = [
Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange),
@@ -184,12 +194,13 @@ export function PrimaryArmorSceneComponent({
(state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment)
),
() => transitionEvent.off(handleTransitionEvent),
];
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
});
}, []);
useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
@@ -1,12 +1,9 @@
precision mediump float;
#define USE_FOG
#include <fog_pars_fragment>
varying vec3 vNormal;
varying vec3 vViewPosition;
uniform float opacity;
uniform float thickness;
uniform float penetration;
uniform float caliber;
@@ -119,5 +116,5 @@ void main() {
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
}
#include <fog_fragment>
gl_FragColor.a *= opacity;
}
@@ -1,9 +1,5 @@
precision mediump float;
#define USE_FOG
#include <fog_pars_vertex>
varying vec3 vNormal;
varying vec3 vViewPosition;
@@ -14,6 +10,4 @@ void main() {
vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * mvPosition;
#include <fog_vertex>
}
@@ -24,6 +24,7 @@ import {
} from "../../../core/blitzkit/modelTransform";
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
import { Tankopedia } from "../../../stores/tankopedia";
import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { ArmorType } from "./SpacedArmorScene";
import type {
ArmorUserData,
@@ -108,10 +109,11 @@ export function StaticArmorSceneComponent({
() =>
new MeshBasicMaterial({
color,
opacity,
transparent: opacity < 1,
transparent: true,
depthWrite,
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
opacity: 0,
userData: { opacity0: opacity },
}),
[thickness]
);
@@ -121,6 +123,8 @@ export function StaticArmorSceneComponent({
color: color
.clone()
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
opacity: 0,
transparent: true,
}),
[thickness]
);
@@ -163,11 +167,22 @@ export function StaticArmorSceneComponent({
surfaceMaterial.needsUpdate = true;
}
function handleTransitionEvent(event: QuicklimeEvent<number>) {
surfaceMaterial.opacity = surfaceMaterial.userData.opacity0 * event.data;
outlineMaterial.opacity = event.data;
surfaceMaterial.transparent = surfaceMaterial.opacity < 1;
outlineMaterial.transparent = outlineMaterial.opacity < 1;
}
transitionEvent.on(handleTransitionEvent);
const unsubscribes = [
Tankopedia.on(
(state) => state.highlightArmor?.name,
handleHighlightArmor
),
() => transitionEvent.off(handleTransitionEvent),
];
if (props.clip) {
@@ -3,6 +3,7 @@ import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { CompareCellDirection } from "../CompareCell";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { CompareRow } from "./CompareRow";
import { CompareSectionTitle } from "./CompareSectionTitle";
@@ -79,7 +80,7 @@ export function Body({ stats }: BodyProps) {
<CompareRow
stats={stats}
name={strings.website.tools.tankopedia.characteristics.values.reload}
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
value={(stats) =>
stats.shellReload ??
stats.shellReloads!.reduce((a, b) => a + b, 0) /
@@ -95,7 +96,7 @@ export function Body({ stats }: BodyProps) {
<>
<CompareRow
stats={stats}
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
value="intraClip"
decimals={2}
/>
@@ -107,7 +108,7 @@ export function Body({ stats }: BodyProps) {
<CompareRow
stats={stats}
value="shellRicochet"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={0}
/>
<CompareRow stats={stats} value="shellVelocity" decimals={0} />
@@ -116,48 +117,48 @@ export function Body({ stats }: BodyProps) {
<CompareRow
stats={stats}
value="aimTime"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2}
/>
<CompareRow
stats={stats}
value="dispersion"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3}
/>
<CompareRow
stats={stats}
indent
value="dispersionMoving"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3}
/>
<CompareRow
stats={stats}
indent
value="dispersionHullTraversing"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3}
/>
<CompareRow
stats={stats}
indent
value="dispersionTurretTraversing"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3}
/>
<CompareRow
stats={stats}
indent
value="dispersionShooting"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3}
/>
<CompareRow
stats={stats}
indent
value="dispersionGunDamaged"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={3}
/>
<CompareRow stats={stats} value="gunDepression" decimals={1} />
@@ -186,21 +187,21 @@ export function Body({ stats }: BodyProps) {
<CompareRow
stats={stats}
value="hardTerrainCoefficientRaw"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2}
/>
<CompareRow
stats={stats}
indent
value="mediumTerrainCoefficientRaw"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2}
/>
<CompareRow
stats={stats}
indent
value="softTerrainCoefficientRaw"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={2}
/>
<CompareRow
@@ -246,7 +247,7 @@ export function Body({ stats }: BodyProps) {
<CompareRow
stats={stats}
value="fireChance"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
display={(stats) => (stats.fireChance * 100).toFixed(0)}
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/>
@@ -288,28 +289,28 @@ export function Body({ stats }: BodyProps) {
<CompareRow
stats={stats}
value="volume"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1}
/>
<CompareRow
stats={stats}
indent
value="width"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1}
/>
<CompareRow
stats={stats}
indent
value="height"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1}
/>
<CompareRow
stats={stats}
indent
value="length"
deltaType="lowerIsBetter"
deltaType={CompareCellDirection.LOWER_IS_BETTER}
decimals={1}
/>
</Table.Body>
@@ -6,17 +6,17 @@ import {
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
import { useRef } from "react";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { Var } from "../../core/radix/var";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { ComparePersistent } from "../../stores/comparePersistent";
import { CompareCell, CompareCellDirection } from "../CompareCell";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
type CompareRowProps = {
display?: (
member: Awaited<TankCharacteristics>
) => number | string | undefined;
deltaType?: "higherIsBetter" | "lowerIsBetter";
deltaType?: CompareCellDirection;
decimals?: number;
deltaNominalDisplay?: (delta: number) => string | number;
indent?: boolean;
@@ -33,7 +33,7 @@ type CompareRowProps = {
export function CompareRow({
display,
deltaType = "higherIsBetter",
deltaType = CompareCellDirection.HIGHER_IS_BETTER,
decimals,
deltaNominalDisplay,
indent,
@@ -108,33 +108,18 @@ export function CompareRow({
{values.map((value, index) => {
const delta = value - values[0];
const deltaPercentage = value / values[0] - 1;
const normalizedDeltaPercentage = Math.round(
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
);
const resolvedDisplayValue = display
? display(stats[index])
: decimals === undefined
? value
: value?.toFixed(decimals);
? value
: value?.toFixed(decimals);
return (
<Table.Cell
<CompareCell
key={index}
style={{
backgroundColor:
index === 0 ||
value === undefined ||
values[0] === undefined ||
value === values[0]
? undefined
: (
deltaType === "higherIsBetter"
? value > values[0]
: value < values[0]
)
? `color-mix(in srgb, ${Var("green-7")} ${normalizedDeltaPercentage}%, ${Var("green-3")})`
: `color-mix(in srgb, ${Var("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
}}
truth={values[0]}
value={value}
direction={deltaType}
>
<Flex
align="center"
@@ -160,8 +145,8 @@ export function CompareRow({
deltaNominalDisplay
? deltaNominalDisplay(delta)
: decimals === undefined
? delta
: delta.toFixed(decimals)
? delta
: delta.toFixed(decimals)
}`}
)
</Text>
@@ -179,7 +164,7 @@ export function CompareRow({
</>
)}
</Flex>
</Table.Cell>
</CompareCell>
);
})}
</Table.Row>
@@ -51,19 +51,17 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="surface"
style={{
position: "absolute",
width: "100%",
maxWidth: "100%",
height: "100%",
borderRadius: 0,
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
}}
>
<Table.Header>
<Table.Row>
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
<Flex
style={{
width: "100%",
height: "100%",
}}
style={{ width: "100%", height: "100%" }}
align="center"
justify="center"
>
@@ -107,8 +105,8 @@ export function CompareTable({ stats }: CompareTableProps) {
crewSkills[skill] === 0
? Var("gray-11")
: permanentSkills.includes(skill)
? Var("crimson-11")
: Var("amber-11"),
? Var("crimson-11")
: Var("amber-11"),
}}
/>
))}
@@ -102,7 +102,7 @@ export function TankCard({ index, tank }: TankCardProps) {
}, []);
return (
<StickyColumnHeaderCell>
<StickyColumnHeaderCell width="0">
<Flex direction="column" align="center" justify="between" gap="2">
<TankControl index={index} key={tank.id} slug={tank.slug} />
@@ -0,0 +1,41 @@
import { Table } from "@radix-ui/themes";
import type { CellProps } from "@radix-ui/themes/components/table";
export enum CompareCellDirection {
HIGHER_IS_BETTER,
LOWER_IS_BETTER,
}
interface CompareCellProps extends CellProps {
value: number;
truth: number;
direction?: CompareCellDirection;
}
export function CompareCell({
value,
truth,
style,
direction = CompareCellDirection.HIGHER_IS_BETTER,
...props
}: CompareCellProps) {
const percentage = value / truth - 1;
const shiftedPercentage = Math.round(
Math.min(100, Math.abs(percentage) * 2 * 100 + 25)
);
return (
<Table.Cell
style={{
backgroundColor:
value === truth
? undefined
: (direction === CompareCellDirection.HIGHER_IS_BETTER) ===
value > truth
? `color-mix(in srgb, var(--green-7) ${shiftedPercentage}%, var(--green-3))`
: `color-mix(in srgb, var(--red-7) ${shiftedPercentage}%, var(--red-3))`,
}}
{...props}
/>
);
}
@@ -1,3 +1,5 @@
import { Box, Text } from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { memo, type ComponentProps } from "react";
interface Props extends ComponentProps<"spotLight"> {
@@ -8,10 +10,20 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
return (
<>
{debug && (
<mesh position={props.position}>
<icosahedronGeometry args={[0.1, 2]} />
<meshBasicMaterial color={props.color} />
</mesh>
<group position={props.position}>
<mesh>
<icosahedronGeometry args={[0.1, 2]} />
<meshBasicMaterial color={props.color} />
</mesh>
<Html center>
<Box pb="9">
<Text wrap="nowrap">
{props.intensity} @ {JSON.stringify(props.userData)}
</Text>
</Box>
</Html>
</group>
)}
<spotLight {...props} />
@@ -1,20 +1,22 @@
import { Flex } from '@radix-ui/themes';
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
import { StickyTableRoot } from '../StickyTableRoot';
import { Header } from './Header';
import { Tanks } from './Tanks';
import { Flex } from "@radix-ui/themes";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { StickyTableRoot } from "../StickyTableRoot";
import { Header } from "./Header";
import { Tanks } from "./Tanks";
export function TankTable({ skeleton }: MaybeSkeletonComponentProps) {
return (
<Flex justify="center" maxWidth="100%" flexGrow="1" position="relative">
<StickyTableRoot
size={{ initial: '1', sm: '2' }}
size={{ initial: "1", sm: "2" }}
variant="surface"
style={{
position: 'absolute',
maxWidth: '100%',
maxHeight: '100%',
height: '100%',
position: "absolute",
maxWidth: "100%",
maxHeight: "100%",
height: "100%",
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
}}
>
<Header />
@@ -1,24 +1,26 @@
import { TankDefinition, formatCompact } from '@blitzkit/core';
import { Table } from '@radix-ui/themes';
import { memo } from 'react';
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
import { useLocale } from '../../hooks/useLocale';
import { Performance } from '../../stores/performance';
import { TankRowHeaderCell } from '../TankRowHeaderCell';
import { TankDefinition, formatCompact } from "@blitzkit/core";
import { Table } from "@radix-ui/themes";
import { memo } from "react";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from "../../hooks/useLocale";
import { Performance } from "../../stores/performance";
import { CompareCell, CompareCellDirection } from "../CompareCell";
import { TankRowHeaderCell } from "../TankRowHeaderCell";
interface TankRowProps {
tank: TankDefinition;
average: Record<string, number>;
}
const averageDefinitions = await awaitableAverageDefinitions;
export const TankRow = memo<TankRowProps>(
({ tank }) => {
({ tank, average }) => {
const averages = averageDefinitions.averages[tank.id];
const ratio = useAveragesExclusionRatio();
const playerCountPeriod = Performance.use(
(state) => state.playerCountPeriod,
(state) => state.playerCountPeriod
);
const { locale } = useLocale();
@@ -26,59 +28,108 @@ export const TankRow = memo<TankRowProps>(
<Table.Row>
<TankRowHeaderCell tank={tank} />
<Table.Cell align="center">
<CompareCell
value={averages.mu.wins / averages.mu.battles}
truth={average.winrate}
align="center"
>
{((averages.mu.wins / averages.mu.battles) * 100).toFixed(1)}%
</Table.Cell>
</CompareCell>
<Table.Cell align="center">
{formatCompact(
locale,
Math.round(ratio * averages.samples[playerCountPeriod]),
Math.round(ratio * averages.samples[playerCountPeriod])
)}
</Table.Cell>
<Table.Cell align="center">
<CompareCell
value={averages.mu.damage_dealt / averages.mu.battles}
truth={average.damage}
align="center"
>
{Math.round(
averages.mu.damage_dealt / averages.mu.battles,
averages.mu.damage_dealt / averages.mu.battles
).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.survived_battles / averages.mu.battles}
truth={average.survival}
align="center"
>
{Math.round(
(averages.mu.survived_battles / averages.mu.battles) * 100,
(averages.mu.survived_battles / averages.mu.battles) * 100
)}
%
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.xp / averages.mu.battles}
truth={average.xp}
align="center"
>
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
locale,
locale
)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.frags / averages.mu.battles}
truth={average.kills}
align="center"
>
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.spotted / averages.mu.battles}
truth={average.spots}
align="center"
>
{(averages.mu.spotted / averages.mu.battles).toFixed(2)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.hits / averages.mu.shots}
truth={average.accuracy}
align="center"
>
{Math.round((averages.mu.hits / averages.mu.shots) * 100)}%
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.shots / averages.mu.battles}
truth={average.shots}
align="center"
>
{(averages.mu.shots / averages.mu.battles).toFixed(1)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.hits / averages.mu.battles}
truth={average.hits}
align="center"
>
{(averages.mu.hits / averages.mu.battles).toFixed(1)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.damage_dealt / averages.mu.damage_received}
truth={average.damageRatio}
align="center"
>
{(averages.mu.damage_dealt / averages.mu.damage_received).toFixed(2)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.damage_received / averages.mu.battles}
truth={average.damageReceived}
direction={CompareCellDirection.LOWER_IS_BETTER}
align="center"
>
{Math.round(
averages.mu.damage_received / averages.mu.battles,
averages.mu.damage_received / averages.mu.battles
).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
</CompareCell>
<CompareCell
value={averages.mu.capture_points / averages.mu.battles}
truth={average.capturePoints}
align="center"
>
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
</Table.Cell>
</CompareCell>
</Table.Row>
);
},
(prev, next) => prev.tank.id === next.tank.id,
(prev, next) => prev.tank.id === next.tank.id
);
@@ -1,17 +1,27 @@
import { formatCompact, type BlitzkitStats } from "@blitzkit/core";
import { Table } from "@radix-ui/themes";
import { times } from "lodash-es";
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
import {
Fragment,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import usePromise from "react-promise-suspense";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
import { filterTanks } from "../../core/blitzkit/filterTanks";
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from "../../hooks/useLocale";
import { Performance } from "../../stores/performance";
import { TankFilters } from "../../stores/tankFilters";
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
import { RowLoader } from "./RowLoader";
import { TankRow } from "./TankRow";
import { Total } from "./Total";
const PREVIEW_COUNT = 10;
const DEFAULT_LOADED_ROWS = 25;
@@ -145,9 +155,83 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
setLoadedRows(DEFAULT_LOADED_ROWS);
}, [filters, sort]);
const playerCountPeriod = Performance.use((state) => state.playerCountPeriod);
const { strings, locale } = useLocale();
const ratio = useAveragesExclusionRatio();
const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => {
return tanksSorted.reduce(
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
0
);
},
[tanksSorted]
);
const battles = sum(({ battles }) => battles);
const winrate = sum(({ wins }) => wins) / battles;
const damage = sum(({ damage_dealt }) => damage_dealt) / battles;
const survival = sum(({ survived_battles }) => survived_battles) / battles;
const xp = sum(({ xp }) => xp) / battles;
const kills = sum(({ frags }) => frags) / battles;
const spots = sum(({ spotted }) => spotted) / battles;
const hits = sum(({ hits }) => hits) / battles;
const shots = sum(({ shots }) => shots) / battles;
const accuracy = hits / shots;
const damageReceived =
sum(({ damage_received }) => damage_received) / battles;
const damageRatio = damage / damageReceived;
const capturePoints = sum(({ capture_points }) => capture_points) / battles;
const average = {
battles,
winrate,
damage,
survival,
xp,
kills,
spots,
hits,
shots,
accuracy,
damageReceived,
damageRatio,
capturePoints,
};
return (
<Table.Body>
<Total tanks={tanksSorted} />
<Table.Row>
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
{strings.website.tools.performance.table.tanks.total}
</StickyRowHeaderCell>
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
<Table.Cell align="center">
{formatCompact(
locale,
ratio * averageDefinitions.samples[playerCountPeriod]
)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(damage).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(survival * 100).toFixed(1)}%
</Table.Cell>
<Table.Cell align="center">
{Math.round(xp).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{(accuracy * 100).toFixed(0)}%</Table.Cell>
<Table.Cell align="center">{shots.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
<Table.Cell align="center">
{Math.round(damageReceived).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
</Table.Row>
{tanksSorted.slice(0, loadedRows).map((averages) => {
const tank = tankDefinitions.tanks[averages.id];
@@ -157,7 +241,7 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
return (
<Fragment key={tank.id}>
<Suspense fallback={<RowLoader />}>
<TankRow tank={tank} />
<TankRow average={average} tank={tank} />
</Suspense>
</Fragment>
);
@@ -1,91 +0,0 @@
import {
AverageDefinitionsEntry,
formatCompact,
type BlitzkitStats,
} from "@blitzkit/core";
import { Table } from "@radix-ui/themes";
import { memo, useCallback } from "react";
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
import { useAveragesExclusionRatio } from "../../hooks/useAveragesExclusionRatio";
import { useLocale } from "../../hooks/useLocale";
import { Performance } from "../../stores/performance";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
export interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
id: number;
}
interface TotalProps {
tanks: AverageDefinitionsEntryWithId[];
}
const averageDefinitions = await awaitableAverageDefinitions;
export const Total = memo<TotalProps>(
({ tanks }) => {
const playerCountPeriod = Performance.use(
(state) => state.playerCountPeriod
);
const { strings, locale } = useLocale();
const ratio = useAveragesExclusionRatio();
const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => {
return tanks.reduce(
(acc, tank) => acc + tank.samples[playerCountPeriod] * slice(tank.mu),
0
);
},
[tanks]
);
const battles = sum(({ battles }) => battles);
const winrate = sum(({ wins }) => wins) / battles;
const damage = sum(({ damage_dealt }) => damage_dealt) / battles;
const survival = sum(({ survived_battles }) => survived_battles) / battles;
const xp = sum(({ xp }) => xp) / battles;
const kills = sum(({ frags }) => frags) / battles;
const spots = sum(({ spotted }) => spotted) / battles;
const hits = sum(({ hits }) => hits) / battles;
const shots = sum(({ shots }) => shots) / battles;
const accuracy = hits / shots;
const damageReceived =
sum(({ damage_received }) => damage_received) / battles;
const damageRatio = damage / damageReceived;
const capturePoints = sum(({ capture_points }) => capture_points) / battles;
return (
<Table.Row>
<StickyRowHeaderCell style={{ overflow: "hidden", display: "flex" }}>
{strings.website.tools.performance.table.tanks.total}
</StickyRowHeaderCell>
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
<Table.Cell align="center">
{formatCompact(
locale,
ratio * averageDefinitions.samples[playerCountPeriod]
)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(damage).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(survival * 100).toFixed(1)}%
</Table.Cell>
<Table.Cell align="center">
{Math.round(xp).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{(accuracy * 100).toFixed(0)}%</Table.Cell>
<Table.Cell align="center">{shots.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
<Table.Cell align="center">
{Math.round(damageReceived).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
</Table.Row>
);
},
(prev, next) => prev.tanks === next.tanks
);
@@ -37,26 +37,26 @@ export function PlaylistActions() {
{strings.website.tools.playlist.shuffle}
</Button>
<Button
size={{ initial: "1", xs: "2" }}
variant="outline"
onClick={() => {
Playlist.mutate((draft) => {
draft.list = draft.list?.filter(
(entry) =>
(!entry.now ||
!entry.then ||
entry.now.battles === entry.then.battles) &&
!entry.checked
);
});
}}
>
{strings.website.tools.playlist.remove}
</Button>
{wargaming && (
<>
<Button
size={{ initial: "1", xs: "2" }}
variant="outline"
onClick={() => {
Playlist.mutate((draft) => {
draft.list = draft.list?.filter(
(entry) =>
(!entry.now ||
!entry.then ||
entry.now.battles === entry.then.battles) &&
!entry.checked
);
});
}}
>
{strings.website.tools.playlist.remove}
</Button>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button size={{ initial: "1", xs: "2" }}>
@@ -5,11 +5,7 @@ import {
TankType,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import {
ComponentBooleanIcon,
LockClosedIcon,
LockOpen2Icon,
} from "@radix-ui/react-icons";
import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons";
import {
Box,
Flex,
@@ -71,7 +67,9 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
<img
style={{ width: "1em", height: "1em" }}
src={asset(
`icons/shells/${shellTypeIcons[shells[index]]}${premium ? "_premium" : ""}.webp`
`icons/shells/${shellTypeIcons[shells[index]]}${
premium ? "_premium" : ""
}.webp`
)}
/>
)}
@@ -107,7 +105,9 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
>
<img
src={asset(
`icons/shells/${shellTypeIcons[shellType]}${premium ? "_premium" : ""}.webp`
`icons/shells/${shellTypeIcons[shellType]}${
premium ? "_premium" : ""
}.webp`
)}
style={{ width: "1em", height: "1em" }}
/>
@@ -135,29 +135,13 @@ function OwnershipFilter(props: FlexProps) {
<IconButton
size={size}
disabled={!wargaming}
variant={tankFilters.ownership === "all" ? "solid" : "soft"}
variant={tankFilters.showOwned ? "solid" : "soft"}
radius="none"
color={tankFilters.ownership === "all" ? undefined : "gray"}
color={tankFilters.showOwned ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.ownership = "all";
});
}}
>
<ComponentBooleanIcon />
</IconButton>
<IconButton
size={size}
disabled={!wargaming}
variant={tankFilters.ownership === "owned" ? "solid" : "soft"}
radius="none"
color={tankFilters.ownership === "owned" ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.ownership = "owned";
draft.showOwned = !draft.showOwned;
});
}}
>
@@ -167,13 +151,13 @@ function OwnershipFilter(props: FlexProps) {
<IconButton
size={size}
disabled={!wargaming}
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"}
variant={tankFilters.showUnowned ? "solid" : "soft"}
radius="none"
color={tankFilters.ownership === "unowned" ? undefined : "gray"}
color={tankFilters.showUnowned ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.ownership = "unowned";
draft.showUnowned = !draft.showUnowned;
});
}}
>
@@ -579,27 +563,13 @@ export function FilterControl() {
>
<IconButton
size={size}
variant={tankFilters.testing === "include" ? "solid" : "soft"}
variant={tankFilters.showNonTesting ? "solid" : "soft"}
radius="none"
color={tankFilters.testing === "include" ? undefined : "gray"}
color={tankFilters.showNonTesting ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.testing = "include";
});
}}
>
<ComponentBooleanIcon />
</IconButton>
<IconButton
size={size}
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
radius="none"
color={tankFilters.testing === "exclude" ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.testing = "exclude";
draft.showNonTesting = !draft.showNonTesting;
});
}}
>
@@ -607,15 +577,16 @@ export function FilterControl() {
style={{ width: "1em", height: "1em", color: "currentColor" }}
/>
</IconButton>
<IconButton
size={size}
variant={tankFilters.testing === "only" ? "solid" : "soft"}
variant={tankFilters.showTesting ? "solid" : "soft"}
radius="none"
color={tankFilters.testing === "only" ? undefined : "gray"}
color={tankFilters.showTesting ? undefined : "gray"}
highContrast
onClick={() => {
TankFilters.mutate((draft) => {
draft.testing = "only";
draft.showTesting = !draft.showTesting;
});
}}
>
@@ -487,7 +487,7 @@ export const TankSearch = memo<TankSearchProps>(
)}
</Flex>
{tankFilters.testing === "only" && (
{tankFilters.showTesting && !tankFilters.showNonTesting && (
<Flex justify="center" mt="4">
<Callout.Root color="amber">
<Callout.Icon>
@@ -12,6 +12,7 @@ import {
EyeOpenIcon,
ShadowInnerIcon,
ShadowNoneIcon,
SunIcon,
} from "@radix-ui/react-icons";
import {
Button,
@@ -55,7 +56,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined
);
const display = Tankopedia.use((state) => state.display);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const isFullScreen = useFullScreen();
const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting
@@ -73,6 +74,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const { strings, unwrap } = useLocale();
const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
return (
<>
@@ -84,7 +86,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
gap="2"
direction="column"
top="50%"
right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"}
right={
requestedDisplay === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"
}
style={{
position: "absolute",
transform: "translateY(-50%)",
@@ -296,7 +300,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
>
{disturbed && (
<>
{display === TankopediaDisplay.DynamicArmor && (
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
<IconButton
size="2"
highContrast
@@ -349,6 +353,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</DropdownMenu.Content>
</DropdownMenu.Root>
<IconButton
size="2"
highContrast
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.highGraphics = !draft.highGraphics;
});
}}
color={highGraphics ? undefined : "gray"}
variant={highGraphics ? "solid" : "surface"}
>
<SunIcon />
</IconButton>
<ScreenshotButton
color="gray"
size="2"
@@ -384,11 +402,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex gap="3" align="center" mt="2">
<SegmentedControl.Root
variant="classic"
value={`${disturbed ? display : -1}`}
value={`${disturbed ? requestedDisplay : -1}`}
onValueChange={(value) => {
Tankopedia.mutate((draft) => {
draft.disturbed = true;
draft.display = Number(value);
draft.requestedDisplay = Number(value);
});
}}
>
@@ -430,7 +448,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</SegmentedControl.Item>
</SegmentedControl.Root>
{display === TankopediaDisplay.DynamicArmor && (
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
<Dialog.Root
open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen}
@@ -22,14 +22,12 @@ const poseDistances: Record<Pose, number> = {
const modelDefinitions = await awaitableModelDefinitions;
interface ControlsProps {
naked?: boolean;
autoRotate?: boolean;
zoomable?: boolean;
enableRotate?: boolean;
}
export function Controls({
naked,
autoRotate = true,
zoomable = true,
enableRotate = true,
@@ -79,11 +77,7 @@ export function Controls({
protagonistHullOrigin.y +
protagonistTurretOrigin.y +
protagonistGunOrigin.y;
const inspectModeInitialPosition = new Vector3(
-8,
naked ? gunHeight + 10 : 2,
-13
);
const inspectModeInitialPosition = new Vector3(-8, 2, -13);
const t0 = useRef(Date.now() / 1000);
useFrame(() => {
@@ -236,14 +230,10 @@ export function Controls({
orbitControls.current.rotateSpeed = 0.25;
camera.position.copy(inspectModeInitialPosition);
orbitControls.current.target.set(
0,
gunHeight * (naked ? 1 / 4 : 3 / 4),
0
);
orbitControls.current.target.set(0, gunHeight * 0.6, 0);
orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true;
camera.fov = naked ? 20 : 25;
camera.fov = 25;
camera.updateProjectionMatrix();
}
@@ -1,40 +0,0 @@
import { invalidate, useThree } from "@react-three/fiber";
import { useEffect } from "react";
import type { Fog } from "three";
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
import { Tankopedia } from "../../../../../../stores/tankopedia";
export function InitialFogReveal() {
const fog = useThree((state) => state.scene.fog as Fog | null);
useEffect(() => {
Tankopedia.mutate((draft) => {
draft.revealed = true;
});
if (!fog) return;
const t0 = Date.now();
const interval = setInterval(() => {
const t = (Date.now() - t0) / 1000;
const x = t / fogAnimationTime;
const near = forNear1 + (forNear0 - forNear1) * x;
const far = fogFar1 + (fogFar0 - fogFar1) * x;
fog.near = near;
fog.far = far;
invalidate();
if (x >= 1) {
fog.near = forNear0;
fog.far = fogFar0;
clearInterval(interval);
}
}, 1000 / 60);
return () => clearInterval(interval);
}, []);
return null;
}
@@ -1,82 +1,118 @@
import { useHelper } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useFrame, useThree } from "@react-three/fiber";
import { times } from "lodash-es";
import { useRef, useState } from "react";
import { Color, SpotLight, SpotLightHelper } from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { isHalloween } from "../../../../../../core/blitzkit/isHalloween";
import { Quicklime } from "quicklime";
import { useEffect, useRef } from "react";
import { HemisphereLight, SpotLight, type Group } from "three";
import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
const ANGLE = Math.PI * 2 ** -2;
const REVEAL_ANIMATION_TIME = 3;
const TRANSITION_ANIMATION_TIME = 0.5;
const LIGHTS_COUNT = 5;
const THETA_OFFSET = degToRad(-152);
const LIGHT_DISTANCE = 13;
const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 2;
const INTENSITY = 50;
const INTENSITY_0 = 3.5;
const INTENSITY_1 = 1.5;
const HEMISPHERE_INTENSITY = 2;
const N = 128;
const TARGET_FRAME_RATE = 30;
export const transitionEvent = new Quicklime<number>(0);
export function Lighting() {
const [cast, setCast] = useState(true);
const wrapper = useRef<Group>(null!);
const t0 = useRef(0);
const topLight = useRef<SpotLight>(null!);
const frontLight = useRef<SpotLight>(null!);
const transition = useRef(false);
const clock = useThree((state) => state.clock);
const display = Tankopedia.use((state) => state.display);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const animationTime = useRef(REVEAL_ANIMATION_TIME);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
useHelper(topLight, SpotLightHelper, "red");
useHelper(frontLight, SpotLightHelper, "green");
useEffect(() => {
Tankopedia.mutate((draft) => {
draft.revealed = true;
});
const last = useRef<number>(0);
const samples = useRef<number[]>([]);
t0.current = clock.elapsedTime - REVEAL_ANIMATION_TIME;
}, []);
useFrame(({ clock, frameloop }) => {
if (frameloop === "never") {
useFrame(({ invalidate }) => {
if (transition.current) {
transition.current = false;
t0.current = clock.elapsedTime;
samples.current = [];
return;
}
if (!cast || clock.elapsedTime - t0.current < TARGET_FRAME_RATE * N) {
return;
}
const dt = clock.elapsedTime - last.current;
last.current = clock.elapsedTime;
const x = clamp(
(clock.elapsedTime - t0.current) / animationTime.current,
0,
2
);
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
const fps = 1 / dt;
samples.current.push(fps);
if (t !== transitionEvent.last) {
transitionEvent.dispatch(t);
}
if (samples.current.length > N) samples.current.shift();
for (const child of wrapper.current.children) {
if (child instanceof SpotLight) {
child.angle = ANGLE * t;
} else if (child instanceof HemisphereLight) {
child.intensity = HEMISPHERE_INTENSITY * t;
}
}
const avg =
samples.current.reduce((a, b) => a + b, 0) / samples.current.length;
if (x < 2) invalidate();
if (avg < TARGET_FRAME_RATE) setCast(false);
if (
x > 1 &&
Tankopedia.state.display !== Tankopedia.state.requestedDisplay
) {
Tankopedia.mutate((draft) => {
draft.display = requestedDisplay;
});
}
});
useEffect(() => {
if (requestedDisplay === display) return;
transition.current = true;
animationTime.current = TRANSITION_ANIMATION_TIME;
}, [requestedDisplay]);
return (
<>
<group ref={wrapper}>
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
{times(LIGHTS_COUNT, (index) => {
const x = index / (LIGHTS_COUNT - 1);
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152);
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
const position = [
LIGHT_DISTANCE * Math.sin(theta),
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x,
lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
LIGHT_DISTANCE * Math.cos(theta),
] as const;
const color = new Color();
const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
return (
<HelpingSpotLight
userData={{ index }}
key={index}
position={position}
intensity={INTENSITY}
intensity={intensity}
penumbra={1}
castShadow={cast}
decay={1}
color={color}
castShadow={highGraphics}
decay={0}
color="#ffffff"
angle={0}
/>
);
})}
</>
</group>
);
}
@@ -4,7 +4,7 @@ import { Quicklime, type QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import { Mesh, MeshStandardMaterial } from "three";
const SIZE = 2 ** 5;
const SIZE = 2 ** 6;
export const screenshotReadyEvent = new Quicklime(false);
@@ -36,6 +36,7 @@ export function SceneProps() {
receiveShadow
visible={!screenshotReadyEvent.last!}
ref={mesh}
renderOrder={-1}
>
<planeGeometry args={[SIZE, SIZE]} />
<meshStandardMaterial
@@ -43,7 +44,7 @@ export function SceneProps() {
ref={material}
color="black"
roughness={1}
metalness={1}
metalness={0}
/>
</mesh>
);
@@ -106,7 +106,9 @@ export function TankModel() {
const isTurret = node.name.startsWith("turret_");
const isCurrentTurret =
node.name ===
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
`turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`;
const isVisible = isCurrentTurret;
const position = new Vector2();
const delta = new Vector2();
@@ -1,12 +1,9 @@
import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core";
import { invalidate } from "@react-three/fiber";
import {
forwardRef,
Suspense,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import { Fog } from "three";
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
@@ -29,26 +26,16 @@ import { SmartCanvas } from "../../../../SmartCanvas";
import { AutoClear } from "./components/AutoClear";
import { Controls } from "./components/Control";
import { InitialAligner } from "./components/InitialAligner";
import { InitialFogReveal } from "./components/InitialFogReveal";
import { Lighting } from "./components/Lighting";
import { SceneProps } from "./components/SceneProps";
import { TankModel } from "./components/TankModel";
import { TransitionSkeleton } from "./components/TransitionSkeleton";
interface TankSandboxProps {
thicknessRange: ThicknessRange;
naked?: boolean;
}
export const forNear0 = 30;
export const fogFar0 = 50;
export const forNear1 = 0;
export const fogFar1 = 0;
export const fogAnimationTime = 2.5;
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
({ thicknessRange, naked }, ref) => {
const fog = useRef(new Fog("black", forNear1, fogFar1));
({ thicknessRange }, ref) => {
const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124);
@@ -58,40 +45,11 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const turretModelDefinition =
tankModelDefinition.turrets[protagonistTurret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
const rawDisplay = Tankopedia.use((state) => state.display);
const [display, setDisplay] = useState(rawDisplay);
const display = Tankopedia.use((state) => state.display);
const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor
);
useEffect(() => {
if (rawDisplay === display) return;
const t0 = Date.now();
const interval = setInterval(() => {
const t = (Date.now() - t0) / 1000;
const x = t / fogAnimationTime;
const y = Math.sin(Math.PI * x);
const near = forNear0 + (forNear1 - forNear0) * y;
const far = fogFar0 + (fogFar1 - fogFar0) * y;
fog.current.near = near;
fog.current.far = far;
invalidate();
if (rawDisplay !== display && x >= 0.5) setDisplay(rawDisplay);
if (x >= 1) {
fog.current.near = forNear0;
fog.current.far = fogFar0;
clearInterval(interval);
}
}, 1000 / 60);
return () => clearInterval(interval);
}, [rawDisplay]);
useImperativeHandle(ref, () => canvas.current!, []);
function handlePointerDown() {
@@ -188,10 +146,12 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
}
}, [display]);
console.log(TankopediaDisplay[display]);
return (
<SmartCanvas
ref={canvas}
scene={{ fog: naked ? undefined : fog.current }}
scene={{ fog: new Fog("black", 30, 50) }}
gl={{
clippingPlanes: [],
localClippingEnabled: true,
@@ -207,30 +167,29 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
}}
style={{
userSelect: "none",
width: naked ? BLITZKIT_TANK_ICON_SIZE.width : "100%",
height: naked ? BLITZKIT_TANK_ICON_SIZE.height : "100%",
outline: naked ? "1rem red solid" : undefined,
width: "100%",
height: "100%",
outline: undefined,
}}
>
{!naked && <SceneProps />}
<Lighting />
<SceneProps />
{(display === TankopediaDisplay.Model ||
(display === TankopediaDisplay.DynamicArmor &&
!hideTankModelUnderArmor)) && <TankModel />}
<TransitionSkeleton />
<ShotDisplay />
<ArmorPlateDisplay />
<AutoClear />
<Suspense>
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
<Controls naked={naked} />
<InitialFogReveal />
<Controls />
{display === TankopediaDisplay.DynamicArmor && <Armor />}
{display === TankopediaDisplay.StaticArmor && (
<StaticArmor thicknessRange={thicknessRange} />
)}
<Lighting />
</Suspense>
<InitialAligner />
@@ -1,6 +1,6 @@
import { Box, Flex } from "@radix-ui/themes";
import { times } from "lodash-es";
import { Suspense, useEffect, useMemo, useRef } from "react";
import { useEffect, useMemo, useRef } from "react";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { useFullScreen } from "../../../hooks/useFullScreen";
import { Duel } from "../../../stores/duel";
@@ -99,11 +99,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
isFullScreen
? "100vh"
: disturbed
? "calc(100svh - 8rem)"
: {
initial: "28rem",
md: "calc(100svh - 14rem)",
}
? "calc(100svh - 8rem)"
: {
initial: "28rem",
md: "calc(100svh - 14rem)",
}
}
maxWidth={isFullScreen ? undefined : "120rem"}
flexGrow="1"
@@ -120,30 +120,17 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
position="relative"
>
<Box position="absolute" width="100%" height="100%" overflow="hidden">
<Box
width="100%"
height="100%"
position="relative"
style={{
transitionDuration: "1s",
}}
>
<Box width="100%" height="100%" position="relative">
<Box
style={{
width: "100%",
height: "100%",
transitionDuration: "2s",
opacity: revealed ? 1 : 0,
filter: disturbed
? undefined
: `drop-shadow(0 1rem 1rem black)`,
}}
position="absolute"
width="100%"
height="100%"
top="0"
left="0"
>
<Suspense>
{!skeleton && (
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
)}
</Suspense>
{!skeleton && (
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
)}
</Box>
</Box>
@@ -10,16 +10,14 @@ export async function filterTank(
owned: number[] = []
) {
return (
(filters.ownership === "all" ||
(filters.ownership === "owned" && owned.includes(tank.id)) ||
(filters.ownership === "unowned" && !owned.includes(tank.id))) &&
((filters.showOwned && owned.includes(tank.id)) ||
(filters.showUnowned && !owned.includes(tank.id))) &&
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
(filters.testing === "include" ||
(filters.testing === "only" && tank.testing) ||
(filters.testing === "exclude" && !tank.testing)) &&
((filters.showTesting && tank.testing) ||
(filters.showNonTesting && !tank.testing)) &&
(filters.gunType.length === 0 ||
(filters.gunType.includes("regular") &&
tank.turrets.some((turret) =>
@@ -1,4 +1,6 @@
---
import ToolOpenGraph from "../../../components/ToolOpenGraph.astro";
import { tools } from "../../../constants/tools";
import BaseLayout from "../../../layouts/BaseLayout.astro";
import { Page } from "./_index";
@@ -7,6 +9,8 @@ export { getStaticPaths } from "../index.astro";
const { locale } = Astro.props;
---
<ToolOpenGraph {locale} tool={tools.guess} />
<BaseLayout {locale} noFooter>
<Page {locale} client:only="react">
<Page {locale} skeleton slot="fallback" />
@@ -17,6 +17,7 @@ import {
TankSearch,
type TankSearchProps,
} from "../../../components/TankSearch";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { curateMixer } from "../../../core/blitzkit/curateMixer";
import {
LocaleProvider,
@@ -24,8 +25,13 @@ import {
type LocaleAcceptorProps,
} from "../../../hooks/useLocale";
import { Mixer } from "../../../stores/mixer";
import { Tankopedia } from "../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
const modelDefinition = await awaitableModelDefinitions.then(
({ models }) => models[1]
);
export function Page({
locale,
skeleton,
@@ -33,6 +39,7 @@ export function Page({
const initial = useRef(curateMixer());
Mixer.useInitialization(initial.current);
Tankopedia.useInitialization(modelDefinition);
return (
<PageWrapper color="gray" p="0" maxWidth="unset">
@@ -1,5 +1,4 @@
import { PageWrapper } from "../../../components/PageWrapper";
import { PerformanceInfo } from "../../../components/Performance/PerformanceInfo";
import { PlayerCountControl } from "../../../components/Performance/PlayerCountControl";
import { TankTable } from "../../../components/Performance/Table";
import { FilterControl } from "../../../components/TankSearch/components/FilterControl";
@@ -21,8 +20,7 @@ export function Page({ skeleton, locale }: PageProps) {
function Content({ skeleton }: MaybeSkeletonComponentProps) {
return (
<PageWrapper color="jade" maxWidth="100%">
<PerformanceInfo skeleton={skeleton} />
<PageWrapper pb="0" px="0" color="jade" maxWidth="100%">
<FilterControl />
<PlayerCountControl />
@@ -17,9 +17,7 @@ export function Page({
}: Props & MaybeSkeletonComponentProps) {
const generated = Playlist.use((state) => state.list !== undefined);
if (App.state.logins.wargaming && TankFilters.state.ownership !== "owned") {
TankFilters.state.ownership = "owned";
}
if (App.state.logins.wargaming) TankFilters.state.showUnowned = false;
return (
<LocaleProvider locale={locale}>
+3 -3
View File
@@ -1,5 +1,5 @@
import type { Samples } from '@blitzkit/core';
import { Varuna } from 'varuna';
import type { Samples } from "@blitzkit/core";
import { Varuna } from "varuna";
export interface Performance {
playerCountPeriod: PlayerCountPeriod;
@@ -8,5 +8,5 @@ export interface Performance {
export type PlayerCountPeriod = keyof Samples;
export const Performance = new Varuna<Performance>({
playerCountPeriod: 'd_30',
playerCountPeriod: "total",
});
+10 -4
View File
@@ -17,12 +17,15 @@ export interface TankFilters {
nations: string[];
classes: TankClass[];
types: TankType[];
testing: "include" | "exclude" | "only";
search: string | null;
searching: boolean;
gunType: CaseType<GunDefinition>[];
shells: [ShellType | null, ShellType | null, ShellType | null];
ownership: "all" | "owned" | "unowned";
showTesting: boolean;
showNonTesting: boolean;
showOwned: boolean;
showUnowned: boolean;
}
export const TankFilters = new Varuna<TankFilters>({
@@ -30,10 +33,13 @@ export const TankFilters = new Varuna<TankFilters>({
nations: [],
classes: [],
types: [],
testing: "include",
search: null,
searching: false,
gunType: [],
shells: [null, null, null],
ownership: "all",
showTesting: true,
showNonTesting: true,
showOwned: true,
showUnowned: true,
});
@@ -100,6 +100,7 @@ interface Tankopedia {
);
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
customShell?: ShellDefinition;
requestedDisplay: TankopediaDisplay;
display: TankopediaDisplay;
statSearch?: string;
}
@@ -114,5 +115,6 @@ export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
skills: createDefaultSkills(skillDefinitions),
model,
xpMultiplier: 1,
requestedDisplay: TankopediaDisplay.Model,
display: TankopediaDisplay.Model,
}));
@@ -15,6 +15,7 @@ export interface TankopediaPersistent {
showPrimaryArmor: boolean;
recentlyViewed: number[];
hideTankModelUnderArmor: boolean;
highGraphics: boolean;
sort: {
by: TankopediaSortBy;
@@ -33,6 +34,7 @@ export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
showExternalModules: true,
showPrimaryArmor: true,
recentlyViewed: [],
highGraphics: true,
sort: {
by: "meta.none",
direction: "ascending",