mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b00a63a9f8 | ||
|
|
8ec7fcb90a | ||
|
|
830e1597e7 | ||
|
|
3131217600 | ||
|
|
0b740e3f60 | ||
|
|
1416d64e34 | ||
|
|
bc6257a0c3 | ||
|
|
3d8cfc3aeb | ||
|
|
1c4ef7c129 | ||
|
|
a649995cdc | ||
|
|
df53f687f7 | ||
|
|
cae63a70b0 | ||
|
|
de80519f06 | ||
|
|
f935adb70e | ||
|
|
3902c63a65 | ||
|
|
e7b81f39b8 | ||
|
|
9692110733 | ||
|
|
ae2839cb83 | ||
|
|
eb0c35d6c6 | ||
|
|
1f0392681a | ||
|
|
53eebc7a43 | ||
|
|
5c482e0bac | ||
|
|
fe28738fe4 | ||
|
|
e1bc11644d | ||
|
|
e7708ce01b | ||
|
|
9a66fb5115 | ||
|
|
62da186a25 | ||
|
|
95318d0a99 | ||
|
|
9c67fd3204 | ||
|
|
1b08393f65 | ||
|
|
7f7d0cc499 | ||
|
|
0c91ee3600 | ||
|
|
1988ecff92 | ||
|
|
ff03f8ffb2 | ||
|
|
d050776628 | ||
|
|
78bff2cdd2 | ||
|
|
072174ad45 | ||
|
|
03f2bcdc1a | ||
|
|
b6ae34dafa | ||
|
|
2e10052732 | ||
|
|
9a95306b37 | ||
|
|
fbc4355984 | ||
|
|
24026a95ee | ||
|
|
8f4a43fc8a | ||
|
|
646d6fa7d5 | ||
|
|
37fae7477d | ||
|
|
940c3c537b | ||
|
|
fbdfa2c063 | ||
|
|
7234c39ad8 | ||
|
|
c292adac41 | ||
|
|
1af104d4d6 | ||
|
|
7b7513f227 | ||
|
|
02f5cc0686 | ||
|
|
6f1d653d54 | ||
|
|
92c8b90bb3 | ||
|
|
1b807e6d0e | ||
|
|
0e7b020d60 | ||
|
|
eda5d10290 | ||
|
|
85e93cd827 | ||
|
|
bc2dd8c7b7 | ||
|
|
904d50f4c3 | ||
|
|
112472762d | ||
|
|
f6e5fabb65 | ||
|
|
e19d0d5895 | ||
|
|
aa6268a86b | ||
|
|
0f878485b3 | ||
|
|
0e119b6ba8 | ||
|
|
3397e7f67b | ||
|
|
de03a2ab95 | ||
|
|
d4e649804e | ||
|
|
c31d5705eb | ||
|
|
16be5faae6 | ||
|
|
e1b079ca02 | ||
|
|
00149a3d9d | ||
|
|
e3e94e2b49 | ||
|
|
5ce927be4e | ||
|
|
541da65bfc | ||
|
|
e4e92d76b4 | ||
|
|
273881c460 | ||
|
|
3aefe7d985 | ||
|
|
bfe31053a9 | ||
|
|
a5eac5572e | ||
|
|
643e1dd352 | ||
|
|
fbba0bf13b | ||
|
|
c2c586c2e7 | ||
|
|
aaf0e52be4 | ||
|
|
6907e5fb7a | ||
|
|
e5e2f2505c |
No files matched your search
Vendored
+2
@@ -96,6 +96,7 @@
|
|||||||
"fullstats",
|
"fullstats",
|
||||||
"fuzzysort",
|
"fuzzysort",
|
||||||
"gamemode",
|
"gamemode",
|
||||||
|
"gamemodes",
|
||||||
"gameplay",
|
"gameplay",
|
||||||
"glsl",
|
"glsl",
|
||||||
"GLTF",
|
"GLTF",
|
||||||
@@ -157,6 +158,7 @@
|
|||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
"padder",
|
"padder",
|
||||||
"partytown",
|
"partytown",
|
||||||
|
"passability",
|
||||||
"patchnotes",
|
"patchnotes",
|
||||||
"PIXELFORMAT",
|
"PIXELFORMAT",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
|
|||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"words": [
|
|
||||||
"Debuff",
|
|
||||||
"gamemodes",
|
|
||||||
"Passability",
|
|
||||||
"Recoverkit",
|
|
||||||
"Repairkit",
|
|
||||||
"Respawns",
|
|
||||||
"Tankopedia"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,10 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Combined game mode passive and active abilities into a single dropdown
|
- Combined game mode passive and active abilities into a single dropdown
|
||||||
- [Mark Greene] Added tier filters to the Guess game
|
- Added tier filters to the Guess game [Mark Greene]
|
||||||
- Changing tiers will break your streak
|
- Changing tiers will break your streak
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- [Mark Greene] module damage is now properly shown when sorting with it in the tank search
|
- 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
|
||||||
@@ -4,7 +4,7 @@ Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions
|
|||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- [GonnaHetzMe] The turret for Centurion I is no longer offset from the body
|
- The turret for Centurion I is no longer offset from the body [GonnaHetzMe]
|
||||||
- Please allow up to 24 hours for the model to update
|
- Please allow up to 24 hours for the model to update
|
||||||
- Vertical Stabilizers now give the correct 12% buff instead of 15%
|
- Vertical Stabilizers now give the correct 12% buff instead of 15%
|
||||||
- Re-ordering tanks in Compare now works once again
|
- Re-ordering tanks in Compare now works once again
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions
|
|||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- The Tankopedia now smoothly resizes when enlarging on interaction
|
- The Tankopedia now smoothly resizes when enlarging on interaction
|
||||||
- [Mark Greene] Stock turrets now correctly mount the corresponding stock gun
|
- 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
|
||||||
@@ -5,10 +5,10 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions
|
|||||||
## 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
|
||||||
- [Mark Greene] Added the ability to filter tanks by consumables
|
- 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
|
||||||
- [Mark Greene] Provisions now properly affect terrain resistances
|
- Provisions now properly affect terrain resistances [Mark Greene]
|
||||||
@@ -13,11 +13,11 @@ Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions
|
|||||||
|
|
||||||
## Technical Features
|
## Technical Features
|
||||||
|
|
||||||
- [GonnaHetzMe] Enabling dev mode now displays the tank's non-numerical id
|
- Enabling dev mode now displays the tank's non-numerical id [GonnaHetzMe]
|
||||||
- [GonnaHetzMe] The dev id and the new dev name are now copyable on click
|
- The dev id and the new dev name are now copyable on click [GonnaHetzMe]
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- [GonnaHetzMe] Tech trees that start at tiers higher than tier 1 like the Blitz Nation ones now properly render their trees in Tankopedia
|
- Tech trees that start at tiers higher than tier 1 like the Blitz Nation ones now properly render their trees in Tankopedia [GonnaHetzMe]
|
||||||
- [GonnaHetzMe] The tank pitch/yaw inputs in the Tank Sandbox now initialize with the correct values instead of 0
|
- The tank pitch/yaw inputs in the Tank Sandbox now initialize with the correct values instead of 0 [GonnaHetzMe]
|
||||||
- [GonnaHetzMe] Large Repair Kits once again use the correct modern icon
|
- Large Repair Kits once again use the correct modern icon [GonnaHetzMe]
|
||||||
@@ -4,5 +4,5 @@ Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions
|
|||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- [GonnaHetzMe] Tank hit points in Equalizer mode is now accurate to the game
|
- Tank hit points in Equalizer mode is now accurate to the game [GonnaHetzMe]
|
||||||
- Armor highlighting now reflects the actual thicknesses of tanks in Equalizer mode
|
- Armor highlighting now reflects the actual thicknesses of tanks in Equalizer mode
|
||||||
@@ -4,9 +4,9 @@ Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions
|
|||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- [GonnaHetzMe] Tungsten shells are now properly cropped
|
- Tungsten shells are now properly cropped [GonnaHetzMe]
|
||||||
- Equalizer no longer gives insanely high penetration values
|
- Equalizer no longer gives insanely high penetration values
|
||||||
- The tank reveal animation no longer repeats when you scroll away and back to the tank sandbox
|
- The tank reveal animation no longer repeats when you scroll away and back to the tank sandbox
|
||||||
- [GonnaHetzMe] The RHM. Pzw.'s turret no longer has the floating plate
|
- The RHM. Pzw.'s turret no longer has the floating plate [GonnaHetzMe]
|
||||||
- Please give BlitzKit up to 1 hour to update the model
|
- Please give BlitzKit up to 1 hour to update the model
|
||||||
- [GonnaHetzMe] Tanks with inclined turret rings like the Minotauro now display the correct pitch in the tank sandbox inputs
|
- Tanks with inclined turret rings like the Minotauro now display the correct pitch in the tank sandbox inputs [GonnaHetzMe]
|
||||||
@@ -4,7 +4,7 @@ Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [GonnaHetzMe] Added tooltips to the module selector in Tankopedia
|
- Added tooltips to the module selector in Tankopedia [GonnaHetzMe]
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions
|
|||||||
- Removed tank visual and collision model caching
|
- Removed tank visual and collision model caching
|
||||||
- Guesser once again breaks streaks if not all tiers are selected
|
- Guesser once again breaks streaks if not all tiers are selected
|
||||||
- BlitzKit now properly unloads tank models from memory when visually hidden in Guesser
|
- BlitzKit now properly unloads tank models from memory when visually hidden in Guesser
|
||||||
- [GonnaHetzMe] Tanks with complex transparency like the Herdbreaker once again handle opacity correctly
|
- Tanks with complex transparency like the Herdbreaker once again handle opacity correctly [GonnaHetzMe]
|
||||||
- [GonnaHetzMe] Pieces of tank models that have transparency now handle culling properly (they're visible from both sides)
|
- Pieces of tank models that have transparency now handle culling properly (they're visible from both sides) [GonnaHetzMe]
|
||||||
- [GonnaHetzMe] Tanks with "cardboard" textures have been fixed, thanks to better PVR file parsing
|
- Tanks with "cardboard" textures have been fixed, thanks to better PVR file parsing [GonnaHetzMe]
|
||||||
- [GonnaHetzMe] Tank icons are now scaled correctly so small tanks are now properly zoomed out in contrast to bigger ones
|
- Tank icons are now scaled correctly so small tanks are now properly zoomed out in contrast to bigger ones [GonnaHetzMe]
|
||||||
@@ -8,13 +8,13 @@ Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions
|
|||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
|
- Clicking on the armor multiple times with HE no longer crashes BlitzKit [Mark Greene]
|
||||||
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
|
- 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
|
||||||
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
|
- 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
|
||||||
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
|
- Armor plate names in Static Armor are now properly capitalized in English and translated to other languages [Mark Greene]
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# BlitzKit 1.31.4
|
|
||||||
|
|
||||||
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- [Mark Greene] Added Peekaboo DPM
|
|
||||||
- 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
|
|
||||||
- [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
|
|
||||||
- [Mark Greene] Added a spall liner button to the Tankopedia beside the other equipment
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- [Mark Greene] BlitzKit Session now properly handles tanks removed from the game
|
|
||||||
- [Mark Greene] Removed the Beta badge from the Equalizer button
|
|
||||||
- Updated the background image for the Equalizer button to the actual banner of the new game mode
|
|
||||||
- [Mark Greene] The armor no longer disappears when you turn on Dynamic Armor System
|
|
||||||
- [Mark Greene] Camouflage values in Tankopedia are now more descriptive when shooting
|
|
||||||
- [Mark Greene] The contact shadows on the ground no longer grow & darken when switching between tabs
|
|
||||||
- The "Help Translate BlitzKit" text on the home page is now properly centered vertically
|
|
||||||
- [Mark Greene] 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
|
|
||||||
- [Mark Greene] The reload visualizer now displays just 1 decimal place
|
|
||||||
- [Mark Greene] 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
|
|
||||||
- [Mark Greene] The loading bar below the name of the tanks in Tankopedia is now sufficiently spaced below
|
|
||||||
- [Mark Greene] The gun depression visualizer now properly handles the inherent tilt of tanks like the Minotauro
|
|
||||||
|
|
||||||
## Technical Features
|
|
||||||
|
|
||||||
- Added documentation to the README on setting up and developing BlitzKit locally
|
|
||||||
- Added bug report and feature request forms to GitHub
|
|
||||||
- Switched to the GNU AGPLv3 license
|
|
||||||
|
|
||||||
## Technical Fixes
|
|
||||||
|
|
||||||
- BlitzKit now handles the new ID system for game-modes in WoTB
|
|
||||||
- The directory which preview versions of the game are downloaded to are now created properly
|
|
||||||
- This was the last bug standing in the way of BlitzKit Previews
|
|
||||||
- Welcome back, BlitzKit Previews!
|
|
||||||
- Removed some dead code from the BlitzKit model mangler
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# BlitzKit 1.32.0
|
||||||
|
|
||||||
|
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Tankopedia & Armor
|
||||||
|
|
||||||
|
- Added a spall liner button to the Tankopedia beside the other equipment [Mark Greene]
|
||||||
|
- Listed track repair times in the Tankopedia [Mark Greene]
|
||||||
|
- The penetration chance is now displayed when penetration testing armor [Mark Greene]
|
||||||
|
- Added a new setting to disable automatically loading tanks in Tankopedia [Mark Greene]
|
||||||
|
|
||||||
|
### Tank Performance & Mechanics
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### 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 [Mark Greene]
|
||||||
|
|
||||||
|
### Game Compatibility
|
||||||
|
|
||||||
|
- Added compatibility for tier XI tanks [Mark Greene]
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
### Tankopedia & Armor
|
||||||
|
|
||||||
|
- The armor no longer disappears when you turn on Dynamic Armor System [Mark Greene]
|
||||||
|
- Camouflage values in Tankopedia are now more descriptive when shooting [Mark Greene]
|
||||||
|
- The penetration chance is now displayed when penetration testing armor [Mark Greene]
|
||||||
|
- Listed track repair times in the Tankopedia [Mark Greene]
|
||||||
|
- 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 [Mark Greene]
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Tank Search & Filters
|
||||||
|
|
||||||
|
- 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 [Mark Greene]
|
||||||
|
- 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 [Mark Greene]
|
||||||
|
|
||||||
|
### Visuals & UI
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- 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 [Mark Greene]
|
||||||
|
- The "Help Translate BlitzKit" text on the home page is now properly centered vertically
|
||||||
|
- 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 [Mark Greene]
|
||||||
|
- 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 [Mark Greene]
|
||||||
|
- Mixer's controllers now scale better on smaller screens [Mark Greene]
|
||||||
|
- The list of available tanks in the Playlist is no longer excessively wide [Mark Greene]
|
||||||
|
|
||||||
|
### Tank Mechanics & Models
|
||||||
|
|
||||||
|
- 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 [Mark Greene]
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
- Users with a preference on a language are once again redirected to the correct localized pages [Mark Greene]
|
||||||
|
|
||||||
|
### Session & Game Compatibility
|
||||||
|
|
||||||
|
- BlitzKit Session now properly handles tanks removed from the game [Mark Greene]
|
||||||
|
|
||||||
|
## Technical Fixes
|
||||||
|
|
||||||
|
- BlitzKit now handles the new ID system for game-modes in WoTB
|
||||||
|
- The directory which preview versions of the game are downloaded to are now created properly
|
||||||
|
- This was the last bug standing in the way of BlitzKit Previews
|
||||||
|
- Welcome back, BlitzKit Previews!
|
||||||
|
- Removed some dead code from the BlitzKit model mangler
|
||||||
|
- The player discovery loop now persists for longer than 1 iteration [Mark Greene]
|
||||||
|
- Bun is now forced to run scripts with the `--bun` flag
|
||||||
|
|
||||||
|
## Technical Features
|
||||||
|
|
||||||
|
- Added documentation to the README on setting up and developing BlitzKit locally
|
||||||
|
- Added bug report and feature request forms to GitHub
|
||||||
|
- Switched to the GNU AGPLv3 license
|
||||||
|
|
||||||
|
## Technical Fixes
|
||||||
|
|
||||||
|
- BlitzKit now handles the new ID system for game-modes in WoTB
|
||||||
|
- The directory which preview versions of the game are downloaded to are now created properly
|
||||||
|
- This was the last bug standing in the way of BlitzKit Previews
|
||||||
|
- Welcome back, BlitzKit Previews!
|
||||||
|
- Removed some dead code from the BlitzKit model mangler
|
||||||
|
- The player discovery loop now persists for longer than 1 iteration [Mark Greene]
|
||||||
|
- Bun is now forced to run scripts with the `--bun` flag
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Timezones
|
|
||||||
|
|
||||||
What is considered a "day's worth of games" is determined by the reset time. The reset time is generally at midnight everyday after which newer games are counted towards the next day. Reset times vary from region to region:
|
|
||||||
|
|
||||||
- North America: `UTC-5:00` (Central North American Time midnight)
|
|
||||||
- Europe: `UTC+1:00` (Central European Time midnight)
|
|
||||||
- Asia: `UTC+7:00` (Central Asia Standard Time midnight)
|
|
||||||
|
|
||||||
**Reset times are different from Blitz Stars** (Blitz Stars resets at 5:00 am locally relative to the user opening their website). Inconsistencies with Blitz Stars only occur with sleep-deprived players and players who play in the wrong region.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Blitzkit Rating Leaderboard
|
|
||||||
|
|
||||||
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
primary BlitzkitRatingLeaderboard {
|
|
||||||
utf8"BKRL" magic;
|
|
||||||
Header header;
|
|
||||||
primary Body<header> body;
|
|
||||||
}
|
|
||||||
|
|
||||||
Header {
|
|
||||||
uint16 version; // always 1
|
|
||||||
Format format;
|
|
||||||
uint32 count;
|
|
||||||
}
|
|
||||||
|
|
||||||
Format enum<uint8> {
|
|
||||||
Base,
|
|
||||||
Superset1,
|
|
||||||
}
|
|
||||||
|
|
||||||
Body<Header header> match<header.format> {
|
|
||||||
Base {
|
|
||||||
primary {
|
|
||||||
uint32 id;
|
|
||||||
uint16 score;
|
|
||||||
}[header.count] entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
Superset1 {
|
|
||||||
primary {
|
|
||||||
uint32 id;
|
|
||||||
uint16 score;
|
|
||||||
|
|
||||||
uint32 battles;
|
|
||||||
uint32 wins;
|
|
||||||
uint32 survived;
|
|
||||||
|
|
||||||
uint32 damage;
|
|
||||||
uint32 kills;
|
|
||||||
}[header.count] entries;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Discovered Ids
|
|
||||||
|
|
||||||
```c
|
|
||||||
primary DiscoveredIds {
|
|
||||||
utf8"DIDS" magic;
|
|
||||||
Header header;
|
|
||||||
primary Body body;
|
|
||||||
}
|
|
||||||
|
|
||||||
Header {
|
|
||||||
uint16 version; // always 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Body {
|
|
||||||
uint32 count;
|
|
||||||
uint32[count] ids;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
+1
-170
@@ -1,172 +1,3 @@
|
|||||||
# KitScore
|
# KitScore
|
||||||
|
|
||||||
## Memo
|
Remind me to write out the linearized model!
|
||||||
|
|
||||||
KitScore is a performance metric that makes sense. This metric can be applied to any game, virtual or real. Specific pieces of information relating directly to World of Tanks Blitz will be provided within parentheses and with a "WoTB:" prefix.
|
|
||||||
|
|
||||||
## Consumption of Observations
|
|
||||||
|
|
||||||
KitScore is driven solely by the behavior of other players and finding meaning of a new observation within a pool of condensed data based on previously observed data. Generally, we will be provided with a number of statistics (the $x_i$ of an observation) per player (WoTB: $x_\text{frags}$, $x_\text{xp}$, $x_\text{spots}$, etc.) including a target statistic (the $x_j$ of an observation) that all other statistics try to maximize (WoTB: "winrate" which we will defined later as $x_j$).
|
|
||||||
|
|
||||||
If the statistics are cumulative, they will have to be normalized (WoTB: statistics fetched from Wargaming are indeed cumulative even for a single player; $x_\text{frags}=\frac{\Sigma x_\text{frags}}{\Sigma x_\text{battles}}$, $x_\text{spots}=\frac{\Sigma x_\text{spots}}{\Sigma x_\text{battles}}$, $x_j=\frac{\Sigma x_\text{wins}}{\Sigma x_\text{battles}}$, etc.). Each player will have such average statistics, giving us many points of data to work with. If the statistics are not cumulative, then you will be dealing with multiple observations per player which will only help improve accuracy.
|
|
||||||
|
|
||||||
## It's Impossible to "Find What Makes the Metric Tick"
|
|
||||||
|
|
||||||
WN8, a widely adopted metric in the World of Tanks and World of Tanks Blitz communities is plagued with problems. One of them being that people often find and exploit "what makes the metric tick." In other words, people easily exploit the rudimentary nature of WN8 to score undeservedly high.
|
|
||||||
|
|
||||||
In WN8, the gains of some rewarding categories overshadow the losses of others though that may not necessarily be in the best interest in maximizing winrate. KitScore makes this nearly impossible as it punishes a player for scoring well solely high in a certain category. You will never exploit KitScore. Period.
|
|
||||||
|
|
||||||
## Generating Reasonable Values
|
|
||||||
|
|
||||||
TODO: there's like no runaway cases
|
|
||||||
|
|
||||||
## Symbol Interpretation
|
|
||||||
|
|
||||||
| Symbol | Interpretation |
|
|
||||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| $A_\text{human}$ | The human readable version of the raw score $A$. |
|
|
||||||
| $A$ | The raw KitScore. |
|
|
||||||
| $C$ | An arbitrary coefficient. |
|
|
||||||
| $I$ | The number of statistics including the target statistic. This means that there are $I-1$ entries for $x_i$ and $1$ entry for $y$ for each observation. |
|
|
||||||
| $i$ | The statistic index of where there are $I$ entries. |
|
|
||||||
| $j$ | A discriminator for the target statistic. There may never be more than $1$ entries for $j$. |
|
|
||||||
| $m$ | The degree of the polynomial regression. |
|
|
||||||
| $n$ | The number of observations. |
|
|
||||||
| $a$ | An "atomic" score which is a function of the $i$th statistic. |
|
|
||||||
| $w$ | The weight of the $i$th atomic statistic. |
|
|
||||||
|
|
||||||
## Computing the Score
|
|
||||||
|
|
||||||
The final metric $A_\text{human}$ is an arbitrarily scaled value of the raw score $A$ where $C$ is any coefficient of your liking. BlitzKit uses $C=10^3$.
|
|
||||||
|
|
||||||
$$
|
|
||||||
A_\text{human}=CA
|
|
||||||
$$
|
|
||||||
|
|
||||||
The final metric $A$ is the weighted average of individual scores $a$ which are functions of the $i$th statistic excluding the target statistic $j$ itself.
|
|
||||||
|
|
||||||
$$
|
|
||||||
A=\frac{\sum_{i \neq j} w_{i}a_{i}}{\sum_{i \neq j} w_{i}}
|
|
||||||
$$
|
|
||||||
|
|
||||||
The "atomic" score $a$ is the difference between the predicted target statistic $P_{i \leftrightarrow j}(x_i)$ acquired using a polynomial regression of degree $m$ and the actual newly observed $x_j$.
|
|
||||||
|
|
||||||
$$
|
|
||||||
a_i=x_j-P_{i \leftrightarrow j}(x_i)
|
|
||||||
$$
|
|
||||||
|
|
||||||
The polynomial regression of $m$th degree can be written as follows:
|
|
||||||
|
|
||||||
$$
|
|
||||||
P(x)=\beta_0x^0+\beta_1x^1+\beta_2x^2+\dots+\beta_mx^m
|
|
||||||
$$
|
|
||||||
|
|
||||||
Polynomial regression coefficients $\beta$ can be acquired through basic linear algebra where an $m \times m$ of constants is multiplied with $m \times 1$ of unknowns which equals another $m \times 1$ of constants.
|
|
||||||
|
|
||||||
```math
|
|
||||||
\begin{bmatrix}
|
|
||||||
\sum_{k=1}^n x_i^0 & \sum_{k=1}^n x_i^1 & \sum_{k=1}^n x_i^2 & \dots & \sum_{k=1}^n x_i^m \\
|
|
||||||
\sum_{k=1}^n x_i^1 & \sum_{k=1}^n x_i^2 & \sum_{k=1}^n x_i^3 & \dots & \sum_{k=1}^n x_i^{m+1} \\
|
|
||||||
\sum_{k=1}^n x_i^2 & \sum_{k=1}^n x_i^3 & \sum_{k=1}^n x_i^4 & \dots & \sum_{k=1}^n x_i^{m+2} \\
|
|
||||||
\vdots & \vdots & \vdots & \ddots & \vdots \\
|
|
||||||
\sum_{k=1}^n x_i^m & \sum_{k=1}^n x_i^{m+1} & \sum_{k=1}^n x_i^{m+1} & \dots & \sum_{k=1}^n x_i^{2m} \\
|
|
||||||
\end{bmatrix}
|
|
||||||
|
|
||||||
\begin{bmatrix}
|
|
||||||
\beta_0 \\
|
|
||||||
\beta_1 \\
|
|
||||||
\beta_2 \\
|
|
||||||
\vdots \\
|
|
||||||
\beta_m
|
|
||||||
\end{bmatrix}
|
|
||||||
|
|
||||||
=
|
|
||||||
|
|
||||||
\begin{bmatrix}
|
|
||||||
\sum_{k=1}^n x_jx_i^0 \\
|
|
||||||
\sum_{k=1}^n x_jx_i^1 \\
|
|
||||||
\sum_{k=1}^n x_jx_i^2 \\
|
|
||||||
\vdots \\
|
|
||||||
\sum_{k=1}^n x_jx_i^m
|
|
||||||
\end{bmatrix}
|
|
||||||
```
|
|
||||||
|
|
||||||
The weight $w$ is a slice of the Pearson corelation matrix. The numerator is the corelation between the $i$th statistic and the target statistic $j$ while the denominator the sum of the corelations between the $i$th statistic and all statistics but the target statistic $j$.
|
|
||||||
|
|
||||||
$$
|
|
||||||
w_{i}=\frac{r_{i\leftrightarrow j}^2}{\sum_{i_{k}\neq j}r_{i\leftrightarrow i_{k}}^2}
|
|
||||||
$$
|
|
||||||
|
|
||||||
## Understanding the Corelation Matrix
|
|
||||||
|
|
||||||
Forming the corelation matrix will create an $I \times I$ matrix where. This matrix is best represented as a table. The example below illustrates a matrix/table for $I=4$ which includes $3$ entries for $i$ statistics and $1$ entry for the $j$ target statistic.
|
|
||||||
|
|
||||||
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
|
|
||||||
| ----- | --------------------------- | --------------------------- | --------------------------- | --------------------------- |
|
|
||||||
| $x_1$ | $1$ | $r_{1 \leftrightarrow 2}^2$ | $r_{1 \leftrightarrow 3}^2$ | $r_{1 \leftrightarrow j}^2$ |
|
|
||||||
| $x_2$ | $r_{1 \leftrightarrow 2}^2$ | $1$ | $r_{2 \leftrightarrow 3}^2$ | $r_{2 \leftrightarrow j}^2$ |
|
|
||||||
| $x_3$ | $r_{1 \leftrightarrow 3}^2$ | $r_{2 \leftrightarrow 3}^2$ | $1$ | $r_{3 \leftrightarrow j}^2$ |
|
|
||||||
| $x_j$ | $r_{1 \leftrightarrow j}^2$ | $r_{2 \leftrightarrow j}^2$ | $r_{3 \leftrightarrow j}^2$ | $1$ |
|
|
||||||
|
|
||||||
It should not be a surprise to find $r_{i \leftrightarrow i}^2=1$ which forms a neat diagonal across the table. Note that there are many duplicates within the $I^2$ entries. The "bottom-right triangle" can be eliminated to remove redundancies leaving us with only $\frac{I(I+1)}{2}$ fields occupied.
|
|
||||||
|
|
||||||
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
|
|
||||||
| ----- | ----- | --------------------------- | --------------------------- | --------------------------- |
|
|
||||||
| $x_1$ | $1$ | $r_{1 \leftrightarrow 2}^2$ | $r_{1 \leftrightarrow 3}^2$ | $r_{1 \leftrightarrow j}^2$ |
|
|
||||||
| $x_2$ | | $1$ | $r_{2 \leftrightarrow 3}^2$ | $r_{2 \leftrightarrow j}^2$ |
|
|
||||||
| $x_3$ | | | $1$ | $r_{3 \leftrightarrow j}^2$ |
|
|
||||||
| $x_j$ | | | | $1$ |
|
|
||||||
|
|
||||||
When programming this matrix into an array, $r_{i \leftrightarrow i}^2=1$ will be redundant. I recommend condensing the matrix into an array of size $\frac{I(I+1)}{2}+I=\frac{I(I-1)}{2}$ to avoid all redundant entries. For $I=4$, the array will look something like the one illustrated below (here, $j=4$).
|
|
||||||
|
|
||||||
$$
|
|
||||||
[r_{1 \leftrightarrow 2}^2, r_{1 \leftrightarrow 3}^2, r_{1 \leftrightarrow 4}^2, r_{2 \leftrightarrow 3}^2, r_{2 \leftrightarrow 4}^2, r_{3 \leftrightarrow 4}^2]
|
|
||||||
$$
|
|
||||||
|
|
||||||
## Understanding the Weights
|
|
||||||
|
|
||||||
Some may find the weight equation arbitrary; it evidently isn't. The goal of $w$ is to eliminate over-rewarding the same behavior represented in multiple statistics. It makes all atomic statistics concurrent by leveraging the Pearson corelation matrix.
|
|
||||||
|
|
||||||
A simplified example will clarify the idea. First, we compute a Pearson corelation matrix for $I=3$ statistics. We have $x_1$ and $x_2$, the statistics that are trying to maximize $x_j$. Reminder: a Pearson corelation matrix finds the $r$ value not only between $x_i$ and $x_j$ but also between all combinations of $x_i$ and themselves. Here, $r^2$ is calculated using polynomial regression corelation analysis to the $m$th degree.
|
|
||||||
|
|
||||||
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
|
|
||||||
| ----- | ----- | ----------- | ----- | ----- |
|
|
||||||
| $x_1$ | $1$ | $\boxed{0}$ | $0$ | $1$ |
|
|
||||||
| $x_2$ | | $1$ | $0$ | $1$ |
|
|
||||||
| $x_3$ | | | $1$ | $1$ |
|
|
||||||
| $x_j$ | | | | $1$ |
|
|
||||||
|
|
||||||
In the case above, $r^2_{1 \leftrightarrow 2}=0$ meaning that both statistics have absolutely nothing to do with each other. Hence, the player will be awarded for optimizing both individually. And since $r^2_{1 \leftrightarrow j}=r^2_{2 \leftrightarrow j}$, they should both be rewarded equally; in other words, they should have equal weights.
|
|
||||||
|
|
||||||
$$
|
|
||||||
w_{1}=\frac{1}{1 + 0 + 0}=1
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
w_{2}=\frac{1}{0 + 1 + 0}=1
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
w_{3}=\frac{1}{0 + 0 + 1}=1
|
|
||||||
$$
|
|
||||||
|
|
||||||
Now consider a case where $x_1$ and $x_2$ are fully correlated. It would be a bad idea to reward them equally; hence, their weights suffer.
|
|
||||||
|
|
||||||
| | $x_1$ | $x_2$ | $x_3$ | $x_j$ |
|
|
||||||
| ----- | ----- | ----------- | ----- | ----- |
|
|
||||||
| $x_1$ | $1$ | $\boxed{1}$ | $0$ | $1$ |
|
|
||||||
| $x_2$ | | $1$ | $0$ | $1$ |
|
|
||||||
| $x_3$ | | | $1$ | $1$ |
|
|
||||||
| $x_j$ | | | | $1$ |
|
|
||||||
|
|
||||||
$$
|
|
||||||
w_{1}=\frac{1}{1 + 1 + 0}=0.5
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
w_{2}=\frac{1}{1 + 1 + 0}=0.5
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
w_{3}=\frac{1}{0 + 0 + 1}=1
|
|
||||||
$$
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# BlitzKit Regular Tank Stats
|
|
||||||
|
|
||||||
A straightforward way of storing the entire World of Tanks Blitz tank statistics for players to disk.
|
|
||||||
|
|
||||||
## Manifest `manifest.rtsm`
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
primary RegularTankStatisticsManifest {
|
|
||||||
utf8"RTSM" magic;
|
|
||||||
Header header;
|
|
||||||
primary Body body;
|
|
||||||
}
|
|
||||||
|
|
||||||
Header {
|
|
||||||
uint16 version; // always 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Body {
|
|
||||||
uint32 time;
|
|
||||||
uint8 base;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Chunk `*.rtsc`
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
primary RegularTankStatisticsChunk {
|
|
||||||
utf8"RTSC" magic;
|
|
||||||
Header header;
|
|
||||||
primary Body body<header.playerCount>;
|
|
||||||
}
|
|
||||||
|
|
||||||
Header {
|
|
||||||
uint16 version; // always 1
|
|
||||||
uint32 playerCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
Body<uint32 playerCount> {
|
|
||||||
Player[playerCount] players;
|
|
||||||
}
|
|
||||||
|
|
||||||
Player {
|
|
||||||
uint32 id;
|
|
||||||
uint16 tankCount;
|
|
||||||
Tank[tankCount] tanks;
|
|
||||||
}
|
|
||||||
|
|
||||||
Tank {
|
|
||||||
uint32 id;
|
|
||||||
uint32 battleLifeTime;
|
|
||||||
uint32 lastBattleTime;
|
|
||||||
uint8 markOfMastery;
|
|
||||||
uint32 battles;
|
|
||||||
uint32 capturePoints;
|
|
||||||
uint32 damageDealt;
|
|
||||||
uint32 damageReceived;
|
|
||||||
uint32 droppedCapturePoints;
|
|
||||||
uint32 frags;
|
|
||||||
uint32 frags8p;
|
|
||||||
uint32 hits;
|
|
||||||
uint32 losses;
|
|
||||||
uint8 maxFrags;
|
|
||||||
uint16 maxXp;
|
|
||||||
uint32 shots;
|
|
||||||
uint32 spotted;
|
|
||||||
uint32 survivedBattles;
|
|
||||||
uint32 winAndSurvived;
|
|
||||||
uint32 wins;
|
|
||||||
uint32 xp;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
+4
-3
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.31.4",
|
"version": "1.32.0",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd packages/website && bun run dev",
|
"dev": "cd packages/website && bun --bun run dev",
|
||||||
"build": "cd packages/website && bun run build",
|
"build": "cd packages/website && bun run build",
|
||||||
"discover-ids": "cd packages/scripts && bun run discover-ids",
|
"discover-ids": "cd packages/scripts && bun run discover-ids",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
|
|||||||
resistance_hard: terrainResistances[0],
|
resistance_hard: terrainResistances[0],
|
||||||
resistance_medium: terrainResistances[1],
|
resistance_medium: terrainResistances[1],
|
||||||
resistance_soft: terrainResistances[2],
|
resistance_soft: terrainResistances[2],
|
||||||
|
repair_time: track.hysteresisHealth / track.healthRegenPerSec,
|
||||||
tier: track.level,
|
tier: track.level,
|
||||||
unlocks: resolveUnlocks(
|
unlocks: resolveUnlocks(
|
||||||
this.blitzModuleTypeToBlitzkit,
|
this.blitzModuleTypeToBlitzkit,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { SkillDefinitions } from "@blitzkit/protos";
|
||||||
|
import { createDefaultSkills } from "./createDefaultSkills";
|
||||||
|
import { TankEnvironment } from "./tankEnvironment";
|
||||||
|
|
||||||
|
export function createDefaultEnvironment(skills: SkillDefinitions) {
|
||||||
|
return {
|
||||||
|
equalize: false,
|
||||||
|
distance: 0,
|
||||||
|
skills: createDefaultSkills(skills),
|
||||||
|
} satisfies TankEnvironment;
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { SkillDefinitions } from "..";
|
import { SkillDefinitions } from "@blitzkit/protos";
|
||||||
import { permanentSkills } from "../blitz/permanentSkills";
|
import { permanentSkills } from "../blitz/permanentSkills";
|
||||||
|
import { MAX_SKILL_LEVEL } from "../config/skills";
|
||||||
|
|
||||||
export function createDefaultSkills(skillDefinitions: SkillDefinitions) {
|
export function createDefaultSkills(skillDefinitions: SkillDefinitions) {
|
||||||
const skills: Record<string, number> = {};
|
const skills: Record<string, number> = {};
|
||||||
|
|
||||||
Object.values(skillDefinitions.classes).forEach((tankSkills) => {
|
Object.values(skillDefinitions.classes).forEach((tankSkills) => {
|
||||||
tankSkills.skills.forEach((skill) => {
|
tankSkills.skills.forEach((skill) => {
|
||||||
skills[skill] = permanentSkills.includes(skill) ? 7 : 0;
|
skills[skill] = permanentSkills.includes(skill) ? MAX_SKILL_LEVEL : 0;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export function degressiveStat(crew: number) {
|
export function degressiveStat(crew: number) {
|
||||||
return 0.875 / (0.00375 * 100 * crew + 0.5) - 1;
|
return 0.875 / (0.00375 * 100 * crew + 0.5);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ export * from "./api";
|
|||||||
export * from "./assertSecret";
|
export * from "./assertSecret";
|
||||||
export * from "./availableProvisions";
|
export * from "./availableProvisions";
|
||||||
export * from "./coefficient";
|
export * from "./coefficient";
|
||||||
|
export * from "./createDefaultEnvironment";
|
||||||
export * from "./createDefaultProvisions";
|
export * from "./createDefaultProvisions";
|
||||||
export * from "./createDefaultSkills";
|
export * from "./createDefaultSkills";
|
||||||
export * from "./degressiveStat";
|
export * from "./degressiveStat";
|
||||||
@@ -17,7 +18,6 @@ export * from "./normalizeBoundingBox";
|
|||||||
export * from "./patientFetchJSON";
|
export * from "./patientFetchJSON";
|
||||||
export * from "./popularTanks";
|
export * from "./popularTanks";
|
||||||
export * from "./progressiveStat";
|
export * from "./progressiveStat";
|
||||||
export * from "./resolveDpm";
|
|
||||||
export * from "./resolvePenetrationCoefficient";
|
export * from "./resolvePenetrationCoefficient";
|
||||||
export * from "./reviews";
|
export * from "./reviews";
|
||||||
export * from "./romanize";
|
export * from "./romanize";
|
||||||
@@ -25,6 +25,7 @@ export * from "./sluggify";
|
|||||||
export * from "./staticAsset";
|
export * from "./staticAsset";
|
||||||
export * from "./sum";
|
export * from "./sum";
|
||||||
export * from "./tankDefinitions";
|
export * from "./tankDefinitions";
|
||||||
|
export * from "./tankEnvironment";
|
||||||
export * from "./tankIcon";
|
export * from "./tankIcon";
|
||||||
export * from "./tankNames";
|
export * from "./tankNames";
|
||||||
export * from "./unionBoundingBox";
|
export * from "./unionBoundingBox";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function metaSortTank(
|
|||||||
gameDefinitions: GameDefinitions,
|
gameDefinitions: GameDefinitions,
|
||||||
) {
|
) {
|
||||||
return tanks
|
return tanks
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
.sort((a, b) => b.tier - a.tier)
|
.sort((a, b) => b.tier - a.tier)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
|
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export function progressiveStat(crew: number) {
|
export function progressiveStat(crew: number) {
|
||||||
return (0.00375 * 100 * crew + 0.5) / 0.875 - 1;
|
return (0.00375 * 100 * crew + 0.5) / 0.875;
|
||||||
}
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import {
|
|
||||||
GunDefinition,
|
|
||||||
GunDefinitionAutoReloader,
|
|
||||||
ShellDefinition,
|
|
||||||
} from '../protos';
|
|
||||||
|
|
||||||
export function resolveDpm(
|
|
||||||
gun: GunDefinition,
|
|
||||||
shell: ShellDefinition,
|
|
||||||
damageCoefficient = 1,
|
|
||||||
reloadCoefficient = 1,
|
|
||||||
intraClipCoefficient = 1,
|
|
||||||
) {
|
|
||||||
const alpha = shell.armor_damage * damageCoefficient;
|
|
||||||
let dps: number;
|
|
||||||
|
|
||||||
if (gun.gun_type!.$case === 'regular') {
|
|
||||||
dps = alpha / (reloadCoefficient * gun.gun_type!.value.reload);
|
|
||||||
} else if (gun.gun_type!.$case === 'auto_loader') {
|
|
||||||
const damage = alpha * gun.gun_type!.value.shell_count;
|
|
||||||
let time = gun.gun_type!.value.clip_reload * reloadCoefficient;
|
|
||||||
|
|
||||||
if (gun.burst) {
|
|
||||||
time +=
|
|
||||||
(gun.gun_type!.value.shell_count / gun.burst.count - 1) *
|
|
||||||
gun.gun_type!.value.intra_clip *
|
|
||||||
intraClipCoefficient;
|
|
||||||
|
|
||||||
time +=
|
|
||||||
(gun.gun_type!.value.shell_count / gun.burst.count) *
|
|
||||||
(gun.burst.count - 1) *
|
|
||||||
gun.burst.interval;
|
|
||||||
} else {
|
|
||||||
time +=
|
|
||||||
(gun.gun_type!.value.shell_count - 1) *
|
|
||||||
gun.gun_type!.value.intra_clip *
|
|
||||||
intraClipCoefficient;
|
|
||||||
}
|
|
||||||
|
|
||||||
dps = damage / time;
|
|
||||||
} else {
|
|
||||||
const mostOptimalShell = gun.gun_type!.value.shell_reloads.reduce<null | {
|
|
||||||
index: number;
|
|
||||||
reload: number;
|
|
||||||
}>((current, reloadRaw, index) => {
|
|
||||||
const reload =
|
|
||||||
reloadRaw * reloadCoefficient +
|
|
||||||
(index > 0
|
|
||||||
? (gun.gun_type!.value as GunDefinitionAutoReloader).intra_clip *
|
|
||||||
intraClipCoefficient
|
|
||||||
: 0);
|
|
||||||
|
|
||||||
if (current === null || reload < current.reload) {
|
|
||||||
return { index, reload };
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}, null)!;
|
|
||||||
|
|
||||||
dps = alpha / mostOptimalShell?.reload;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dps * 60;
|
|
||||||
}
|
|
||||||
@@ -43,18 +43,6 @@ export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
|
|||||||
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
|
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
|
||||||
};
|
};
|
||||||
export const TIERS = times(10, (index) => index + 1);
|
export const TIERS = times(10, (index) => index + 1);
|
||||||
export const TIER_ROMAN_NUMERALS: Record<number, string> = {
|
|
||||||
1: "I",
|
|
||||||
2: "II",
|
|
||||||
3: "III",
|
|
||||||
4: "IV",
|
|
||||||
5: "V",
|
|
||||||
6: "VI",
|
|
||||||
7: "VII",
|
|
||||||
8: "VIII",
|
|
||||||
9: "IX",
|
|
||||||
10: "X",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const flags: Record<string, string> = {
|
export const flags: Record<string, string> = {
|
||||||
ussr: "<:ussr:1218421042033197197>",
|
ussr: "<:ussr:1218421042033197197>",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface TankEnvironment {
|
||||||
|
equalize: boolean;
|
||||||
|
distance: number;
|
||||||
|
skills: Record<string, number>;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./gunTypeOrder";
|
export * from "./gunTypeOrder";
|
||||||
|
export * from "./skills";
|
||||||
export * from "./tankClassOrder";
|
export * from "./tankClassOrder";
|
||||||
export * from "./treeTypeOrder";
|
export * from "./treeTypeOrder";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const MAX_SKILL_LEVEL = 7;
|
||||||
@@ -34,6 +34,8 @@ export interface VehicleDefinitions {
|
|||||||
};
|
};
|
||||||
level: number;
|
level: number;
|
||||||
hullPosition: string;
|
hullPosition: string;
|
||||||
|
hysteresisHealth: number;
|
||||||
|
healthRegenPerSec: number;
|
||||||
armor: {
|
armor: {
|
||||||
leftTrack:
|
leftTrack:
|
||||||
| number
|
| number
|
||||||
|
|||||||
@@ -8,11 +8,46 @@
|
|||||||
"s": "{value}s",
|
"s": "{value}s",
|
||||||
"percentage": "{value}%",
|
"percentage": "{value}%",
|
||||||
"deg_s": "{value}°/s",
|
"deg_s": "{value}°/s",
|
||||||
"plus": "+{value}"
|
"m_s": "{value}m/s",
|
||||||
|
"km_h": "{value}km/h",
|
||||||
|
"t": "{value}t",
|
||||||
|
"bhp": "{value}bhp",
|
||||||
|
"bhp_t": "{value}bhp/t",
|
||||||
|
"kN": "{value}kN",
|
||||||
|
"hp_s": "{value}hp/s",
|
||||||
|
"plus": "+{value}",
|
||||||
|
"fraction": "{numerator} / {denominator}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"tanks": {
|
"tanks": {
|
||||||
|
"toys": {
|
||||||
|
"ricochet": {
|
||||||
|
"angle": "Angle",
|
||||||
|
"normalization": "Normalization",
|
||||||
|
"effective": "Effective",
|
||||||
|
"armor": "Armor",
|
||||||
|
"no_ricochet": "Explosive shells don't ricochet"
|
||||||
|
},
|
||||||
|
|
||||||
|
"traverse": {
|
||||||
|
"effective": "Effective",
|
||||||
|
"hull": "Hull",
|
||||||
|
"turret": "Turret"
|
||||||
|
},
|
||||||
|
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"characteristics": {
|
"characteristics": {
|
||||||
|
"compare": {
|
||||||
|
"label": "Compare against",
|
||||||
|
"class": "Tier {tier} {class}",
|
||||||
|
"tier": "All tier {tier}",
|
||||||
|
"all": "All tanks"
|
||||||
|
},
|
||||||
|
|
||||||
"titles": {
|
"titles": {
|
||||||
"gun": "Gun",
|
"gun": "Gun",
|
||||||
"clip": "Clip",
|
"clip": "Clip",
|
||||||
@@ -38,11 +73,11 @@
|
|||||||
"burst_size": "Burst size",
|
"burst_size": "Burst size",
|
||||||
"intra_burst": "Intra-burst",
|
"intra_burst": "Intra-burst",
|
||||||
"penetration": "Penetration",
|
"penetration": "Penetration",
|
||||||
"penetration_loss_by_distance": "Penetration loss by distance",
|
"penetration_loss_by_distance": "Loss by 500m",
|
||||||
"penetration_loss_after_ricochet": "Penetration loss after ricochet",
|
"penetration_loss_after_ricochet": "Loss after ricochet",
|
||||||
"shell_velocity": "Shell velocity",
|
"shell_velocity": "Velocity",
|
||||||
"shell_range": "Shell range",
|
"shell_range": "Range",
|
||||||
"shell_capacity": "Shell capacity",
|
"shell_capacity": "Capacity",
|
||||||
"caliber": "Caliber",
|
"caliber": "Caliber",
|
||||||
"normalization": "Normalization",
|
"normalization": "Normalization",
|
||||||
"ricochet": "Ricochet",
|
"ricochet": "Ricochet",
|
||||||
@@ -51,16 +86,32 @@
|
|||||||
"dispersion_angle": "Dispersion angle",
|
"dispersion_angle": "Dispersion angle",
|
||||||
"gun_depression": "Gun depression",
|
"gun_depression": "Gun depression",
|
||||||
"gun_elevation": "Gun elevation",
|
"gun_elevation": "Gun elevation",
|
||||||
|
"gun_frontal_depression": "Frontal depression",
|
||||||
|
"gun_frontal_elevation": "Frontal elevation",
|
||||||
|
"gun_rear_depression": "Rear depression",
|
||||||
|
"gun_rear_elevation": "Rear elevation",
|
||||||
"azimuth_left": "Azimuth left",
|
"azimuth_left": "Azimuth left",
|
||||||
"azimuth_right": "Azimuth right",
|
"azimuth_right": "Azimuth right",
|
||||||
"speed_forward": "Speed forward",
|
"speed_forward": "Forward",
|
||||||
"speed_backward": "Speed backward",
|
"speed_backward": "Backward",
|
||||||
|
"speeds": "Speed",
|
||||||
"engine_power": "Engine power",
|
"engine_power": "Engine power",
|
||||||
"brake_force": "Brake force",
|
"brake_force": "Brake force",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
"power_to_weight": "Power to weight",
|
"power_to_weight": "Power to weight",
|
||||||
"terrain_coefficient": "Terrain coefficient",
|
"terrain_coefficient": "Terrain coefficients",
|
||||||
|
"raw_terrain_coefficient": "Raw terrain coefficients",
|
||||||
|
"hard_terrain_coefficient": "Hard",
|
||||||
|
"medium_terrain_coefficient": "Medium",
|
||||||
|
"soft_terrain_coefficient": "Soft",
|
||||||
|
"hard_terrain_coefficient_raw": "Hard",
|
||||||
|
"medium_terrain_coefficient_raw": "Medium",
|
||||||
|
"soft_terrain_coefficient_raw": "Soft",
|
||||||
"hull_traverse_speed": "Hull traverse speed",
|
"hull_traverse_speed": "Hull traverse speed",
|
||||||
|
"hull_traverse_speeds": "Hull traverse speed",
|
||||||
|
"hull_traverse_speed_hard": "Hard",
|
||||||
|
"hull_traverse_speed_medium": "Medium",
|
||||||
|
"hull_traverse_speed_soft": "Soft",
|
||||||
"turret_traverse_speed": "Turret traverse speed",
|
"turret_traverse_speed": "Turret traverse speed",
|
||||||
"gun_traverse_speed": "Gun traverse speed",
|
"gun_traverse_speed": "Gun traverse speed",
|
||||||
"health": "Health",
|
"health": "Health",
|
||||||
@@ -75,6 +126,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"game_modes": {
|
||||||
|
"title": "Game mode abilities"
|
||||||
|
},
|
||||||
|
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"tank": "Tank",
|
"tank": "Tank",
|
||||||
"characteristics": "Characteristics"
|
"characteristics": "Characteristics"
|
||||||
@@ -87,6 +142,10 @@
|
|||||||
"provisions": "Provisions",
|
"provisions": "Provisions",
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
|
"shells": "Shells",
|
||||||
|
"clear": "Clear",
|
||||||
|
"maximize": "Maximize",
|
||||||
|
"skills": "Crew skills",
|
||||||
"alternative": "Alt"
|
"alternative": "Alt"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -387,6 +446,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Development mode",
|
"dev_mode": "Development mode",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Language"
|
"language": "Language"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -424,53 +484,15 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"sort": {
|
"sort": {
|
||||||
"meta.none": "none",
|
|
||||||
"survivability.health": "health",
|
|
||||||
"survivability.viewRange": "view range",
|
|
||||||
"survivability.camouflageStill": "camouflage still",
|
|
||||||
"survivability.camouflageMoving": "camouflage moving",
|
|
||||||
"survivability.camouflageShooting": "camouflage shooting",
|
|
||||||
"survivability.volume": "volume",
|
|
||||||
"survivability.length": "length",
|
|
||||||
"fire.dpm": "standard DPM",
|
|
||||||
"fire.dpmPremium": "premium DPM",
|
|
||||||
"fire.reload": "reload",
|
|
||||||
"fire.caliber": "caliber",
|
|
||||||
"fire.standardPenetration": "standard penetration",
|
|
||||||
"fire.premiumPenetration": "premium penetration",
|
|
||||||
"fire.tertiaryPenetration": "tertiary penetration",
|
|
||||||
"fire.damage": "damage",
|
|
||||||
"fire.moduleDamage": "module damage",
|
|
||||||
"fire.shellVelocity": "shell velocity",
|
|
||||||
"fire.shellRange": "shell range",
|
|
||||||
"fire.shellCapacity": "shell capacity",
|
|
||||||
"fire.aimTime": "aim time",
|
|
||||||
"fire.dispersionStill": "dispersion still",
|
|
||||||
"fire.dispersionMoving": "dispersion moving",
|
|
||||||
"fire.gunDepression": "gun depression",
|
|
||||||
"fire.gunElevation": "gun elevation",
|
|
||||||
"maneuverability.forwardsSpeed": "forwards speed",
|
|
||||||
"maneuverability.backwardsSpeed": "backwards speed",
|
|
||||||
"maneuverability.power": "power",
|
|
||||||
"maneuverability.powerToWeight": "power to weight",
|
|
||||||
"maneuverability.weight": "weight",
|
|
||||||
"maneuverability.traverseSpeed": "traverse speed"
|
|
||||||
},
|
|
||||||
"sort_dropdown": {
|
|
||||||
"label": "Sort",
|
"label": "Sort",
|
||||||
"groups": {
|
|
||||||
"sort": {
|
"none": "None",
|
||||||
"no_sort": "No sort",
|
"by": "Sort by",
|
||||||
"label": "Sort by",
|
|
||||||
"fire": "Fire",
|
"direction": {
|
||||||
"maneuverability": "Maneuverability",
|
"label": "Direction",
|
||||||
"survivability": "Survivability"
|
"ascending": "Ascending",
|
||||||
},
|
"descending": "Descending"
|
||||||
"direction": {
|
|
||||||
"label": "Direction",
|
|
||||||
"ascending": "Ascending",
|
|
||||||
"descending": "Descending"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Modo de desarrollador",
|
"dev_mode": "Modo de desarrollador",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Idioma"
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
"face_hug": "Cara a cara",
|
"face_hug": "Cara a cara",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "Suave",
|
"hullTraverseSoftTerrain": "Suave",
|
||||||
"health": "Salud",
|
"health": "Salud",
|
||||||
"fireChance": "Probabilidad de incendio",
|
"fireChance": "Probabilidad de incendio",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "Rango de visión",
|
"viewRange": "Rango de visión",
|
||||||
"camouflageStill": "Quieto",
|
"camouflageStill": "Quieto",
|
||||||
"camouflageMoving": "En movimiento",
|
"camouflageMoving": "En movimiento",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Paramètres",
|
"title": "Paramètres",
|
||||||
"description": "Modifier les paramètres de BlitzKit",
|
"description": "Modifier les paramètres de BlitzKit",
|
||||||
"dev_mode": "Mode développement",
|
"dev_mode": "Mode développement",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Langue"
|
"language": "Langue"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
"face_hug": "Face hug",
|
"face_hug": "Face hug",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "Soft",
|
"hullTraverseSoftTerrain": "Soft",
|
||||||
"health": "Health",
|
"health": "Health",
|
||||||
"fireChance": "Fire chance",
|
"fireChance": "Fire chance",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "View range",
|
"viewRange": "View range",
|
||||||
"camouflageStill": "Still",
|
"camouflageStill": "Still",
|
||||||
"camouflageMoving": "Moving",
|
"camouflageMoving": "Moving",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "開発者モード",
|
"dev_mode": "開発者モード",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "言語 / Language"
|
"language": "言語 / Language"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "ハルダウン",
|
"hull_down": "ハルダウン",
|
||||||
"face_hug": "密着",
|
"face_hug": "密着",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "軟弱地",
|
"hullTraverseSoftTerrain": "軟弱地",
|
||||||
"health": "耐久値",
|
"health": "耐久値",
|
||||||
"fireChance": "火災発生率",
|
"fireChance": "火災発生率",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "視認範囲",
|
"viewRange": "視認範囲",
|
||||||
"camouflageStill": "静止時",
|
"camouflageStill": "静止時",
|
||||||
"camouflageMoving": "移動時",
|
"camouflageMoving": "移動時",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Development mode",
|
"dev_mode": "Development mode",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Language"
|
"language": "Language"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Hull down",
|
"hull_down": "Hull down",
|
||||||
"face_hug": "Face hug",
|
"face_hug": "Face hug",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "Soft",
|
"hullTraverseSoftTerrain": "Soft",
|
||||||
"health": "Health",
|
"health": "Health",
|
||||||
"fireChance": "Fire chance",
|
"fireChance": "Fire chance",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "View range",
|
"viewRange": "View range",
|
||||||
"camouflageStill": "Still",
|
"camouflageStill": "Still",
|
||||||
"camouflageMoving": "Moving",
|
"camouflageMoving": "Moving",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change BlitzKit settings",
|
"description": "Change BlitzKit settings",
|
||||||
"dev_mode": "Modo de desenvolvimento",
|
"dev_mode": "Modo de desenvolvimento",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Idioma"
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Casco para baixo",
|
"hull_down": "Casco para baixo",
|
||||||
"face_hug": "Abraço facial",
|
"face_hug": "Abraço facial",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "Macio",
|
"hullTraverseSoftTerrain": "Macio",
|
||||||
"health": "Vida",
|
"health": "Vida",
|
||||||
"fireChance": "Chance de incêndio",
|
"fireChance": "Chance de incêndio",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "Alcance de visão",
|
"viewRange": "Alcance de visão",
|
||||||
"camouflageStill": "Parado",
|
"camouflageStill": "Parado",
|
||||||
"camouflageMoving": "Em movimento",
|
"camouflageMoving": "Em movimento",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Настройки",
|
"title": "Настройки",
|
||||||
"description": "Изменить настройки BlitzKit",
|
"description": "Изменить настройки BlitzKit",
|
||||||
"dev_mode": "Режим разработчика",
|
"dev_mode": "Режим разработчика",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Язык"
|
"language": "Язык"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "За холмом",
|
"hull_down": "За холмом",
|
||||||
"face_hug": "В упор",
|
"face_hug": "В упор",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "На мягком грунте",
|
"hullTraverseSoftTerrain": "На мягком грунте",
|
||||||
"health": "Прочность",
|
"health": "Прочность",
|
||||||
"fireChance": "Вероятность возгорания",
|
"fireChance": "Вероятность возгорания",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "Радиус обзора",
|
"viewRange": "Радиус обзора",
|
||||||
"camouflageStill": "Когда неподвижен",
|
"camouflageStill": "Когда неподвижен",
|
||||||
"camouflageMoving": "В движении",
|
"camouflageMoving": "В движении",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "Налаштування",
|
"title": "Налаштування",
|
||||||
"description": "Змінити налаштування BlitzKit ",
|
"description": "Змінити налаштування BlitzKit ",
|
||||||
"dev_mode": "Режим розробника",
|
"dev_mode": "Режим розробника",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "Мова"
|
"language": "Мова"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "Від башти",
|
"hull_down": "Від башти",
|
||||||
"face_hug": "Клінч",
|
"face_hug": "Клінч",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "Soft",
|
"hullTraverseSoftTerrain": "Soft",
|
||||||
"health": "Здоров'я",
|
"health": "Здоров'я",
|
||||||
"fireChance": "Fire chance",
|
"fireChance": "Fire chance",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "View range",
|
"viewRange": "View range",
|
||||||
"camouflageStill": "Нерухомо",
|
"camouflageStill": "Нерухомо",
|
||||||
"camouflageMoving": "У русі",
|
"camouflageMoving": "У русі",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@
|
|||||||
"title": "设置",
|
"title": "设置",
|
||||||
"description": "更改 BlitzKit 设置",
|
"description": "更改 BlitzKit 设置",
|
||||||
"dev_mode": "开发者模式",
|
"dev_mode": "开发者模式",
|
||||||
|
"auto_load_models": "Auto-load models",
|
||||||
"language": "语言"
|
"language": "语言"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
@@ -412,6 +413,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
"load_model": "Load model",
|
||||||
"poses": {
|
"poses": {
|
||||||
"hull_down": "俯角视角",
|
"hull_down": "俯角视角",
|
||||||
"face_hug": "顶牛视角",
|
"face_hug": "顶牛视角",
|
||||||
@@ -621,6 +623,7 @@
|
|||||||
"hullTraverseSoftTerrain": "水面上车转速率",
|
"hullTraverseSoftTerrain": "水面上车转速率",
|
||||||
"health": "血量",
|
"health": "血量",
|
||||||
"fireChance": "发动机受击时着火概率",
|
"fireChance": "发动机受击时着火概率",
|
||||||
|
"trackRepairTime": "Track repair time",
|
||||||
"viewRange": "视野范围",
|
"viewRange": "视野范围",
|
||||||
"camouflageStill": "静止隐蔽",
|
"camouflageStill": "静止隐蔽",
|
||||||
"camouflageMoving": "移动时",
|
"camouflageMoving": "移动时",
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ let regionI = 0;
|
|||||||
|
|
||||||
console.log("Starting discovery loop...");
|
console.log("Starting discovery loop...");
|
||||||
|
|
||||||
atMaxRate(async ({ _break: break1 }) => {
|
await atMaxRate(async ({ _break: break1 }) => {
|
||||||
if (regions.length === 0) {
|
if (regions.length === 0) {
|
||||||
console.log("All regions exhausted, exiting discovery loop...");
|
console.log("All regions exhausted, exiting discovery loop...");
|
||||||
return break1();
|
return break1();
|
||||||
@@ -166,18 +166,19 @@ atMaxRate(async ({ _break: break1 }) => {
|
|||||||
fields
|
fields
|
||||||
}&account_id=${accountIds}`;
|
}&account_id=${accountIds}`;
|
||||||
|
|
||||||
atMaxRate(async ({ _break: break2 }) => {
|
if (idRange.length < MAX_IDS_PER_CALL) {
|
||||||
if (idRange.length < MAX_IDS_PER_CALL) {
|
console.log(
|
||||||
console.log(
|
`Removing region ${region.domain} removed due to ID range exhaustion...`,
|
||||||
`Removing region ${region.domain} removed due to ID range exhaustion...`,
|
);
|
||||||
);
|
regions.splice(regionI, 1);
|
||||||
regions.splice(regionI, 1);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
await atMaxRate(async ({ _break: break2 }) => {
|
||||||
const data = (await response.json()) as QuickInfo;
|
const data = await fetch(url)
|
||||||
|
.then((response) => response.json() as Promise<QuickInfo>)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
if (data.status !== "ok") {
|
if (data === null || data.status !== "ok") {
|
||||||
console.warn("Failed API call; trying again...");
|
console.warn("Failed API call; trying again...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ interface MethodContext {
|
|||||||
let lastT = 0;
|
let lastT = 0;
|
||||||
const maxDelta = 1000 / API_RATE;
|
const maxDelta = 1000 / API_RATE;
|
||||||
const DELTA_BUFFER = 0;
|
const DELTA_BUFFER = 0;
|
||||||
export async function atMaxRate(method: (context: MethodContext) => void) {
|
export async function atMaxRate(
|
||||||
|
method: (context: MethodContext) => void | Promise<void>,
|
||||||
|
) {
|
||||||
let isBroken = false;
|
let isBroken = false;
|
||||||
|
|
||||||
const context: MethodContext = {
|
const context: MethodContext = {
|
||||||
@@ -39,7 +41,7 @@ export async function atMaxRate(method: (context: MethodContext) => void) {
|
|||||||
await sleep(maxDelta - delta + DELTA_BUFFER);
|
await sleep(maxDelta - delta + DELTA_BUFFER);
|
||||||
}
|
}
|
||||||
|
|
||||||
method(context);
|
await method(context);
|
||||||
lastT = t;
|
lastT = t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -16,6 +16,11 @@
|
|||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&[data-disabled="true"] {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
&[data-size="minor"] {
|
&[data-size="minor"] {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
@@ -81,5 +86,9 @@
|
|||||||
&[data-variant="surface"],
|
&[data-variant="surface"],
|
||||||
&[data-variant="outline"] {
|
&[data-variant="outline"] {
|
||||||
outline: var(--space-1) solid var(--accent-6);
|
outline: var(--space-1) solid var(--accent-6);
|
||||||
|
|
||||||
|
&[data-high-contrast="true"] {
|
||||||
|
outline: var(--space-1) solid var(--accent-11);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ export interface ButtonProps extends ComponentProps<"button"> {
|
|||||||
radius?: `${1 | 2 | 3}` | "max";
|
radius?: `${1 | 2 | 3}` | "max";
|
||||||
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
|
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
|
||||||
size?: "regular" | "minor" | "major";
|
size?: "regular" | "minor" | "major";
|
||||||
highContrast?: boolean; // TODO: add high contrast
|
highContrast?: boolean;
|
||||||
|
|
||||||
listChild?: boolean;
|
listChild?: boolean;
|
||||||
parentListChild?: boolean;
|
parentListChild?: boolean;
|
||||||
@@ -39,6 +39,8 @@ export function Button({
|
|||||||
|
|
||||||
listChild,
|
listChild,
|
||||||
parentListChild,
|
parentListChild,
|
||||||
|
disabled,
|
||||||
|
highContrast,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +53,9 @@ export function Button({
|
|||||||
data-size={size}
|
data-size={size}
|
||||||
data-list-child={listChild}
|
data-list-child={listChild}
|
||||||
data-parent-list-child={parentListChild}
|
data-parent-list-child={parentListChild}
|
||||||
|
data-high-contrast={highContrast}
|
||||||
|
data-disabled={disabled}
|
||||||
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
.control {
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
accent-color: var(--accent-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&,
|
||||||
|
* {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useId, type ReactNode } from "react";
|
||||||
|
import { Text } from "../Text";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
export interface CheckboxProps {
|
||||||
|
checked: boolean;
|
||||||
|
onCheckedChange(checked: boolean): void;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Checkbox({
|
||||||
|
checked,
|
||||||
|
onCheckedChange,
|
||||||
|
children,
|
||||||
|
}: CheckboxProps) {
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.checkbox}>
|
||||||
|
<input
|
||||||
|
className={styles.control}
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onChange={(event) => onCheckedChange(event.target.checked)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children && (
|
||||||
|
<label htmlFor={id}>
|
||||||
|
<Text lowContrast>{children}</Text>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { alias, availableProvisions, permanentSkills } from "@blitzkit/core";
|
import {
|
||||||
|
alias,
|
||||||
|
availableProvisions,
|
||||||
|
MAX_SKILL_LEVEL,
|
||||||
|
permanentSkills,
|
||||||
|
} from "@blitzkit/core";
|
||||||
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
||||||
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
|
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
@@ -153,7 +158,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
CompareEphemeral.mutate((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||||
draft.crewSkills[skill] = 7;
|
draft.crewSkills[skill] = MAX_SKILL_LEVEL;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { alias, permanentSkills, romanize } from "@blitzkit/core";
|
import {
|
||||||
|
alias,
|
||||||
|
MAX_SKILL_LEVEL,
|
||||||
|
permanentSkills,
|
||||||
|
romanize,
|
||||||
|
} from "@blitzkit/core";
|
||||||
import { Flex } from "@radix-ui/themes";
|
import { Flex } from "@radix-ui/themes";
|
||||||
import { api } from "../api/dynamic";
|
import { api } from "../api/dynamic";
|
||||||
import { GenericTankComponentButton } from "./ModuleButtons/GenericTankComponentButton";
|
import { GenericTankComponentButton } from "./ModuleButtons/GenericTankComponentButton";
|
||||||
@@ -34,10 +39,10 @@ export function CrewSkillManager({
|
|||||||
const draft = { ...skillLevels };
|
const draft = { ...skillLevels };
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
draft[skill] = draft[skill] === 0 ? 7 : 0;
|
draft[skill] = draft[skill] === 0 ? MAX_SKILL_LEVEL : 0;
|
||||||
} else {
|
} else {
|
||||||
draft[skill] = level - 1;
|
draft[skill] = level - 1;
|
||||||
if (draft[skill] < 0) draft[skill] = 7;
|
if (draft[skill] < 0) draft[skill] = MAX_SKILL_LEVEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(draft);
|
onChange(draft);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { alias, SEARCH_KEYS, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
import { alias, romanize, SEARCH_KEYS } from "@blitzkit/core";
|
||||||
import { literals } from "@blitzkit/i18n";
|
import { literals } from "@blitzkit/i18n";
|
||||||
import { TankDefinition } from "@blitzkit/protos";
|
import { TankDefinition } from "@blitzkit/protos";
|
||||||
import {
|
import {
|
||||||
@@ -23,10 +23,12 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import fuzzysort from "fuzzysort";
|
import fuzzysort from "fuzzysort";
|
||||||
import { debounce, times } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { api } from "../api/dynamic";
|
import { api } from "../api/dynamic";
|
||||||
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||||
|
import { awaitableTiers } from "../core/awaitables/tiers";
|
||||||
|
import { api } from "../core/blitzkit/api";
|
||||||
import { useLocale } from "../hooks/useLocale";
|
import { useLocale } from "../hooks/useLocale";
|
||||||
import { Guess, GuessState } from "../stores/guess";
|
import { Guess, GuessState } from "../stores/guess";
|
||||||
import { classIcons } from "./ClassIcon";
|
import { classIcons } from "./ClassIcon";
|
||||||
@@ -34,9 +36,10 @@ import { SearchResults } from "./SearchResults";
|
|||||||
|
|
||||||
const { go } = fuzzysort;
|
const { go } = fuzzysort;
|
||||||
|
|
||||||
const [tankNames, tankDefinitions] = await Promise.all([
|
const [tankNames, tankDefinitions, TIERS] = await Promise.all([
|
||||||
awaitableTankNames,
|
awaitableTankNames,
|
||||||
api.tanks(),
|
api.tankDefinitions(),
|
||||||
|
awaitableTiers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ids = Object.keys(tankDefinitions.tanks);
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
@@ -160,7 +163,7 @@ export function Guesser() {
|
|||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<Icon width="1em" height="1em" />
|
<Icon width="1em" height="1em" />
|
||||||
{TIER_ROMAN_NUMERALS[result.tier]}
|
{romanize(result.tier)}
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -189,8 +192,7 @@ export function Guesser() {
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
{times(10, (index) => {
|
{TIERS.map((tier) => {
|
||||||
const tier = 10 - index;
|
|
||||||
const isSelected = tiers.includes(tier);
|
const isSelected = tiers.includes(tier);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -213,7 +215,7 @@ export function Guesser() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{literals(strings.website.tools.guess.tier, {
|
{literals(strings.website.tools.guess.tier, {
|
||||||
tier: TIER_ROMAN_NUMERALS[tier],
|
tier: romanize(tier),
|
||||||
})}
|
})}
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export function Lighting({ hasPbr }: Props) {
|
|||||||
blur={2 ** 1}
|
blur={2 ** 1}
|
||||||
resolution={2 ** 9}
|
resolution={2 ** 9}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
|
renderOrder={1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{animate && (
|
{animate && (
|
||||||
|
|||||||
@@ -1,3 +1,27 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
|
import { getBestLocale } from "../core/i18n/getBestLocale";
|
||||||
|
|
||||||
|
const pathname = window.location.pathname;
|
||||||
|
const hasPreference = localStorage.getItem("preferred-locale") !== null;
|
||||||
|
const bestLocale = getBestLocale();
|
||||||
|
const currentLocale =
|
||||||
|
locales.supported.find(
|
||||||
|
({ locale }) =>
|
||||||
|
pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
|
||||||
|
)?.locale ?? locales.default;
|
||||||
|
|
||||||
|
if (
|
||||||
|
bestLocale !== currentLocale &&
|
||||||
|
(hasPreference || currentLocale === locales.default)
|
||||||
|
) {
|
||||||
|
const rawPath =
|
||||||
|
currentLocale === locales.default
|
||||||
|
? pathname
|
||||||
|
: pathname.slice(`/${currentLocale}`.length) || "/";
|
||||||
|
|
||||||
|
window.location.pathname = `${
|
||||||
|
bestLocale === locales.default ? "" : `/${bestLocale}`
|
||||||
|
}${rawPath}`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
import { romanize } from "@blitzkit/core";
|
||||||
import type {
|
import type {
|
||||||
EngineDefinition,
|
EngineDefinition,
|
||||||
GunDefinition,
|
GunDefinition,
|
||||||
@@ -42,7 +42,7 @@ export function ModuleManager({
|
|||||||
<Flex gap="1" wrap="wrap">
|
<Flex gap="1" wrap="wrap">
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="turret"
|
module="turret"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[modules.turret.tier]}
|
discriminator={romanize(modules.turret.tier)}
|
||||||
onClick={() => setTurretMenuOpen(true)}
|
onClick={() => setTurretMenuOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export function ModuleManager({
|
|||||||
<ModuleButton
|
<ModuleButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
module="turret"
|
module="turret"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
|
discriminator={romanize(turret.tier)}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
{unwrap(turret.name!)}
|
{unwrap(turret.name!)}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
@@ -83,7 +83,7 @@ export function ModuleManager({
|
|||||||
|
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="gun"
|
module="gun"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[modules.gun.tier]}
|
discriminator={romanize(modules.gun.tier)}
|
||||||
onClick={() => setGunMenuOpen(true)}
|
onClick={() => setGunMenuOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export function ModuleManager({
|
|||||||
<ModuleButton
|
<ModuleButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
module="gun"
|
module="gun"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
discriminator={romanize(gun.tier)}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
{unwrap(gun.name!)}
|
{unwrap(gun.name!)}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
@@ -123,7 +123,7 @@ export function ModuleManager({
|
|||||||
|
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="engine"
|
module="engine"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[modules.engine.tier]}
|
discriminator={romanize(modules.engine.tier)}
|
||||||
onClick={() => setEngineMenuOpen(true)}
|
onClick={() => setEngineMenuOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ export function ModuleManager({
|
|||||||
<ModuleButton
|
<ModuleButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
module="engine"
|
module="engine"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[engine.tier]}
|
discriminator={romanize(engine.tier)}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
{unwrap(engine.name!)}
|
{unwrap(engine.name!)}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
@@ -162,7 +162,7 @@ export function ModuleManager({
|
|||||||
|
|
||||||
<ModuleButton
|
<ModuleButton
|
||||||
module="chassis"
|
module="chassis"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[modules.track.tier]}
|
discriminator={romanize(modules.track.tier)}
|
||||||
onClick={() => setTrackMenuOpen(true)}
|
onClick={() => setTrackMenuOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ export function ModuleManager({
|
|||||||
<ModuleButton
|
<ModuleButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
module="chassis"
|
module="chassis"
|
||||||
discriminator={TIER_ROMAN_NUMERALS[track.tier]}
|
discriminator={romanize(track.tier)}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
{unwrap(track.name!)}
|
{unwrap(track.name!)}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { TankSearchFilters } from "./TankSearchFilters";
|
|||||||
const tankDefinitions = await api.tanks();
|
const tankDefinitions = await api.tanks();
|
||||||
|
|
||||||
const POOL_HEIGHT = "16rem";
|
const POOL_HEIGHT = "16rem";
|
||||||
|
const FILTERS_WIDTH = "32rem";
|
||||||
|
|
||||||
export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
|
export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const [largeWarningOpen, setLargeWarningOpen] = useState(false);
|
const [largeWarningOpen, setLargeWarningOpen] = useState(false);
|
||||||
@@ -80,14 +81,20 @@ export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
<Flex justify="center" width="100%" align="center" px="4">
|
<Flex justify="center" width="100%" align="center" px="4">
|
||||||
<Flex
|
<Flex
|
||||||
flexGrow="1"
|
flexGrow="1"
|
||||||
gap="9"
|
gap="6"
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
maxWidth="80rem"
|
maxWidth="80rem"
|
||||||
direction={{ initial: "column-reverse", md: "row" }}
|
direction={{ initial: "column-reverse", md: "row" }}
|
||||||
>
|
>
|
||||||
<Flex direction="column" gap="4" align="center">
|
<Flex
|
||||||
<TankSearchFilters />
|
direction="column"
|
||||||
|
gap="4"
|
||||||
|
align="center"
|
||||||
|
flexShrink="0"
|
||||||
|
width={{ initial: "100%", md: FILTERS_WIDTH }}
|
||||||
|
>
|
||||||
|
<FilterControl />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function RecentlyViewedTanks() {
|
|||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const by = TankSort.use((state) => state.by);
|
const by = TankSort.use((state) => state.by);
|
||||||
|
|
||||||
if (recentlyViewed.length === 0 || by !== "meta.none" || hasFilters) {
|
if (recentlyViewed.length === 0 || by !== null || hasFilters) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
.scroller {
|
||||||
|
--scroller-curtain-color: var(--gray-1);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
padding-bottom: var(--space-4);
|
||||||
|
|
||||||
|
transition: scrollbar-color var(--duration-brief);
|
||||||
|
scrollbar-color: transparent transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scrollbar-color: var(--gray-7) var(--gray-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
.bar-wrapper {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-orientation="horizontal"] {
|
||||||
|
.curtain {
|
||||||
|
width: min(25%, var(--space-7));
|
||||||
|
height: calc(100% - var(--space-3) - 2 * var(--space-1));
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
&[data-side="start"] {
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--scroller-curtain-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-side="end"] {
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
var(--scroller-curtain-color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-orientation="vertical"] {
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curtain {
|
||||||
|
width: calc(100% - var(--space-3) - 2 * var(--space-1));
|
||||||
|
height: min(25%, var(--space-7));
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
&[data-side="start"] {
|
||||||
|
top: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--scroller-curtain-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-side="end"] {
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
transparent,
|
||||||
|
var(--scroller-curtain-color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.curtain {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect, useRef, type ComponentProps } from "react";
|
||||||
|
import { clamp } from "three/src/math/MathUtils.js";
|
||||||
|
import { classNames } from "../../ui/classNames";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
interface ScrollerProps extends ComponentProps<"div"> {
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Scroller({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
...props
|
||||||
|
}: ScrollerProps) {
|
||||||
|
const content = useRef<HTMLDivElement>(null!);
|
||||||
|
const curtainStart = useRef<HTMLDivElement>(null!);
|
||||||
|
const curtainEnd = useRef<HTMLDivElement>(null!);
|
||||||
|
|
||||||
|
const isHorizontal = orientation === "horizontal";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function syncCurtains() {
|
||||||
|
const maxScroll = isHorizontal
|
||||||
|
? content.current.scrollWidth - content.current.clientWidth
|
||||||
|
: content.current.scrollHeight - content.current.clientHeight;
|
||||||
|
const scrollPosition = isHorizontal
|
||||||
|
? content.current.scrollLeft
|
||||||
|
: content.current.scrollTop;
|
||||||
|
|
||||||
|
if (maxScroll <= 0) {
|
||||||
|
curtainStart.current.style.opacity = "0";
|
||||||
|
curtainEnd.current.style.opacity = "0";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimension = isHorizontal
|
||||||
|
? content.current.clientWidth
|
||||||
|
: content.current.clientHeight;
|
||||||
|
const fadeDistance = Math.min(60, dimension * 0.15);
|
||||||
|
const startOpacity = clamp(scrollPosition / fadeDistance, 0, 1);
|
||||||
|
const endOpacity = clamp(
|
||||||
|
(maxScroll - scrollPosition) / fadeDistance,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
curtainStart.current.style.opacity = `${startOpacity}`;
|
||||||
|
curtainEnd.current.style.opacity = `${endOpacity}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncCurtains();
|
||||||
|
|
||||||
|
window.addEventListener("resize", syncCurtains);
|
||||||
|
content.current.addEventListener("scroll", syncCurtains);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", syncCurtains);
|
||||||
|
content.current.removeEventListener("scroll", syncCurtains);
|
||||||
|
};
|
||||||
|
}, [orientation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.scroller, className)}
|
||||||
|
{...props}
|
||||||
|
data-orientation={orientation}
|
||||||
|
>
|
||||||
|
<div className={styles.content} ref={content}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={curtainStart} data-side="start" className={styles.curtain} />
|
||||||
|
<div ref={curtainEnd} data-side="end" className={styles.curtain} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
.section {
|
.section {
|
||||||
|
padding: var(--space-5);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
.options {
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
|
||||||
|
&[data-size="minor"] {
|
||||||
|
.option {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
outline: var(--outline-focus);
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
|
||||||
|
transition: var(--duration-brief);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-variant="solid"] {
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
background-color: var(--accent-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-variant="underline"] {
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
background: linear-gradient(transparent, var(--accent-a2));
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
border-bottom: var(--space-1) solid var(--accent-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
|
||||||
|
color: var(--gray-11);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:checked) {
|
||||||
|
color: var(--gray-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:focus-visible) {
|
||||||
|
outline: 2px solid var(--accent-8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
export interface SegmentedControlOption {
|
||||||
|
value: string;
|
||||||
|
label: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SegmentedControlProps {
|
||||||
|
options: readonly SegmentedControlOption[];
|
||||||
|
value: string;
|
||||||
|
onValueChange(value: string): void;
|
||||||
|
size?: "regular" | "minor";
|
||||||
|
variant?: "solid" | "underline";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SegmentedControl({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
size = "regular",
|
||||||
|
variant = "solid",
|
||||||
|
}: SegmentedControlProps) {
|
||||||
|
const selectedIndex = options.findIndex((option) => option.value === value);
|
||||||
|
const segmentWidth = 100 / options.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.options}
|
||||||
|
role="radiogroup"
|
||||||
|
data-size={size}
|
||||||
|
data-variant={variant}
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${options.length}, minmax(0, 1fr))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.indicator}
|
||||||
|
style={{
|
||||||
|
left: `${Math.max(selectedIndex, 0) * segmentWidth}%`,
|
||||||
|
width: `${segmentWidth}%`,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{options.map((option) => (
|
||||||
|
<label className={styles.option} key={option.value}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={option.value}
|
||||||
|
checked={option.value === value}
|
||||||
|
onChange={() => onValueChange(option.value)}
|
||||||
|
/>
|
||||||
|
<span className={styles.label}>{option.label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -128,19 +128,30 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
? shotStatusColors[shot.out.status]
|
? shotStatusColors[shot.out.status]
|
||||||
: undefined;
|
: undefined;
|
||||||
const inTitleColor = shotStatusColors[shot.in.status];
|
const inTitleColor = shotStatusColors[shot.in.status];
|
||||||
|
const hasOutLayers = shot.out !== undefined && shot.out.layers.length > 0;
|
||||||
|
const penetrationChance = shot.penetrationChance !== undefined && (
|
||||||
|
<Text color="gray" weight="bold">
|
||||||
|
{literals(strings.common.units.percentage, {
|
||||||
|
value: (shot.penetrationChance * 100).toFixed(0),
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" {...props}>
|
<Card variant="classic" {...props}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Flex direction="column" gap="1">
|
<Flex direction="column" gap="1">
|
||||||
<Text color={inTitleColor} weight="bold">
|
<Flex justify="between" align="center" gap="4">
|
||||||
{literals(
|
<Text color={inTitleColor} weight="bold">
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
{literals(
|
||||||
shot.in.status
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
],
|
.status[shot.in.status],
|
||||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{!hasOutLayers && penetrationChance}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<table
|
<table
|
||||||
style={{
|
style={{
|
||||||
@@ -204,13 +215,17 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Inset>
|
</Inset>
|
||||||
|
|
||||||
<Text color={outTitleColor} weight="bold">
|
<Flex justify="between" align="center" gap="4">
|
||||||
{literals(
|
<Text color={outTitleColor} weight="bold">
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
{literals(
|
||||||
.status[shot.out.status],
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
.status[shot.out.status],
|
||||||
)}
|
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
||||||
</Text>
|
)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{penetrationChance}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
{shot.out.layers.map((layer, index) => {
|
{shot.out.layers.map((layer, index) => {
|
||||||
|
|||||||
@@ -56,6 +56,36 @@ export type ArmorUserData = {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PENETRATION_RANDOMIZATION = 0.05;
|
||||||
|
|
||||||
|
function resolvePenetrationChance(shoot: (scale: number) => Shot | null) {
|
||||||
|
function penetrates(scale: number) {
|
||||||
|
const shot = shoot(scale);
|
||||||
|
|
||||||
|
return (shot?.out ?? shot?.in)?.status === "penetration";
|
||||||
|
}
|
||||||
|
|
||||||
|
let low = 1 - PENETRATION_RANDOMIZATION;
|
||||||
|
let high = 1 + PENETRATION_RANDOMIZATION;
|
||||||
|
|
||||||
|
if (penetrates(low)) return 1;
|
||||||
|
if (!penetrates(high)) return 0;
|
||||||
|
|
||||||
|
for (let iteration = 0; iteration < 16; iteration++) {
|
||||||
|
const middle = (low + high) / 2;
|
||||||
|
|
||||||
|
if (penetrates(middle)) {
|
||||||
|
high = middle;
|
||||||
|
} else {
|
||||||
|
low = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(1 + PENETRATION_RANDOMIZATION - high) / (2 * PENETRATION_RANDOMIZATION)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const omitMaterial = new MeshBasicMaterial({
|
const omitMaterial = new MeshBasicMaterial({
|
||||||
colorWrite: false,
|
colorWrite: false,
|
||||||
depthWrite: true,
|
depthWrite: true,
|
||||||
@@ -82,6 +112,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
intersections: Intersection[],
|
intersections: Intersection[],
|
||||||
allowRicochet: boolean,
|
allowRicochet: boolean,
|
||||||
remainingPenetrationInput?: number,
|
remainingPenetrationInput?: number,
|
||||||
|
penetrationScale = 1,
|
||||||
) => {
|
) => {
|
||||||
const protagonist = getDuel("protagonist", DuelModule.Tank);
|
const protagonist = getDuel("protagonist", DuelModule.Tank);
|
||||||
const antagonist = getDuel("antagonist", DuelModule.Tank);
|
const antagonist = getDuel("antagonist", DuelModule.Tank);
|
||||||
@@ -119,6 +150,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
Tankopedia.state.protagonist.equipment,
|
Tankopedia.state.protagonist.equipment,
|
||||||
);
|
);
|
||||||
const penetration =
|
const penetration =
|
||||||
|
penetrationScale *
|
||||||
shell.penetration!.near *
|
shell.penetration!.near *
|
||||||
resolvePenetrationCoefficient(
|
resolvePenetrationCoefficient(
|
||||||
hasCalibratedShells,
|
hasCalibratedShells,
|
||||||
@@ -361,6 +393,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
ricochetIntersections,
|
ricochetIntersections,
|
||||||
false,
|
false,
|
||||||
remainingPenetration,
|
remainingPenetration,
|
||||||
|
penetrationScale,
|
||||||
);
|
);
|
||||||
|
|
||||||
shot.in.status = "ricochet";
|
shot.in.status = "ricochet";
|
||||||
@@ -416,6 +449,16 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
const shot = shoot(event.point, event.intersections, true)!;
|
const shot = shoot(event.point, event.intersections, true)!;
|
||||||
|
|
||||||
|
shot.penetrationChance = resolvePenetrationChance((scale) =>
|
||||||
|
shoot(
|
||||||
|
event.point,
|
||||||
|
event.intersections,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = shot;
|
draft.shot = shot;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,4 +40,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.discriminator {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ export const TankCard = (props: MaybeSkeletonComponentProps<TankCardProps>) => {
|
|||||||
|
|
||||||
{props.discriminator && (
|
{props.discriminator && (
|
||||||
<Text lowContrast align="center">
|
<Text lowContrast align="center">
|
||||||
{props.discriminator}
|
<div className={styles.discriminator}>{props.discriminator}</div>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -62,11 +62,17 @@ export function TankModel() {
|
|||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
|
|
||||||
if (material.map) material.map.offset.y += offset;
|
const textures = new Set([
|
||||||
if (material.aoMap) material.aoMap.offset.y += offset;
|
material.map,
|
||||||
if (material.normalMap) material.normalMap.offset.y += offset;
|
material.aoMap,
|
||||||
if (material.roughnessMap) material.roughnessMap.offset.y += offset;
|
material.normalMap,
|
||||||
if (material.metalnessMap) material.metalnessMap.offset.y += offset;
|
material.roughnessMap,
|
||||||
|
material.metalnessMap,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const texture of textures) {
|
||||||
|
if (texture) texture.offset.y += offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import {
|
|
||||||
normalizeBoundingBox,
|
|
||||||
resolveDpm,
|
|
||||||
unionBoundingBox,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import type { TankDefinition } from "@blitzkit/protos";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { api } from "../../../api/dynamic";
|
|
||||||
import { resolveReload } from "../../../core/blitzkit/resolveReload";
|
|
||||||
import { TankSort } from "../../../stores/tankopediaSort";
|
|
||||||
import { TankCard } from "../../TankCard";
|
|
||||||
|
|
||||||
interface TankSearchCardProps {
|
|
||||||
tank: TankDefinition;
|
|
||||||
onSelect?: (tank: TankDefinition) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelDefinitions = await api.models();
|
|
||||||
|
|
||||||
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|
||||||
const by = TankSort.use((state) => state.by);
|
|
||||||
const discriminator = useMemo(() => {
|
|
||||||
if (by.startsWith("meta")) return undefined;
|
|
||||||
|
|
||||||
const turret = tank.turrets.at(-1)!;
|
|
||||||
const gun = turret.guns.at(-1)!;
|
|
||||||
const shell0 = gun.shells[0];
|
|
||||||
const shell1 = gun.shells[1];
|
|
||||||
const shell2 = gun.shells[2];
|
|
||||||
const tracks = tank.tracks.at(-1)!;
|
|
||||||
const engine = tank.engines.at(-1)!;
|
|
||||||
const tankModelDefinition = modelDefinitions.models[tank.id];
|
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
|
||||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
|
||||||
|
|
||||||
switch (by) {
|
|
||||||
case "fire.aimTime":
|
|
||||||
return gun.aim_time.toFixed(2);
|
|
||||||
case "fire.caliber":
|
|
||||||
return shell0.caliber.toFixed(0);
|
|
||||||
case "fire.damage":
|
|
||||||
return shell0.armor_damage.toFixed(0);
|
|
||||||
case "fire.moduleDamage":
|
|
||||||
return shell0.module_damage.toFixed(0);
|
|
||||||
case "fire.dispersionMoving":
|
|
||||||
return tracks.dispersion_move.toFixed(3);
|
|
||||||
case "fire.dispersionStill":
|
|
||||||
return gun.dispersion_base.toFixed(3);
|
|
||||||
case "fire.dpm":
|
|
||||||
return resolveDpm(gun, shell0).toFixed(0);
|
|
||||||
case "fire.dpmPremium":
|
|
||||||
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : "--";
|
|
||||||
case "fire.reload":
|
|
||||||
return resolveReload(gun).toFixed(2);
|
|
||||||
case "fire.standardPenetration":
|
|
||||||
return shell0?.penetration!.near.toFixed(0);
|
|
||||||
case "fire.premiumPenetration":
|
|
||||||
return shell1 ? shell1?.penetration!.near.toFixed(0) : "--";
|
|
||||||
case "fire.tertiaryPenetration":
|
|
||||||
return shell1 ? shell2?.penetration!.near.toFixed(0) : "--";
|
|
||||||
case "fire.shellVelocity":
|
|
||||||
return shell0.velocity.toFixed(0);
|
|
||||||
case "fire.shellCapacity":
|
|
||||||
return gun.shell_capacity.toFixed(0);
|
|
||||||
case "fire.shellRange":
|
|
||||||
return shell0.range.toFixed(0);
|
|
||||||
case "fire.gunDepression":
|
|
||||||
return (
|
|
||||||
gunModelDefinition.pitch!.max +
|
|
||||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
|
||||||
).toFixed(1);
|
|
||||||
case "fire.gunElevation":
|
|
||||||
return (
|
|
||||||
-gunModelDefinition.pitch!.min -
|
|
||||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
|
||||||
).toFixed(1);
|
|
||||||
case "maneuverability.forwardsSpeed":
|
|
||||||
return tank.speed_forwards.toFixed(0);
|
|
||||||
case "maneuverability.backwardsSpeed":
|
|
||||||
return tank.speed_backwards.toFixed(0);
|
|
||||||
case "maneuverability.power":
|
|
||||||
return engine.power.toFixed(0);
|
|
||||||
case "maneuverability.powerToWeight":
|
|
||||||
return (
|
|
||||||
(1000 * engine.power) /
|
|
||||||
(tank.weight +
|
|
||||||
engine.weight +
|
|
||||||
tracks.weight +
|
|
||||||
turret.weight +
|
|
||||||
gun.weight)
|
|
||||||
).toFixed(1);
|
|
||||||
case "maneuverability.weight":
|
|
||||||
return (
|
|
||||||
(tank.weight +
|
|
||||||
engine.weight +
|
|
||||||
tracks.weight +
|
|
||||||
turret.weight +
|
|
||||||
gun.weight) /
|
|
||||||
1000
|
|
||||||
).toFixed(1);
|
|
||||||
case "maneuverability.traverseSpeed":
|
|
||||||
return tracks.traverse_speed.toFixed(1);
|
|
||||||
case "survivability.health":
|
|
||||||
return (tank.health + turret.health).toFixed(0);
|
|
||||||
case "survivability.viewRange":
|
|
||||||
return turret.view_range.toFixed(0);
|
|
||||||
case "survivability.camouflageStill":
|
|
||||||
return (tank.camouflage_still * 100).toFixed(0);
|
|
||||||
case "survivability.camouflageMoving":
|
|
||||||
return (tank.camouflage_moving * 100).toFixed(0);
|
|
||||||
case "survivability.camouflageShooting":
|
|
||||||
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
|
|
||||||
case "survivability.volume": {
|
|
||||||
const dimensions = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
tankModelDefinition.bounding_box!,
|
|
||||||
turretModelDefinition.bounding_box!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "survivability.length": {
|
|
||||||
const bounds = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
tankModelDefinition.bounding_box!,
|
|
||||||
turretModelDefinition.bounding_box!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [by]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TankCard
|
|
||||||
key={tank.id}
|
|
||||||
onTankSelect={onSelect}
|
|
||||||
tank={tank}
|
|
||||||
discriminator={discriminator}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,37 @@
|
|||||||
import { CaretDownIcon } from "@radix-ui/react-icons";
|
import { CaretDownIcon } from "@radix-ui/react-icons";
|
||||||
|
import { characteristicsGroups } from "../../../config/characteristicsGroups";
|
||||||
import { useStrings } from "../../../hooks/useStrings";
|
import { useStrings } from "../../../hooks/useStrings";
|
||||||
import type {
|
import { TankopediaSortDirection } from "../../../stores/tankopediaPersistent";
|
||||||
TankopediaSortBy,
|
|
||||||
TankopediaSortDirection,
|
|
||||||
} from "../../../stores/tankopediaPersistent";
|
|
||||||
import { TankSort } from "../../../stores/tankopediaSort";
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
|
import { sortableCharacteristics } from "../../../tankopedia/sortableCharacteristics";
|
||||||
|
import type { CharacteristicName } from "../../../types/characteristics";
|
||||||
import { Button } from "../../Button";
|
import { Button } from "../../Button";
|
||||||
import { DropdownMenu } from "../../DropdownMenu";
|
import { DropdownMenu } from "../../DropdownMenu";
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
by: TankopediaSortBy;
|
by: CharacteristicName;
|
||||||
}
|
|
||||||
|
|
||||||
interface ItemProps {
|
|
||||||
by: TankopediaSortBy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Item({ by }: ItemProps) {
|
function Item({ by }: ItemProps) {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const _by = TankSort.use((state) => state.by);
|
const selectedBy = TankSort.use((state) => state.by);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={_by === by}
|
checked={selectedBy === by}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankSort.mutate((draft) => {
|
TankSort.mutate((draft) => {
|
||||||
draft.by = by;
|
draft.by = by;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{strings.website.common.tank_search.sort[by]}
|
{strings.tanks.characteristics.items[by]}
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sort() {
|
export function Sort() {
|
||||||
|
const by = TankSort.use((state) => state.by);
|
||||||
const direction = TankSort.use((state) => state.direction);
|
const direction = TankSort.use((state) => state.direction);
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
|
|
||||||
@@ -42,103 +39,79 @@ export function Sort() {
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<Button variant="surface" color="gray">
|
<Button variant="surface" color="gray">
|
||||||
{strings.website.common.tank_search.sort_dropdown.label}
|
{strings.website.common.tank_search.sort.label}
|
||||||
<CaretDownIcon />
|
<CaretDownIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content align="end">
|
<DropdownMenu.Content align="end">
|
||||||
<DropdownMenu.Label>
|
<DropdownMenu.Label>
|
||||||
{strings.website.common.tank_search.sort_dropdown.groups.sort.label}
|
{strings.website.common.tank_search.sort.direction.label}
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
|
|
||||||
<Item by="meta.none" />
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={by === null || !sortableCharacteristics.has(by)}
|
||||||
|
onClick={() => {
|
||||||
|
TankSort.mutate((draft) => {
|
||||||
|
draft.by = null;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.website.common.tank_search.sort.none}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
<DropdownMenu.Sub>
|
{characteristicsGroups.map((group) => {
|
||||||
<DropdownMenu.SubTrigger>
|
const items = group.order.filter(
|
||||||
{strings.website.common.tank_search.sort_dropdown.groups.sort.fire}
|
(item) => "name" in item && sortableCharacteristics.has(item.name),
|
||||||
</DropdownMenu.SubTrigger>
|
);
|
||||||
<DropdownMenu.SubContent>
|
|
||||||
<Item by="fire.dpm" />
|
|
||||||
<Item by="fire.dpmPremium" />
|
|
||||||
<Item by="fire.reload" />
|
|
||||||
<Item by="fire.caliber" />
|
|
||||||
<Item by="fire.standardPenetration" />
|
|
||||||
<Item by="fire.premiumPenetration" />
|
|
||||||
<Item by="fire.tertiaryPenetration" />
|
|
||||||
<Item by="fire.damage" />
|
|
||||||
<Item by="fire.moduleDamage" />
|
|
||||||
<Item by="fire.shellVelocity" />
|
|
||||||
<Item by="fire.shellRange" />
|
|
||||||
<Item by="fire.shellCapacity" />
|
|
||||||
<Item by="fire.aimTime" />
|
|
||||||
<Item by="fire.dispersionStill" />
|
|
||||||
<Item by="fire.dispersionMoving" />
|
|
||||||
<Item by="fire.gunDepression" />
|
|
||||||
<Item by="fire.gunElevation" />
|
|
||||||
</DropdownMenu.SubContent>
|
|
||||||
</DropdownMenu.Sub>
|
|
||||||
|
|
||||||
<DropdownMenu.Sub>
|
return (
|
||||||
<DropdownMenu.SubTrigger>
|
<DropdownMenu.Sub key={group.name}>
|
||||||
{
|
<DropdownMenu.SubTrigger>
|
||||||
strings.website.common.tank_search.sort_dropdown.groups.sort
|
{
|
||||||
.maneuverability
|
(
|
||||||
}
|
strings.tanks.characteristics.titles as Record<
|
||||||
</DropdownMenu.SubTrigger>
|
string,
|
||||||
<DropdownMenu.SubContent>
|
string
|
||||||
<Item by="maneuverability.forwardsSpeed" />
|
>
|
||||||
<Item by="maneuverability.backwardsSpeed" />
|
)[group.name]
|
||||||
<Item by="maneuverability.power" />
|
}
|
||||||
<Item by="maneuverability.powerToWeight" />
|
</DropdownMenu.SubTrigger>
|
||||||
<Item by="maneuverability.weight" />
|
|
||||||
<Item by="maneuverability.traverseSpeed" />
|
|
||||||
</DropdownMenu.SubContent>
|
|
||||||
</DropdownMenu.Sub>
|
|
||||||
|
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.SubContent>
|
||||||
<DropdownMenu.SubTrigger>
|
{items.map((item) => {
|
||||||
{
|
if (!("name" in item)) return null;
|
||||||
strings.website.common.tank_search.sort_dropdown.groups.sort
|
|
||||||
.survivability
|
return <Item key={item.name} by={item.name} />;
|
||||||
}
|
})}
|
||||||
</DropdownMenu.SubTrigger>
|
</DropdownMenu.SubContent>
|
||||||
<DropdownMenu.SubContent>
|
</DropdownMenu.Sub>
|
||||||
<Item by="survivability.health" />
|
);
|
||||||
<Item by="survivability.viewRange" />
|
})}
|
||||||
<Item by="survivability.camouflageStill" />
|
|
||||||
<Item by="survivability.camouflageMoving" />
|
|
||||||
<Item by="survivability.camouflageShooting" />
|
|
||||||
<Item by="survivability.volume" />
|
|
||||||
<Item by="survivability.length" />
|
|
||||||
</DropdownMenu.SubContent>
|
|
||||||
</DropdownMenu.Sub>
|
|
||||||
|
|
||||||
<DropdownMenu.Label>
|
<DropdownMenu.Label>
|
||||||
{
|
{strings.website.common.tank_search.sort.direction.label}
|
||||||
strings.website.common.tank_search.sort_dropdown.groups.direction
|
|
||||||
.label
|
|
||||||
}
|
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.RadioGroup
|
<DropdownMenu.RadioGroup
|
||||||
value={direction}
|
value={direction}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
if (
|
||||||
|
value !== TankopediaSortDirection.Ascending &&
|
||||||
|
value !== TankopediaSortDirection.Descending
|
||||||
|
) {
|
||||||
|
throw new Error(`Unknown tankopedia sort direction: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
TankSort.mutate((draft) => {
|
TankSort.mutate((draft) => {
|
||||||
draft.direction = value as TankopediaSortDirection;
|
draft.direction = value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.RadioItem value="ascending">
|
<DropdownMenu.RadioItem value={TankopediaSortDirection.Ascending}>
|
||||||
{
|
{strings.website.common.tank_search.sort.direction.ascending}
|
||||||
strings.website.common.tank_search.sort_dropdown.groups.direction
|
|
||||||
.ascending
|
|
||||||
}
|
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
<DropdownMenu.RadioItem value="descending">
|
<DropdownMenu.RadioItem value={TankopediaSortDirection.Descending}>
|
||||||
{
|
{strings.website.common.tank_search.sort.direction.descending}
|
||||||
strings.website.common.tank_search.sort_dropdown.groups.direction
|
|
||||||
.descending
|
|
||||||
}
|
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
import { romanize } from "@blitzkit/core";
|
||||||
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
||||||
import { times } from "lodash-es";
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
import { awaitableTiers } from "../../../core/awaitables/tiers";
|
||||||
import { TankFilters } from "../../../stores/tankFilters";
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { TankSort } from "../../../stores/tankopediaSort";
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
|
|
||||||
|
const TIERS = await awaitableTiers;
|
||||||
|
|
||||||
export const TierFilter = memo(() => {
|
export const TierFilter = memo(() => {
|
||||||
const by = TankSort.use((state) => state.by);
|
const by = TankSort.use((state) => state.by);
|
||||||
const search = TankFilters.use((state) => state.search);
|
const search = TankFilters.use((state) => state.search);
|
||||||
const tiers = TankFilters.use((state) => state.tiers);
|
const tiers = TankFilters.use((state) => state.tiers);
|
||||||
|
|
||||||
if (by !== "meta.none" || search) return null;
|
if (by !== null || search) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
@@ -19,8 +23,7 @@ export const TierFilter = memo(() => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: "var(--radius-full)" }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
>
|
>
|
||||||
{times(10, (index) => {
|
{TIERS.map((tier) => {
|
||||||
const tier = 10 - index;
|
|
||||||
const selected = tiers.includes(tier);
|
const selected = tiers.includes(tier);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,7 +43,7 @@ export const TierFilter = memo(() => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
<Text size="2">{romanize(tier)}</Text>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { metaSortTank, SEARCH_KEYS } from "@blitzkit/core";
|
||||||
metaSortTank,
|
|
||||||
normalizeBoundingBox,
|
|
||||||
resolveDpm,
|
|
||||||
SEARCH_KEYS,
|
|
||||||
unionBoundingBox,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import type { TankDefinition } from "@blitzkit/protos";
|
import type { TankDefinition } from "@blitzkit/protos";
|
||||||
import fuzzysort from "fuzzysort";
|
import fuzzysort from "fuzzysort";
|
||||||
@@ -12,16 +6,26 @@ import { uniq } from "lodash-es";
|
|||||||
import { memo, useMemo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
import usePromise from "react-promise-suspense";
|
import usePromise from "react-promise-suspense";
|
||||||
import { api } from "../../api/dynamic";
|
import { api } from "../../api/dynamic";
|
||||||
|
import { characteristics } from "../../config/characteristics";
|
||||||
|
import { characteristicRenderConfigs } from "../../config/characteristicsGroups";
|
||||||
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
||||||
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
||||||
import { resolveReload } from "../../core/blitzkit/resolveReload";
|
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
|
||||||
|
import { computeDefaultCharacteristics } from "../../hooks/useCharacteristics";
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useStrings } from "../../hooks/useStrings";
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
import { App } from "../../stores/app";
|
import { App } from "../../stores/app";
|
||||||
import { TankFilters } from "../../stores/tankFilters";
|
import { TankFilters } from "../../stores/tankFilters";
|
||||||
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
import {
|
||||||
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
|
TankopediaPersistent,
|
||||||
|
TankopediaSortDirection,
|
||||||
|
} from "../../stores/tankopediaPersistent";
|
||||||
import { TankSort } from "../../stores/tankopediaSort";
|
import { TankSort } from "../../stores/tankopediaSort";
|
||||||
|
import { sortableCharacteristics } from "../../tankopedia/sortableCharacteristics";
|
||||||
|
import {
|
||||||
|
CharacteristicType,
|
||||||
|
type ComputedCharacteristics,
|
||||||
|
} from "../../types/characteristics";
|
||||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import { Callout } from "../Callout";
|
import { Callout } from "../Callout";
|
||||||
import { ExperimentIcon } from "../ExperimentIcon";
|
import { ExperimentIcon } from "../ExperimentIcon";
|
||||||
@@ -44,386 +48,170 @@ export type TankSearchProps = MaybeSkeletonComponentProps & {
|
|||||||
onSelectAll?: (tanks: TankDefinition[]) => void;
|
onSelectAll?: (tanks: TankDefinition[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
|
const [gameDefinitions, tankDefinitions, tankNames] = await Promise.all([
|
||||||
await Promise.all([
|
api.game(),
|
||||||
api.game(),
|
api.tanks(),
|
||||||
api.models(),
|
awaitableTankNames,
|
||||||
api.tanks(),
|
]);
|
||||||
awaitableTankNames,
|
const tankDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||||
]);
|
|
||||||
|
|
||||||
export const TankSearch = memo<TankSearchProps>(
|
export const TankSearch = memo<TankSearchProps>(
|
||||||
({ compact, onSelect, onSelectAll, skeleton }) => {
|
({ compact, onSelect, onSelectAll, skeleton }) => {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
const wargaming = App.use((state) => state.logins.wargaming);
|
const wargaming = App.use((state) => state.logins.wargaming);
|
||||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
|
||||||
const tankopediaSort = TankSort.use();
|
const tankopediaSort = TankSort.use();
|
||||||
const tankFilters = TankFilters.use();
|
const tankFilters = TankFilters.use();
|
||||||
|
|
||||||
|
const defaultCharacteristics = useMemo(() => {
|
||||||
|
const computed = new Map<number, ComputedCharacteristics>();
|
||||||
|
|
||||||
|
for (const tank of tankDefinitionsArray) {
|
||||||
|
computed.set(tank.id, computeDefaultCharacteristics(tank));
|
||||||
|
}
|
||||||
|
|
||||||
|
return computed;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filtered = usePromise(
|
const filtered = usePromise(
|
||||||
() => {
|
async () => {
|
||||||
if (tankFilters.search === null) {
|
let filtered = tankDefinitionsArray;
|
||||||
return filterTanks(
|
|
||||||
tankFilters,
|
console.log("A");
|
||||||
awaitedTanksDefinitionsArray,
|
|
||||||
wargaming?.id,
|
if (tankopediaSort.by !== null) {
|
||||||
);
|
console.log("B", filtered.length);
|
||||||
} else {
|
|
||||||
return Promise.resolve(
|
filtered = filtered.filter((tank) =>
|
||||||
fuzzysort
|
defaultCharacteristics.get(tank.id)!.has(tankopediaSort.by!),
|
||||||
.go(tankFilters.search, tankNames, { keys: SEARCH_KEYS })
|
|
||||||
.map((result) => tankDefinitions.tanks[result.obj.id]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("C", filtered.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tankFilters.search === null) {
|
||||||
|
filtered = await filterTanks(tankFilters, filtered, wargaming?.id);
|
||||||
|
} else {
|
||||||
|
filtered = fuzzysort
|
||||||
|
.go(tankFilters.search, tankNames, { keys: SEARCH_KEYS })
|
||||||
|
.map((result) => tankDefinitions.tanks[result.obj.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
},
|
},
|
||||||
// react-promise-suspense has awful type annotations
|
// react-promise-suspense has awful type annotations
|
||||||
[tankFilters] as any,
|
[tankFilters, tankopediaSort.by] as any,
|
||||||
);
|
);
|
||||||
|
|
||||||
const sorted = useMemo(() => {
|
const sorted = useMemo(() => {
|
||||||
if (tankFilters.search === null) {
|
if (tankFilters.search !== null) return filtered;
|
||||||
let sorted: TankDefinition[];
|
|
||||||
|
|
||||||
switch (tankopediaSort.by) {
|
if (
|
||||||
case "meta.none":
|
tankopediaSort.by === null ||
|
||||||
sorted = metaSortTank(filtered, gameDefinitions);
|
!sortableCharacteristics.has(tankopediaSort.by)
|
||||||
break;
|
) {
|
||||||
|
const byMeta = metaSortTank([...filtered], gameDefinitions);
|
||||||
|
|
||||||
case "survivability.health": {
|
return tankopediaSort.direction === TankopediaSortDirection.Ascending
|
||||||
sorted = filtered.sort((a, b) => {
|
? byMeta
|
||||||
const aHealth = a.health + a.turrets.at(-1)!.health;
|
: byMeta.reverse();
|
||||||
const bHealth = b.health + b.turrets.at(-1)!.health;
|
}
|
||||||
|
|
||||||
|
const values = new Map<number, number | undefined>();
|
||||||
|
const by = tankopediaSort.by;
|
||||||
|
const characteristic = characteristics[by];
|
||||||
|
|
||||||
|
for (const tank of filtered) {
|
||||||
|
const computedCharacteristics = defaultCharacteristics.get(tank.id)!;
|
||||||
|
if (!computedCharacteristics.has(by)) {
|
||||||
|
values.set(tank.id, undefined);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = computedCharacteristics.get(by);
|
||||||
|
|
||||||
|
let sortValue: number | undefined;
|
||||||
|
|
||||||
|
switch (characteristic.type) {
|
||||||
|
case CharacteristicType.Number: {
|
||||||
|
sortValue = value as number;
|
||||||
|
|
||||||
return aHealth - bHealth;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "survivability.viewRange":
|
case CharacteristicType.NumberList: {
|
||||||
sorted = filtered.sort(
|
const values = value as number[];
|
||||||
(a, b) =>
|
if (values.length === 0) {
|
||||||
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range,
|
sortValue = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (const item of values) {
|
||||||
|
sum += item;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortValue = sum / values.length;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsortable characteristic type ${characteristic.type}`,
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
|
|
||||||
case "survivability.camouflageStill":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) => a.camouflage_still - b.camouflage_still,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "survivability.camouflageMoving":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) => a.camouflage_moving - b.camouflage_moving,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "survivability.camouflageShooting":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.camouflage_still *
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
|
|
||||||
b.camouflage_still *
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "survivability.volume":
|
|
||||||
sorted = filtered.sort((a, b) => {
|
|
||||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
|
||||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
|
||||||
const aTurretModelDefinition =
|
|
||||||
aTankModelDefinition.turrets[a.turrets.at(-1)!.id];
|
|
||||||
const bTurretModelDefinition =
|
|
||||||
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
|
|
||||||
const aSize = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
aTankModelDefinition.bounding_box!,
|
|
||||||
aTurretModelDefinition.bounding_box!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const bSize = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
bTankModelDefinition.bounding_box!,
|
|
||||||
bTurretModelDefinition.bounding_box!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const aVolume = aSize.x * aSize.y * aSize.z;
|
|
||||||
const bVolume = bSize.x * bSize.y * bSize.z;
|
|
||||||
|
|
||||||
return aVolume - bVolume;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "survivability.length":
|
|
||||||
sorted = filtered.sort((a, b) => {
|
|
||||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
|
||||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
|
||||||
const aTurretModelDefinition =
|
|
||||||
aTankModelDefinition.turrets[a.turrets.at(-1)!.id];
|
|
||||||
const bTurretModelDefinition =
|
|
||||||
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
|
|
||||||
const aSize = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
aTankModelDefinition.bounding_box!,
|
|
||||||
aTurretModelDefinition.bounding_box!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const bSize = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
bTankModelDefinition.bounding_box!,
|
|
||||||
bTurretModelDefinition.bounding_box!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
|
||||||
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
|
||||||
|
|
||||||
return aLength - bLength;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.dpm":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
resolveDpm(
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
|
||||||
) -
|
|
||||||
resolveDpm(
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.dpmPremium":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
|
||||||
? resolveDpm(
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
|
||||||
)
|
|
||||||
: 0) -
|
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
|
||||||
? resolveDpm(
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
|
||||||
)
|
|
||||||
: 0),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.reload":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
|
|
||||||
resolveReload(b.turrets.at(-1)!.guns.at(-1)!),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.caliber":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.standardPenetration":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.premiumPenetration":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ??
|
|
||||||
0) -
|
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration!.near ??
|
|
||||||
0),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.tertiaryPenetration":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ??
|
|
||||||
0) -
|
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration!.near ??
|
|
||||||
0),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.damage":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.moduleDamage":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.shellVelocity":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.shellRange":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].range -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.shellCapacity":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shell_capacity -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.aimTime":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.aim_time -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.aim_time,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.dispersionStill":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.dispersion_base -
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.dispersionMoving":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.tracks.at(-1)!.dispersion_move -
|
|
||||||
b.tracks.at(-1)!.dispersion_move,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.gunDepression":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
|
||||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max +
|
|
||||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
|
||||||
0) -
|
|
||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
|
||||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.max -
|
|
||||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
|
||||||
0),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fire.gunElevation":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
|
||||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min +
|
|
||||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
|
||||||
0) -
|
|
||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
|
||||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch!.min -
|
|
||||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
|
||||||
0),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "maneuverability.forwardsSpeed":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) => a.speed_forwards - b.speed_forwards,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "maneuverability.backwardsSpeed":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) => a.speed_backwards - b.speed_backwards,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "maneuverability.power":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "maneuverability.powerToWeight":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.engines.at(-1)!.power /
|
|
||||||
(a.weight +
|
|
||||||
a.engines.at(-1)!.weight +
|
|
||||||
a.tracks.at(-1)!.weight +
|
|
||||||
a.turrets.at(-1)!.weight +
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.weight) -
|
|
||||||
b.engines.at(-1)!.power /
|
|
||||||
(b.weight +
|
|
||||||
b.engines.at(-1)!.weight +
|
|
||||||
b.tracks.at(-1)!.weight +
|
|
||||||
b.turrets.at(-1)!.weight +
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "maneuverability.weight":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.weight +
|
|
||||||
a.engines.at(-1)!.weight +
|
|
||||||
a.tracks.at(-1)!.weight +
|
|
||||||
a.turrets.at(-1)!.weight +
|
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.weight -
|
|
||||||
(b.weight +
|
|
||||||
b.engines.at(-1)!.weight +
|
|
||||||
b.tracks.at(-1)!.weight +
|
|
||||||
b.turrets.at(-1)!.weight +
|
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "maneuverability.traverseSpeed":
|
|
||||||
sorted = filtered.sort(
|
|
||||||
(a, b) =>
|
|
||||||
a.tracks.at(-1)!.traverse_speed -
|
|
||||||
b.tracks.at(-1)!.traverse_speed,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankopediaSort.direction === "ascending"
|
values.set(tank.id, sortValue);
|
||||||
? sorted
|
|
||||||
: sorted.reverse();
|
|
||||||
} else {
|
|
||||||
return filtered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const direction =
|
||||||
|
tankopediaSort.direction === TankopediaSortDirection.Ascending ? 1 : -1;
|
||||||
|
|
||||||
|
return [...filtered].sort((a, b) => {
|
||||||
|
const aValue = values.get(a.id);
|
||||||
|
const bValue = values.get(b.id);
|
||||||
|
|
||||||
|
if (aValue === undefined) return bValue === undefined ? 0 : 1;
|
||||||
|
if (bValue === undefined) return -1;
|
||||||
|
|
||||||
|
if (aValue === bValue) return 0;
|
||||||
|
|
||||||
|
return (aValue < bValue ? -1 : 1) * direction;
|
||||||
|
});
|
||||||
}, [tankFilters, tankopediaSort]);
|
}, [tankFilters, tankopediaSort]);
|
||||||
|
|
||||||
const data = sorted.map((tank) => ({
|
console.log("V", sorted.length);
|
||||||
key: `${tank.id}`,
|
|
||||||
tank,
|
const sortConfig =
|
||||||
onTankSelect: onSelect,
|
tankopediaSort.by !== null &&
|
||||||
}));
|
sortableCharacteristics.has(tankopediaSort.by)
|
||||||
|
? tankopediaSort.by
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const renderCharacteristic = useCharacteristicRenderer();
|
||||||
|
|
||||||
|
const data = sorted.map((tank) => {
|
||||||
|
const tankDefaultCharacteristics = defaultCharacteristics.get(tank.id)!;
|
||||||
|
const shouldRenderDiscriminator =
|
||||||
|
tankopediaSort.by !== null &&
|
||||||
|
tankDefaultCharacteristics.has(tankopediaSort.by);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `${tank.id}`,
|
||||||
|
tank,
|
||||||
|
discriminator: shouldRenderDiscriminator
|
||||||
|
? renderCharacteristic(
|
||||||
|
tankDefaultCharacteristics.get(tankopediaSort.by!)!,
|
||||||
|
characteristicRenderConfigs.get(tankopediaSort.by!)!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTankSelect: onSelect,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex className={styles.container} column gap="4">
|
<Flex className={styles.container} column gap="4">
|
||||||
@@ -470,16 +258,16 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tankopediaSort.by !== "meta.none" && (
|
{sortConfig && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
{literals(strings.website.common.tank_search.sorting_by, {
|
{literals(strings.website.common.tank_search.sorting_by, {
|
||||||
name: strings.website.common.tank_search.sort[
|
name: (
|
||||||
tankopediaSort.by
|
strings.tanks.characteristics.items as Record<
|
||||||
],
|
string,
|
||||||
|
string
|
||||||
|
>
|
||||||
|
)[sortConfig],
|
||||||
})}
|
})}
|
||||||
{SORT_UNITS[tankopediaSort.by] === undefined
|
|
||||||
? ""
|
|
||||||
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
|
||||||
, {tankopediaSort.direction}
|
, {tankopediaSort.direction}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { alias, gunTypeOrder, romanize, treeTypeOrder } from "@blitzkit/core";
|
||||||
alias,
|
|
||||||
gunTypeOrder,
|
|
||||||
TIER_ROMAN_NUMERALS,
|
|
||||||
treeTypeOrder,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import { tankClassOrder } from "@blitzkit/core/src/config/tankClassOrder";
|
import { tankClassOrder } from "@blitzkit/core/src/config/tankClassOrder";
|
||||||
import { literals } from "@blitzkit/i18n";
|
import { literals } from "@blitzkit/i18n";
|
||||||
import locales from "@blitzkit/i18n/locales.json";
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
@@ -14,9 +9,9 @@ import {
|
|||||||
ResetIcon,
|
ResetIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { times } from "lodash-es";
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { api } from "../../api/dynamic";
|
import { api } from "../../api/dynamic";
|
||||||
|
import { awaitableTiers } from "../../core/awaitables/tiers";
|
||||||
import { useStrings } from "../../hooks/useStrings";
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
||||||
import { App } from "../../stores/app";
|
import { App } from "../../stores/app";
|
||||||
@@ -153,7 +148,7 @@ const TANK_TYPE_COLORS: Record<TankType, string> = {
|
|||||||
|
|
||||||
const MAX_ICONS = 4;
|
const MAX_ICONS = 4;
|
||||||
|
|
||||||
const TIERS = times(10, (i) => 10 - i);
|
const TIERS = await awaitableTiers;
|
||||||
|
|
||||||
export function TankSearchFilters() {
|
export function TankSearchFilters() {
|
||||||
return (
|
return (
|
||||||
@@ -203,7 +198,7 @@ function TiersFilter() {
|
|||||||
<Flex gap="1">
|
<Flex gap="1">
|
||||||
{tiers.slice(0, MAX_ICONS).map((tier) => (
|
{tiers.slice(0, MAX_ICONS).map((tier) => (
|
||||||
<Text size="minor" key={tier}>
|
<Text size="minor" key={tier}>
|
||||||
{TIER_ROMAN_NUMERALS[tier]}
|
{romanize(tier)}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -239,7 +234,7 @@ function TiersFilter() {
|
|||||||
key={tier}
|
key={tier}
|
||||||
>
|
>
|
||||||
{literals(strings.website.common.tank_search.tier, {
|
{literals(strings.website.common.tank_search.tier, {
|
||||||
tier: TIER_ROMAN_NUMERALS[tier],
|
tier: romanize(tier),
|
||||||
})}
|
})}
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
);
|
);
|
||||||
@@ -620,13 +615,7 @@ function IndividualShellFilter({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu.Root modal={false}>
|
<DropdownMenu.Root modal={false}>
|
||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<IconButton
|
<IconButton listChild size="minor" variant="surface" color="gray">
|
||||||
listChild
|
|
||||||
size="minor"
|
|
||||||
variant="surface"
|
|
||||||
color="gray"
|
|
||||||
highContrast
|
|
||||||
>
|
|
||||||
{shells[index] === null && (
|
{shells[index] === null && (
|
||||||
<Text lowContrast className={styles["shell-icon"]}>
|
<Text lowContrast className={styles["shell-icon"]}>
|
||||||
<MissingShellIcon width="1em" height="1em" />
|
<MissingShellIcon width="1em" height="1em" />
|
||||||
@@ -739,10 +728,10 @@ function OwnershipFilterInternal() {
|
|||||||
borderTopRightRadius: 0,
|
borderTopRightRadius: 0,
|
||||||
borderBottomRightRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
}}
|
}}
|
||||||
|
listChild
|
||||||
disabled={!wargaming}
|
disabled={!wargaming}
|
||||||
variant={showOwned ? "solid" : "surface"}
|
variant={showOwned ? "solid" : "surface"}
|
||||||
color={showOwned ? undefined : "gray"}
|
color={showOwned ? undefined : "gray"}
|
||||||
highContrast
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showOwned = !draft.showOwned;
|
draft.showOwned = !draft.showOwned;
|
||||||
@@ -759,10 +748,10 @@ function OwnershipFilterInternal() {
|
|||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
marginLeft: "-1px",
|
marginLeft: "-1px",
|
||||||
}}
|
}}
|
||||||
|
listChild
|
||||||
disabled={!wargaming}
|
disabled={!wargaming}
|
||||||
variant={showUnowned ? "solid" : "surface"}
|
variant={showUnowned ? "solid" : "surface"}
|
||||||
color={showUnowned ? undefined : "gray"}
|
color={showUnowned ? undefined : "gray"}
|
||||||
highContrast
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showUnowned = !draft.showUnowned;
|
draft.showUnowned = !draft.showUnowned;
|
||||||
@@ -789,7 +778,7 @@ function TestFilter() {
|
|||||||
}}
|
}}
|
||||||
variant={showNonTesting ? "solid" : "surface"}
|
variant={showNonTesting ? "solid" : "surface"}
|
||||||
color={showNonTesting ? undefined : "gray"}
|
color={showNonTesting ? undefined : "gray"}
|
||||||
highContrast
|
listChild
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showNonTesting = !draft.showNonTesting;
|
draft.showNonTesting = !draft.showNonTesting;
|
||||||
@@ -810,7 +799,7 @@ function TestFilter() {
|
|||||||
}}
|
}}
|
||||||
variant={showTesting ? "solid" : "surface"}
|
variant={showTesting ? "solid" : "surface"}
|
||||||
color={showTesting ? undefined : "gray"}
|
color={showTesting ? undefined : "gray"}
|
||||||
highContrast
|
listChild
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
TankFilters.mutate((draft) => {
|
TankFilters.mutate((draft) => {
|
||||||
draft.showTesting = !draft.showTesting;
|
draft.showTesting = !draft.showTesting;
|
||||||
|
|||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
|
||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
|
||||||
import { Flex, SegmentedControl, Text, type FlexProps } from "@radix-ui/themes";
|
|
||||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
|
||||||
import {
|
|
||||||
Tankopedia,
|
|
||||||
TankopediaRelativeAgainst,
|
|
||||||
} from "../../../../../../stores/tankopedia";
|
|
||||||
|
|
||||||
export function CompareOptions(props: FlexProps) {
|
|
||||||
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const { strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
gap="2"
|
|
||||||
justify="center"
|
|
||||||
direction={{ initial: "column", sm: "row" }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Text color="gray">
|
|
||||||
{strings.website.tools.tankopedia.characteristics.compare.label}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<SegmentedControl.Root
|
|
||||||
variant="classic"
|
|
||||||
size="1"
|
|
||||||
value={`${relativeAgainst}`}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Class}`}>
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.characteristics.compare.class,
|
|
||||||
{
|
|
||||||
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
|
||||||
class:
|
|
||||||
strings.common.tank_class_short_plural_lowercase[tank.class],
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</SegmentedControl.Item>
|
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.characteristics.compare.tier,
|
|
||||||
{ tier: TIER_ROMAN_NUMERALS[tank.tier] }
|
|
||||||
)}
|
|
||||||
</SegmentedControl.Item>
|
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
|
||||||
{strings.website.tools.tankopedia.characteristics.compare.all}
|
|
||||||
</SegmentedControl.Item>
|
|
||||||
</SegmentedControl.Root>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+4
-2
@@ -13,6 +13,7 @@ import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTra
|
|||||||
import { Var } from "../../../../../../../core/radix/var";
|
import { Var } from "../../../../../../../core/radix/var";
|
||||||
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 type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
|
import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { VisualizerCard } from "../VisualizerCard";
|
import { VisualizerCard } from "../VisualizerCard";
|
||||||
import { VisualizerCornerStat } from "../VisualizerCornerStat";
|
import { VisualizerCornerStat } from "../VisualizerCornerStat";
|
||||||
@@ -43,6 +44,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
|
|
||||||
const tankModel = modelDefinition.models[tank.id];
|
const tankModel = modelDefinition.models[tank.id];
|
||||||
const turretModel = tankModel.turrets[turret.id];
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
@@ -193,7 +195,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
>
|
>
|
||||||
{!skeleton && <FlexibilityCanvas />}
|
{!skeleton && modelRequested && <FlexibilityCanvas />}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
@@ -294,7 +296,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
opacity: 2 ** -3,
|
opacity: 2 ** -3,
|
||||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
backgroundImage: `url(/media/toys/armor-hash.png)`,
|
||||||
boxShadow: "inset 0 0 4rem 1rem black",
|
boxShadow: "inset 0 0 4rem 1rem black",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
-185
@@ -1,185 +0,0 @@
|
|||||||
import { createDefaultSkills } from "@blitzkit/core";
|
|
||||||
import { Flex, Progress, Text } from "@radix-ui/themes";
|
|
||||||
import { clamp } from "lodash-es";
|
|
||||||
import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
|
|
||||||
import { api } from "../../../../../../api/dynamic";
|
|
||||||
import {
|
|
||||||
tankCharacteristics,
|
|
||||||
type TankCharacteristics,
|
|
||||||
} from "../../../../../../core/blitzkit/tankCharacteristics";
|
|
||||||
import { tankToDuelMember } from "../../../../../../core/blitzkit/tankToDuelMember";
|
|
||||||
import { useDelta } from "../../../../../../hooks/useDelta";
|
|
||||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
|
||||||
import {
|
|
||||||
Tankopedia,
|
|
||||||
TankopediaRelativeAgainst,
|
|
||||||
} from "../../../../../../stores/tankopedia";
|
|
||||||
import { Info, type InfoProps } from "./Info";
|
|
||||||
|
|
||||||
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
|
||||||
stats: TankCharacteristics;
|
|
||||||
noRanking?: boolean;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
value: keyof TankCharacteristics;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
value: (tank: TankCharacteristics) => number | undefined;
|
|
||||||
name: ReactNode;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [
|
|
||||||
tankDefinitions,
|
|
||||||
provisionDefinitions,
|
|
||||||
equipmentDefinitions,
|
|
||||||
modelDefinitions,
|
|
||||||
skillDefinitions,
|
|
||||||
] = await Promise.all([
|
|
||||||
api.tanks(),
|
|
||||||
api.provisions(),
|
|
||||||
api.equipment(),
|
|
||||||
api.models(),
|
|
||||||
api.skills(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|
||||||
({ stats, indent, noRanking, deltaType, ...props }) => {
|
|
||||||
const { strings } = useLocale();
|
|
||||||
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
|
||||||
const uhWhatDoICallThisVariable =
|
|
||||||
typeof props.value === "function"
|
|
||||||
? props.value(stats)!
|
|
||||||
: (stats[props.value] as number);
|
|
||||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
|
||||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const shellIndex = Duel.use((state) =>
|
|
||||||
state.protagonist.gun.shells.indexOf(state.protagonist.shell),
|
|
||||||
);
|
|
||||||
const equipmentMatrix = Duel.use(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
const equalize = Duel.use((state) => state.equalize);
|
|
||||||
const others = useMemo(() => {
|
|
||||||
const defaultSkills = createDefaultSkills(skillDefinitions);
|
|
||||||
|
|
||||||
return Object.values(tankDefinitions.tanks)
|
|
||||||
.filter(
|
|
||||||
(tank) =>
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
|
||||||
tank.tier === protagonistTank.tier &&
|
|
||||||
tank.class === protagonistTank.class) ||
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
|
||||||
tank.tier === protagonistTank.tier) ||
|
|
||||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
|
||||||
)
|
|
||||||
.map((tank) => {
|
|
||||||
const member = tankToDuelMember(tank, provisionDefinitions);
|
|
||||||
|
|
||||||
if (member.gun.shells[shellIndex] === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tankCharacteristics(
|
|
||||||
{
|
|
||||||
applyDynamicArmor: false,
|
|
||||||
applyReactiveArmor: false,
|
|
||||||
applySpallLiner: false,
|
|
||||||
camouflage: member.camouflage,
|
|
||||||
consumables: member.consumables,
|
|
||||||
crewSkills: defaultSkills,
|
|
||||||
provisions: member.provisions,
|
|
||||||
engine: member.engine,
|
|
||||||
gun: member.gun,
|
|
||||||
equipmentMatrix: equipmentMatrix,
|
|
||||||
shell: member.gun.shells[shellIndex],
|
|
||||||
stockEngine: tank.engines[0],
|
|
||||||
stockGun: tank.turrets[0].guns[0],
|
|
||||||
stockTrack: tank.tracks[0],
|
|
||||||
stockTurret: tank.turrets[0],
|
|
||||||
tank,
|
|
||||||
track: member.track,
|
|
||||||
turret: member.turret,
|
|
||||||
assaultDistance: member.assaultDistance,
|
|
||||||
equalize,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
equipmentDefinitions,
|
|
||||||
provisionDefinitions,
|
|
||||||
tankModelDefinition: modelDefinitions.models[tank.id],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.filter((tank) => {
|
|
||||||
if (tank === undefined) return false;
|
|
||||||
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === "function"
|
|
||||||
? props.value(tank)!
|
|
||||||
: (tank[props.value] as number);
|
|
||||||
|
|
||||||
return othersValue !== undefined;
|
|
||||||
}) as TankCharacteristics[];
|
|
||||||
}, [relativeAgainst, shellIndex, equipmentMatrix, equalize]);
|
|
||||||
const betterTanks = others.filter((tank) => {
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === "function"
|
|
||||||
? props.value(tank)!
|
|
||||||
: (tank[props.value] as number);
|
|
||||||
|
|
||||||
if (othersValue === undefined) return false;
|
|
||||||
return deltaType === "lowerIsBetter"
|
|
||||||
? othersValue < uhWhatDoICallThisVariable
|
|
||||||
: othersValue > uhWhatDoICallThisVariable;
|
|
||||||
});
|
|
||||||
const goodness = (others.length - betterTanks.length) / others.length;
|
|
||||||
let color: ComponentProps<typeof Progress>["color"];
|
|
||||||
|
|
||||||
if (goodness <= 0.25) color = "red";
|
|
||||||
else if (goodness <= 0.5) color = "orange";
|
|
||||||
else if (goodness <= 0.75) color = "yellow";
|
|
||||||
else color = "green";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column">
|
|
||||||
<Info
|
|
||||||
deltaType={deltaType}
|
|
||||||
indent={indent}
|
|
||||||
name={
|
|
||||||
"name" in props
|
|
||||||
? props.name
|
|
||||||
: strings.website.tools.tankopedia.characteristics.values[
|
|
||||||
props.value
|
|
||||||
]
|
|
||||||
}
|
|
||||||
delta={delta}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{uhWhatDoICallThisVariable}
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
{!noRanking && (
|
|
||||||
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
|
||||||
<Progress
|
|
||||||
variant="soft"
|
|
||||||
size="1"
|
|
||||||
value={goodness * 100}
|
|
||||||
color={color}
|
|
||||||
style={{ height: "0.125rem", opacity: 0.5 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text color="gray" size="1">
|
|
||||||
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
|
||||||
{others.length}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
(a, b) =>
|
|
||||||
(typeof a.value === "function" ? a.value(a.stats) : a.stats[a.value]) ===
|
|
||||||
(typeof b.value === "function" ? b.value(b.stats) : b.stats[b.value]),
|
|
||||||
);
|
|
||||||
+1
-1
@@ -69,7 +69,7 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
|
|||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maskImage:
|
maskImage:
|
||||||
'url(/assets/images/tankopedia/visualizers/reload/shell.png)',
|
'url(/media/toys/shell.png)',
|
||||||
maskSize: 'contain',
|
maskSize: 'contain',
|
||||||
maskRepeat: 'no-repeat',
|
maskRepeat: 'no-repeat',
|
||||||
maskPosition: 'center',
|
maskPosition: 'center',
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ export function Target() {
|
|||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
'url(/assets/images/tankopedia/visualizers/reload/background.jpg)',
|
'url(/media/toys/background.jpg)',
|
||||||
backgroundSize: '25rem',
|
backgroundSize: '25rem',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
opacity: 2 ** -3,
|
opacity: 2 ** -3,
|
||||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
backgroundImage: `url(/media/toys/armor-hash.png)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
+8
@@ -24,6 +24,14 @@ export function Survivability({ stats }: StatsAcceptorProps) {
|
|||||||
decimals={0}
|
decimals={0}
|
||||||
value={(stats) => stats.fireChance * 100}
|
value={(stats) => stats.fireChance * 100}
|
||||||
/>
|
/>
|
||||||
|
{stats.trackRepairTime !== undefined && (
|
||||||
|
<InfoWithDelta
|
||||||
|
value="trackRepairTime"
|
||||||
|
stats={stats}
|
||||||
|
deltaType="lowerIsBetter"
|
||||||
|
decimals={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<InfoWithDelta value="viewRange" stats={stats} decimals={0} />
|
<InfoWithDelta value="viewRange" stats={stats} decimals={0} />
|
||||||
<ViewRangeVisualizer stats={stats} />
|
<ViewRangeVisualizer stats={stats} />
|
||||||
<Info
|
<Info
|
||||||
|
|||||||
+7
-13
@@ -28,23 +28,17 @@ export function Equalizer() {
|
|||||||
width="100%"
|
width="100%"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
borderRadius: "var(--radius-3)",
|
borderRadius: "var(--radius-3) 0 0 var(--radius-3)",
|
||||||
backgroundImage: `url(${alias("api", `/gamemodes/Insanity/banner.webp`)})`,
|
backgroundImage: `
|
||||||
backgroundSize: "cover",
|
linear-gradient(90deg, var(--black-a3), var(--gray-1)),
|
||||||
|
url(${alias("api", `/gamemodes/Insanity/banner.webp`)})
|
||||||
|
`,
|
||||||
|
backgroundSize: "auto, fit",
|
||||||
backgroundPosition: "50% 25%",
|
backgroundPosition: "50% 25%",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex align="center" width="100%" justify="between" p="4" style={{}}>
|
||||||
align="center"
|
|
||||||
width="100%"
|
|
||||||
justify="between"
|
|
||||||
p="4"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
"linear-gradient(90deg, var(--black-a3), var(--gray-1))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
textShadow: "0 0 var(--space-1) var(--black-a12)",
|
textShadow: "0 0 var(--space-1) var(--black-a12)",
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ import {
|
|||||||
alias,
|
alias,
|
||||||
formatCompact,
|
formatCompact,
|
||||||
tankIcon,
|
tankIcon,
|
||||||
TIER_ROMAN_NUMERALS,
|
romanize,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import {
|
import {
|
||||||
EngineDefinition,
|
EngineDefinition,
|
||||||
@@ -93,7 +93,7 @@ function ModuleButton({
|
|||||||
top: isTank ? 8 : 4,
|
top: isTank ? 8 : 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{TIER_ROMAN_NUMERALS[tier]}
|
{romanize(tier)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{unlock.cost!.value > 0 && (
|
{unlock.cost!.value > 0 && (
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { MAX_SKILL_LEVEL } from "@blitzkit/core";
|
||||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -59,7 +60,7 @@ export function Skills() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
draft.skills[skill] = 7;
|
draft.skills[skill] = MAX_SKILL_LEVEL;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
|
||||||
import { Box, Flex, Heading, Text } from "@radix-ui/themes";
|
|
||||||
import { api } from "../../api/dynamic";
|
|
||||||
import { useLocale } from "../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../stores/duel";
|
|
||||||
|
|
||||||
const [gameDefinitions, consumableDefinitions, provisionDefinitions] =
|
|
||||||
await Promise.all([api.game(), api.consumables(), api.provisions()]);
|
|
||||||
|
|
||||||
export function GameModeSection() {
|
|
||||||
const { unwrap, strings } = useLocale();
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const roles = Object.entries(tank.roles);
|
|
||||||
|
|
||||||
if (roles.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" gap="4" align="center">
|
|
||||||
<Heading size="6">
|
|
||||||
{strings.website.tools.tankopedia.game_modes.title}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Flex justify="center" gap="4" wrap="wrap" px="4">
|
|
||||||
{roles.map(([id, value]) => {
|
|
||||||
const gameMode = gameDefinitions.gameModes[id];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
key={id}
|
|
||||||
width="16rem"
|
|
||||||
height="20rem"
|
|
||||||
overflow="hidden"
|
|
||||||
direction="column"
|
|
||||||
style={{
|
|
||||||
borderRadius: "var(--radius-3)",
|
|
||||||
background: `url(${alias(
|
|
||||||
"api",
|
|
||||||
`/gamemodes/${id}/banner.webp`,
|
|
||||||
)})`,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
flexGrow="1"
|
|
||||||
style={{
|
|
||||||
background: `url(${alias(
|
|
||||||
"api",
|
|
||||||
`/gamemodes/${id}/banner.webp`,
|
|
||||||
)})`,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
p="3"
|
|
||||||
direction="column"
|
|
||||||
height="98px"
|
|
||||||
flexShrink="0"
|
|
||||||
style={{
|
|
||||||
backdropFilter: "blur(4rem)",
|
|
||||||
WebkitBackdropFilter: "blur(4rem)",
|
|
||||||
}}
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
>
|
|
||||||
<Text size="4" weight="bold">
|
|
||||||
{unwrap(gameMode.name!)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Flex direction="column">
|
|
||||||
{gameDefinitions.roles[value].provisions.map((id) => {
|
|
||||||
const provisions = provisionDefinitions.provisions[id];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text color="gray" wrap="nowrap" key={id}>
|
|
||||||
<Flex align="center" gap="1">
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
width: "1.5em",
|
|
||||||
height: "1.5em",
|
|
||||||
objectFit: "contain",
|
|
||||||
}}
|
|
||||||
src={alias("api", `/icons/provisions/${id}.webp`)}
|
|
||||||
alt={unwrap(provisions.name!)}
|
|
||||||
/>
|
|
||||||
{unwrap(provisions.name!)}
|
|
||||||
</Flex>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{gameDefinitions.roles[value].consumables.map((id) => {
|
|
||||||
const consumable = consumableDefinitions.consumables[id];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text color="gray" wrap="nowrap" key={id}>
|
|
||||||
<Flex align="center" gap="1">
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
width: "1.5em",
|
|
||||||
height: "1.5em",
|
|
||||||
objectFit: "contain",
|
|
||||||
}}
|
|
||||||
src={alias("api", `/icons/consumables/${id}.webp`)}
|
|
||||||
alt={unwrap(consumable.name!)}
|
|
||||||
/>
|
|
||||||
{unwrap(consumable.name!)}
|
|
||||||
</Flex>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+2
-2
@@ -31,11 +31,11 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
right={display === TankopediaDisplay.StaticArmor ? "0" : "-6rem"}
|
right="0"
|
||||||
top="50%"
|
top="50%"
|
||||||
mr="4"
|
mr="4"
|
||||||
style={{
|
style={{
|
||||||
transform: "translateY(-50%)",
|
transform: `translate(${display === TankopediaDisplay.StaticArmor ? "0" : "calc(100% + var(--space-4))"}, -50%)`,
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
transitionDuration: "200ms",
|
transitionDuration: "200ms",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
alias,
|
alias,
|
||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
TIER_ROMAN_NUMERALS,
|
romanize,
|
||||||
uniqueGuns,
|
uniqueGuns,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
@@ -77,6 +77,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
const revealed = Tankopedia.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||||
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
const equalize = Duel.use((state) => state.equalize);
|
const equalize = Duel.use((state) => state.equalize);
|
||||||
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
mr="2"
|
mr="2"
|
||||||
mb="1"
|
mb="1"
|
||||||
>
|
>
|
||||||
{TIER_ROMAN_NUMERALS[antagonistGun.tier]}
|
{romanize(antagonistGun.tier)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
@@ -165,7 +166,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
selected={gun.id === antagonistGun.id}
|
selected={gun.id === antagonistGun.id}
|
||||||
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
discriminator={romanize(gun.tier)}
|
||||||
secondaryDiscriminator={
|
secondaryDiscriminator={
|
||||||
<Text style={{ fontSize: "0.75em" }}>
|
<Text style={{ fontSize: "0.75em" }}>
|
||||||
{literals(strings.common.units.mm, {
|
{literals(strings.common.units.mm, {
|
||||||
@@ -283,7 +284,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<SpallLinerSwitcher />
|
<SpallLinerSwitcher />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{!skeleton && (
|
{!skeleton && modelRequested && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<DynamicArmorSwitcher />
|
<DynamicArmorSwitcher />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
+38
-4
@@ -1,11 +1,14 @@
|
|||||||
import { Flex, Heading } from "@radix-ui/themes";
|
import { UpdateIcon } from "@radix-ui/react-icons";
|
||||||
|
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||||
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 { Tracker } from "./components/Tracker";
|
import { Tracker } from "./components/Tracker";
|
||||||
|
|
||||||
export function Title() {
|
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const { unwrap } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
|
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);
|
||||||
@@ -21,6 +24,8 @@ export function Title() {
|
|||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!modelRequested) return;
|
||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.revealed = true;
|
draft.revealed = true;
|
||||||
});
|
});
|
||||||
@@ -60,7 +65,36 @@ export function Title() {
|
|||||||
{name}
|
{name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{!revealed && <Tracker fontSize={fontSize} />}
|
{!skeleton && !revealed && modelRequested && (
|
||||||
|
<Tracker fontSize={fontSize} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!skeleton && !modelRequested && (
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
left="50%"
|
||||||
|
top="50%"
|
||||||
|
width="min(100%, 16rem)"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4) - 50%))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
highContrast
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={() => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.modelRequested = true;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UpdateIcon />
|
||||||
|
{strings.website.tools.tankopedia.sandbox.load_model}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
const canvas = useRef<HTMLCanvasElement>(null!);
|
const canvas = useRef<HTMLCanvasElement>(null!);
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||||
const thicknessRange = useMemo(() => {
|
const thicknessRange = useMemo(() => {
|
||||||
const entries = Object.values(tankDefinitions.tanks);
|
const entries = Object.values(tankDefinitions.tanks);
|
||||||
const filtered = entries.filter(
|
const filtered = entries.filter(
|
||||||
@@ -131,13 +132,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
>
|
>
|
||||||
{!skeleton && (
|
{!skeleton && modelRequested && (
|
||||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
<Suspense>
|
||||||
|
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Title />
|
<Title skeleton={skeleton} />
|
||||||
|
|
||||||
<Options
|
<Options
|
||||||
skeleton={skeleton}
|
skeleton={skeleton}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { alias, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
import { alias, romanize } from "@blitzkit/core";
|
||||||
import { TankPriceType, TankType } from "@blitzkit/protos";
|
import { TankPriceType, TankType } from "@blitzkit/protos";
|
||||||
import { Box, Flex } from "@radix-ui/themes";
|
import { Box, Flex } from "@radix-ui/themes";
|
||||||
import { Var } from "../../../core/radix/var";
|
import { Var } from "../../../core/radix/var";
|
||||||
@@ -80,7 +80,7 @@ export function MetaSection() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Listing>
|
</Listing>
|
||||||
<Listing label={strings.website.tools.tankopedia.meta.tier}>
|
<Listing label={strings.website.tools.tankopedia.meta.tier}>
|
||||||
{TIER_ROMAN_NUMERALS[protagonist.tier]}
|
{romanize(protagonist.tier)}
|
||||||
</Listing>
|
</Listing>
|
||||||
<Listing
|
<Listing
|
||||||
label={strings.website.tools.tankopedia.meta.type}
|
label={strings.website.tools.tankopedia.meta.type}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { alias, formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
import { alias, formatCompact, romanize } from "@blitzkit/core";
|
||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { Box, Flex, Skeleton, Text } from "@radix-ui/themes";
|
import { Box, Flex, Skeleton, Text } from "@radix-ui/themes";
|
||||||
import { api } from "../../../../api/dynamic";
|
import { api } from "../../../../api/dynamic";
|
||||||
@@ -90,7 +90,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
|||||||
<Flex direction="column" align="center">
|
<Flex direction="column" align="center">
|
||||||
<Flex gap="2" align="center">
|
<Flex gap="2" align="center">
|
||||||
<Text color="gray" size="1">
|
<Text color="gray" size="1">
|
||||||
{TIER_ROMAN_NUMERALS[tank.tier]}
|
{romanize(tank.tier)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text wrap="nowrap">{unwrap(tank.name!)}</Text>
|
<Text wrap="nowrap">{unwrap(tank.name!)}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -127,7 +127,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{averages && nextTanks && tank.tier !== 10 && (
|
{averages && nextTanks && (
|
||||||
<Text color="gray" size="1" mt="2">
|
<Text color="gray" size="1" mt="2">
|
||||||
{skeleton && <Skeleton height="1em" width="4em" />}
|
{skeleton && <Skeleton height="1em" width="4em" />}
|
||||||
{!skeleton &&
|
{!skeleton &&
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{master.tier < 10 && (
|
{master.successors.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Arrow />
|
<Arrow />
|
||||||
{master.successors!.map((id, index) => (
|
{master.successors!.map((id, index) => (
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
.characteristics-wrapper {
|
.characteristics-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
container-type: inline-size;
|
||||||
|
|
||||||
|
gap: var(--space-4);
|
||||||
margin: var(--space-4);
|
margin: var(--space-4);
|
||||||
|
|
||||||
--base-cell-width: 20rem;
|
--base-cell-width: 20rem;
|
||||||
--group-span: 7;
|
--group-span: 7;
|
||||||
|
|
||||||
container-type: inline-size;
|
.compare-options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.characteristics {
|
.characteristics {
|
||||||
--N: round(down, 100cqw / var(--base-cell-width));
|
--N: round(down, 100cqw / var(--base-cell-width));
|
||||||
--cell-width: calc(100cqw / var(--N));
|
--cell-width: calc(100cqw / var(--N));
|
||||||
--cell-height: calc(var(--cell-width) / var(--group-span));
|
--cell-height: calc(var(--cell-width) / var(--group-span));
|
||||||
|
|
||||||
|
margin-bottom: calc(-1 * var(--cell-height));
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
auto-fill,
|
auto-fill,
|
||||||
@@ -35,7 +48,7 @@
|
|||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 calc(var(--space-4));
|
margin: 0 calc(var(--space-3));
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -67,8 +80,6 @@
|
|||||||
grid-auto-rows: inherit;
|
grid-auto-rows: inherit;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
padding: 0 var(--space-3);
|
|
||||||
|
|
||||||
grid-row: span 1;
|
grid-row: span 1;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -76,6 +87,10 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
|
|
||||||
|
&[data-indent="true"] {
|
||||||
|
padding-left: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -102,11 +117,19 @@
|
|||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
transition: var(--duration-brief);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
grid-row: span 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.toy {
|
.toy {
|
||||||
grid-row: span var(--group-span);
|
grid-row: span var(--group-span);
|
||||||
padding: var(--space-4) 0;
|
padding: var(--space-4) 0;
|
||||||
@@ -115,10 +138,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
background-color: var(--gray-2);
|
background-color: var(--gray-2);
|
||||||
outline: var(--gray-5) solid var(--space-1);
|
outline: var(--gray-5) solid var(--space-1);
|
||||||
border-radius: var(--space-3);
|
border-radius: var(--space-3);
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
|
import { romanize } from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
|
import { useRef } from "react";
|
||||||
import { characteristicsGroups } from "../../config/characteristicsGroups";
|
import { characteristicsGroups } from "../../config/characteristicsGroups";
|
||||||
|
import { toys } from "../../config/toys";
|
||||||
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
|
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
|
||||||
|
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
||||||
|
import { useRankings } from "../../hooks/useRankings";
|
||||||
import { useStrings } from "../../hooks/useStrings";
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
|
import { Tankopedia, TankopediaRelativeAgainst } from "../../stores/tankopedia";
|
||||||
|
import type { CharacteristicRank } from "../../tankopedia/calculateRankings";
|
||||||
import type {
|
import type {
|
||||||
|
CharacteristicName,
|
||||||
CharacteristicRenderConfig,
|
CharacteristicRenderConfig,
|
||||||
CharacteristicReturnType,
|
CharacteristicReturnType,
|
||||||
CharacteristicsGroup,
|
CharacteristicsGroup,
|
||||||
ComputedCharacteristics,
|
ComputedCharacteristics,
|
||||||
|
ToyName,
|
||||||
} from "../../types/characteristics";
|
} from "../../types/characteristics";
|
||||||
import { Heading } from "../Heading";
|
import { Heading } from "../Heading";
|
||||||
|
import { SegmentedControl } from "../SegmentedControl";
|
||||||
import { Text } from "../Text";
|
import { Text } from "../Text";
|
||||||
import styles from "./index.module.css";
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
@@ -18,8 +29,12 @@ interface TankopediaCharacteristicsProps {
|
|||||||
export function TankopediaCharacteristics({
|
export function TankopediaCharacteristics({
|
||||||
characteristics,
|
characteristics,
|
||||||
}: TankopediaCharacteristicsProps) {
|
}: TankopediaCharacteristicsProps) {
|
||||||
|
const rankings = useRankings(characteristics);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["characteristics-wrapper"]}>
|
<div className={styles["characteristics-wrapper"]}>
|
||||||
|
<CompareOptions />
|
||||||
|
|
||||||
<div className={styles.characteristics}>
|
<div className={styles.characteristics}>
|
||||||
{characteristicsGroups.map((group) => {
|
{characteristicsGroups.map((group) => {
|
||||||
const items = group.order.filter(
|
const items = group.order.filter(
|
||||||
@@ -34,6 +49,7 @@ export function TankopediaCharacteristics({
|
|||||||
key={group.name}
|
key={group.name}
|
||||||
group={group}
|
group={group}
|
||||||
characteristics={characteristics}
|
characteristics={characteristics}
|
||||||
|
rankings={rankings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -45,14 +61,20 @@ export function TankopediaCharacteristics({
|
|||||||
interface GroupProps {
|
interface GroupProps {
|
||||||
group: CharacteristicsGroup;
|
group: CharacteristicsGroup;
|
||||||
characteristics: ComputedCharacteristics;
|
characteristics: ComputedCharacteristics;
|
||||||
|
rankings: Map<CharacteristicName, CharacteristicRank>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Group({ group, characteristics }: GroupProps) {
|
function Group({ group, characteristics, rankings }: GroupProps) {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const toys = group.order.filter((item) => "toy" in item).length;
|
const toys = group.order.filter((item) => "toy" in item).length;
|
||||||
const items = group.order.filter(
|
const characteristicsCount = group.order.filter(
|
||||||
(item) => "name" in item && characteristics.has(item.name),
|
(item) => "name" in item && characteristics.has(item.name),
|
||||||
).length;
|
).length;
|
||||||
|
const labelsCount =
|
||||||
|
characteristicsCount > 0
|
||||||
|
? group.order.filter((item) => "label" in item).length
|
||||||
|
: 0;
|
||||||
|
const items = characteristicsCount + labelsCount;
|
||||||
const groupSpan = `calc(${toys} * var(--group-span) + ${items} + 2)`;
|
const groupSpan = `calc(${toys} * var(--group-span) + ${items} + 2)`;
|
||||||
const contentSpan = `calc(${toys} * var(--group-span) + ${items})`;
|
const contentSpan = `calc(${toys} * var(--group-span) + ${items})`;
|
||||||
|
|
||||||
@@ -87,9 +109,16 @@ function Group({ group, characteristics }: GroupProps) {
|
|||||||
>
|
>
|
||||||
{group.order.map((item) => (
|
{group.order.map((item) => (
|
||||||
<>
|
<>
|
||||||
{"toy" in item && <Toy {...item} />}
|
{"toy" in item && (
|
||||||
{"name" in item && characteristics.get(item.name) && (
|
<ToyAssigner characteristics={characteristics} {...item} />
|
||||||
<Item config={item} value={characteristics.get(item.name)!} />
|
)}
|
||||||
|
{"label" in item && <Label {...item} />}
|
||||||
|
{"name" in item && characteristics.has(item.name) && (
|
||||||
|
<Item
|
||||||
|
config={item}
|
||||||
|
value={characteristics.get(item.name)!}
|
||||||
|
ranking={rankings.get(item.name)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
@@ -100,18 +129,35 @@ function Group({ group, characteristics }: GroupProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LabelProps {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Label({ label }: LabelProps) {
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.label}>
|
||||||
|
<Text lowContrast>
|
||||||
|
{(strings.tanks.characteristics.items as Record<string, string>)[label]}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
config: CharacteristicRenderConfig;
|
config: CharacteristicRenderConfig;
|
||||||
value: CharacteristicReturnType;
|
value: CharacteristicReturnType;
|
||||||
|
ranking?: CharacteristicRank;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Item({ config, value }: ItemProps) {
|
function Item({ config, value, ranking }: ItemProps) {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const renderCharacteristic = useCharacteristicRenderer();
|
const renderCharacteristic = useCharacteristicRenderer();
|
||||||
const hasUnits = config.units !== undefined;
|
const hasUnits = config.units !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.item}>
|
<span className={styles.item} data-indent={config.indent}>
|
||||||
<div className={styles.value}>
|
<div className={styles.value}>
|
||||||
<Text lowContrast>
|
<Text lowContrast>
|
||||||
{
|
{
|
||||||
@@ -128,17 +174,67 @@ function Item({ config, value }: ItemProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.ranking}>
|
{ranking && (
|
||||||
<Bar value={0.5} />
|
<div className={styles.ranking}>
|
||||||
|
<Bar value={ranking.score} />
|
||||||
|
|
||||||
<Text lowContrast size="minor" className={styles.number}>
|
<Text lowContrast size="minor" className={styles.number}>
|
||||||
1 / 132
|
{literals(strings.units.fraction, {
|
||||||
</Text>
|
numerator: ranking.rank,
|
||||||
</div>
|
denominator: ranking.total,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CompareOptions() {
|
||||||
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
|
const relativeAgainst = Tankopedia.use((state) => state.relative_against);
|
||||||
|
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
const options = useRef([
|
||||||
|
{
|
||||||
|
value: `${TankopediaRelativeAgainst.ClassAndTier}`,
|
||||||
|
relativeAgainst: TankopediaRelativeAgainst.ClassAndTier,
|
||||||
|
label: literals(strings.tanks.characteristics.compare.class, {
|
||||||
|
tier: romanize(tank.tier),
|
||||||
|
class: strings.common.tank_class_short_plural_lowercase[tank.class],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${TankopediaRelativeAgainst.Tier}`,
|
||||||
|
relativeAgainst: TankopediaRelativeAgainst.Tier,
|
||||||
|
label: literals(strings.tanks.characteristics.compare.tier, {
|
||||||
|
tier: romanize(tank.tier),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${TankopediaRelativeAgainst.All}`,
|
||||||
|
relativeAgainst: TankopediaRelativeAgainst.All,
|
||||||
|
label: strings.tanks.characteristics.compare.all,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["compare-options"]}>
|
||||||
|
<Text lowContrast>{strings.tanks.characteristics.compare.label}</Text>
|
||||||
|
<SegmentedControl
|
||||||
|
options={options.current}
|
||||||
|
value={`${relativeAgainst}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.relative_against = Number(value);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface BarProps {
|
interface BarProps {
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
@@ -168,15 +264,22 @@ function Bar({ value }: BarProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToyProps {
|
interface ToyAssignerProps {
|
||||||
toy: string;
|
toy: ToyName;
|
||||||
|
characteristics: ComputedCharacteristics;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Toy({ toy }: ToyProps) {
|
function ToyAssigner({ toy, characteristics }: ToyAssignerProps) {
|
||||||
|
const Toy = toys[toy];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.toy}>
|
<div className={styles.toy}>
|
||||||
<div className={styles.sandbox}>
|
<div className={styles.sandbox}>
|
||||||
<Text lowContrast>{toy} toy</Text>
|
{Toy ? (
|
||||||
|
<Toy characteristics={characteristics} />
|
||||||
|
) : (
|
||||||
|
<Text lowContrast>{toy}</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-5);
|
||||||
|
padding: var(--space-4);
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 16rem;
|
||||||
|
height: 20rem;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--space-3);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
flex: 1;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
min-height: 6.125rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3);
|
||||||
|
backdrop-filter: blur(4rem);
|
||||||
|
-webkit-backdrop-filter: blur(4rem);
|
||||||
|
|
||||||
|
.abilities {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.ability {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { alias } from "@blitzkit/core";
|
||||||
|
import { api } from "../../api/dynamic";
|
||||||
|
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
||||||
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
|
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
||||||
|
import { Heading } from "../Heading";
|
||||||
|
import { Text } from "../Text";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
const [gameDefinitions, consumableDefinitions, provisionDefinitions, tanks] =
|
||||||
|
await Promise.all([
|
||||||
|
api.game(),
|
||||||
|
api.consumables(),
|
||||||
|
api.provisions(),
|
||||||
|
api.tanks(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function TankopediaGameModeAbilities() {
|
||||||
|
const unwrap = useUnwrapper();
|
||||||
|
const strings = useStrings();
|
||||||
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
|
const roles = Object.entries(tank.roles);
|
||||||
|
|
||||||
|
if (roles.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<Heading size="3">{strings.tanks.game_modes.title}</Heading>
|
||||||
|
|
||||||
|
<div className={styles.cards}>
|
||||||
|
{roles.map(([id, value]) => {
|
||||||
|
const gameMode = gameDefinitions.gameModes[id];
|
||||||
|
const role = gameDefinitions.roles[value];
|
||||||
|
const banner = alias("api", `/gamemodes/${id}/banner.webp`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={id}
|
||||||
|
className={styles.card}
|
||||||
|
style={{ backgroundImage: `url(${banner})` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.banner}
|
||||||
|
style={{ backgroundImage: `url(${banner})` }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<Text size="major" weight="bold" align="center">
|
||||||
|
{unwrap(gameMode.name!)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div className={styles.abilities}>
|
||||||
|
{role.provisions.map((id) => {
|
||||||
|
const provisions = provisionDefinitions.provisions[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Ability
|
||||||
|
key={`provision-${id}`}
|
||||||
|
icon={alias("api", `/icons/provisions/${id}.webp`)}
|
||||||
|
name={unwrap(provisions.name!)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{role.consumables.map((id) => {
|
||||||
|
const consumable = consumableDefinitions.consumables[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Ability
|
||||||
|
key={`consumable-${id}`}
|
||||||
|
icon={alias("api", `/icons/consumables/${id}.webp`)}
|
||||||
|
name={unwrap(consumable.name!)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ability({ icon, name }: { icon: string; name: string }) {
|
||||||
|
return (
|
||||||
|
<Text lowContrast className={styles.ability}>
|
||||||
|
<img src={icon} alt={name} />
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
.section {
|
.section {
|
||||||
max-width: 21rem;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -13,6 +11,31 @@
|
|||||||
|
|
||||||
.upgrade-buttons {
|
.upgrade-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.crew-skills {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.level {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
right: var(--space-1);
|
||||||
|
bottom: var(--space-1);
|
||||||
|
|
||||||
|
font-size: var(--font-size-minor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +45,10 @@
|
|||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shells {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -51,8 +78,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.consumables {
|
.consumables {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(3, max-content);
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
|
|
||||||
.consumable-wrapper {
|
.consumable-wrapper {
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
import {
|
||||||
|
alias,
|
||||||
|
MAX_SKILL_LEVEL,
|
||||||
|
permanentSkills,
|
||||||
|
romanize,
|
||||||
|
} from "@blitzkit/core";
|
||||||
import type { Consumable, EquipmentSlot, Provision } from "@blitzkit/protos";
|
import type { Consumable, EquipmentSlot, Provision } from "@blitzkit/protos";
|
||||||
import { ClockIcon, ResetIcon, TrashIcon } from "@radix-ui/react-icons";
|
import {
|
||||||
|
ClockIcon,
|
||||||
|
DoubleArrowUpIcon,
|
||||||
|
ResetIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
import { chunk } from "lodash-es";
|
import { chunk } from "lodash-es";
|
||||||
import { api } from "../../api/dynamic";
|
import { api } from "../../api/dynamic";
|
||||||
import { equipmentColumns } from "../../config/equipment";
|
import { equipmentColumns } from "../../config/equipment";
|
||||||
@@ -11,7 +21,6 @@ import { useStrings } from "../../hooks/useStrings";
|
|||||||
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
|
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
|
||||||
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
||||||
import { Tankopedia } from "../../stores/tankopedia";
|
import { Tankopedia } from "../../stores/tankopedia";
|
||||||
import { hasUpgrades } from "../../tankopedia/hasUpgrades";
|
|
||||||
import { vehicleStatusKeys } from "../../tankopedia/tankState";
|
import { vehicleStatusKeys } from "../../tankopedia/tankState";
|
||||||
import type { ComputedCharacteristics } from "../../types/characteristics";
|
import type { ComputedCharacteristics } from "../../types/characteristics";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
@@ -24,27 +33,120 @@ import styles from "./index.module.css";
|
|||||||
const equipment = await api.equipment();
|
const equipment = await api.equipment();
|
||||||
const provisions = await api.provisions();
|
const provisions = await api.provisions();
|
||||||
const consumables = await api.consumables();
|
const consumables = await api.consumables();
|
||||||
|
const skills = await api.skills();
|
||||||
|
|
||||||
interface TankopediaLoadoutProps {
|
interface TankopediaLoadoutProps {
|
||||||
characteristics: ComputedCharacteristics;
|
characteristics: ComputedCharacteristics;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
||||||
const tank = useDuel("protagonist", DuelModule.Tank);
|
// const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const showModules = hasUpgrades(tank);
|
// const showModules = hasUpgrades(tank);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* {showModules && <Modules />} */}
|
{/* {showModules && <Modules />} */}
|
||||||
|
<Shells />
|
||||||
<Equipment />
|
<Equipment />
|
||||||
<Provisions />
|
<Provisions />
|
||||||
<Consumables characteristics={characteristics} />
|
<Consumables characteristics={characteristics} />
|
||||||
|
<CrewSkills />
|
||||||
<State />
|
<State />
|
||||||
<Status />
|
<Status />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CrewSkills() {
|
||||||
|
const strings = useStrings();
|
||||||
|
const selectedSkills = Tankopedia.use((state) => state.environment.skills);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Heading size="4">{strings.tanks.loadout.skills}</Heading>
|
||||||
|
|
||||||
|
<div className={styles["upgrade-buttons"]}>
|
||||||
|
<IconButton
|
||||||
|
color="red"
|
||||||
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.skills}`}
|
||||||
|
onClick={() => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
Object.keys(draft.environment.skills).forEach((skill) => {
|
||||||
|
draft.environment.skills[skill] = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
aria-label={`${strings.tanks.loadout.maximize} ${strings.tanks.loadout.skills}`}
|
||||||
|
onClick={() => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
Object.keys(draft.environment.skills).forEach((skill) => {
|
||||||
|
draft.environment.skills[skill] = MAX_SKILL_LEVEL;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DoubleArrowUpIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["crew-skills"]}>
|
||||||
|
{Object.entries(skills.classes).map(([tankClass, group]) => (
|
||||||
|
<div key={tankClass} className={styles.row}>
|
||||||
|
{group.skills.map((skill) => (
|
||||||
|
<CrewSkillEntry
|
||||||
|
key={skill}
|
||||||
|
id={skill}
|
||||||
|
level={selectedSkills[skill]}
|
||||||
|
permanent={permanentSkills.includes(skill)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CrewSkillEntryProps {
|
||||||
|
id: string;
|
||||||
|
level: number;
|
||||||
|
permanent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CrewSkillEntry({ id, level, permanent }: CrewSkillEntryProps) {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="major"
|
||||||
|
radius="1"
|
||||||
|
className={styles.skill}
|
||||||
|
color={level === 0 ? "gray" : permanent ? undefined : "amber"}
|
||||||
|
variant={level === 0 ? "soft" : "surface"}
|
||||||
|
onClick={(event) => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
if (event.shiftKey) {
|
||||||
|
draft.environment.skills[id] = level === 0 ? MAX_SKILL_LEVEL : 0;
|
||||||
|
} else {
|
||||||
|
draft.environment.skills[id] =
|
||||||
|
level === 0 ? MAX_SKILL_LEVEL : level - 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={alias("api", `/icons/skills/${id}.webp`)} />
|
||||||
|
{level > 0 && <span className={styles.level}>{romanize(level)}</span>}
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function State() {
|
function State() {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
|
|
||||||
@@ -58,6 +160,7 @@ function State() {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="red"
|
color="red"
|
||||||
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.consumables}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.protagonist.consumables = [];
|
draft.protagonist.consumables = [];
|
||||||
@@ -87,6 +190,7 @@ function Status() {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="red"
|
color="red"
|
||||||
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.status}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.protagonist.consumables = [];
|
draft.protagonist.consumables = [];
|
||||||
@@ -103,9 +207,10 @@ function Status() {
|
|||||||
{group.map((key) => (
|
{group.map((key) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={key}
|
key={key}
|
||||||
|
aria-label={key.replaceAll("_", " ")}
|
||||||
|
aria-pressed={status[key]}
|
||||||
radius="1"
|
radius="1"
|
||||||
className={styles.state}
|
className={styles.state}
|
||||||
data-selected={status[key]}
|
|
||||||
color={status[key] ? "amber" : "gray"}
|
color={status[key] ? "amber" : "gray"}
|
||||||
variant={status[key] ? "surface" : "soft"}
|
variant={status[key] ? "surface" : "soft"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -125,6 +230,52 @@ function Status() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Shells() {
|
||||||
|
const strings = useStrings();
|
||||||
|
const unwrap = useUnwrapper();
|
||||||
|
|
||||||
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
||||||
|
const shell = useDuel("protagonist", DuelModule.Shell);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Heading size="4">{strings.tanks.loadout.shells}</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.shells}>
|
||||||
|
{gun.shells.map((candidate) => {
|
||||||
|
const isSelected = candidate.id === shell.id;
|
||||||
|
const name = unwrap(candidate.name!);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={candidate.id} tooltip={name}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={name}
|
||||||
|
size="major"
|
||||||
|
radius="1"
|
||||||
|
color={isSelected ? undefined : "gray"}
|
||||||
|
variant={isSelected ? "surface" : "soft"}
|
||||||
|
parentListChild
|
||||||
|
onClick={() => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.protagonist.shell = candidate.id;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={name}
|
||||||
|
src={alias("api", `/icons/shells/${candidate.icon}.webp`)}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Provisions() {
|
function Provisions() {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const tank = useDuel("protagonist", DuelModule.Tank);
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
@@ -138,6 +289,7 @@ function Provisions() {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="red"
|
color="red"
|
||||||
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.provisions}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.protagonist.provisions = [];
|
draft.protagonist.provisions = [];
|
||||||
@@ -173,21 +325,34 @@ interface ProvisionEntryProps {
|
|||||||
|
|
||||||
function ProvisionEntry({ id, provision }: ProvisionEntryProps) {
|
function ProvisionEntry({ id, provision }: ProvisionEntryProps) {
|
||||||
const isSelected = useProvision("protagonist", id);
|
const isSelected = useProvision("protagonist", id);
|
||||||
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
|
const selectedCount = Tankopedia.use(
|
||||||
|
(state) => state.protagonist.provisions.length,
|
||||||
|
);
|
||||||
|
|
||||||
const unwrap = useUnwrapper();
|
const unwrap = useUnwrapper();
|
||||||
|
|
||||||
|
const disabled = !isSelected && selectedCount >= tank.max_provisions;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltip={unwrap(provision.name)}>
|
<Tooltip tooltip={unwrap(provision.name)}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
aria-label={unwrap(provision.name)}
|
||||||
size="major"
|
size="major"
|
||||||
radius="1"
|
radius="1"
|
||||||
variant={isSelected ? "surface" : "soft"}
|
variant={isSelected ? "surface" : "soft"}
|
||||||
color={isSelected ? undefined : "gray"}
|
color={isSelected ? undefined : "gray"}
|
||||||
className={styles.consumable}
|
className={styles.consumable}
|
||||||
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
const index = draft.protagonist.provisions.indexOf(id);
|
const index = draft.protagonist.provisions.indexOf(id);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
if (draft.protagonist.provisions.length === tank.max_provisions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
draft.protagonist.provisions.push(id);
|
draft.protagonist.provisions.push(id);
|
||||||
} else {
|
} else {
|
||||||
draft.protagonist.provisions.splice(index, 1);
|
draft.protagonist.provisions.splice(index, 1);
|
||||||
@@ -218,6 +383,7 @@ function Consumables({ characteristics }: ConsumablesProps) {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="red"
|
color="red"
|
||||||
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.consumables}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.protagonist.consumables = [];
|
draft.protagonist.consumables = [];
|
||||||
@@ -252,24 +418,39 @@ interface ConsumableProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ConsumableEntry({ id, consumable }: ConsumableProps) {
|
function ConsumableEntry({ id, consumable }: ConsumableProps) {
|
||||||
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const isSelected = Tankopedia.use((state) =>
|
const isSelected = Tankopedia.use((state) =>
|
||||||
state.protagonist.consumables.includes(id),
|
state.protagonist.consumables.includes(id),
|
||||||
);
|
);
|
||||||
|
const selectedCount = Tankopedia.use(
|
||||||
|
(state) => state.protagonist.consumables.length,
|
||||||
|
);
|
||||||
|
|
||||||
const unwrap = useUnwrapper();
|
const unwrap = useUnwrapper();
|
||||||
|
|
||||||
|
const disabled = !isSelected && selectedCount >= tank.max_consumables;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["consumable-wrapper"]}>
|
<div className={styles["consumable-wrapper"]}>
|
||||||
<Tooltip tooltip={unwrap(consumable.name)}>
|
<Tooltip tooltip={unwrap(consumable.name)}>
|
||||||
<Button
|
<Button
|
||||||
|
aria-label={unwrap(consumable.name)}
|
||||||
radius="1"
|
radius="1"
|
||||||
variant={isSelected ? "surface" : "soft"}
|
variant={isSelected ? "surface" : "soft"}
|
||||||
color={isSelected ? undefined : "gray"}
|
color={isSelected ? undefined : "gray"}
|
||||||
className={styles.consumable}
|
className={styles.consumable}
|
||||||
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
const index = draft.protagonist.consumables.indexOf(id);
|
const index = draft.protagonist.consumables.indexOf(id);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
if (
|
||||||
|
draft.protagonist.consumables.length === tank.max_consumables
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
draft.protagonist.consumables.push(id);
|
draft.protagonist.consumables.push(id);
|
||||||
} else {
|
} else {
|
||||||
draft.protagonist.consumables.splice(index, 1);
|
draft.protagonist.consumables.splice(index, 1);
|
||||||
@@ -313,6 +494,7 @@ function Equipment() {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="red"
|
color="red"
|
||||||
|
aria-label={`${strings.tanks.loadout.clear} ${strings.tanks.loadout.equipment}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
for (const key in draft.protagonist.equipment) {
|
for (const key in draft.protagonist.equipment) {
|
||||||
@@ -418,10 +600,10 @@ function EquipmentOption({
|
|||||||
return (
|
return (
|
||||||
<Tooltip tooltip={unwrap(specificEquipment.name)}>
|
<Tooltip tooltip={unwrap(specificEquipment.name)}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
aria-label={unwrap(specificEquipment.name)}
|
||||||
size="major"
|
size="major"
|
||||||
color={isSelected ? undefined : "gray"}
|
color={isSelected ? undefined : "gray"}
|
||||||
variant={isSelected ? "surface" : "soft"}
|
variant={isSelected ? "surface" : "soft"}
|
||||||
data-selected={isSelected}
|
|
||||||
radius="1"
|
radius="1"
|
||||||
parentListChild
|
parentListChild
|
||||||
className={styles.slot}
|
className={styles.slot}
|
||||||
|
|||||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user