mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-08 10:20:37 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd665349e3 | ||
|
|
2557f06f01 | ||
|
|
bbdb63fd7d | ||
|
|
7a9a1360f9 | ||
|
|
bd26c172e3 | ||
|
|
eeb65d926a | ||
|
|
9c82e846a1 | ||
|
|
44431c4011 | ||
|
|
051d846525 | ||
|
|
31d77d395f | ||
|
|
056e0bead9 | ||
|
|
1db426399c | ||
|
|
6d98216a96 | ||
|
|
cbe6e9ca48 | ||
|
|
c7b21b8dfb | ||
|
|
cc8f893293 | ||
|
|
0f5a5d275d | ||
|
|
e787f00804 | ||
|
|
d30ccdcd6a | ||
|
|
206733542f | ||
|
|
b9b4adde42 | ||
|
|
e2bfd1b688 | ||
|
|
1d95f95155 | ||
|
|
a6f49c19f6 | ||
|
|
74038e83fb | ||
|
|
f5a5879d03 | ||
|
|
a4e168dae9 | ||
|
|
29077d313b | ||
|
|
3bdfc42754 | ||
|
|
b7c0eac30c | ||
|
|
4c6880bbdf | ||
|
|
9b1131e42f | ||
|
|
54432c1a25 | ||
|
|
0e8db43eb5 | ||
|
|
f85daaf61b |
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -89,9 +89,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
|
||||||
@@ -110,19 +107,10 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
|
# the PR's ref, or the manual input, or production (selected when cron)
|
||||||
ref: ${{ github.event.pull_request.head.sha || inputs.branch || 'production' }}
|
ref: ${{ github.event.pull_request.head.sha || 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 +125,19 @@ 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
|
|
||||||
run: scripts/lint.sh
|
|
||||||
|
|
||||||
- name: Download client
|
- name: Download client
|
||||||
run: scripts/download-client.sh
|
run: bun run download-client
|
||||||
|
|
||||||
- name: Download SmartDLC
|
- name: Download SmartDLC
|
||||||
if: ${{ env.CLIENT_PROVIDER == 'steam' && env.SHOULD_USE_BLITZKIT_CLOSED == 'true' }}
|
if: ${{ env.CLIENT_PROVIDER == 'steam' }}
|
||||||
run: scripts/download-smart-dlc.sh
|
|
||||||
|
run: bun run download-smart-dlc
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: scripts/build-website.sh
|
run: bun run --bun build
|
||||||
env:
|
env:
|
||||||
CHUNK: ${{ matrix.chunk }}
|
CHUNK: ${{ matrix.chunk }}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
@@ -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=="],
|
||||||
|
|||||||
@@ -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
|
|
||||||
+12
-1
@@ -1,8 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.33.0",
|
"version": "1.33.0-beta.1",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cd packages/website && bun --bun run dev",
|
||||||
|
"build": "cd packages/website && bun --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",
|
||||||
|
"download-smart-dlc": "cd packages/scripts && bun run download-smart-dlc",
|
||||||
|
"clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite",
|
||||||
|
"vfs": "cd packages/scripts && bun run vfs"
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
+1
-1
Submodule packages/closed updated: 182fcdb273...f89f0e0772.
@@ -87,19 +87,14 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
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;
|
let hasPreset = false;
|
||||||
|
|
||||||
if (preset !== undefined) {
|
if (camo.group === "legendary" && yamlEntry.preset !== undefined) {
|
||||||
if (!skinPresets.has(vehicle)) {
|
if (!skinPresets.has(vehicle)) {
|
||||||
skinPresets.set(vehicle, this.skinPresets(vehicle));
|
skinPresets.set(vehicle, this.skinPresets(vehicle));
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPreset = (await skinPresets.get(vehicle)!).has(preset);
|
hasPreset = (await skinPresets.get(vehicle)!).has(yamlEntry.preset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedParts.some(Boolean) || hasPreset) {
|
if (resolvedParts.some(Boolean) || hasPreset) {
|
||||||
@@ -115,7 +110,6 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
|||||||
tank_name: resolvedTankName,
|
tank_name: resolvedTankName,
|
||||||
tank_name_full: resolvedTankNameFull,
|
tank_name_full: resolvedTankNameFull,
|
||||||
tank_id: tankId,
|
tank_id: tankId,
|
||||||
attachment: camo.kind === "customization",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,14 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
I_HAT,
|
I_HAT,
|
||||||
-MathUtils.degToRad(turretRotation.pitch),
|
-MathUtils.degToRad(turretRotation.pitch),
|
||||||
)
|
)
|
||||||
.applyAxisAngle(J_HAT, -MathUtils.degToRad(turretRotation.roll))
|
.applyAxisAngle(
|
||||||
.applyAxisAngle(K_HAT, -MathUtils.degToRad(turretRotation.yaw));
|
J_HAT,
|
||||||
|
-MathUtils.degToRad(turretRotation.roll),
|
||||||
|
)
|
||||||
|
.applyAxisAngle(
|
||||||
|
K_HAT,
|
||||||
|
-MathUtils.degToRad(turretRotation.yaw),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const coordinates = position.toArray();
|
const coordinates = position.toArray();
|
||||||
@@ -292,29 +298,23 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
|
|
||||||
turret.userString;
|
turret.userString;
|
||||||
|
|
||||||
let turretCollisionName = parsePath(
|
const turretCollisionName = parsePath(
|
||||||
turret.hitTester.collisionModel,
|
turret.hitTester.collisionModel,
|
||||||
).name.toLowerCase();
|
).name.toLowerCase();
|
||||||
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
|
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
|
||||||
|
|
||||||
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
|
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
|
||||||
turretCollisionName = "turret_01";
|
throw new Error(
|
||||||
|
`Missing turret collision mesh for ${nation}-${tankKey} ${turretKey}`,
|
||||||
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];
|
const turretBoundingBox = collisionBoundingBoxes[
|
||||||
|
turretCollisionName
|
||||||
if (turretBoundingBox === undefined) {
|
] ?? {
|
||||||
console.warn("HACK: missing collision; using 0's");
|
min: [0, 0, 0] as Vector3Tuple,
|
||||||
turretBoundingBox = { min: [0, 0, 0], max: [0, 0, 0] };
|
max: [0, 0, 0] as Vector3Tuple,
|
||||||
}
|
};
|
||||||
|
|
||||||
modelDefinitions.models[tankId].turrets[turretId] = {
|
modelDefinitions.models[tankId].turrets[turretId] = {
|
||||||
armor: turretArmor,
|
armor: turretArmor,
|
||||||
@@ -342,18 +342,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;
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ message Camouflage {
|
|||||||
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 uint32 tank_id = 5;
|
||||||
optional bool attachment = 6;
|
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,6 @@ export interface Camouflage {
|
|||||||
tank_name?: I18nString | undefined;
|
tank_name?: I18nString | undefined;
|
||||||
tank_name_full?: I18nString | undefined;
|
tank_name_full?: I18nString | undefined;
|
||||||
tank_id?: number | undefined;
|
tank_id?: number | undefined;
|
||||||
attachment?: boolean | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
||||||
@@ -193,7 +192,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, tank_id: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Camouflage: MessageFns<Camouflage> = {
|
export const Camouflage: MessageFns<Camouflage> = {
|
||||||
@@ -213,9 +212,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
||||||
writer.uint32(40).uint32(message.tank_id);
|
writer.uint32(40).uint32(message.tank_id);
|
||||||
}
|
}
|
||||||
if (message.attachment !== undefined && message.attachment !== false) {
|
|
||||||
writer.uint32(48).bool(message.attachment);
|
|
||||||
}
|
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -266,14 +262,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
message.tank_id = reader.uint32();
|
message.tank_id = reader.uint32();
|
||||||
continue;
|
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;
|
||||||
@@ -290,7 +278,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
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,
|
tank_id: isSet(object.tank_id) ? globalThis.Number(object.tank_id) : 0,
|
||||||
attachment: isSet(object.attachment) ? globalThis.Boolean(object.attachment) : false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -311,9 +298,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
||||||
obj.tank_id = Math.round(message.tank_id);
|
obj.tank_id = Math.round(message.tank_id);
|
||||||
}
|
}
|
||||||
if (message.attachment !== undefined && message.attachment !== false) {
|
|
||||||
obj.attachment = message.attachment;
|
|
||||||
}
|
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -333,7 +317,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
? I18nString.fromPartial(object.tank_name_full)
|
? I18nString.fromPartial(object.tank_name_full)
|
||||||
: undefined;
|
: undefined;
|
||||||
message.tank_id = object.tank_id ?? 0;
|
message.tank_id = object.tank_id ?? 0;
|
||||||
message.attachment = object.attachment ?? false;
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ export interface ConfigArchive {
|
|||||||
|
|
||||||
interface EnabledPresets {
|
interface EnabledPresets {
|
||||||
AlphaTest: boolean;
|
AlphaTest: boolean;
|
||||||
AlphaBlend?: boolean;
|
|
||||||
AlphaBlendAdditive?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Flags {
|
interface Flags {
|
||||||
@@ -158,11 +156,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";
|
||||||
|
|||||||
@@ -5,10 +5,5 @@ export type CamouflagesYaml = Record<
|
|||||||
shortUserString?: string;
|
shortUserString?: string;
|
||||||
preset?: string;
|
preset?: string;
|
||||||
customEntities?: Record<string, { item: string }>;
|
customEntities?: Record<string, { item: string }>;
|
||||||
// nation -> tank
|
|
||||||
override?: Record<
|
|
||||||
string,
|
|
||||||
{ override?: Record<string, { preset?: string }> }
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
@@ -744,8 +744,7 @@
|
|||||||
"none": "No deltas",
|
"none": "No deltas",
|
||||||
"percentage": "Percentage",
|
"percentage": "Percentage",
|
||||||
"absolute": "Absolute"
|
"absolute": "Absolute"
|
||||||
},
|
}
|
||||||
"sync_shells": "Sync shells"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"shells": {
|
"shells": {
|
||||||
"ap": "AP",
|
"ap": "AP",
|
||||||
"ap_cr": "APCR",
|
"ap_cr": "APCR",
|
||||||
"hc": "HEAT",
|
"hc": "AEAC",
|
||||||
"he": "AE"
|
"he": "AE"
|
||||||
},
|
},
|
||||||
"player_count_period": {
|
"player_count_period": {
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Cambiar configuración de BlitzKit",
|
||||||
"dev_mode": "Modo de desarrollador",
|
"dev_mode": "Modo de desarrollador",
|
||||||
"auto_load_models": "Auto-load models",
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Idioma"
|
"language": "Idioma"
|
||||||
@@ -679,7 +679,8 @@
|
|||||||
"none": "Sin deltas",
|
"none": "Sin deltas",
|
||||||
"percentage": "Porcentaje",
|
"percentage": "Porcentaje",
|
||||||
"absolute": "Absoluto"
|
"absolute": "Absoluto"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
"title": "No hay tanques seleccionados",
|
"title": "No hay tanques seleccionados",
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"motto": "Todo sobre World of Tanks Blitz",
|
"motto": "Todo sobre World of Tanks Blitz",
|
||||||
"copyright": "No afiliado con Wargaming u otros servicios",
|
"association": "An Open Collective Project",
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
|
|||||||
@@ -32,10 +32,10 @@
|
|||||||
"d_1": "Hier"
|
"d_1": "Hier"
|
||||||
},
|
},
|
||||||
"branches": {
|
"branches": {
|
||||||
"beta": "Beta",
|
"beta": "Bêta",
|
||||||
"development": "Development",
|
"development": "Development",
|
||||||
"opentest": "OpenTest",
|
"opentest": "Test ouvert",
|
||||||
"preview": "Preview",
|
"preview": "Aperçu",
|
||||||
"production": "Production"
|
"production": "Production"
|
||||||
},
|
},
|
||||||
"locales": {
|
"locales": {
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
"title": "Paramètres",
|
"title": "Paramètres",
|
||||||
"description": "Modifier les paramètres de BlitzKit",
|
"description": "Modifier les paramètres de BlitzKit",
|
||||||
"dev_mode": "Mode développement",
|
"dev_mode": "Mode développement",
|
||||||
"auto_load_models": "Auto-load models",
|
"auto_load_models": "Modèles à chargement automatique",
|
||||||
"language": "Langue"
|
"language": "Langue"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -236,69 +236,69 @@
|
|||||||
"fire.caliber": "calibration",
|
"fire.caliber": "calibration",
|
||||||
"fire.standardPenetration": "pénétration standard",
|
"fire.standardPenetration": "pénétration standard",
|
||||||
"fire.premiumPenetration": "pénétration premium",
|
"fire.premiumPenetration": "pénétration premium",
|
||||||
"fire.tertiaryPenetration": "tertiary penetration",
|
"fire.tertiaryPenetration": "pénétration tertiaire",
|
||||||
"fire.damage": "damage",
|
"fire.damage": "dégât",
|
||||||
"fire.moduleDamage": "module damage",
|
"fire.moduleDamage": "Dégâts aux modules",
|
||||||
"fire.shellVelocity": "shell velocity",
|
"fire.shellVelocity": "Vitesse des obus",
|
||||||
"fire.shellRange": "shell range",
|
"fire.shellRange": "Portée des obus",
|
||||||
"fire.shellCapacity": "shell capacity",
|
"fire.shellCapacity": "Capacité d'obus",
|
||||||
"fire.aimTime": "aim time",
|
"fire.aimTime": "Temps de visé",
|
||||||
"fire.dispersionStill": "dispersion still",
|
"fire.dispersionStill": "Dispersion à l'arrêt",
|
||||||
"fire.dispersionMoving": "dispersion moving",
|
"fire.dispersionMoving": "Dispersion en mouvement",
|
||||||
"fire.gunDepression": "gun depression",
|
"fire.gunDepression": "Dépression du canon",
|
||||||
"fire.gunElevation": "gun elevation",
|
"fire.gunElevation": "élévation du canon",
|
||||||
"maneuverability.forwardsSpeed": "forwards speed",
|
"maneuverability.forwardsSpeed": "Vitesse en marche avant",
|
||||||
"maneuverability.backwardsSpeed": "backwards speed",
|
"maneuverability.backwardsSpeed": "Vitesse en marche arrière",
|
||||||
"maneuverability.power": "power",
|
"maneuverability.power": "Puissance",
|
||||||
"maneuverability.powerToWeight": "power to weight",
|
"maneuverability.powerToWeight": "Rapport puissance/poids",
|
||||||
"maneuverability.weight": "weight",
|
"maneuverability.weight": "Poids",
|
||||||
"maneuverability.traverseSpeed": "traverse speed"
|
"maneuverability.traverseSpeed": "vitesse de traverse"
|
||||||
},
|
},
|
||||||
"sort_dropdown": {
|
"sort_dropdown": {
|
||||||
"label": "Sort",
|
"label": "Trier",
|
||||||
"groups": {
|
"groups": {
|
||||||
"sort": {
|
"sort": {
|
||||||
"no_sort": "No sort",
|
"no_sort": "Pas trier",
|
||||||
"label": "Sort by",
|
"label": "Trier par",
|
||||||
"fire": "Fire",
|
"fire": "Feu",
|
||||||
"maneuverability": "Maneuverability",
|
"maneuverability": "Manœuvrabilité",
|
||||||
"survivability": "Survivability"
|
"survivability": "Survivabilité"
|
||||||
},
|
},
|
||||||
"direction": {
|
"direction": {
|
||||||
"label": "Direction",
|
"label": "Direction",
|
||||||
"ascending": "Ascending",
|
"ascending": "Croissant",
|
||||||
"descending": "Descending"
|
"descending": "Décroissant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"warnings": {
|
"warnings": {
|
||||||
"test": "Tanks in testing are subject to change and may not represent the final product."
|
"test": "Les chars en phase de test sont susceptibles de changer et peuvent ne pas représenter le produit final."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Rejoins des gens géniaux",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Rejoins des gens super géniaux",
|
||||||
"action": "Discord"
|
"action": "Discord"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"credit": "By {name} ❤️"
|
"credit": "Par {name}❤️"
|
||||||
},
|
},
|
||||||
"hot_tanks": {
|
"hot_tanks": {
|
||||||
"title": "Popular tanks",
|
"title": "Chars populaire",
|
||||||
"subtitle": "Last {days} days"
|
"subtitle": "Dernier {days} jours"
|
||||||
},
|
},
|
||||||
"other_projects": {
|
"other_projects": {
|
||||||
"title": "Other cool projects"
|
"title": "Autres projets géniaux"
|
||||||
},
|
},
|
||||||
"help_translate": "Please help translate {name} to {language}!",
|
"help_translate": "Aidez-nous à traduire {name} en {language} !",
|
||||||
"seo_description": "Tools for everything World of Tanks Blitz"
|
"seo_description": "Outils pour tout World of Tanks Blitz"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"patches": {
|
"patches": {
|
||||||
"name": "BlitzKit Changelogs",
|
"name": "Journal des modifications",
|
||||||
"seo_description": "Changelogs for BlitzKit Discord bot and website"
|
"seo_description": "Changelogs pour le bot Discord BlitzKit et site Web"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
@@ -314,22 +314,22 @@
|
|||||||
"remove": "Remove played",
|
"remove": "Remove played",
|
||||||
"tracking": "BlitzKit is tracking your battles",
|
"tracking": "BlitzKit is tracking your battles",
|
||||||
"sort": {
|
"sort": {
|
||||||
"button": "Sort by",
|
"button": "Trier par",
|
||||||
"last_played": "Last played",
|
"last_played": "Dernier joué",
|
||||||
"least_played": "Least played"
|
"least_played": "Least played"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"index": "#",
|
"index": "#",
|
||||||
"never": "Never",
|
"never": "Jamais",
|
||||||
"tank": "Tank",
|
"tank": "Char d'assaut",
|
||||||
"last_played": "Last played",
|
"last_played": "Dernier joué",
|
||||||
"battles": "Career battles",
|
"battles": "Career battles",
|
||||||
"unrecorded": "--",
|
"unrecorded": "--",
|
||||||
"wins": "Victoires"
|
"wins": "Victoires"
|
||||||
},
|
},
|
||||||
"large_warning": {
|
"large_warning": {
|
||||||
"title": "That's a lot of tanks!",
|
"title": "That's a lot of tanks!",
|
||||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
"description": "Êtes-vous sûr de vouloir créer une playlist de {count} chars ? C'est {time} heures de jeu à 50% de taux de victoire !",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"proceed": "Proceed"
|
"proceed": "Proceed"
|
||||||
},
|
},
|
||||||
@@ -362,7 +362,7 @@
|
|||||||
},
|
},
|
||||||
"stats": "{correct} / {total} guessed correctly",
|
"stats": "{correct} / {total} guessed correctly",
|
||||||
"help": {
|
"help": {
|
||||||
"title": "Partially reveal tank?",
|
"title": "Révéler partiellement le char ?",
|
||||||
"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",
|
"cancel": "Cancel",
|
||||||
"reveal": "Reveal"
|
"reveal": "Reveal"
|
||||||
@@ -373,7 +373,7 @@
|
|||||||
},
|
},
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"search": "Search a stat...",
|
"search": "Search a stat...",
|
||||||
"name": "Tankopedia",
|
"name": "Tankopédie",
|
||||||
"button": "Find tanks",
|
"button": "Find tanks",
|
||||||
"description": "Blitz tank encyclopedia",
|
"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",
|
||||||
@@ -657,10 +657,10 @@
|
|||||||
"loader": "Loader"
|
"loader": "Loader"
|
||||||
},
|
},
|
||||||
"maneuverability": {
|
"maneuverability": {
|
||||||
"title": "Maneuverability"
|
"title": "Manœuvrabilité"
|
||||||
},
|
},
|
||||||
"survivability": {
|
"survivability": {
|
||||||
"title": "Survivability"
|
"title": "Survivabilité"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
@@ -679,7 +679,8 @@
|
|||||||
"none": "No deltas",
|
"none": "No deltas",
|
||||||
"percentage": "Percentage",
|
"percentage": "Percentage",
|
||||||
"absolute": "Absolute"
|
"absolute": "Absolute"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
"title": "No tanks selected",
|
"title": "No tanks selected",
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"motto": "Everything World of Tanks Blitz",
|
"motto": "Everything World of Tanks Blitz",
|
||||||
"copyright": "Unaffiliated with Wargaming or any service",
|
"association": "An Open Collective Project",
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
|
|||||||
@@ -679,7 +679,8 @@
|
|||||||
"none": "差分なし",
|
"none": "差分なし",
|
||||||
"percentage": "割合",
|
"percentage": "割合",
|
||||||
"absolute": "絶対値"
|
"absolute": "絶対値"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
"title": "車輌が選択されていません",
|
"title": "車輌が選択されていません",
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"motto": "Everything World of Tanks Blitz",
|
"motto": "Everything World of Tanks Blitz",
|
||||||
"copyright": "Wargamingやその他のサービスとは関係ありません",
|
"association": "An Open Collective Project",
|
||||||
"about": {
|
"about": {
|
||||||
"title": "概要",
|
"title": "概要",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
|
|||||||
+356
-355
File diff suppressed because it is too large.
Load diff
@@ -679,7 +679,8 @@
|
|||||||
"none": "Sem deltas",
|
"none": "Sem deltas",
|
||||||
"percentage": "Porcentagem",
|
"percentage": "Porcentagem",
|
||||||
"absolute": "Absoluto"
|
"absolute": "Absoluto"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
"title": "Nenhum tanque selecionado",
|
"title": "Nenhum tanque selecionado",
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"motto": "Tudo World of Tanks Blitz",
|
"motto": "Tudo World of Tanks Blitz",
|
||||||
"copyright": "Não afiliado à Wargaming ou qualquer serviço",
|
"association": "An Open Collective Project",
|
||||||
"about": {
|
"about": {
|
||||||
"title": "Sobre",
|
"title": "Sobre",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"gun_types": {
|
"gun_types": {
|
||||||
"auto_loader": "Барабан",
|
"auto_loader": "Барабан",
|
||||||
"auto_reloader": "Барабан с дозарядкой",
|
"auto_reloader": "Автомат заряжания",
|
||||||
"regular": "Цикличный"
|
"regular": "Цикличный"
|
||||||
},
|
},
|
||||||
"shells": {
|
"shells": {
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"branches": {
|
"branches": {
|
||||||
"beta": "Бета",
|
"beta": "Бета",
|
||||||
"development": "Разработка",
|
"development": "Разработка",
|
||||||
"opentest": "Открытый тест",
|
"opentest": "OpenTest",
|
||||||
"preview": "Предпросмотр",
|
"preview": "Предпросмотр",
|
||||||
"production": "Производство"
|
"production": "Производство"
|
||||||
},
|
},
|
||||||
@@ -304,9 +304,9 @@
|
|||||||
"tools": {
|
"tools": {
|
||||||
"credits": {
|
"credits": {
|
||||||
"name": "Credits",
|
"name": "Credits",
|
||||||
"button": "View people",
|
"button": "Посмотреть людей",
|
||||||
"description": "Contributors of BlitzKit",
|
"description": "Участники проекта blitzkit",
|
||||||
"seo_description": "BlitzKit is the work of many"
|
"seo_description": "BlitzKit создан усилиями многих"
|
||||||
},
|
},
|
||||||
"playlist": {
|
"playlist": {
|
||||||
"reset": "Сброс",
|
"reset": "Сброс",
|
||||||
@@ -624,12 +624,12 @@
|
|||||||
"hullTraverseSoftTerrain": "На мягком грунте",
|
"hullTraverseSoftTerrain": "На мягком грунте",
|
||||||
"health": "Прочность",
|
"health": "Прочность",
|
||||||
"fireChance": "Вероятность возгорания",
|
"fireChance": "Вероятность возгорания",
|
||||||
"trackRepairTime": "Track repair time",
|
"trackRepairTime": "Время ремонта гусениц",
|
||||||
"viewRange": "Радиус обзора",
|
"viewRange": "Радиус обзора",
|
||||||
"camouflageStill": "Когда неподвижен",
|
"camouflageStill": "Когда неподвижен",
|
||||||
"camouflageMoving": "В движении",
|
"camouflageMoving": "В движении",
|
||||||
"camouflageShootingStill": "Shooting while still",
|
"camouflageShootingStill": "Стрельба без движения",
|
||||||
"camouflageShootingMoving": "Shooting while moving",
|
"camouflageShootingMoving": "Стрельба в движении",
|
||||||
"camouflageCaughtOnFire": "При пожаре",
|
"camouflageCaughtOnFire": "При пожаре",
|
||||||
"width": "Ширина",
|
"width": "Ширина",
|
||||||
"height": "Высота",
|
"height": "Высота",
|
||||||
@@ -679,7 +679,8 @@
|
|||||||
"none": "Без дельты",
|
"none": "Без дельты",
|
||||||
"percentage": "Проценты",
|
"percentage": "Проценты",
|
||||||
"absolute": "Абсолютные"
|
"absolute": "Абсолютные"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
"title": "Нет выбранных танков",
|
"title": "Нет выбранных танков",
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"motto": "Всё о World of Tanks Blitz",
|
"motto": "Всё о World of Tanks Blitz",
|
||||||
"copyright": "Не связан с Wargaming или иным сервисом",
|
"association": "Открытый коллективный проект",
|
||||||
"about": {
|
"about": {
|
||||||
"title": "О сайте",
|
"title": "О сайте",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
"beta": "Бета",
|
"beta": "Бета",
|
||||||
"development": "Розробка",
|
"development": "Розробка",
|
||||||
"opentest": "Відкритий тест",
|
"opentest": "Відкритий тест",
|
||||||
"preview": "Preview",
|
"preview": "Перегляд",
|
||||||
"production": "Production"
|
"production": "Виробництво"
|
||||||
},
|
},
|
||||||
"locales": {
|
"locales": {
|
||||||
"en": "Англійська",
|
"en": "Англійська",
|
||||||
@@ -206,13 +206,13 @@
|
|||||||
"swap_dialog_title": "Резервуар підміни",
|
"swap_dialog_title": "Резервуар підміни",
|
||||||
"clear_filters": "Очистити",
|
"clear_filters": "Очистити",
|
||||||
"login": "Увійдіть, щоб скористатися",
|
"login": "Увійдіть, щоб скористатися",
|
||||||
"search_bar_hint": "Шукайте техніку...",
|
"search_bar_hint": "Пошук техніки...",
|
||||||
"recent": "Недавно переглянуті",
|
"recent": "Недавно переглянуті",
|
||||||
"select_all": "Вибрати все",
|
"select_all": "Вибрати все",
|
||||||
"count_singular": "1 танк",
|
"count_singular": "1 танк",
|
||||||
"count_plural": "Танків: {count} ",
|
"count_plural": "Танків: {count} ",
|
||||||
"sorting_by": "Відсортувати за {name}",
|
"sorting_by": "Відсортувати за {name}",
|
||||||
"clear": "Clear",
|
"clear": "Очистити",
|
||||||
"tier": "Рівень {tier}",
|
"tier": "Рівень {tier}",
|
||||||
"passive": "",
|
"passive": "",
|
||||||
"active": "{name}{name}",
|
"active": "{name}{name}",
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
"fire.premiumPenetration": "преміум пробиття",
|
"fire.premiumPenetration": "преміум пробиття",
|
||||||
"fire.tertiaryPenetration": "tertiary penetration",
|
"fire.tertiaryPenetration": "tertiary penetration",
|
||||||
"fire.damage": "шкода",
|
"fire.damage": "шкода",
|
||||||
"fire.moduleDamage": "шкода по модулях",
|
"fire.moduleDamage": "шкода по модулям",
|
||||||
"fire.shellVelocity": "швидкість польоту снаряда",
|
"fire.shellVelocity": "швидкість польоту снаряда",
|
||||||
"fire.shellRange": "дальність польоту",
|
"fire.shellRange": "дальність польоту",
|
||||||
"fire.shellCapacity": "кількість снарядів",
|
"fire.shellCapacity": "кількість снарядів",
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
"maneuverability.forwardsSpeed": "максимальна швидкість вперед",
|
"maneuverability.forwardsSpeed": "максимальна швидкість вперед",
|
||||||
"maneuverability.backwardsSpeed": "максимальна швидкість назад",
|
"maneuverability.backwardsSpeed": "максимальна швидкість назад",
|
||||||
"maneuverability.power": "потужність двигуна",
|
"maneuverability.power": "потужність двигуна",
|
||||||
"maneuverability.powerToWeight": "притома потужність",
|
"maneuverability.powerToWeight": "питома потужність",
|
||||||
"maneuverability.weight": "маса",
|
"maneuverability.weight": "маса",
|
||||||
"maneuverability.traverseSpeed": "швидкість повороту"
|
"maneuverability.traverseSpeed": "швидкість повороту"
|
||||||
},
|
},
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
"tools": {
|
"tools": {
|
||||||
"credits": {
|
"credits": {
|
||||||
"name": "Credits",
|
"name": "Credits",
|
||||||
"button": "View people",
|
"button": "Переглянути людей",
|
||||||
"description": "Contributors of BlitzKit",
|
"description": "Contributors of BlitzKit",
|
||||||
"seo_description": "BlitzKit is the work of many"
|
"seo_description": "BlitzKit is the work of many"
|
||||||
},
|
},
|
||||||
@@ -349,25 +349,25 @@
|
|||||||
"randomize": "Рандомізувати"
|
"randomize": "Рандомізувати"
|
||||||
},
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess",
|
"name": "Вгадайка",
|
||||||
"button": "What could it be?",
|
"button": "Що це може бути?",
|
||||||
"description": "Silhouette mini-game",
|
"description": "Мінігра Силует",
|
||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Зіграти",
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Type to guess...",
|
"placeholder": "Час Відгадувати",
|
||||||
"skip": "Пропустити",
|
"skip": "Пропустити",
|
||||||
"guess": "Guess",
|
"guess": "Відгадати",
|
||||||
"next": "Наступний",
|
"next": "Наступний",
|
||||||
"no_results": "Немає результатів"
|
"no_results": "Немає результатів"
|
||||||
},
|
},
|
||||||
"stats": "{correct} / {total} correct, {streak} streak",
|
"stats": "{correct} / {total} відгадано, {streak} серія",
|
||||||
"help": {
|
"help": {
|
||||||
"title": "Partially reveal tank?",
|
"title": "Розкрити танк частково?",
|
||||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
"description": "Показує танк без назви. Цей раунд",
|
||||||
"cancel": "",
|
"cancel": "Скасувати",
|
||||||
"reveal": "Рівень"
|
"reveal": "Показати"
|
||||||
},
|
},
|
||||||
"clear": "Clear",
|
"clear": "Очистити",
|
||||||
"select_all": "Вибрати все",
|
"select_all": "Вибрати все",
|
||||||
"tier": "Рівень {tier}"
|
"tier": "Рівень {tier}"
|
||||||
},
|
},
|
||||||
@@ -389,9 +389,9 @@
|
|||||||
"rotator": {
|
"rotator": {
|
||||||
"hull": "Корпус",
|
"hull": "Корпус",
|
||||||
"turret": "Башта",
|
"turret": "Башта",
|
||||||
"hard": "Жорстко",
|
"hard": "По дорозі",
|
||||||
"medium": "Середній",
|
"medium": "По ґрунту",
|
||||||
"soft": "Ніжно",
|
"soft": "По воді",
|
||||||
"effective": "Ефективний"
|
"effective": "Ефективний"
|
||||||
},
|
},
|
||||||
"ricochet": {
|
"ricochet": {
|
||||||
@@ -437,7 +437,7 @@
|
|||||||
"dynamic": {
|
"dynamic": {
|
||||||
"name": "Динамічна броня",
|
"name": "Динамічна броня",
|
||||||
"custom_shell": {
|
"custom_shell": {
|
||||||
"type": "Вид",
|
"type": "Тип",
|
||||||
"caliber": "Калібр",
|
"caliber": "Калібр",
|
||||||
"penetration": "Бронепробиття",
|
"penetration": "Бронепробиття",
|
||||||
"normalization": "Нормалізація",
|
"normalization": "Нормалізація",
|
||||||
@@ -493,13 +493,13 @@
|
|||||||
"nation": "Нація",
|
"nation": "Нація",
|
||||||
"class": "Клас",
|
"class": "Клас",
|
||||||
"tier": "Рівень",
|
"tier": "Рівень",
|
||||||
"type": "Вид",
|
"type": "Тип",
|
||||||
"dev_id": "DEV: ID",
|
"dev_id": "DEV: ID",
|
||||||
"dev_name": "DEV: NAME",
|
"dev_name": "DEV: NAME",
|
||||||
"purchase": "Purchase price",
|
"purchase": "Ціна покупки",
|
||||||
"restoration": "Restoration price",
|
"restoration": "Restoration price",
|
||||||
"sale": "Sale price",
|
"sale": "Ціна продажу",
|
||||||
"research": "Research XP"
|
"research": "Дослідження"
|
||||||
},
|
},
|
||||||
"tech_tree": {
|
"tech_tree": {
|
||||||
"title": "Звичайний",
|
"title": "Звичайний",
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
"title": "Здібності ігрового режиму"
|
"title": "Здібності ігрового режиму"
|
||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Відеоогляди",
|
||||||
"join": "Apply to {name} YouTuber program"
|
"join": "Apply to {name} YouTuber program"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -522,12 +522,12 @@
|
|||||||
},
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"title": "Модулі",
|
"title": "Модулі",
|
||||||
"stock": "Stock",
|
"stock": "Базові",
|
||||||
"upgrade": "Upgrade"
|
"upgrade": "Топовий"
|
||||||
},
|
},
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"title": "Обладнання",
|
"title": "Обладнання",
|
||||||
"clear": "Clear"
|
"clear": "Очистити"
|
||||||
},
|
},
|
||||||
"provisions": {
|
"provisions": {
|
||||||
"title": "Амуніція",
|
"title": "Амуніція",
|
||||||
@@ -553,88 +553,88 @@
|
|||||||
"compare": {
|
"compare": {
|
||||||
"label": "Compare against",
|
"label": "Compare against",
|
||||||
"class": "{tier} {class}",
|
"class": "{tier} {class}",
|
||||||
"tier": "All tier {tier}",
|
"tier": "Весь рівень{tier}",
|
||||||
"all": "Усі танки"
|
"all": "Усі танки"
|
||||||
},
|
},
|
||||||
"values": {
|
"values": {
|
||||||
"shell_index": "Shell {index}",
|
"shell_index": "Shell {index}",
|
||||||
"at_distance": "At {distance}m",
|
"at_distance": "At {distance}m",
|
||||||
"at_distance_slider_label": "Відстань",
|
"at_distance_slider_label": "Відстань",
|
||||||
"dispersion_at_distance": "Dispersion at {distance}m",
|
"dispersion_at_distance": "Розкид на {distance}м",
|
||||||
"gun_flexibility": "Gun flexibility",
|
"gun_flexibility": "Кути наведення",
|
||||||
"speed": "Швидкість",
|
"speed": "Швидкість",
|
||||||
"terrain_coefficients": "Terrain coefficients",
|
"terrain_coefficients": "Прохідність",
|
||||||
"raw_terrain_coefficients": "Raw terrain coefficients",
|
"raw_terrain_coefficients": "Raw terrain coefficients",
|
||||||
"power_to_weight_ratio": "Power to weight ratio",
|
"power_to_weight_ratio": "Питома потужність",
|
||||||
"hull_traverse_speed": "Hull traverse speed",
|
"hull_traverse_speed": "Швидкість обертання корпуса",
|
||||||
"camouflage": "Камуфляж",
|
"camouflage": "Камуфляж",
|
||||||
"reload": "Час перезаряджання",
|
"reload": "Час перезаряджання",
|
||||||
"crewCount": "Crew count",
|
"crewCount": "Розмір екіпажу",
|
||||||
"shellNormalization": "Нормалізація",
|
"shellNormalization": "Нормалізація",
|
||||||
"shellRicochet": "Рикошет",
|
"shellRicochet": "Рикошет",
|
||||||
"dpm": "DPM",
|
"dpm": "DPM",
|
||||||
"dpmEffective": "Effective at 60s",
|
"dpmEffective": "Effective at 60s",
|
||||||
"peekabooDpm": "Peekaboo DPM",
|
"peekabooDpm": "Peekaboo DPM",
|
||||||
"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.",
|
"peekabooDpmInfo": "Шкода на хвилину за умови пострілу тільки після повторного маскування (або після повного перезаряджання, якщо воно триваліше) — не менше 10 секунд між пострілами.",
|
||||||
"shells": "Снаряди",
|
"shells": "Снаряди",
|
||||||
"mostOptimalShellIndex": "Most optimal shell",
|
"mostOptimalShellIndex": "Most optimal shell",
|
||||||
"shellReloads": "Reloads",
|
"shellReloads": "Reloads",
|
||||||
"shellReload": "Час перезаряджання",
|
"shellReload": "Час перезаряджання",
|
||||||
"intraClip": "Intra-clip",
|
"intraClip": "Між пострілами",
|
||||||
"caliber": "Калібр",
|
"caliber": "Калібр",
|
||||||
"penetration": "Бронепробиття",
|
"penetration": "Бронепробиття",
|
||||||
"damage": "Шкода",
|
"damage": "Шкода",
|
||||||
"damageWithoutAssault": "Шкода без урахування золотих снарядів",
|
"damageWithoutAssault": "Шкода без урахування золотих снарядів",
|
||||||
"clipDamage": "Clipping potential",
|
"clipDamage": "Шкода за барабан",
|
||||||
"moduleDamage": "Шкода по модулях",
|
"moduleDamage": "Шкода по модулям",
|
||||||
"explosionRadius": "Радіус розльоту уламків",
|
"explosionRadius": "Радіус розльоту уламків",
|
||||||
"shellVelocity": "Швидкість польоту снаряда",
|
"shellVelocity": "Швидкість польоту снаряда",
|
||||||
"aimTime": "Час зведення",
|
"aimTime": "Час зведення",
|
||||||
"dispersion": "Нерухомо",
|
"dispersion": "Нерухомо",
|
||||||
"dispersionMoving": "У русі",
|
"dispersionMoving": "У русі",
|
||||||
"dispersionHullTraversing": "Hull traversing",
|
"dispersionHullTraversing": "Обертання корпусу",
|
||||||
"dispersionTurretTraversing": "Обертання башти",
|
"dispersionTurretTraversing": "Обертання башти",
|
||||||
"dispersionShooting": "Shooting",
|
"dispersionShooting": "Після пострілу",
|
||||||
"dispersionGunDamaged": "Gun damaged",
|
"dispersionGunDamaged": "Гармата пошкоджена",
|
||||||
"gunDepression": "Depression",
|
"gunDepression": "Вниз",
|
||||||
"gunElevation": "Elevation",
|
"gunElevation": "Вгору",
|
||||||
"gunFrontalDepression": "Frontal depression",
|
"gunFrontalDepression": "КВН вниз спереду",
|
||||||
"gunFrontalElevation": "Frontal elevation",
|
"gunFrontalElevation": "КВН вгору спереду",
|
||||||
"gunRearDepression": "Rear depression",
|
"gunRearDepression": "КВН вниз позаду",
|
||||||
"gunRearElevation": "Rear elevation",
|
"gunRearElevation": "КВН вгору позаду",
|
||||||
"azimuthLeft": "Azimuth left",
|
"azimuthLeft": "Azimuth left",
|
||||||
"azimuthRight": "Azimuth right",
|
"azimuthRight": "Azimuth right",
|
||||||
"speedForwards": "Forwards",
|
"speedForwards": "Вперед",
|
||||||
"speedBackwards": "Backwards",
|
"speedBackwards": "Назад",
|
||||||
"hardTerrainCoefficient": "Hard",
|
"hardTerrainCoefficient": "По дорозі",
|
||||||
"hardTerrainCoefficientRaw": "Hard",
|
"hardTerrainCoefficientRaw": "По дорозі",
|
||||||
"mediumTerrainCoefficient": "Середній",
|
"mediumTerrainCoefficient": "По ґрунту",
|
||||||
"mediumTerrainCoefficientRaw": "Середній",
|
"mediumTerrainCoefficientRaw": "По ґрунту",
|
||||||
"softTerrainCoefficient": "Soft",
|
"softTerrainCoefficient": "По воді",
|
||||||
"softTerrainCoefficientRaw": "Soft",
|
"softTerrainCoefficientRaw": "По воді",
|
||||||
"enginePower": "Engine power",
|
"enginePower": "Потужність двигуна",
|
||||||
"powerToWeightRatioHardTerrain": "Hard",
|
"powerToWeightRatioHardTerrain": "По дорозі",
|
||||||
"powerToWeightRatioMediumTerrain": "Середній",
|
"powerToWeightRatioMediumTerrain": "По ґрунту",
|
||||||
"powerToWeightRatioSoftTerrain": "Soft",
|
"powerToWeightRatioSoftTerrain": "По воді",
|
||||||
"weight": "Маса",
|
"weight": "Маса",
|
||||||
"turretTraverseSpeed": "Turret traverse speed",
|
"turretTraverseSpeed": "Швидкість обертання башти",
|
||||||
"gunTraverseSpeed": "Gun traverse speed",
|
"gunTraverseSpeed": "Gun traverse speed",
|
||||||
"hullTraverseHardTerrain": "Hard",
|
"hullTraverseHardTerrain": "Поворот корпусу на дорозі",
|
||||||
"hullTraverseMediumTerrain": "Середній",
|
"hullTraverseMediumTerrain": "По ґрунту",
|
||||||
"hullTraverseSoftTerrain": "Soft",
|
"hullTraverseSoftTerrain": "По воді",
|
||||||
"health": "Здоров'я",
|
"health": "Здоров'я",
|
||||||
"fireChance": "Fire chance",
|
"fireChance": "Імовірність загоряння",
|
||||||
"trackRepairTime": "Track repair time",
|
"trackRepairTime": "Час ремонту гусениці",
|
||||||
"viewRange": "View range",
|
"viewRange": "Радіус огляду",
|
||||||
"camouflageStill": "Нерухомо",
|
"camouflageStill": "Нерухомо",
|
||||||
"camouflageMoving": "У русі",
|
"camouflageMoving": "У русі",
|
||||||
"camouflageShootingStill": "Нерухомо",
|
"camouflageShootingStill": "Постріл в нерухомості",
|
||||||
"camouflageShootingMoving": "У русі",
|
"camouflageShootingMoving": "Постріл у русі",
|
||||||
"camouflageCaughtOnFire": "Caught on fire",
|
"camouflageCaughtOnFire": "Під час загоряння",
|
||||||
"width": "Ширина",
|
"width": "Ширина",
|
||||||
"height": "Height",
|
"height": "Висота",
|
||||||
"length": "Length",
|
"length": "Довжина",
|
||||||
"volume": "Volume",
|
"volume": "Об'єм",
|
||||||
"shellRange": "Shell range",
|
"shellRange": "Shell range",
|
||||||
"shellCapacity": "Shell capacity",
|
"shellCapacity": "Shell capacity",
|
||||||
"gunType": "Тип гармати",
|
"gunType": "Тип гармати",
|
||||||
@@ -644,16 +644,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
"title": "Firepower"
|
"title": "Вогнева міць"
|
||||||
},
|
},
|
||||||
"crew": {
|
"crew": {
|
||||||
"title": "Crew training",
|
"title": "Майстерність екіпажу",
|
||||||
"info": "represents the other roles a crew member can take.",
|
"info": "відображає інші ролі, які може виконувати член екіпажу.",
|
||||||
"count": "Crew count",
|
"count": "Розмір команди",
|
||||||
"commander": "Командир",
|
"commander": "Командир",
|
||||||
"gunner": "Навідник",
|
"gunner": "Навідник",
|
||||||
"driver": "Водій",
|
"driver": "Водій",
|
||||||
"radioman": "Radioman",
|
"radioman": "Радист",
|
||||||
"loader": "Заряджаючий"
|
"loader": "Заряджаючий"
|
||||||
},
|
},
|
||||||
"maneuverability": {
|
"maneuverability": {
|
||||||
@@ -666,20 +666,21 @@
|
|||||||
"compare": {
|
"compare": {
|
||||||
"name": "Порівняти",
|
"name": "Порівняти",
|
||||||
"button": "Порівняти танки",
|
"button": "Порівняти танки",
|
||||||
"description": "Compare tank statistics",
|
"description": "Порівняти статистику танків",
|
||||||
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
|
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
|
||||||
"actions": {
|
"actions": {
|
||||||
"add": {
|
"add": {
|
||||||
"button": "Додати",
|
"button": "Додати",
|
||||||
"title": "Add tanks",
|
"title": "Додати танки",
|
||||||
"description": "Select tanks to add to comparison"
|
"description": "Виберіть танки, щоб додати для порівняння"
|
||||||
},
|
},
|
||||||
"clear": "Clear",
|
"clear": "Очистити",
|
||||||
"deltas": {
|
"deltas": {
|
||||||
"none": "No deltas",
|
"none": "No deltas",
|
||||||
"percentage": "Відсоток",
|
"percentage": "Відсоток",
|
||||||
"absolute": "Absolute"
|
"absolute": "Absolute"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
"title": "Не вибрано жодного танку",
|
"title": "Не вибрано жодного танку",
|
||||||
@@ -687,7 +688,7 @@
|
|||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"clear": "Clear",
|
"clear": "Очистити",
|
||||||
"apply_to_all": "Apply to all"
|
"apply_to_all": "Apply to all"
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
@@ -696,8 +697,8 @@
|
|||||||
"equalize": "Equalize"
|
"equalize": "Equalize"
|
||||||
},
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"stock": "Stock",
|
"stock": "Базові",
|
||||||
"upgrade": "Upgrade"
|
"upgrade": "Топовий"
|
||||||
},
|
},
|
||||||
"provisions": {
|
"provisions": {
|
||||||
"clear": "Clear this tank",
|
"clear": "Clear this tank",
|
||||||
@@ -710,9 +711,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"performance": {
|
"performance": {
|
||||||
"name": "Performance",
|
"name": "Результативність",
|
||||||
"button": "Переглянути танки",
|
"button": "Переглянути танки",
|
||||||
"description": "Tank performance statistics",
|
"description": "Статистика Результативності танків",
|
||||||
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
|
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
|
||||||
"estimation": {
|
"estimation": {
|
||||||
"body": "Estimated stats based on {count} players. Updated {age} ago.",
|
"body": "Estimated stats based on {count} players. Updated {age} ago.",
|
||||||
@@ -720,7 +721,7 @@
|
|||||||
"minutes": "{count} minutes"
|
"minutes": "{count} minutes"
|
||||||
},
|
},
|
||||||
"player_count": {
|
"player_count": {
|
||||||
"label": "Player count"
|
"label": "Кількість гравців"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"tanks": {
|
"tanks": {
|
||||||
@@ -738,8 +739,8 @@
|
|||||||
"accuracy": "Точність",
|
"accuracy": "Точність",
|
||||||
"shots": "Постріли",
|
"shots": "Постріли",
|
||||||
"hits": "Пробиття",
|
"hits": "Пробиття",
|
||||||
"damage_ratio": "Dmg. ratio",
|
"damage_ratio": "Коефіцієнт шкоди",
|
||||||
"damage_taken": "Dmg. taken",
|
"damage_taken": "Шкоди отримано",
|
||||||
"capture_points": "Cap points"
|
"capture_points": "Cap points"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -849,7 +850,7 @@
|
|||||||
"name": "Breakdown",
|
"name": "Breakdown",
|
||||||
"options": {
|
"options": {
|
||||||
"width": "Ширина",
|
"width": "Ширина",
|
||||||
"height": "Height",
|
"height": "Висота",
|
||||||
"showTotal": "Show total",
|
"showTotal": "Show total",
|
||||||
"listGap": "List gap",
|
"listGap": "List gap",
|
||||||
"listMaxTanks": "Max tanks",
|
"listMaxTanks": "Max tanks",
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"motto": "Everything World of Tanks Blitz",
|
"motto": "Everything World of Tanks Blitz",
|
||||||
"copyright": "Unaffiliated with Wargaming or any service",
|
"association": "An Open Collective Project",
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
|
|||||||
@@ -679,7 +679,8 @@
|
|||||||
"none": "无变化",
|
"none": "无变化",
|
||||||
"percentage": "百分比",
|
"percentage": "百分比",
|
||||||
"absolute": "绝对值"
|
"absolute": "绝对值"
|
||||||
}
|
},
|
||||||
|
"sync_shells": "Sync shells"
|
||||||
},
|
},
|
||||||
"no_tanks": {
|
"no_tanks": {
|
||||||
"title": "未选择坦克",
|
"title": "未选择坦克",
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"motto": "《坦克世界:闪击战》全方位工具",
|
"motto": "《坦克世界:闪击战》全方位工具",
|
||||||
"copyright": "本网站与 Wargaming战游网 或任何服务无关",
|
"association": "An Open Collective Project",
|
||||||
"about": {
|
"about": {
|
||||||
"title": "关于",
|
"title": "关于",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
|
"download-smart-dlc": "bun src/downloadSmartDLC/index.ts",
|
||||||
|
"vfs": "bun src/vfs/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",
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { dvp } from "@blitzkit/closed";
|
||||||
|
import { assertSecret } from "@blitzkit/core";
|
||||||
|
import { mkdir, writeFile } from "fs/promises";
|
||||||
|
import { parse as parsePath } from "path";
|
||||||
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
||||||
|
import { RegionsPublish } from "../types/regionsPublish";
|
||||||
|
import { vfs } from "../vfs/vfs";
|
||||||
|
|
||||||
|
const CLIENT_DIR = assertSecret(import.meta.env.CLIENT_DIR);
|
||||||
|
|
||||||
|
const regionsPublish = await vfs.yaml<RegionsPublish>(
|
||||||
|
"Data/regions_publish.yaml",
|
||||||
|
);
|
||||||
|
const [smartDlcUrl] = regionsPublish.Common.smartDlcUrls;
|
||||||
|
|
||||||
|
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(
|
||||||
|
`${smartDlcUrl}/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(`${smartDlcUrl}/${data.dx11}`).then((response) =>
|
||||||
|
response.arrayBuffer(),
|
||||||
|
);
|
||||||
|
const dvpd = await fetch(
|
||||||
|
`${smartDlcUrl}/${data.dx11.replace(".dvpm", ".dvpd")}`,
|
||||||
|
).then((response) => response.arrayBuffer());
|
||||||
|
const files = await dvp(dvpm, dvpd);
|
||||||
|
|
||||||
|
for (const { path, data } of files) {
|
||||||
|
console.log(`Patching "${path}"...`);
|
||||||
|
|
||||||
|
const { dir } = parsePath(path);
|
||||||
|
|
||||||
|
await mkdir(`${CLIENT_DIR}/Data/${dir}`, { recursive: true });
|
||||||
|
|
||||||
|
const buffer = Buffer.from(data);
|
||||||
|
|
||||||
|
await writeFile(`${CLIENT_DIR}/Data/${path}`, new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("patches done for patch");
|
||||||
|
|
||||||
|
if ("dynamicContentLocalizationsDir" in data) {
|
||||||
|
console.log(
|
||||||
|
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
|
||||||
|
);
|
||||||
|
const localizationsResponse = await fetch(
|
||||||
|
`${smartDlcUrl}/${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(`${CLIENT_DIR}/Data/Strings`, { recursive: true });
|
||||||
|
console.log("mkdir");
|
||||||
|
await writeFile(
|
||||||
|
`${CLIENT_DIR}/Data/Strings/en.yaml`,
|
||||||
|
new Uint8Array(Buffer.from(patchedContent)),
|
||||||
|
);
|
||||||
|
console.log("file");
|
||||||
|
}
|
||||||
|
|
||||||
|
patchIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
vfs.dispose();
|
||||||
|
process.exit(0);
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
syncShellSlot(draft.members, shellIndex);
|
|
||||||
} else {
|
|
||||||
draft.members[index].shell = thisShell;
|
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>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ 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 = 1792598400 * 1000;
|
||||||
const HREF = "https://preview.blitzkit.app/tanks";
|
|
||||||
|
|
||||||
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
|
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
|
||||||
@@ -32,7 +31,7 @@ export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
|
|
||||||
string = (
|
string = (
|
||||||
<>
|
<>
|
||||||
in{" "}
|
revealed in{" "}
|
||||||
<Code highContrast>
|
<Code highContrast>
|
||||||
{hours}h {minutes}m {seconds}s
|
{hours}h {minutes}m {seconds}s
|
||||||
</Code>
|
</Code>
|
||||||
@@ -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://preview.blitzkit.app/tanks"
|
||||||
underline="none"
|
underline="none"
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -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}{" "}
|
Tier XIs {!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
|
Get ready to go to the Tankopedia!
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
+1
-3
@@ -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];
|
||||||
|
|||||||
+2
-24
@@ -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">
|
|
||||||
<StatScale
|
|
||||||
values={values}
|
|
||||||
value={uhWhatDoICallThisVariable}
|
|
||||||
lowerIsBetter={deltaType === "lowerIsBetter"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Progress
|
<Progress
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="1"
|
size="1"
|
||||||
value={goodness * 100}
|
value={goodness * 100}
|
||||||
color={color}
|
color={color}
|
||||||
style={{
|
style={{ height: "0.125rem", opacity: 0.5 }}
|
||||||
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)} /{" "}
|
||||||
|
|||||||
-91
@@ -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",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
+6
-29
@@ -13,10 +13,7 @@ const camouflageDefinitions = await api.camouflageDefinitions();
|
|||||||
|
|
||||||
export function SkinSwitcher() {
|
export function SkinSwitcher() {
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
|
const skin = Tankopedia.use((state) => state.requestedSkin);
|
||||||
const attachment = Tankopedia.use(
|
|
||||||
(state) => state.loaders.attachment.requested,
|
|
||||||
);
|
|
||||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
const { unwrap } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
@@ -27,18 +24,11 @@ export function SkinSwitcher() {
|
|||||||
(camouflage) => unwrap(camouflage.name!),
|
(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;
|
if (skins.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
gap="3"
|
|
||||||
top="50%"
|
top="50%"
|
||||||
right={
|
right={
|
||||||
disturbed && requestedDisplay === TankopediaDisplay.Model
|
disturbed && requestedDisplay === TankopediaDisplay.Model
|
||||||
@@ -52,24 +42,18 @@ export function SkinSwitcher() {
|
|||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{groups.map((group) => (
|
{skins.map((camouflage, index) => {
|
||||||
<Flex key={group[0].id} direction="column">
|
|
||||||
{group.map((camouflage, index) => {
|
|
||||||
const name = unwrap(camouflage.name!).replace(
|
const name = unwrap(camouflage.name!).replace(
|
||||||
"%(camo_num)",
|
"%(camo_num)",
|
||||||
SKIN_NUMBER,
|
SKIN_NUMBER,
|
||||||
);
|
);
|
||||||
const top = index === 0 ? "var(--radius-full)" : "0";
|
const top = index === 0 ? "var(--radius-full)" : "0";
|
||||||
const bottom =
|
const bottom = index === skins.length - 1 ? "var(--radius-full)" : "0";
|
||||||
index === group.length - 1 ? "var(--radius-full)" : "0";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={camouflage.id} content={name} side="left">
|
<Tooltip key={camouflage.id} content={name} side="left">
|
||||||
<SidebarIconButton
|
<SidebarIconButton
|
||||||
selected={
|
selected={skin === camouflage.id}
|
||||||
(camouflage.attachment ? attachment : skin) ===
|
|
||||||
camouflage.id
|
|
||||||
}
|
|
||||||
alt={name}
|
alt={name}
|
||||||
src={alias("api", `/icons/camouflages/${camouflage.id}.webp`)}
|
src={alias("api", `/icons/camouflages/${camouflage.id}.webp`)}
|
||||||
size={{ initial: "3", sm: "4" }}
|
size={{ initial: "3", sm: "4" }}
|
||||||
@@ -84,13 +68,8 @@ export function SkinSwitcher() {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
const loader =
|
draft.requestedSkin =
|
||||||
draft.loaders[
|
draft.requestedSkin === camouflage.id
|
||||||
camouflage.attachment ? "attachment" : "skin"
|
|
||||||
];
|
|
||||||
|
|
||||||
loader.requested =
|
|
||||||
loader.requested === camouflage.id
|
|
||||||
? undefined
|
? undefined
|
||||||
: camouflage.id;
|
: camouflage.id;
|
||||||
});
|
});
|
||||||
@@ -100,7 +79,5 @@ export function SkinSwitcher() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Flex>
|
</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";
|
||||||
@@ -83,9 +82,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);
|
||||||
|
|
||||||
@@ -475,7 +472,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>
|
||||||
|
|||||||
+6
-13
@@ -13,25 +13,18 @@ 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 hasRevealed = useRef(false);
|
||||||
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
|
const skin = Tankopedia.use((state) => state.requestedSkin);
|
||||||
const camouflage =
|
const camouflage =
|
||||||
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
||||||
const tankName = unwrap(protagonist.name!);
|
const tankName = unwrap(protagonist.name!);
|
||||||
// legendary camos carry the tank's in-battle name
|
const name = camouflage
|
||||||
const name = (
|
? `${tankName} ${unwrap(camouflage.name!).replace("%(camo_num)", SKIN_NUMBER)}`
|
||||||
camouflage
|
: tankName;
|
||||||
? 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"
|
||||||
@@ -109,7 +102,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;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+7
-10
@@ -48,9 +48,7 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const skinPending = Tankopedia.use(
|
const skinPending = Tankopedia.use(
|
||||||
(state) =>
|
(state) => state.loadedSkin !== state.skin,
|
||||||
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);
|
||||||
@@ -65,7 +63,6 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
draft.loaders.model.loaded = true;
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -165,7 +162,6 @@ 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}
|
||||||
decay={1}
|
decay={1}
|
||||||
@@ -216,11 +212,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;
|
||||||
}
|
}
|
||||||
@@ -253,6 +244,12 @@ function Animator({
|
|||||||
draft.display = requestedDisplay;
|
draft.display = requestedDisplay;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (x >= 1 && Tankopedia.state.skin !== Tankopedia.state.loadedSkin) {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.skin = draft.loadedSkin;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+4
-75
@@ -1,6 +1,5 @@
|
|||||||
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
|
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
|
||||||
import type { QuicklimeEvent } from "quicklime";
|
import { useRef } from "react";
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
|
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
|
||||||
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
|
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
|
||||||
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||||
@@ -9,14 +8,12 @@ import { skinTree } from "../../../../../../core/blitzkit/skinTree";
|
|||||||
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
|
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
|
||||||
import { useModel } from "../../../../../../hooks/useModel";
|
import { useModel } from "../../../../../../hooks/useModel";
|
||||||
import { useSkin } from "../../../../../../hooks/useSkin";
|
import { useSkin } from "../../../../../../hooks/useSkin";
|
||||||
import { useSkinAnimation } from "../../../../../../hooks/useSkinAnimation";
|
|
||||||
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
||||||
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
|
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||||
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
|
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
|
||||||
import { transitionEvent } from "./Lighting";
|
|
||||||
|
|
||||||
export function TankModel() {
|
export function TankModel() {
|
||||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||||
@@ -33,49 +30,8 @@ export function TankModel() {
|
|||||||
const { gltf } = useModel(protagonist.tank.id);
|
const { gltf } = useModel(protagonist.tank.id);
|
||||||
const nodes = Object.values(gltf.nodes);
|
const nodes = Object.values(gltf.nodes);
|
||||||
const skinGltf = useSkin(protagonist.tank.id);
|
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);
|
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 (
|
return (
|
||||||
<ModelTankWrapper ref={hullContainer}>
|
<ModelTankWrapper ref={hullContainer}>
|
||||||
@@ -138,7 +94,7 @@ export function TankModel() {
|
|||||||
window.removeEventListener("pointerup", handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return skinTree(node, skins, {
|
return skinTree(node, skinGltf, {
|
||||||
mesh(_, props, key) {
|
mesh(_, props, key) {
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -150,15 +106,6 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
primitive(object, key) {
|
|
||||||
return (
|
|
||||||
<primitive
|
|
||||||
key={key}
|
|
||||||
object={object}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -224,7 +171,7 @@ export function TankModel() {
|
|||||||
window.removeEventListener("pointerup", handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return skinTree(node, skins, {
|
return skinTree(node, skinGltf, {
|
||||||
mesh(_, props, key) {
|
mesh(_, props, key) {
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -236,15 +183,6 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
primitive(object, key) {
|
|
||||||
return (
|
|
||||||
<primitive
|
|
||||||
key={key}
|
|
||||||
object={object}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -314,7 +252,7 @@ export function TankModel() {
|
|||||||
window.removeEventListener("pointerup", handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return skinTree(node, skins, {
|
return skinTree(node, skinGltf, {
|
||||||
mesh(_, props, key) {
|
mesh(_, props, key) {
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -326,15 +264,6 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
primitive(object, key) {
|
|
||||||
return (
|
|
||||||
<primitive
|
|
||||||
key={key}
|
|
||||||
object={object}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,6 +1,4 @@
|
|||||||
export function isHalloween() {
|
export function isHalloween() {
|
||||||
return false;
|
|
||||||
|
|
||||||
const data = new Date();
|
const data = new Date();
|
||||||
return data.getMonth() === 9 && data.getDate() === 31;
|
return data.getMonth() === 9 && data.getDate() === 31;
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import {
|
import { Mesh, MeshStandardMaterial, Object3D } from "three";
|
||||||
AdditiveBlending,
|
|
||||||
Mesh,
|
|
||||||
MeshStandardMaterial,
|
|
||||||
Object3D,
|
|
||||||
type Material,
|
|
||||||
} from "three";
|
|
||||||
|
|
||||||
export function prepareMaterial(material: Material | Material[]) {
|
|
||||||
if (!(material instanceof MeshStandardMaterial)) return;
|
|
||||||
|
|
||||||
material.depthWrite = true;
|
|
||||||
|
|
||||||
if (material.userData.blending === "additive") {
|
|
||||||
material.blending = AdditiveBlending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function jsxTree(
|
export function jsxTree(
|
||||||
node: Object3D,
|
node: Object3D,
|
||||||
@@ -30,7 +14,6 @@ export function jsxTree(
|
|||||||
props: ComponentProps<"group">,
|
props: ComponentProps<"group">,
|
||||||
key: string,
|
key: string,
|
||||||
) => ReactNode;
|
) => ReactNode;
|
||||||
primitive?: (object3d: Object3D, key: string) => ReactNode;
|
|
||||||
},
|
},
|
||||||
): ReactNode {
|
): ReactNode {
|
||||||
if (node instanceof Mesh) {
|
if (node instanceof Mesh) {
|
||||||
@@ -42,7 +25,9 @@ export function jsxTree(
|
|||||||
scale: node.scale,
|
scale: node.scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
prepareMaterial(props.material);
|
if (props.material instanceof MeshStandardMaterial) {
|
||||||
|
props.material.depthWrite = true;
|
||||||
|
}
|
||||||
|
|
||||||
return mergers?.mesh ? (
|
return mergers?.mesh ? (
|
||||||
mergers.mesh(node, props, node.uuid)
|
mergers.mesh(node, props, node.uuid)
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { ReadStream } from "@blitzkit/core";
|
|
||||||
import { times } from "lodash-es";
|
|
||||||
|
|
||||||
const HEADER_SIZE = 16;
|
|
||||||
|
|
||||||
export enum MotionChannel {
|
|
||||||
Translation,
|
|
||||||
Rotation,
|
|
||||||
Scale,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MotionKeys {
|
|
||||||
times: number[];
|
|
||||||
values: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Motion {
|
|
||||||
duration: number;
|
|
||||||
tracks: Map<string, Map<MotionChannel, MotionKeys>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MotionReadStream extends ReadStream {
|
|
||||||
motion(): Motion {
|
|
||||||
this.seek(HEADER_SIZE);
|
|
||||||
|
|
||||||
const duration = this.float32();
|
|
||||||
const tracks: Motion["tracks"] = new Map();
|
|
||||||
|
|
||||||
times(this.uint32(), () => {
|
|
||||||
const uid = this.paddedString();
|
|
||||||
const channels = new Map<MotionChannel, MotionKeys>();
|
|
||||||
|
|
||||||
this.paddedString();
|
|
||||||
this.seek(4);
|
|
||||||
|
|
||||||
times(this.uint32(), () => {
|
|
||||||
const type = this.uint32() & 0xff;
|
|
||||||
|
|
||||||
this.seek(4);
|
|
||||||
|
|
||||||
const dimension = this.uint32() & 0xff;
|
|
||||||
const keys: MotionKeys = { times: [], values: [] };
|
|
||||||
|
|
||||||
times(this.uint32(), () => {
|
|
||||||
keys.times.push(this.float32());
|
|
||||||
times(dimension, () => keys.values.push(this.float32()));
|
|
||||||
});
|
|
||||||
channels.set(type, keys);
|
|
||||||
});
|
|
||||||
tracks.set(uid, channels);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { duration, tracks };
|
|
||||||
}
|
|
||||||
|
|
||||||
paddedString() {
|
|
||||||
const end = new Uint8Array(this.buffer).indexOf(0, this.index);
|
|
||||||
const value = this.utf8(end - this.index);
|
|
||||||
|
|
||||||
this.index = (end + 4) & ~3;
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
import { VertexAttribute, type Component } from "@blitzkit/core";
|
|
||||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
|
||||||
import {
|
|
||||||
MathUtils,
|
|
||||||
type Buffer,
|
|
||||||
type Document,
|
|
||||||
type mat4,
|
|
||||||
type Node,
|
|
||||||
type Primitive,
|
|
||||||
type Skin,
|
|
||||||
type TypedArray,
|
|
||||||
type vec3,
|
|
||||||
type vec4,
|
|
||||||
} from "@gltf-transform/core";
|
|
||||||
import { isEqual, times } from "lodash-es";
|
|
||||||
import { dirname, normalize } from "path/posix";
|
|
||||||
import { Matrix4 } from "three";
|
|
||||||
import { MotionChannel, MotionReadStream, type Motion } from "./motion";
|
|
||||||
|
|
||||||
type SkeletonComponent = Extract<
|
|
||||||
Component,
|
|
||||||
{ "comp.typename": "SkeletonComponent" }
|
|
||||||
>;
|
|
||||||
type MotionComponent = Extract<
|
|
||||||
Component,
|
|
||||||
{ "comp.typename": "MotionComponent" }
|
|
||||||
>;
|
|
||||||
type AccessorType = "SCALAR" | "VEC3" | "VEC4" | "MAT4";
|
|
||||||
|
|
||||||
const JOINT_ROOT = 0xffffff;
|
|
||||||
const MOTION_PATHS = {
|
|
||||||
[MotionChannel.Translation]: "translation",
|
|
||||||
[MotionChannel.Rotation]: "rotation",
|
|
||||||
[MotionChannel.Scale]: "scale",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function createAccessor(
|
|
||||||
document: Document,
|
|
||||||
buffer: Buffer,
|
|
||||||
type: AccessorType,
|
|
||||||
array: TypedArray,
|
|
||||||
) {
|
|
||||||
return document
|
|
||||||
.createAccessor()
|
|
||||||
.setType(type)
|
|
||||||
.setArray(array)
|
|
||||||
.setBuffer(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadMotion(
|
|
||||||
vfs: AbstractVFS,
|
|
||||||
path: string,
|
|
||||||
component: MotionComponent,
|
|
||||||
) {
|
|
||||||
const file = await vfs.file(
|
|
||||||
normalize(`Data/3d/${dirname(path)}/${component["motion.filepath"]}`),
|
|
||||||
);
|
|
||||||
|
|
||||||
return new MotionReadStream(
|
|
||||||
file.buffer.slice(
|
|
||||||
file.byteOffset,
|
|
||||||
file.byteOffset + file.byteLength,
|
|
||||||
) as ArrayBuffer,
|
|
||||||
).motion();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSkeleton(
|
|
||||||
document: Document,
|
|
||||||
buffer: Buffer,
|
|
||||||
node: Node,
|
|
||||||
skeleton: SkeletonComponent,
|
|
||||||
motion: Motion | undefined,
|
|
||||||
) {
|
|
||||||
const joints = times(
|
|
||||||
skeleton.jointsCount,
|
|
||||||
(index) => skeleton.joints[index.toString().padStart(4, "0")],
|
|
||||||
);
|
|
||||||
const jointNodes = joints.map((joint) => {
|
|
||||||
const translation: vec3 = [0, 0, 0];
|
|
||||||
const rotation: vec4 = [0, 0, 0, 1];
|
|
||||||
const scale: vec3 = [1, 1, 1];
|
|
||||||
const track = motion?.tracks.get(joint["joint.uid"]);
|
|
||||||
const trackScale = track?.get(MotionChannel.Scale)?.values[0];
|
|
||||||
|
|
||||||
MathUtils.decompose(
|
|
||||||
joint["joint.bindPose"].flat() as mat4,
|
|
||||||
translation,
|
|
||||||
rotation,
|
|
||||||
scale,
|
|
||||||
);
|
|
||||||
|
|
||||||
// rest on the first frame so the static pose matches the animation
|
|
||||||
return document
|
|
||||||
.createNode(joint["joint.name"])
|
|
||||||
.setTranslation(
|
|
||||||
(track?.get(MotionChannel.Translation)?.values.slice(0, 3) as vec3) ??
|
|
||||||
translation,
|
|
||||||
)
|
|
||||||
.setRotation(
|
|
||||||
(track?.get(MotionChannel.Rotation)?.values.slice(0, 4) as vec4) ??
|
|
||||||
rotation,
|
|
||||||
)
|
|
||||||
.setScale(
|
|
||||||
trackScale === undefined ? scale : [trackScale, trackScale, trackScale],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
joints.forEach((joint, index) => {
|
|
||||||
const parent = joint["joint.parentIndex"];
|
|
||||||
|
|
||||||
(parent === JOINT_ROOT ? node : jointNodes[parent]).addChild(
|
|
||||||
jointNodes[index],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const skin = document
|
|
||||||
.createSkin()
|
|
||||||
.setInverseBindMatrices(
|
|
||||||
createAccessor(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
"MAT4",
|
|
||||||
new Float32Array(
|
|
||||||
joints.flatMap((joint) => joint["joint.invBindPose"].flat()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
jointNodes.forEach((jointNode) => skin.addJoint(jointNode));
|
|
||||||
|
|
||||||
return {
|
|
||||||
skin,
|
|
||||||
joints: new Map(
|
|
||||||
joints.map((joint, index) => [joint["joint.uid"], jointNodes[index]]),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMotionClip(
|
|
||||||
document: Document,
|
|
||||||
buffer: Buffer,
|
|
||||||
clip: string,
|
|
||||||
joints: Map<string, Node>,
|
|
||||||
motion: Motion,
|
|
||||||
component: MotionComponent,
|
|
||||||
) {
|
|
||||||
const animation = document.createAnimation(clip);
|
|
||||||
const playbackRate = component["motion.playbackRate"];
|
|
||||||
const duration = motion.duration / playbackRate;
|
|
||||||
|
|
||||||
for (const [uid, track] of motion.tracks) {
|
|
||||||
const joint = joints.get(uid);
|
|
||||||
|
|
||||||
if (!joint) continue;
|
|
||||||
|
|
||||||
for (const [type, { times, values }] of track) {
|
|
||||||
const keyTimes = times.map((time) => time / playbackRate);
|
|
||||||
const keyValues =
|
|
||||||
type === MotionChannel.Scale
|
|
||||||
? values.flatMap((value) => [value, value, value])
|
|
||||||
: [...values];
|
|
||||||
|
|
||||||
// the game loops over the clip's duration, which can outlast its keys
|
|
||||||
if (keyTimes[keyTimes.length - 1] < duration) {
|
|
||||||
const size = keyValues.length / keyTimes.length;
|
|
||||||
|
|
||||||
keyTimes.push(duration);
|
|
||||||
keyValues.push(...keyValues.slice(-size));
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampler = document
|
|
||||||
.createAnimationSampler()
|
|
||||||
.setInput(
|
|
||||||
createAccessor(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
"SCALAR",
|
|
||||||
new Float32Array(keyTimes),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setOutput(
|
|
||||||
createAccessor(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
type === MotionChannel.Rotation ? "VEC4" : "VEC3",
|
|
||||||
new Float32Array(keyValues),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setInterpolation("LINEAR");
|
|
||||||
|
|
||||||
animation
|
|
||||||
.addSampler(sampler)
|
|
||||||
.addChannel(
|
|
||||||
document
|
|
||||||
.createAnimationChannel()
|
|
||||||
.setTargetNode(joint)
|
|
||||||
.setTargetPath(MOTION_PATHS[type])
|
|
||||||
.setSampler(sampler),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a socket follow a joint. The game replaces the socket's own transform
|
|
||||||
* with the joint's, so it sits right on the joint.
|
|
||||||
*/
|
|
||||||
export function bindSocket(socket: Node, joint: Node, skin: Skin) {
|
|
||||||
joint.addChild(socket);
|
|
||||||
socket.setMatrix(new Matrix4().toArray() as mat4);
|
|
||||||
socket.traverse((child) => {
|
|
||||||
const childSkin = child.getSkin();
|
|
||||||
|
|
||||||
// skinned meshes carrying a copy of the owner's skeleton deform with it
|
|
||||||
if (
|
|
||||||
childSkin &&
|
|
||||||
isEqual(
|
|
||||||
childSkin.listJoints().map((joint) => joint.getName()),
|
|
||||||
skin.listJoints().map((joint) => joint.getName()),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
child.setSkin(skin);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function skinPrimitive(
|
|
||||||
document: Document,
|
|
||||||
buffer: Buffer,
|
|
||||||
primitive: Primitive,
|
|
||||||
attributes: Map<VertexAttribute, number[][]>,
|
|
||||||
targetsData: ArrayBuffer | undefined,
|
|
||||||
) {
|
|
||||||
const jointIndices =
|
|
||||||
attributes.get(VertexAttribute.JOINTINDEX) ??
|
|
||||||
attributes.get(VertexAttribute.HARD_JOINTINDEX);
|
|
||||||
|
|
||||||
if (!jointIndices || !targetsData) return false;
|
|
||||||
|
|
||||||
const targets = new Uint32Array(new Uint8Array(targetsData).slice().buffer);
|
|
||||||
const weights =
|
|
||||||
attributes.get(VertexAttribute.JOINTWEIGHT) ?? jointIndices.map(() => [1]);
|
|
||||||
|
|
||||||
primitive
|
|
||||||
.setAttribute(
|
|
||||||
"JOINTS_0",
|
|
||||||
createAccessor(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
"VEC4",
|
|
||||||
new Uint16Array(
|
|
||||||
jointIndices.flatMap((indices) =>
|
|
||||||
times(4, (index) => {
|
|
||||||
if (index >= indices.length) return 0;
|
|
||||||
|
|
||||||
const target = targets[indices[index]];
|
|
||||||
|
|
||||||
if (target === undefined) {
|
|
||||||
throw new Error(`Missing joint target ${indices[index]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setAttribute(
|
|
||||||
"WEIGHTS_0",
|
|
||||||
createAccessor(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
"VEC4",
|
|
||||||
new Float32Array(
|
|
||||||
weights.flatMap((vertexWeights) => {
|
|
||||||
const padded = times(4, (index) => vertexWeights[index] ?? 0);
|
|
||||||
// some game weights sum below 1; glTF requires normalized weights
|
|
||||||
const sum = padded.reduce((total, weight) => total + weight, 0);
|
|
||||||
|
|
||||||
return padded.map((weight) => (sum > 0 ? weight / sum : weight));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -4,66 +4,39 @@ import { jsxTree } from "./jsxTree";
|
|||||||
|
|
||||||
type Mergers = Parameters<typeof jsxTree>[1];
|
type Mergers = Parameters<typeof jsxTree>[1];
|
||||||
|
|
||||||
// names get sanitized and made unique on load
|
|
||||||
export function nameOf(object3d: Object3D): string {
|
|
||||||
return object3d.userData.name ?? object3d.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* skins are layered in order, so an attachment's states win over the
|
|
||||||
* camouflage's; `primitive` renders customization parts as they are so their
|
|
||||||
* bones can animate
|
|
||||||
*/
|
|
||||||
export function skinTree(
|
export function skinTree(
|
||||||
node: Object3D,
|
node: Object3D,
|
||||||
skins: Skin[],
|
skin: Skin | undefined,
|
||||||
mergers: NonNullable<Mergers>,
|
mergers: Mergers,
|
||||||
) {
|
) {
|
||||||
const materials = new Map<string, Material>();
|
const skinNodes = new Map(
|
||||||
const states: Record<string, string | null> = {};
|
skin?.scene.children.map((node) => [node.name, node]),
|
||||||
|
);
|
||||||
for (const skin of skins) {
|
|
||||||
for (const [name, material] of skin.materials) {
|
|
||||||
materials.set(name, material);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(states, skin.scene.userData.states);
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchChildren(object3d: Object3D) {
|
|
||||||
if (object3d.userData.state === undefined) return;
|
|
||||||
|
|
||||||
const name = nameOf(object3d);
|
|
||||||
const state = name in states ? states[name] : object3d.userData.state;
|
|
||||||
|
|
||||||
return object3d.children
|
|
||||||
.filter((child) => nameOf(child) === state)
|
|
||||||
.map((child) => jsxTree(child, skinMergers));
|
|
||||||
}
|
|
||||||
|
|
||||||
const skinMergers: Mergers = {
|
const skinMergers: Mergers = {
|
||||||
mesh(mesh, props, key) {
|
mesh(mesh, props, key) {
|
||||||
const material = materials.get((mesh.material as Material).name);
|
const material = skin?.materials.get((mesh.material as Material).name);
|
||||||
|
|
||||||
return mergers.mesh!(
|
return mergers!.mesh!(
|
||||||
mesh,
|
mesh,
|
||||||
material ? { ...props, material } : props,
|
material ? { ...props, material } : props,
|
||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
group(object3d, props, key) {
|
group(object3d, props, key) {
|
||||||
|
const skinNode = skinNodes.get(object3d.name);
|
||||||
|
const isSwitch = object3d.userData.state !== undefined;
|
||||||
|
const state = skin
|
||||||
|
? skin.scene.userData.states[object3d.name]
|
||||||
|
: object3d.userData.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group key={key} {...props}>
|
<group key={key} {...props}>
|
||||||
{switchChildren(object3d) ?? props.children}
|
{isSwitch
|
||||||
{skins.flatMap((skin) =>
|
? object3d.children
|
||||||
(skin.parts.get(object3d.name) ?? []).map((part) =>
|
.filter((child) => child.name === state)
|
||||||
mergers.primitive ? (
|
.map((child) => jsxTree(child, skinMergers))
|
||||||
mergers.primitive(part, part.uuid)
|
: props.children}
|
||||||
) : (
|
{skinNode?.children.map((child) => jsxTree(child, mergers))}
|
||||||
<primitive key={part.uuid} object={part} />
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,540 +0,0 @@
|
|||||||
import {
|
|
||||||
parseHierarchies,
|
|
||||||
type Hierarchy,
|
|
||||||
type Sc2ReadStream,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import { sortBy, times } from "lodash-es";
|
|
||||||
|
|
||||||
interface Value {
|
|
||||||
"any.type": string;
|
|
||||||
"any.value": unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScriptNode {
|
|
||||||
type: string;
|
|
||||||
node: Record<string, unknown> & { defaults?: Record<string, Value> };
|
|
||||||
connections?: Record<
|
|
||||||
string,
|
|
||||||
{ pinIn: string; pinOut: string; target: string }
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Script {
|
|
||||||
nodes: Record<string, ScriptNode>;
|
|
||||||
variables?: Record<string, Value>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Action {
|
|
||||||
"act.delay": number;
|
|
||||||
"act.entityName": string;
|
|
||||||
"act.event": number;
|
|
||||||
"act.eventToTrigger": string;
|
|
||||||
"act.stopAfterNRepeats": number;
|
|
||||||
"act.switchIndex": number;
|
|
||||||
"act.type": number;
|
|
||||||
"act.userEventId": string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Entity {
|
|
||||||
name: string;
|
|
||||||
parent?: Entity;
|
|
||||||
children: Entity[];
|
|
||||||
hierarchy?: Hierarchy;
|
|
||||||
repeats?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SELF_ENTITY = "*** Self ***";
|
|
||||||
const SCENE_LOADED_EVENT = 1;
|
|
||||||
const USER_EVENT = 2;
|
|
||||||
const ANIMATION_END_EVENTS = [3, 8];
|
|
||||||
const START_ANIMATION_ACTION = 6;
|
|
||||||
const SWITCH_ACTIONS = [8, 9];
|
|
||||||
const TRIGGER_EVENT_ACTION = 10;
|
|
||||||
// what the game fires as a tank is shown in the garage, in order
|
|
||||||
const LOAD_EVENTS = ["Added", "TankAliveInHangar", "TankIsShownInNewScreen"];
|
|
||||||
const MAX_DEPTH = 64;
|
|
||||||
|
|
||||||
function findChild(entity: Entity, name: string): Entity | undefined {
|
|
||||||
for (const child of entity.children) {
|
|
||||||
if (child.name === name) return child;
|
|
||||||
|
|
||||||
const found = findChild(child, name);
|
|
||||||
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTarget(owner: Entity, entityName: string) {
|
|
||||||
if (entityName === SELF_ENTITY) return owner;
|
|
||||||
|
|
||||||
const [first, ...rest] = entityName.split("->");
|
|
||||||
let target: Entity | undefined;
|
|
||||||
|
|
||||||
for (
|
|
||||||
let ancestor: Entity | undefined = owner;
|
|
||||||
ancestor && !target;
|
|
||||||
ancestor = ancestor.parent
|
|
||||||
) {
|
|
||||||
target = ancestor.name === first ? ancestor : findChild(ancestor, first);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const name of rest) target = target && findChild(target, name);
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* replays what the game does as a tank is shown in the garage, with the camo's
|
|
||||||
* preset applied and its items attached; `animate` follows customization parts
|
|
||||||
* through their motions, delays and alarms until they settle
|
|
||||||
*/
|
|
||||||
function replay(
|
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
|
||||||
preset: string | undefined,
|
|
||||||
attachments: Map<string, string[]>,
|
|
||||||
animate: boolean,
|
|
||||||
) {
|
|
||||||
const scripts: Record<string, Script> = {};
|
|
||||||
const root: Entity = { name: "", children: [] };
|
|
||||||
const stateNames = new Map<Entity, string[]>();
|
|
||||||
const scenarios: {
|
|
||||||
owner: Entity;
|
|
||||||
script: Script;
|
|
||||||
variables: Map<string, unknown>;
|
|
||||||
}[] = [];
|
|
||||||
const actions: { owner: Entity; action: Action }[] = [];
|
|
||||||
// names can repeat, so states are kept per entity
|
|
||||||
const states = new Map<Entity, string | null>();
|
|
||||||
const looping = new Set<Entity>();
|
|
||||||
const finished = new Set<Entity>();
|
|
||||||
const pending: (() => void)[] = [];
|
|
||||||
const fired = new Set<unknown>();
|
|
||||||
|
|
||||||
function activateState(entity: Entity | undefined, state: number) {
|
|
||||||
const names = entity && stateNames.get(entity);
|
|
||||||
|
|
||||||
if (!names) return;
|
|
||||||
|
|
||||||
const previous = states.get(entity);
|
|
||||||
|
|
||||||
states.set(entity, names[state] ?? null);
|
|
||||||
|
|
||||||
if (!animate || previous === states.get(entity)) return;
|
|
||||||
|
|
||||||
for (const scenario of scenarios) {
|
|
||||||
if (scenario.owner === entity) {
|
|
||||||
runScenario(scenario, "State Activated", state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isShown(entity: Entity) {
|
|
||||||
for (let child = entity; child.parent; child = child.parent) {
|
|
||||||
if (
|
|
||||||
stateNames.get(child.parent)?.includes(child.name) &&
|
|
||||||
states.get(child.parent) !== child.name
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAnimation(entity: Entity, repeats?: number) {
|
|
||||||
if (entity.repeats === undefined) {
|
|
||||||
for (const child of entity.children) startAnimation(child, repeats);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (looping.has(entity)) return;
|
|
||||||
|
|
||||||
looping.add(entity);
|
|
||||||
|
|
||||||
// some clips restart themselves when they end, so each finishes once
|
|
||||||
if ((repeats ?? entity.repeats) > 0 && !finished.has(entity)) {
|
|
||||||
pending.push(() => finishAnimation(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishAnimation(entity: Entity) {
|
|
||||||
const statesBefore = new Map(states);
|
|
||||||
const shownBefore = [...looping].filter(isShown);
|
|
||||||
|
|
||||||
looping.delete(entity);
|
|
||||||
finished.add(entity);
|
|
||||||
|
|
||||||
for (const { owner, action } of actions) {
|
|
||||||
if (
|
|
||||||
owner === entity &&
|
|
||||||
ANIMATION_END_EVENTS.includes(action["act.event"])
|
|
||||||
) {
|
|
||||||
runAction(owner, action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (
|
|
||||||
let ancestor: Entity | undefined = entity;
|
|
||||||
ancestor;
|
|
||||||
ancestor = ancestor.parent
|
|
||||||
) {
|
|
||||||
const owned = scenarios.filter(({ owner }) => owner === ancestor);
|
|
||||||
|
|
||||||
for (const scenario of owned) {
|
|
||||||
runScenario(scenario, "Motion Fragment Stop");
|
|
||||||
runScenario(scenario, "Motion Sequence Stop");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (owned.length > 0) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// as in game, a clip stops on whatever it switched to or revealed; one that
|
|
||||||
// changes nothing is the only animation there is, so it keeps looping
|
|
||||||
if (
|
|
||||||
[...states].every(
|
|
||||||
([other, state]) => statesBefore.get(other) === state,
|
|
||||||
) &&
|
|
||||||
[...looping].every(
|
|
||||||
(other) => !isShown(other) || shownBefore.includes(other),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
looping.add(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runAction(owner: Entity, action: Action) {
|
|
||||||
const target = resolveTarget(owner, action["act.entityName"]);
|
|
||||||
|
|
||||||
if (SWITCH_ACTIONS.includes(action["act.type"])) {
|
|
||||||
// what shows for a while in game stays, as its animation loops here
|
|
||||||
if (action["act.delay"] > 0 && action["act.switchIndex"] < 0) return;
|
|
||||||
|
|
||||||
activateState(target, action["act.switchIndex"]);
|
|
||||||
} else if (animate && action["act.type"] === START_ANIMATION_ACTION) {
|
|
||||||
if (target) {
|
|
||||||
const repeats = action["act.stopAfterNRepeats"];
|
|
||||||
|
|
||||||
startAnimation(target, repeats > 0 ? repeats : undefined);
|
|
||||||
}
|
|
||||||
} else if (animate && action["act.type"] === TRIGGER_EVENT_ACTION) {
|
|
||||||
fireEvent(action["act.eventToTrigger"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const component of Object.values(
|
|
||||||
sc2["#sceneComponents"] as unknown as Record<string, unknown>,
|
|
||||||
) as { "comp.typename"?: string; "ssc.scripts"?: Record<string, Script> }[]) {
|
|
||||||
if (component?.["comp.typename"] !== "ScenarioSingletonComponent") continue;
|
|
||||||
|
|
||||||
Object.assign(scripts, component["ssc.scripts"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseHierarchies<Entity>(
|
|
||||||
sc2["#hierarchy"],
|
|
||||||
root,
|
|
||||||
(hierarchy, components, parent) => {
|
|
||||||
const entity: Entity = {
|
|
||||||
name: hierarchy.name,
|
|
||||||
parent,
|
|
||||||
children: [],
|
|
||||||
hierarchy,
|
|
||||||
};
|
|
||||||
|
|
||||||
parent.children.push(entity);
|
|
||||||
|
|
||||||
for (const item of attachments.get(hierarchy.name) ?? []) {
|
|
||||||
entity.children.push({ name: item, parent: entity, children: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const component of components) {
|
|
||||||
switch (component["comp.typename"]) {
|
|
||||||
case "StateSwitcherComponent": {
|
|
||||||
const names = times(
|
|
||||||
component["ssc.statesCount"],
|
|
||||||
(index) => component[`ssc.state${index}`],
|
|
||||||
);
|
|
||||||
|
|
||||||
stateNames.set(entity, names);
|
|
||||||
states.set(entity, names[component["ssc.activeState"]] ?? null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ScenarioComponent": {
|
|
||||||
const script = scripts[component["scenario.scriptId"]];
|
|
||||||
|
|
||||||
if (script) {
|
|
||||||
scenarios.push({
|
|
||||||
owner: entity,
|
|
||||||
script,
|
|
||||||
variables: new Map(
|
|
||||||
Object.entries(script.variables ?? {}).map(
|
|
||||||
([name, variable]) => [name, variable["any.value"]],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ActionComponent":
|
|
||||||
times(component["ac.actionCount"], (index) => {
|
|
||||||
actions.push({
|
|
||||||
owner: entity,
|
|
||||||
action: component[index.toString().padStart(4, "0")] as Action,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "MotionComponent":
|
|
||||||
if (animate) {
|
|
||||||
entity.repeats = component["simpleMotion.repeatsCount"];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function runScenario(
|
|
||||||
scenario: (typeof scenarios)[number],
|
|
||||||
event: string,
|
|
||||||
value?: number,
|
|
||||||
) {
|
|
||||||
const { owner, script, variables } = scenario;
|
|
||||||
const { nodes } = script;
|
|
||||||
const links = (id: string) => Object.values(nodes[id].connections ?? {});
|
|
||||||
|
|
||||||
function input(id: string, pin: string): unknown {
|
|
||||||
const link = links(id).find((link) => link.pinIn === pin);
|
|
||||||
|
|
||||||
return link
|
|
||||||
? output(link.target, link.pinOut)
|
|
||||||
: nodes[id].node.defaults?.[pin]?.["any.value"];
|
|
||||||
}
|
|
||||||
|
|
||||||
function output(id: string, pin: string): unknown {
|
|
||||||
const { type, node } = nodes[id];
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "ScenarioSceneInfoNode":
|
|
||||||
switch (node["scenarioSceneInfoNode.infoKind"]) {
|
|
||||||
case "GetCurrentEntity":
|
|
||||||
return owner;
|
|
||||||
case "GetCurrentScene":
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
case "ScenarioGetVarNode":
|
|
||||||
return variables.get(node["scenarioGetVarNode.varPath"] as string);
|
|
||||||
|
|
||||||
case "ScenarioSetVarNode":
|
|
||||||
return variables.get(node["scenarioSetVarNode.varPath"] as string);
|
|
||||||
|
|
||||||
case "ScenarioGetMemberNode": {
|
|
||||||
const object = input(id, "object") as Entity | undefined;
|
|
||||||
|
|
||||||
switch (node["scenarioGetMemberNode.fieldName"]) {
|
|
||||||
case "parent":
|
|
||||||
return pin === "parent" ? object?.parent : undefined;
|
|
||||||
case "name":
|
|
||||||
return object?.name;
|
|
||||||
case "stateSwitcherVisibility":
|
|
||||||
return object && isShown(object);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ScenarioFunctionNode": {
|
|
||||||
const name = node["scenarioFunctionNode.functionName"] as string;
|
|
||||||
const number = (pin: string) => Number(input(id, pin) ?? 0);
|
|
||||||
|
|
||||||
switch (`${node["scenarioFunctionNode.className"]}.${name}`) {
|
|
||||||
case "Entity.FindChildByName": {
|
|
||||||
const entity = input(id, "entity") as Entity | undefined;
|
|
||||||
return entity && findChild(entity, input(id, "name") as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Entity.GetVisibility": {
|
|
||||||
const entity = input(id, "arg0") as Entity | undefined;
|
|
||||||
return entity ? isShown(entity) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "TankComponent.GetAppliedSkinMaterialConfigName":
|
|
||||||
return preset;
|
|
||||||
|
|
||||||
case "String.Compare":
|
|
||||||
return input(id, "1") === input(id, "2");
|
|
||||||
|
|
||||||
case "String.Merge":
|
|
||||||
// graphs use Separator as the prefix and Postfix as the number
|
|
||||||
return ["Suffix", "Separator", "Postfix"]
|
|
||||||
.map((pin) => input(id, pin) ?? "")
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
case "bool.Or":
|
|
||||||
return Boolean(input(id, "a")) || Boolean(input(id, "b"));
|
|
||||||
|
|
||||||
case "Random.Random":
|
|
||||||
case "Random.RandomFloat":
|
|
||||||
case "TankComponent.GetSpeed":
|
|
||||||
case "TankComponent.GetTurretAngle":
|
|
||||||
case "TankComponent.GetGunAngle":
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "==") return number("a") === number("b");
|
|
||||||
if (name === ">") return number("a") > number("b");
|
|
||||||
if (name === ">=") return number("a") >= number("b");
|
|
||||||
if (name === "<=") return number("a") <= number("b");
|
|
||||||
if (name.startsWith("Add")) return number("a") + number("b");
|
|
||||||
if (name.startsWith("Subtract")) return number("a") - number("b");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function execute(id: string, depth: number) {
|
|
||||||
if (depth > MAX_DEPTH) return;
|
|
||||||
|
|
||||||
const { type, node } = nodes[id];
|
|
||||||
let pin = "exit";
|
|
||||||
|
|
||||||
if (type === "ScenarioEventNode") {
|
|
||||||
pin = "event";
|
|
||||||
} else if (type === "ScenarioBranchNode") {
|
|
||||||
pin = input(id, "condition") ? "true" : "false";
|
|
||||||
} else if (type === "ScenarioSetVarNode") {
|
|
||||||
variables.set(
|
|
||||||
node["scenarioSetVarNode.varPath"] as string,
|
|
||||||
input(id, "set"),
|
|
||||||
);
|
|
||||||
} else if (type === "ScenarioSetAlarmClockNode") {
|
|
||||||
const entity = (input(id, "entity") as Entity | undefined) ?? owner;
|
|
||||||
|
|
||||||
// alarms re-arm themselves
|
|
||||||
if (animate && !fired.has(node)) {
|
|
||||||
fired.add(node);
|
|
||||||
pending.push(() => {
|
|
||||||
for (const other of scenarios) {
|
|
||||||
if (other.owner === entity) runScenario(other, "Alarm Clock");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const entity = input(id, "entity") as Entity | undefined;
|
|
||||||
|
|
||||||
switch (node["scenarioFunctionNode.functionName"]) {
|
|
||||||
case "ActivateState":
|
|
||||||
activateState(entity, Number(input(id, "state")));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "StartAnimation":
|
|
||||||
if (animate && entity) {
|
|
||||||
startAnimation(entity, Number(input(id, "repeats") ?? 0));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "TriggerCustomEvent":
|
|
||||||
if (animate) fireEvent(input(id, "event name") as string);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const link of links(id)) {
|
|
||||||
if (link.pinIn === pin) execute(link.target, depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id in nodes) {
|
|
||||||
const { type, node } = nodes[id];
|
|
||||||
const name =
|
|
||||||
node["ScenarioEventNode.eventName"] === "Custom"
|
|
||||||
? node.defaults?.["Event Name"]?.["any.value"]
|
|
||||||
: node["ScenarioEventNode.eventName"];
|
|
||||||
|
|
||||||
if (
|
|
||||||
type === "ScenarioEventNode" &&
|
|
||||||
name === event &&
|
|
||||||
(event !== "State Activated" ||
|
|
||||||
Number(node.defaults?.State?.["any.value"] ?? 0) === value)
|
|
||||||
) {
|
|
||||||
execute(id, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fireEvent(event: string) {
|
|
||||||
if (fired.has(event)) return;
|
|
||||||
|
|
||||||
fired.add(event);
|
|
||||||
|
|
||||||
// the game runs delayed actions once their delay is up, so they go in that
|
|
||||||
// order rather than the order they're stored in
|
|
||||||
for (const { owner, action } of sortBy(
|
|
||||||
actions,
|
|
||||||
({ action }) => action["act.delay"],
|
|
||||||
)) {
|
|
||||||
if (
|
|
||||||
(animate || action["act.delay"] === 0) &&
|
|
||||||
(event === LOAD_EVENTS[0]
|
|
||||||
? action["act.event"] === SCENE_LOADED_EVENT
|
|
||||||
: action["act.event"] === USER_EVENT &&
|
|
||||||
action["act.userEventId"] === event)
|
|
||||||
) {
|
|
||||||
runAction(owner, action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const scenario of scenarios) runScenario(scenario, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const event of LOAD_EVENTS) fireEvent(event);
|
|
||||||
|
|
||||||
for (let run = pending.shift(); run; run = pending.shift()) run();
|
|
||||||
|
|
||||||
return {
|
|
||||||
states: new Map(
|
|
||||||
[...states].map(([entity, state]) => [
|
|
||||||
entity.hierarchy as Hierarchy,
|
|
||||||
state,
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
motions: new Set(
|
|
||||||
[...looping]
|
|
||||||
.filter(isShown)
|
|
||||||
.map((entity) => entity.hierarchy as Hierarchy),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* picks each state switcher's state; delayed actions are skipped so states stay
|
|
||||||
* as they are when the tank appears
|
|
||||||
*/
|
|
||||||
export function switcherStates(
|
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
|
||||||
preset: string | undefined,
|
|
||||||
attachments: Map<string, string[]>,
|
|
||||||
) {
|
|
||||||
const states: Record<string, string | null> = {};
|
|
||||||
|
|
||||||
for (const [hierarchy, state] of replay(sc2, preset, attachments, false)
|
|
||||||
.states) {
|
|
||||||
states[hierarchy.name] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return states;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* where a customization part ends up: each state switcher's state and the
|
|
||||||
* motions left looping
|
|
||||||
*/
|
|
||||||
export function partGarage(
|
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
|
||||||
preset: string | undefined,
|
|
||||||
) {
|
|
||||||
return replay(sc2, preset, new Map(), true);
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,13 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
import { alias } from "@blitzkit/core";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import {
|
import type { Group, Material } from "three";
|
||||||
AnimationMixer,
|
|
||||||
Mesh,
|
|
||||||
SkinnedMesh,
|
|
||||||
type Group,
|
|
||||||
type Material,
|
|
||||||
type Object3D,
|
|
||||||
} from "three";
|
|
||||||
import { GLTFLoader } from "three-stdlib";
|
import { GLTFLoader } from "three-stdlib";
|
||||||
import { TRANSITION_ANIMATION_TIME } from "../components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
import { TRANSITION_ANIMATION_TIME } from "../components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
import { prepareMaterial } from "../core/blitzkit/jsxTree";
|
|
||||||
import { nameOf } from "../core/blitzkit/skinTree";
|
|
||||||
import { Tankopedia } from "../stores/tankopedia";
|
import { Tankopedia } from "../stores/tankopedia";
|
||||||
|
|
||||||
export interface Skin {
|
export interface Skin {
|
||||||
scene: Group;
|
scene: Group;
|
||||||
materials: Map<string, Material>;
|
materials: Map<string, Material>;
|
||||||
/** customization parts by the tank node they attach to */
|
|
||||||
parts: Map<string, Object3D[]>;
|
|
||||||
mixer?: AnimationMixer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const skins = new Map<string, Promise<Skin>>();
|
const skins = new Map<string, Promise<Skin>>();
|
||||||
@@ -30,78 +18,42 @@ async function loadSkin(path: string) {
|
|||||||
const materials: Material[] = await gltf.parser.getDependencies("material");
|
const materials: Material[] = await gltf.parser.getDependencies("material");
|
||||||
const skin = {
|
const skin = {
|
||||||
scene: gltf.scene,
|
scene: gltf.scene,
|
||||||
materials: new Map(
|
materials: new Map(materials.map((material) => [material.name, material])),
|
||||||
materials
|
|
||||||
.filter((material) => material.userData.camouflage)
|
|
||||||
.map((material) => [material.name, material]),
|
|
||||||
),
|
|
||||||
// taken now because rendering moves the parts out of the scene
|
|
||||||
parts: new Map(
|
|
||||||
gltf.scene.children.map((target) => [target.name, [...target.children]]),
|
|
||||||
),
|
|
||||||
mixer:
|
|
||||||
gltf.animations.length > 0 ? new AnimationMixer(gltf.scene) : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// bound now for the same reason, every exported clip loops in the garage
|
|
||||||
for (const clip of gltf.animations) skin.mixer!.clipAction(clip).play();
|
|
||||||
|
|
||||||
for (const parts of skin.parts.values()) {
|
|
||||||
for (const part of parts) preparePart(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedSkins.set(path, skin);
|
loadedSkins.set(path, skin);
|
||||||
|
|
||||||
return skin;
|
return skin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parts render as they are, so what jsxTree and skinTree would do happens here
|
|
||||||
function preparePart(part: Object3D) {
|
|
||||||
part.traverse((object) => {
|
|
||||||
if (object.userData.state !== undefined) {
|
|
||||||
for (const child of object.children) {
|
|
||||||
child.visible = nameOf(child) === object.userData.state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(object instanceof Mesh)) return;
|
|
||||||
|
|
||||||
object.castShadow = true;
|
|
||||||
object.receiveShadow = true;
|
|
||||||
// bounds are from the bind pose
|
|
||||||
if (object instanceof SkinnedMesh) object.frustumCulled = false;
|
|
||||||
|
|
||||||
prepareMaterial(object.material);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function skinPath(id: number, skin: number) {
|
function skinPath(id: number, skin: number) {
|
||||||
return alias("models_legendary", `/${id}/skins/${skin}.glb`);
|
return alias("models_legendary", `/${id}/skins/${skin}.glb`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// attachments load the same way as camouflages and can be shown with one
|
export function useSkin(id: number) {
|
||||||
export function useSkin(id: number, loader: "skin" | "attachment" = "skin") {
|
const requestedSkin = Tankopedia.use((state) => state.requestedSkin);
|
||||||
const requested = Tankopedia.use((state) => state.loaders[loader].requested);
|
const skin = Tankopedia.use((state) => state.skin);
|
||||||
const loaded = Tankopedia.use((state) => state.loaders[loader].loaded);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (requested === Tankopedia.state.loaders[loader].loaded) return;
|
const path =
|
||||||
|
requestedSkin === undefined ? undefined : skinPath(id, requestedSkin);
|
||||||
const path = requested === undefined ? undefined : skinPath(id, requested);
|
|
||||||
const cached = path === undefined || loadedSkins.has(path);
|
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
|
||||||
if (!cached) {
|
if (path === undefined || loadedSkins.has(path)) {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.loadedSkin = requestedSkin;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = false;
|
draft.revealed = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!skins.has(path)) skins.set(path, loadSkin(path));
|
if (!skins.has(path)) skins.set(path, loadSkin(path));
|
||||||
}
|
|
||||||
|
|
||||||
// swaps halfway through the lighting transition
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
path && skins.get(path),
|
skins.get(path),
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
setTimeout(resolve, TRANSITION_ANIMATION_TIME * 1e3),
|
setTimeout(resolve, TRANSITION_ANIMATION_TIME * 1e3),
|
||||||
),
|
),
|
||||||
@@ -110,8 +62,9 @@ export function useSkin(id: number, loader: "skin" | "attachment" = "skin") {
|
|||||||
|
|
||||||
settled = true;
|
settled = true;
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.loaders[loader].loaded = requested;
|
draft.loadedSkin = requestedSkin;
|
||||||
if (!cached) draft.revealed = true;
|
draft.skin = requestedSkin;
|
||||||
|
draft.revealed = true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,16 +72,11 @@ export function useSkin(id: number, loader: "skin" | "attachment" = "skin") {
|
|||||||
if (settled) return;
|
if (settled) return;
|
||||||
|
|
||||||
settled = true;
|
settled = true;
|
||||||
|
|
||||||
if (cached) return;
|
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [id, loader, requested]);
|
}, [id, requestedSkin]);
|
||||||
|
|
||||||
return loaded === undefined
|
return skin === undefined ? undefined : loadedSkins.get(skinPath(id, skin));
|
||||||
? undefined
|
|
||||||
: loadedSkins.get(skinPath(id, loaded));
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { invalidate, useFrame } from "@react-three/fiber";
|
|
||||||
import type { Skin } from "./useSkin";
|
|
||||||
|
|
||||||
export function useSkinAnimation(skins: Skin[]) {
|
|
||||||
useFrame((_, delta) => {
|
|
||||||
for (const { mixer } of skins) {
|
|
||||||
if (!mixer) continue;
|
|
||||||
|
|
||||||
mixer.update(delta);
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import { literals } from "@blitzkit/i18n/src/literals";
|
|||||||
import { Box, Flex, Link } from "@radix-ui/themes";
|
import { Box, Flex, Link } from "@radix-ui/themes";
|
||||||
import { AesonPlug } from "../../../components/AesonPlug";
|
import { AesonPlug } from "../../../components/AesonPlug";
|
||||||
import { Beg } from "../../../components/Beg";
|
import { Beg } from "../../../components/Beg";
|
||||||
import { Countdown } from "../../../components/Countdown";
|
|
||||||
import HomeHero from "../../../components/HomeHero.astro";
|
import HomeHero from "../../../components/HomeHero.astro";
|
||||||
import { Plugs } from "../../../components/Plugs";
|
import { Plugs } from "../../../components/Plugs";
|
||||||
import ToolCard from "../../../components/ToolCard.astro";
|
import ToolCard from "../../../components/ToolCard.astro";
|
||||||
@@ -67,10 +66,6 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
|
|||||||
<BaseLayout {locale}>
|
<BaseLayout {locale}>
|
||||||
<HomeHero />
|
<HomeHero />
|
||||||
|
|
||||||
<Countdown client:only="react">
|
|
||||||
<Countdown skeleton slot="fallback" />
|
|
||||||
</Countdown>
|
|
||||||
|
|
||||||
<Beg />
|
<Beg />
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { ModuleButton } from "../../../../components/ModuleButtons/ModuleButton"
|
|||||||
import { PageWrapper } from "../../../../components/PageWrapper";
|
import { PageWrapper } from "../../../../components/PageWrapper";
|
||||||
import { ScreenshotButton } from "../../../../components/ScreenshotButton";
|
import { ScreenshotButton } from "../../../../components/ScreenshotButton";
|
||||||
import { TankSearch } from "../../../../components/TankSearch";
|
import { TankSearch } from "../../../../components/TankSearch";
|
||||||
import { DIALOG_MAX_HEIGHT } from "../../../../constants/dialog";
|
|
||||||
import { api } from "../../../../core/blitzkit/api";
|
import { api } from "../../../../core/blitzkit/api";
|
||||||
import { curateMixer } from "../../../../core/blitzkit/curateMixer";
|
import { curateMixer } from "../../../../core/blitzkit/curateMixer";
|
||||||
import {
|
import {
|
||||||
@@ -118,7 +117,7 @@ function ModuleChooser({
|
|||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content maxHeight={DIALOG_MAX_HEIGHT}>
|
<Dialog.Content>
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
{strings.website.common.tank_search.select_dialog_title}
|
{strings.website.common.tank_search.select_dialog_title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import {
|
import {
|
||||||
Document,
|
Document,
|
||||||
Material,
|
|
||||||
Node,
|
Node,
|
||||||
NodeIO,
|
NodeIO,
|
||||||
PropertyType,
|
PropertyType,
|
||||||
@@ -20,7 +19,6 @@ import type { Vector3Tuple, Vector4Tuple } from "three";
|
|||||||
import { SKIN_NUMBER } from "../../../../constants/skinNumber";
|
import { SKIN_NUMBER } from "../../../../constants/skinNumber";
|
||||||
import { api } from "../../../../core/blitzkit/api";
|
import { api } from "../../../../core/blitzkit/api";
|
||||||
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
||||||
import { switcherStates } from "../../../../core/blitzkit/switcherStates";
|
|
||||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||||
import {
|
import {
|
||||||
extractMaterials,
|
extractMaterials,
|
||||||
@@ -69,10 +67,8 @@ export async function GET({ props }: APIContext<{ skin: number }>) {
|
|||||||
const [nation, tankKey] = (
|
const [nation, tankKey] = (
|
||||||
include as { vehicle: { name: string } }
|
include as { vehicle: { name: string } }
|
||||||
).vehicle.name.split(":");
|
).vehicle.name.split(":");
|
||||||
const { customEntities } = camouflagesYaml[key];
|
const { preset, customEntities } = camouflagesYaml[key];
|
||||||
const preset =
|
const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1);
|
||||||
camouflagesYaml[key].preset ??
|
|
||||||
camouflagesYaml[key].override?.[nation]?.override?.[tankKey]?.preset;
|
|
||||||
const parameters = await vfs.yaml<TankParameters>(
|
const parameters = await vfs.yaml<TankParameters>(
|
||||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||||
);
|
);
|
||||||
@@ -92,6 +88,7 @@ export async function GET({ props }: APIContext<{ skin: number }>) {
|
|||||||
scale: Vector3Tuple;
|
scale: Vector3Tuple;
|
||||||
}
|
}
|
||||||
> = {};
|
> = {};
|
||||||
|
const states: Record<string, string | null> = {};
|
||||||
|
|
||||||
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
||||||
for (const component of components) {
|
for (const component of components) {
|
||||||
@@ -114,50 +111,43 @@ export async function GET({ props }: APIContext<{ skin: number }>) {
|
|||||||
scale: component["sc.attachmentScale"] as Vector3Tuple,
|
scale: component["sc.attachmentScale"] as Vector3Tuple,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (component["comp.typename"] !== "StateSwitcherComponent") continue;
|
||||||
|
|
||||||
|
const active = component[`ssc.state${component["ssc.activeState"]}`];
|
||||||
|
// camos without their own variant keep the default piece, but groups
|
||||||
|
// of elements to hide stay hidden
|
||||||
|
let state: string | null =
|
||||||
|
active && !/skin\d*/.test(active) && !active.includes("hide")
|
||||||
|
? active
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (let index = 0; index < component["ssc.statesCount"]; index++) {
|
||||||
|
const name = component[`ssc.state${index}`];
|
||||||
|
const match = name.match(/skin(\d*)/);
|
||||||
|
|
||||||
|
if (
|
||||||
|
match &&
|
||||||
|
!name.includes("hide") &&
|
||||||
|
Number(match[1] || 1) === skinNumber
|
||||||
|
) {
|
||||||
|
state = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
states[hierarchy.name] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const attachments = new Map<string, string[]>();
|
|
||||||
|
|
||||||
for (const name in customEntities) {
|
|
||||||
const slot = slots[name];
|
|
||||||
|
|
||||||
if (!slot) continue;
|
|
||||||
|
|
||||||
attachments.set(slot.target, [
|
|
||||||
...(attachments.get(slot.target) ?? []),
|
|
||||||
customEntities[name].item,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const states = switcherStates(sc2, preset, attachments);
|
|
||||||
|
|
||||||
// attachments are layered over camouflages, so they only keep what they change
|
|
||||||
if (camouflagesXml.root.camouflages[key].kind === "customization") {
|
|
||||||
const baseline = switcherStates(sc2, preset, new Map());
|
|
||||||
|
|
||||||
for (const name in states) {
|
|
||||||
if (states[name] === baseline[name]) delete states[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const document = new Document();
|
const document = new Document();
|
||||||
const scene = document.createScene();
|
const scene = document.createScene().setExtras({ states });
|
||||||
const targets = new Map<string, Node>();
|
const targets = new Map<string, Node>();
|
||||||
|
|
||||||
document.createBuffer();
|
document.createBuffer();
|
||||||
|
|
||||||
if (preset) {
|
if (preset) await extractMaterials(document, sc2, basePath, preset);
|
||||||
const materials = await extractMaterials(document, sc2, basePath, preset);
|
|
||||||
|
|
||||||
// parts may share these names, so mark the ones that replace the tank's
|
|
||||||
for (const material of materials.values()) {
|
|
||||||
if (!(material instanceof Material)) continue;
|
|
||||||
|
|
||||||
material.setExtras({ ...material.getExtras(), camouflage: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const name in customEntities) {
|
for (const name in customEntities) {
|
||||||
const slot = slots[name];
|
const slot = slots[name];
|
||||||
@@ -172,7 +162,7 @@ export async function GET({ props }: APIContext<{ skin: number }>) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const part = await extractModel(vfs, path, true, preset);
|
const part = await extractModel(vfs, path, true);
|
||||||
const map = mergeDocuments(document, part);
|
const map = mergeDocuments(document, part);
|
||||||
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
||||||
const node = document
|
const node = document
|
||||||
@@ -196,8 +186,6 @@ export async function GET({ props }: APIContext<{ skin: number }>) {
|
|||||||
target.addChild(node);
|
target.addChild(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.setExtras({ states });
|
|
||||||
|
|
||||||
applySkinNumber(document, SKIN_NUMBER);
|
applySkinNumber(document, SKIN_NUMBER);
|
||||||
|
|
||||||
await document.transform(
|
await document.transform(
|
||||||
|
|||||||
@@ -8,35 +8,18 @@ import {
|
|||||||
toUniqueId,
|
toUniqueId,
|
||||||
VertexAttribute,
|
VertexAttribute,
|
||||||
type ConfigArchive,
|
type ConfigArchive,
|
||||||
type Hierarchy,
|
|
||||||
type TankParameters,
|
type TankParameters,
|
||||||
type Textures,
|
type Textures,
|
||||||
type VehicleDefinitionList,
|
type VehicleDefinitionList,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
import {
|
import { Document, Material, Node, NodeIO, Scene } from "@gltf-transform/core";
|
||||||
Document,
|
|
||||||
Material,
|
|
||||||
Node,
|
|
||||||
NodeIO,
|
|
||||||
Scene,
|
|
||||||
type Skin,
|
|
||||||
} from "@gltf-transform/core";
|
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
import { dedup, prune } from "@gltf-transform/functions";
|
import { dedup, prune } from "@gltf-transform/functions";
|
||||||
import type { APIContext } from "astro";
|
import type { APIContext } from "astro";
|
||||||
import { isEqual, times } from "lodash-es";
|
import { isEqual, times } from "lodash-es";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import type { Motion } from "../../../core/blitzkit/skinAnimations/motion";
|
|
||||||
import {
|
|
||||||
bindSocket,
|
|
||||||
createMotionClip,
|
|
||||||
createSkeleton,
|
|
||||||
loadMotion,
|
|
||||||
skinPrimitive,
|
|
||||||
} from "../../../core/blitzkit/skinAnimations/skeleton";
|
|
||||||
import { partGarage } from "../../../core/blitzkit/switcherStates";
|
|
||||||
import { vfs } from "../../../core/blitzkit/vfs";
|
import { vfs } from "../../../core/blitzkit/vfs";
|
||||||
import {
|
import {
|
||||||
vertexAttributeGLTFName,
|
vertexAttributeGLTFName,
|
||||||
@@ -82,13 +65,7 @@ export async function GET({ props }: APIContext<{ id: number }>) {
|
|||||||
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
||||||
|
|
||||||
const omitMeshNames = {
|
const omitMeshNames = {
|
||||||
start: [
|
start: ["chassis_chassis_", "chassis_track_crash_", "HP_"],
|
||||||
"chassis_chassis_",
|
|
||||||
"chassis_track_crash_",
|
|
||||||
"HP_",
|
|
||||||
// mastery marks a new tank doesn't have yet
|
|
||||||
"custom_gun_marks_",
|
|
||||||
],
|
|
||||||
end: ["_POINT"],
|
end: ["_POINT"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +73,6 @@ export async function extractModel(
|
|||||||
vfs: AbstractVFS,
|
vfs: AbstractVFS,
|
||||||
path: string,
|
path: string,
|
||||||
isPart = false,
|
isPart = false,
|
||||||
preset?: string,
|
|
||||||
) {
|
) {
|
||||||
const sc2Path = `Data/3d/${path}.sc2`;
|
const sc2Path = `Data/3d/${path}.sc2`;
|
||||||
const scgPath = `Data/3d/${path}.scg`;
|
const scgPath = `Data/3d/${path}.scg`;
|
||||||
@@ -110,35 +86,6 @@ export async function extractModel(
|
|||||||
const scene = document.createScene();
|
const scene = document.createScene();
|
||||||
const buffer = document.createBuffer();
|
const buffer = document.createBuffer();
|
||||||
const materials = await extractMaterials(document, sc2, path);
|
const materials = await extractMaterials(document, sc2, path);
|
||||||
const skeletons = new Map<Hierarchy, ReturnType<typeof createSkeleton>>();
|
|
||||||
const jointBindings: [
|
|
||||||
owner: Node,
|
|
||||||
entity: string,
|
|
||||||
joint: Node,
|
|
||||||
skin: Skin,
|
|
||||||
][] = [];
|
|
||||||
const motions = new Map<Hierarchy, Motion>();
|
|
||||||
const garage = isPart ? partGarage(sc2, preset) : undefined;
|
|
||||||
|
|
||||||
if (isPart) {
|
|
||||||
const loading: Promise<void>[] = [];
|
|
||||||
|
|
||||||
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
|
||||||
for (const component of components) {
|
|
||||||
if (component["comp.typename"] !== "MotionComponent") continue;
|
|
||||||
|
|
||||||
loading.push(
|
|
||||||
loadMotion(vfs, path, component).then((motion) => {
|
|
||||||
motions.set(hierarchy, motion);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(loading);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseHierarchies<Scene | Node>(
|
parseHierarchies<Scene | Node>(
|
||||||
sc2["#hierarchy"],
|
sc2["#hierarchy"],
|
||||||
@@ -152,40 +99,6 @@ export async function extractModel(
|
|||||||
|
|
||||||
const node = document.createNode(hierarchy.name);
|
const node = document.createNode(hierarchy.name);
|
||||||
|
|
||||||
if (isPart) {
|
|
||||||
const skeleton = components.find(
|
|
||||||
(component) => component["comp.typename"] === "SkeletonComponent",
|
|
||||||
);
|
|
||||||
const motionComponent = components.find(
|
|
||||||
(component) => component["comp.typename"] === "MotionComponent",
|
|
||||||
);
|
|
||||||
const motion = motions.get(hierarchy);
|
|
||||||
|
|
||||||
if (skeleton) {
|
|
||||||
const created = createSkeleton(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
node,
|
|
||||||
skeleton,
|
|
||||||
motion,
|
|
||||||
);
|
|
||||||
|
|
||||||
skeletons.set(hierarchy, created);
|
|
||||||
|
|
||||||
// only what loops in the garage is exported
|
|
||||||
if (motion && motionComponent && garage?.motions.has(hierarchy)) {
|
|
||||||
createMotionClip(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
`motion${document.getRoot().listAnimations().length}`,
|
|
||||||
created.joints,
|
|
||||||
motion,
|
|
||||||
motionComponent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
components.forEach((component) => {
|
components.forEach((component) => {
|
||||||
switch (component["comp.typename"]) {
|
switch (component["comp.typename"]) {
|
||||||
case "LodComponent":
|
case "LodComponent":
|
||||||
@@ -209,44 +122,17 @@ export async function extractModel(
|
|||||||
|
|
||||||
case "StateSwitcherComponent":
|
case "StateSwitcherComponent":
|
||||||
node.setExtras({
|
node.setExtras({
|
||||||
// a part's scenarios can pick its state as it loads
|
state: component[`ssc.state${component["ssc.activeState"]}`],
|
||||||
state: garage
|
|
||||||
? (garage.states.get(hierarchy) ?? null)
|
|
||||||
: component[`ssc.state${component["ssc.activeState"]}`],
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "SkeletonJointBindingComponent": {
|
|
||||||
const skeleton = skeletons.get(hierarchy);
|
|
||||||
|
|
||||||
if (!skeleton) break;
|
|
||||||
|
|
||||||
times(component.count, (index) => {
|
|
||||||
const binding = component.map[
|
|
||||||
index.toString().padStart(4, "0")
|
|
||||||
] as unknown as { entityUid: string; jointUid: string };
|
|
||||||
const joint = skeleton.joints.get(binding.jointUid);
|
|
||||||
|
|
||||||
if (!joint) throw new Error(`Missing joint ${binding.jointUid}`);
|
|
||||||
|
|
||||||
jointBindings.push([
|
|
||||||
node,
|
|
||||||
binding.entityUid,
|
|
||||||
joint,
|
|
||||||
skeleton.skin,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "RenderComponent": {
|
case "RenderComponent": {
|
||||||
const renderObject = component["rc.renderObj"];
|
const renderObject = component["rc.renderObj"];
|
||||||
|
|
||||||
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
||||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||||
|
|
||||||
// -1 means the batch has no lods and is always drawn
|
if (lodIndex !== 0) return;
|
||||||
if (lodIndex !== 0 && !(isPart && lodIndex === -1)) return;
|
|
||||||
|
|
||||||
const batchKey = batchIndex.toString().padStart(4, "0");
|
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||||
const batch = renderObject["ro.batches"][batchKey];
|
const batch = renderObject["ro.batches"][batchKey];
|
||||||
@@ -307,24 +193,10 @@ export async function extractModel(
|
|||||||
primitive.setAttribute(name, attributeAccessor);
|
primitive.setAttribute(name, attributeAccessor);
|
||||||
});
|
});
|
||||||
|
|
||||||
const skeleton = skeletons.get(hierarchy);
|
|
||||||
const mesh = document
|
const mesh = document
|
||||||
.createMesh(batch["##name"])
|
.createMesh(batch["##name"])
|
||||||
.addPrimitive(primitive);
|
.addPrimitive(primitive);
|
||||||
lodNode.setMesh(mesh);
|
lodNode.setMesh(mesh);
|
||||||
|
|
||||||
if (
|
|
||||||
skeleton &&
|
|
||||||
skinPrimitive(
|
|
||||||
document,
|
|
||||||
buffer,
|
|
||||||
primitive,
|
|
||||||
attributes,
|
|
||||||
renderObject[`skinnedObject.batch${batchIndex}.targetsData`],
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
lodNode.setSkin(skeleton.skin);
|
|
||||||
}
|
|
||||||
node.addChild(lodNode);
|
node.addChild(lodNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,20 +219,6 @@ export async function extractModel(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [owner, entity, joint, skin] of jointBindings) {
|
|
||||||
// the game only binds the skeleton entity's own children
|
|
||||||
const socket = owner
|
|
||||||
.listChildren()
|
|
||||||
.find(
|
|
||||||
(child) =>
|
|
||||||
child.getName() === entity && !skin.listJoints().includes(child),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!socket) throw new Error(`Missing socket ${entity}`);
|
|
||||||
|
|
||||||
bindSocket(socket, joint, skin);
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.addChild(document.createNode("test"));
|
scene.addChild(document.createNode("test"));
|
||||||
|
|
||||||
await document.transform(prune({ keepAttributes: true }), dedup());
|
await document.transform(prune({ keepAttributes: true }), dedup());
|
||||||
@@ -487,27 +345,6 @@ export async function extractMaterials(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabledPresets =
|
|
||||||
node.enabledPresets ?? defaultConfigArchive?.enabledPresets;
|
|
||||||
const fxName = node.fxName ?? defaultConfigArchive?.fxName;
|
|
||||||
|
|
||||||
if (enabledPresets?.AlphaBlend || enabledPresets?.AlphaBlendAdditive) {
|
|
||||||
material.setAlphaMode("BLEND");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enabledPresets?.AlphaBlendAdditive) {
|
|
||||||
// gltf has no additive blending so the viewer applies it
|
|
||||||
material.setExtras({ blending: "additive" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fxName?.endsWith("/Textured.material")) {
|
|
||||||
// unlit in game, emissive so the viewer can fade it with the lights
|
|
||||||
material
|
|
||||||
.setEmissiveTexture(material.getBaseColorTexture())
|
|
||||||
.setEmissiveFactor([1, 1, 1])
|
|
||||||
.setBaseColorFactor([0, 0, 0, 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textures.miscMap) {
|
if (textures.miscMap) {
|
||||||
material.setOcclusionTexture(
|
material.setOcclusionTexture(
|
||||||
document
|
document
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export interface CompareMember extends DuelMember {
|
|||||||
export interface CompareEphemeral {
|
export interface CompareEphemeral {
|
||||||
crewSkills: Record<string, number>;
|
crewSkills: Record<string, number>;
|
||||||
equalize: boolean;
|
equalize: boolean;
|
||||||
syncShells: boolean;
|
|
||||||
members: CompareMember[];
|
members: CompareMember[];
|
||||||
sorting?: {
|
sorting?: {
|
||||||
direction: "ascending" | "descending";
|
direction: "ascending" | "descending";
|
||||||
@@ -22,14 +21,5 @@ export const CompareEphemeral = new Varuna<
|
|||||||
>((crewSkills) => ({
|
>((crewSkills) => ({
|
||||||
crewSkills,
|
crewSkills,
|
||||||
equalize: false,
|
equalize: false,
|
||||||
syncShells: false,
|
|
||||||
members: [],
|
members: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function syncShellSlot(members: CompareMember[], slot: number) {
|
|
||||||
for (const member of members) {
|
|
||||||
const shell = member.gun.shells[slot];
|
|
||||||
|
|
||||||
if (shell) member.shell = shell;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -78,19 +78,10 @@ export enum TankopediaRelativeAgainst {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoaderState<Type> {
|
|
||||||
requested: Type;
|
|
||||||
loaded: Type;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Tankopedia {
|
interface Tankopedia {
|
||||||
disturbed: boolean;
|
disturbed: boolean;
|
||||||
revealed: boolean;
|
revealed: boolean;
|
||||||
loaders: {
|
modelRequested: boolean;
|
||||||
model: LoaderState<boolean>;
|
|
||||||
skin: LoaderState<number | undefined>;
|
|
||||||
attachment: LoaderState<number | undefined>;
|
|
||||||
};
|
|
||||||
shot?: Shot;
|
shot?: Shot;
|
||||||
skills: Record<string, number>;
|
skills: Record<string, number>;
|
||||||
relativeAgainst: TankopediaRelativeAgainst;
|
relativeAgainst: TankopediaRelativeAgainst;
|
||||||
@@ -112,6 +103,9 @@ interface Tankopedia {
|
|||||||
);
|
);
|
||||||
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
||||||
customShell?: ShellDefinition;
|
customShell?: ShellDefinition;
|
||||||
|
requestedSkin?: number;
|
||||||
|
loadedSkin?: number;
|
||||||
|
skin?: number;
|
||||||
requestedDisplay: TankopediaDisplay;
|
requestedDisplay: TankopediaDisplay;
|
||||||
display: TankopediaDisplay;
|
display: TankopediaDisplay;
|
||||||
statSearch?: string;
|
statSearch?: string;
|
||||||
@@ -122,11 +116,7 @@ const skillDefinitions = await api.skillDefinitions();
|
|||||||
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
||||||
disturbed: false,
|
disturbed: false,
|
||||||
revealed: false,
|
revealed: false,
|
||||||
loaders: {
|
modelRequested: App.state.autoLoadModels,
|
||||||
model: { requested: App.state.autoLoadModels, loaded: false },
|
|
||||||
skin: { requested: undefined, loaded: undefined },
|
|
||||||
attachment: { requested: undefined, loaded: undefined },
|
|
||||||
},
|
|
||||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||||
editStatic: false,
|
editStatic: false,
|
||||||
skills: createDefaultSkills(skillDefinitions),
|
skills: createDefaultSkills(skillDefinitions),
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/scripts
|
|
||||||
bun src/protos.ts
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/website
|
|
||||||
bun x --bun astro build
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
rm -rf \
|
|
||||||
packages/website/.astro \
|
|
||||||
packages/website/node_modules/.astro \
|
|
||||||
packages/website/node_modules/.vite
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/website
|
|
||||||
bun x --bun astro dev
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/scripts
|
|
||||||
bun src/discoverIds/index.ts
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/scripts
|
|
||||||
bun src/downloadClient/index.ts
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/closed
|
|
||||||
bun src/dava/downloadSmartDLC.ts
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/website
|
|
||||||
bun x --bun astro check
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/website
|
|
||||||
bun x --bun astro preview
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/scripts
|
|
||||||
bun src/recordPerformance/index.ts
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
chmod +x scripts/*.sh
|
|
||||||
bun install
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd packages/scripts
|
|
||||||
bun src/vfs/index.ts
|
|
||||||
Reference in new issue
Block a user