Compare commits

..
7 Commits
Author SHA1 Message Date
tresabhi 1a2d4aaa9c revert colors 2026-10-18 00:32:25 -05:00
tresabhi d7914bb2fb reduce call count 2026-10-18 00:31:53 -05:00
tresabhi 8cb15d488d code split 2026-10-18 00:10:24 -05:00
tresabhi 5c3ab2e10d actual batching 2026-10-18 00:08:43 -05:00
tresabhi 28c30f210e cool 2026-10-18 00:02:48 -05:00
tresabhi 5c92f9f7ee hull 2026-10-17 23:41:23 -05:00
tresabhi 249b185836 proof of concept 2026-10-17 23:21:47 -05:00
140 changed files with 1451 additions and 4127 deletions

No files matched your search

-1
View File
@@ -26,7 +26,6 @@ body:
description: What branch of BlitzKit were you using? Please see the "Branch" section of the footer at the very bottom of any page. description: What branch of BlitzKit were you using? Please see the "Branch" section of the footer at the very bottom of any page.
options: options:
- Production - Production
- Beta
- Preview - Preview
- OpenTest - OpenTest
default: 0 default: 0
+3 -3
View File
@@ -30,8 +30,8 @@ jobs:
- name: Setup bun - name: Setup bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Set up script - name: Install dependencies
run: scripts/setup.sh run: bun install
- name: Setup Git - name: Setup Git
run: | run: |
@@ -39,7 +39,7 @@ jobs:
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Discover IDs - name: Discover IDs
run: scripts/discover-ids.sh run: bun run discover-ids
env: env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }} PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.IDS_TOKEN }} GH_TOKEN: ${{ secrets.IDS_TOKEN }}
+14 -41
View File
@@ -2,11 +2,6 @@ name: Website
on: on:
push: push:
branches:
- main
- production
pull_request:
schedule: schedule:
- cron: "0 4 * * *" - cron: "0 4 * * *"
@@ -89,9 +84,6 @@ env:
# wargaming # wargaming
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }} PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
# workflow
SHOULD_USE_BLITZKIT_CLOSED: ${{ github.event_name != 'pull_request' }}
jobs: jobs:
build: build:
name: Build website name: Build website
@@ -101,7 +93,7 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
chunk: [default, api, models_tanks, models_legendary, models_maps] chunk: [default, api]
steps: steps:
- name: Free disk space - name: Free disk space
@@ -110,19 +102,9 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.pull_request.head.sha || inputs.branch || 'production' }} ref: ${{ inputs.branch || 'production' }}
submodules: false submodules: recursive
token: ${{ secrets.GH_TOKEN || github.token }} token: ${{ secrets.GH_TOKEN }}
# Future proofing for Reforged: we'll have both closed and open repos, so
# we need to handle them here instead of disabling submodules outright
- name: Checkout submodules
run: |
if [ "$SHOULD_USE_BLITZKIT_CLOSED" = "true" ]; then
git submodule update --init --recursive --depth=1
else
git submodule update --init --recursive --depth=1 -- . ':!packages/closed'
fi
- name: Setup bun - name: Setup bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
@@ -137,21 +119,14 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y aria2 libarchive-tools sudo apt-get install -y aria2 libarchive-tools
- name: Set up script - name: Install dependencies
run: scripts/setup.sh run: bun install
- name: Lint - name: Download Client
run: scripts/lint.sh run: bun run download-client
- name: Download client
run: scripts/download-client.sh
- name: Download SmartDLC
if: ${{ env.CLIENT_PROVIDER == 'steam' && env.SHOULD_USE_BLITZKIT_CLOSED == 'true' }}
run: scripts/download-smart-dlc.sh
- name: Build - name: Build
run: scripts/build-website.sh run: bun run --bun build
env: env:
CHUNK: ${{ matrix.chunk }} CHUNK: ${{ matrix.chunk }}
@@ -164,15 +139,13 @@ jobs:
- name: Trigger deployment - name: Trigger deployment
# workflow should trigger if: # Deploy if:
# - is by cron and is on main: triggering to production # 1. Running cron from main using content from production to target production
# - is by push and is on production: triggering to production # 2. Running manually and targeting non-production
# - is by manual and is on anything: triggering to anything
if: >- if: >-
${{ ${{
(github.event_name == 'schedule' && github.ref == 'refs/heads/main') github.ref == 'refs/heads/production' ||
|| (github.event_name == 'push' && github.ref == 'refs/heads/production') (inputs.environment != '' && inputs.environment != 'production')
|| (github.event_name == 'workflow_dispatch')
}} }}
env: env:
+6 -6
View File
@@ -44,16 +44,16 @@ cd blitzkit
> [!NOTE] > [!NOTE]
> There is a closed source submodule under `packages/closed` that you can safely ignore. This sole purpose of this submodule is to provide `dvp.ts`, a proprietary Wargaming game asset tree compression tool. You will not need this at any point. > There is a closed source submodule under `packages/closed` that you can safely ignore. This sole purpose of this submodule is to provide `dvp.ts`, a proprietary Wargaming game asset tree compression tool. You will not need this at any point.
Then, grand execution permission to the setup script. Then, download the dependencies with Bun.
```bash ```bash
chmod +x scripts/setup.sh bun install
``` ```
Finally, run the setup script. Finally, compile the proto files into TypeScript.
```bash ```bash
scripts/setup.sh build build-protos
``` ```
## Developing: Environment ## Developing: Environment
@@ -65,11 +65,11 @@ Go through the different packages in the `packages` directory and clone their re
Launching the website is as easy as running the `dev` script. Launching the website is as easy as running the `dev` script.
```bash ```bash
scripts/dev-website.sh bun dev
``` ```
If you ever make a change to any proto file, make sure to rebuild them. If you ever make a change to any proto file, make sure to rebuild them.
```bash ```bash
scripts/build-protos.sh build build-protos
``` ```
+6 -1
View File
@@ -55,6 +55,7 @@
"packages/scripts": { "packages/scripts": {
"name": "@blitzkit/scripts", "name": "@blitzkit/scripts",
"dependencies": { "dependencies": {
"@blitzkit/closed": "workspace:*",
"@blitzkit/core": "workspace:*", "@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*", "@blitzkit/i18n": "workspace:*",
"@gltf-transform/core": "^4.2.1", "@gltf-transform/core": "^4.2.1",
@@ -1642,6 +1643,8 @@
"jimp": ["[email protected]", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], "jimp": ["[email protected]", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
"jpeg-js": ["[email protected]", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], "jpeg-js": ["[email protected]", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -1948,7 +1951,7 @@
"potpack": ["[email protected]", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="], "potpack": ["[email protected]", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
"prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="], "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"pretty-bytes": ["[email protected]", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="], "pretty-bytes": ["[email protected]", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
@@ -2730,6 +2733,8 @@
"wrap-ansi/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "wrap-ansi/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"yaml-language-server/prettier": ["[email protected]", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
"yaml-language-server/request-light": ["[email protected]", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/request-light": ["[email protected]", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
"yaml-language-server/vscode-languageserver": ["[email protected]", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="], "yaml-language-server/vscode-languageserver": ["[email protected]", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],
+47
View File
@@ -0,0 +1,47 @@
# BlitzKit Credits
BlitzKit is a one man band **TrèsAbhi** but I am far from solo. Helpers, in the order of contribution over time:
### S0AP
BlitzKit Discord bot verification, testing, advertisement, and the initial encouragement to create BlitzKit.
### Prince_NA
BlitzKit Discord bot and website testing and general advise.
### Vovko
BlitzKit Discord bot and website rendering pipeline.
### Maddox
Game file decompression.
### Pyogenics
Game 3D model parsing.
### Aeson
Game file layout, general advise, advertisement, and Tankopedia pre-release testing.
### Minitelrose, ChickenMan7777
3-caliber rule edge cases.
### HisRoyalFatness, Synx
Embed UX.
### [OPTML] clan, \_34, xkdkw, Droodles Blitz
Tankopedia pre-release testing.
### GonnaHetzMe
Redacted by request.
### Mark Greene
GitHub contributor on bug fixes and feature additions.
+3 -3
View File
@@ -1,14 +1,14 @@
# BlitzKit 1.29.10 # BlitzKit 1.29.10
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features ## Features
- Combined game mode passive and active abilities into a single dropdown - Combined game mode passive and active abilities into a single dropdown
- Added tier filters to the Guess game [HitJack] - Added tier filters to the Guess game [Mark Greene]
- Changing tiers will break your streak - Changing tiers will break your streak
## Fixes ## Fixes
- module damage is now properly shown when sorting with it in the tank search [HitJack] - module damage is now properly shown when sorting with it in the tank search [Mark Greene]
- Duplicate abilities in the tank filters are now removed - Duplicate abilities in the tank filters are now removed
+2 -2
View File
@@ -1,6 +1,6 @@
# BlitzKit 1.29.8 # BlitzKit 1.29.8
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features ## Features
@@ -15,5 +15,5 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
## Fixes ## Fixes
- The Tankopedia now smoothly resizes when enlarging on interaction - The Tankopedia now smoothly resizes when enlarging on interaction
- Stock turrets now correctly mount the corresponding stock gun [HitJack] - Stock turrets now correctly mount the corresponding stock gun [Mark Greene]
- The consumable and provision buttons in Compare are no longer tiny - The consumable and provision buttons in Compare are no longer tiny
+3 -3
View File
@@ -1,14 +1,14 @@
# BlitzKit 1.29.9 # BlitzKit 1.29.9
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features ## Features
- Reworked tank search filters to be far more compact to make way for new filters - Reworked tank search filters to be far more compact to make way for new filters
- Added the ability to filter tanks by consumables [HitJack] - Added the ability to filter tanks by consumables [Mark Greene]
- Added the ability to filter tanks by provisions and game-mode abilities and powers - Added the ability to filter tanks by provisions and game-mode abilities and powers
## Fixes ## Fixes
- Consumable and provision icons in Compare are now properly sized - Consumable and provision icons in Compare are now properly sized
- Provisions now properly affect terrain resistances [HitJack] - Provisions now properly affect terrain resistances [Mark Greene]
+5 -5
View File
@@ -1,6 +1,6 @@
# BlitzKit 1.30.7 # BlitzKit 1.30.7
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features ## Features
@@ -8,13 +8,13 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
## Fixes ## Fixes
- Clicking on the armor multiple times with HE no longer crashes BlitzKit [HitJack] - Clicking on the armor multiple times with HE no longer crashes BlitzKit [Mark Greene]
- Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration [HitJack] - Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration [Mark Greene]
- The lighting animation no longer stops prematurely when BlitzKit is unfocused - The lighting animation no longer stops prematurely when BlitzKit is unfocused
- The spot light angle has been increased to more evenly light up larger tanks - The spot light angle has been increased to more evenly light up larger tanks
- BlitzKit once again uses soft shadows, reducing the pixelated shadows - BlitzKit once again uses soft shadows, reducing the pixelated shadows
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows - Fixed the weird patterns that formed on the tracks of tanks under self-shadows
- Increased the floor size and light decay to create the illusion that the floor is infinite - Increased the floor size and light decay to create the illusion that the floor is infinite
- Safari no longer crashes when unloading textures for unloaded tanks [HitJack] - Safari no longer crashes when unloading textures for unloaded tanks [Mark Greene]
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0 - The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
- Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [HitJack] - Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [Mark Greene]
+34 -34
View File
@@ -1,75 +1,75 @@
# BlitzKit 1.32.0 # BlitzKit 1.32.0
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update. Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features ## Features
### Tankopedia & Armor ### Tankopedia & Armor
- Added a spall liner button to the Tankopedia beside the other equipment [HitJack] - Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene]
- Listed track repair times in the Tankopedia [HitJack] - Listed track repair times in the Tankopedia [Mark Greene]
- The penetration chance is now displayed when penetration testing armor [HitJack] - The penetration chance is now displayed when penetration testing armor [Mark Greene]
- Added a new setting to disable automatically loading tanks in Tankopedia [HitJack] - Added a new setting to disable automatically loading tanks in Tankopedia [Mark Greene]
### Tank Performance & Mechanics ### Tank Performance & Mechanics
- Added Peekaboo DPM [HitJack] - Added Peekaboo DPM [Mark Greene]
- Your Peekaboo DPM is lower than your normal DPM if your reload is faster than 10s as you'll have to wait for the remaining 10s for you to get unspotted before you can poke and shoot - Your Peekaboo DPM is lower than your normal DPM if your reload is faster than 10s as you'll have to wait for the remaining 10s for you to get unspotted before you can poke and shoot
### Tank Filters ### Tank Filters
- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [HitJack] - The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [Mark Greene]
### Game Compatibility ### Game Compatibility
- Added compatibility for tier XI tanks [HitJack] - Added compatibility for tier XI tanks [Mark Greene]
## Fixes ## Fixes
### Tankopedia & Armor ### Tankopedia & Armor
- The armor no longer disappears when you turn on Dynamic Armor System [HitJack] - The armor no longer disappears when you turn on Dynamic Armor System [Mark Greene]
- Camouflage values in Tankopedia are now more descriptive when shooting [HitJack] - Camouflage values in Tankopedia are now more descriptive when shooting [Mark Greene]
- The penetration chance is now displayed when penetration testing armor [HitJack] - The penetration chance is now displayed when penetration testing armor [Mark Greene]
- Listed track repair times in the Tankopedia [HitJack] - Listed track repair times in the Tankopedia [Mark Greene]
- Added a spall liner button to the Tankopedia beside the other equipment [HitJack] - Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene]
- The flags in the Tankopedia tech tree are no longer offset to the top left [HitJack] - The flags in the Tankopedia tech tree are no longer offset to the top left [Mark Greene]
- The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below [HitJack] - The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below [Mark Greene]
- Removed the vertical sliver of pixels found to the right of the Equalizer toggle in Tankopedia - Removed the vertical sliver of pixels found to the right of the Equalizer toggle in Tankopedia
### Tank Search & Filters ### Tank Search & Filters
- The search bar no longer redirects to a seemingly random tank when clicking enter when searching [HitJack] - The search bar no longer redirects to a seemingly random tank when clicking enter when searching [Mark Greene]
- Tank search now properly handles the full names of tanks like the Super Pershing [HitJack] - Tank search now properly handles the full names of tanks like the Super Pershing [Mark Greene]
- The tank search now remains consistent in order even when an added filter does not add/remove tanks from the list [HitJack] - The tank search now remains consistent in order even when an added filter does not add/remove tanks from the list [Mark Greene]
- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [HitJack] - The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [Mark Greene]
### Visuals & UI ### Visuals & UI
- Removed the Beta badge from the Equalizer button [HitJack] - Removed the Beta badge from the Equalizer button [Mark Greene]
- Updated the background image for the Equalizer button to the actual banner of the new game mode - Updated the background image for the Equalizer button to the actual banner of the new game mode
- The contact shadows on the ground no longer grow & darken when switching between tabs [HitJack] - The contact shadows on the ground no longer grow & darken when switching between tabs [Mark Greene]
- Additionally, the contact shadows stays visible at low angles [HitJack] - Additionally, the contact shadows stays visible at low angles [Mark Greene]
- The "Help Translate BlitzKit" text on the home page is now properly centered vertically - The "Help Translate BlitzKit" text on the home page is now properly centered vertically
- The footer now says "Branch" instead of "CDN" [HitJack] - The footer now says "Branch" instead of "CDN" [Mark Greene]
- The game version in the footer now displays a shorthand version instead of the entire version number [HitJack] - The game version in the footer now displays a shorthand version instead of the entire version number [Mark Greene]
- The reload visualizer now displays just 1 decimal place [HitJack] - The reload visualizer now displays just 1 decimal place [Mark Greene]
- Plot controls for the Static armor tab no longer pokes in from the right on non-Static armor tabs in some languages [HitJack] - Plot controls for the Static armor tab no longer pokes in from the right on non-Static armor tabs in some languages [Mark Greene]
- Mixer's controllers now scale better on smaller screens [HitJack] - Mixer's controllers now scale better on smaller screens [Mark Greene]
- The list of available tanks in the Playlist is no longer excessively wide [HitJack] - The list of available tanks in the Playlist is no longer excessively wide [Mark Greene]
### Tank Mechanics & Models ### Tank Mechanics & Models
- The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro [HitJack] - The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro [Mark Greene]
- When moving the tracks of tanks, the textures no longer move at different rates [HitJack] - When moving the tracks of tanks, the textures no longer move at different rates [Mark Greene]
### Localization ### Localization
- Users with a preference on a language are once again redirected to the correct localized pages [HitJack] - Users with a preference on a language are once again redirected to the correct localized pages [Mark Greene]
### Session & Game Compatibility ### Session & Game Compatibility
- BlitzKit Session now properly handles tanks removed from the game [HitJack] - BlitzKit Session now properly handles tanks removed from the game [Mark Greene]
## Technical Fixes ## Technical Fixes
@@ -78,7 +78,7 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
- This was the last bug standing in the way of BlitzKit Previews - This was the last bug standing in the way of BlitzKit Previews
- Welcome back, BlitzKit Previews! - Welcome back, BlitzKit Previews!
- Removed some dead code from the BlitzKit model mangler - Removed some dead code from the BlitzKit model mangler
- The player discovery loop now persists for longer than 1 iteration [HitJack] - The player discovery loop now persists for longer than 1 iteration [Mark Greene]
- Bun is now forced to run scripts with the `--bun` flag - Bun is now forced to run scripts with the `--bun` flag
## Technical Features ## Technical Features
@@ -94,5 +94,5 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
- This was the last bug standing in the way of BlitzKit Previews - This was the last bug standing in the way of BlitzKit Previews
- Welcome back, BlitzKit Previews! - Welcome back, BlitzKit Previews!
- Removed some dead code from the BlitzKit model mangler - Removed some dead code from the BlitzKit model mangler
- The player discovery loop now persists for longer than 1 iteration [HitJack] - The player discovery loop now persists for longer than 1 iteration [Mark Greene]
- Bun is now forced to run scripts with the `--bun` flag - Bun is now forced to run scripts with the `--bun` flag
-21
View File
@@ -1,21 +0,0 @@
# BlitzKit 1.33.0
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
## Features
- Added legendary camouflages to Tankopedia [HitJack]
- The rank bars below every statistic in Tankopedia now shows the distribution of every other tank [HitJack]
- Added a Credits page for everyone who has contributed to BlitzKit
- Added an option to synchronize shells across tanks in Compare [HitJack]
## Fixes
- The tank selector popups are now properly sized vertically, stopping them from clipping the top of the page [HitJack]
- When the lights recede, there is no longer a 1-pixel spot where the light still shines
## Technical Changes
- The BlitzKit API is now split across `legendary.models.blitzkit.app`, `tanks.models.blitzkit.app`, `maps.models.blitzkit.app`, and `api.models.blitzkit.app`
- Added some compatibility for the new tier XI tanks
- More work is needed in this area to implement the new mechanics they bring
+3 -22
View File
@@ -4,33 +4,14 @@
"paths": { "paths": {
"default": null, "default": null,
"api": null
"api": null,
"models_tanks": null,
"models_legendary": null,
"models_maps": null
}, },
"nodes": { "nodes": {
"default": "blitzkit/deploy-node-beta.blitzkit.app", "default": "blitzkit/deploy-node-beta.blitzkit.app",
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
"api": "blitzkit/deploy-node-api.beta.blitzkit.app",
"models_tanks": "blitzkit/deploy-node-tanks.models.beta.blitzkit.app",
"models_legendary": "blitzkit/deploy-node-legendary.models.beta.blitzkit.app",
"models_maps": "blitzkit/deploy-node-maps.models.beta.blitzkit.app"
}, },
"aliases": { "aliases": {
"default": "/",
"api": "https://api.beta.blitzkit.app", "api": "https://api.beta.blitzkit.app",
"static": "https://static.beta.blitzkit.app"
"models_tanks": "https://tanks.models.beta.blitzkit.app",
"models_legendary": "https://legendary.models.beta.blitzkit.app",
"models_maps": "https://maps.models.beta.blitzkit.app",
"static": "https://static.blitzkit.app"
} }
} }
+1 -15
View File
@@ -4,25 +4,11 @@
"paths": { "paths": {
"default": null, "default": null,
"api": "api"
"api": "api",
"models_tanks": "models/tanks",
"models_legendary": "models/legendary",
"models_maps": "models/maps"
}, },
"nodes": {}, "nodes": {},
"aliases": { "aliases": {
"default": "/",
"api": "/api", "api": "/api",
"models_tanks": "/models/tanks",
"models_legendary": "/models/legendary",
"models_maps": "/models/maps",
"static": "https://static.blitzkit.app" "static": "https://static.blitzkit.app"
} }
} }
+3 -22
View File
@@ -4,33 +4,14 @@
"paths": { "paths": {
"default": null, "default": null,
"api": null
"api": null,
"models_tanks": null,
"models_legendary": null,
"models_maps": null
}, },
"nodes": { "nodes": {
"default": "blitzkit/deploy-node-opentest.blitzkit.app", "default": "blitzkit/deploy-node-opentest.blitzkit.app",
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app",
"models_tanks": "blitzkit/deploy-node-tanks.models.opentest.blitzkit.app",
"models_legendary": "blitzkit/deploy-node-legendary.models.opentest.blitzkit.app",
"models_maps": "blitzkit/deploy-node-maps.models.opentest.blitzkit.app"
}, },
"aliases": { "aliases": {
"default": "/",
"api": "https://api.opentest.blitzkit.app", "api": "https://api.opentest.blitzkit.app",
"static": "https://static.opentest.blitzkit.app"
"models_tanks": "https://tanks.models.opentest.blitzkit.app",
"models_legendary": "https://legendary.models.opentest.blitzkit.app",
"models_maps": "https://maps.models.opentest.blitzkit.app",
"static": "https://static.blitzkit.app"
} }
} }
+3 -22
View File
@@ -4,33 +4,14 @@
"paths": { "paths": {
"default": null, "default": null,
"api": null
"api": null,
"models_tanks": null,
"models_legendary": null,
"models_maps": null
}, },
"nodes": { "nodes": {
"default": "blitzkit/deploy-node-preview.blitzkit.app", "default": "blitzkit/deploy-node-preview.blitzkit.app",
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
"api": "blitzkit/deploy-node-api.preview.blitzkit.app",
"models_tanks": "blitzkit/deploy-node-tanks.models.preview.blitzkit.app",
"models_legendary": "blitzkit/deploy-node-legendary.models.preview.blitzkit.app",
"models_maps": "blitzkit/deploy-node-maps.models.preview.blitzkit.app"
}, },
"aliases": { "aliases": {
"default": "/",
"api": "https://api.preview.blitzkit.app", "api": "https://api.preview.blitzkit.app",
"static": "https://static.preview.blitzkit.app"
"models_tanks": "https://tanks.models.preview.blitzkit.app",
"models_legendary": "https://legendary.models.preview.blitzkit.app",
"models_maps": "https://maps.models.preview.blitzkit.app",
"static": "https://static.blitzkit.app"
} }
} }
+2 -21
View File
@@ -4,33 +4,14 @@
"paths": { "paths": {
"default": null, "default": null,
"api": null
"api": null,
"models_tanks": null,
"models_legendary": null,
"models_maps": null
}, },
"nodes": { "nodes": {
"default": "blitzkit/deploy-node-blitzkit.app", "default": "blitzkit/deploy-node-blitzkit.app",
"api": "blitzkit/deploy-node-api.blitzkit.app"
"api": "blitzkit/deploy-node-api.blitzkit.app",
"models_tanks": "blitzkit/deploy-node-tanks.models.blitzkit.app",
"models_legendary": "blitzkit/deploy-node-legendary.models.blitzkit.app",
"models_maps": "blitzkit/deploy-node-maps.models.blitzkit.app"
}, },
"aliases": { "aliases": {
"default": "/",
"api": "https://api.blitzkit.app", "api": "https://api.blitzkit.app",
"models_tanks": "https://tanks.models.blitzkit.app",
"models_legendary": "https://legendary.models.blitzkit.app",
"models_maps": "https://maps.models.blitzkit.app",
"static": "https://static.blitzkit.app" "static": "https://static.blitzkit.app"
} }
} }
+10 -1
View File
@@ -1,8 +1,17 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"version": "1.33.0", "version": "1.32.0",
"license": "BlitzKit License", "license": "BlitzKit License",
"private": true, "private": true,
"scripts": {
"dev": "cd packages/website && bun --bun run dev",
"build": "cd packages/website && bun run build",
"discover-ids": "cd packages/scripts && bun run discover-ids",
"build-protos": "cd packages/scripts && bun run build-protos",
"record-performance": "cd packages/scripts && bun run record-performance",
"download-client": "cd packages/scripts && bun run download-client",
"clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite"
},
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -246,27 +246,6 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
return this; return this;
} }
protected getTankFilterIds(
name: string,
definitionKey: string,
filter: "include" | "exclude",
) {
const keys = name.trim().split(/\s+/).filter(Boolean);
return keys.flatMap((key) => {
const id = this.tankStringIdMap[key];
if (id === undefined) {
console.warn(
`Skipping unavailable tank "${key}" in ${filter} vehicle filter for "${definitionKey}"`,
);
return [];
}
return [id];
});
}
protected getString(name: string) { protected getString(name: string) {
const collection: Record<string, string> = { const collection: Record<string, string> = {
[locales.default]: this.stringsI18n[locales.default][name], [locales.default]: this.stringsI18n[locales.default][name],
@@ -285,8 +264,6 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
collection[locale] = localizedString; collection[locale] = localizedString;
} }
collection[locales.default] = collection[locales.default] ?? name;
return { locales: collection } satisfies I18nString; return { locales: collection } satisfies I18nString;
} }
} }
@@ -387,7 +387,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
: TankType.TANK_TYPE_RESEARCHABLE, : TankType.TANK_TYPE_RESEARCHABLE,
tier: tank.level, tier: tank.level,
class: tankClass, class: tankClass,
testing: tankTags.includes("testTank") || tank.level === 11, testing: tankTags.includes("testTank"),
deprecated: tankTags.includes("deprecated"), deprecated: tankTags.includes("deprecated"),
price: tankPrice, price: tankPrice,
camouflage_still: tankDefinition.root.invisibility.still, camouflage_still: tankDefinition.root.invisibility.still,
@@ -595,25 +595,9 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
for (const shellKey in gunListEntry.shots) { for (const shellKey in gunListEntry.shots) {
const gunShellEntry = gunListEntry.shots[shellKey]; const gunShellEntry = gunListEntry.shots[shellKey];
const shell = shellList.root[shellKey]; const shell = shellList.root[shellKey];
// Newer client data stores shot properties on the shell rather
// than repeating them under every gun's <shots> entry.
const speed =
typeof gunShellEntry === "string"
? shell.speed
: gunShellEntry.speed;
const maxDistance =
typeof gunShellEntry === "string"
? shell.maxDistance
: gunShellEntry.maxDistance;
const piercingPower =
typeof gunShellEntry === "string"
? shell.piercingPower
: gunShellEntry.piercingPower;
const shellId = toUniqueId(nation, shell.id); const shellId = toUniqueId(nation, shell.id);
const shellName = this.getString(shell.userString); const shellName = this.getString(shell.userString);
const penetrationRaw = piercingPower const penetrationRaw = gunShellEntry.piercingPower
.split(" ") .split(" ")
.filter((penetrationString) => penetrationString !== "") .filter((penetrationString) => penetrationString !== "")
.map(Number); .map(Number);
@@ -623,7 +607,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
].shells.push({ ].shells.push({
id: shellId, id: shellId,
name: shellName, name: shellName,
velocity: speed, velocity: gunShellEntry.speed,
armor_damage: shell.damage.armor, armor_damage: shell.damage.armor,
module_damage: shell.damage.devices, module_damage: shell.damage.devices,
caliber: shell.caliber, caliber: shell.caliber,
@@ -639,7 +623,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
near: penetrationRaw[0], near: penetrationRaw[0],
far: penetrationRaw[1], far: penetrationRaw[1],
}, },
range: maxDistance, range: gunShellEntry.maxDistance,
}); });
} }
@@ -1,60 +1,15 @@
import { import { CamouflageDefinitions } from "@blitzkit/core";
CamouflageDefinitions,
Sc2ReadStream,
type TankParameters,
} from "@blitzkit/core";
import { isEqual, times } from "lodash-es";
import { Cache } from "./0_base"; import { Cache } from "./0_base";
import { ServerBlitzKitAPI1 } from "./1_tankDefinitions"; import { ServerBlitzKitAPI1 } from "./1_tankDefinitions";
export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 { export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
private async skinPresets(vehicle: string) {
const [nation, tankKey] = vehicle.split(":");
const parameters = await this.vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
);
const sc2 = new Sc2ReadStream(
(
await this.vfs.file(
`Data/3d/${parameters.resourcesPath.blitzModelPath}`,
)
).buffer as ArrayBuffer,
).sc2();
const presets = new Set<string>();
for (const node of sc2["#dataNodes"]) {
if (typeof node.configCount !== "number") continue;
times(node.configCount, (index) => node[`configArchive_${index}`])
.filter(
(archive) =>
!isEqual(archive.textures, node.configArchive_0.textures),
)
.forEach((archive) => presets.add(archive.configName));
}
return presets;
}
@Cache() @Cache()
async camouflageDefinitions() { async camouflageDefinitions() {
const camouflageDefinitions = CamouflageDefinitions.create(); const camouflageDefinitions = CamouflageDefinitions.create();
const customization = await this.vfs.yaml<
Record<string, { Name: string; Path: string }>
>("Data/3d/Customization.yaml");
const items = new Map(
Object.values(customization).map(({ Name, Path }) => [
Name,
Path.replace("~res:/", "Data/").replace(/\.sc2$/, ""),
]),
);
const skinPresets = new Map<string, Promise<Set<string>>>();
for (const camoKey in this.camouflagesXml!.root.camouflages) { for (const camoKey in this.camouflagesXml!.root.camouflages) {
const camo = this.camouflagesXml!.root.camouflages[camoKey]; const camo = this.camouflagesXml!.root.camouflages[camoKey];
if (!(camoKey in this.camouflagesYaml!)) continue;
const yamlEntry = this.camouflagesYaml![camoKey]; const yamlEntry = this.camouflagesYaml![camoKey];
const fullName = yamlEntry.userString const fullName = yamlEntry.userString
? this.getString(yamlEntry.userString) ? this.getString(yamlEntry.userString)
@@ -65,57 +20,12 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
const resolvedTankName = shortName ?? fullName; const resolvedTankName = shortName ?? fullName;
const resolvedTankNameFull = const resolvedTankNameFull =
resolvedTankName === fullName ? undefined : fullName; resolvedTankName === fullName ? undefined : fullName;
const include = camo.vehicleFilter.include;
const vehicle =
include && !Array.isArray(include) && "vehicle" in include
? include.vehicle?.name
: undefined;
let tankId: number | undefined = undefined;
if (vehicle && vehicle in this.tankStringIdMap) {
const resolvedParts = await Promise.all(
Object.values(yamlEntry.customEntities ?? {}).map(
async ({ item }) => {
const path = items.get(item);
return (
path !== undefined &&
(await this.vfs.resolve(`${path}.sc2`)) &&
(await this.vfs.resolve(`${path}.scg`))
);
},
),
);
const [nation, tankKey] = vehicle.split(":");
// some unique camos only set the preset for their one tank
const preset =
yamlEntry.preset ??
yamlEntry.override?.[nation]?.override?.[tankKey]?.preset;
let hasPreset = false;
if (preset !== undefined) {
if (!skinPresets.has(vehicle)) {
skinPresets.set(vehicle, this.skinPresets(vehicle));
}
hasPreset = (await skinPresets.get(vehicle)!).has(preset);
}
if (resolvedParts.some(Boolean) || hasPreset) {
tankId = this.tankStringIdMap[vehicle];
}
}
const name = this.getString(camo.userString);
camouflageDefinitions.camouflages[camo.id] = { camouflageDefinitions.camouflages[camo.id] = {
id: camo.id, id: camo.id,
name, name: this.getString(camo.userString),
tank_name: resolvedTankName, tank_name: resolvedTankName,
tank_name_full: resolvedTankNameFull, tank_name_full: resolvedTankNameFull,
tank_id: tankId,
attachment: camo.kind === "customization",
}; };
} }
@@ -4,13 +4,7 @@ import {
ChassisDefinitionsList, ChassisDefinitionsList,
Crew, Crew,
GunDefinitionsList, GunDefinitionsList,
I_HAT,
J_HAT,
K_HAT,
ModelDefinitions, ModelDefinitions,
parseHierarchies,
Sc2ReadStream,
ScgReadStream,
TankParameters, TankParameters,
toUniqueId, toUniqueId,
TurretDefinitionsList, TurretDefinitionsList,
@@ -18,10 +12,9 @@ import {
VehicleDefinitionArmor, VehicleDefinitionArmor,
VehicleDefinitionList, VehicleDefinitionList,
VehicleDefinitions, VehicleDefinitions,
VertexAttribute,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { parse as parsePath } from "path"; import { parse as parsePath } from "path";
import { MathUtils, Vector3 as THREEVector3, Vector3Tuple } from "three"; import { Vector3Tuple } from "three";
import { Cache } from "./0_base"; import { Cache } from "./0_base";
import { ServerBlitzKitAPI2 } from "./2_camouflageDefinitions"; import { ServerBlitzKitAPI2 } from "./2_camouflageDefinitions";
@@ -30,81 +23,6 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3; return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3;
} }
private async collisionBoundingBoxes(
nation: string,
tankKey: string,
turretRotation?: { yaw: number; pitch: number; roll: number },
) {
const boundingBoxes: Record<
string,
{ min: Vector3Tuple; max: Vector3Tuple }
> = {};
const path = `Data/3d/Tanks/CollisionMeshes/${nation}-${tankKey}`;
if (!(await this.vfs.resolve(`${path}.sc2`))) {
throw new Error(`Missing collision mesh for ${nation}-${tankKey}`);
}
const sc2 = new Sc2ReadStream(
(await this.vfs.file(`${path}.sc2`)).buffer as ArrayBuffer,
).sc2();
const scg = new ScgReadStream(
(await this.vfs.file(`${path}.scg`)).buffer as ArrayBuffer,
).scg();
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
for (const component of components) {
if (component["comp.typename"] !== "RenderComponent") continue;
const polygonGroup = scg.get(
component["rc.renderObj"]["ro.batches"]["0000"]["rb.datasource"],
);
if (!polygonGroup) continue;
const min: Vector3Tuple = [Infinity, Infinity, Infinity];
const max: Vector3Tuple = [-Infinity, -Infinity, -Infinity];
const rotate =
turretRotation && hierarchy.name.toLowerCase().startsWith("turret");
polygonGroup.vertices.forEach((vertex) => {
vertex.forEach(({ attribute, value }) => {
if (attribute !== VertexAttribute.VERTEX) return;
const position = new THREEVector3(...value);
if (rotate) {
position
.applyAxisAngle(
I_HAT,
-MathUtils.degToRad(turretRotation.pitch),
)
.applyAxisAngle(J_HAT, -MathUtils.degToRad(turretRotation.roll))
.applyAxisAngle(K_HAT, -MathUtils.degToRad(turretRotation.yaw));
}
const coordinates = position.toArray();
for (let axis = 0; axis < 3; axis++) {
min[axis] = Math.min(min[axis], coordinates[axis]);
max[axis] = Math.max(max[axis], coordinates[axis]);
}
});
});
boundingBoxes[hierarchy.name.toLowerCase()] = { min, max };
}
return null;
});
if (!boundingBoxes.hull) {
throw new Error(`Missing hull collision mesh for ${nation}-${tankKey}`);
}
return boundingBoxes;
}
private assignArmor( private assignArmor(
raw: VehicleDefinitionArmor[string], raw: VehicleDefinitionArmor[string],
id: number, id: number,
@@ -149,12 +67,6 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
const tankParameters = await this.vfs.yaml<TankParameters>( const tankParameters = await this.vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
); );
const collisionBoundingBoxes = await this.collisionBoundingBoxes(
nation,
tankKey,
tankDefinition.root.hull.turretInitialRotation,
);
const hullBoundingBox = collisionBoundingBoxes.hull;
const turretOrigin = tankDefinition.root.hull.turretPositions.turret const turretOrigin = tankDefinition.root.hull.turretPositions.turret
.split(" ") .split(" ")
.map(Number) as Vector3Tuple; .map(Number) as Vector3Tuple;
@@ -229,8 +141,12 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
} }
: undefined, : undefined,
bounding_box: { bounding_box: {
min: this.vector3TupleToBlitzkit(hullBoundingBox.min), min: this.vector3TupleToBlitzkit(
max: this.vector3TupleToBlitzkit(hullBoundingBox.max), tankParameters.collision.hull.bbox.min,
),
max: this.vector3TupleToBlitzkit(
tankParameters.collision.hull.bbox.max,
),
}, },
turrets: {}, turrets: {},
tracks: {}, tracks: {},
@@ -292,41 +208,26 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
turret.userString; turret.userString;
let turretCollisionName = parsePath(
turret.hitTester.collisionModel,
).name.toLowerCase();
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
turretCollisionName = "turret_01";
console.warn(
`HACK: Missing turret collision mesh for ${nation}/${tankKey}; turret key: ${
turretKey
}; collision model: ${turretCollisionName}; available options: ${Object.keys(
collisionBoundingBoxes,
).join(", ")}; using ${turretCollisionName} instead`,
);
}
let turretBoundingBox = collisionBoundingBoxes[turretCollisionName];
if (turretBoundingBox === undefined) {
console.warn("HACK: missing collision; using 0's");
turretBoundingBox = { min: [0, 0, 0], max: [0, 0, 0] };
}
modelDefinitions.models[tankId].turrets[turretId] = { modelDefinitions.models[tankId].turrets[turretId] = {
armor: turretArmor, armor: turretArmor,
gun_origin: this.vector3TupleToBlitzkit(gunOrigin), gun_origin: this.vector3TupleToBlitzkit(gunOrigin),
model_id: turretModel, model_id: turretModel,
yaw: isFullYaw yaw:
? undefined -turretYaw[0] + turretYaw[1] === 360
: { min: turretYaw[0], max: turretYaw[1] }, ? undefined
: { min: turretYaw[0], max: turretYaw[1] },
guns: {}, guns: {},
bounding_box: { bounding_box: {
min: this.vector3TupleToBlitzkit(turretBoundingBox.min), min: this.vector3TupleToBlitzkit(
max: this.vector3TupleToBlitzkit(turretBoundingBox.max), tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.min,
),
max: this.vector3TupleToBlitzkit(
tankParameters.collision[
parsePath(turret.hitTester.collisionModel).name.toLowerCase()
].bbox.max,
),
}, },
}; };
@@ -342,18 +243,9 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
) )
.split(" ") .split(" ")
.map(Number) as [number, number]; .map(Number) as [number, number];
let gunModel = Number( const gunModel = Number(
parsePath(gun.models.undamaged).name.split("_")[1], parsePath(gun.models.undamaged).name.split("_")[1],
); );
if (tankId === 30497) {
console.warn(
"HACK: using gun_02 for hacker tank instead of gun_01... hehe, that's quite appropriate for this tank haha",
);
gunModel = 2;
}
const front = gun.extraPitchLimits?.front const front = gun.extraPitchLimits?.front
? gun.extraPitchLimits.front.split(" ").map(Number) ? gun.extraPitchLimits.front.split(" ").map(Number)
: undefined; : undefined;
@@ -44,7 +44,9 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
filter_type: { filter_type: {
$case: "ids", $case: "ids",
value: { value: {
ids: this.getTankFilterIds(includeRaw.name, key, "include"), ids: includeRaw.name.split(/ +/).map((key) => {
return this.tankStringIdMap[key];
}),
}, },
}, },
}); });
@@ -70,7 +72,9 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
filter_type: { filter_type: {
$case: "ids", $case: "ids",
value: { value: {
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"), ids: excludeRaw.name.split(/ +/).map((key) => {
return this.tankStringIdMap[key];
}),
}, },
}, },
}); });
@@ -11,7 +11,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
async provisionDefinitions() { async provisionDefinitions() {
const provisionDefinitions = ProvisionDefinitions.create(); const provisionDefinitions = ProvisionDefinitions.create();
Object.entries(this.provisionsCommon).forEach(([key, provision]) => { Object.entries(this.provisionsCommon).forEach(([, provision]) => {
const entry: Provision = { const entry: Provision = {
id: provision.id, id: provision.id,
exclude: [], exclude: [],
@@ -42,7 +42,9 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
filter_type: { filter_type: {
$case: "ids", $case: "ids",
value: { value: {
ids: this.getTankFilterIds(includeRaw.name, key, "include"), ids: includeRaw.name.split(/ +/).map((key) => {
return this.tankStringIdMap[key];
}),
}, },
}, },
}); });
@@ -68,7 +70,9 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
filter_type: { filter_type: {
$case: "ids", $case: "ids",
value: { value: {
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"), ids: excludeRaw.name
.split(/ +/)
.map((key) => this.tankStringIdMap[key]),
}, },
}, },
}); });
-1
View File
@@ -25,7 +25,6 @@ export * from "./sluggify";
export * from "./staticAsset"; export * from "./staticAsset";
export * from "./sum"; export * from "./sum";
export * from "./tankDefinitions"; export * from "./tankDefinitions";
export * from "./tankBoundingBox";
export * from "./tankIcon"; export * from "./tankIcon";
export * from "./tankNames"; export * from "./tankNames";
export * from "./unionBoundingBox"; export * from "./unionBoundingBox";
@@ -1,32 +0,0 @@
import { ModelDefinition, TurretModelDefinition } from "../protos";
import { unionBoundingBox } from "./unionBoundingBox";
export function tankBoundingBox(
tankModelDefinition: ModelDefinition,
turretModelDefinition: TurretModelDefinition,
) {
const hull = tankModelDefinition.bounding_box!;
const turret = turretModelDefinition.bounding_box!;
const origin = tankModelDefinition.turret_origin!;
if (
turret.min!.x === turret.max!.x &&
turret.min!.y === turret.max!.y &&
turret.min!.z === turret.max!.z
) {
return unionBoundingBox(hull, hull);
}
return unionBoundingBox(hull, {
min: {
x: turret.min!.x + origin.x,
y: turret.min!.y + origin.z,
z: turret.min!.z + origin.y,
},
max: {
x: turret.max!.x + origin.x,
y: turret.max!.y + origin.z,
z: turret.max!.z + origin.y,
},
});
}
@@ -1,5 +1,5 @@
import { times } from "lodash-es"; import { times } from "lodash-es";
import { CrewType, ShellType } from "../../protos"; import { CrewType, ShellType, TankClass, TankType } from "../../protos";
export const SHELL_NAMES: Record<ShellType, string> = { export const SHELL_NAMES: Record<ShellType, string> = {
[ShellType.SHELL_TYPE_AP]: "AP", [ShellType.SHELL_TYPE_AP]: "AP",
@@ -15,6 +15,33 @@ export const CREW_MEMBER_NAMES = {
[CrewType.CREW_TYPE_RADIOMAN]: "radioman", [CrewType.CREW_TYPE_RADIOMAN]: "radioman",
} as const; } as const;
/**
* @deprecated use svg
*/
export const TANK_ICONS: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/BIHSEH0.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/CSNha5V.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/wvf3ltm.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/ECeqlZa.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/TCu3EdR.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/zdkpTRb.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/3z7eHX6.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/P3vbmyA.png",
};
/**
* @deprecated use svg
*/
export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/WTjeirB.png",
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/EwhtKkU.png",
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/u8YDMBh.png",
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
};
export const TIERS = times(10, (index) => index + 1); export const TIERS = times(10, (index) => index + 1);
export const TIER_ROMAN_NUMERALS: Record<number, string> = { export const TIER_ROMAN_NUMERALS: Record<number, string> = {
1: "I", 1: "I",
@@ -41,3 +68,8 @@ export const flags: Record<string, string> = {
european: "🇪🇺", european: "🇪🇺",
other: "<:other:1218421572243558482>", other: "<:other:1218421572243558482>",
}; };
export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = {
[TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS,
[TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM,
[TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR,
};
+50
View File
@@ -0,0 +1,50 @@
export enum ImgurSize {
/**
* The default size
*/
Default = '',
/**
* Unknown size but really really small
*/
UltraSmall = 'ss',
/**
* 90x90
*/
SmallSquare = 's',
/**
* 160x160
*/
BigSquare = 'b',
/**
* 160x160
*/
SmallThumbnail = 't',
/**
* 320x320
*/
MediumThumbnail = 'm',
/**
* 640x640
*/
LargeThumbnail = 'l',
/**
* 1024x1024
*/
HugeThumbnail = 'h',
}
interface ImgurOptions {
format: 'jpeg' | 'png';
size: ImgurSize;
}
const defaultImgurOptions: ImgurOptions = {
format: 'png',
size: ImgurSize.Default,
};
export function imgur(id: string, imgurOptions?: Partial<ImgurOptions>) {
const options = { ...defaultImgurOptions, ...imgurOptions };
return `https://i.imgur.com/${id}${options.size}.${options.format}`;
}
+1
View File
@@ -0,0 +1 @@
export * from './imgur';
+1
View File
@@ -1,5 +1,6 @@
export * from "./blitz"; export * from "./blitz";
export * from "./blitzkit"; export * from "./blitzkit";
export * from "./imgur";
export * from "./math"; export * from "./math";
export * from "./patreon"; export * from "./patreon";
export * from "./protobuf"; export * from "./protobuf";
@@ -13,6 +13,4 @@ message Camouflage {
required I18nString name = 2; required I18nString name = 2;
optional I18nString tank_name = 3; optional I18nString tank_name = 3;
optional I18nString tank_name_full = 4; optional I18nString tank_name_full = 4;
optional uint32 tank_id = 5;
optional bool attachment = 6;
} }
@@ -24,8 +24,6 @@ export interface Camouflage {
name: I18nString | undefined; name: I18nString | undefined;
tank_name?: I18nString | undefined; tank_name?: I18nString | undefined;
tank_name_full?: I18nString | undefined; tank_name_full?: I18nString | undefined;
tank_id?: number | undefined;
attachment?: boolean | undefined;
} }
function createBaseCamouflageDefinitions(): CamouflageDefinitions { function createBaseCamouflageDefinitions(): CamouflageDefinitions {
@@ -193,7 +191,7 @@ export const CamouflageDefinitions_CamouflagesEntry: MessageFns<CamouflageDefini
}; };
function createBaseCamouflage(): Camouflage { function createBaseCamouflage(): Camouflage {
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined, tank_id: 0, attachment: false }; return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined };
} }
export const Camouflage: MessageFns<Camouflage> = { export const Camouflage: MessageFns<Camouflage> = {
@@ -210,12 +208,6 @@ export const Camouflage: MessageFns<Camouflage> = {
if (message.tank_name_full !== undefined) { if (message.tank_name_full !== undefined) {
I18nString.encode(message.tank_name_full, writer.uint32(34).fork()).join(); I18nString.encode(message.tank_name_full, writer.uint32(34).fork()).join();
} }
if (message.tank_id !== undefined && message.tank_id !== 0) {
writer.uint32(40).uint32(message.tank_id);
}
if (message.attachment !== undefined && message.attachment !== false) {
writer.uint32(48).bool(message.attachment);
}
return writer; return writer;
}, },
@@ -258,22 +250,6 @@ export const Camouflage: MessageFns<Camouflage> = {
message.tank_name_full = I18nString.decode(reader, reader.uint32()); message.tank_name_full = I18nString.decode(reader, reader.uint32());
continue; continue;
} }
case 5: {
if (tag !== 40) {
break;
}
message.tank_id = reader.uint32();
continue;
}
case 6: {
if (tag !== 48) {
break;
}
message.attachment = reader.bool();
continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
@@ -289,8 +265,6 @@ export const Camouflage: MessageFns<Camouflage> = {
name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined, name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined,
tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined, tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined,
tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined, tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined,
tank_id: isSet(object.tank_id) ? globalThis.Number(object.tank_id) : 0,
attachment: isSet(object.attachment) ? globalThis.Boolean(object.attachment) : false,
}; };
}, },
@@ -308,12 +282,6 @@ export const Camouflage: MessageFns<Camouflage> = {
if (message.tank_name_full !== undefined) { if (message.tank_name_full !== undefined) {
obj.tank_name_full = I18nString.toJSON(message.tank_name_full); obj.tank_name_full = I18nString.toJSON(message.tank_name_full);
} }
if (message.tank_id !== undefined && message.tank_id !== 0) {
obj.tank_id = Math.round(message.tank_id);
}
if (message.attachment !== undefined && message.attachment !== false) {
obj.attachment = message.attachment;
}
return obj; return obj;
}, },
@@ -332,8 +300,6 @@ export const Camouflage: MessageFns<Camouflage> = {
message.tank_name_full = (object.tank_name_full !== undefined && object.tank_name_full !== null) message.tank_name_full = (object.tank_name_full !== undefined && object.tank_name_full !== null)
? I18nString.fromPartial(object.tank_name_full) ? I18nString.fromPartial(object.tank_name_full)
: undefined; : undefined;
message.tank_id = object.tank_id ?? 0;
message.attachment = object.attachment ?? false;
return message; return message;
}, },
}; };
+1 -26
View File
@@ -88,7 +88,6 @@ enum PvrChannelType {
enum ResolvedBitRatePixelFormat { enum ResolvedBitRatePixelFormat {
R4G4B4A4, R4G4B4A4,
R8G8B8A8,
R5G6B5A0, R5G6B5A0,
} }
@@ -138,27 +137,6 @@ export class PvrReadStream extends ReadStream {
}; };
} }
case ResolvedBitRatePixelFormat.R8G8B8A8: {
const data = Buffer.alloc(pixelCount * 4);
times(pixelCount, (index) => {
const buffer = this.consumeUint8Array(4);
const bufferIndex = index * 4;
data[bufferIndex] = buffer[0]; // R
data[bufferIndex + 1] = buffer[1]; // G
data[bufferIndex + 2] = buffer[2]; // B
data[bufferIndex + 3] = buffer[3]; // A
});
return {
data,
width: header.width,
height: header.height,
channels: 4 as const,
};
}
case ResolvedBitRatePixelFormat.R5G6B5A0: { case ResolvedBitRatePixelFormat.R5G6B5A0: {
const data = Buffer.alloc(pixelCount * 3); const data = Buffer.alloc(pixelCount * 3);
@@ -230,13 +208,10 @@ export class PvrReadStream extends ReadStream {
if (pixelFormat.order === "rgba") { if (pixelFormat.order === "rgba") {
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) { if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
return ResolvedBitRatePixelFormat.R4G4B4A4; return ResolvedBitRatePixelFormat.R4G4B4A4;
} else if (isEqual(pixelFormat.bitRates, [8, 8, 8, 8])) { } else
return ResolvedBitRatePixelFormat.R8G8B8A8;
} else {
throw new TypeError( throw new TypeError(
`Unhandled rgba bit rate ${pixelFormat.bitRates.join(", ")}`, `Unhandled rgba bit rate ${pixelFormat.bitRates.join(", ")}`,
); );
}
} else if (pixelFormat.order === "rgb\0") { } else if (pixelFormat.order === "rgb\0") {
if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) { if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) {
return ResolvedBitRatePixelFormat.R5G6B5A0; return ResolvedBitRatePixelFormat.R5G6B5A0;
+1 -29
View File
@@ -40,8 +40,6 @@ export interface ConfigArchive {
interface EnabledPresets { interface EnabledPresets {
AlphaTest: boolean; AlphaTest: boolean;
AlphaBlend?: boolean;
AlphaBlendAdditive?: boolean;
} }
interface Flags { interface Flags {
@@ -92,28 +90,6 @@ export interface Hierarchy {
name: string; name: string;
} }
export function parseHierarchies<Parent>(
hierarchies: Hierarchy[],
parent: Parent,
parseHierarchy: (
hierarchy: Hierarchy,
components: Component[],
parent: Parent,
) => Parent | undefined,
) {
for (const hierarchy of hierarchies) {
const components = Array.from(
{ length: hierarchy.components.count },
(_, index) => hierarchy.components[index.toString().padStart(4, "0")],
);
const child = parseHierarchy(hierarchy, components, parent);
if (child !== undefined && hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy["#hierarchy"], child, parseHierarchy);
}
}
}
// 0001, 0002, 0003...: Component // 0001, 0002, 0003...: Component
type Components = { type Components = {
count: number; count: number;
@@ -158,11 +134,7 @@ export type Component =
"ro.notShadowOnly": boolean; "ro.notShadowOnly": boolean;
"ro.sOclCull": boolean; "ro.sOclCull": boolean;
"ro.sOclIndex": number; "ro.sOclIndex": number;
} & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number> & } & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number>;
Record<
`skinnedObject.batch${number}.targetsData`,
ArrayBuffer | undefined
>;
} }
| { | {
"comp.typename": "SlotComponent"; "comp.typename": "SlotComponent";
@@ -3,12 +3,5 @@ export type CamouflagesYaml = Record<
{ {
userString?: string; userString?: string;
shortUserString?: string; shortUserString?: string;
preset?: string;
customEntities?: Record<string, { item: string }>;
// nation -> tank
override?: Record<
string,
{ override?: Record<string, { preset?: string }> }
>;
} }
>; >;
@@ -20,13 +20,11 @@ export interface GunDefinitionsList {
burst?: { count: number; rate: number }; burst?: { count: number; rate: number };
clip?: { count: number; rate: number }; clip?: { count: number; rate: number };
shots: { shots: {
[key: string]: [key: string]: {
| "" speed: number;
| { piercingPower: string;
speed: number; maxDistance: number;
piercingPower: string; };
maxDistance: number;
};
}; };
}; };
}; };
@@ -10,9 +10,6 @@ export type ShellDefinitionsList = Record<
normalizationAngle?: number; normalizationAngle?: number;
ricochetAngle?: number; ricochetAngle?: number;
explosionRadius?: number; explosionRadius?: number;
speed: number;
maxDistance?: number;
piercingPower: string;
} }
> & { > & {
icons: Record<string, string>; icons: Record<string, string>;
+10 -11
View File
@@ -1,4 +1,10 @@
{ {
"test": {
"default": "test",
"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}.",
"fox": "The quick brown fox jumps over the lazy dog."
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
@@ -38,7 +44,7 @@
"branches": { "branches": {
"beta": "Beta", "beta": "Beta",
"development": "Post Malone Edition", "development": "Development",
"opentest": "OpenTest", "opentest": "OpenTest",
"preview": "Preview", "preview": "Preview",
"production": "Production" "production": "Production"
@@ -332,13 +338,6 @@
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "Reset", "reset": "Reset",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -744,8 +743,7 @@
"none": "No deltas", "none": "No deltas",
"percentage": "Percentage", "percentage": "Percentage",
"absolute": "Absolute" "absolute": "Absolute"
}, }
"sync_shells": "Sync shells"
}, },
"no_tanks": { "no_tanks": {
@@ -994,10 +992,11 @@
"footer": { "footer": {
"motto": "Everything World of Tanks Blitz", "motto": "Everything World of Tanks Blitz",
"association": "An Open Collective Project", "copyright": "Unaffiliated with Wargaming or any service",
"about": { "about": {
"title": "About", "title": "About",
"credits": "Credits",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "Patches" "patches": "Patches"
+6 -6
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "test",
"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}.",
"fox": "El rápido zorro marrón salta sobre el perro perezoso."
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -302,12 +307,6 @@
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "Restablecer", "reset": "Restablecer",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -923,6 +922,7 @@
"copyright": "No afiliado con Wargaming u otros servicios", "copyright": "No afiliado con Wargaming u otros servicios",
"about": { "about": {
"title": "About", "title": "About",
"credits": "Credits",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "Parches" "patches": "Parches"
+6 -6
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "teste",
"long": "Ceci est une longue chaîne de test. Vous ne DEVEZ JAMAIS le voir dans l'interface utilisateur. Mon fruit préféré est {fruit}, car il a la couleur {color}. Je l'aime tellement que je vais dire son nom 3 fois ! {fruit}! {fruit}! {fruit} ! Encore une fois, c'est {color}, pas {not_color}.",
"fox": "Le renard brun et rapide saute par-dessus le chien paresseux."
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -302,12 +307,6 @@
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "Reset", "reset": "Reset",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -923,6 +922,7 @@
"copyright": "Unaffiliated with Wargaming or any service", "copyright": "Unaffiliated with Wargaming or any service",
"about": { "about": {
"title": "About", "title": "About",
"credits": "Credits",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "Patches" "patches": "Patches"
+6 -6
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "test",
"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}.",
"fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。"
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -302,12 +307,6 @@
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "リセット", "reset": "リセット",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -923,6 +922,7 @@
"copyright": "Wargamingやその他のサービスとは関係ありません", "copyright": "Wargamingやその他のサービスとは関係ありません",
"about": { "about": {
"title": "概要", "title": "概要",
"credits": "クレジット",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "パッチノート" "patches": "パッチノート"
+236 -236
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "test",
"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}.",
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -14,7 +19,7 @@
"gun_types": { "gun_types": {
"auto_loader": "Samo ładowacz", "auto_loader": "Samo ładowacz",
"auto_reloader": "Automatyczne przeładowanie", "auto_reloader": "Automatyczne przeładowanie",
"regular": "Regularny" "regular": "Regular"
}, },
"shells": { "shells": {
"ap": "AP", "ap": "AP",
@@ -33,10 +38,10 @@
}, },
"branches": { "branches": {
"beta": "Beta", "beta": "Beta",
"development": "Rozwój", "development": "Development",
"opentest": "OtwartyTest", "opentest": "OpenTest",
"preview": "Przegląd", "preview": "Preview",
"production": "Produkcja" "production": "Production"
}, },
"locales": { "locales": {
"en": "Angielski", "en": "Angielski",
@@ -97,28 +102,28 @@
"other": "Hybryda" "other": "Hybryda"
}, },
"tank_class_short": { "tank_class_short": {
"-1": "nierozpoznany", "-1": "unrecognized",
"0": "Lekki", "0": "Lekki",
"1": "Średni", "1": "Średni",
"2": "Ciężki", "2": "Ciężki",
"3": "NC" "3": "TD"
}, },
"tank_class_short_plural_lowercase": { "tank_class_short_plural_lowercase": {
"0": "lekkie", "0": "lekkie",
"1": "średnie", "1": "średnie",
"2": "ciężkie", "2": "ciężkie",
"3": "NC-ów" "3": "TDs"
}, },
"tank_class_medium": { "tank_class_medium": {
"-1": "nierozpoznany", "-1": "unrecognized",
"0": "Lekki", "0": "Lekki",
"1": "Średni", "1": "Średni",
"2": "Ciężki", "2": "Ciężki",
"3": "Niszczyciel czołgów" "3": "Niszczyciel czołgów"
}, },
"tree_type": { "tree_type": {
"-1": "nierozpoznany", "-1": "unrecognized",
"0": "Drzewko technologiczne", "0": "Tech tree",
"1": "Premium", "1": "Premium",
"2": "Kolekcjonerski" "2": "Kolekcjonerski"
}, },
@@ -182,229 +187,223 @@
}, },
"website": { "website": {
"redirect": { "redirect": {
"title": "301: przeniesiony na stałe", "title": "301: moved permanently",
"body": "Ta strona została przeniesiona. Przekierowywanie..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Ustawienia", "title": "Settings",
"description": "Zmień ustawienia BlitzKit", "description": "Change BlitzKit settings",
"dev_mode": "Tryb deweloperski", "dev_mode": "Development mode",
"auto_load_models": "Automatycznie wczytaj modele", "auto_load_models": "Auto-load models",
"language": "Język" "language": "Language"
}, },
"common": { "common": {
"player_search": { "player_search": {
"hint": "Szukaj graczy...", "hint": "Search players...",
"no_results": "Brak wyników", "no_results": "No results",
"searching": "Wyszukiwanie..." "searching": "Searching..."
}, },
"copy_button": { "copy_button": {
"copied": "Skopiowano!" "copied": "Copied!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Szukaj czołgów", "select_dialog_title": "Search for tanks",
"swap_dialog_title": "Zamień czołg", "swap_dialog_title": "Swap tank",
"clear_filters": "Wyczyść filtry", "clear_filters": "Clear filters",
"login": "Zaloguj się, aby tego użyć", "login": "Login to use this",
"search_bar_hint": "Szukaj czołgów...", "search_bar_hint": "Search tanks...",
"recent": "Ostatnio oglądane", "recent": "Recently viewed",
"select_all": "Zaznacz wszystko", "select_all": "Select all",
"count_singular": "1 czołg", "count_singular": "1 tank",
"count_plural": "{count} czołgi", "count_plural": "{count} tanks",
"sorting_by": "Sortowanie według {name}", "sorting_by": "Sorting by {name}",
"clear": "Wyczyść", "clear": "Clear",
"tier": "Poziom czołgu {tier}", "tier": "Tier {tier}",
"passive": "{name} (Pasywny)", "passive": "{name} (Passive)",
"active": "{name} (Aktywne)", "active": "{name} (Active)",
"no_tanks_found": { "no_tanks_found": {
"body": "Nie znaleziono czołgu.", "body": "No tanks found.",
"search_again": "", "search_again": "",
"clear_filters": "Spróbuj wyczyścić filtry." "clear_filters": "Try clearing filters."
}, },
"sort": { "sort": {
"meta.none": "brak", "meta.none": "none",
"survivability.health": "punkty.życia", "survivability.health": "health",
"survivability.viewRange": "zasięg widzenia", "survivability.viewRange": "view range",
"survivability.camouflageStill": "kamuflaż w bezruchu", "survivability.camouflageStill": "camouflage still",
"survivability.camouflageMoving": "kamuflaż w trakcie ruchu", "survivability.camouflageMoving": "camouflage moving",
"survivability.camouflageShooting": "kamuflaż podczas strzału", "survivability.camouflageShooting": "camouflage shooting",
"survivability.volume": "wartość", "survivability.volume": "volume",
"survivability.length": "długość", "survivability.length": "length",
"fire.dpm": "DPM pocisków podstawowych", "fire.dpm": "standard DPM",
"fire.dpmPremium": "DPM pocisków premium", "fire.dpmPremium": "premium DPM",
"fire.reload": "przeładowanie", "fire.reload": "reload",
"fire.caliber": "kaliber", "fire.caliber": "caliber",
"fire.standardPenetration": "Penetracja pocisków standardowych", "fire.standardPenetration": "standard penetration",
"fire.premiumPenetration": "Penetracja pocisków premium", "fire.premiumPenetration": "premium penetration",
"fire.tertiaryPenetration": "tertiary penetration", "fire.tertiaryPenetration": "tertiary penetration",
"fire.damage": "uszkodzenia", "fire.damage": "damage",
"fire.moduleDamage": "Uszkodzenia modułów", "fire.moduleDamage": "module damage",
"fire.shellVelocity": "Prędkość pocisku", "fire.shellVelocity": "shell velocity",
"fire.shellRange": "Zasięg pocisku", "fire.shellRange": "shell range",
"fire.shellCapacity": "Maksymalna ilość pocisków", "fire.shellCapacity": "shell capacity",
"fire.aimTime": "Czas celowania", "fire.aimTime": "aim time",
"fire.dispersionStill": "dispersion still", "fire.dispersionStill": "dispersion still",
"fire.dispersionMoving": "dispersion moving", "fire.dispersionMoving": "dispersion moving",
"fire.gunDepression": "Depresja działa", "fire.gunDepression": "gun depression",
"fire.gunElevation": "Elewacja działa", "fire.gunElevation": "gun elevation",
"maneuverability.forwardsSpeed": "Maksymalna prędkość do przodu", "maneuverability.forwardsSpeed": "forwards speed",
"maneuverability.backwardsSpeed": "Maksymalna prędkość do tyłu", "maneuverability.backwardsSpeed": "backwards speed",
"maneuverability.power": "Moc silnika", "maneuverability.power": "power",
"maneuverability.powerToWeight": "Proporcja mocy do masy", "maneuverability.powerToWeight": "power to weight",
"maneuverability.weight": "Masa czołgu", "maneuverability.weight": "weight",
"maneuverability.traverseSpeed": "traverse speed" "maneuverability.traverseSpeed": "traverse speed"
}, },
"sort_dropdown": { "sort_dropdown": {
"label": "Filtruj", "label": "Sort",
"groups": { "groups": {
"sort": { "sort": {
"no_sort": "Brak filtrów", "no_sort": "No sort",
"label": "Filtruj według", "label": "Sort by",
"fire": "Siła ognia", "fire": "Fire",
"maneuverability": "Mobilność ", "maneuverability": "Maneuverability",
"survivability": "Przetrwanie" "survivability": "Survivability"
}, },
"direction": { "direction": {
"label": "Direction", "label": "Direction",
"ascending": "Rosnąco", "ascending": "Ascending",
"descending": "Malejąco" "descending": "Descending"
} }
} }
} }
}, },
"warnings": { "warnings": {
"test": "Wartości statystyk czołgów w fazie testowej mogą ulec zmianie." "test": "Tanks in testing are subject to change and may not represent the final product."
} }
}, },
"home": { "home": {
"discord": { "discord": {
"join_short": "Dołącz do elity!", "join_short": "Join cool people!",
"join_long": "Dołącz do topowej elity!", "join_long": "Join super duper cool people!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "Według {name} ♥️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Popularne czołgi", "title": "Popular tanks",
"subtitle": "Ostatnie {days} dni" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "Inne epickie projekty" "title": "Other cool projects"
}, },
"help_translate": "Proszę pomóż przetłumaczyć {name} na {language}!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "Narzędzia do wszystkiego World of Tanks Blitz" "seo_description": "Tools for everything World of Tanks Blitz"
}, },
"docs": { "docs": {
"patches": { "patches": {
"name": "Zapisy zmian BlitzKit", "name": "BlitzKit Changelogs",
"seo_description": "Zapisy zmian BlitzKit: Discord bot i strona internetowa" "seo_description": "Changelogs for BlitzKit Discord bot and website"
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "Resetuj", "reset": "Reset",
"shuffle": "Wymieszaj", "shuffle": "Shuffle",
"remove": "Usuń czołgi, którymi grałem", "remove": "Remove played",
"tracking": "BlitzKit zapisuje twoje bitwy", "tracking": "BlitzKit is tracking your battles",
"sort": { "sort": {
"button": "Filtruj według", "button": "Sort by",
"last_played": "Ostatnio używane czołgi", "last_played": "Last played",
"least_played": "Najmniej używane czołgi" "least_played": "Least played"
}, },
"table": { "table": {
"index": "#", "index": "#",
"never": "Nigdy", "never": "Never",
"tank": "Czołg", "tank": "Tank",
"last_played": "Ostatnio używane czołgi", "last_played": "Last played",
"battles": "Wszystkie bitwy na koncie", "battles": "Career battles",
"unrecorded": "--", "unrecorded": "--",
"wins": "Zwycięstwa" "wins": "Zwycięstwa"
}, },
"large_warning": { "large_warning": {
"title": "To jest mnóstwo czołgów!", "title": "That's a lot of tanks!",
"description": "Czy na pewno chcesz stworzyć playlistę złożoną z {count} czołgów? To {time} godzin gry przy 50% zwycięstw!", "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
"cancel": "Anuluj", "cancel": "Cancel",
"proceed": "Zatwierdź" "proceed": "Proceed"
}, },
"generate": "Wygeneruj playlistę", "generate": "Generate playlist",
"name": "Playlista", "name": "Playlist",
"description": "Generator playlisty czołgów", "description": "Tank playlist generator",
"seo_description": "Nie wiesz, którym czołgiem grać w World of Tanks Blitz? Użyj funkcji playlisty BlitzKit, i ułóż własną listę czołgów do gry!", "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
"button": "Wygeneruj", "button": "Generate",
"pool_size": "{count} możliwe czołgi" "pool_size": "{count} possible tanks"
}, },
"mixer": { "mixer": {
"name": "Mikser", "name": "Mixer",
"button": "Mix and match", "button": "Mix and match",
"description": "Mix tank modules", "description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz", "seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Zrzut ekranu", "screenshot": "Screenshot",
"randomize": "Wymieszaj" "randomize": "Randomize"
}, },
"guess": { "guess": {
"name": "Zgadnij", "name": "Guess",
"button": "What could it be?", "button": "What could it be?",
"description": "Silhouette mini-game", "description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": { "search": {
"placeholder": "Wpisz odpowiedź", "placeholder": "Type to guess...",
"skip": "Pomiń", "skip": "Skip",
"guess": "Zgaduję", "guess": "Guess",
"next": "Następny", "next": "Next",
"no_results": "Brak wyników" "no_results": "Brak wyników"
}, },
"stats": "{correct} / {total} correct, {streak} streak", "stats": "{correct} / {total} correct, {streak} streak",
"help": { "help": {
"title": "Odsłonić część czołgu?", "title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Anuluj", "cancel": "Cancel",
"reveal": "Pokaż" "reveal": "Reveal"
}, },
"clear": "Wyczyść", "clear": "Clear",
"select_all": "Zaznacz wszystko ", "select_all": "Select all",
"tier": "Poziom czołgu {tier}" "tier": "Tier {tier}"
}, },
"tankopedia": { "tankopedia": {
"search": "Wyszukaj statystykę...", "search": "Search a stat...",
"name": "Tankopedia", "name": "Tankopedia",
"button": "Znajdź czołg", "button": "Find tanks",
"description": "Encyklopedia czołgów w WoTb", "description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz", "seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.", "tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"visualizers": { "visualizers": {
"reload": { "reload": {
"skip": "Pomiń" "skip": "Skip"
}, },
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
"rotator": { "rotator": {
"hull": "Kadłub", "hull": "Hull",
"turret": "Wieża", "turret": "Turret",
"hard": "Utwardzony", "hard": "Hard",
"medium": "Średni", "medium": "Średni",
"soft": "Miękki", "soft": "Soft",
"effective": "Rzeczywisty" "effective": "Effective"
}, },
"ricochet": { "ricochet": {
"angle": "Kąt", "angle": "Angle",
"normalization": "Normalization", "normalization": "Normalization",
"effective_angle": "Rzeczywisty kąt nachylenia pancerza", "effective_angle": "Effective angle",
"armor": "Pancerz", "armor": "Armor",
"no_ricochet": "Pociski odłamkowe nie rykoszetują." "no_ricochet": "Explosive shells don't ricochet"
}, },
"view_range": { "view_range": {
"camo": "Kamuflaż", "camo": "Camo",
"ghost_factory": "Fabryka duchów", "ghost_factory": "Ghost Factory",
"dead_rail": "Martwy tor" "dead_rail": "Dead Rail"
}, },
"flexibility": { "flexibility": {
"yaw": "Yaw", "yaw": "Yaw",
@@ -444,71 +443,71 @@
"ricochet": "Ricochet", "ricochet": "Ricochet",
"explosion_radius": "Explosion radius", "explosion_radius": "Explosion radius",
"exit": "Exit", "exit": "Exit",
"reset": "Zresetuj" "reset": "Reset"
}, },
"advanced": "Zaawansowane podświetlanie", "advanced": "Advanced highlighting",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "Główny", "primary": "Primary",
"spaced": "Spaced", "spaced": "Spaced",
"external": "Zewnętrzny", "external": "External",
"gap": "Odstęp", "gap": "Gap",
"gun": "Działo", "gun": "Gun",
"track": "Gąsienice" "track": "Tracks"
}, },
"status": { "status": {
"penetration": "{damage} penetracja punktów wytrzymałości", "penetration": "{damage}hp penetration",
"blocked": "Zablokowany", "blocked": "Blocked",
"ricochet": "Rykoszet", "ricochet": "Ricochet",
"splash": "{damage} rozprysk punktów wytrzymałości" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "{thickness}mm {angle}", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominalny", "nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetracji" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
"static": { "static": {
"name": "Pancerz statyczny", "name": "Static armor",
"primary": "Główny Pancerz", "primary": "Primary",
"spaced": "Ekran przeciwkumulacyjny", "spaced": "Spaced",
"external": "Zewnętrzny", "external": "External",
"edit": { "edit": {
"label": "Edytuj", "label": "Edit",
"reset": "Zresetuj" "reset": "Reset"
} }
} }
}, },
"meta": { "meta": {
"back": "Powrót", "back": "Back",
"compare": "Porównaj", "compare": "Compare",
"swap": { "swap": {
"button": "Zamień", "button": "Swap",
"title": "Zamień czołgi" "title": "Swap tanks"
}, },
"vanilla": "Vanilla", "vanilla": "Vanilla",
"opentest": "OpenTest", "opentest": "OpenTest",
"full_name": "Pełna nazwa", "full_name": "Full name",
"nation": "Naród", "nation": "Naród",
"class": "Klasa", "class": "Klasa",
"tier": "Poziom", "tier": "Poziom",
"type": "Rodzaj", "type": "Rodzaj",
"dev_id": "DEV: ID", "dev_id": "DEV: ID",
"dev_name": "DEV: NAZWA", "dev_name": "DEV: NAME",
"purchase": "Cena zakupu", "purchase": "Purchase price",
"restoration": "Cena odzyskania", "restoration": "Restoration price",
"sale": "Sale price", "sale": "Sale price",
"research": "Research XP" "research": "Research XP"
}, },
"tech_tree": { "tech_tree": {
"title": "Drzewko technologiczne", "title": "Tech tree",
"battle": "1 bitwa", "battle": "1 battle",
"battles": "{count} bitew", "battles": "{count} battles",
"route": "Ścieżka {index} z {count}" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "Umiejętności trybu gry" "title": "Game mode abilities"
}, },
"review": { "review": {
"title": "Review videos", "title": "Review videos",
@@ -521,26 +520,26 @@
"equalize": "Equalize" "equalize": "Equalize"
}, },
"modules": { "modules": {
"title": "Moduły", "title": "Modules",
"stock": "Standardowy", "stock": "Stock",
"upgrade": "Ulepsz" "upgrade": "Upgrade"
}, },
"equipment": { "equipment": {
"title": "Wyposażenie", "title": "Equipment",
"clear": "Wyczyść" "clear": "Clear"
}, },
"provisions": { "provisions": {
"title": "Materiały eksploatacyjne", "title": "Provisions",
"clear": "Wyczyść" "clear": "Clear"
}, },
"consumables": { "consumables": {
"title": "Consumables", "title": "Consumables",
"clear": "Wyczyść", "clear": "Clear",
"info": "Yellow consumables do not affect the statistics." "info": "Yellow consumables do not affect the statistics."
}, },
"skills": { "skills": {
"title": "Crew skills", "title": "Crew skills",
"clear": "Wyczyść", "clear": "Clear",
"maximize": "Maximize", "maximize": "Maximize",
"info": "Yellow skills only apply under special circumstances. Hold Space to quickly toggle between level 0 and 7." "info": "Yellow skills only apply under special circumstances. Hold Space to quickly toggle between level 0 and 7."
}, },
@@ -558,49 +557,49 @@
}, },
"values": { "values": {
"shell_index": "Shell {index}", "shell_index": "Shell {index}",
"at_distance": "Z odległości {distance}m", "at_distance": "At {distance}m",
"at_distance_slider_label": "Zasięg widzenia", "at_distance_slider_label": "Distance",
"dispersion_at_distance": "Rozrzut na {distance}m", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Elastyczność działa", "gun_flexibility": "Gun flexibility",
"speed": "Prędkość", "speed": "Speed",
"terrain_coefficients": "Współczynniki terenu", "terrain_coefficients": "Terrain coefficients",
"raw_terrain_coefficients": "Surowe współczynniki terenu", "raw_terrain_coefficients": "Raw terrain coefficients",
"power_to_weight_ratio": "Stosunek mocy do masy", "power_to_weight_ratio": "Power to weight ratio",
"hull_traverse_speed": "Prędkość obrotu kadłuba", "hull_traverse_speed": "Hull traverse speed",
"camouflage": "Kamuflaż", "camouflage": "Camouflage",
"reload": "Przeładowanie", "reload": "Reload",
"crewCount": "Liczba członków załogi", "crewCount": "Crew count",
"shellNormalization": "Normalization", "shellNormalization": "Normalization",
"shellRicochet": "Ricochet", "shellRicochet": "Ricochet",
"dpm": "Uszkodzenia na minutę", "dpm": "DPM",
"dpmEffective": "Efektywne w 60s", "dpmEffective": "Effective at 60s",
"peekabooDpm": "Obrażenia na minutę w stylu „wychyl i schowaj”", "peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Obrażenia na minutę przy założeniu, że strzelisz tylko po zresetowaniu kamuflażu (lub ponownym załadowaniu, jeśli jest wolniejszy) — co najmniej 10 sekund pomiędzy strzałami.", "peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Pociski", "shells": "Shells",
"mostOptimalShellIndex": "Optymalny pocisk", "mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Przeładowania", "shellReloads": "Reloads",
"shellReload": "Przeładowanie", "shellReload": "Reload",
"intraClip": "Czas przeładowania pocisku", "intraClip": "Intra-clip",
"caliber": "Caliber", "caliber": "Caliber",
"penetration": "Penetration", "penetration": "Penetration",
"damage": "Uszkodzenia", "damage": "Uszkodzenia",
"damageWithoutAssault": "Uszkodzenia bez pocisków szturmowych", "damageWithoutAssault": "Damage without assault shells",
"clipDamage": "Potencjał magazynu", "clipDamage": "Clipping potential",
"moduleDamage": "Uszkodzenia modułów", "moduleDamage": "Module damage",
"explosionRadius": "Promień eksplozji", "explosionRadius": "Explosion radius",
"shellVelocity": "Prędkość pocisku", "shellVelocity": "Shell velocity",
"aimTime": "Czas celowania", "aimTime": "Aim time",
"dispersion": "Still", "dispersion": "Still",
"dispersionMoving": "Podczas ruchu", "dispersionMoving": "Moving",
"dispersionHullTraversing": "Obrót kadłuba", "dispersionHullTraversing": "Hull traversing",
"dispersionTurretTraversing": "Obrót wieży", "dispersionTurretTraversing": "Turret traversing",
"dispersionShooting": "Strzelanie", "dispersionShooting": "Shooting",
"dispersionGunDamaged": "Uszkodzone Działo", "dispersionGunDamaged": "Gun damaged",
"gunDepression": "Limit obrotu działa w dół", "gunDepression": "Depression",
"gunElevation": "Limit obrotu działa w górę", "gunElevation": "Elevation",
"gunFrontalDepression": "Przedni limit obrotu działa w dół", "gunFrontalDepression": "Frontal depression",
"gunFrontalElevation": "Przedni limit obrotu działa w górę", "gunFrontalElevation": "Frontal elevation",
"gunRearDepression": "Tylne opuszczanie działa", "gunRearDepression": "Rear depression",
"gunRearElevation": "Rear elevation", "gunRearElevation": "Rear elevation",
"azimuthLeft": "Azimuth left", "azimuthLeft": "Azimuth left",
"azimuthRight": "Azimuth right", "azimuthRight": "Azimuth right",
@@ -627,7 +626,7 @@
"trackRepairTime": "Track repair time", "trackRepairTime": "Track repair time",
"viewRange": "View range", "viewRange": "View range",
"camouflageStill": "Still", "camouflageStill": "Still",
"camouflageMoving": "Podczas ruchu", "camouflageMoving": "Moving",
"camouflageShootingStill": "Shooting while still", "camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving", "camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "Caught on fire", "camouflageCaughtOnFire": "Caught on fire",
@@ -649,7 +648,7 @@
"crew": { "crew": {
"title": "Crew training", "title": "Crew training",
"info": "represents the other roles a crew member can take.", "info": "represents the other roles a crew member can take.",
"count": "Liczba członków załogi", "count": "Crew count",
"commander": "Commander", "commander": "Commander",
"gunner": "Gunner", "gunner": "Gunner",
"driver": "Driver", "driver": "Driver",
@@ -657,14 +656,14 @@
"loader": "Loader" "loader": "Loader"
}, },
"maneuverability": { "maneuverability": {
"title": "Mobilność " "title": "Maneuverability"
}, },
"survivability": { "survivability": {
"title": "Przetrwanie" "title": "Survivability"
} }
}, },
"compare": { "compare": {
"name": "Porównaj", "name": "Compare",
"button": "Compare tanks", "button": "Compare tanks",
"description": "Compare tank statistics", "description": "Compare tank statistics",
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz", "seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
@@ -674,7 +673,7 @@
"title": "Add tanks", "title": "Add tanks",
"description": "Select tanks to add to comparison" "description": "Select tanks to add to comparison"
}, },
"clear": "Wyczyść", "clear": "Clear",
"deltas": { "deltas": {
"none": "No deltas", "none": "No deltas",
"percentage": "Percentage", "percentage": "Percentage",
@@ -687,17 +686,17 @@
}, },
"table": { "table": {
"equipment": { "equipment": {
"clear": "Wyczyść", "clear": "Clear",
"apply_to_all": "Apply to all" "apply_to_all": "Apply to all"
}, },
"skills": { "skills": {
"clear": "Wyczyść", "clear": "Clear",
"maximize": "Maximize", "maximize": "Maximize",
"equalize": "Equalize" "equalize": "Equalize"
}, },
"modules": { "modules": {
"stock": "Standardowy", "stock": "Stock",
"upgrade": "Ulepsz" "upgrade": "Upgrade"
}, },
"provisions": { "provisions": {
"clear": "Clear this tank", "clear": "Clear this tank",
@@ -762,14 +761,14 @@
"seo_description": "Track your stats in real time as you play World of Tanks Blitz", "seo_description": "Track your stats in real time as you play World of Tanks Blitz",
"search": { "search": {
"lookup": "Look up a player to get started", "lookup": "Look up a player to get started",
"hint": "Szukaj graczy...", "hint": "Search players...",
"searching": "Wyszukiwanie...", "searching": "Searching...",
"no_results": "No players found" "no_results": "No players found"
}, },
"controls": { "controls": {
"title": "Tracking {name}", "title": "Tracking {name}",
"subtitle": "Since {time}", "subtitle": "Since {time}",
"reset": "Zresetuj", "reset": "Reset",
"change": "Change" "change": "Change"
}, },
"no_cc": { "no_cc": {
@@ -793,7 +792,7 @@
"description": "Rank Blitz tanks", "description": "Rank Blitz tanks",
"seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz", "seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz",
"buttons": { "buttons": {
"reset": "Zresetuj", "reset": "Reset",
"share": "Share" "share": "Share"
} }
}, },
@@ -829,7 +828,7 @@
"success": { "success": {
"title": "Copy URL", "title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".", "info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Powrót", "back": "Back",
"copy": "Copy URL" "copy": "Copy URL"
} }
}, },
@@ -840,7 +839,7 @@
}, },
"customize": { "customize": {
"title": "Customize", "title": "Customize",
"reset": "Zresetuj", "reset": "Reset",
"description": "Bells and whistles of your embed" "description": "Bells and whistles of your embed"
} }
}, },
@@ -923,6 +922,7 @@
"copyright": "Unaffiliated with Wargaming or any service", "copyright": "Unaffiliated with Wargaming or any service",
"about": { "about": {
"title": "About", "title": "About",
"credits": "Credits",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "Patches" "patches": "Patches"
+11 -11
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "teste",
"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}.",
"fox": "A ágil raposa castanha salta por cima do cão preguiçoso."
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -36,7 +41,7 @@
"development": "Development", "development": "Development",
"opentest": "TesteAberto", "opentest": "TesteAberto",
"preview": "Preview", "preview": "Preview",
"production": "Produção" "production": "Production"
}, },
"locales": { "locales": {
"en": "Inglês", "en": "Inglês",
@@ -182,11 +187,11 @@
}, },
"website": { "website": {
"redirect": { "redirect": {
"title": "301: movido permanentemente", "title": "301: moved permanently",
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Configurações", "title": "Settings",
"description": "Change BlitzKit settings", "description": "Change BlitzKit settings",
"dev_mode": "Modo de desenvolvimento", "dev_mode": "Modo de desenvolvimento",
"auto_load_models": "Auto-load models", "auto_load_models": "Auto-load models",
@@ -202,9 +207,9 @@
"copied": "Copiado!" "copied": "Copiado!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Procura por tanque", "select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank", "swap_dialog_title": "Swap tank",
"clear_filters": "Limpar filtros", "clear_filters": "Clear filters",
"login": "Login to use this", "login": "Login to use this",
"search_bar_hint": "Procurar tanques...", "search_bar_hint": "Procurar tanques...",
"recent": "Visto recentemente", "recent": "Visto recentemente",
@@ -302,12 +307,6 @@
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "Restaurar", "reset": "Restaurar",
"shuffle": "Shuffle", "shuffle": "Shuffle",
@@ -923,6 +922,7 @@
"copyright": "Não afiliado à Wargaming ou qualquer serviço", "copyright": "Não afiliado à Wargaming ou qualquer serviço",
"about": { "about": {
"title": "Sobre", "title": "Sobre",
"credits": "Créditos",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "Atualizações" "patches": "Atualizações"
+12 -12
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "тест",
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — {fruit}, потому что это цвет {color}. Он мне так нравится, так что я скажу его 3 раза {fruit}! {fruit}! {fruit}! И ещё раз, это {color}, а не {not_color}.",
"fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -33,10 +38,10 @@
}, },
"branches": { "branches": {
"beta": "Бета", "beta": "Бета",
"development": "Разработка", "development": "Development",
"opentest": "Открытый тест", "opentest": "Открытый тест",
"preview": "Предпросмотр", "preview": "Preview",
"production": "Производство" "production": "Production"
}, },
"locales": { "locales": {
"en": "Английский", "en": "Английский",
@@ -189,7 +194,7 @@
"title": "Настройки", "title": "Настройки",
"description": "Изменить настройки BlitzKit", "description": "Изменить настройки BlitzKit",
"dev_mode": "Режим разработчика", "dev_mode": "Режим разработчика",
"auto_load_models": "Автозагрузка моделей", "auto_load_models": "Auto-load models",
"language": "Язык" "language": "Язык"
}, },
"common": { "common": {
@@ -302,12 +307,6 @@
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "Сброс", "reset": "Сброс",
"shuffle": "Перемешать", "shuffle": "Перемешать",
@@ -414,7 +413,7 @@
} }
}, },
"sandbox": { "sandbox": {
"load_model": "Загрузить модель", "load_model": "Load model",
"poses": { "poses": {
"hull_down": "За холмом", "hull_down": "За холмом",
"face_hug": "В упор", "face_hug": "В упор",
@@ -575,7 +574,7 @@
"dpm": "ДПМ", "dpm": "ДПМ",
"dpmEffective": "Эффективность за 60с", "dpmEffective": "Эффективность за 60с",
"peekabooDpm": "Peekaboo DPM", "peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Урон в минуту при условии, если вы сбросите камуфляж(или перезарядка окончена, если она медленнее) - не менее 10 секунд между выстрелами.", "peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Снарядов в барабане", "shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд", "mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Перезарядка", "shellReloads": "Перезарядка",
@@ -923,6 +922,7 @@
"copyright": "Не связан с Wargaming или иным сервисом", "copyright": "Не связан с Wargaming или иным сервисом",
"about": { "about": {
"title": "О сайте", "title": "О сайте",
"credits": "Авторы",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "Патчи" "patches": "Патчи"
+7 -7
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "тест",
"long": "{color}{fruit}",
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -302,12 +307,6 @@
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "Скинути", "reset": "Скинути",
"shuffle": "Перемішати", "shuffle": "Перемішати",
@@ -365,7 +364,7 @@
"title": "Partially reveal tank?", "title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!", "description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "", "cancel": "",
"reveal": "Рівень" "reveal": "Reveal"
}, },
"clear": "Clear", "clear": "Clear",
"select_all": "Вибрати все", "select_all": "Вибрати все",
@@ -923,6 +922,7 @@
"copyright": "Unaffiliated with Wargaming or any service", "copyright": "Unaffiliated with Wargaming or any service",
"about": { "about": {
"title": "About", "title": "About",
"credits": "Credits",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "Patches" "patches": "Patches"
+6 -6
View File
@@ -1,4 +1,9 @@
{ {
"test": {
"default": "测试",
"long": "这是一个测试用长字符串。你绝对不会在用户界面中看见这些。我最喜欢的水果是{fruit}因为它的颜色是{color}的。我如此喜欢它,以至于我会把它的名字念3次!{fruit}!{fruit}!{fruit}!重申一次,它是{color}的而不是{not_color}的。",
"fox": "褐色的狐狸迅速地跳过那只懒狗"
},
"common": { "common": {
"name": "BlitzKit", "name": "BlitzKit",
"units": { "units": {
@@ -302,12 +307,6 @@
} }
}, },
"tools": { "tools": {
"credits": {
"name": "Credits",
"button": "View people",
"description": "Contributors of BlitzKit",
"seo_description": "BlitzKit is the work of many"
},
"playlist": { "playlist": {
"reset": "重置", "reset": "重置",
"shuffle": "换一批", "shuffle": "换一批",
@@ -923,6 +922,7 @@
"copyright": "本网站与 Wargaming战游网 或任何服务无关", "copyright": "本网站与 Wargaming战游网 或任何服务无关",
"about": { "about": {
"title": "关于", "title": "关于",
"credits": "鸣谢",
"discord": "Discord", "discord": "Discord",
"github": "GitHub", "github": "GitHub",
"patches": "补丁" "patches": "补丁"
+7
View File
@@ -1,7 +1,14 @@
{ {
"name": "@blitzkit/scripts", "name": "@blitzkit/scripts",
"private": true, "private": true,
"scripts": {
"discover-ids": "bun src/discoverIds/index.ts",
"build-protos": "bun src/protos.ts",
"record-performance": "bun src/recordPerformance/index.ts",
"download-client": "bun src/downloadClient/index.ts"
},
"dependencies": { "dependencies": {
"@blitzkit/closed": "workspace:*",
"@blitzkit/core": "workspace:*", "@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*", "@blitzkit/i18n": "workspace:*",
"@gltf-transform/core": "^4.2.1", "@gltf-transform/core": "^4.2.1",
+102
View File
@@ -0,0 +1,102 @@
import { dvp } from "@blitzkit/closed";
import { assertSecret } from "@blitzkit/core";
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 { PATCHES_ROOT, vfs } from "./buildAssets/constants";
const versionTextFile = await vfs.text(`Data/version.txt`);
const currentVersion = versionTextFile
.split(" ")[0]
.split(".")
.slice(0, 3)
.join(".");
console.log(`Installing patches for ${currentVersion}...`);
let patchIndex = 1;
while (true) {
const response = await fetch(
`${assertSecret(
import.meta.env.WOTB_DLC_CDN,
)}/dlc/s${currentVersion}_${patchIndex}.yaml`,
);
console.log(patchIndex, response.status);
if (!response.ok) break;
console.log(`Applying patch ${patchIndex}...`);
const data = parseYaml(await response.text());
const dvpm = await fetch(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`,
).then((response) => response.arrayBuffer());
const dvpd = await fetch(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11.replace(
".dvpm",
".dvpd",
)}`,
).then((response) => response.arrayBuffer());
const files = await dvp(dvpm, dvpd);
const bar = new ProgressBar(
`Patching ${files.length} files :bar`,
files.length,
);
for (const { path, data } of files) {
console.log(`Patching "${path}"...`);
const { dir } = parsePath(path);
try {
await mkdir(`${PATCHES_ROOT}/Data/${dir}`, { recursive: true });
} catch (error) {
console.warn(`Failed to make directory "${dir}"`);
}
const buffer = Buffer.from(data);
await writeFile(`${PATCHES_ROOT}/Data/${path}`, new Uint8Array(buffer));
bar.tick();
}
console.log("patches done for patch");
if ("dynamicContentLocalizationsDir" in data) {
console.log(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
data.dynamicContentLocalizationsDir
}/en.yaml`,
);
const localizationsResponse = await fetch(
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
data.dynamicContentLocalizationsDir
}/en.yaml`,
);
console.log("fetched");
const newStrings = parseYaml(await localizationsResponse.text());
console.log("got new strings");
const oldStrings =
await vfs.yaml<Record<string, string>>(`Data/Strings/en.yaml`);
console.log("got old strings");
const patchedStrings = { ...oldStrings, ...newStrings };
const patchedContent = stringifyYaml(patchedStrings);
console.log("stringified");
await mkdir(`${PATCHES_ROOT}/Data/Strings`, { recursive: true });
console.log("mkdir");
await writeFile(
`${PATCHES_ROOT}/Data/Strings/en.yaml`,
new Uint8Array(Buffer.from(patchedContent)),
);
console.log("file");
}
patchIndex++;
}
vfs.dispose();
process.exit(0);
+5 -23
View File
@@ -9,8 +9,6 @@ import {
} from "./constants"; } from "./constants";
import { QuickGitHubRelease } from "./types"; import { QuickGitHubRelease } from "./types";
const DEPOT_DOWNLOADER_RETRY_TIME = 30_000;
const provider = assertSecret(import.meta.env.CLIENT_PROVIDER); const provider = assertSecret(import.meta.env.CLIENT_PROVIDER);
const source = assertSecret(import.meta.env.CLIENT_SOURCE); const source = assertSecret(import.meta.env.CLIENT_SOURCE);
const dir = assertSecret(import.meta.env.CLIENT_DIR); const dir = assertSecret(import.meta.env.CLIENT_DIR);
@@ -45,27 +43,11 @@ if (provider === "steam") {
const username = assertSecret(import.meta.env.STEAM_USERNAME); const username = assertSecret(import.meta.env.STEAM_USERNAME);
const password = assertSecret(import.meta.env.STEAM_PASSWORD); const password = assertSecret(import.meta.env.STEAM_PASSWORD);
while (true) { await $`${DEPOT_DOWNLOADER_EXECUTABLE} -os ${
const result = await $`${ platform
DEPOT_DOWNLOADER_EXECUTABLE } -app ${id} -username ${username} -password ${
} -os ${platform} -app ${id} -username ${ password
username } -dir ${dir} -max-downloads ${DEPOT_DOWNLOADER_MAX_DOWNLOADS}`;
} -password ${password} -dir ${dir} -max-downloads ${
DEPOT_DOWNLOADER_MAX_DOWNLOADS
}`.nothrow();
if (result.exitCode === 0) {
console.log("Client downloaded successfully!");
break;
}
console.error(
`DepotDownloader failed (exit ${result.exitCode}). Retrying in ${DEPOT_DOWNLOADER_RETRY_TIME / 1000} seconds...`,
);
await new Promise((resolve) =>
setTimeout(resolve, DEPOT_DOWNLOADER_RETRY_TIME),
);
}
} else if (provider === "zip") { } else if (provider === "zip") {
const url = assertSecret(import.meta.env.CLIENT_SOURCE); const url = assertSecret(import.meta.env.CLIENT_SOURCE);
@@ -1,7 +0,0 @@
export interface RegionsPublish {
Common: Common;
}
interface Common {
smartDlcUrls: string[];
}
-23
View File
@@ -1,23 +0,0 @@
import { vfs } from "./vfs";
const command = Bun.argv[2];
const path = Bun.argv[3];
if (!path) {
console.error("Gimme a path to assess dumb ass");
process.exit(1);
}
if (command === "cat") {
const content = await vfs.file(path);
await Bun.write(Bun.stdout, content);
} else if (command === "ls") {
const files = await vfs.dir(path);
for (const file of files) {
console.log(file);
}
} else if (command === "yaml") {
const content = await vfs.yaml(path);
await Bun.write(Bun.stdout, JSON.stringify(content, null, 2));
}
-4
View File
@@ -1,4 +0,0 @@
import { assertSecret } from "@blitzkit/core";
import { LocalVFS } from "@blitzkit/core/src/blitzkit/vfs/local";
export const vfs = new LocalVFS(assertSecret(import.meta.env.CLIENT_DIR));
+7
View File
@@ -1,6 +1,13 @@
{ {
"name": "@blitzkit/website", "name": "@blitzkit/website",
"type": "module", "type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"lint": "astro check",
"astro": "astro"
},
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/node": "^9.4.4", "@astrojs/node": "^9.4.4",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,38 @@
---
import { tankIcon } from "@blitzkit/core";
import { Flex, Heading, Link, Strong, Text } from "@radix-ui/themes";
---
<Flex justify="center" px="4">
<Link
color="gray"
highContrast
style={{ display: "content" }}
href="/docs/guides/christmas-2025"
underline="none"
>
<Flex
mt="3"
px="5"
py="4"
style={{
borderRadius: "var(--radius-3)",
backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
backgroundSize: "cover",
backgroundPosition: "center",
boxSizing: "border-box",
}}
align="center"
gap="5"
>
<img src={tankIcon(25169)} style={{ height: "min(15vw, 5rem)" }} />
<Flex direction="column">
<Heading size={{ initial: "4", sm: "6" }}>Christmas 2025</Heading>
<Text size={{ initial: "2", sm: "3" }}>
Win the <Strong>GSOR the TANK</Strong> and more!
</Text>
</Flex>
</Flex>
</Link>
</Flex>
@@ -1,36 +0,0 @@
.outer {
padding: 0 var(--space-4);
margin-top: var(--space-3);
img {
width: 1.25em;
height: 1.25em;
border-radius: 50%;
}
.center {
position: relative;
background: linear-gradient(var(--pink-3), var(--purple-2));
box-shadow: var(--shadow-3);
max-width: 40rem;
padding: var(--space-5);
border-radius: var(--radius-4);
.important {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
background-color: inherit;
padding: var(--space-3);
}
}
}
@@ -1,48 +0,0 @@
import { HeartFilledIcon } from "@radix-ui/react-icons";
import { Button, Em, Flex, Strong, Text } from "@radix-ui/themes";
import { donationTiers } from "../../config/donations";
import styles from "./index.module.css";
export function Beg() {
return (
<Flex align="center" gap="4" direction="column" className={styles.outer}>
<Text size="3" className={styles.important}>
<Flex gap="2" align="center">
<img src="https://github.com/tresabhi.png" />
<Em>We need your attention</Em>
</Flex>
</Text>
<Flex direction="column" className={styles.center} gap="4">
<Flex direction="column" gap="3">
<Text color="tomato">
We can no longer keep BlitzKit sustainable for <Strong>free</Strong>
.
</Text>
<Text>
We want to add tank modules, player statistics, rebalancing patch
notes, bring back the Discord bot, and more.
</Text>
<Text>
But paying out of pocket makes that impossible. We are an earnest
non-profit, so consider donating to make the above possible. 100% of
your money goes back into BlitzKit.
</Text>
</Flex>
<Flex gap="3">
{donationTiers.map(({ name, value, link }) => (
<a target="_blank" key={name} href={link}>
<Button size="3" color="crimson">
{value === null && <HeartFilledIcon />}
{value === null ? "Custom" : `$${value}`}
</Button>
</a>
))}
</Flex>
</Flex>
</Flex>
);
}
@@ -2,7 +2,7 @@ import { alias } from "@blitzkit/core";
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes"; import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics"; import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { useLocale } from "../../hooks/useLocale"; import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral"; import { CompareEphemeral } from "../../stores/compareEphemeral";
import { CompareCellDirection } from "../CompareCell"; import { CompareCellDirection } from "../CompareCell";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell"; import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { CompareRow } from "./CompareRow"; import { CompareRow } from "./CompareRow";
@@ -50,11 +50,7 @@ export function Body({ stats }: BodyProps) {
}} }}
onClick={() => { onClick={() => {
CompareEphemeral.mutate((draft) => { CompareEphemeral.mutate((draft) => {
if (draft.syncShells) { draft.members[index].shell = thisShell;
syncShellSlot(draft.members, shellIndex);
} else {
draft.members[index].shell = thisShell;
}
}); });
}} }}
> >
@@ -1,17 +1,9 @@
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons"; import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
import { import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
Button,
Checkbox,
Dialog,
Flex,
SegmentedControl,
Text,
} from "@radix-ui/themes";
import { DIALOG_MAX_HEIGHT } from "../../constants/dialog";
import { api } from "../../core/blitzkit/api"; import { api } from "../../core/blitzkit/api";
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember"; import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
import { useLocale } from "../../hooks/useLocale"; import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral, syncShellSlot } from "../../stores/compareEphemeral"; import { CompareEphemeral } from "../../stores/compareEphemeral";
import { import {
ComparePersistent, ComparePersistent,
type DeltaMode, type DeltaMode,
@@ -30,7 +22,6 @@ export function Controls({
onAddTankDialogOpenChange, onAddTankDialogOpenChange,
}: ControlsProps) { }: ControlsProps) {
const deltaMode = ComparePersistent.use((state) => state.deltaMode); const deltaMode = ComparePersistent.use((state) => state.deltaMode);
const syncShells = CompareEphemeral.use((state) => state.syncShells);
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
@@ -45,11 +36,11 @@ export function Controls({
</Button> </Button>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}> <Dialog.Content>
<Dialog.Title align="center"> <Dialog.Title align="center">
{strings.website.tools.compare.actions.add.title} {strings.website.tools.compare.actions.add.title}
</Dialog.Title> </Dialog.Title>
<Dialog.Description align="center" mb="4"> <Dialog.Description align="center">
{strings.website.tools.compare.actions.add.description} {strings.website.tools.compare.actions.add.description}
</Dialog.Description> </Dialog.Description>
@@ -120,31 +111,6 @@ export function Controls({
{strings.website.tools.compare.actions.deltas.absolute} {strings.website.tools.compare.actions.deltas.absolute}
</SegmentedControl.Item> </SegmentedControl.Item>
</SegmentedControl.Root> </SegmentedControl.Root>
<Flex
align="center"
gap="2"
style={{ cursor: "pointer" }}
onClick={() => {
CompareEphemeral.mutate((draft) => {
draft.syncShells = !draft.syncShells;
if (!draft.syncShells || draft.members.length === 0) return;
const [first] = draft.members;
syncShellSlot(
draft.members,
first.gun.shells.findIndex(({ id }) => id === first.shell.id),
);
});
}}
>
<Checkbox variant="classic" checked={syncShells} />
<Text size="2">
{strings.website.tools.compare.actions.sync_shells}
</Text>
</Flex>
</Flex> </Flex>
); );
} }
@@ -1,7 +1,6 @@
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons"; import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
import { Dialog, Flex, IconButton } from "@radix-ui/themes"; import { Dialog, Flex, IconButton } from "@radix-ui/themes";
import { useState } from "react"; import { useState } from "react";
import { DIALOG_MAX_HEIGHT } from "../../constants/dialog";
import { api } from "../../core/blitzkit/api"; import { api } from "../../core/blitzkit/api";
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember"; import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
import { useLocale } from "../../hooks/useLocale"; import { useLocale } from "../../hooks/useLocale";
@@ -44,7 +43,7 @@ export function TankControl({ index, slug }: TankControlProps) {
</IconButton> </IconButton>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}> <Dialog.Content>
<Dialog.Title align="center"> <Dialog.Title align="center">
{strings.website.common.tank_search.swap_dialog_title} {strings.website.common.tank_search.swap_dialog_title}
</Dialog.Title> </Dialog.Title>
@@ -1,135 +0,0 @@
---
import { Grid, Text } from "@radix-ui/themes";
import { credits } from "../config/credits";
import { getStrings } from "../core/i18n/getStrings";
interface Props {
locale: string;
}
const { locale } = Astro.props;
const strings = getStrings(locale);
---
<style>
.credits {
display: flex;
justify-content: center;
padding: var(--space-4);
.inner {
display: flex;
flex-direction: column;
gap: var(--space-2);
width: 100%;
max-width: 80rem;
.list {
gap: var(--space-5);
.link {
display: contents;
.person {
--profile-size: 5rem;
position: relative;
display: flex;
flex-direction: column;
padding: var(--space-4);
padding-top: 0;
margin-top: calc(var(--profile-size) / 2);
gap: var(--space-1);
box-shadow: var(--shadow-3);
border-radius: var(--radius-3);
.contain {
border-radius: inherit;
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
.hint {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
filter: saturate(20) brightness(0.125) blur(4rem);
}
}
.text {
position: relative;
}
img {
border-radius: 50%;
position: relative;
margin-top: calc(var(--profile-size) / -2);
object-fit: cover;
width: var(--profile-size);
height: var(--profile-size);
margin-bottom: var(--space-2);
}
}
}
}
}
}
</style>
<div class="credits">
<div class="inner">
<Grid
className="list"
py="4"
columns="repeat(auto-fill, minmax(16rem, 1fr))"
flow="dense"
gap="4"
gapY="8"
>
{
credits.map((person) => (
<a target="_blank" href={person.url} class="link">
<div class="person">
<div class="contain">
<div
class="hint"
style={`background-image: url(${person.image})`}
/>
</div>
<img src={person.image} />
<Text weight="medium" className="text" color="gray" highContrast>
{person.name}
</Text>
<Text className="text" color="gray" wrap="pretty">
{person.description}
</Text>
</div>
</a>
))
}
</Grid>
</div>
</div>
+6 -3
View File
@@ -43,9 +43,9 @@ const strings = getStrings(locale);
}} }}
> >
<Heading>BlitzKit</Heading> <Heading>BlitzKit</Heading>
<Text size="2" color="gray">{strings.website.footer.motto}</Text> <Text color="gray">{strings.website.footer.motto}</Text>
<Text size="2" color="gray"> <Text size="1" color="gray">
{strings.website.footer.association} {strings.website.footer.copyright}
</Text> </Text>
<Box mt="2"> <Box mt="2">
@@ -84,6 +84,9 @@ const strings = getStrings(locale);
sm: "start", sm: "start",
}} }}
> >
<LinkI18n {locale} href="/docs/about/credits" color="gray">
{strings.website.footer.about.credits}
</LinkI18n>
<Link <Link
href="https://discord.gg/nDt7AjGJQH" href="https://discord.gg/nDt7AjGJQH"
target="_blank" target="_blank"
@@ -3,10 +3,9 @@ import { useEffect, useState, type ReactNode } from "react";
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
import { InlineSkeleton } from "./InlineSkeleton"; import { InlineSkeleton } from "./InlineSkeleton";
const TIME = 1793898000 * 1000; const TIME = 1764898200 * 1000;
const HREF = "https://preview.blitzkit.app/tanks";
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) { export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now()); const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
useEffect(() => { useEffect(() => {
@@ -39,7 +38,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
</> </>
); );
} else { } else {
string = "have been revealed."; string = "has ended. Come again later!";
} }
return ( return (
@@ -48,7 +47,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
color="gray" color="gray"
highContrast highContrast
style={{ display: "content" }} style={{ display: "content" }}
href={timeLeft <= 0 ? HREF : undefined} href="https://discord.gg/njQdBhNS?event=1446208951136813117"
underline="none" underline="none"
> >
<Flex <Flex
@@ -57,17 +56,17 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
py="4" py="4"
style={{ style={{
borderRadius: "var(--radius-3)", borderRadius: "var(--radius-3)",
backgroundImage: "url(https://i.imgur.com/xgngxoy.png)", backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
boxSizing: "border-box", boxSizing: "border-box",
boxShadow: "0 0 var(--space-7) var(--white-a2)", boxShadow: "0 0 var(--space-7) var(--blue-a5)",
}} }}
align="center" align="center"
gap="5" gap="5"
> >
<img <img
src="https://i.imgur.com/kXV3r3P.png" src="https://i.imgur.com/mvmJQgB.png"
style={{ style={{
height: "min(15vw, 5rem)", height: "min(15vw, 5rem)",
filter: "drop-shadow(0.5rem 0.5rem 1rem #00000080)", filter: "drop-shadow(0.5rem 0.5rem 1rem #00000080)",
@@ -76,11 +75,11 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
<Flex direction="column"> <Flex direction="column">
<Heading size={{ initial: "4", sm: "6" }}> <Heading size={{ initial: "4", sm: "6" }}>
More Tier XIs {!skeleton && string}{" "} Music Box giveaway {!skeleton && string}{" "}
{skeleton && <InlineSkeleton width="10rem" />} {skeleton && <InlineSkeleton width="10rem" />}
</Heading> </Heading>
<Text size={{ initial: "2", sm: "3" }}> <Text size={{ initial: "2", sm: "3" }}>
Catch the next 3 tanks on BlitzKit Preview Join the BlitzKit Discord server event
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>
@@ -1,9 +1,9 @@
import { useModel } from "../hooks/useModel"; import { useModel } from "../hooks/useModel";
import { Guess, GuessState } from "../stores/guess"; import { Guess, GuessState } from "../stores/guess";
import { SmartCanvas } from "./SmartCanvas"; import { SmartCanvas } from "./SmartCanvas";
import { TankModel } from "./TankModel";
import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control"; import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control";
import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting"; import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { TankModel } from "./Tankopedia/HeroSection/components/TankSandbox/components/TankModel";
const BRIGHTNESS = 2 ** -5.5; const BRIGHTNESS = 2 ** -5.5;
const CONTRAST = 2 ** 2; const CONTRAST = 2 ** 2;
+2 -9
View File
@@ -1,20 +1,13 @@
import { Link, Table } from '@radix-ui/themes'; import { Link, Table } from '@radix-ui/themes';
import MarkdownToJSX from 'markdown-to-jsx'; import MarkdownToJSX from 'markdown-to-jsx';
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import { docImageUrl, isExternalImage } from '../core/blitzkit/docImages';
interface MarkdownProps { interface MarkdownProps {
children: string; children: string;
} }
function Image({ src, ...props }: ComponentProps<'img'>) { function Image(props: ComponentProps<'img'>) {
return ( return <img style={{ maxWidth: '100%' }} {...props} />;
<img
style={{ maxWidth: '100%' }}
src={src && isExternalImage(src) ? docImageUrl(src) : src}
{...props}
/>
);
} }
export function Markdown({ children }: MarkdownProps) { export function Markdown({ children }: MarkdownProps) {
+44 -8
View File
@@ -5,7 +5,7 @@ import {
GearIcon, GearIcon,
HamburgerMenuIcon, HamburgerMenuIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { Box, Code, Flex, Grid, IconButton, Link } from "@radix-ui/themes"; import { Box, Code, Flex, IconButton, Link } from "@radix-ui/themes";
import { resolveBranchName } from "../core/blitzkit/branches"; import { resolveBranchName } from "../core/blitzkit/branches";
import { getStrings } from "../core/i18n/getStrings"; import { getStrings } from "../core/i18n/getStrings";
import { Var } from "../core/radix/var"; import { Var } from "../core/radix/var";
@@ -20,7 +20,7 @@ export interface Props {
const { integrated = false } = Astro.props; const { integrated = false } = Astro.props;
const toolsFiltered = Object.values(tools).filter( const toolsFiltered = Object.values(tools).filter(
(tool) => !tool.href && !tool.disabled, (tool) => !tool.href && !tool.disabled
); );
const { locale } = Astro.props; const { locale } = Astro.props;
const branchName = resolveBranchName(locale); const branchName = resolveBranchName(locale);
@@ -105,7 +105,7 @@ const strings = getStrings(locale);
<Flex <Flex
className="content" className="content"
width="100%" width="100%"
p="3" p={{ initial: "4", sm: "3" }}
direction="column" direction="column"
align="center" align="center"
> >
@@ -133,6 +133,41 @@ const strings = getStrings(locale);
<Box flexGrow="1" /> <Box flexGrow="1" />
<Flex
align="center"
gap={{ initial: "3", md: "6" }}
display={{ initial: "none", sm: "flex" }}
justify="center"
className="tools"
>
{
toolsFiltered.map((tool) => {
const selected = Astro.url.pathname.includes(`/${tool.id}`);
const toolStrings =
strings.website.tools[
(tool.strings ??
tool.id) as keyof typeof strings.website.tools
];
return (
<LinkI18n
{locale}
weight={selected ? "medium" : "regular"}
color="gray"
highContrast={selected}
size={{ initial: "1", md: "2" }}
href={`/${tool.id}`}
underline="hover"
>
{toolStrings.name}
</LinkI18n>
);
})
}
</Flex>
<Box flexGrow="1" />
<Flex align="center" gap="4"> <Flex align="center" gap="4">
<Link <Link
style={{ display: "flex", alignContent: "center" }} style={{ display: "flex", alignContent: "center" }}
@@ -156,7 +191,7 @@ const strings = getStrings(locale);
<NavbarAccount {locale} client:idle /> <NavbarAccount {locale} client:idle />
<Flex align="center"> <Flex align="center" display={{ initial: "flex", sm: "none" }}>
<IconButton variant="ghost" color="gray" className="hamburger"> <IconButton variant="ghost" color="gray" className="hamburger">
<Cross1Icon data-expanded /> <Cross1Icon data-expanded />
<HamburgerMenuIcon data-closed /> <HamburgerMenuIcon data-closed />
@@ -165,10 +200,11 @@ const strings = getStrings(locale);
</Flex> </Flex>
</Flex> </Flex>
<Grid <Flex
direction="column"
data-expanded data-expanded
columns="repeat(auto-fill, minmax(30rem, 1fr))"
gap="3" gap="3"
display={{ initial: "flex", sm: "none" }}
> >
{ {
toolsFiltered.map((tool) => { toolsFiltered.map((tool) => {
@@ -193,7 +229,7 @@ const strings = getStrings(locale);
}} }}
> >
<Flex <Flex
p="6" py="4"
justify="end" justify="end"
flexGrow="1" flexGrow="1"
style={{ style={{
@@ -207,7 +243,7 @@ const strings = getStrings(locale);
); );
}) })
} }
</Grid> </Flex>
</Flex> </Flex>
</Flex> </Flex>
@@ -0,0 +1,3 @@
export function StupidCat() {
return <img src="https://i.imgur.com/6FBBzb9.png" />;
}
@@ -0,0 +1,297 @@
import {
invalidate,
useThree,
type ObjectMap,
type ThreeEvent,
} from "@react-three/fiber";
import { useRef } from "react";
import type { Texture } from "three";
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { hasEquipment } from "../core/blitzkit/hasEquipment";
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
import { controlsEnabledEvent } from "../core/controlsEnabled";
import { useBatched } from "../hooks/useBatched";
import { useModel } from "../hooks/useModel";
import { useTankModelDefinition } from "../hooks/useTankModelDefinition";
import { useTankTransform } from "../hooks/useTankTransform";
import { Duel } from "../stores/duel";
import { Tankopedia } from "../stores/tankopedia";
import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
import { ModelTankWrapper } from "./Armor/components/ModelTankWrapper";
interface SectionProps {
map: ObjectMap;
}
function Tracks({ map }: SectionProps) {
const Batched = useBatched(map, true);
const position = useRef(new Vector2());
const delta = useRef(new Vector2());
const textures = useRef(new Set<Texture | null>());
function handlePointerMove(event: PointerEvent) {
delta.current.set(event.clientX, event.clientY).sub(position.current);
position.current.set(event.clientX, event.clientY);
const deltaX = delta.current.x / window.innerWidth;
const deltaY = delta.current.y / window.innerHeight;
const offset = deltaX + deltaY;
for (const texture of textures.current) {
if (!texture) continue;
texture.offset.y += offset;
}
invalidate();
}
function handlePointerUp() {
controlsEnabledEvent.dispatch(true);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
if (Tankopedia.state.display !== TankopediaDisplay.Model) {
return;
}
const material = (event.object as Mesh).material as MeshStandardMaterial;
textures.current = new Set([
material.map,
material.aoMap,
material.normalMap,
material.roughnessMap,
material.metalnessMap,
]);
position.current.set(event.clientX, event.clientY);
event.stopPropagation();
controlsEnabledEvent.dispatch(false);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
return (
<Batched
onPointerDown={handlePointerDown}
slicer={(node) => {
return node.name.startsWith("chassis_track_");
}}
/>
);
}
function Hull({ map }: SectionProps) {
const Batched = useBatched(map, true);
function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
}
return (
<Batched
onPointerDown={handlePointerDown}
slicer={(node) => {
const isHull = node.name === "hull";
const isWheel = node.name.startsWith("chassis_wheel_");
return isHull || isWheel;
}}
/>
);
}
function Turret({ map }: SectionProps) {
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const Batched = useBatched(map, true);
const position = useRef(new Vector2());
const delta = useRef(new Vector2());
const canvas = useThree((state) => state.gl.domElement);
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
position.current.set(event.clientX, event.clientY);
controlsEnabledEvent.dispatch(false);
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.current.set(event.clientX, event.clientY).sub(position.current);
position.current.set(event.clientX, event.clientY);
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw +
delta.current.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
function handlePointerUp() {
controlsEnabledEvent.dispatch(true);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
return (
<Batched
onPointerDown={onPointerDown}
slicer={(node) => {
const isCurrentTurret =
node.name ===
`turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`;
return isCurrentTurret;
}}
/>
);
}
function Gun({ map }: SectionProps) {
const Batched = useBatched(map, true);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const position = useRef(new Vector2());
const delta = useRef(new Vector2());
const pitch = useRef(0);
const yaw = useRef(0);
const canvas = useThree((state) => state.gl.domElement);
function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.current.set(event.clientX, event.clientY).sub(position.current);
position.current.set(event.clientX, event.clientY);
[pitch.current, yaw.current] = applyPitchYawLimits(
pitch.current - delta.current.y * (Math.PI / boundingRect.height),
yaw.current + delta.current.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch: pitch.current, yaw: yaw.current });
}
function handlePointerUp() {
controlsEnabledEvent.dispatch(true);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
controlsEnabledEvent.dispatch(false);
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
position.current.set(event.clientX, event.clientY);
pitch.current = modelTransformEvent.last!.pitch;
yaw.current = modelTransformEvent.last!.yaw;
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
return (
<Batched
onPointerDown={onPointerDown}
slicer={(node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
return isCurrentGun || isCurrentMantlet;
}}
/>
);
}
export function TankModel() {
const protagonist = Duel.use((draft) => draft.protagonist);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
const { gltf } = useModel(protagonist.tank.id);
useTankTransform(track, turret, turretContainer, gunContainer);
return (
<ModelTankWrapper ref={hullContainer}>
<Tracks map={gltf} />
<Hull map={gltf} />
<group ref={turretContainer}>
<Turret map={gltf} />
<group ref={gunContainer}>
<Gun map={gltf} />
</group>
</group>
</ModelTankWrapper>
);
}
@@ -1,7 +1,7 @@
import { import {
normalizeBoundingBox, normalizeBoundingBox,
resolveDpm, resolveDpm,
tankBoundingBox, unionBoundingBox,
type TankDefinition, type TankDefinition,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { useMemo } from "react"; import { useMemo } from "react";
@@ -112,7 +112,10 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0); return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
case "survivability.volume": { case "survivability.volume": {
const dimensions = normalizeBoundingBox( const dimensions = normalizeBoundingBox(
tankBoundingBox(tankModelDefinition, turretModelDefinition), unionBoundingBox(
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
); );
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0); return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
@@ -120,7 +123,10 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case "survivability.length": { case "survivability.length": {
const bounds = normalizeBoundingBox( const bounds = normalizeBoundingBox(
tankBoundingBox(tankModelDefinition, turretModelDefinition), unionBoundingBox(
tankModelDefinition.bounding_box!,
turretModelDefinition.bounding_box!,
),
); );
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0); return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
@@ -3,7 +3,7 @@ import {
normalizeBoundingBox, normalizeBoundingBox,
resolveDpm, resolveDpm,
SEARCH_KEYS, SEARCH_KEYS,
tankBoundingBox, unionBoundingBox,
type TankDefinition, type TankDefinition,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals"; import { literals } from "@blitzkit/i18n/src/literals";
@@ -137,10 +137,16 @@ export const TankSearch = memo<TankSearchProps>(
const bTurretModelDefinition = const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox( const aSize = normalizeBoundingBox(
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition), unionBoundingBox(
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
); );
const bSize = normalizeBoundingBox( const bSize = normalizeBoundingBox(
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition), unionBoundingBox(
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
); );
const aVolume = aSize.x * aSize.y * aSize.z; const aVolume = aSize.x * aSize.y * aSize.z;
const bVolume = bSize.x * bSize.y * bSize.z; const bVolume = bSize.x * bSize.y * bSize.z;
@@ -158,10 +164,16 @@ export const TankSearch = memo<TankSearchProps>(
const bTurretModelDefinition = const bTurretModelDefinition =
bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
const aSize = normalizeBoundingBox( const aSize = normalizeBoundingBox(
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition), unionBoundingBox(
aTankModelDefinition.bounding_box!,
aTurretModelDefinition.bounding_box!,
),
); );
const bSize = normalizeBoundingBox( const bSize = normalizeBoundingBox(
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition), unionBoundingBox(
bTankModelDefinition.bounding_box!,
bTurretModelDefinition.bounding_box!,
),
); );
const aLength = Math.max(aSize.x, aSize.y, aSize.z); const aLength = Math.max(aSize.x, aSize.y, aSize.z);
const bLength = Math.max(bSize.x, bSize.y, bSize.z); const bLength = Math.max(bSize.x, bSize.y, bSize.z);
@@ -44,9 +44,7 @@ export function GunFlexibilityVisualizer({
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
const modelRequested = Tankopedia.use( const modelRequested = Tankopedia.use((state) => state.modelRequested);
(state) => state.loaders.model.requested,
);
const tankModel = modelDefinition.models[tank.id]; const tankModel = modelDefinition.models[tank.id];
const turretModel = tankModel.turrets[turret.id]; const turretModel = tankModel.turrets[turret.id];
@@ -1,5 +1,5 @@
import { createDefaultSkills } from "@blitzkit/core"; import { createDefaultSkills } from "@blitzkit/core";
import { Box, Flex, Progress, Text } from "@radix-ui/themes"; import { Flex, Progress, Text } from "@radix-ui/themes";
import { clamp } from "lodash-es"; import { clamp } from "lodash-es";
import { memo, useMemo, type ComponentProps, type ReactNode } from "react"; import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
import { api } from "../../../../../../core/blitzkit/api"; import { api } from "../../../../../../core/blitzkit/api";
@@ -16,7 +16,6 @@ import {
TankopediaRelativeAgainst, TankopediaRelativeAgainst,
} from "../../../../../../stores/tankopedia"; } from "../../../../../../stores/tankopedia";
import { Info, type InfoProps } from "./Info"; import { Info, type InfoProps } from "./Info";
import { StatScale } from "./StatScale";
type InfoWithDeltaProps = Omit<InfoProps, "name"> & { type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
stats: TankCharacteristics; stats: TankCharacteristics;
@@ -123,15 +122,6 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
return othersValue !== undefined; return othersValue !== undefined;
}) as TankCharacteristics[]; }) as TankCharacteristics[];
}, [relativeAgainst, shellIndex, equipmentMatrix, equalize]); }, [relativeAgainst, shellIndex, equipmentMatrix, equalize]);
const values = useMemo(
() =>
others.map((tank) =>
typeof props.value === "function"
? props.value(tank)!
: (tank[props.value] as number),
),
[others],
);
const betterTanks = others.filter((tank) => { const betterTanks = others.filter((tank) => {
const othersValue = const othersValue =
typeof props.value === "function" typeof props.value === "function"
@@ -171,25 +161,13 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
{!noRanking && ( {!noRanking && (
<Flex pl={indent ? "2" : "0"} align="center" gap="2"> <Flex pl={indent ? "2" : "0"} align="center" gap="2">
<Box position="relative" flexGrow="1"> <Progress
<StatScale variant="soft"
values={values} size="1"
value={uhWhatDoICallThisVariable} value={goodness * 100}
lowerIsBetter={deltaType === "lowerIsBetter"} color={color}
/> style={{ height: "0.125rem", opacity: 0.5 }}
/>
<Progress
variant="soft"
size="1"
value={goodness * 100}
color={color}
style={{
position: "relative",
height: "0.125rem",
opacity: 0.75,
}}
/>
</Box>
<Text color="gray" size="1"> <Text color="gray" size="1">
{clamp(betterTanks.length + 1, 1, others.length)} /{" "} {clamp(betterTanks.length + 1, 1, others.length)} /{" "}
@@ -1,91 +0,0 @@
import { memo, useEffect, useMemo, useRef } from "react";
const RESOLUTION = 1000;
const HEIGHT = 8;
const DENSE_TICKS = 100;
interface StatScaleProps {
values: number[];
value: number;
lowerIsBetter?: boolean;
}
export const StatScale = memo<StatScaleProps>(
({ values, value, lowerIsBetter }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const ticks = useMemo(() => {
const min = Math.min(value, ...values);
const max = Math.max(value, ...values);
const range = max - min;
const positions = new Set<number>();
for (const other of values) {
const raw = range === 0 ? 0.5 : (other - min) / range;
const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION);
if (x === 0 || x === RESOLUTION) continue;
positions.add(x);
}
return Array.from(positions, (x) => ({ x }));
}, [values, value, lowerIsBetter]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const draw = () => {
const { width } = canvas.getBoundingClientRect();
const pixelRatio = window.devicePixelRatio || 1;
const canvasWidth = Math.ceil(width * pixelRatio);
const canvasHeight = Math.ceil(HEIGHT * pixelRatio);
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
const context = canvas.getContext("2d");
if (!context) return;
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
context.clearRect(0, 0, width, HEIGHT);
const styles = getComputedStyle(canvas);
for (const { x } of ticks) {
context.fillStyle = styles.getPropertyValue("--gray-a6").trim();
context.fillRect((x / RESOLUTION) * width, 0, 1, HEIGHT);
}
};
const observer = new ResizeObserver(draw);
observer.observe(canvas);
window.addEventListener("resize", draw);
draw();
return () => {
observer.disconnect();
window.removeEventListener("resize", draw);
};
}, [ticks]);
return (
<canvas
ref={canvasRef}
aria-hidden="true"
style={{
position: "absolute",
top: 0,
bottom: 0,
left: 0,
width: "100%",
height: HEIGHT,
margin: "auto 0",
opacity: Math.min(1, Math.sqrt(DENSE_TICKS / ticks.length)),
pointerEvents: "none",
}}
/>
);
},
);
@@ -1,44 +0,0 @@
import { Flex, IconButton, type FlexProps } from "@radix-ui/themes";
import type { ComponentProps, CSSProperties } from "react";
type SidebarIconButtonProps = ComponentProps<typeof IconButton> & {
selected: boolean;
src: string;
alt: string;
imageStyle?: CSSProperties;
};
export function SidebarIconGroup({ style, ...props }: FlexProps) {
return (
<Flex
direction="column"
overflow="hidden"
style={{ borderRadius: "var(--radius-full)", ...style }}
{...props}
/>
);
}
export function SidebarIconButton({
selected,
src,
alt,
imageStyle,
...props
}: SidebarIconButtonProps) {
return (
<IconButton
color={selected ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="none"
{...props}
>
<img
alt={alt}
src={src}
style={{ width: "50%", height: "50%", ...imageStyle }}
/>
</IconButton>
);
}
@@ -1,106 +0,0 @@
import { alias } from "@blitzkit/core";
import { Flex, Tooltip } from "@radix-ui/themes";
import { uniqBy } from "lodash-es";
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
import { api } from "../../../../../../core/blitzkit/api";
import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
import { SidebarIconButton } from "./SidebarIconButton";
const camouflageDefinitions = await api.camouflageDefinitions();
export function SkinSwitcher() {
const tank = Duel.use((state) => state.protagonist.tank);
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
const attachment = Tankopedia.use(
(state) => state.loaders.attachment.requested,
);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const disturbed = Tankopedia.use((state) => state.disturbed);
const { unwrap } = useLocale();
const skins = uniqBy(
Object.values(camouflageDefinitions.camouflages).filter(
(camouflage) => camouflage.tank_id === tank.id,
),
(camouflage) => unwrap(camouflage.name!),
);
// attachments aren't camouflages so they get their own group
const groups = [
skins.filter((camouflage) => !camouflage.attachment),
skins.filter((camouflage) => camouflage.attachment),
].filter((group) => group.length > 0);
if (skins.length === 0) return null;
return (
<Flex
direction="column"
gap="3"
top="50%"
right={
disturbed && requestedDisplay === TankopediaDisplay.Model
? "3"
: "-4rem"
}
style={{
position: "absolute",
transform: "translateY(-50%)",
transitionDuration: "200ms",
pointerEvents: "auto",
}}
>
{groups.map((group) => (
<Flex key={group[0].id} direction="column">
{group.map((camouflage, index) => {
const name = unwrap(camouflage.name!).replace(
"%(camo_num)",
SKIN_NUMBER,
);
const top = index === 0 ? "var(--radius-full)" : "0";
const bottom =
index === group.length - 1 ? "var(--radius-full)" : "0";
return (
<Tooltip key={camouflage.id} content={name} side="left">
<SidebarIconButton
selected={
(camouflage.attachment ? attachment : skin) ===
camouflage.id
}
alt={name}
src={alias("api", `/icons/camouflages/${camouflage.id}.webp`)}
size={{ initial: "3", sm: "4" }}
radius="full"
style={{
borderRadius: `${top} ${top} ${bottom} ${bottom}`,
}}
imageStyle={{
width: "70%",
height: "70%",
borderRadius: "50%",
}}
onClick={() => {
Tankopedia.mutate((draft) => {
const loader =
draft.loaders[
camouflage.attachment ? "attachment" : "skin"
];
loader.requested =
loader.requested === camouflage.id
? undefined
: camouflage.id;
});
}}
/>
</Tooltip>
);
})}
</Flex>
))}
</Flex>
);
}
@@ -27,7 +27,6 @@ import {
Tooltip, Tooltip,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import { Suspense, useState, type RefObject } from "react"; import { Suspense, useState, type RefObject } from "react";
import { DIALOG_MAX_HEIGHT } from "../../../../../constants/dialog";
import { Pose, poseEvent } from "../../../../../core/blitzkit/pose"; import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
import { useEquipment } from "../../../../../hooks/useEquipment"; import { useEquipment } from "../../../../../hooks/useEquipment";
import { useFullScreen } from "../../../../../hooks/useFullScreen"; import { useFullScreen } from "../../../../../hooks/useFullScreen";
@@ -47,11 +46,6 @@ import { TankSearch } from "../../../../TankSearch";
import { CustomShellButton } from "./components/CustomShellButton"; import { CustomShellButton } from "./components/CustomShellButton";
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher"; import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { QuickInputs } from "./components/QuickInputs"; import { QuickInputs } from "./components/QuickInputs";
import {
SidebarIconButton,
SidebarIconGroup,
} from "./components/SidebarIconButton";
import { SkinSwitcher } from "./components/SkinSwitcher";
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher"; import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
import { Thicknesses } from "./components/Thicknesses"; import { Thicknesses } from "./components/Thicknesses";
@@ -83,9 +77,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics); const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
const modelRequested = Tankopedia.use( const modelRequested = Tankopedia.use((state) => state.modelRequested);
(state) => state.loaders.model.requested,
);
const equalize = Duel.use((state) => state.equalize); const equalize = Duel.use((state) => state.equalize);
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer); const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
@@ -95,8 +87,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} /> <Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
<SkinSwitcher />
<Flex <Flex
gap="2" gap="2"
direction="column" direction="column"
@@ -191,13 +181,24 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</Popover.Root> </Popover.Root>
)} )}
<SidebarIconGroup> <Flex
direction="column"
style={{
borderRadius: "var(--radius-full)",
}}
overflow="hidden"
>
{antagonistGun.shells.map((thisShell) => ( {antagonistGun.shells.map((thisShell) => (
<SidebarIconButton <IconButton
color={
thisShell.id === antagonistShell.id && !hasCustomShell
? undefined
: "gray"
}
variant="soft"
key={thisShell.id} key={thisShell.id}
selected={thisShell.id === antagonistShell.id && !hasCustomShell} size={{ initial: "2", sm: "3" }}
alt={unwrap(thisShell.name!)} radius="none"
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
onClick={() => { onClick={() => {
Duel.mutate((draft) => { Duel.mutate((draft) => {
draft.antagonist.shell = thisShell; draft.antagonist.shell = thisShell;
@@ -207,10 +208,19 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
draft.customShell = undefined; draft.customShell = undefined;
}); });
}} }}
/> >
<img
alt={unwrap(thisShell.name!)}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
))} ))}
<CustomShellButton /> <CustomShellButton />
</SidebarIconGroup> </Flex>
<Flex <Flex
direction="column" direction="column"
@@ -475,7 +485,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</Button> </Button>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}> <Dialog.Content>
<Dialog.Title align="center"> <Dialog.Title align="center">
{strings.website.common.tank_search.select_dialog_title} {strings.website.common.tank_search.select_dialog_title}
</Dialog.Title> </Dialog.Title>
@@ -1,53 +1,30 @@
import { UpdateIcon } from "@radix-ui/react-icons"; import { UpdateIcon } from "@radix-ui/react-icons";
import { Button, Flex, Heading } from "@radix-ui/themes"; import { Button, Flex, Heading } from "@radix-ui/themes";
import { useEffect, useRef } from "react";
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
import { api } from "../../../../../../core/blitzkit/api";
import { useLocale } from "../../../../../../hooks/useLocale"; import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../stores/duel"; import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia"; import { Tankopedia } from "../../../../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
import { Tracker } from "./components/Tracker"; import { Tracker } from "./components/Tracker";
const camouflageDefinitions = await api.camouflageDefinitions();
export function Title({ skeleton }: MaybeSkeletonComponentProps) { export function Title({ skeleton }: MaybeSkeletonComponentProps) {
const { unwrap, strings } = useLocale(); const { unwrap, strings } = useLocale();
const modelRequested = Tankopedia.use( const modelRequested = Tankopedia.use((state) => state.modelRequested);
(state) => state.loaders.model.requested,
);
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const hasRevealed = useRef(false); const name = unwrap(protagonist.name!);
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
const camouflage =
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
const tankName = unwrap(protagonist.name!);
// legendary camos carry the tank's in-battle name
const name = (
camouflage
? camouflage.tank_name
? unwrap(camouflage.tank_name)
: `${tankName} ${unwrap(camouflage.name!)}`
: tankName
).replace("%(camo_num)", SKIN_NUMBER);
const fontSize = revealed const fontSize = revealed
? disturbed ? disturbed
? "1.5rem" ? "1.5rem"
: "2rem" : "2rem"
: `min(12vh, ${75 / name.length}vw)`; : `min(12vh, ${75 / name.length}vw)`;
useEffect(() => {
if (revealed) hasRevealed.current = true;
}, [revealed]);
return ( return (
<Flex <Flex
onPointerDown={(event) => { onPointerDown={(event) => {
event.preventDefault(); event.preventDefault();
if (!modelRequested || hasRevealed.current) return; if (!modelRequested) return;
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.revealed = true; draft.revealed = true;
@@ -109,7 +86,7 @@ export function Title({ skeleton }: MaybeSkeletonComponentProps) {
onPointerDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}
onClick={() => { onClick={() => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.loaders.model.requested = true; draft.modelRequested = true;
}); });
}} }}
> >
@@ -18,7 +18,7 @@ import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent
const ANGLE = degToRad(15); const ANGLE = degToRad(15);
const REVEAL_ANIMATION_TIME = 3; const REVEAL_ANIMATION_TIME = 3;
export const TRANSITION_ANIMATION_TIME = 0.5; const TRANSITION_ANIMATION_TIME = 0.5;
const LIGHTS_COUNT = 3; const LIGHTS_COUNT = 3;
const THETA_OFFSET = degToRad(180 - 45); const THETA_OFFSET = degToRad(180 - 45);
@@ -46,26 +46,17 @@ export function Lighting({ hasPbr }: Props) {
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics); const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay); const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const revealed = Tankopedia.use((state) => state.revealed);
const skinPending = Tankopedia.use(
(state) =>
state.loaders.skin.requested !== state.loaders.skin.loaded ||
state.loaders.attachment.requested !== state.loaders.attachment.loaded,
);
const animationTime = useRef(REVEAL_ANIMATION_TIME); const animationTime = useRef(REVEAL_ANIMATION_TIME);
const t0 = useRef(performance.now() / 1e3 - animationTime.current); const t0 = useRef(performance.now() / 1e3 - animationTime.current);
const holding = useRef(false);
const isRevealing = useRef(true); const isRevealing = useRef(true);
const isFirstReveal = useRef(true);
const [animate, setAnimate] = useState(true); const [animate, setAnimate] = useState(true);
useEffect(() => { useEffect(() => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.revealed = true; draft.revealed = true;
draft.loaders.model.loaded = true;
}); });
}, []); }, []);
@@ -80,37 +71,6 @@ export function Lighting({ hasPbr }: Props) {
setAnimate(true); setAnimate(true);
}, [requestedDisplay]); }, [requestedDisplay]);
useEffect(() => {
if (isFirstReveal.current) {
isFirstReveal.current = false;
return;
}
if (!revealed) {
animationTime.current = TRANSITION_ANIMATION_TIME;
t0.current = performance.now() / 1e3;
holding.current = true;
} else if (holding.current) {
animationTime.current = REVEAL_ANIMATION_TIME;
t0.current = performance.now() / 1e3 - animationTime.current;
holding.current = false;
} else {
return;
}
setAnimate(true);
invalidate();
}, [revealed]);
useEffect(() => {
if (!skinPending) return;
animationTime.current = TRANSITION_ANIMATION_TIME;
t0.current = performance.now() / 1e3;
setAnimate(true);
}, [skinPending]);
return ( return (
<> <>
<ContactShadows <ContactShadows
@@ -130,7 +90,6 @@ export function Lighting({ hasPbr }: Props) {
hasPbr={hasPbr} hasPbr={hasPbr}
t0={t0} t0={t0}
animationTime={animationTime} animationTime={animationTime}
holding={holding}
wrapper={wrapper} wrapper={wrapper}
stop={() => { stop={() => {
setAnimate(false); setAnimate(false);
@@ -165,9 +124,8 @@ export function Lighting({ hasPbr }: Props) {
key={index} key={index}
position={position} position={position}
intensity={intensity} intensity={intensity}
userData={{ intensity }}
penumbra={1} penumbra={1}
castShadow={highGraphics} castShadow={highGraphics && index === 0}
decay={1} decay={1}
color="#ffffff" color="#ffffff"
angle={0} angle={0}
@@ -187,19 +145,11 @@ interface AnimatorProps {
t0: RefObject<number>; t0: RefObject<number>;
animationTime: RefObject<number>; animationTime: RefObject<number>;
holding: RefObject<boolean>;
wrapper: RefObject<Group | null>; wrapper: RefObject<Group | null>;
hasPbr: boolean; hasPbr: boolean;
} }
function Animator({ function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
stop,
t0,
animationTime,
holding,
wrapper,
hasPbr,
}: AnimatorProps) {
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay); const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const display = Tankopedia.use((state) => state.display); const display = Tankopedia.use((state) => state.display);
@@ -216,11 +166,6 @@ function Animator({
for (const child of wrapper.current.children) { for (const child of wrapper.current.children) {
if (child instanceof SpotLight) { if (child instanceof SpotLight) {
child.angle = ANGLE * t * factor; child.angle = ANGLE * t * factor;
child.intensity = child.userData.intensity;
if (child.angle === 0) {
child.intensity = 0;
}
} else if (child instanceof HemisphereLight) { } else if (child instanceof HemisphereLight) {
child.intensity = HEMISPHERE_INTENSITY * t * factor; child.intensity = HEMISPHERE_INTENSITY * t * factor;
} }
@@ -231,7 +176,7 @@ function Animator({
useFrame(({ invalidate }) => { useFrame(({ invalidate }) => {
const dt = performance.now() / 1e3 - t0.current; const dt = performance.now() / 1e3 - t0.current;
const x = clamp(dt / animationTime.current, 0, holding.current ? 1 : 2); const x = clamp(dt / animationTime.current, 0, 2);
if (x === 2) { if (x === 2) {
apply(1); apply(1);
@@ -243,9 +188,6 @@ function Animator({
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2; const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
apply(t); apply(t);
if (holding.current && x === 1) return;
invalidate(); invalidate();
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) { if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
@@ -1,344 +0,0 @@
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
import { skinTree } from "../../../../../../core/blitzkit/skinTree";
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
import { useModel } from "../../../../../../hooks/useModel";
import { useSkin } from "../../../../../../hooks/useSkin";
import { useSkinAnimation } from "../../../../../../hooks/useSkinAnimation";
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
import { transitionEvent } from "./Lighting";
export function TankModel() {
const protagonist = Duel.use((draft) => draft.protagonist);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
const canvas = useThree((state) => state.gl.domElement);
const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const { gltf } = useModel(protagonist.tank.id);
const nodes = Object.values(gltf.nodes);
const skinGltf = useSkin(protagonist.tank.id);
const attachmentGltf = useSkin(protagonist.tank.id, "attachment");
const skins = [skinGltf, attachmentGltf].filter((skin) => skin !== undefined);
useTankTransform(track, turret, turretContainer, gunContainer);
useSkinAnimation(skins);
useEffect(() => {
const emissiveMaterials = new Set<MeshStandardMaterial>();
const roots = [
gltf.scene,
...skins.flatMap((skin) => [...skin.parts.values()].flat()),
];
for (const root of roots) {
root.traverse((object) => {
if (
object instanceof Mesh &&
object.material instanceof MeshStandardMaterial &&
object.material.emissiveMap
) {
emissiveMaterials.add(object.material);
}
});
}
// emissive parts fade with the lights instead of glowing in the dark
function applyIntensity(intensity: number) {
for (const material of emissiveMaterials) {
material.emissiveIntensity = intensity;
}
}
function handleTransitionEvent(event: QuicklimeEvent<number>) {
applyIntensity(event.data);
}
applyIntensity(transitionEvent.last!);
transitionEvent.on(handleTransitionEvent);
return () => {
transitionEvent.off(handleTransitionEvent);
};
}, [gltf, skinGltf, attachmentGltf]);
return (
<ModelTankWrapper ref={hullContainer}>
{nodes.map((node) => {
const isHull = node.name === "hull";
const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith("chassis_track_");
const isVisible = isHull || isWheel || isTrack;
const position = new Vector2();
const delta = new Vector2();
if (!isVisible) return null;
function translateTexture(offset: number) {
const mesh = node.children[0] as Mesh;
const material = mesh?.material as MeshStandardMaterial;
if (!material) return;
invalidate();
const textures = new Set([
material.map,
material.aoMap,
material.normalMap,
material.roughnessMap,
material.metalnessMap,
]);
for (const texture of textures) {
if (texture) texture.offset.y += offset;
}
}
function onPointerDown(event: ThreeEvent<PointerEvent>) {
if (isTrack && Tankopedia.state.display === TankopediaDisplay.Model) {
position.set(event.clientX, event.clientY);
event.stopPropagation();
controlsEnabledEvent.dispatch(false);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
} else {
event.stopPropagation();
}
}
function handlePointerMove(event: PointerEvent) {
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
const deltaX = delta.x / window.innerWidth;
const deltaY = delta.y / window.innerHeight;
translateTexture(deltaX + deltaY);
}
function handlePointerUp() {
controlsEnabledEvent.dispatch(true);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
return skinTree(node, skins, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
receiveShadow
/>
);
},
primitive(object, key) {
return (
<primitive
key={key}
object={object}
onPointerDown={onPointerDown}
/>
);
},
});
})}
<group ref={turretContainer}>
{nodes.map((node) => {
const isTurret = node.name.startsWith("turret_");
const isCurrentTurret =
node.name ===
`turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`;
const isVisible = isCurrentTurret;
const position = new Vector2();
const delta = new Vector2();
if (!isVisible) return null;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
if (!isTurret) return;
position.set(event.clientX, event.clientY);
controlsEnabledEvent.dispatch(false);
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw +
delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
function handlePointerUp() {
controlsEnabledEvent.dispatch(true);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
return skinTree(node, skins, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
receiveShadow
/>
);
},
primitive(object, key) {
return (
<primitive
key={key}
object={object}
onPointerDown={onPointerDown}
/>
);
},
});
})}
<group ref={gunContainer}>
{nodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModelDefinition.model_id
.toString()
.padStart(2, "0")}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentGun || isCurrentMantlet;
const position = new Vector2();
const delta = new Vector2();
if (!isVisible) return null;
let pitch = 0;
let yaw = 0;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
controlsEnabledEvent.dispatch(false);
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
position.set(event.clientX, event.clientY);
pitch = modelTransformEvent.last!.pitch;
yaw = modelTransformEvent.last!.yaw;
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
[pitch, yaw] = applyPitchYawLimits(
pitch - delta.y * (Math.PI / boundingRect.height),
yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}
function handlePointerUp() {
controlsEnabledEvent.dispatch(true);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
return skinTree(node, skins, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
receiveShadow
/>
);
},
primitive(object, key) {
return (
<primitive
key={key}
object={object}
onPointerDown={onPointerDown}
/>
);
},
});
})}
</group>
</group>
</ModelTankWrapper>
);
}
@@ -23,12 +23,12 @@ import {
type ThicknessRange, type ThicknessRange,
} from "../../../../Armor/components/StaticArmor"; } from "../../../../Armor/components/StaticArmor";
import { SmartCanvas } from "../../../../SmartCanvas"; import { SmartCanvas } from "../../../../SmartCanvas";
import { TankModel } from "../../../../TankModel";
import { AutoClear } from "./components/AutoClear"; import { AutoClear } from "./components/AutoClear";
import { Controls } from "./components/Control"; import { Controls } from "./components/Control";
import { InitialAligner } from "./components/InitialAligner"; import { InitialAligner } from "./components/InitialAligner";
import { Lighting } from "./components/Lighting"; import { Lighting } from "./components/Lighting";
import { SceneProps } from "./components/SceneProps"; import { SceneProps } from "./components/SceneProps";
import { TankModel } from "./components/TankModel";
interface TankSandboxProps { interface TankSandboxProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -20,9 +20,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const canvas = useRef<HTMLCanvasElement>(null!); const canvas = useRef<HTMLCanvasElement>(null!);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const modelRequested = Tankopedia.use( const modelRequested = Tankopedia.use((state) => state.modelRequested);
(state) => state.loaders.model.requested,
);
const thicknessRange = useMemo(() => { const thicknessRange = useMemo(() => {
const entries = Object.values(tankDefinitions.tanks); const entries = Object.values(tankDefinitions.tanks);
const filtered = entries.filter( const filtered = entries.filter(
-102
View File
@@ -1,102 +0,0 @@
interface CreditPeople {
name: string;
description: string;
url?: string;
image: string;
}
export const credits: CreditPeople[] = [
{
name: "TrèsAbhi",
description: "Founder and lead developer",
url: "https://abhi-deep.com/",
image: "https://github.com/tresabhi.png",
},
{
name: "HitJack",
description: "Developer on website",
url: "https://marksstuff.com",
image: "https://github.com/Hitjack007.png",
},
{
name: "aeson000",
description: "Advisor on game and website",
url: "https://discord.gg/WHdER7ZPAD",
image: "/media/users/aeson000.png",
},
{
name: "GonnaHetzMe",
description: "Developer and advisor on back end",
url: "https://github.com/karelpak1",
image: "https://github.com/karelpak1.png",
},
{
name: "ChickenMan7777",
description: "Moderator and former advisor on game mechanics",
url: "https://discord.gg/rEsUbXCqD9",
image: "/media/users/chickenman7777.webp",
},
{
name: "Avenir",
description: "Moderator and elite Advanced Recon Commando",
image: "/media/users/avenir.webp",
},
{
name: "Pyogenics",
description: "Former advisor on game 3D format",
url: "https://github.com/Pyogenics",
image: "https://github.com/Pyogenics.png",
},
{
name: "Prince_NA",
description: "Former advisor on bot and website",
image: "/media/users/prince-na.webp",
},
{
name: "Vovko",
description: "Former advisor on bot",
url: "https://amth.one/",
image: "https://github.com/cufee.png",
},
{
name: "S0AP",
description: "Former advisor on and advertiser for bot",
url: "https://discord.gg/3KfH93um3Y",
image: "/media/users/s0ap.webp",
},
{
name: "Synx",
description: "Former advisor on bot and website",
url: "https://www.youtube.com/@Synx_Blitz",
image: "/media/users/synx.webp",
},
{
name: "34wotb",
description: "Former advisor on bot",
url: "https://www.twitch.tv/34wotb/",
image: "/media/users/34wotb.jpg",
},
{
name: "HisRoyalFatness",
description: "Former advisor on website",
url: "https://www.youtube.com/@hisroyalfatness",
image: "/media/users/hisroyalfatness.jpg",
},
{
name: "Minitelrose",
description: "Former advisor on game mechanics",
url: "https://wotinspector.com/",
image: "/media/users/minitelrose.webp",
},
{
name: "Maddox",
description: "Game asset decompression",
url: "https://github.com/Maddoxkkm",
image: "https://github.com/Maddoxkkm.png",
},
{
name: "TJ Duff",
description: "Chief executive cheer leader",
image: "/media/users/tj-duff.jpg",
},
];
-23
View File
@@ -1,23 +0,0 @@
// TODO: source this from the open collective api, if even possible
export const donationTiers = [
{
name: "The Backer",
value: 5,
link: "https://opencollective.com/blitzkit/contribute/backers-106312/checkout",
},
{
name: "The Engineer",
value: 10,
link: "https://opencollective.com/blitzkit/contribute/the-engineer-107057/checkout",
},
{
name: "The Patron",
value: 25,
link: "https://opencollective.com/blitzkit/contribute/the-patron-107056/checkout",
},
{
name: "The Sponsor",
value: null,
link: "https://opencollective.com/blitzkit/donate",
},
];
-3
View File
@@ -1,3 +0,0 @@
import { NAVBAR_HEIGHT } from "./navbar";
export const DIALOG_MAX_HEIGHT = `calc(100svh - 2 * (${NAVBAR_HEIGHT}px + var(--space-4) + var(--space-2)))`;
@@ -1 +0,0 @@
export const SKIN_NUMBER = "0417";
-4
View File
@@ -74,8 +74,4 @@ export const tools: Record<string, Tool> = {
href: "https://discord.gg/nDt7AjGJQH", href: "https://discord.gg/nDt7AjGJQH",
button: "plum", button: "plum",
}, },
credits: {
id: "credits",
button: "bronze",
},
}; };
@@ -1,32 +0,0 @@
import { alias } from "@blitzkit/core";
const EXTERNAL_IMAGE = /^https?:\/\//;
const MARKDOWN_IMAGES = [
/!\[[^\]]*\]\((\S+?)(?:\s+"[^"]*")?\)/g,
/<img[^>]*\ssrc=["']([^"']+)["']/g,
];
export function isExternalImage(src: string) {
return EXTERNAL_IMAGE.test(src);
}
export function docImageId(url: string) {
let hash = 0x811c9dc5;
for (let index = 0; index < url.length; index++) {
hash ^= url.charCodeAt(index);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
return hash.toString(16).padStart(8, "0");
}
export function docImageUrl(url: string) {
return alias("api", `/docs/images/${docImageId(url)}.webp`);
}
export function externalDocImages(markdown: string) {
return MARKDOWN_IMAGES.flatMap((pattern) =>
Array.from(markdown.matchAll(pattern), (match) => match[1]),
).filter(isExternalImage);
}
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more