Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3daa56e100 | ||
|
|
37c257c8c4 | ||
|
|
dfc6a2bada | ||
|
|
9b8b06ca70 | ||
|
|
f7ffb2d34d | ||
|
|
2b7e88e30c | ||
|
|
af172c95f2 | ||
|
|
f8794de782 | ||
|
|
5fe295b209 | ||
|
|
3b75d7b523 | ||
|
|
0cbd67ff38 | ||
|
|
068f481b93 | ||
|
|
1430c32440 | ||
|
|
d14afc8b39 | ||
|
|
cba591b368 | ||
|
|
42b5ab6f02 | ||
|
|
c1363be70a | ||
|
|
602bf37411 | ||
|
|
10b0e29a26 | ||
|
|
ed36eedbfb |
No files matched your search
@@ -3,8 +3,6 @@ name: Website
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 4 * * *"
|
- cron: "0 4 * * *"
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
chunk: [default, api, models_tanks, models_legendary, models_maps]
|
chunk: [default, api]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Free disk space
|
- name: Free disk space
|
||||||
@@ -104,8 +102,7 @@ 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: ${{ inputs.branch || 'production' }}
|
||||||
ref: ${{ github.event.pull_request.head.sha || inputs.branch || 'production' }}
|
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
@@ -142,15 +139,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Trigger deployment
|
- name: Trigger deployment
|
||||||
|
|
||||||
# workflow should trigger if:
|
# Deploy if:
|
||||||
# - is by cron and is on main: triggering to production
|
# 1. Running cron from main using content from production to target production
|
||||||
# - is by push and is on production: triggering to production
|
# 2. Running manually and targeting non-production
|
||||||
# - is by manual and is on anything: triggering to anything
|
|
||||||
if: >-
|
if: >-
|
||||||
${{
|
${{
|
||||||
(github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|
github.ref == 'refs/heads/production' ||
|
||||||
|| (github.event_name == 'push' && github.ref == 'refs/heads/production')
|
(inputs.environment != '' && inputs.environment != 'production')
|
||||||
|| (github.event_name == 'workflow_dispatch')
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# BlitzKit Credits
|
||||||
|
|
||||||
|
BlitzKit is a one man band **TrèsAbhi** but I am far from solo. Helpers, in the order of contribution over time:
|
||||||
|
|
||||||
|
### S0AP
|
||||||
|
|
||||||
|
BlitzKit Discord bot verification, testing, advertisement, and the initial encouragement to create BlitzKit.
|
||||||
|
|
||||||
|
### Prince_NA
|
||||||
|
|
||||||
|
BlitzKit Discord bot and website testing and general advise.
|
||||||
|
|
||||||
|
### Vovko
|
||||||
|
|
||||||
|
BlitzKit Discord bot and website rendering pipeline.
|
||||||
|
|
||||||
|
### Maddox
|
||||||
|
|
||||||
|
Game file decompression.
|
||||||
|
|
||||||
|
### Pyogenics
|
||||||
|
|
||||||
|
Game 3D model parsing.
|
||||||
|
|
||||||
|
### Aeson
|
||||||
|
|
||||||
|
Game file layout, general advise, advertisement, and Tankopedia pre-release testing.
|
||||||
|
|
||||||
|
### Minitelrose, ChickenMan7777
|
||||||
|
|
||||||
|
3-caliber rule edge cases.
|
||||||
|
|
||||||
|
### HisRoyalFatness, Synx
|
||||||
|
|
||||||
|
Embed UX.
|
||||||
|
|
||||||
|
### [OPTML] clan, \_34, xkdkw, Droodles Blitz
|
||||||
|
|
||||||
|
Tankopedia pre-release testing.
|
||||||
|
|
||||||
|
### GonnaHetzMe
|
||||||
|
|
||||||
|
Redacted by request.
|
||||||
|
|
||||||
|
### Mark Greene
|
||||||
|
|
||||||
|
GitHub contributor on bug fixes and feature additions.
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
# BlitzKit 1.29.10
|
# BlitzKit 1.29.10
|
||||||
|
|
||||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Combined game mode passive and active abilities into a single dropdown
|
- Combined game mode passive and active abilities into a single dropdown
|
||||||
- Added tier filters to the Guess game [HitJack]
|
- Added tier filters to the Guess game [Mark Greene]
|
||||||
- Changing tiers will break your streak
|
- Changing tiers will break your streak
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- module damage is now properly shown when sorting with it in the tank search [HitJack]
|
- module damage is now properly shown when sorting with it in the tank search [Mark Greene]
|
||||||
- Duplicate abilities in the tank filters are now removed
|
- Duplicate abilities in the tank filters are now removed
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# BlitzKit 1.29.8
|
# BlitzKit 1.29.8
|
||||||
|
|
||||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -15,5 +15,5 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
|||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- The Tankopedia now smoothly resizes when enlarging on interaction
|
- The Tankopedia now smoothly resizes when enlarging on interaction
|
||||||
- Stock turrets now correctly mount the corresponding stock gun [HitJack]
|
- Stock turrets now correctly mount the corresponding stock gun [Mark Greene]
|
||||||
- The consumable and provision buttons in Compare are no longer tiny
|
- The consumable and provision buttons in Compare are no longer tiny
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
# BlitzKit 1.29.9
|
# BlitzKit 1.29.9
|
||||||
|
|
||||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Reworked tank search filters to be far more compact to make way for new filters
|
- Reworked tank search filters to be far more compact to make way for new filters
|
||||||
- Added the ability to filter tanks by consumables [HitJack]
|
- Added the ability to filter tanks by consumables [Mark Greene]
|
||||||
- Added the ability to filter tanks by provisions and game-mode abilities and powers
|
- Added the ability to filter tanks by provisions and game-mode abilities and powers
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- Consumable and provision icons in Compare are now properly sized
|
- Consumable and provision icons in Compare are now properly sized
|
||||||
- Provisions now properly affect terrain resistances [HitJack]
|
- Provisions now properly affect terrain resistances [Mark Greene]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# BlitzKit 1.30.7
|
# BlitzKit 1.30.7
|
||||||
|
|
||||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -8,13 +8,13 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
|||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- Clicking on the armor multiple times with HE no longer crashes BlitzKit [HitJack]
|
- Clicking on the armor multiple times with HE no longer crashes BlitzKit [Mark Greene]
|
||||||
- Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration [HitJack]
|
- Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration [Mark Greene]
|
||||||
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
|
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
|
||||||
- The spot light angle has been increased to more evenly light up larger tanks
|
- The spot light angle has been increased to more evenly light up larger tanks
|
||||||
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
|
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
|
||||||
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
|
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
|
||||||
- Increased the floor size and light decay to create the illusion that the floor is infinite
|
- Increased the floor size and light decay to create the illusion that the floor is infinite
|
||||||
- Safari no longer crashes when unloading textures for unloaded tanks [HitJack]
|
- Safari no longer crashes when unloading textures for unloaded tanks [Mark Greene]
|
||||||
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
|
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
|
||||||
- Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [HitJack]
|
- Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [Mark Greene]
|
||||||
@@ -1,75 +1,75 @@
|
|||||||
# BlitzKit 1.32.0
|
# BlitzKit 1.32.0
|
||||||
|
|
||||||
Thank you to [HitJack](https://github.com/Hitjack007) for your contributions this update.
|
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Tankopedia & Armor
|
### Tankopedia & Armor
|
||||||
|
|
||||||
- Added a spall liner button to the Tankopedia beside the other equipment [HitJack]
|
- Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene]
|
||||||
- Listed track repair times in the Tankopedia [HitJack]
|
- Listed track repair times in the Tankopedia [Mark Greene]
|
||||||
- The penetration chance is now displayed when penetration testing armor [HitJack]
|
- The penetration chance is now displayed when penetration testing armor [Mark Greene]
|
||||||
- Added a new setting to disable automatically loading tanks in Tankopedia [HitJack]
|
- Added a new setting to disable automatically loading tanks in Tankopedia [Mark Greene]
|
||||||
|
|
||||||
### Tank Performance & Mechanics
|
### Tank Performance & Mechanics
|
||||||
|
|
||||||
- Added Peekaboo DPM [HitJack]
|
- Added Peekaboo DPM [Mark Greene]
|
||||||
- Your Peekaboo DPM is lower than your normal DPM if your reload is faster than 10s as you'll have to wait for the remaining 10s for you to get unspotted before you can poke and shoot
|
- Your Peekaboo DPM is lower than your normal DPM if your reload is faster than 10s as you'll have to wait for the remaining 10s for you to get unspotted before you can poke and shoot
|
||||||
|
|
||||||
### Tank Filters
|
### Tank Filters
|
||||||
|
|
||||||
- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [HitJack]
|
- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [Mark Greene]
|
||||||
|
|
||||||
### Game Compatibility
|
### Game Compatibility
|
||||||
|
|
||||||
- Added compatibility for tier XI tanks [HitJack]
|
- Added compatibility for tier XI tanks [Mark Greene]
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
### Tankopedia & Armor
|
### Tankopedia & Armor
|
||||||
|
|
||||||
- The armor no longer disappears when you turn on Dynamic Armor System [HitJack]
|
- The armor no longer disappears when you turn on Dynamic Armor System [Mark Greene]
|
||||||
- Camouflage values in Tankopedia are now more descriptive when shooting [HitJack]
|
- Camouflage values in Tankopedia are now more descriptive when shooting [Mark Greene]
|
||||||
- The penetration chance is now displayed when penetration testing armor [HitJack]
|
- The penetration chance is now displayed when penetration testing armor [Mark Greene]
|
||||||
- Listed track repair times in the Tankopedia [HitJack]
|
- Listed track repair times in the Tankopedia [Mark Greene]
|
||||||
- Added a spall liner button to the Tankopedia beside the other equipment [HitJack]
|
- Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene]
|
||||||
- The flags in the Tankopedia tech tree are no longer offset to the top left [HitJack]
|
- The flags in the Tankopedia tech tree are no longer offset to the top left [Mark Greene]
|
||||||
- The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below [HitJack]
|
- The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below [Mark Greene]
|
||||||
- Removed the vertical sliver of pixels found to the right of the Equalizer toggle in Tankopedia
|
- Removed the vertical sliver of pixels found to the right of the Equalizer toggle in Tankopedia
|
||||||
|
|
||||||
### Tank Search & Filters
|
### Tank Search & Filters
|
||||||
|
|
||||||
- The search bar no longer redirects to a seemingly random tank when clicking enter when searching [HitJack]
|
- The search bar no longer redirects to a seemingly random tank when clicking enter when searching [Mark Greene]
|
||||||
- Tank search now properly handles the full names of tanks like the Super Pershing [HitJack]
|
- Tank search now properly handles the full names of tanks like the Super Pershing [Mark Greene]
|
||||||
- The tank search now remains consistent in order even when an added filter does not add/remove tanks from the list [HitJack]
|
- The tank search now remains consistent in order even when an added filter does not add/remove tanks from the list [Mark Greene]
|
||||||
- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [HitJack]
|
- The test and owned tank filters are now more intuitive as clicking on them now turns the clicked option on instead of off [Mark Greene]
|
||||||
|
|
||||||
### Visuals & UI
|
### Visuals & UI
|
||||||
|
|
||||||
- Removed the Beta badge from the Equalizer button [HitJack]
|
- Removed the Beta badge from the Equalizer button [Mark Greene]
|
||||||
- Updated the background image for the Equalizer button to the actual banner of the new game mode
|
- Updated the background image for the Equalizer button to the actual banner of the new game mode
|
||||||
- The contact shadows on the ground no longer grow & darken when switching between tabs [HitJack]
|
- The contact shadows on the ground no longer grow & darken when switching between tabs [Mark Greene]
|
||||||
- Additionally, the contact shadows stays visible at low angles [HitJack]
|
- Additionally, the contact shadows stays visible at low angles [Mark Greene]
|
||||||
- The "Help Translate BlitzKit" text on the home page is now properly centered vertically
|
- The "Help Translate BlitzKit" text on the home page is now properly centered vertically
|
||||||
- The footer now says "Branch" instead of "CDN" [HitJack]
|
- The footer now says "Branch" instead of "CDN" [Mark Greene]
|
||||||
- The game version in the footer now displays a shorthand version instead of the entire version number [HitJack]
|
- The game version in the footer now displays a shorthand version instead of the entire version number [Mark Greene]
|
||||||
- The reload visualizer now displays just 1 decimal place [HitJack]
|
- The reload visualizer now displays just 1 decimal place [Mark Greene]
|
||||||
- Plot controls for the Static armor tab no longer pokes in from the right on non-Static armor tabs in some languages [HitJack]
|
- Plot controls for the Static armor tab no longer pokes in from the right on non-Static armor tabs in some languages [Mark Greene]
|
||||||
- Mixer's controllers now scale better on smaller screens [HitJack]
|
- Mixer's controllers now scale better on smaller screens [Mark Greene]
|
||||||
- The list of available tanks in the Playlist is no longer excessively wide [HitJack]
|
- The list of available tanks in the Playlist is no longer excessively wide [Mark Greene]
|
||||||
|
|
||||||
### Tank Mechanics & Models
|
### Tank Mechanics & Models
|
||||||
|
|
||||||
- The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro [HitJack]
|
- The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro [Mark Greene]
|
||||||
- When moving the tracks of tanks, the textures no longer move at different rates [HitJack]
|
- When moving the tracks of tanks, the textures no longer move at different rates [Mark Greene]
|
||||||
|
|
||||||
### Localization
|
### Localization
|
||||||
|
|
||||||
- Users with a preference on a language are once again redirected to the correct localized pages [HitJack]
|
- Users with a preference on a language are once again redirected to the correct localized pages [Mark Greene]
|
||||||
|
|
||||||
### Session & Game Compatibility
|
### Session & Game Compatibility
|
||||||
|
|
||||||
- BlitzKit Session now properly handles tanks removed from the game [HitJack]
|
- BlitzKit Session now properly handles tanks removed from the game [Mark Greene]
|
||||||
|
|
||||||
## Technical Fixes
|
## Technical Fixes
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
|||||||
- This was the last bug standing in the way of BlitzKit Previews
|
- This was the last bug standing in the way of BlitzKit Previews
|
||||||
- Welcome back, BlitzKit Previews!
|
- Welcome back, BlitzKit Previews!
|
||||||
- Removed some dead code from the BlitzKit model mangler
|
- Removed some dead code from the BlitzKit model mangler
|
||||||
- The player discovery loop now persists for longer than 1 iteration [HitJack]
|
- The player discovery loop now persists for longer than 1 iteration [Mark Greene]
|
||||||
- Bun is now forced to run scripts with the `--bun` flag
|
- Bun is now forced to run scripts with the `--bun` flag
|
||||||
|
|
||||||
## Technical Features
|
## Technical Features
|
||||||
@@ -94,5 +94,5 @@ Thank you to [HitJack](https://github.com/Hitjack007) for your contributions thi
|
|||||||
- This was the last bug standing in the way of BlitzKit Previews
|
- This was the last bug standing in the way of BlitzKit Previews
|
||||||
- Welcome back, BlitzKit Previews!
|
- Welcome back, BlitzKit Previews!
|
||||||
- Removed some dead code from the BlitzKit model mangler
|
- Removed some dead code from the BlitzKit model mangler
|
||||||
- The player discovery loop now persists for longer than 1 iteration [HitJack]
|
- The player discovery loop now persists for longer than 1 iteration [Mark Greene]
|
||||||
- Bun is now forced to run scripts with the `--bun` flag
|
- Bun is now forced to run scripts with the `--bun` flag
|
||||||
@@ -4,33 +4,14 @@
|
|||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"default": null,
|
"default": null,
|
||||||
|
"api": null
|
||||||
"api": null,
|
|
||||||
|
|
||||||
"models_tanks": null,
|
|
||||||
"models_legendary": null,
|
|
||||||
"models_maps": null
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"default": "blitzkit/deploy-node-beta.blitzkit.app",
|
"default": "blitzkit/deploy-node-beta.blitzkit.app",
|
||||||
|
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
|
||||||
"api": "blitzkit/deploy-node-api.beta.blitzkit.app",
|
|
||||||
|
|
||||||
"models_tanks": "blitzkit/deploy-node-tanks.models.beta.blitzkit.app",
|
|
||||||
"models_legendary": "blitzkit/deploy-node-legendary.models.beta.blitzkit.app",
|
|
||||||
"models_maps": "blitzkit/deploy-node-maps.models.beta.blitzkit.app"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"default": "/",
|
|
||||||
|
|
||||||
"api": "https://api.beta.blitzkit.app",
|
"api": "https://api.beta.blitzkit.app",
|
||||||
|
"static": "https://static.beta.blitzkit.app"
|
||||||
"models_tanks": "https://tanks.models.beta.blitzkit.app",
|
|
||||||
"models_legendary": "https://legendary.models.beta.blitzkit.app",
|
|
||||||
"models_maps": "https://maps.models.beta.blitzkit.app",
|
|
||||||
|
|
||||||
"static": "https://static.blitzkit.app"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,25 +4,11 @@
|
|||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"default": null,
|
"default": null,
|
||||||
|
"api": "api"
|
||||||
"api": "api",
|
|
||||||
|
|
||||||
"models_tanks": "models/tanks",
|
|
||||||
"models_legendary": "models/legendary",
|
|
||||||
"models_maps": "models/maps"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"nodes": {},
|
"nodes": {},
|
||||||
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"default": "/",
|
|
||||||
|
|
||||||
"api": "/api",
|
"api": "/api",
|
||||||
|
|
||||||
"models_tanks": "/models/tanks",
|
|
||||||
"models_legendary": "/models/legendary",
|
|
||||||
"models_maps": "/models/maps",
|
|
||||||
|
|
||||||
"static": "https://static.blitzkit.app"
|
"static": "https://static.blitzkit.app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,33 +4,14 @@
|
|||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"default": null,
|
"default": null,
|
||||||
|
"api": null
|
||||||
"api": null,
|
|
||||||
|
|
||||||
"models_tanks": null,
|
|
||||||
"models_legendary": null,
|
|
||||||
"models_maps": null
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"default": "blitzkit/deploy-node-opentest.blitzkit.app",
|
"default": "blitzkit/deploy-node-opentest.blitzkit.app",
|
||||||
|
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
|
||||||
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app",
|
|
||||||
|
|
||||||
"models_tanks": "blitzkit/deploy-node-tanks.models.opentest.blitzkit.app",
|
|
||||||
"models_legendary": "blitzkit/deploy-node-legendary.models.opentest.blitzkit.app",
|
|
||||||
"models_maps": "blitzkit/deploy-node-maps.models.opentest.blitzkit.app"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"default": "/",
|
|
||||||
|
|
||||||
"api": "https://api.opentest.blitzkit.app",
|
"api": "https://api.opentest.blitzkit.app",
|
||||||
|
"static": "https://static.opentest.blitzkit.app"
|
||||||
"models_tanks": "https://tanks.models.opentest.blitzkit.app",
|
|
||||||
"models_legendary": "https://legendary.models.opentest.blitzkit.app",
|
|
||||||
"models_maps": "https://maps.models.opentest.blitzkit.app",
|
|
||||||
|
|
||||||
"static": "https://static.blitzkit.app"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,33 +4,14 @@
|
|||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"default": null,
|
"default": null,
|
||||||
|
"api": null
|
||||||
"api": null,
|
|
||||||
|
|
||||||
"models_tanks": null,
|
|
||||||
"models_legendary": null,
|
|
||||||
"models_maps": null
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"default": "blitzkit/deploy-node-preview.blitzkit.app",
|
"default": "blitzkit/deploy-node-preview.blitzkit.app",
|
||||||
|
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
|
||||||
"api": "blitzkit/deploy-node-api.preview.blitzkit.app",
|
|
||||||
|
|
||||||
"models_tanks": "blitzkit/deploy-node-tanks.models.preview.blitzkit.app",
|
|
||||||
"models_legendary": "blitzkit/deploy-node-legendary.models.preview.blitzkit.app",
|
|
||||||
"models_maps": "blitzkit/deploy-node-maps.models.preview.blitzkit.app"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"default": "/",
|
|
||||||
|
|
||||||
"api": "https://api.preview.blitzkit.app",
|
"api": "https://api.preview.blitzkit.app",
|
||||||
|
"static": "https://static.preview.blitzkit.app"
|
||||||
"models_tanks": "https://tanks.models.preview.blitzkit.app",
|
|
||||||
"models_legendary": "https://legendary.models.preview.blitzkit.app",
|
|
||||||
"models_maps": "https://maps.models.preview.blitzkit.app",
|
|
||||||
|
|
||||||
"static": "https://static.blitzkit.app"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,33 +4,14 @@
|
|||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"default": null,
|
"default": null,
|
||||||
|
"api": null
|
||||||
"api": null,
|
|
||||||
|
|
||||||
"models_tanks": null,
|
|
||||||
"models_legendary": null,
|
|
||||||
"models_maps": null
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"default": "blitzkit/deploy-node-blitzkit.app",
|
"default": "blitzkit/deploy-node-blitzkit.app",
|
||||||
|
"api": "blitzkit/deploy-node-api.blitzkit.app"
|
||||||
"api": "blitzkit/deploy-node-api.blitzkit.app",
|
|
||||||
|
|
||||||
"models_tanks": "blitzkit/deploy-node-tanks.models.blitzkit.app",
|
|
||||||
"models_legendary": "blitzkit/deploy-node-legendary.models.blitzkit.app",
|
|
||||||
"models_maps": "blitzkit/deploy-node-maps.models.blitzkit.app"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"default": "/",
|
|
||||||
|
|
||||||
"api": "https://api.blitzkit.app",
|
"api": "https://api.blitzkit.app",
|
||||||
|
|
||||||
"models_tanks": "https://tanks.models.blitzkit.app",
|
|
||||||
"models_legendary": "https://legendary.models.blitzkit.app",
|
|
||||||
"models_maps": "https://maps.models.blitzkit.app",
|
|
||||||
|
|
||||||
"static": "https://static.blitzkit.app"
|
"static": "https://static.blitzkit.app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.33.0-beta.1",
|
"version": "1.32.0",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
"build-protos": "cd packages/scripts && bun run build-protos",
|
"build-protos": "cd packages/scripts && bun run build-protos",
|
||||||
"record-performance": "cd packages/scripts && bun run record-performance",
|
"record-performance": "cd packages/scripts && bun run record-performance",
|
||||||
"download-client": "cd packages/scripts && bun run download-client",
|
"download-client": "cd packages/scripts && bun run download-client",
|
||||||
"clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite",
|
"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": [
|
||||||
|
|||||||
@@ -246,27 +246,6 @@ export abstract class ServerBlitzKitAPI0 extends BlitzKitAPI {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTankFilterIds(
|
|
||||||
name: string,
|
|
||||||
definitionKey: string,
|
|
||||||
filter: "include" | "exclude",
|
|
||||||
) {
|
|
||||||
const keys = name.trim().split(/\s+/).filter(Boolean);
|
|
||||||
|
|
||||||
return keys.flatMap((key) => {
|
|
||||||
const id = this.tankStringIdMap[key];
|
|
||||||
|
|
||||||
if (id === undefined) {
|
|
||||||
console.warn(
|
|
||||||
`Skipping unavailable tank "${key}" in ${filter} vehicle filter for "${definitionKey}"`,
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [id];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getString(name: string) {
|
protected getString(name: string) {
|
||||||
const collection: Record<string, string> = {
|
const collection: Record<string, string> = {
|
||||||
[locales.default]: this.stringsI18n[locales.default][name],
|
[locales.default]: this.stringsI18n[locales.default][name],
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
|||||||
: TankType.TANK_TYPE_RESEARCHABLE,
|
: TankType.TANK_TYPE_RESEARCHABLE,
|
||||||
tier: tank.level,
|
tier: tank.level,
|
||||||
class: tankClass,
|
class: tankClass,
|
||||||
testing: tankTags.includes("testTank") || tank.level === 11,
|
testing: tankTags.includes("testTank"),
|
||||||
deprecated: tankTags.includes("deprecated"),
|
deprecated: tankTags.includes("deprecated"),
|
||||||
price: tankPrice,
|
price: tankPrice,
|
||||||
camouflage_still: tankDefinition.root.invisibility.still,
|
camouflage_still: tankDefinition.root.invisibility.still,
|
||||||
@@ -595,25 +595,9 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
|||||||
for (const shellKey in gunListEntry.shots) {
|
for (const shellKey in gunListEntry.shots) {
|
||||||
const gunShellEntry = gunListEntry.shots[shellKey];
|
const gunShellEntry = gunListEntry.shots[shellKey];
|
||||||
const shell = shellList.root[shellKey];
|
const shell = shellList.root[shellKey];
|
||||||
|
|
||||||
// Newer client data stores shot properties on the shell rather
|
|
||||||
// than repeating them under every gun's <shots> entry.
|
|
||||||
const speed =
|
|
||||||
typeof gunShellEntry === "string"
|
|
||||||
? shell.speed
|
|
||||||
: gunShellEntry.speed;
|
|
||||||
const maxDistance =
|
|
||||||
typeof gunShellEntry === "string"
|
|
||||||
? shell.maxDistance
|
|
||||||
: gunShellEntry.maxDistance;
|
|
||||||
const piercingPower =
|
|
||||||
typeof gunShellEntry === "string"
|
|
||||||
? shell.piercingPower
|
|
||||||
: gunShellEntry.piercingPower;
|
|
||||||
|
|
||||||
const shellId = toUniqueId(nation, shell.id);
|
const shellId = toUniqueId(nation, shell.id);
|
||||||
const shellName = this.getString(shell.userString);
|
const shellName = this.getString(shell.userString);
|
||||||
const penetrationRaw = piercingPower
|
const penetrationRaw = gunShellEntry.piercingPower
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter((penetrationString) => penetrationString !== "")
|
.filter((penetrationString) => penetrationString !== "")
|
||||||
.map(Number);
|
.map(Number);
|
||||||
@@ -623,7 +607,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
|||||||
].shells.push({
|
].shells.push({
|
||||||
id: shellId,
|
id: shellId,
|
||||||
name: shellName,
|
name: shellName,
|
||||||
velocity: speed,
|
velocity: gunShellEntry.speed,
|
||||||
armor_damage: shell.damage.armor,
|
armor_damage: shell.damage.armor,
|
||||||
module_damage: shell.damage.devices,
|
module_damage: shell.damage.devices,
|
||||||
caliber: shell.caliber,
|
caliber: shell.caliber,
|
||||||
@@ -639,7 +623,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
|||||||
near: penetrationRaw[0],
|
near: penetrationRaw[0],
|
||||||
far: penetrationRaw[1],
|
far: penetrationRaw[1],
|
||||||
},
|
},
|
||||||
range: maxDistance,
|
range: gunShellEntry.maxDistance,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,15 @@
|
|||||||
import {
|
import { CamouflageDefinitions } from "@blitzkit/core";
|
||||||
CamouflageDefinitions,
|
|
||||||
Sc2ReadStream,
|
|
||||||
type TankParameters,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import { isEqual, times } from "lodash-es";
|
|
||||||
import { Cache } from "./0_base";
|
import { Cache } from "./0_base";
|
||||||
import { ServerBlitzKitAPI1 } from "./1_tankDefinitions";
|
import { ServerBlitzKitAPI1 } from "./1_tankDefinitions";
|
||||||
|
|
||||||
export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
||||||
private async skinPresets(vehicle: string) {
|
|
||||||
const [nation, tankKey] = vehicle.split(":");
|
|
||||||
const parameters = await this.vfs.yaml<TankParameters>(
|
|
||||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
|
||||||
);
|
|
||||||
const sc2 = new Sc2ReadStream(
|
|
||||||
(
|
|
||||||
await this.vfs.file(`Data/3d/${parameters.resourcesPath.blitzModelPath}`)
|
|
||||||
).buffer as ArrayBuffer,
|
|
||||||
).sc2();
|
|
||||||
const presets = new Set<string>();
|
|
||||||
|
|
||||||
for (const node of sc2["#dataNodes"]) {
|
|
||||||
if (typeof node.configCount !== "number") continue;
|
|
||||||
|
|
||||||
times(node.configCount, (index) => node[`configArchive_${index}`])
|
|
||||||
.filter(
|
|
||||||
(archive) =>
|
|
||||||
!isEqual(archive.textures, node.configArchive_0.textures),
|
|
||||||
)
|
|
||||||
.forEach((archive) => presets.add(archive.configName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return presets;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Cache()
|
@Cache()
|
||||||
async camouflageDefinitions() {
|
async camouflageDefinitions() {
|
||||||
const camouflageDefinitions = CamouflageDefinitions.create();
|
const camouflageDefinitions = CamouflageDefinitions.create();
|
||||||
const customization = await this.vfs.yaml<
|
|
||||||
Record<string, { Name: string; Path: string }>
|
|
||||||
>("Data/3d/Customization.yaml");
|
|
||||||
const items = new Map(
|
|
||||||
Object.values(customization).map(({ Name, Path }) => [
|
|
||||||
Name,
|
|
||||||
Path.replace("~res:/", "Data/").replace(/\.sc2$/, ""),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
const skinPresets = new Map<string, Promise<Set<string>>>();
|
|
||||||
|
|
||||||
for (const camoKey in this.camouflagesXml!.root.camouflages) {
|
for (const camoKey in this.camouflagesXml!.root.camouflages) {
|
||||||
const camo = this.camouflagesXml!.root.camouflages[camoKey];
|
const camo = this.camouflagesXml!.root.camouflages[camoKey];
|
||||||
|
|
||||||
if (!(camoKey in this.camouflagesYaml!)) continue;
|
|
||||||
|
|
||||||
const yamlEntry = this.camouflagesYaml![camoKey];
|
const yamlEntry = this.camouflagesYaml![camoKey];
|
||||||
const fullName = yamlEntry.userString
|
const fullName = yamlEntry.userString
|
||||||
? this.getString(yamlEntry.userString)
|
? this.getString(yamlEntry.userString)
|
||||||
@@ -63,49 +20,12 @@ export abstract class ServerBlitzKitAPI2 extends ServerBlitzKitAPI1 {
|
|||||||
const resolvedTankName = shortName ?? fullName;
|
const resolvedTankName = shortName ?? fullName;
|
||||||
const resolvedTankNameFull =
|
const resolvedTankNameFull =
|
||||||
resolvedTankName === fullName ? undefined : fullName;
|
resolvedTankName === fullName ? undefined : fullName;
|
||||||
const include = camo.vehicleFilter.include;
|
|
||||||
const vehicle =
|
|
||||||
include && !Array.isArray(include) && "vehicle" in include
|
|
||||||
? include.vehicle?.name
|
|
||||||
: undefined;
|
|
||||||
let tankId: number | undefined = undefined;
|
|
||||||
|
|
||||||
if (vehicle && vehicle in this.tankStringIdMap) {
|
|
||||||
const resolvedParts = await Promise.all(
|
|
||||||
Object.values(yamlEntry.customEntities ?? {}).map(
|
|
||||||
async ({ item }) => {
|
|
||||||
const path = items.get(item);
|
|
||||||
|
|
||||||
return (
|
|
||||||
path !== undefined &&
|
|
||||||
(await this.vfs.resolve(`${path}.sc2`)) &&
|
|
||||||
(await this.vfs.resolve(`${path}.scg`))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let hasPreset = false;
|
|
||||||
|
|
||||||
if (camo.group === "legendary" && yamlEntry.preset !== undefined) {
|
|
||||||
if (!skinPresets.has(vehicle)) {
|
|
||||||
skinPresets.set(vehicle, this.skinPresets(vehicle));
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPreset = (await skinPresets.get(vehicle)!).has(yamlEntry.preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedParts.some(Boolean) || hasPreset) {
|
|
||||||
tankId = this.tankStringIdMap[vehicle];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
camouflageDefinitions.camouflages[camo.id] = {
|
camouflageDefinitions.camouflages[camo.id] = {
|
||||||
id: camo.id,
|
id: camo.id,
|
||||||
name: this.getString(camo.userString),
|
name: this.getString(camo.userString),
|
||||||
tank_name: resolvedTankName,
|
tank_name: resolvedTankName,
|
||||||
tank_name_full: resolvedTankNameFull,
|
tank_name_full: resolvedTankNameFull,
|
||||||
tank_id: tankId,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
|
|||||||
filter_type: {
|
filter_type: {
|
||||||
$case: "ids",
|
$case: "ids",
|
||||||
value: {
|
value: {
|
||||||
ids: this.getTankFilterIds(includeRaw.name, key, "include"),
|
ids: includeRaw.name.split(/ +/).map((key) => {
|
||||||
|
return this.tankStringIdMap[key];
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -70,7 +72,9 @@ export abstract class ServerBlitzKitAPI6 extends ServerBlitzKitAPI5 {
|
|||||||
filter_type: {
|
filter_type: {
|
||||||
$case: "ids",
|
$case: "ids",
|
||||||
value: {
|
value: {
|
||||||
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"),
|
ids: excludeRaw.name.split(/ +/).map((key) => {
|
||||||
|
return this.tankStringIdMap[key];
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
|||||||
async provisionDefinitions() {
|
async provisionDefinitions() {
|
||||||
const provisionDefinitions = ProvisionDefinitions.create();
|
const provisionDefinitions = ProvisionDefinitions.create();
|
||||||
|
|
||||||
Object.entries(this.provisionsCommon).forEach(([key, provision]) => {
|
Object.entries(this.provisionsCommon).forEach(([, provision]) => {
|
||||||
const entry: Provision = {
|
const entry: Provision = {
|
||||||
id: provision.id,
|
id: provision.id,
|
||||||
exclude: [],
|
exclude: [],
|
||||||
@@ -42,7 +42,9 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
|||||||
filter_type: {
|
filter_type: {
|
||||||
$case: "ids",
|
$case: "ids",
|
||||||
value: {
|
value: {
|
||||||
ids: this.getTankFilterIds(includeRaw.name, key, "include"),
|
ids: includeRaw.name.split(/ +/).map((key) => {
|
||||||
|
return this.tankStringIdMap[key];
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -68,7 +70,9 @@ export abstract class ServerBlitzKitAPI7 extends ServerBlitzKitAPI6 {
|
|||||||
filter_type: {
|
filter_type: {
|
||||||
$case: "ids",
|
$case: "ids",
|
||||||
value: {
|
value: {
|
||||||
ids: this.getTankFilterIds(excludeRaw.name, key, "exclude"),
|
ids: excludeRaw.name
|
||||||
|
.split(/ +/)
|
||||||
|
.map((key) => this.tankStringIdMap[key]),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,5 +13,4 @@ message Camouflage {
|
|||||||
required I18nString name = 2;
|
required I18nString name = 2;
|
||||||
optional I18nString tank_name = 3;
|
optional I18nString tank_name = 3;
|
||||||
optional I18nString tank_name_full = 4;
|
optional I18nString tank_name_full = 4;
|
||||||
optional uint32 tank_id = 5;
|
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,6 @@ export interface Camouflage {
|
|||||||
name: I18nString | undefined;
|
name: I18nString | undefined;
|
||||||
tank_name?: I18nString | undefined;
|
tank_name?: I18nString | undefined;
|
||||||
tank_name_full?: I18nString | undefined;
|
tank_name_full?: I18nString | undefined;
|
||||||
tank_id?: number | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
function createBaseCamouflageDefinitions(): CamouflageDefinitions {
|
||||||
@@ -192,7 +191,7 @@ export const CamouflageDefinitions_CamouflagesEntry: MessageFns<CamouflageDefini
|
|||||||
};
|
};
|
||||||
|
|
||||||
function createBaseCamouflage(): Camouflage {
|
function createBaseCamouflage(): Camouflage {
|
||||||
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined, tank_id: 0 };
|
return { id: 0, name: undefined, tank_name: undefined, tank_name_full: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Camouflage: MessageFns<Camouflage> = {
|
export const Camouflage: MessageFns<Camouflage> = {
|
||||||
@@ -209,9 +208,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
if (message.tank_name_full !== undefined) {
|
if (message.tank_name_full !== undefined) {
|
||||||
I18nString.encode(message.tank_name_full, writer.uint32(34).fork()).join();
|
I18nString.encode(message.tank_name_full, writer.uint32(34).fork()).join();
|
||||||
}
|
}
|
||||||
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
|
||||||
writer.uint32(40).uint32(message.tank_id);
|
|
||||||
}
|
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -254,14 +250,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
message.tank_name_full = I18nString.decode(reader, reader.uint32());
|
message.tank_name_full = I18nString.decode(reader, reader.uint32());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
case 5: {
|
|
||||||
if (tag !== 40) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.tank_id = reader.uint32();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
@@ -277,7 +265,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined,
|
name: isSet(object.name) ? I18nString.fromJSON(object.name) : undefined,
|
||||||
tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined,
|
tank_name: isSet(object.tank_name) ? I18nString.fromJSON(object.tank_name) : undefined,
|
||||||
tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined,
|
tank_name_full: isSet(object.tank_name_full) ? I18nString.fromJSON(object.tank_name_full) : undefined,
|
||||||
tank_id: isSet(object.tank_id) ? globalThis.Number(object.tank_id) : 0,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -295,9 +282,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
if (message.tank_name_full !== undefined) {
|
if (message.tank_name_full !== undefined) {
|
||||||
obj.tank_name_full = I18nString.toJSON(message.tank_name_full);
|
obj.tank_name_full = I18nString.toJSON(message.tank_name_full);
|
||||||
}
|
}
|
||||||
if (message.tank_id !== undefined && message.tank_id !== 0) {
|
|
||||||
obj.tank_id = Math.round(message.tank_id);
|
|
||||||
}
|
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -316,7 +300,6 @@ export const Camouflage: MessageFns<Camouflage> = {
|
|||||||
message.tank_name_full = (object.tank_name_full !== undefined && object.tank_name_full !== null)
|
message.tank_name_full = (object.tank_name_full !== undefined && object.tank_name_full !== null)
|
||||||
? I18nString.fromPartial(object.tank_name_full)
|
? I18nString.fromPartial(object.tank_name_full)
|
||||||
: undefined;
|
: undefined;
|
||||||
message.tank_id = object.tank_id ?? 0;
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,5 @@ export type CamouflagesYaml = Record<
|
|||||||
{
|
{
|
||||||
userString?: string;
|
userString?: string;
|
||||||
shortUserString?: string;
|
shortUserString?: string;
|
||||||
preset?: string;
|
|
||||||
customEntities?: Record<string, { item: string }>;
|
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
@@ -20,13 +20,11 @@ export interface GunDefinitionsList {
|
|||||||
burst?: { count: number; rate: number };
|
burst?: { count: number; rate: number };
|
||||||
clip?: { count: number; rate: number };
|
clip?: { count: number; rate: number };
|
||||||
shots: {
|
shots: {
|
||||||
[key: string]:
|
[key: string]: {
|
||||||
| ""
|
speed: number;
|
||||||
| {
|
piercingPower: string;
|
||||||
speed: number;
|
maxDistance: number;
|
||||||
piercingPower: string;
|
};
|
||||||
maxDistance: number;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ export type ShellDefinitionsList = Record<
|
|||||||
normalizationAngle?: number;
|
normalizationAngle?: number;
|
||||||
ricochetAngle?: number;
|
ricochetAngle?: number;
|
||||||
explosionRadius?: number;
|
explosionRadius?: number;
|
||||||
speed: number;
|
|
||||||
maxDistance?: number;
|
|
||||||
piercingPower: string;
|
|
||||||
}
|
}
|
||||||
> & {
|
> & {
|
||||||
icons: Record<string, string>;
|
icons: Record<string, string>;
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"test": {
|
||||||
|
"default": "test",
|
||||||
|
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
|
||||||
|
"fox": "The quick brown fox jumps over the lazy dog."
|
||||||
|
},
|
||||||
|
|
||||||
"common": {
|
"common": {
|
||||||
"name": "BlitzKit",
|
"name": "BlitzKit",
|
||||||
|
|
||||||
@@ -332,13 +338,6 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"tools": {
|
||||||
"credits": {
|
|
||||||
"name": "Credits",
|
|
||||||
"button": "To the docs",
|
|
||||||
"description": "Contributors of BlitzKit",
|
|
||||||
"seo_description": "BlitzKit is the work of many"
|
|
||||||
},
|
|
||||||
|
|
||||||
"playlist": {
|
"playlist": {
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "Shuffle",
|
||||||
@@ -997,6 +996,7 @@
|
|||||||
|
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
|
"credits": "Credits",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"patches": "Patches"
|
"patches": "Patches"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"test": {
|
"test": {
|
||||||
"default": "test",
|
"default": "test",
|
||||||
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
|
"long": "To jest długi ciąg testowy. NIGDY NIE ZOBACZYSZ tego w interfejsie użytkownika. Mój ulubiony owoc to {fruit}, ponieważ ma kolor {color}. Tak bardzo go lubię, że powiem jego nazwę 3 razy! {fruit}! {fruit}! {fruit}! Po raz kolejny to {color}, a nie {not_color}.",
|
||||||
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
|
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"gun_types": {
|
"gun_types": {
|
||||||
"auto_loader": "Samo ładowacz",
|
"auto_loader": "Samo ładowacz",
|
||||||
"auto_reloader": "Automatyczne przeładowanie",
|
"auto_reloader": "Automatyczne przeładowanie",
|
||||||
"regular": "Regular"
|
"regular": "Regularny"
|
||||||
},
|
},
|
||||||
"shells": {
|
"shells": {
|
||||||
"ap": "AP",
|
"ap": "AP",
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
},
|
},
|
||||||
"branches": {
|
"branches": {
|
||||||
"beta": "Beta",
|
"beta": "Beta",
|
||||||
"development": "Development",
|
"development": "Rozwój",
|
||||||
"opentest": "OpenTest",
|
"opentest": "OtwartyTest",
|
||||||
"preview": "Preview",
|
"preview": "Przegląd",
|
||||||
"production": "Production"
|
"production": "Produkcja"
|
||||||
},
|
},
|
||||||
"locales": {
|
"locales": {
|
||||||
"en": "Angielski",
|
"en": "Angielski",
|
||||||
@@ -102,28 +102,28 @@
|
|||||||
"other": "Hybryda"
|
"other": "Hybryda"
|
||||||
},
|
},
|
||||||
"tank_class_short": {
|
"tank_class_short": {
|
||||||
"-1": "unrecognized",
|
"-1": "nierozpoznany",
|
||||||
"0": "Lekki",
|
"0": "Lekki",
|
||||||
"1": "Średni",
|
"1": "Średni",
|
||||||
"2": "Ciężki",
|
"2": "Ciężki",
|
||||||
"3": "TD"
|
"3": "NC"
|
||||||
},
|
},
|
||||||
"tank_class_short_plural_lowercase": {
|
"tank_class_short_plural_lowercase": {
|
||||||
"0": "lekkie",
|
"0": "lekkie",
|
||||||
"1": "średnie",
|
"1": "średnie",
|
||||||
"2": "ciężkie",
|
"2": "ciężkie",
|
||||||
"3": "TDs"
|
"3": "NC-ów"
|
||||||
},
|
},
|
||||||
"tank_class_medium": {
|
"tank_class_medium": {
|
||||||
"-1": "unrecognized",
|
"-1": "nierozpoznany",
|
||||||
"0": "Lekki",
|
"0": "Lekki",
|
||||||
"1": "Średni",
|
"1": "Średni",
|
||||||
"2": "Ciężki",
|
"2": "Ciężki",
|
||||||
"3": "Niszczyciel czołgów"
|
"3": "Niszczyciel czołgów"
|
||||||
},
|
},
|
||||||
"tree_type": {
|
"tree_type": {
|
||||||
"-1": "unrecognized",
|
"-1": "nierozpoznany",
|
||||||
"0": "Tech tree",
|
"0": "Drzewko technologiczne",
|
||||||
"1": "Premium",
|
"1": "Premium",
|
||||||
"2": "Kolekcjonerski"
|
"2": "Kolekcjonerski"
|
||||||
},
|
},
|
||||||
@@ -187,44 +187,44 @@
|
|||||||
},
|
},
|
||||||
"website": {
|
"website": {
|
||||||
"redirect": {
|
"redirect": {
|
||||||
"title": "301: moved permanently",
|
"title": "301: przeniesiony na stałe",
|
||||||
"body": "This page has moved. Redirecting..."
|
"body": "Ta strona została przeniesiona. Przekierowywanie..."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Ustawienia",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Zmień ustawienia BlitzKit",
|
||||||
"dev_mode": "Development mode",
|
"dev_mode": "Tryb deweloperski",
|
||||||
"auto_load_models": "Auto-load models",
|
"auto_load_models": "Automatycznie wczytaj modele",
|
||||||
"language": "Language"
|
"language": "Język"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"player_search": {
|
"player_search": {
|
||||||
"hint": "Search players...",
|
"hint": "Szukaj graczy...",
|
||||||
"no_results": "No results",
|
"no_results": "Brak wyników",
|
||||||
"searching": "Searching..."
|
"searching": "Wyszukiwanie..."
|
||||||
},
|
},
|
||||||
"copy_button": {
|
"copy_button": {
|
||||||
"copied": "Copied!"
|
"copied": "Skopiowano!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
"select_dialog_title": "Search for tanks",
|
"select_dialog_title": "Szukaj czołgów",
|
||||||
"swap_dialog_title": "Swap tank",
|
"swap_dialog_title": "Zamień czołg",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Wyczyść filtry",
|
||||||
"login": "Login to use this",
|
"login": "Zaloguj się, aby tego użyć",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Szukaj czołgów...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Ostatnio oglądane",
|
||||||
"select_all": "Select all",
|
"select_all": "Zaznacz wszystko",
|
||||||
"count_singular": "1 tank",
|
"count_singular": "1 czołg",
|
||||||
"count_plural": "{count} tanks",
|
"count_plural": "{count} czołgi",
|
||||||
"sorting_by": "Sorting by {name}",
|
"sorting_by": "Sortowanie według {name}",
|
||||||
"clear": "Clear",
|
"clear": "Wyczyść",
|
||||||
"tier": "Tier {tier}",
|
"tier": "Poziom czołgu {tier}",
|
||||||
"passive": "{name} (Passive)",
|
"passive": "{name} (Pasywny)",
|
||||||
"active": "{name} (Aktywne)",
|
"active": "{name} (Aktywne)",
|
||||||
"no_tanks_found": {
|
"no_tanks_found": {
|
||||||
"body": "Nie znaleziono czołgu.",
|
"body": "Nie znaleziono czołgu.",
|
||||||
"search_again": "",
|
"search_again": "",
|
||||||
"clear_filters": "Try clearing filters."
|
"clear_filters": "Spróbuj wyczyścić filtry."
|
||||||
},
|
},
|
||||||
"sort": {
|
"sort": {
|
||||||
"meta.none": "brak",
|
"meta.none": "brak",
|
||||||
@@ -443,71 +443,71 @@
|
|||||||
"ricochet": "Ricochet",
|
"ricochet": "Ricochet",
|
||||||
"explosion_radius": "Explosion radius",
|
"explosion_radius": "Explosion radius",
|
||||||
"exit": "Exit",
|
"exit": "Exit",
|
||||||
"reset": "Reset"
|
"reset": "Zresetuj"
|
||||||
},
|
},
|
||||||
"advanced": "Advanced highlighting",
|
"advanced": "Zaawansowane podświetlanie",
|
||||||
"shot_card": {
|
"shot_card": {
|
||||||
"element": {
|
"element": {
|
||||||
"primary": "Primary",
|
"primary": "Główny",
|
||||||
"spaced": "Spaced",
|
"spaced": "Spaced",
|
||||||
"external": "External",
|
"external": "Zewnętrzny",
|
||||||
"gap": "Gap",
|
"gap": "Odstęp",
|
||||||
"gun": "Gun",
|
"gun": "Działo",
|
||||||
"track": "Tracks"
|
"track": "Gąsienice"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"penetration": "{damage}hp penetration",
|
"penetration": "{damage} penetracja punktów wytrzymałości",
|
||||||
"blocked": "Blocked",
|
"blocked": "Zablokowany",
|
||||||
"ricochet": "Ricochet",
|
"ricochet": "Rykoszet",
|
||||||
"splash": "{damage}hp splash"
|
"splash": "{damage} rozprysk punktów wytrzymałości"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"thickness_and_angle": "{thickness}mm {angle}°",
|
"thickness_and_angle": "{thickness}mm {angle}",
|
||||||
"nominal": "{thickness}mm nominal",
|
"nominal": "{thickness}mm nominalny",
|
||||||
"ricochet_loss": "{loss}% penetration"
|
"ricochet_loss": "{loss}% penetracji"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"static": {
|
"static": {
|
||||||
"name": "Static armor",
|
"name": "Pancerz statyczny",
|
||||||
"primary": "Primary",
|
"primary": "Główny Pancerz",
|
||||||
"spaced": "Spaced",
|
"spaced": "Ekran przeciwkumulacyjny",
|
||||||
"external": "External",
|
"external": "Zewnętrzny",
|
||||||
"edit": {
|
"edit": {
|
||||||
"label": "Edit",
|
"label": "Edytuj",
|
||||||
"reset": "Reset"
|
"reset": "Zresetuj"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"back": "Back",
|
"back": "Powrót",
|
||||||
"compare": "Compare",
|
"compare": "Porównaj",
|
||||||
"swap": {
|
"swap": {
|
||||||
"button": "Swap",
|
"button": "Zamień",
|
||||||
"title": "Swap tanks"
|
"title": "Zamień czołgi"
|
||||||
},
|
},
|
||||||
"vanilla": "Vanilla",
|
"vanilla": "Vanilla",
|
||||||
"opentest": "OpenTest",
|
"opentest": "OpenTest",
|
||||||
"full_name": "Full name",
|
"full_name": "Pełna nazwa",
|
||||||
"nation": "Naród",
|
"nation": "Naród",
|
||||||
"class": "Klasa",
|
"class": "Klasa",
|
||||||
"tier": "Poziom",
|
"tier": "Poziom",
|
||||||
"type": "Rodzaj",
|
"type": "Rodzaj",
|
||||||
"dev_id": "DEV: ID",
|
"dev_id": "DEV: ID",
|
||||||
"dev_name": "DEV: NAME",
|
"dev_name": "DEV: NAZWA",
|
||||||
"purchase": "Purchase price",
|
"purchase": "Cena zakupu",
|
||||||
"restoration": "Restoration price",
|
"restoration": "Cena odzyskania",
|
||||||
"sale": "Sale price",
|
"sale": "Sale price",
|
||||||
"research": "Research XP"
|
"research": "Research XP"
|
||||||
},
|
},
|
||||||
"tech_tree": {
|
"tech_tree": {
|
||||||
"title": "Tech tree",
|
"title": "Drzewko technologiczne",
|
||||||
"battle": "1 battle",
|
"battle": "1 bitwa",
|
||||||
"battles": "{count} battles",
|
"battles": "{count} bitew",
|
||||||
"route": "Route {index} of {count}"
|
"route": "Ścieżka {index} z {count}"
|
||||||
},
|
},
|
||||||
"game_modes": {
|
"game_modes": {
|
||||||
"title": "Game mode abilities"
|
"title": "Umiejętności trybu gry"
|
||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review videos",
|
"title": "Review videos",
|
||||||
@@ -520,26 +520,26 @@
|
|||||||
"equalize": "Equalize"
|
"equalize": "Equalize"
|
||||||
},
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"title": "Modules",
|
"title": "Moduły",
|
||||||
"stock": "Stock",
|
"stock": "Standardowy",
|
||||||
"upgrade": "Upgrade"
|
"upgrade": "Ulepsz"
|
||||||
},
|
},
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"title": "Equipment",
|
"title": "Wyposażenie",
|
||||||
"clear": "Clear"
|
"clear": "Wyczyść"
|
||||||
},
|
},
|
||||||
"provisions": {
|
"provisions": {
|
||||||
"title": "Provisions",
|
"title": "Materiały eksploatacyjne",
|
||||||
"clear": "Clear"
|
"clear": "Wyczyść"
|
||||||
},
|
},
|
||||||
"consumables": {
|
"consumables": {
|
||||||
"title": "Consumables",
|
"title": "Consumables",
|
||||||
"clear": "Clear",
|
"clear": "Wyczyść",
|
||||||
"info": "Yellow consumables do not affect the statistics."
|
"info": "Yellow consumables do not affect the statistics."
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
"title": "Crew skills",
|
"title": "Crew skills",
|
||||||
"clear": "Clear",
|
"clear": "Wyczyść",
|
||||||
"maximize": "Maximize",
|
"maximize": "Maximize",
|
||||||
"info": "Yellow skills only apply under special circumstances. Hold Space to quickly toggle between level 0 and 7."
|
"info": "Yellow skills only apply under special circumstances. Hold Space to quickly toggle between level 0 and 7."
|
||||||
},
|
},
|
||||||
@@ -557,49 +557,49 @@
|
|||||||
},
|
},
|
||||||
"values": {
|
"values": {
|
||||||
"shell_index": "Shell {index}",
|
"shell_index": "Shell {index}",
|
||||||
"at_distance": "At {distance}m",
|
"at_distance": "Z odległości {distance}m",
|
||||||
"at_distance_slider_label": "Distance",
|
"at_distance_slider_label": "Zasięg widzenia",
|
||||||
"dispersion_at_distance": "Dispersion at {distance}m",
|
"dispersion_at_distance": "Rozrzut na {distance}m",
|
||||||
"gun_flexibility": "Gun flexibility",
|
"gun_flexibility": "Elastyczność działa",
|
||||||
"speed": "Speed",
|
"speed": "Prędkość",
|
||||||
"terrain_coefficients": "Terrain coefficients",
|
"terrain_coefficients": "Współczynniki terenu",
|
||||||
"raw_terrain_coefficients": "Raw terrain coefficients",
|
"raw_terrain_coefficients": "Surowe współczynniki terenu",
|
||||||
"power_to_weight_ratio": "Power to weight ratio",
|
"power_to_weight_ratio": "Stosunek mocy do masy",
|
||||||
"hull_traverse_speed": "Hull traverse speed",
|
"hull_traverse_speed": "Prędkość obrotu kadłuba",
|
||||||
"camouflage": "Camouflage",
|
"camouflage": "Kamuflaż",
|
||||||
"reload": "Reload",
|
"reload": "Przeładowanie",
|
||||||
"crewCount": "Crew count",
|
"crewCount": "Liczba członków załogi",
|
||||||
"shellNormalization": "Normalization",
|
"shellNormalization": "Normalization",
|
||||||
"shellRicochet": "Ricochet",
|
"shellRicochet": "Ricochet",
|
||||||
"dpm": "DPM",
|
"dpm": "Uszkodzenia na minutę",
|
||||||
"dpmEffective": "Effective at 60s",
|
"dpmEffective": "Efektywne w 60s",
|
||||||
"peekabooDpm": "Peekaboo DPM",
|
"peekabooDpm": "Obrażenia na minutę w stylu „wychyl i schowaj”",
|
||||||
"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": "Obrażenia na minutę przy założeniu, że strzelisz tylko po zresetowaniu kamuflażu (lub ponownym załadowaniu, jeśli jest wolniejszy) — co najmniej 10 sekund pomiędzy strzałami.",
|
||||||
"shells": "Shells",
|
"shells": "Pociski",
|
||||||
"mostOptimalShellIndex": "Most optimal shell",
|
"mostOptimalShellIndex": "Optymalny pocisk",
|
||||||
"shellReloads": "Reloads",
|
"shellReloads": "Przeładowania",
|
||||||
"shellReload": "Reload",
|
"shellReload": "Przeładowanie",
|
||||||
"intraClip": "Intra-clip",
|
"intraClip": "Czas przeładowania pocisku",
|
||||||
"caliber": "Caliber",
|
"caliber": "Caliber",
|
||||||
"penetration": "Penetration",
|
"penetration": "Penetration",
|
||||||
"damage": "Uszkodzenia",
|
"damage": "Uszkodzenia",
|
||||||
"damageWithoutAssault": "Damage without assault shells",
|
"damageWithoutAssault": "Uszkodzenia bez pocisków szturmowych",
|
||||||
"clipDamage": "Clipping potential",
|
"clipDamage": "Potencjał magazynu",
|
||||||
"moduleDamage": "Module damage",
|
"moduleDamage": "Uszkodzenia modułów",
|
||||||
"explosionRadius": "Explosion radius",
|
"explosionRadius": "Promień eksplozji",
|
||||||
"shellVelocity": "Shell velocity",
|
"shellVelocity": "Prędkość pocisku",
|
||||||
"aimTime": "Aim time",
|
"aimTime": "Czas celowania",
|
||||||
"dispersion": "Still",
|
"dispersion": "Still",
|
||||||
"dispersionMoving": "Moving",
|
"dispersionMoving": "Podczas ruchu",
|
||||||
"dispersionHullTraversing": "Hull traversing",
|
"dispersionHullTraversing": "Obrót kadłuba",
|
||||||
"dispersionTurretTraversing": "Turret traversing",
|
"dispersionTurretTraversing": "Obrót wieży",
|
||||||
"dispersionShooting": "Shooting",
|
"dispersionShooting": "Strzelanie",
|
||||||
"dispersionGunDamaged": "Gun damaged",
|
"dispersionGunDamaged": "Uszkodzone Działo",
|
||||||
"gunDepression": "Depression",
|
"gunDepression": "Limit obrotu działa w dół",
|
||||||
"gunElevation": "Elevation",
|
"gunElevation": "Limit obrotu działa w górę",
|
||||||
"gunFrontalDepression": "Frontal depression",
|
"gunFrontalDepression": "Przedni limit obrotu działa w dół",
|
||||||
"gunFrontalElevation": "Frontal elevation",
|
"gunFrontalElevation": "Przedni limit obrotu działa w górę",
|
||||||
"gunRearDepression": "Rear depression",
|
"gunRearDepression": "Tylne opuszczanie działa",
|
||||||
"gunRearElevation": "Rear elevation",
|
"gunRearElevation": "Rear elevation",
|
||||||
"azimuthLeft": "Azimuth left",
|
"azimuthLeft": "Azimuth left",
|
||||||
"azimuthRight": "Azimuth right",
|
"azimuthRight": "Azimuth right",
|
||||||
@@ -626,7 +626,7 @@
|
|||||||
"trackRepairTime": "Track repair time",
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "View range",
|
"viewRange": "View range",
|
||||||
"camouflageStill": "Still",
|
"camouflageStill": "Still",
|
||||||
"camouflageMoving": "Moving",
|
"camouflageMoving": "Podczas ruchu",
|
||||||
"camouflageShootingStill": "Shooting while still",
|
"camouflageShootingStill": "Shooting while still",
|
||||||
"camouflageShootingMoving": "Shooting while moving",
|
"camouflageShootingMoving": "Shooting while moving",
|
||||||
"camouflageCaughtOnFire": "Caught on fire",
|
"camouflageCaughtOnFire": "Caught on fire",
|
||||||
@@ -648,7 +648,7 @@
|
|||||||
"crew": {
|
"crew": {
|
||||||
"title": "Crew training",
|
"title": "Crew training",
|
||||||
"info": "represents the other roles a crew member can take.",
|
"info": "represents the other roles a crew member can take.",
|
||||||
"count": "Crew count",
|
"count": "Liczba członków załogi",
|
||||||
"commander": "Commander",
|
"commander": "Commander",
|
||||||
"gunner": "Gunner",
|
"gunner": "Gunner",
|
||||||
"driver": "Driver",
|
"driver": "Driver",
|
||||||
@@ -663,7 +663,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
"name": "Compare",
|
"name": "Porównaj",
|
||||||
"button": "Compare tanks",
|
"button": "Compare tanks",
|
||||||
"description": "Compare tank statistics",
|
"description": "Compare tank statistics",
|
||||||
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
|
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
|
||||||
@@ -673,7 +673,7 @@
|
|||||||
"title": "Add tanks",
|
"title": "Add tanks",
|
||||||
"description": "Select tanks to add to comparison"
|
"description": "Select tanks to add to comparison"
|
||||||
},
|
},
|
||||||
"clear": "Clear",
|
"clear": "Wyczyść",
|
||||||
"deltas": {
|
"deltas": {
|
||||||
"none": "No deltas",
|
"none": "No deltas",
|
||||||
"percentage": "Percentage",
|
"percentage": "Percentage",
|
||||||
@@ -686,17 +686,17 @@
|
|||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"clear": "Clear",
|
"clear": "Wyczyść",
|
||||||
"apply_to_all": "Apply to all"
|
"apply_to_all": "Apply to all"
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
"clear": "Clear",
|
"clear": "Wyczyść",
|
||||||
"maximize": "Maximize",
|
"maximize": "Maximize",
|
||||||
"equalize": "Equalize"
|
"equalize": "Equalize"
|
||||||
},
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"stock": "Stock",
|
"stock": "Standardowy",
|
||||||
"upgrade": "Upgrade"
|
"upgrade": "Ulepsz"
|
||||||
},
|
},
|
||||||
"provisions": {
|
"provisions": {
|
||||||
"clear": "Clear this tank",
|
"clear": "Clear this tank",
|
||||||
@@ -761,14 +761,14 @@
|
|||||||
"seo_description": "Track your stats in real time as you play World of Tanks Blitz",
|
"seo_description": "Track your stats in real time as you play World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
"lookup": "Look up a player to get started",
|
"lookup": "Look up a player to get started",
|
||||||
"hint": "Search players...",
|
"hint": "Szukaj graczy...",
|
||||||
"searching": "Searching...",
|
"searching": "Wyszukiwanie...",
|
||||||
"no_results": "No players found"
|
"no_results": "No players found"
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
"title": "Tracking {name}",
|
"title": "Tracking {name}",
|
||||||
"subtitle": "Since {time}",
|
"subtitle": "Since {time}",
|
||||||
"reset": "Reset",
|
"reset": "Zresetuj",
|
||||||
"change": "Change"
|
"change": "Change"
|
||||||
},
|
},
|
||||||
"no_cc": {
|
"no_cc": {
|
||||||
@@ -792,7 +792,7 @@
|
|||||||
"description": "Rank Blitz tanks",
|
"description": "Rank Blitz tanks",
|
||||||
"seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz",
|
"seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"reset": "Reset",
|
"reset": "Zresetuj",
|
||||||
"share": "Share"
|
"share": "Share"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -828,7 +828,7 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"title": "Copy URL",
|
"title": "Copy URL",
|
||||||
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
|
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
|
||||||
"back": "Back",
|
"back": "Powrót",
|
||||||
"copy": "Copy URL"
|
"copy": "Copy URL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -839,7 +839,7 @@
|
|||||||
},
|
},
|
||||||
"customize": {
|
"customize": {
|
||||||
"title": "Customize",
|
"title": "Customize",
|
||||||
"reset": "Reset",
|
"reset": "Zresetuj",
|
||||||
"description": "Bells and whistles of your embed"
|
"description": "Bells and whistles of your embed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,10 +38,10 @@
|
|||||||
},
|
},
|
||||||
"branches": {
|
"branches": {
|
||||||
"beta": "Бета",
|
"beta": "Бета",
|
||||||
"development": "Development",
|
"development": "Разработка",
|
||||||
"opentest": "Открытый тест",
|
"opentest": "Открытый тест",
|
||||||
"preview": "Preview",
|
"preview": "Предпросмотр",
|
||||||
"production": "Production"
|
"production": "Производство"
|
||||||
},
|
},
|
||||||
"locales": {
|
"locales": {
|
||||||
"en": "Английский",
|
"en": "Английский",
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
"title": "Настройки",
|
"title": "Настройки",
|
||||||
"description": "Изменить настройки BlitzKit",
|
"description": "Изменить настройки BlitzKit",
|
||||||
"dev_mode": "Режим разработчика",
|
"dev_mode": "Режим разработчика",
|
||||||
"auto_load_models": "Auto-load models",
|
"auto_load_models": "Автозагрузка моделей",
|
||||||
"language": "Язык"
|
"language": "Язык"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"load_model": "Load model",
|
"load_model": "Загрузить модель",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "За холмом",
|
"hull_down": "За холмом",
|
||||||
"face_hug": "В упор",
|
"face_hug": "В упор",
|
||||||
@@ -574,7 +574,7 @@
|
|||||||
"dpm": "ДПМ",
|
"dpm": "ДПМ",
|
||||||
"dpmEffective": "Эффективность за 60с",
|
"dpmEffective": "Эффективность за 60с",
|
||||||
"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": "Самый эффективный снаряд",
|
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
||||||
"shellReloads": "Перезарядка",
|
"shellReloads": "Перезарядка",
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
"discover-ids": "bun src/discoverIds/index.ts",
|
"discover-ids": "bun src/discoverIds/index.ts",
|
||||||
"build-protos": "bun src/protos.ts",
|
"build-protos": "bun src/protos.ts",
|
||||||
"record-performance": "bun src/recordPerformance/index.ts",
|
"record-performance": "bun src/recordPerformance/index.ts",
|
||||||
"download-client": "bun src/downloadClient/index.ts",
|
"download-client": "bun src/downloadClient/index.ts"
|
||||||
"vfs": "bun src/vfs/index.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blitzkit/closed": "workspace:*",
|
"@blitzkit/closed": "workspace:*",
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { assertSecret } from "@blitzkit/core";
|
|
||||||
import { LocalVFS } from "@blitzkit/core/src/blitzkit/vfs/local";
|
|
||||||
|
|
||||||
export const vfs = new LocalVFS(assertSecret(import.meta.env.CLIENT_DIR));
|
|
||||||
|
|
||||||
const command = Bun.argv[2];
|
|
||||||
const path = Bun.argv[3];
|
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
console.error("Gimme a path to assess dumb ass");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === "cat") {
|
|
||||||
const content = await vfs.file(path);
|
|
||||||
await Bun.write(Bun.stdout, content);
|
|
||||||
} else if (command === "ls") {
|
|
||||||
const files = await vfs.dir(path);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
console.log(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,89 +0,0 @@
|
|||||||
import { Code, Flex, Heading, Link, Text } from "@radix-ui/themes";
|
|
||||||
import { useEffect, useState, type ReactNode } from "react";
|
|
||||||
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
|
|
||||||
import { InlineSkeleton } from "./InlineSkeleton";
|
|
||||||
|
|
||||||
const TIME = 1792598400 * 1000;
|
|
||||||
|
|
||||||
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
|
||||||
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setTimeLeft(TIME - Date.now());
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let string: ReactNode;
|
|
||||||
|
|
||||||
if (timeLeft > 0) {
|
|
||||||
const hours = Math.floor(timeLeft / (1000 * 60 * 60));
|
|
||||||
const minutes = Math.floor((timeLeft / (1000 * 60)) % 60)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, "0");
|
|
||||||
const seconds = Math.floor((timeLeft / 1000) % 60)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, "0");
|
|
||||||
|
|
||||||
string = (
|
|
||||||
<>
|
|
||||||
revealed in{" "}
|
|
||||||
<Code highContrast>
|
|
||||||
{hours}h {minutes}m {seconds}s
|
|
||||||
</Code>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
string = "have been revealed.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex justify="center" px="4">
|
|
||||||
<Link
|
|
||||||
color="gray"
|
|
||||||
highContrast
|
|
||||||
style={{ display: "content" }}
|
|
||||||
href="https://preview.blitzkit.app/tanks"
|
|
||||||
underline="none"
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
mt="3"
|
|
||||||
px="5"
|
|
||||||
py="4"
|
|
||||||
style={{
|
|
||||||
borderRadius: "var(--radius-3)",
|
|
||||||
backgroundImage: "url(https://i.imgur.com/xgngxoy.png)",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
boxShadow: "0 0 var(--space-7) var(--white-a2)",
|
|
||||||
}}
|
|
||||||
align="center"
|
|
||||||
gap="5"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="https://i.imgur.com/kXV3r3P.png"
|
|
||||||
style={{
|
|
||||||
height: "min(15vw, 5rem)",
|
|
||||||
filter: "drop-shadow(0.5rem 0.5rem 1rem #00000080)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Flex direction="column">
|
|
||||||
<Heading size={{ initial: "4", sm: "6" }}>
|
|
||||||
Tier XIs {!skeleton && string}{" "}
|
|
||||||
{skeleton && <InlineSkeleton width="10rem" />}
|
|
||||||
</Heading>
|
|
||||||
<Text size={{ initial: "2", sm: "3" }}>
|
|
||||||
Get ready to go to the Tankopedia!
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Link>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
---
|
|
||||||
import { Grid, Text } from "@radix-ui/themes";
|
|
||||||
import { credits } from "../config/credits";
|
|
||||||
import { getStrings } from "../core/i18n/getStrings";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
locale: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { locale } = Astro.props;
|
|
||||||
|
|
||||||
const strings = getStrings(locale);
|
|
||||||
---
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.credits {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
padding: var(--space-4);
|
|
||||||
|
|
||||||
.inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-2);
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
max-width: 80rem;
|
|
||||||
|
|
||||||
.list {
|
|
||||||
gap: var(--space-5);
|
|
||||||
|
|
||||||
.link {
|
|
||||||
display: contents;
|
|
||||||
|
|
||||||
.person {
|
|
||||||
--profile-size: 5rem;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
padding: var(--space-4);
|
|
||||||
padding-top: 0;
|
|
||||||
margin-top: calc(var(--profile-size) / 2);
|
|
||||||
gap: var(--space-1);
|
|
||||||
|
|
||||||
box-shadow: var(--shadow-3);
|
|
||||||
border-radius: var(--radius-3);
|
|
||||||
|
|
||||||
.contain {
|
|
||||||
border-radius: inherit;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
|
|
||||||
filter: saturate(20) brightness(0.125) blur(4rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
margin-top: calc(var(--profile-size) / -2);
|
|
||||||
|
|
||||||
object-fit: cover;
|
|
||||||
width: var(--profile-size);
|
|
||||||
height: var(--profile-size);
|
|
||||||
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="credits">
|
|
||||||
<div class="inner">
|
|
||||||
<Grid
|
|
||||||
className="list"
|
|
||||||
py="4"
|
|
||||||
columns="repeat(auto-fill, minmax(16rem, 1fr))"
|
|
||||||
flow="dense"
|
|
||||||
gap="4"
|
|
||||||
gapY="8"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
credits.map((person) => (
|
|
||||||
<a target="_blank" href={person.url} class="link">
|
|
||||||
<div class="person">
|
|
||||||
<div class="contain">
|
|
||||||
<div
|
|
||||||
class="hint"
|
|
||||||
style={`background-image: url(${person.image})`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img src={person.image} />
|
|
||||||
|
|
||||||
<Text weight="medium" className="text" color="gray" highContrast>
|
|
||||||
{person.name}
|
|
||||||
</Text>
|
|
||||||
<Text className="text" color="gray" wrap="pretty">
|
|
||||||
{person.description}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -84,6 +84,9 @@ const strings = getStrings(locale);
|
|||||||
sm: "start",
|
sm: "start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<LinkI18n {locale} href="/docs/about/credits" color="gray">
|
||||||
|
{strings.website.footer.about.credits}
|
||||||
|
</LinkI18n>
|
||||||
<Link
|
<Link
|
||||||
href="https://discord.gg/nDt7AjGJQH"
|
href="https://discord.gg/nDt7AjGJQH"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Flex, IconButton, type FlexProps } from "@radix-ui/themes";
|
|
||||||
import type { ComponentProps, CSSProperties } from "react";
|
|
||||||
|
|
||||||
type SidebarIconButtonProps = ComponentProps<typeof IconButton> & {
|
|
||||||
selected: boolean;
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
imageStyle?: CSSProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SidebarIconGroup({ style, ...props }: FlexProps) {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
overflow="hidden"
|
|
||||||
style={{ borderRadius: "var(--radius-full)", ...style }}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SidebarIconButton({
|
|
||||||
selected,
|
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
imageStyle,
|
|
||||||
...props
|
|
||||||
}: SidebarIconButtonProps) {
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
color={selected ? undefined : "gray"}
|
|
||||||
variant="soft"
|
|
||||||
size={{ initial: "2", sm: "3" }}
|
|
||||||
radius="none"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt={alt}
|
|
||||||
src={src}
|
|
||||||
style={{ width: "50%", height: "50%", ...imageStyle }}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
|
||||||
import { Flex, Tooltip } from "@radix-ui/themes";
|
|
||||||
import { uniqBy } from "lodash-es";
|
|
||||||
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
|
||||||
import { api } from "../../../../../../core/blitzkit/api";
|
|
||||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
|
||||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
|
||||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
|
||||||
import { SidebarIconButton } from "./SidebarIconButton";
|
|
||||||
|
|
||||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
|
||||||
|
|
||||||
export function SkinSwitcher() {
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const skin = Tankopedia.use((state) => state.requestedSkin);
|
|
||||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
|
||||||
const { unwrap } = useLocale();
|
|
||||||
const skins = uniqBy(
|
|
||||||
Object.values(camouflageDefinitions.camouflages).filter(
|
|
||||||
(camouflage) => camouflage.tank_id === tank.id,
|
|
||||||
),
|
|
||||||
(camouflage) => unwrap(camouflage.name!),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (skins.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
top="50%"
|
|
||||||
right={
|
|
||||||
disturbed && requestedDisplay === TankopediaDisplay.Model
|
|
||||||
? "3"
|
|
||||||
: "-4rem"
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
transform: "translateY(-50%)",
|
|
||||||
transitionDuration: "200ms",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{skins.map((camouflage, index) => {
|
|
||||||
const name = unwrap(camouflage.name!).replace(
|
|
||||||
"%(camo_num)",
|
|
||||||
SKIN_NUMBER,
|
|
||||||
);
|
|
||||||
const top = index === 0 ? "var(--radius-full)" : "0";
|
|
||||||
const bottom = index === skins.length - 1 ? "var(--radius-full)" : "0";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip key={camouflage.id} content={name} side="left">
|
|
||||||
<SidebarIconButton
|
|
||||||
selected={skin === camouflage.id}
|
|
||||||
alt={name}
|
|
||||||
src={alias("api", `/icons/camouflages/${camouflage.id}.webp`)}
|
|
||||||
size={{ initial: "3", sm: "4" }}
|
|
||||||
radius="full"
|
|
||||||
style={{
|
|
||||||
borderRadius: `${top} ${top} ${bottom} ${bottom}`,
|
|
||||||
}}
|
|
||||||
imageStyle={{
|
|
||||||
width: "70%",
|
|
||||||
height: "70%",
|
|
||||||
borderRadius: "50%",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.requestedSkin =
|
|
||||||
draft.requestedSkin === camouflage.id
|
|
||||||
? undefined
|
|
||||||
: camouflage.id;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -46,11 +46,6 @@ import { TankSearch } from "../../../../TankSearch";
|
|||||||
import { CustomShellButton } from "./components/CustomShellButton";
|
import { CustomShellButton } from "./components/CustomShellButton";
|
||||||
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||||
import { QuickInputs } from "./components/QuickInputs";
|
import { QuickInputs } from "./components/QuickInputs";
|
||||||
import {
|
|
||||||
SidebarIconButton,
|
|
||||||
SidebarIconGroup,
|
|
||||||
} from "./components/SidebarIconButton";
|
|
||||||
import { SkinSwitcher } from "./components/SkinSwitcher";
|
|
||||||
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
|
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
|
||||||
import { Thicknesses } from "./components/Thicknesses";
|
import { Thicknesses } from "./components/Thicknesses";
|
||||||
|
|
||||||
@@ -92,8 +87,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
|
|
||||||
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
||||||
|
|
||||||
<SkinSwitcher />
|
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="2"
|
gap="2"
|
||||||
direction="column"
|
direction="column"
|
||||||
@@ -188,13 +181,24 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SidebarIconGroup>
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
style={{
|
||||||
|
borderRadius: "var(--radius-full)",
|
||||||
|
}}
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
{antagonistGun.shells.map((thisShell) => (
|
{antagonistGun.shells.map((thisShell) => (
|
||||||
<SidebarIconButton
|
<IconButton
|
||||||
|
color={
|
||||||
|
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||||
|
? undefined
|
||||||
|
: "gray"
|
||||||
|
}
|
||||||
|
variant="soft"
|
||||||
key={thisShell.id}
|
key={thisShell.id}
|
||||||
selected={thisShell.id === antagonistShell.id && !hasCustomShell}
|
size={{ initial: "2", sm: "3" }}
|
||||||
alt={unwrap(thisShell.name!)}
|
radius="none"
|
||||||
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Duel.mutate((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.antagonist.shell = thisShell;
|
draft.antagonist.shell = thisShell;
|
||||||
@@ -204,10 +208,19 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
draft.customShell = undefined;
|
draft.customShell = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<img
|
||||||
|
alt={unwrap(thisShell.name!)}
|
||||||
|
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
|
||||||
|
style={{
|
||||||
|
width: "50%",
|
||||||
|
height: "50%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
))}
|
))}
|
||||||
<CustomShellButton />
|
<CustomShellButton />
|
||||||
</SidebarIconGroup>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
|
|||||||
@@ -1,46 +1,30 @@
|
|||||||
import { UpdateIcon } from "@radix-ui/react-icons";
|
import { UpdateIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Flex, Heading } from "@radix-ui/themes";
|
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
|
||||||
import { api } from "../../../../../../core/blitzkit/api";
|
|
||||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { Tracker } from "./components/Tracker";
|
import { Tracker } from "./components/Tracker";
|
||||||
|
|
||||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
|
||||||
|
|
||||||
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
const hasRevealed = useRef(false);
|
const name = unwrap(protagonist.name!);
|
||||||
const skin = Tankopedia.use((state) => state.requestedSkin);
|
|
||||||
const camouflage =
|
|
||||||
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
|
||||||
const tankName = unwrap(protagonist.name!);
|
|
||||||
const name = camouflage
|
|
||||||
? `${tankName} ${unwrap(camouflage.name!).replace("%(camo_num)", SKIN_NUMBER)}`
|
|
||||||
: tankName;
|
|
||||||
const fontSize = revealed
|
const fontSize = revealed
|
||||||
? disturbed
|
? disturbed
|
||||||
? "1.5rem"
|
? "1.5rem"
|
||||||
: "2rem"
|
: "2rem"
|
||||||
: `min(12vh, ${75 / name.length}vw)`;
|
: `min(12vh, ${75 / name.length}vw)`;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (revealed) hasRevealed.current = true;
|
|
||||||
}, [revealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!modelRequested || hasRevealed.current) return;
|
if (!modelRequested) return;
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent
|
|||||||
|
|
||||||
const ANGLE = degToRad(15);
|
const ANGLE = degToRad(15);
|
||||||
const REVEAL_ANIMATION_TIME = 3;
|
const REVEAL_ANIMATION_TIME = 3;
|
||||||
export const TRANSITION_ANIMATION_TIME = 0.5;
|
const TRANSITION_ANIMATION_TIME = 0.5;
|
||||||
|
|
||||||
const LIGHTS_COUNT = 3;
|
const LIGHTS_COUNT = 3;
|
||||||
const THETA_OFFSET = degToRad(180 - 45);
|
const THETA_OFFSET = degToRad(180 - 45);
|
||||||
@@ -46,17 +46,11 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
|
|
||||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
|
||||||
const skinPending = Tankopedia.use(
|
|
||||||
(state) => state.loadedSkin !== state.skin,
|
|
||||||
);
|
|
||||||
|
|
||||||
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||||
const t0 = useRef(performance.now() / 1e3 - animationTime.current);
|
const t0 = useRef(performance.now() / 1e3 - animationTime.current);
|
||||||
const holding = useRef(false);
|
|
||||||
|
|
||||||
const isRevealing = useRef(true);
|
const isRevealing = useRef(true);
|
||||||
const isFirstReveal = useRef(true);
|
|
||||||
|
|
||||||
const [animate, setAnimate] = useState(true);
|
const [animate, setAnimate] = useState(true);
|
||||||
|
|
||||||
@@ -77,37 +71,6 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
setAnimate(true);
|
setAnimate(true);
|
||||||
}, [requestedDisplay]);
|
}, [requestedDisplay]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFirstReveal.current) {
|
|
||||||
isFirstReveal.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!revealed) {
|
|
||||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
|
||||||
t0.current = performance.now() / 1e3;
|
|
||||||
holding.current = true;
|
|
||||||
} else if (holding.current) {
|
|
||||||
animationTime.current = REVEAL_ANIMATION_TIME;
|
|
||||||
t0.current = performance.now() / 1e3 - animationTime.current;
|
|
||||||
holding.current = false;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAnimate(true);
|
|
||||||
invalidate();
|
|
||||||
}, [revealed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!skinPending) return;
|
|
||||||
|
|
||||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
|
||||||
t0.current = performance.now() / 1e3;
|
|
||||||
|
|
||||||
setAnimate(true);
|
|
||||||
}, [skinPending]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContactShadows
|
<ContactShadows
|
||||||
@@ -127,7 +90,6 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
hasPbr={hasPbr}
|
hasPbr={hasPbr}
|
||||||
t0={t0}
|
t0={t0}
|
||||||
animationTime={animationTime}
|
animationTime={animationTime}
|
||||||
holding={holding}
|
|
||||||
wrapper={wrapper}
|
wrapper={wrapper}
|
||||||
stop={() => {
|
stop={() => {
|
||||||
setAnimate(false);
|
setAnimate(false);
|
||||||
@@ -183,19 +145,11 @@ interface AnimatorProps {
|
|||||||
|
|
||||||
t0: RefObject<number>;
|
t0: RefObject<number>;
|
||||||
animationTime: RefObject<number>;
|
animationTime: RefObject<number>;
|
||||||
holding: RefObject<boolean>;
|
|
||||||
wrapper: RefObject<Group | null>;
|
wrapper: RefObject<Group | null>;
|
||||||
hasPbr: boolean;
|
hasPbr: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Animator({
|
function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
|
||||||
stop,
|
|
||||||
t0,
|
|
||||||
animationTime,
|
|
||||||
holding,
|
|
||||||
wrapper,
|
|
||||||
hasPbr,
|
|
||||||
}: AnimatorProps) {
|
|
||||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||||
const display = Tankopedia.use((state) => state.display);
|
const display = Tankopedia.use((state) => state.display);
|
||||||
|
|
||||||
@@ -222,7 +176,7 @@ function Animator({
|
|||||||
|
|
||||||
useFrame(({ invalidate }) => {
|
useFrame(({ invalidate }) => {
|
||||||
const dt = performance.now() / 1e3 - t0.current;
|
const dt = performance.now() / 1e3 - t0.current;
|
||||||
const x = clamp(dt / animationTime.current, 0, holding.current ? 1 : 2);
|
const x = clamp(dt / animationTime.current, 0, 2);
|
||||||
|
|
||||||
if (x === 2) {
|
if (x === 2) {
|
||||||
apply(1);
|
apply(1);
|
||||||
@@ -234,9 +188,6 @@ function Animator({
|
|||||||
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
|
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
|
||||||
|
|
||||||
apply(t);
|
apply(t);
|
||||||
|
|
||||||
if (holding.current && x === 1) return;
|
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
|
|
||||||
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
|
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
|
||||||
@@ -244,12 +195,6 @@ 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;
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { 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";
|
||||||
|
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||||
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
|
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
|
||||||
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 { 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";
|
||||||
@@ -29,7 +28,6 @@ export function TankModel() {
|
|||||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
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);
|
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
|
|
||||||
@@ -94,7 +92,7 @@ export function TankModel() {
|
|||||||
window.removeEventListener("pointerup", handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return skinTree(node, skinGltf, {
|
return jsxTree(node, {
|
||||||
mesh(_, props, key) {
|
mesh(_, props, key) {
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -171,7 +169,7 @@ export function TankModel() {
|
|||||||
window.removeEventListener("pointerup", handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return skinTree(node, skinGltf, {
|
return jsxTree(node, {
|
||||||
mesh(_, props, key) {
|
mesh(_, props, key) {
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -252,7 +250,7 @@ export function TankModel() {
|
|||||||
window.removeEventListener("pointerup", handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return skinTree(node, skinGltf, {
|
return jsxTree(node, {
|
||||||
mesh(_, props, key) {
|
mesh(_, props, key) {
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
interface CreditPeople {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
url?: string;
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const credits: CreditPeople[] = [
|
|
||||||
{
|
|
||||||
name: "TrèsAbhi",
|
|
||||||
description: "Founder and lead developer",
|
|
||||||
url: "https://abhi-deep.com/",
|
|
||||||
image: "https://github.com/tresabhi.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "HitJack",
|
|
||||||
description: "Developer on website",
|
|
||||||
url: "https://marksstuff.com",
|
|
||||||
image: "https://github.com/Hitjack007.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "aeson000",
|
|
||||||
description: "Advisor on game and website",
|
|
||||||
url: "https://discord.gg/WHdER7ZPAD",
|
|
||||||
image: "/media/users/aeson000.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "GonnaHetzMe",
|
|
||||||
description: "Developer and advisor on back end",
|
|
||||||
url: "https://github.com/karelpak1",
|
|
||||||
image: "https://github.com/karelpak1.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ChickenMan7777",
|
|
||||||
description: "Moderator and former advisor on game mechanics",
|
|
||||||
url: "https://discord.gg/rEsUbXCqD9",
|
|
||||||
image: "/media/users/chickenman7777.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Avenir",
|
|
||||||
description: "Moderator and elite Advanced Recon Commando",
|
|
||||||
image: "/media/users/avenir.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pyogenics",
|
|
||||||
description: "Former advisor on back end",
|
|
||||||
url: "https://github.com/Pyogenics",
|
|
||||||
image: "https://github.com/Pyogenics.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Prince_NA",
|
|
||||||
description: "Former advisor on bot and website",
|
|
||||||
image: "/media/users/prince-na.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vovko",
|
|
||||||
description: "Former advisor on bot",
|
|
||||||
url: "https://amth.one/",
|
|
||||||
image: "https://github.com/cufee.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "S0AP",
|
|
||||||
description: "Former advisor and advertiser on bot",
|
|
||||||
url: "https://discord.gg/3KfH93um3Y",
|
|
||||||
image: "/media/users/s0ap.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Synx",
|
|
||||||
description: "Former advisor on bot and website",
|
|
||||||
url: "https://www.youtube.com/@Synx_Blitz",
|
|
||||||
image: "/media/users/synx.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "34wotb",
|
|
||||||
description: "Former advisor on bot",
|
|
||||||
url: "https://www.twitch.tv/34wotb/",
|
|
||||||
image: "/media/users/34wotb.jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "HisRoyalFatness",
|
|
||||||
description: "Former advisor on website",
|
|
||||||
url: "https://www.youtube.com/@hisroyalfatness",
|
|
||||||
image: "/media/users/hisroyalfatness.jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Minitelrose",
|
|
||||||
description: "Former advisor on game mechanics",
|
|
||||||
url: "https://wotinspector.com/",
|
|
||||||
image: "/media/users/minitelrose.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Maddox",
|
|
||||||
description: "Game asset decompression",
|
|
||||||
url: "https://github.com/Maddoxkkm",
|
|
||||||
image: "https://github.com/Maddoxkkm.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TJ Duff",
|
|
||||||
description: "Chief executive cheer leader",
|
|
||||||
image: "/media/users/tj-duff.jpg",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const SKIN_NUMBER = "0417";
|
|
||||||
@@ -74,8 +74,4 @@ export const tools: Record<string, Tool> = {
|
|||||||
href: "https://discord.gg/nDt7AjGJQH",
|
href: "https://discord.gg/nDt7AjGJQH",
|
||||||
button: "plum",
|
button: "plum",
|
||||||
},
|
},
|
||||||
credits: {
|
|
||||||
id: "credits",
|
|
||||||
button: "bronze",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import type { Material, Object3D } from "three";
|
|
||||||
import type { Skin } from "../../hooks/useSkin";
|
|
||||||
import { jsxTree } from "./jsxTree";
|
|
||||||
|
|
||||||
type Mergers = Parameters<typeof jsxTree>[1];
|
|
||||||
|
|
||||||
export function skinTree(
|
|
||||||
node: Object3D,
|
|
||||||
skin: Skin | undefined,
|
|
||||||
mergers: Mergers,
|
|
||||||
) {
|
|
||||||
const skinNodes = new Map(
|
|
||||||
skin?.scene.children.map((node) => [node.name, node]),
|
|
||||||
);
|
|
||||||
const skinMergers: Mergers = {
|
|
||||||
mesh(mesh, props, key) {
|
|
||||||
const material = skin?.materials.get((mesh.material as Material).name);
|
|
||||||
|
|
||||||
return mergers!.mesh!(
|
|
||||||
mesh,
|
|
||||||
material ? { ...props, material } : 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 (
|
|
||||||
<group key={key} {...props}>
|
|
||||||
{isSwitch
|
|
||||||
? object3d.children
|
|
||||||
.filter((child) => child.name === state)
|
|
||||||
.map((child) => jsxTree(child, skinMergers))
|
|
||||||
: props.children}
|
|
||||||
{skinNode?.children.map((child) => jsxTree(child, mergers))}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return jsxTree(node, skinMergers);
|
|
||||||
}
|
|
||||||
@@ -7,23 +7,12 @@ import {
|
|||||||
import type { DuelMember } from "../../stores/duel";
|
import type { DuelMember } from "../../stores/duel";
|
||||||
import { genericDefaultEquipmentMatrix } from "../../stores/duel/constants";
|
import { genericDefaultEquipmentMatrix } from "../../stores/duel/constants";
|
||||||
|
|
||||||
export function defaultTankWeapon(tank: TankDefinition) {
|
|
||||||
for (const turret of [...tank.turrets].reverse()) {
|
|
||||||
for (const gun of [...turret.guns].reverse()) {
|
|
||||||
const shell = gun.shells[0];
|
|
||||||
|
|
||||||
if (shell) return { turret, gun, shell };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Tank ${tank.id} has no gun with shells`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tankToDuelMember(
|
export function tankToDuelMember(
|
||||||
tank: TankDefinition,
|
tank: TankDefinition,
|
||||||
provisionDefinitions: ProvisionDefinitions,
|
provisionDefinitions: ProvisionDefinitions,
|
||||||
) {
|
) {
|
||||||
const { turret, gun, shell } = defaultTankWeapon(tank);
|
const turret = tank.turrets.at(-1)!;
|
||||||
|
const gun = turret.guns.at(-1)!;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
camouflage: true,
|
camouflage: true,
|
||||||
@@ -33,7 +22,7 @@ export function tankToDuelMember(
|
|||||||
provisions: createDefaultProvisions(tank, gun, provisionDefinitions),
|
provisions: createDefaultProvisions(tank, gun, provisionDefinitions),
|
||||||
engine: tank.engines.at(-1)!,
|
engine: tank.engines.at(-1)!,
|
||||||
gun,
|
gun,
|
||||||
shell,
|
shell: tank.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||||
tank,
|
tank,
|
||||||
track: tank.tracks.at(-1)!,
|
track: tank.tracks.at(-1)!,
|
||||||
turret,
|
turret,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { GLTFLoader } from "three-stdlib";
|
|||||||
import { useDispose } from "./useDispose";
|
import { useDispose } from "./useDispose";
|
||||||
|
|
||||||
export function useArmor(id: number) {
|
export function useArmor(id: number) {
|
||||||
const path = alias("models_tanks", `/${id}/collision.glb`);
|
const path = alias("api", `/tanks/${id}/collision.glb`);
|
||||||
const gltf = useLoader(GLTFLoader, path);
|
const gltf = useLoader(GLTFLoader, path);
|
||||||
|
|
||||||
useDispose(gltf, path);
|
useDispose(gltf, path);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GLTFLoader } from "three-stdlib";
|
|||||||
import { useDispose } from "./useDispose";
|
import { useDispose } from "./useDispose";
|
||||||
|
|
||||||
export function useModel(id: number) {
|
export function useModel(id: number) {
|
||||||
const path = alias("models_tanks", `/${id}/model.glb`);
|
const path = alias("api", `/tanks/${id}/model.glb`);
|
||||||
const gltf = useLoader(GLTFLoader, path);
|
const gltf = useLoader(GLTFLoader, path);
|
||||||
|
|
||||||
useDispose(gltf, path);
|
useDispose(gltf, path);
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import type { Group, Material } from "three";
|
|
||||||
import { GLTFLoader } from "three-stdlib";
|
|
||||||
import { TRANSITION_ANIMATION_TIME } from "../components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
|
||||||
import { Tankopedia } from "../stores/tankopedia";
|
|
||||||
|
|
||||||
export interface Skin {
|
|
||||||
scene: Group;
|
|
||||||
materials: Map<string, Material>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const skins = new Map<string, Promise<Skin>>();
|
|
||||||
const loadedSkins = new Map<string, Skin>();
|
|
||||||
|
|
||||||
async function loadSkin(path: string) {
|
|
||||||
const gltf = await new GLTFLoader().loadAsync(path);
|
|
||||||
const materials: Material[] = await gltf.parser.getDependencies("material");
|
|
||||||
const skin = {
|
|
||||||
scene: gltf.scene,
|
|
||||||
materials: new Map(materials.map((material) => [material.name, material])),
|
|
||||||
};
|
|
||||||
|
|
||||||
loadedSkins.set(path, skin);
|
|
||||||
|
|
||||||
return skin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function skinPath(id: number, skin: number) {
|
|
||||||
return alias("models_legendary", `/${id}/skins/${skin}.glb`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSkin(id: number) {
|
|
||||||
const requestedSkin = Tankopedia.use((state) => state.requestedSkin);
|
|
||||||
const skin = Tankopedia.use((state) => state.skin);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const path =
|
|
||||||
requestedSkin === undefined ? undefined : skinPath(id, requestedSkin);
|
|
||||||
let settled = false;
|
|
||||||
|
|
||||||
if (path === undefined || loadedSkins.has(path)) {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.loadedSkin = requestedSkin;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.revealed = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!skins.has(path)) skins.set(path, loadSkin(path));
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
skins.get(path),
|
|
||||||
new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, TRANSITION_ANIMATION_TIME * 1e3),
|
|
||||||
),
|
|
||||||
]).then(() => {
|
|
||||||
if (settled) return;
|
|
||||||
|
|
||||||
settled = true;
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.loadedSkin = requestedSkin;
|
|
||||||
draft.skin = requestedSkin;
|
|
||||||
draft.revealed = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (settled) return;
|
|
||||||
|
|
||||||
settled = true;
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.revealed = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [id, requestedSkin]);
|
|
||||||
|
|
||||||
return skin === undefined ? undefined : loadedSkins.get(skinPath(id, skin));
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import type { CamouflagesXml } from "@blitzkit/core";
|
|
||||||
import type { APIContext, GetStaticPathsItem } from "astro";
|
|
||||||
import sharp from "sharp";
|
|
||||||
import { extractPackedIcon } from "../../../../core/blitz/extractPackedIcon";
|
|
||||||
import { parsePackedSpriteRect } from "../../../../core/blitz/parsePackedSpriteRect";
|
|
||||||
import { api } from "../../../../core/blitzkit/api";
|
|
||||||
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
|
||||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
|
||||||
import { getStaticPaths as _getStaticPaths } from "../../_index";
|
|
||||||
|
|
||||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
|
||||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
|
||||||
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
|
|
||||||
"Data/XML/item_defs/vehicles/common/camouflages.xml",
|
|
||||||
);
|
|
||||||
const icons = new Map(
|
|
||||||
Object.values(camouflagesXml.root.camouflages).map((camouflage) => [
|
|
||||||
camouflage.id,
|
|
||||||
camouflage.icon,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
const paths: GetStaticPathsItem[] = [];
|
|
||||||
|
|
||||||
for (const camouflage of Object.values(camouflageDefinitions.camouflages)) {
|
|
||||||
const icon = icons.get(camouflage.id);
|
|
||||||
|
|
||||||
if (camouflage.tank_id === undefined || !icon) continue;
|
|
||||||
|
|
||||||
paths.push({
|
|
||||||
params: { id: camouflage.id },
|
|
||||||
props: {
|
|
||||||
path: icon.replace("~res:/", "Data/").replace(/\.txt$/, ""),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths;
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function GET({ props }: APIContext<{ path: string }>) {
|
|
||||||
const path = (await vfs.resolve(`${props.path}@2x.packed.webp`))
|
|
||||||
? `${props.path}@2x.packed.webp`
|
|
||||||
: `${props.path}.packed.webp`;
|
|
||||||
const packed = await vfs.file(path);
|
|
||||||
const texture = sharp(packed);
|
|
||||||
const sizes = parsePackedSpriteRect(packed);
|
|
||||||
const image = sizes
|
|
||||||
? sharp(await extractPackedIcon(texture, sizes))
|
|
||||||
: texture;
|
|
||||||
const bytes = await image.webp().toBuffer();
|
|
||||||
|
|
||||||
return new Response(new Uint8Array(bytes));
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
import { Accessor, Document, Node, NodeIO, Scene } from "@gltf-transform/core";
|
import { Accessor, Document, Node, NodeIO, Scene } from "@gltf-transform/core";
|
||||||
import type { APIContext } from "astro";
|
import type { APIContext } from "astro";
|
||||||
import { vfs } from "../../../core/blitzkit/vfs";
|
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||||
|
|
||||||
export { getStaticPaths } from "./_index";
|
export { getStaticPaths } from "./_index";
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@ import { Document, Material, Node, NodeIO, Scene } 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 { times } from "lodash-es";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { vfs } from "../../../core/blitzkit/vfs";
|
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||||
import {
|
import {
|
||||||
vertexAttributeGLTFName,
|
vertexAttributeGLTFName,
|
||||||
vertexAttributeGltfVectorSizes,
|
vertexAttributeGltfVectorSizes,
|
||||||
@@ -69,11 +69,7 @@ const omitMeshNames = {
|
|||||||
end: ["_POINT"],
|
end: ["_POINT"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function extractModel(
|
async function extractModel(vfs: AbstractVFS, path: string) {
|
||||||
vfs: AbstractVFS,
|
|
||||||
path: string,
|
|
||||||
isPart = false,
|
|
||||||
) {
|
|
||||||
const sc2Path = `Data/3d/${path}.sc2`;
|
const sc2Path = `Data/3d/${path}.sc2`;
|
||||||
const scgPath = `Data/3d/${path}.scg`;
|
const scgPath = `Data/3d/${path}.scg`;
|
||||||
const sc2 = new Sc2ReadStream(
|
const sc2 = new Sc2ReadStream(
|
||||||
@@ -85,153 +81,6 @@ export async function extractModel(
|
|||||||
const document = new Document();
|
const document = new Document();
|
||||||
const scene = document.createScene();
|
const scene = document.createScene();
|
||||||
const buffer = document.createBuffer();
|
const buffer = document.createBuffer();
|
||||||
const materials = await extractMaterials(document, sc2, path);
|
|
||||||
|
|
||||||
parseHierarchies<Scene | Node>(
|
|
||||||
sc2["#hierarchy"],
|
|
||||||
scene,
|
|
||||||
(hierarchy, components, parent) => {
|
|
||||||
if (
|
|
||||||
omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) ||
|
|
||||||
omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit))
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const node = document.createNode(hierarchy.name);
|
|
||||||
|
|
||||||
components.forEach((component) => {
|
|
||||||
switch (component["comp.typename"]) {
|
|
||||||
case "LodComponent":
|
|
||||||
// found and used later by transform component
|
|
||||||
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "TransformComponent": {
|
|
||||||
const localTranslation = component["tc.localTranslation"];
|
|
||||||
// The game resets top-level node translation to [0, 0, 0] on load.
|
|
||||||
// Child nodes and customization parts must keep their authored
|
|
||||||
// local offsets.
|
|
||||||
node.setTranslation(
|
|
||||||
parent instanceof Scene && !isPart ? [0, 0, 0] : localTranslation,
|
|
||||||
);
|
|
||||||
node.setRotation(component["tc.localRotation"]);
|
|
||||||
node.setScale(component["tc.localScale"]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "StateSwitcherComponent":
|
|
||||||
node.setExtras({
|
|
||||||
state: component[`ssc.state${component["ssc.activeState"]}`],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "RenderComponent": {
|
|
||||||
const renderObject = component["rc.renderObj"];
|
|
||||||
|
|
||||||
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
|
||||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
|
||||||
|
|
||||||
if (lodIndex !== 0) return;
|
|
||||||
|
|
||||||
const batchKey = batchIndex.toString().padStart(4, "0");
|
|
||||||
const batch = renderObject["ro.batches"][batchKey];
|
|
||||||
const material = materials.get(batch["rb.nmatname"]);
|
|
||||||
const polygonGroup = scg.get(batch["rb.datasource"]);
|
|
||||||
|
|
||||||
if (!(material instanceof Material)) {
|
|
||||||
// probably shadow material
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (polygonGroup === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing polygon group ${batch["rb.datasource"]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lodNode = document.createNode(batchKey);
|
|
||||||
const indicesAccessor = document
|
|
||||||
.createAccessor()
|
|
||||||
.setType("SCALAR")
|
|
||||||
.setArray(new Uint16Array(polygonGroup.indices))
|
|
||||||
.setBuffer(buffer);
|
|
||||||
const primitive = document
|
|
||||||
.createPrimitive()
|
|
||||||
.setIndices(indicesAccessor)
|
|
||||||
.setMaterial(material)
|
|
||||||
.setName(batchKey);
|
|
||||||
|
|
||||||
const attributes = new Map<VertexAttribute, number[][]>();
|
|
||||||
|
|
||||||
polygonGroup.vertices.forEach((vertex) => {
|
|
||||||
vertex.forEach(({ attribute, value }) => {
|
|
||||||
if (!attributes.has(attribute)) {
|
|
||||||
attributes.set(attribute, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.get(attribute)!.push(value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
attributes.forEach((value, attribute) => {
|
|
||||||
const name = vertexAttributeGLTFName[attribute];
|
|
||||||
|
|
||||||
if (
|
|
||||||
name === undefined ||
|
|
||||||
primitive.getAttribute(name) !== null
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
|
||||||
const attributeAccessor = document
|
|
||||||
.createAccessor(name)
|
|
||||||
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
|
||||||
.setArray(new Float32Array(value.flat()))
|
|
||||||
.setBuffer(buffer);
|
|
||||||
|
|
||||||
primitive.setAttribute(name, attributeAccessor);
|
|
||||||
});
|
|
||||||
|
|
||||||
const mesh = document
|
|
||||||
.createMesh(batch["##name"])
|
|
||||||
.addPrimitive(primitive);
|
|
||||||
lodNode.setMesh(mesh);
|
|
||||||
node.addChild(lodNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
|
||||||
throw new Error(
|
|
||||||
`Unhandled component type: ${component["comp.typename"]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
parent.addChild(node);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
scene.addChild(document.createNode("test"));
|
|
||||||
|
|
||||||
await document.transform(prune({ keepAttributes: true }), dedup());
|
|
||||||
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function extractMaterials(
|
|
||||||
document: Document,
|
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
|
||||||
path: string,
|
|
||||||
config?: string,
|
|
||||||
) {
|
|
||||||
const materials = new Map<bigint, Material | bigint | undefined>();
|
const materials = new Map<bigint, Material | bigint | undefined>();
|
||||||
|
|
||||||
for (const node of sc2["#dataNodes"]) {
|
for (const node of sc2["#dataNodes"]) {
|
||||||
@@ -246,32 +95,15 @@ export async function extractMaterials(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const material = document.createMaterial(node.materialName);
|
||||||
let textures: Textures | undefined = undefined;
|
let textures: Textures | undefined = undefined;
|
||||||
|
|
||||||
if (config !== undefined) {
|
if (node.textures) {
|
||||||
if (typeof node.configCount !== "number") continue;
|
|
||||||
|
|
||||||
const archive = times(
|
|
||||||
node.configCount,
|
|
||||||
(index) => node[`configArchive_${index}`],
|
|
||||||
).find((archive) => archive.configName === config);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!archive ||
|
|
||||||
isEqual(archive.textures, node.configArchive_0.textures)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
textures = archive.textures;
|
|
||||||
} else if (node.textures) {
|
|
||||||
textures = node.textures;
|
textures = node.textures;
|
||||||
} else if (typeof node.configCount === "number") {
|
} else if (typeof node.configCount === "number") {
|
||||||
textures = node.configArchive_0.textures;
|
textures = node.configArchive_0.textures;
|
||||||
}
|
}
|
||||||
|
|
||||||
const material = document.createMaterial(node.materialName);
|
|
||||||
|
|
||||||
if (textures) {
|
if (textures) {
|
||||||
const baseColor = await readBaseColor(
|
const baseColor = await readBaseColor(
|
||||||
`Data/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
|
`Data/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
|
||||||
@@ -408,7 +240,136 @@ export async function extractMaterials(
|
|||||||
materials.set(id, resolvedMaterial);
|
materials.set(id, resolvedMaterial);
|
||||||
});
|
});
|
||||||
|
|
||||||
return materials;
|
parseHierarchies<Scene | Node>(
|
||||||
|
sc2["#hierarchy"],
|
||||||
|
scene,
|
||||||
|
(hierarchy, components, parent) => {
|
||||||
|
if (
|
||||||
|
omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) ||
|
||||||
|
omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit))
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const node = document.createNode(hierarchy.name);
|
||||||
|
|
||||||
|
components.forEach((component) => {
|
||||||
|
switch (component["comp.typename"]) {
|
||||||
|
case "LodComponent":
|
||||||
|
// found and used later by transform component
|
||||||
|
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "TransformComponent": {
|
||||||
|
const localTranslation = component["tc.localTranslation"];
|
||||||
|
// The game resets top-level node translation to [0, 0, 0] on load.
|
||||||
|
// Child nodes must keep their authored local offsets.
|
||||||
|
node.setTranslation(
|
||||||
|
parent instanceof Scene ? [0, 0, 0] : localTranslation,
|
||||||
|
);
|
||||||
|
node.setRotation(component["tc.localRotation"]);
|
||||||
|
node.setScale(component["tc.localScale"]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "RenderComponent": {
|
||||||
|
const renderObject = component["rc.renderObj"];
|
||||||
|
|
||||||
|
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
||||||
|
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||||
|
|
||||||
|
if (lodIndex !== 0) return;
|
||||||
|
|
||||||
|
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||||
|
const batch = renderObject["ro.batches"][batchKey];
|
||||||
|
const material = materials.get(batch["rb.nmatname"]);
|
||||||
|
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||||
|
|
||||||
|
if (!(material instanceof Material)) {
|
||||||
|
// probably shadow material
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (polygonGroup === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing polygon group ${batch["rb.datasource"]}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lodNode = document.createNode(batchKey);
|
||||||
|
const indicesAccessor = document
|
||||||
|
.createAccessor()
|
||||||
|
.setType("SCALAR")
|
||||||
|
.setArray(new Uint16Array(polygonGroup.indices))
|
||||||
|
.setBuffer(buffer);
|
||||||
|
const primitive = document
|
||||||
|
.createPrimitive()
|
||||||
|
.setIndices(indicesAccessor)
|
||||||
|
.setMaterial(material)
|
||||||
|
.setName(batchKey);
|
||||||
|
|
||||||
|
const attributes = new Map<VertexAttribute, number[][]>();
|
||||||
|
|
||||||
|
polygonGroup.vertices.forEach((vertex) => {
|
||||||
|
vertex.forEach(({ attribute, value }) => {
|
||||||
|
if (!attributes.has(attribute)) {
|
||||||
|
attributes.set(attribute, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.get(attribute)!.push(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
attributes.forEach((value, attribute) => {
|
||||||
|
const name = vertexAttributeGLTFName[attribute];
|
||||||
|
|
||||||
|
if (
|
||||||
|
name === undefined ||
|
||||||
|
primitive.getAttribute(name) !== null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||||
|
const attributeAccessor = document
|
||||||
|
.createAccessor(name)
|
||||||
|
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||||
|
.setArray(new Float32Array(value.flat()))
|
||||||
|
.setBuffer(buffer);
|
||||||
|
|
||||||
|
primitive.setAttribute(name, attributeAccessor);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mesh = document
|
||||||
|
.createMesh(batch["##name"])
|
||||||
|
.addPrimitive(primitive);
|
||||||
|
lodNode.setMesh(mesh);
|
||||||
|
node.addChild(lodNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
||||||
|
throw new Error(
|
||||||
|
`Unhandled component type: ${component["comp.typename"]}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.addChild(node);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
scene.addChild(document.createNode("test"));
|
||||||
|
|
||||||
|
await document.transform(prune({ keepAttributes: true }), dedup());
|
||||||
|
|
||||||
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readBaseColor(path: string) {
|
async function readBaseColor(path: string) {
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import {
|
|
||||||
parseHierarchies,
|
|
||||||
Sc2ReadStream,
|
|
||||||
type CamouflagesXml,
|
|
||||||
type CamouflagesYaml,
|
|
||||||
type TankParameters,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import {
|
|
||||||
Document,
|
|
||||||
Node,
|
|
||||||
NodeIO,
|
|
||||||
PropertyType,
|
|
||||||
Scene,
|
|
||||||
} from "@gltf-transform/core";
|
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|
||||||
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
|
|
||||||
import type { APIContext } from "astro";
|
|
||||||
import type { Vector3Tuple, Vector4Tuple } from "three";
|
|
||||||
import { SKIN_NUMBER } from "../../../../../constants/skinNumber";
|
|
||||||
import { api } from "../../../../../core/blitzkit/api";
|
|
||||||
import { mixStaticPaths } from "../../../../../core/blitzkit/mixStaticPaths";
|
|
||||||
import { vfs } from "../../../../../core/blitzkit/vfs";
|
|
||||||
import {
|
|
||||||
extractMaterials,
|
|
||||||
extractModel,
|
|
||||||
} from "../../../../[...chunk_models_tanks]/[id]/model.glb";
|
|
||||||
import { getStaticPaths as _getStaticPaths } from "../_index";
|
|
||||||
|
|
||||||
const SKIN_NUMBER_SLOTS = 5;
|
|
||||||
const DIGIT_COLUMNS = 5;
|
|
||||||
const DIGIT_ROWS = 2;
|
|
||||||
|
|
||||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
|
||||||
const camouflages = await api.camouflageDefinitions();
|
|
||||||
|
|
||||||
return Object.values(camouflages.camouflages)
|
|
||||||
.filter((camouflage) => camouflage.tank_id)
|
|
||||||
.map((camouflage) => ({
|
|
||||||
params: { id: camouflage.tank_id!, skin: camouflage.id },
|
|
||||||
props: { skin: camouflage.id },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
|
|
||||||
"Data/XML/item_defs/vehicles/common/camouflages.xml",
|
|
||||||
);
|
|
||||||
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
|
|
||||||
"Data/camouflages.yaml",
|
|
||||||
);
|
|
||||||
const customization = await vfs.yaml<
|
|
||||||
Record<string, { Name: string; Path: string }>
|
|
||||||
>("Data/3d/Customization.yaml");
|
|
||||||
|
|
||||||
export async function GET({ props }: APIContext<{ skin: number }>) {
|
|
||||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
||||||
|
|
||||||
const items = new Map(
|
|
||||||
Object.values(customization).map(({ Name, Path }) => [
|
|
||||||
Name,
|
|
||||||
Path.replace("~res:/3d/", "").replace(/\.sc2$/, ""),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
const key = Object.keys(camouflagesXml.root.camouflages).find(
|
|
||||||
(key) => camouflagesXml.root.camouflages[key].id === props.skin,
|
|
||||||
)!;
|
|
||||||
const include = camouflagesXml.root.camouflages[key].vehicleFilter.include;
|
|
||||||
const [nation, tankKey] = (
|
|
||||||
include as { vehicle: { name: string } }
|
|
||||||
).vehicle.name.split(":");
|
|
||||||
const { preset, customEntities } = camouflagesYaml[key];
|
|
||||||
const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1);
|
|
||||||
const parameters = await vfs.yaml<TankParameters>(
|
|
||||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
|
||||||
);
|
|
||||||
const basePath = parameters.resourcesPath.blitzModelPath.replace(
|
|
||||||
/\.sc2$/,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
const sc2 = new Sc2ReadStream(
|
|
||||||
(await vfs.file(`Data/3d/${basePath}.sc2`)).buffer as ArrayBuffer,
|
|
||||||
).sc2();
|
|
||||||
const slots: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
target: string;
|
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector4Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
}
|
|
||||||
> = {};
|
|
||||||
const states: Record<string, string | null> = {};
|
|
||||||
|
|
||||||
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
|
||||||
for (const component of components) {
|
|
||||||
if (component["comp.typename"] === "NewSlotComponent") {
|
|
||||||
for (const slot of Object.values(component.slots)) {
|
|
||||||
slots[slot.name] = {
|
|
||||||
target: hierarchy.name,
|
|
||||||
position: slot.transform.position,
|
|
||||||
rotation: slot.transform.rotation,
|
|
||||||
scale: slot.transform.scale,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component["comp.typename"] === "SlotComponent") {
|
|
||||||
slots[component["sc.slotName"]] = {
|
|
||||||
target: hierarchy.name,
|
|
||||||
position: component["sc.attachmentTranslation"] as Vector3Tuple,
|
|
||||||
rotation: component["sc.attachmentRotation"] as Vector4Tuple,
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
const document = new Document();
|
|
||||||
const scene = document.createScene().setExtras({ states });
|
|
||||||
const targets = new Map<string, Node>();
|
|
||||||
|
|
||||||
document.createBuffer();
|
|
||||||
|
|
||||||
if (preset) await extractMaterials(document, sc2, basePath, preset);
|
|
||||||
|
|
||||||
for (const name in customEntities) {
|
|
||||||
const slot = slots[name];
|
|
||||||
const path = items.get(customEntities[name].item);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!slot ||
|
|
||||||
!path ||
|
|
||||||
!(await vfs.resolve(`Data/3d/${path}.sc2`)) ||
|
|
||||||
!(await vfs.resolve(`Data/3d/${path}.scg`))
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = await extractModel(vfs, path, true);
|
|
||||||
const map = mergeDocuments(document, part);
|
|
||||||
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
|
||||||
const node = document
|
|
||||||
.createNode(name)
|
|
||||||
.setTranslation(slot.position)
|
|
||||||
.setRotation(slot.rotation)
|
|
||||||
.setScale(slot.scale);
|
|
||||||
let target = targets.get(slot.target);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
target = document.createNode(slot.target);
|
|
||||||
targets.set(slot.target, target);
|
|
||||||
scene.addChild(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const child of partScene.listChildren()) {
|
|
||||||
node.addChild(child as Node);
|
|
||||||
}
|
|
||||||
|
|
||||||
partScene.dispose();
|
|
||||||
target.addChild(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
applySkinNumber(document, SKIN_NUMBER);
|
|
||||||
|
|
||||||
await document.transform(
|
|
||||||
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.TEXTURE] }),
|
|
||||||
unpartition(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(await nodeIO.writeBinary(document));
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySkinNumber(document: Document, number: string) {
|
|
||||||
const start = Math.ceil((SKIN_NUMBER_SLOTS - number.length) / 2) + 1;
|
|
||||||
|
|
||||||
for (const node of document.getRoot().listNodes()) {
|
|
||||||
const match = node.getName().match(/_number_\d+_(\d+)$/);
|
|
||||||
|
|
||||||
if (!match) continue;
|
|
||||||
|
|
||||||
const digit = number[Number(match[1]) - start];
|
|
||||||
|
|
||||||
if (digit === undefined) {
|
|
||||||
node.dispose();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cell = (Number(digit) + 9) % 10;
|
|
||||||
const column = cell % DIGIT_COLUMNS;
|
|
||||||
const row = Math.floor(cell / DIGIT_COLUMNS);
|
|
||||||
|
|
||||||
node.traverse((child) => {
|
|
||||||
for (const primitive of child.getMesh()?.listPrimitives() ?? []) {
|
|
||||||
const uvs = primitive.getAttribute("TEXCOORD_0")?.clone();
|
|
||||||
|
|
||||||
if (!uvs) continue;
|
|
||||||
|
|
||||||
for (let index = 0; index < uvs.getCount(); index++) {
|
|
||||||
const [u, v] = uvs.getElement(index, [0, 0]) as [number, number];
|
|
||||||
|
|
||||||
uvs.setElement(index, [
|
|
||||||
(column + u) / DIGIT_COLUMNS,
|
|
||||||
(row + v) / DIGIT_ROWS,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
primitive.setAttribute("TEXCOORD_0", uvs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
import CreditsPromo from "../../../../components/CreditsPromo.astro";
|
|
||||||
import { tools } from "../../../../constants/tools";
|
|
||||||
import ToolLayout from "../../../../layouts/ToolLayout.astro";
|
|
||||||
|
|
||||||
export { getStaticPaths } from "../index.astro";
|
|
||||||
|
|
||||||
const { locale } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<ToolLayout tool={tools.credits} {locale}>
|
|
||||||
<CreditsPromo {locale} />
|
|
||||||
</ToolLayout>
|
|
||||||
@@ -5,7 +5,6 @@ import locales from "@blitzkit/i18n/locales.json";
|
|||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
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 { 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";
|
||||||
@@ -66,8 +65,6 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
|
|||||||
<BaseLayout {locale}>
|
<BaseLayout {locale}>
|
||||||
<HomeHero />
|
<HomeHero />
|
||||||
|
|
||||||
<Countdown client:load />
|
|
||||||
|
|
||||||
{
|
{
|
||||||
locale !== locales.default && (
|
locale !== locales.default && (
|
||||||
<Flex justify="center" pt="4" my="4">
|
<Flex justify="center" pt="4" my="4">
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { load } from "cheerio";
|
|||||||
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 { tankCharacteristics } from "../../../../../core/blitzkit/tankCharacteristics";
|
import { tankCharacteristics } from "../../../../../core/blitzkit/tankCharacteristics";
|
||||||
import { defaultTankWeapon } from "../../../../../core/blitzkit/tankToDuelMember";
|
|
||||||
import { getStrings } from "../../../../../core/i18n/getStrings";
|
import { getStrings } from "../../../../../core/i18n/getStrings";
|
||||||
import BaseLayout from "../../../../../layouts/BaseLayout.astro";
|
import BaseLayout from "../../../../../layouts/BaseLayout.astro";
|
||||||
import { genericDefaultEquipmentMatrix } from "../../../../../stores/duel/constants";
|
import { genericDefaultEquipmentMatrix } from "../../../../../stores/duel/constants";
|
||||||
@@ -54,7 +53,9 @@ const title = literals(strings.website.tools.tankopedia.tank_seo_title, {
|
|||||||
const url = import.meta.env.SITE + Astro.url.pathname;
|
const url = import.meta.env.SITE + Astro.url.pathname;
|
||||||
|
|
||||||
const icon = alias("api", `/tanks/${id}/icons/big.webp`);
|
const icon = alias("api", `/tanks/${id}/icons/big.webp`);
|
||||||
const { turret, gun, shell } = defaultTankWeapon(tank);
|
const turret = tank.turrets.at(-1)!;
|
||||||
|
const gun = turret.guns.at(-1)!;
|
||||||
|
const shell = gun.shells[0];
|
||||||
const characteristics = tankCharacteristics(
|
const characteristics = tankCharacteristics(
|
||||||
{
|
{
|
||||||
tank,
|
tank,
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { api } from "../../../core/blitzkit/api";
|
|
||||||
import { mixStaticPaths } from "../../../core/blitzkit/mixStaticPaths";
|
|
||||||
import { getStaticPaths as _getStaticPaths } from "../_index";
|
|
||||||
|
|
||||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
|
||||||
const tanks = await api.tankDefinitions();
|
|
||||||
|
|
||||||
return Object.values(tanks.tanks).map((tank) => ({
|
|
||||||
params: { id: tank.id },
|
|
||||||
props: { id: tank.id },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import {
|
|
||||||
parseHierarchies,
|
|
||||||
Sc2ReadStream,
|
|
||||||
type CamouflagesXml,
|
|
||||||
type CamouflagesYaml,
|
|
||||||
type TankParameters,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import {
|
|
||||||
Document,
|
|
||||||
Node,
|
|
||||||
NodeIO,
|
|
||||||
PropertyType,
|
|
||||||
Scene,
|
|
||||||
} from "@gltf-transform/core";
|
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|
||||||
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
|
|
||||||
import type { APIContext } from "astro";
|
|
||||||
import type { Vector3Tuple, Vector4Tuple } from "three";
|
|
||||||
import { SKIN_NUMBER } from "../../../../constants/skinNumber";
|
|
||||||
import { api } from "../../../../core/blitzkit/api";
|
|
||||||
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
|
||||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
|
||||||
import {
|
|
||||||
extractMaterials,
|
|
||||||
extractModel,
|
|
||||||
} from "../../../[...chunk_models_tanks]/[id]/model.glb";
|
|
||||||
import { getStaticPaths as _getStaticPaths } from "../_index";
|
|
||||||
|
|
||||||
const SKIN_NUMBER_SLOTS = 5;
|
|
||||||
const DIGIT_COLUMNS = 5;
|
|
||||||
const DIGIT_ROWS = 2;
|
|
||||||
|
|
||||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
|
||||||
const camouflages = await api.camouflageDefinitions();
|
|
||||||
|
|
||||||
return Object.values(camouflages.camouflages)
|
|
||||||
.filter((camouflage) => camouflage.tank_id)
|
|
||||||
.map((camouflage) => ({
|
|
||||||
params: { id: camouflage.tank_id!, skin: camouflage.id },
|
|
||||||
props: { skin: camouflage.id },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
|
|
||||||
"Data/XML/item_defs/vehicles/common/camouflages.xml",
|
|
||||||
);
|
|
||||||
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
|
|
||||||
"Data/camouflages.yaml",
|
|
||||||
);
|
|
||||||
const customization = await vfs.yaml<
|
|
||||||
Record<string, { Name: string; Path: string }>
|
|
||||||
>("Data/3d/Customization.yaml");
|
|
||||||
|
|
||||||
export async function GET({ props }: APIContext<{ skin: number }>) {
|
|
||||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
||||||
|
|
||||||
const items = new Map(
|
|
||||||
Object.values(customization).map(({ Name, Path }) => [
|
|
||||||
Name,
|
|
||||||
Path.replace("~res:/3d/", "").replace(/\.sc2$/, ""),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
const key = Object.keys(camouflagesXml.root.camouflages).find(
|
|
||||||
(key) => camouflagesXml.root.camouflages[key].id === props.skin,
|
|
||||||
)!;
|
|
||||||
const include = camouflagesXml.root.camouflages[key].vehicleFilter.include;
|
|
||||||
const [nation, tankKey] = (
|
|
||||||
include as { vehicle: { name: string } }
|
|
||||||
).vehicle.name.split(":");
|
|
||||||
const { preset, customEntities } = camouflagesYaml[key];
|
|
||||||
const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1);
|
|
||||||
const parameters = await vfs.yaml<TankParameters>(
|
|
||||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
|
||||||
);
|
|
||||||
const basePath = parameters.resourcesPath.blitzModelPath.replace(
|
|
||||||
/\.sc2$/,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
const sc2 = new Sc2ReadStream(
|
|
||||||
(await vfs.file(`Data/3d/${basePath}.sc2`)).buffer as ArrayBuffer,
|
|
||||||
).sc2();
|
|
||||||
const slots: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
target: string;
|
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector4Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
}
|
|
||||||
> = {};
|
|
||||||
const states: Record<string, string | null> = {};
|
|
||||||
|
|
||||||
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
|
||||||
for (const component of components) {
|
|
||||||
if (component["comp.typename"] === "NewSlotComponent") {
|
|
||||||
for (const slot of Object.values(component.slots)) {
|
|
||||||
slots[slot.name] = {
|
|
||||||
target: hierarchy.name,
|
|
||||||
position: slot.transform.position,
|
|
||||||
rotation: slot.transform.rotation,
|
|
||||||
scale: slot.transform.scale,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component["comp.typename"] === "SlotComponent") {
|
|
||||||
slots[component["sc.slotName"]] = {
|
|
||||||
target: hierarchy.name,
|
|
||||||
position: component["sc.attachmentTranslation"] as Vector3Tuple,
|
|
||||||
rotation: component["sc.attachmentRotation"] as Vector4Tuple,
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
const document = new Document();
|
|
||||||
const scene = document.createScene().setExtras({ states });
|
|
||||||
const targets = new Map<string, Node>();
|
|
||||||
|
|
||||||
document.createBuffer();
|
|
||||||
|
|
||||||
if (preset) await extractMaterials(document, sc2, basePath, preset);
|
|
||||||
|
|
||||||
for (const name in customEntities) {
|
|
||||||
const slot = slots[name];
|
|
||||||
const path = items.get(customEntities[name].item);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!slot ||
|
|
||||||
!path ||
|
|
||||||
!(await vfs.resolve(`Data/3d/${path}.sc2`)) ||
|
|
||||||
!(await vfs.resolve(`Data/3d/${path}.scg`))
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = await extractModel(vfs, path, true);
|
|
||||||
const map = mergeDocuments(document, part);
|
|
||||||
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
|
||||||
const node = document
|
|
||||||
.createNode(name)
|
|
||||||
.setTranslation(slot.position)
|
|
||||||
.setRotation(slot.rotation)
|
|
||||||
.setScale(slot.scale);
|
|
||||||
let target = targets.get(slot.target);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
target = document.createNode(slot.target);
|
|
||||||
targets.set(slot.target, target);
|
|
||||||
scene.addChild(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const child of partScene.listChildren()) {
|
|
||||||
node.addChild(child as Node);
|
|
||||||
}
|
|
||||||
|
|
||||||
partScene.dispose();
|
|
||||||
target.addChild(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
applySkinNumber(document, SKIN_NUMBER);
|
|
||||||
|
|
||||||
await document.transform(
|
|
||||||
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.TEXTURE] }),
|
|
||||||
unpartition(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(await nodeIO.writeBinary(document));
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySkinNumber(document: Document, number: string) {
|
|
||||||
const start = Math.ceil((SKIN_NUMBER_SLOTS - number.length) / 2) + 1;
|
|
||||||
|
|
||||||
for (const node of document.getRoot().listNodes()) {
|
|
||||||
const match = node.getName().match(/_number_\d+_(\d+)$/);
|
|
||||||
|
|
||||||
if (!match) continue;
|
|
||||||
|
|
||||||
const digit = number[Number(match[1]) - start];
|
|
||||||
|
|
||||||
if (digit === undefined) {
|
|
||||||
node.dispose();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cell = (Number(digit) + 9) % 10;
|
|
||||||
const column = cell % DIGIT_COLUMNS;
|
|
||||||
const row = Math.floor(cell / DIGIT_COLUMNS);
|
|
||||||
|
|
||||||
node.traverse((child) => {
|
|
||||||
for (const primitive of child.getMesh()?.listPrimitives() ?? []) {
|
|
||||||
const uvs = primitive.getAttribute("TEXCOORD_0")?.clone();
|
|
||||||
|
|
||||||
if (!uvs) continue;
|
|
||||||
|
|
||||||
for (let index = 0; index < uvs.getCount(); index++) {
|
|
||||||
const [u, v] = uvs.getElement(index, [0, 0]) as [number, number];
|
|
||||||
|
|
||||||
uvs.setElement(index, [
|
|
||||||
(column + u) / DIGIT_COLUMNS,
|
|
||||||
(row + v) / DIGIT_ROWS,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
primitive.setAttribute("TEXCOORD_0", uvs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { chunkedStaticPaths } from "../../core/blitzkit/chunkedStaticPaths";
|
|
||||||
|
|
||||||
export const getStaticPaths = chunkedStaticPaths("models_legendary");
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { api } from "../../../core/blitzkit/api";
|
|
||||||
import { mixStaticPaths } from "../../../core/blitzkit/mixStaticPaths";
|
|
||||||
import { getStaticPaths as _getStaticPaths } from "../_index";
|
|
||||||
|
|
||||||
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
|
||||||
const tanks = await api.tankDefinitions();
|
|
||||||
|
|
||||||
return Object.values(tanks.tanks).map((tank) => ({
|
|
||||||
params: { id: tank.id },
|
|
||||||
props: { id: tank.id },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { chunkedStaticPaths } from "../../core/blitzkit/chunkedStaticPaths";
|
|
||||||
|
|
||||||
export const getStaticPaths = chunkedStaticPaths("models_tanks");
|
|
||||||
@@ -103,9 +103,6 @@ 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;
|
||||||
|
|||||||