Compare commits

...
105 Commits
Author SHA1 Message Date
TrèsAbhi 5dba69f30a New Crowdin updates (#210) 2026-02-03 15:00:55 -06:00
TrèsAbhi 023c348b0d New Crowdin updates (#209)
* New translations en.json (French)

* New translations en.json (French)

* New translations en.json (Russian)
2026-01-21 11:00:30 -06:00
TrèsAbhi 8313dd811d feat: logs 2026-01-16 19:31:53 -06:00
TrèsAbhi 3f7d10c2c6 fix: guesser sizing 2026-01-16 19:31:09 -06:00
TrèsAbhi 98e83d120a New translations en.json (Russian) (#208) 2026-01-16 18:46:33 -06:00
TrèsAbhi dd845d2784 New translations en.json (Chinese Simplified) (#207) 2026-01-12 22:06:59 -06:00
TrèsAbhi dc15df2a40 New Crowdin updates (#206) 2026-01-09 14:06:11 -06:00
TrèsAbhi 7c838aaf06 New Crowdin updates (#205)
* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)
2026-01-08 10:13:55 -06:00
TrèsAbhi f181c96518 Merge pull request #204 from blitzkit/dev
dev
2026-01-08 03:48:00 +00:00
TrèsAbhi 6fbfa2c70b Merge branch 'main' into dev 2026-01-07 20:16:06 -06:00
TrèsAbhi 17dd50b902 feat: logs 2026-01-07 20:15:45 -06:00
TrèsAbhi 9061335292 feat: deduplicate game mode consumales and powers 2026-01-07 20:11:54 -06:00
TrèsAbhi c27d3a3b6a Merge pull request #203 from Hitjack007/filter-by-module-damage
Fixed Module dmage filter showing alpha damage
2026-01-07 03:22:49 +00:00
Mark Greene c37acf4a93 Fixed Module dmage filter showing alpha damage 2026-01-07 15:41:55 +13:00
TrèsAbhi 0c1cef87f6 Merge pull request #200 from blitzkit/tresabhi-patch-1
Rename build job to 'Build website'
2026-01-05 05:18:30 +00:00
TrèsAbhi e8b777be8a Rename build job to 'Build bot' 2026-01-04 19:08:35 -06:00
TrèsAbhi 2941348412 Rename build job to 'Build website' 2026-01-04 19:08:04 -06:00
TrèsAbhi 68b2ecfb6d Merge branch 'main' into dev 2026-01-04 15:19:35 -06:00
TrèsAbhi e058b09621 Merge pull request #199 from blitzkit/main-i18n 2026-01-04 21:18:48 +00:00
TrèsAbhi d5526da1fb Merge pull request #198 from Hitjack007/dev 2026-01-04 21:18:31 +00:00
TrèsAbhi 091305a20f feat: break streak on tier change 2026-01-04 15:18:00 -06:00
TrèsAbhi 85cd70b2d5 feat: disable selecting 0 tiers 2026-01-04 15:16:57 -06:00
TrèsAbhi 7997f490fc feat: clean up tiers ui 2026-01-04 15:11:20 -06:00
TrèsAbhi 5a0b309b0b feat: combine tabs 2026-01-04 14:58:59 -06:00
TrèsAbhi 448e4e0d7d New translations en.json (Russian) 2026-01-04 12:33:13 +00:00
Mark Greene 43c14665de Fix 2026-01-04 14:50:11 +13:00
Mark Greene e249408360 Fixed Filters not centering 2026-01-04 14:22:20 +13:00
Mark Greene 24e57d7125 Added Tier Filter to Guesser 2026-01-04 10:18:48 +13:00
TrèsAbhi 0ea7714578 feat: redo credits 2026-01-03 13:36:15 -06:00
Mark Greene 636a2934a2 Reworked Game Mode Ability Filters 2026-01-03 22:07:01 +13:00
TrèsAbhi cbe22035cb Merge pull request #194 from blitzkit/main-i18n 2026-01-02 18:47:42 +00:00
TrèsAbhi 4c8e5cc8ed New translations en.json (Chinese Simplified) 2026-01-02 18:46:54 +00:00
TrèsAbhi ee4141838c New translations en.json (Ukrainian) 2026-01-02 18:46:53 +00:00
TrèsAbhi 24ff42a672 New translations en.json (Russian) 2026-01-02 18:46:52 +00:00
TrèsAbhi d8748c028e New translations en.json (Portuguese) 2026-01-02 18:46:51 +00:00
TrèsAbhi b55924ce2b New translations en.json (Polish) 2026-01-02 18:46:50 +00:00
TrèsAbhi 66f7a8d369 New translations en.json (Japanese) 2026-01-02 18:46:49 +00:00
TrèsAbhi 8ed97a3173 New translations en.json (Spanish) 2026-01-02 18:46:47 +00:00
TrèsAbhi b4f452b026 New translations en.json (French) 2026-01-02 18:46:46 +00:00
TrèsAbhi 8d4d730b11 feat: logs 2026-01-02 12:43:07 -06:00
TrèsAbhi 8f54bd5971 fix: use role filters 2026-01-02 12:39:45 -06:00
TrèsAbhi 56a09536cd fix: use roles 2026-01-02 12:09:23 -06:00
TrèsAbhi 9df5efa422 feat: enable gamemode exclusive 2026-01-02 11:32:27 -06:00
TrèsAbhi ee5461a297 feat: reset button 2026-01-02 11:28:11 -06:00
TrèsAbhi 5ed5f45240 feat: 90rem 2025-12-31 14:42:13 -06:00
TrèsAbhi a8cdc96382 fix: some filtering 2025-12-31 14:41:13 -06:00
TrèsAbhi 532d677491 feat: provision filters 2025-12-31 14:37:14 -06:00
TrèsAbhi 610ea60ca2 feat: provisions and powers 2025-12-31 14:31:48 -06:00
TrèsAbhi 718576d5a9 feat: provisions 2025-12-31 14:27:48 -06:00
TrèsAbhi f885fba4bd fix: keys 2025-12-31 14:20:44 -06:00
TrèsAbhi 408a14003c fix: allow interactbility oustisde of dropdoiwn 2025-12-31 14:19:10 -06:00
TrèsAbhi 9416e25ee9 feat: start of logs 2025-12-31 14:18:58 -06:00
TrèsAbhi 8a3bbb3647 feat: clear button 2025-12-31 14:08:37 -06:00
TrèsAbhi e16cc63cfe feat: bigger icons 2025-12-31 14:06:08 -06:00
TrèsAbhi b2856eab58 feat: localize 2025-12-31 14:02:28 -06:00
TrèsAbhi 55ffe22fd6 feat: gamemode abilities 2025-12-31 13:56:31 -06:00
TrèsAbhi 98e0f28cd9 feat: consumables 2025-12-31 13:51:54 -06:00
TrèsAbhi 547381286e feat: testing filters 2025-12-31 13:42:19 -06:00
TrèsAbhi bdaf6e662c fix: surface 2025-12-31 13:40:34 -06:00
TrèsAbhi 87438abaa3 feat: simplify 2025-12-31 13:38:18 -06:00
TrèsAbhi f837aaf40a fix: use dropdown 2025-12-30 20:56:42 -06:00
TrèsAbhi 86e080aae9 feat: shells 2025-12-30 20:48:51 -06:00
TrèsAbhi 57efde83df Merge pull request #193 from Hitjack007/filter-by-provision 2025-12-31 02:39:06 +00:00
TrèsAbhi 6b29584424 feat: minor adjusments 2025-12-30 20:38:32 -06:00
TrèsAbhi 71e40a0113 feat: tank types 2025-12-30 20:30:18 -06:00
TrèsAbhi b5e7170697 feat: gun types 2025-12-30 20:23:06 -06:00
TrèsAbhi 4122856024 fix: reduce gap 2025-12-30 20:13:37 -06:00
TrèsAbhi 5b4c85138e feat: classes 2025-12-30 20:12:25 -06:00
TrèsAbhi 727ced967b feat: extract components 2025-12-30 20:05:00 -06:00
TrèsAbhi 4462ebe82c feat: tiers filter 2025-12-30 20:00:18 -06:00
TrèsAbhi 06ce263668 feat: compact nations selector 2025-12-30 19:53:06 -06:00
TrèsAbhi 7e086cc639 Merge pull request #192 from Hitjack007/main 2025-12-31 01:04:11 +00:00
TrèsAbhi 540e56a12d feat: format all 2025-12-30 19:03:00 -06:00
TrèsAbhi faf802cf3d revert: Modules.tsx 2025-12-30 19:02:05 -06:00
TrèsAbhi b6d6fa7212 Remove unused React import from FilterControl.tsx 2025-12-30 09:52:11 -06:00
TrèsAbhi dfa8be63b4 Pet peeve 2025-12-30 09:51:10 -06:00
TrèsAbhi 9eb6acc624 Fix formatting of consumables array in tankFilters 2025-12-30 09:49:06 -06:00
Mark Greene b6204b1c2c Added ability to filter tanks by available provisions 2025-12-30 20:45:44 +13:00
TrèsAbhi 39f1389d77 Remove unused provision logging comments
Removed commented-out logging code for provisions.
2025-12-29 21:27:41 -06:00
Mark Greene 53b0f04512 Revert changes 2025-12-30 15:53:25 +13:00
Mark Greene d2fa93ee71 Revert changes 2025-12-30 15:51:57 +13:00
Mark Greene 7871eae7a3 revert changes 2025-12-30 15:50:43 +13:00
Mark Greene 0bd660f255 Revert changes 2025-12-30 15:48:44 +13:00
Mark Greene 488857b7d8 Merge branch 'blitzkit:main' into main 2025-12-30 15:45:38 +13:00
Mark Greene 5e4c5fc230 Fixed Provisions not affecting terrain resistances and added commented-out helper function to log provisions later if needed. 2025-12-30 15:43:16 +13:00
TrèsAbhi 7e6925772e fix: plural 2025-12-27 20:02:51 -06:00
TrèsAbhi b479c09cf8 feat: logs 2025-12-27 20:01:29 -06:00
TrèsAbhi eddc8168ba feat: helping reveal 2025-12-27 19:51:50 -06:00
TrèsAbhi 80dfc59017 feat: streaks in guesser 2025-12-27 19:39:10 -06:00
TrèsAbhi fdc0f34e08 feat: clear all provisions and consumables 2025-12-27 19:36:12 -06:00
TrèsAbhi c0fdf10eba fix: tiny compare table buttons 2025-12-27 19:33:17 -06:00
TrèsAbhi 2aba4da1af fix: selector 2025-12-27 19:26:16 -06:00
TrèsAbhi 581d889e9b feat: module selection 2025-12-27 19:21:22 -06:00
TrèsAbhi 6a280982d0 fix: remove ue code 2025-12-27 18:50:14 -06:00
TrèsAbhi 45c7d74160 revert: icon scale 2025-12-27 18:45:09 -06:00
Mark Greene 9e225d2093 Fixed Provison Button Sizes 2025-12-28 12:12:56 +13:00
Mark Greene d1001df3cf Add randomise function to modules 2025-12-28 10:06:53 +13:00
Mark Greene d28c56fe1c Add Randomise function to module selection 2025-12-28 10:04:21 +13:00
TrèsAbhi fe68ebf450 Merge pull request #189 from Hitjack007/main
Fix Turret chnage not affecting DPM
2025-12-27 04:17:26 +00:00
TrèsAbhi 18cc885869 feat: hide bug in exclaim
Removed redundant comment about fixing a bug.
2025-12-26 21:13:06 -06:00
Mark Greene 9d4e1a66a1 Fix Turret chnage not affecting DPM 2025-12-27 15:35:16 +13:00
TrèsAbhi baaae612fd Merge branch 'main' into dev 2025-12-15 22:29:04 -06:00
TrèsAbhi a56665f3a3 Merge branch 'main' into dev 2025-12-15 22:24:41 -06:00
TrèsAbhi b05e70faee feat: clear all provisions 2025-12-10 23:03:30 -06:00
TrèsAbhi 5c30169eca fix: canvas smooth resize 2025-12-10 22:57:21 -06:00
39 changed files with 2412 additions and 1220 deletions

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ concurrency:
jobs: jobs:
build: build:
name: Build name: Build bot
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+1 -1
View File
@@ -35,7 +35,7 @@ env:
jobs: jobs:
build: build:
name: Build name: Build website
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
+2
View File
@@ -75,6 +75,7 @@
"DIDS", "DIDS",
"donotremove", "donotremove",
"drei", "drei",
"Droodles",
"dvpd", "dvpd",
"dvpl", "dvpl",
"DVPLCRC", "DVPLCRC",
@@ -101,6 +102,7 @@
"gtag", "gtag",
"haha", "haha",
"Herdbreaker", "Herdbreaker",
"Hetz",
"highp", "highp",
"iife", "iife",
"imgur", "imgur",
+44 -16
View File
@@ -1,19 +1,47 @@
# BlitzKit Credits # BlitzKit Credits
BlitzKit is a one man band **TrèsAbhi** but I am far from solo. BlitzKit is a one man band **TrèsAbhi** but I am far from solo. Helpers, in the order of contribution over time:
| Role | People | ### S0AP
| ------------------------------ | ------------------------------ |
| Bot verification | S0AP | BlitzKit Discord bot verification, testing, advertisement, and the initial encouragement to create BlitzKit.
| General testing | Prince_NA |
| Bot advertisement platform | S0AP | ### Prince_NA
| Bot rendering pipeline | Vovko |
| Game asset decompression | Maddox | BlitzKit Discord bot and website testing and general advise.
| Game 3D model parsing | Pyogenics |
| Game file layout | Aeson | ### Vovko
| 3-caliber rule edge cases | Minitelrose, ChickenMan7777 |
| Embed UX | \_34, HisRoyalFatness, Synx | BlitzKit Discord bot and website rendering pipeline.
| Tankopedia pre-release testing | [OPTML], Aeson, \_34, xkdkw |
| Website advertisement platform | Aeson | ### Maddox
| Compare pre-release testing | [OPTML] |
| Redacted | GonnaHetzMe | Game file decompression.
### Pyogenics
Game 3D model parsing.
### Aeson
Game file layout, general advise, advertisement, and Tankopedia pre-release testing.
### Minitelrose, ChickenMan7777
3-caliber rule edge cases.
### HisRoyalFatness, Synx
Embed UX.
### [OPTML] clan, \_34, xkdkw, Droodles Blitz
Tankopedia pre-release testing.
### GonnaHetzMe
Redacted by request.
### Mark Greene
GitHub contributor on bug fixes and feature additions.
+14
View File
@@ -0,0 +1,14 @@
# BlitzKit 1.29.10
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Combined game mode passive and active abilities into a single dropdown
- [Mark Greene] Added tier filters to the Guess game
- Changing tiers will break your streak
## Fixes
- [Mark Greene] module damage is now properly shown when sorting with it in the tank search
- Duplicate abilities in the tank filters are now removed
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.29.11
## Fixes
- The Guesser game's toolbar now scales properly with screen size
+19
View File
@@ -0,0 +1,19 @@
# BlitzKit 1.29.8
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Added an option to clear provisions and consumables from all tanks in Compare
- Added the ability to choose stock turrets and guns in Mixer if a tank has any
- Added streaks to Guesser
- Introduced an option to partially reveal tanks in Guesser
- This removes the darkness effect but keeps the name hidden
- This will reset your streak to 0
- Regardless of getting the right guess on the tank's name, your response will be counted as incorrect if partially revealed
## Fixes
- The Tankopedia now smoothly resizes when enlarging on interaction
- [Mark Greene] Stock turrets now correctly mount the corresponding stock gun
- The consumable and provision buttons in Compare are no longer tiny
+14
View File
@@ -0,0 +1,14 @@
# BlitzKit 1.29.9
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- 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 provisions and game-mode abilities and powers
## Fixes
- Consumable and provision icons in Compare are now properly sized
- [Mark Greene] Provisions now properly affect terrain resistances
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"version": "1.29.7", "version": "1.29.11",
"license": "BlitzKit License", "license": "BlitzKit License",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -4,63 +4,54 @@ import {
GunDefinition, GunDefinition,
Provision, Provision,
TankDefinition, TankDefinition,
} from '../protos'; } from "../protos";
export function checkConsumableProvisionInclusivity( export function checkConsumableProvisionInclusivity(
consumableProvision: Consumable | Provision, consumableProvision: Consumable | Provision,
tank: TankDefinition, tank: TankDefinition,
gun: GunDefinition, gun: GunDefinition
) { ) {
const included = const included = consumableProvision.include?.every((rule) => {
!consumableProvision.game_mode_exclusive && switch (rule.filter_type!.$case) {
consumableProvision.include?.every((rule) => { case "tiers":
switch (rule.filter_type!.$case) { return (
case 'tiers': rule.filter_type!.value.min <= tank.tier &&
return ( tank.tier <= rule.filter_type!.value.max
rule.filter_type!.value.min <= tank.tier && );
tank.tier <= rule.filter_type!.value.max
);
case 'ids': case "ids":
return rule.filter_type!.value.ids.includes(tank.id); return rule.filter_type!.value.ids.includes(tank.id);
case 'nations': case "nations":
return rule.filter_type!.value.nations.includes(tank.nation); return rule.filter_type!.value.nations.includes(tank.nation);
case 'categories': case "categories":
throw new SyntaxError('Category filtering found in include rule'); throw new SyntaxError("Category filtering found in include rule");
} }
}); });
const excluded = const excluded = consumableProvision.exclude?.some((rule) => {
consumableProvision.game_mode_exclusive || switch (rule.filter_type!.$case) {
consumableProvision.exclude?.some((rule) => { case "tiers":
switch (rule.filter_type!.$case) { return (
case 'tiers': rule.filter_type!.value.min <= tank.tier &&
return ( tank.tier <= rule.filter_type!.value.max
rule.filter_type!.value.min <= tank.tier && );
tank.tier <= rule.filter_type!.value.max
);
case 'ids': case "ids":
return rule.filter_type!.value.ids.includes(tank.id); return rule.filter_type!.value.ids.includes(tank.id);
case 'nations': case "nations":
return rule.filter_type!.value.nations.includes(tank.nation); return rule.filter_type!.value.nations.includes(tank.nation);
case 'categories': case "categories":
return rule.filter_type!.value.categories.some((category) => { return rule.filter_type!.value.categories.some((category) => {
switch (category) { switch (category) {
case ConsumableTankCategoryFilterCategory.CLIP: case ConsumableTankCategoryFilterCategory.CLIP:
return gun.gun_type!.$case !== 'regular'; return gun.gun_type!.$case !== "regular";
} }
}); });
} }
}); });
return ( return consumableProvision.include?.length > 0 && included && !excluded;
!consumableProvision.game_mode_exclusive &&
consumableProvision.include?.length > 0 &&
included &&
!excluded
);
} }
+24 -6
View File
@@ -15,7 +15,8 @@
"hp": "{value}hp", "hp": "{value}hp",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}°/s",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
@@ -26,8 +27,8 @@
"shells": { "shells": {
"ap": "AP", "ap": "AP",
"apcr": "APCR", "ap_cr": "APCR",
"heat": "HEAT", "hc": "HEAT",
"he": "HE" "he": "HE"
}, },
@@ -759,11 +760,17 @@
"count_singular": "1 tank", "count_singular": "1 tank",
"count_plural": "{count} tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}", "sorting_by": "Sorting by {name}",
"clear": "Clear",
"tier": "Tier {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"no_tanks_found": { "no_tanks_found": {
"body": "No tanks found.", "body": "No tanks found.",
"search_again": "Try searching again.", "search_again": "Try searching again.",
"clear_filters": "Try clearing filters." "clear_filters": "Try clearing filters."
}, },
"sort": { "sort": {
"meta.none": "none", "meta.none": "none",
"survivability.health": "health", "survivability.health": "health",
@@ -917,7 +924,16 @@
"next": "Next", "next": "Next",
"no_results": "No results" "no_results": "No results"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
},
"clear": "Clear",
"select_all": "Select all",
"tier": "Tier {tier}"
}, },
"tankopedia": { "tankopedia": {
@@ -1267,10 +1283,12 @@
"upgrade": "Upgrade" "upgrade": "Upgrade"
}, },
"provisions": { "provisions": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
} }
} }
}, },
+22 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp", "hp": "{value}hp",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}°/s",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Auto cargador", "auto_loader": "Auto cargador",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "AP", "ap": "AP",
"apcr": "APCR", "ap_cr": "APCR",
"heat": "AEAC", "hc": "HEAT",
"he": "AE" "he": "AE"
}, },
"player_count_period": { "player_count_period": {
@@ -668,6 +669,10 @@
"count_singular": "1 tanque", "count_singular": "1 tanque",
"count_plural": "{count} tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}", "sorting_by": "Sorting by {name}",
"clear": "Limpiar",
"tier": "Tier {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"no_tanks_found": { "no_tanks_found": {
"body": "No se encontraron tanques.", "body": "No se encontraron tanques.",
"search_again": "Intenta buscar de nuevo.", "search_again": "Intenta buscar de nuevo.",
@@ -814,7 +819,16 @@
"next": "Next", "next": "Next",
"no_results": "Sin resultados" "no_results": "Sin resultados"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancelar",
"reveal": "Reveal"
},
"clear": "Limpiar",
"select_all": "Seleccionar todo",
"tier": "Tier {tier}"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -1135,10 +1149,12 @@
"upgrade": "Mejorar" "upgrade": "Mejorar"
}, },
"provisions": { "provisions": {
"clear": "Limpiar" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "Limpiar" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
} }
} }
}, },
+108 -92
View File
@@ -1,7 +1,7 @@
{ {
"test": { "test": {
"default": "teste", "default": "teste",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.", "long": "Ceci est une longue chaîne de test. Vous ne DEVEZ JAMAIS le voir dans l'interface utilisateur. Mon fruit préféré est {fruit}, car il a la couleur {color}. Je l'aime tellement que je vais dire son nom 3 fois ! {fruit}! {fruit}! {fruit} ! Encore une fois, c'est {color}, pas {not_color}.",
"fox": "Le renard brun et rapide saute par-dessus le chien paresseux." "fox": "Le renard brun et rapide saute par-dessus le chien paresseux."
}, },
"common": { "common": {
@@ -10,10 +10,11 @@
"m": "{value}m", "m": "{value}m",
"mm": "{value}mm", "mm": "{value}mm",
"deg": "{value}°", "deg": "{value}°",
"hp": "{value}hp", "hp": "{value}pv",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}°/s",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Chargement auto", "auto_loader": "Chargement auto",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "AP", "ap": "AP",
"apcr": "APCR", "ap_cr": "APCR",
"heat": "HEAT", "hc": "HEAT",
"he": "HE" "he": "HE"
}, },
"player_count_period": { "player_count_period": {
@@ -186,11 +187,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.", "clan_not_found": "# Impossible de trouver le clan\nImpossible de trouver «{search} ». Essayez de sélectionner un clan dans le résultat de la recherche.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", "player_not_found": "# Impossible de trouver l'utilisateur\nJe n'ai pas pu trouver l'utilisateur \"{search}\". Essayez de choisir un utilisateur dans le résultat de la recherche, en tapant un nom d'utilisateur valide, ou en utilisant la commande `/link`.",
"player_not_linked": "# Utilisez `/link` pour commencer\nJe ne sais pas qui vous êtes, mais vous pouvez me le dire aujourd'hui !", "player_not_linked": "# Utilisez `/link` pour commencer\nJe ne sais pas qui vous êtes, mais vous pouvez me le dire aujourd'hui !",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".", "tank_name_not_found": "# Char introuvable\nImpossible de trouver le char sous le nom \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.", "tank_id_not_found": "# Char introuvable\nImpossible de trouver le char avec l'ID \"{search}\". Essayez de taper le nom du char au lieu d'un nombre.",
"no_tank_stats": "# Aucune statistique de tank disponible\nCe joueur n'a pas de statistiques disponibles. Ce n'est peut-être pas le joueur que vous cherchez.", "no_tank_stats": "# Aucune statistique de tank disponible\nCe joueur n'a pas de statistiques disponibles. Ce n'est peut-être pas le joueur que vous cherchez.",
"get_help": "Aidez le serveur Discord", "get_help": "Aidez le serveur Discord",
"uncaught_error": "# Blitzkit a rencontré une erreur !\nNous sommes désolés de cela. N'hésitez pas à [rejoindre le serveur officiel Discord](https://discord.gg/nDt7AjGJQH) pour obtenir de l'aide.", "uncaught_error": "# Blitzkit a rencontré une erreur !\nNous sommes désolés de cela. N'hésitez pas à [rejoindre le serveur officiel Discord](https://discord.gg/nDt7AjGJQH) pour obtenir de l'aide.",
@@ -284,19 +285,19 @@
"name": "rang", "name": "rang",
"description": "Rang", "description": "Rang",
"choices": { "choices": {
"tier": "Tier {tier_1} - {tier_2}" "tier": "Rang {tier_}1 - {tier_}2"
} }
} }
}, },
"periods": { "periods": {
"career": "Carrière", "career": "Carrière",
"today": "Aujourd'hui", "today": "Aujourd'hui",
"days": "{days} days", "days": "{days} jours",
"custom": "{day_1} - {day_2} days" "custom": "{day_1} - {day_2} jours"
}, },
"filters": { "filters": {
"none": "Aucun filtre", "none": "Aucun filtre",
"tier": "Tier {tier}" "tier": "Rang {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "", "battles_in_period": "",
@@ -339,19 +340,19 @@
"body": { "body": {
"subtitle": "Recherche", "subtitle": "Recherche",
"total": "Total", "total": "Total",
"research": "Research: {cost}", "research": "Recherche : {cost}",
"upgrades": "Upgrades: {cost}", "upgrades": "Améliorations : {cost}",
"purchase": "Purchase: {cost}", "purchase": "Achat : {cost}",
"equipment": "Equipment: {cost}", "equipment": "Équipement : {cost}",
"estimation": "# Ceci est une estimation !\n\nCette liste peut inclure des chars déjà recherchés en raison de l'information inexacte fournie par Wargaming. Essayez de fournir un `char de départ` depuis la ligne lorsque vous utilisez la commande si vous le pouvez." "estimation": "# Ceci est une estimation !\n\nCette liste peut inclure des chars déjà recherchés en raison de l'information inexacte fournie par Wargaming. Essayez de fournir un `char de départ` depuis la ligne lorsque vous utilisez la commande si vous le pouvez."
}, },
"errors": { "errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.", "non_tech_tree": "# {tank} n'est pas dans l'arbre technologique\n\nVous ne pouvez pas rechercher un char premium/de collection.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.", "tier_1": "# {tank} est un char de rang I\n\nLe char de rang 1 sont déverrouillé gratuitement pour tous les joueurs.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.", "already_researched": "# {tank} est déjà recherché\n\nEssayez de fournir un `char de départ` en utilisant la commande pour voir la progression d'une lignée.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "start_end_equal": "# {tank} a été fourni deux fois\n\nAssurez-vous que `starting-tank` et `target-tank` sont différents.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.", "tanks_not_on_line": "{tank_}1 et {tank_}2 ne sont pas sur la même ligne \n\nAssurez-vous que `starting-tank` et `target-tank` sont sur la même ligne d'arbre technologique.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank." "unordered": "# {tank_1} niveau supérieur à {tank_2}\n\nAssurez-vous de fournir {tank_2} comme le `starting-tank` à la place car c'est le char de niveau inférieur."
} }
}, },
"about": { "about": {
@@ -394,7 +395,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Taux de victoires", "winrate": "Taux de victoires",
"damage": "Dégâts", "damage": "Dégâts",
"unknown_tank": "Unknown tank {id}" "unknown_tank": "Char {id} inconnu"
} }
}, },
"debug": { "debug": {
@@ -430,12 +431,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "seuil", "name": "seuil",
"description": "The number of days inactive (default: {days} days)" "description": "Le nombre de jours inactifs (par défaut: {days} jours)"
} }
}, },
"body": { "body": {
"subtitle": "Inactive for {days}+ days", "subtitle": "Inactif depuis plus de {days} jours",
"listing": "{days} days" "listing": "{days} jours"
} }
}, },
"full_stats": { "full_stats": {
@@ -478,7 +479,7 @@
"body": { "body": {
"accounts_linked": "Comptes liés", "accounts_linked": "Comptes liés",
"no_role_permissions": { "no_role_permissions": {
"title": "{name} failed to verify", "title": "La vérification de {name} a échouée",
"description": "Je n'ai pas l'autorisation de gérer les rôles." "description": "Je n'ai pas l'autorisation de gérer les rôles."
} }
}, },
@@ -493,11 +494,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "The max number of clan results (default: {limit})" "description": "Nombre maximum de résultats du clan (par défaut: {limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for {search} in region {region}", "title": "Recherche de clan {search} dans la région {region}",
"no_results": "Pas de clan trouvé" "no_results": "Pas de clan trouvé"
} }
}, },
@@ -507,11 +508,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "The max number of player results (default: {limit})" "description": "Le nombre maximum de résultats de joueur (par défaut: {limit})"
} }
}, },
"body": { "body": {
"title": "Player search for {search} in region {region}", "title": "Recherche de {search} dans la région {region}",
"no_results": "Pas de joueur de trouvé" "no_results": "Pas de joueur de trouvé"
} }
}, },
@@ -531,13 +532,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "How many neighbors to display (default: {count})" "description": "Combien de voisins afficher (par défaut : {count})"
}, },
"season": { "season": {
"name": "saison", "name": "saison",
"description": "Numéro de saison (défaut : courant)", "description": "Numéro de saison (défaut : courant)",
"choices": { "choices": {
"current": "{season} (current)" "current": "{season} (actuel)"
} }
}, },
"league": { "league": {
@@ -546,12 +547,12 @@
} }
}, },
"body": { "body": {
"top_players": "Top {count} players", "top_players": "Top {count} joueurs",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.", "no_participation": "# {player} n'a pas joué de cotées dans la saison {season}\n\nCe joueur n'a pas participé à cette saison ou n'a pas dépassé l'étalonnage.",
"no_ongoing_season": "Pas de saison en cours\n\nIl n'y a pas de saison de notation en cours actuellement.", "no_ongoing_season": "Pas de saison en cours\n\nIl n'y a pas de saison de notation en cours actuellement.",
"deleted_player": "Deleted {id}", "deleted_player": "{id} supprimé",
"neighbors": "évaluer les voisins", "neighbors": "évaluer les voisins",
"more": "+ {count} more" "more": "+ {count} plus"
} }
}, },
"today": { "today": {
@@ -566,7 +567,7 @@
"name": "chars possédés", "name": "chars possédés",
"description": "Affiche tous les chars d'un niveau", "description": "Affiche tous les chars d'un niveau",
"body": { "body": {
"title": "# {name}'s owned tier {tier} tanks\n\n" "title": "# Les chars du palier {tier} possédés par {name}"
} }
}, },
"permissions": { "permissions": {
@@ -602,92 +603,96 @@
"name": "info-joueur", "name": "info-joueur",
"description": "Informations de base sur un joueur", "description": "Informations de base sur un joueur",
"body": { "body": {
"title": "{name}'s information", "title": "Informations de {name}",
"nickname": "Nickname", "nickname": "Pseudo",
"battles": "Batailles", "battles": "Batailles",
"winrate": "Taux de victoires", "winrate": "Taux de victoires",
"account_id": "Account ID", "account_id": "Account ID",
"created": "Created", "created": "Créé",
"last_battle": "Last battle" "last_battle": "Dernière bataille"
} }
}, },
"rating": { "rating": {
"name": "côtée", "name": "côtée",
"description": "Rating player's statistics", "description": "Statistiques des batailles cotées du joueur",
"subcommands": { "subcommands": {
"today": { "today": {
"name": "aujourd'hui", "name": "aujourd'hui",
"description": "Today's rating statistics" "description": "Statistiques des batailles cotées d'aujourd'hui"
}, },
"season": { "season": {
"name": "saison", "name": "saison",
"description": "This season's rating statistics" "description": "Statistiques des batailles cotées de cette saison"
} }
}, },
"body": { "body": {
"subtitle": { "subtitle": {
"today": "Today's rating statistics", "today": "Statistiques de batailles cotées d'aujourd'hui",
"season": "This season's rating statistics" "season": "Statistiques de batailles cotées cette saison"
} }
}, },
"errors": { "errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.", "no_participation": "# {name} n’a pas joué en cotée\n\nCe joueur n’a pas participé au mode cotée cette saison.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours." "unrecorded_stats": "# Statistiques actuellement indisponibles\n\nWargaming rencontre peut-être une interruption de service.\nSi une saison est en cours, les statistiques devraient être disponibles sous 24 heures."
} }
} }
} }
}, },
"website": { "website": {
"redirect": { "redirect": {
"title": "301: moved permanently", "title": "301 : déplacé définitivement",
"body": "This page has moved. Redirecting..." "body": "Cette page a été déplacée. Redirection en cours…"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Paramètres",
"description": "Change BlitzKit settings", "description": "Modifier les paramètres de BlitzKit",
"dev_mode": "Development mode", "dev_mode": "Mode développement",
"language": "Language" "language": "Langue"
}, },
"common": { "common": {
"player_search": { "player_search": {
"hint": "Search players...", "hint": "Recherche de joueurs...",
"no_results": "No results", "no_results": "Aucun résultat",
"searching": "Searching..." "searching": "Recherche..."
}, },
"copy_button": { "copy_button": {
"copied": "Copied!" "copied": "Copié !"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks", "select_dialog_title": "Recherche de chars",
"swap_dialog_title": "Swap tank", "swap_dialog_title": "Échanger le char",
"clear_filters": "Clear filters", "clear_filters": "Supprimer les filtres",
"login": "Login to use this", "login": "Connectez-vous pour utiliser ceci",
"search_bar_hint": "Search tanks...", "search_bar_hint": "Recherche des tanks...",
"recent": "Recently viewed", "recent": "Vu récemment",
"select_all": "Select all", "select_all": "Sélectionner tout",
"count_singular": "1 tank", "count_singular": "1 char",
"count_plural": "{count} tanks", "count_plural": "{count} chars",
"sorting_by": "Sorting by {name}", "sorting_by": "Trier par {name}",
"clear": "Effacer",
"tier": "Rang {tier}",
"passive": "{name} (Passif)",
"active": "{name} (actif)",
"no_tanks_found": { "no_tanks_found": {
"body": "No tanks found.", "body": "Aucun tank trouvé.",
"search_again": "Try searching again.", "search_again": "Essayez de chercher à nouveau.",
"clear_filters": "Try clearing filters." "clear_filters": "Essayez de supprimer les filtres."
}, },
"sort": { "sort": {
"meta.none": "none", "meta.none": "aucun",
"survivability.health": "health", "survivability.health": "santé",
"survivability.viewRange": "view range", "survivability.viewRange": "portée de vue",
"survivability.camouflageStill": "camouflage still", "survivability.camouflageStill": "camouflage à l’arrêt",
"survivability.camouflageMoving": "camouflage moving", "survivability.camouflageMoving": "camouflage en movement",
"survivability.camouflageShooting": "camouflage shooting", "survivability.camouflageShooting": "camouflage en tirant",
"survivability.volume": "volume", "survivability.volume": "volume",
"survivability.length": "length", "survivability.length": "longueur",
"fire.dpm": "standard DPM", "fire.dpm": "DPM standard",
"fire.dpmPremium": "premium DPM", "fire.dpmPremium": "DPM premium",
"fire.reload": "reload", "fire.reload": "rechargement",
"fire.caliber": "caliber", "fire.caliber": "calibration",
"fire.standardPenetration": "standard penetration", "fire.standardPenetration": "pénétration standard",
"fire.premiumPenetration": "premium penetration", "fire.premiumPenetration": "pénétration premium",
"fire.tertiaryPenetration": "tertiary penetration", "fire.tertiaryPenetration": "tertiary penetration",
"fire.damage": "damage", "fire.damage": "damage",
"fire.moduleDamage": "module damage", "fire.moduleDamage": "module damage",
@@ -812,9 +817,18 @@
"skip": "Skip", "skip": "Skip",
"guess": "Guess", "guess": "Guess",
"next": "Next", "next": "Next",
"no_results": "No results" "no_results": "Aucun résultat"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} guessed correctly",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
},
"clear": "Clear",
"select_all": "Sélectionner tout",
"tier": "Rang {tier}"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -1135,10 +1149,12 @@
"upgrade": "Upgrade" "upgrade": "Upgrade"
}, },
"provisions": { "provisions": {
"clear": "Clear" "clear": "Clear",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "Clear" "clear": "Clear",
"clear_all": "Clear all tanks"
} }
} }
}, },
@@ -1195,8 +1211,8 @@
"seo_description": "Track your stats in real time as you play World of Tanks Blitz", "seo_description": "Track your stats in real time as you play World of Tanks Blitz",
"search": { "search": {
"lookup": "Look up a player to get started", "lookup": "Look up a player to get started",
"hint": "Search players...", "hint": "Recherche de joueurs...",
"searching": "Searching...", "searching": "Recherche...",
"no_results": "Pas de joueur de trouvé" "no_results": "Pas de joueur de trouvé"
}, },
"controls": { "controls": {
+28 -12
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp", "hp": "{value}hp",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}°/s",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "自動装填砲", "auto_loader": "自動装填砲",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "AP", "ap": "AP",
"apcr": "APCR", "ap_cr": "APCR",
"heat": "HEAT", "hc": "HEAT",
"he": "HE" "he": "HE"
}, },
"player_count_period": { "player_count_period": {
@@ -186,11 +187,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.", "clan_not_found": "",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", "player_not_found": "# ユーザーが見つかりませんでした\nユーザー「{search}」が見つかりませんでした。検索結果からユーザーを選択するか、有効なユーザー名を入力するか、/link コマンドを使用してください。",
"player_not_linked": "# `/link`を使用して始めましょう\nはじめまして!あなたが誰か、私に教えてください。", "player_not_linked": "# `/link`を使用して始めましょう\nはじめまして!あなたが誰か、私に教えてください。",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.", "tank_id_not_found": "# 車両が見つかりません\nID「{search}」の戦車が見つかりませんでした。数字ではなく戦車の名前を入力してみてください。",
"no_tank_stats": "# 利用可能な車輌統計がありません\nこのプレイヤーには利用可能な統計がありません。あなたが探しているプレイヤーではない可能性があります。", "no_tank_stats": "# 利用可能な車輌統計がありません\nこのプレイヤーには利用可能な統計がありません。あなたが探しているプレイヤーではない可能性があります。",
"get_help": "Discordサーバーに参加する", "get_help": "Discordサーバーに参加する",
"uncaught_error": "# BlitzKitがエラーに遭遇しました!\n申し訳ありません。お気軽に[公式Discordサーバー](https://discord.gg/nDt7AjGJQH)にお問い合わせください。", "uncaught_error": "# BlitzKitがエラーに遭遇しました!\n申し訳ありません。お気軽に[公式Discordサーバー](https://discord.gg/nDt7AjGJQH)にお問い合わせください。",
@@ -394,7 +395,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "勝率", "winrate": "勝率",
"damage": "ダメージ", "damage": "ダメージ",
"unknown_tank": "Unknown tank {id}" "unknown_tank": "不明な車両{id}"
} }
}, },
"debug": { "debug": {
@@ -559,7 +560,7 @@
"description": "Today's played tanks and statistics" "description": "Today's played tanks and statistics"
}, },
"stats": { "stats": {
"name": "stats", "name": "",
"description": "Quick statistics for a period" "description": "Quick statistics for a period"
}, },
"owned_tanks": { "owned_tanks": {
@@ -668,6 +669,10 @@
"count_singular": "1 輌", "count_singular": "1 輌",
"count_plural": "{count} tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}", "sorting_by": "Sorting by {name}",
"clear": "クリア",
"tier": "Tier {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"no_tanks_found": { "no_tanks_found": {
"body": "該当する車輌が存在しません。", "body": "該当する車輌が存在しません。",
"search_again": "もう一度検索してください。", "search_again": "もう一度検索してください。",
@@ -778,7 +783,7 @@
"tank": "車輌名", "tank": "車輌名",
"last_played": "Last played", "last_played": "Last played",
"battles": "Career battles", "battles": "Career battles",
"unrecorded": "--", "unrecorded": "",
"wins": "勝利" "wins": "勝利"
}, },
"large_warning": { "large_warning": {
@@ -814,7 +819,16 @@
"next": "Next", "next": "Next",
"no_results": "該当なし" "no_results": "該当なし"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
},
"clear": "クリア",
"select_all": "全て選択",
"tier": "Tier {tier}"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -1135,10 +1149,12 @@
"upgrade": "最終装備" "upgrade": "最終装備"
}, },
"provisions": { "provisions": {
"clear": "クリア" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "クリア" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
} }
} }
}, },
+22 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp", "hp": "{value}hp",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}°/s",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Auto loader", "auto_loader": "Auto loader",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "AP", "ap": "AP",
"apcr": "APCR", "ap_cr": "APCR",
"heat": "HEAT", "hc": "HEAT",
"he": "OB" "he": "OB"
}, },
"player_count_period": { "player_count_period": {
@@ -668,6 +669,10 @@
"count_singular": "1 tank", "count_singular": "1 tank",
"count_plural": "{count} tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}", "sorting_by": "Sorting by {name}",
"clear": "Clear",
"tier": "Tier {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"no_tanks_found": { "no_tanks_found": {
"body": "No tanks found.", "body": "No tanks found.",
"search_again": "Try searching again.", "search_again": "Try searching again.",
@@ -814,7 +819,16 @@
"next": "Next", "next": "Next",
"no_results": "Brak wyników" "no_results": "Brak wyników"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
},
"clear": "Clear",
"select_all": "Select all",
"tier": "Tier {tier}"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -1135,10 +1149,12 @@
"upgrade": "Upgrade" "upgrade": "Upgrade"
}, },
"provisions": { "provisions": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
} }
} }
}, },
+22 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value}hp", "hp": "{value}hp",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/s" "deg_s": "{value}°/s",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Autocarregador", "auto_loader": "Autocarregador",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "AP", "ap": "AP",
"apcr": "APCR", "ap_cr": "APCR",
"heat": "HEAT", "hc": "HEAT",
"he": "HE" "he": "HE"
}, },
"player_count_period": { "player_count_period": {
@@ -668,6 +669,10 @@
"count_singular": "1 tanque", "count_singular": "1 tanque",
"count_plural": "{count} tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}", "sorting_by": "Sorting by {name}",
"clear": "Limpar",
"tier": "Tier {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"no_tanks_found": { "no_tanks_found": {
"body": "Nenhum tanque encontrado.", "body": "Nenhum tanque encontrado.",
"search_again": "Tente pesquisar novamente.", "search_again": "Tente pesquisar novamente.",
@@ -814,7 +819,16 @@
"next": "Next", "next": "Next",
"no_results": "Sem resultados" "no_results": "Sem resultados"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancelar",
"reveal": "Reveal"
},
"clear": "Limpar",
"select_all": "Selecionar todos",
"tier": "Tier {tier}"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -1135,10 +1149,12 @@
"upgrade": "Melhorar" "upgrade": "Melhorar"
}, },
"provisions": { "provisions": {
"clear": "Limpar" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "Limpar" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
} }
} }
}, },
+23 -7
View File
@@ -13,7 +13,8 @@
"hp": "{value}оп", "hp": "{value}оп",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}°/с" "deg_s": "{value}°/с",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Барабан", "auto_loader": "Барабан",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "ББ", "ap": "ББ",
"apcr": "БП", "ap_cr": "БП",
"heat": "КС", "hc": "ОФ",
"he": "ОФ" "he": "ОФ"
}, },
"player_count_period": { "player_count_period": {
@@ -296,7 +297,7 @@
}, },
"filters": { "filters": {
"none": "Без фильтров", "none": "Без фильтров",
"tier": " {tier} уровня" "tier": "{tier} уровень"
}, },
"no_data": { "no_data": {
"battles_in_period": "За этот период боёв не было", "battles_in_period": "За этот период боёв не было",
@@ -668,6 +669,10 @@
"count_singular": "1 танк", "count_singular": "1 танк",
"count_plural": "Танков: {count}", "count_plural": "Танков: {count}",
"sorting_by": "Сортировать по {name}", "sorting_by": "Сортировать по {name}",
"clear": "Очистить",
"tier": "{tier} уровень",
"passive": "{name} (Пассивный)",
"active": "{name} (Активный)",
"no_tanks_found": { "no_tanks_found": {
"body": "Танков не найдено.", "body": "Танков не найдено.",
"search_again": "Попробуйте повторить поиск.", "search_again": "Попробуйте повторить поиск.",
@@ -814,7 +819,16 @@
"next": "Следующий", "next": "Следующий",
"no_results": "Ничего не найденo" "no_results": "Ничего не найденo"
}, },
"stats": "{correct} / {total} угадано верно" "stats": "{correct} / {total} правильно, серия: {streak}",
"help": {
"title": "Частично раскрыть танк?",
"description": "Показывает танк не показывая название. Этот раунд будет считаться проигранным и прекратит серию!",
"cancel": "Отмена",
"reveal": "Раскрыть"
},
"clear": "Очистить",
"select_all": "Выбрать всё",
"tier": "{tier} уровень"
}, },
"tankopedia": { "tankopedia": {
"search": "Поиск характеристик...", "search": "Поиск характеристик...",
@@ -1135,10 +1149,12 @@
"upgrade": "Топ" "upgrade": "Топ"
}, },
"provisions": { "provisions": {
"clear": "Очистить" "clear": "Убрать",
"clear_all": "Убрать со всех"
}, },
"consumables": { "consumables": {
"clear": "Очистить" "clear": "Убрать",
"clear_all": "Убрать со всех"
} }
} }
}, },
+88 -72
View File
@@ -13,7 +13,8 @@
"hp": "{value}од", "hp": "{value}од",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}град/c" "deg_s": "{value}град/c",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Магазинна система заряджання", "auto_loader": "Магазинна система заряджання",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "ББ", "ap": "ББ",
"apcr": "БП", "ap_cr": "APCR",
"heat": "КС", "hc": "HEAT",
"he": "ОФ" "he": "ОФ"
}, },
"player_count_period": { "player_count_period": {
@@ -78,11 +79,11 @@
"10": "Супер винятковий" "10": "Супер винятковий"
}, },
"nations": { "nations": {
"ussr": "СРСР", "ussr": "С.Р.С.Р.",
"germany": "Німеччина", "germany": "Німеччина",
"usa": "ЗША", "usa": "С.Ш.А.",
"china": "Китай", "china": "Китай",
"uk": "Велика Британія", "uk": "Сполучене Королівство ",
"france": "Франція", "france": "Франція",
"japan": "Японія", "japan": "Японія",
"european": "Європейська нація", "european": "Європейська нація",
@@ -120,7 +121,7 @@
"3": "ПТ-САУ" "3": "ПТ-САУ"
}, },
"tree_type": { "tree_type": {
"-1": "не розпізнано", "-1": "не розпізнаний",
"0": "Звичайний", "0": "Звичайний",
"1": "Преміум", "1": "Преміум",
"2": "Колекційний" "2": "Колекційний"
@@ -340,18 +341,18 @@
"subtitle": "Дослідження", "subtitle": "Дослідження",
"total": "Загалом", "total": "Загалом",
"research": "Дослідження: {cost}", "research": "Дослідження: {cost}",
"upgrades": "Upgrades: {cost}", "upgrades": "Прокачування: {cost}",
"purchase": "Вартість: {cost}", "purchase": "Вартість: {cost}",
"equipment": "Спорядження: {cost}", "equipment": "Спорядження: {cost}",
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди." "estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
}, },
"errors": { "errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.", "non_tech_tree": "#{tank} відсутній в дереві дослідження\n\nВи не можете дослідити преміум/колеційний танк.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.", "tier_1": "#{tank} І рівня \n\nТанки І рівня завжди розблоковані для всіх гравців безкоштовно",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.", "already_researched": "# {tank} вже було досліджено\n\nСпробуйте використати `початковий-танк` коли використовуєте команду для перегляду загальної гілки, яку ви вже дослідили",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "start_end_equal": "#{tank} було введено двічі \n\nПереконайтесь що `початковий-танк` та `ціль-танк` різні",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.", "tanks_not_on_line": "# {tank_1} та {tank_2} не в одній гілці\n\nПереконайтесь що `початковий-танк` та `ціль-танк` знаходяться в єдиній гілці дерева дослідження",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank." "unordered": "# {tank_1} вище рівнем відносно {tank_2}\n\nОбов’язково вкажіть {tank_2} як `початковий-танк`, оскільки це танк нижчого рівня."
} }
}, },
"about": { "about": {
@@ -394,7 +395,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Відсоток перемог", "winrate": "Відсоток перемог",
"damage": "Шкода", "damage": "Шкода",
"unknown_tank": "Unknown tank {id}" "unknown_tank": "Невідомий танк {id}"
} }
}, },
"debug": { "debug": {
@@ -430,11 +431,11 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "поріг", "name": "поріг",
"description": "The number of days inactive (default: {days} days)" "description": "Кількість днів неактивності (за замовчуванням: {days} днів)"
} }
}, },
"body": { "body": {
"subtitle": "Inactive for {days}+ days", "subtitle": "Неактивний {days}+ днів",
"listing": "{days} днів" "listing": "{days} днів"
} }
}, },
@@ -478,7 +479,7 @@
"body": { "body": {
"accounts_linked": "Прив'язані акаунти", "accounts_linked": "Прив'язані акаунти",
"no_role_permissions": { "no_role_permissions": {
"title": "{name} failed to verify", "title": "{name} не вдалося перевірити",
"description": "У мене немає дозволу керувати ролями." "description": "У мене немає дозволу керувати ролями."
} }
}, },
@@ -493,11 +494,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "ліміт", "name": "ліміт",
"description": "The max number of clan results (default: {limit})" "description": "Максимальна кількість результатів клану (за замовчуванням: {limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for {search} in region {region}", "title": "Пошук клану для {search} в регіоні {region}",
"no_results": "Жодного клану не знайдено" "no_results": "Жодного клану не знайдено"
} }
}, },
@@ -507,11 +508,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "ліміт", "name": "ліміт",
"description": "The max number of player results (default: {limit})" "description": "Максимальна кількість результатів гравця (за замовчуванням: {limit})"
} }
}, },
"body": { "body": {
"title": "Player search for {search} in region {region}", "title": "Пошук гравця для {search} у регіоні {region}",
"no_results": "Гравця не знайдено" "no_results": "Гравця не знайдено"
} }
}, },
@@ -520,8 +521,8 @@
"description": "Статистика рейтингових боїв", "description": "Статистика рейтингових боїв",
"subcommands": { "subcommands": {
"neighbors": { "neighbors": {
"name": "neighbors", "name": "сусіди",
"description": "Neighboring players on the leaderboard" "description": "Сусідуючі гравці в лідерборді"
}, },
"league": { "league": {
"name": "ліга", "name": "ліга",
@@ -531,32 +532,32 @@
"options": { "options": {
"limit": { "limit": {
"name": "ліміт", "name": "ліміт",
"description": "How many neighbors to display (default: {count})" "description": "Скільки сусідів відобразити (за замовчуванням: {count})"
}, },
"season": { "season": {
"name": "сезон", "name": "сезон",
"description": "Базова інформація про гравця", "description": "Базова інформація про гравця",
"choices": { "choices": {
"current": "{season} (current)" "current": "{season} (поточний)"
} }
}, },
"league": { "league": {
"name": "ліга", "name": "ліга",
"description": "The league to display" "description": "Ліга для відображення"
} }
}, },
"body": { "body": {
"top_players": "Top {count} players", "top_players": "Топ {count} гравців",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.", "no_participation": "# {player} не грав у рейтингу в сезоні {season}\n\nЦей гравець не брав участь у цьому сезоні або не проходив калібрування",
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "# Немає поточного сезону\n\nВ даний час немає сезону рейтингу.",
"deleted_player": "", "deleted_player": "",
"neighbors": "Rating neighbors", "neighbors": "Сусіди по рейтингу",
"more": "+ {count} more" "more": "+ {count} ще"
} }
}, },
"today": { "today": {
"name": "сьогодні", "name": "сьогодні",
"description": "Today's played tanks and statistics" "description": "Сьогодні зіграні танки та статистика"
}, },
"stats": { "stats": {
"name": "статистика", "name": "статистика",
@@ -566,43 +567,43 @@
"name": "отримані танки", "name": "отримані танки",
"description": "Показує всі ваші танки даного рівня", "description": "Показує всі ваші танки даного рівня",
"body": { "body": {
"title": "# {name}'s owned tier {tier} tanks\n\n" "title": "# {name} отриманих рівнів {tier} танків\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "дозволи", "name": "дозволи",
"description": "Checks wether the bot has the required permissions to function", "description": "Перевіряє чи бот має необхідні дозволи для роботи",
"body": { "body": {
"title": "# Permissions\n\n{body}", "title": "# Дозволи\n\n{body}",
"view_channels": "View channels: needed to find the message the refresh button was used on", "view_channels": "Перегляньте канали: необхідне для знаходження повідомлення, на якому використовувалася кнопка оновлення",
"read_message_history": "Read message history: needed to find the message to refresh", "read_message_history": "Прочитайте історію повідомлень: потрібно знайти повідомлення для оновлення",
"attach_files": "Attach files: needed to add images to image-based commands", "attach_files": "Закріпіть файли: потрібно для додавання зображень до команд на основі зображень",
"external_emojis": "External emojis: needed to add national flags and tank class emojis" "external_emojis": "Зовнішні емодзі: потрібні для додавання емодзі національних прапорів та класів техніки"
} }
}, },
"ping": { "ping": {
"name": "", "name": "",
"description": "Check wether the bot is alive.", "description": "Перевірити чи бот живий",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Понг🏓",
"pong_with_time": "Pong 🏓 - {time}ms" "pong_with_time": "Понг🏓 - {time}мс"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
"name": "blitzkit", "name": "BlitzKit",
"description": "Ping BlitzKit" "description": "Ping BlitzKit"
}, },
"wotb": { "wotb": {
"name": "wotb", "name": "WoTB",
"description": "Ping World of Tanks Blitz" "description": "Ping World of Tanks Blitz"
} }
} }
}, },
"player_info": { "player_info": {
"name": "player-info", "name": "інформація про гравця",
"description": "Базова інформація про гравця", "description": "Базова інформація про гравця",
"body": { "body": {
"title": "{name}'s information", "title": "інформація про {name}",
"nickname": "Нік", "nickname": "Нік",
"battles": "Бої", "battles": "Бої",
"winrate": "Відсоток перемог", "winrate": "Відсоток перемог",
@@ -613,33 +614,33 @@
}, },
"rating": { "rating": {
"name": "рейтинг", "name": "рейтинг",
"description": "Rating player's statistics", "description": "Статистика рейтингових боїв гравця ",
"subcommands": { "subcommands": {
"today": { "today": {
"name": "сьогодні", "name": "сьогодні",
"description": "Today's rating statistics" "description": "Сьогоднішня статистика рейтингових боїв"
}, },
"season": { "season": {
"name": "сезон", "name": "сезон",
"description": "This season's rating statistics" "description": "Статистика рейтингових боїв поточного сезону"
} }
}, },
"body": { "body": {
"subtitle": { "subtitle": {
"today": "Today's rating statistics", "today": "Сьогоднішня статистика рейтингових боїв",
"season": "This season's rating statistics" "season": "Статистика рейтингових боїв даного сезону"
} }
}, },
"errors": { "errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.", "no_participation": "# {name} не грав рейтингові бої\n\nЦей гравець не брав участь у рейтинговому режимі цього сезону.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours." "unrecorded_stats": "# Статистика на цей момент недоступна\n\nWargaming наразі зіткнулась з технічними несправностями. Якщо зараз проходить сезон, статистика буде доступною в найближчі 24 години."
} }
} }
} }
}, },
"website": { "website": {
"redirect": { "redirect": {
"title": "301: moved permanently", "title": "301: переміщено назавжди",
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
@@ -668,6 +669,10 @@
"count_singular": "1 танк", "count_singular": "1 танк",
"count_plural": "Танків: {count} ", "count_plural": "Танків: {count} ",
"sorting_by": "Відсортувати за {name}", "sorting_by": "Відсортувати за {name}",
"clear": "Clear",
"tier": "Рівень {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"no_tanks_found": { "no_tanks_found": {
"body": "Нічого не знайдено.", "body": "Нічого не знайдено.",
"search_again": "Спробуйте шукати заново.", "search_again": "Спробуйте шукати заново.",
@@ -814,7 +819,16 @@
"next": "Next", "next": "Next",
"no_results": "Немає результатів" "no_results": "Немає результатів"
}, },
"stats": "{correct} / {total} guessed correctly" "stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
},
"clear": "Clear",
"select_all": "Вибрати все",
"tier": "Рівень {tier}"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -823,33 +837,33 @@
"description": "Танкова енциклопедія", "description": "Танкова енциклопедія",
"seo_description": "Характеристики, броня та інше для всіх танків WoT Blitz", "seo_description": "Характеристики, броня та інше для всіх танків WoT Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.", "tank_seo_description": "{name} броня, характеристики, обладнання, амуніція і т.д. {dpm} шкода на хвилину, {alpha} разова шкода, {penetration}мм пробиття, {speed} км/год максимальна швидкість, {health} міцність.",
"visualizers": { "visualizers": {
"reload": { "reload": {
"skip": "Skip" "skip": "Пропустити"
}, },
"assault": { "assault": {
"title": "Assault shells" "title": "Штурмові снаряди"
}, },
"rotator": { "rotator": {
"hull": "Hull", "hull": "Корпус",
"turret": "Turret", "turret": "Башта",
"hard": "Hard", "hard": "Жорстко",
"medium": "Середній", "medium": "Середній",
"soft": "Soft", "soft": "Ніжно",
"effective": "Effective" "effective": "Ефективний"
}, },
"ricochet": { "ricochet": {
"angle": "Angle", "angle": "Кут",
"normalization": "Нормалізація", "normalization": "Нормалізація",
"effective_angle": "Effective angle", "effective_angle": "Ефективний кут",
"armor": "Броня", "armor": "Броня",
"no_ricochet": "Explosive shells don't ricochet" "no_ricochet": "Фугасні снаряди не рикошетять"
}, },
"view_range": { "view_range": {
"camo": "Camo", "camo": "Камуфляж",
"ghost_factory": "Ghost Factory", "ghost_factory": "Промзона",
"dead_rail": "Dead Rail" "dead_rail": "Ешелон"
}, },
"flexibility": { "flexibility": {
"yaw": "Yaw", "yaw": "Yaw",
@@ -1135,10 +1149,12 @@
"upgrade": "Upgrade" "upgrade": "Upgrade"
}, },
"provisions": { "provisions": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
}, },
"consumables": { "consumables": {
"clear": "Clear" "clear": "Clear this tank",
"clear_all": "Clear all tanks"
} }
} }
}, },
+22 -6
View File
@@ -13,7 +13,8 @@
"hp": "{value} 生命值", "hp": "{value} 生命值",
"x": "x{value}", "x": "x{value}",
"percentage": "{value}%", "percentage": "{value}%",
"deg_s": "{value}度°/秒" "deg_s": "{value}度°/秒",
"plus": "+{value}"
}, },
"gun_types": { "gun_types": {
"auto_loader": "弹夹型坦克", "auto_loader": "弹夹型坦克",
@@ -22,8 +23,8 @@
}, },
"shells": { "shells": {
"ap": "穿甲弹", "ap": "穿甲弹",
"apcr": "硬式复合穿甲弹", "ap_cr": "硬式复合穿甲弹",
"heat": "反战车高爆弹", "hc": "反战车高爆弹",
"he": "高爆弹" "he": "高爆弹"
}, },
"player_count_period": { "player_count_period": {
@@ -668,6 +669,10 @@
"count_singular": "1辆战车", "count_singular": "1辆战车",
"count_plural": "{count} 辆坦克", "count_plural": "{count} 辆坦克",
"sorting_by": "按 {name} 排序", "sorting_by": "按 {name} 排序",
"clear": "清除",
"tier": "{tier} 阶",
"passive": "{name} (被动)",
"active": "{name} (主动)",
"no_tanks_found": { "no_tanks_found": {
"body": "找不到战车。", "body": "找不到战车。",
"search_again": "请尝试重新搜索。", "search_again": "请尝试重新搜索。",
@@ -814,7 +819,16 @@
"next": "下一个", "next": "下一个",
"no_results": "沒有結果" "no_results": "沒有結果"
}, },
"stats": "猜对了 {correct} / {total}" "stats": "猜对了 {correct} / {total}",
"help": {
"title": "显示坦克?",
"description": "显示坦克而不显示名字。这轮会被视为不正确,并打破你的连续记录!",
"cancel": "取消",
"reveal": "显示"
},
"clear": "清除",
"select_all": "全选",
"tier": "{tier} 阶"
}, },
"tankopedia": { "tankopedia": {
"search": "搜索属性……", "search": "搜索属性……",
@@ -1135,10 +1149,12 @@
"upgrade": "顶级配置" "upgrade": "顶级配置"
}, },
"provisions": { "provisions": {
"clear": "清除" "clear": "清除",
"clear_all": "清除所有坦克"
}, },
"consumables": { "consumables": {
"clear": "清除" "clear": "清除",
"clear_all": "清除所有坦克"
} }
} }
}, },
+134 -138
View File
@@ -1557,87 +1557,85 @@ export async function definitions() {
const includeRaw = consumable.vehicleFilter?.include.vehicle; const includeRaw = consumable.vehicleFilter?.include.vehicle;
const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle; const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle;
if (!entry.game_mode_exclusive) { if (includeRaw) {
if (includeRaw) { entry.include = [];
entry.include = [];
if ("minLevel" in includeRaw) { if ("minLevel" in includeRaw) {
entry.include.push({ entry.include.push({
filter_type: { filter_type: {
$case: "tiers", $case: "tiers",
value: { value: {
min: includeRaw.minLevel, min: includeRaw.minLevel,
max: includeRaw.maxLevel, max: includeRaw.maxLevel,
},
}, },
}); },
} else if ("name" in includeRaw) { });
entry.include.push({ } else if ("name" in includeRaw) {
filter_type: { entry.include.push({
$case: "ids", filter_type: {
value: { $case: "ids",
ids: includeRaw.name.split(/ +/).map((key) => { value: {
return tankStringIdMap[key]; ids: includeRaw.name.split(/ +/).map((key) => {
}), return tankStringIdMap[key];
}, }),
}, },
}); },
} else throw new SyntaxError("Unhandled include type"); });
} else throw new SyntaxError("Unhandled include type");
if (consumable.vehicleFilter?.include.nations) { if (consumable.vehicleFilter?.include.nations) {
entry.include.push({ entry.include.push({
filter_type: { filter_type: {
$case: "nations", $case: "nations",
value: { value: {
nations: consumable.vehicleFilter.include.nations.split(" "), nations: consumable.vehicleFilter.include.nations.split(" "),
},
}, },
}); },
} });
} }
}
if (excludeRaw) { if (excludeRaw) {
entry.exclude = []; entry.exclude = [];
if ("name" in excludeRaw) { if ("name" in excludeRaw) {
entry.exclude!.push({ entry.exclude!.push({
filter_type: { filter_type: {
$case: "ids", $case: "ids",
value: { value: {
ids: excludeRaw.name.split(/ +/).map((key) => { ids: excludeRaw.name.split(/ +/).map((key) => {
return tankStringIdMap[key]; return tankStringIdMap[key];
}), }),
},
}, },
}); },
} else if ("extendedTags" in excludeRaw) { });
entry.exclude!.push({ } else if ("extendedTags" in excludeRaw) {
filter_type: { entry.exclude!.push({
$case: "categories", filter_type: {
value: { $case: "categories",
categories: excludeRaw.extendedTags value: {
.split(" ") categories: excludeRaw.extendedTags
.map( .split(" ")
(item) => .map(
blitzTankFilterDefinitionCategoryToBlitzkit[ (item) =>
item as BlitzTankFilterDefinitionCategory blitzTankFilterDefinitionCategoryToBlitzkit[
] item as BlitzTankFilterDefinitionCategory
), ]
}, ),
}, },
}); },
} else throw new SyntaxError("Unhandled exclude type"); });
} else throw new SyntaxError("Unhandled exclude type");
if (consumable.vehicleFilter?.exclude?.nations) { if (consumable.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({ entry.exclude!.push({
filter_type: { filter_type: {
$case: "nations", $case: "nations",
value: { value: {
nations: consumable.vehicleFilter.exclude.nations.split(" "), nations: consumable.vehicleFilter.exclude.nations.split(" "),
},
}, },
}); },
} });
} }
} }
}); });
@@ -1657,87 +1655,85 @@ export async function definitions() {
const includeRaw = provision.vehicleFilter?.include.vehicle; const includeRaw = provision.vehicleFilter?.include.vehicle;
const excludeRaw = provision.vehicleFilter?.exclude?.vehicle; const excludeRaw = provision.vehicleFilter?.exclude?.vehicle;
if (!entry.game_mode_exclusive) { if (includeRaw) {
if (includeRaw) { entry.include = [];
entry.include = [];
if ("minLevel" in includeRaw) { if ("minLevel" in includeRaw) {
entry.include.push({ entry.include.push({
filter_type: { filter_type: {
$case: "tiers", $case: "tiers",
value: { value: {
min: includeRaw.minLevel, min: includeRaw.minLevel,
max: includeRaw.maxLevel, max: includeRaw.maxLevel,
},
}, },
}); },
} else if ("name" in includeRaw) { });
entry.include.push({ } else if ("name" in includeRaw) {
filter_type: { entry.include.push({
$case: "ids", filter_type: {
value: { $case: "ids",
ids: includeRaw.name.split(/ +/).map((key) => { value: {
return tankStringIdMap[key]; ids: includeRaw.name.split(/ +/).map((key) => {
}), return tankStringIdMap[key];
}, }),
}, },
}); },
} else throw new SyntaxError("Unhandled include type"); });
} else throw new SyntaxError("Unhandled include type");
if (provision.vehicleFilter?.include.nations) { if (provision.vehicleFilter?.include.nations) {
entry.include.push({ entry.include.push({
filter_type: { filter_type: {
$case: "nations", $case: "nations",
value: { value: {
nations: provision.vehicleFilter.include.nations.split(" "), nations: provision.vehicleFilter.include.nations.split(" "),
},
}, },
}); },
} });
} }
}
if (excludeRaw) { if (excludeRaw) {
entry.exclude = []; entry.exclude = [];
if ("name" in excludeRaw) { if ("name" in excludeRaw) {
entry.exclude!.push({ entry.exclude!.push({
filter_type: { filter_type: {
$case: "ids", $case: "ids",
value: { value: {
ids: excludeRaw.name ids: excludeRaw.name
.split(/ +/) .split(/ +/)
.map((key) => tankStringIdMap[key]), .map((key) => tankStringIdMap[key]),
},
}, },
}); },
} else if ("extendedTags" in excludeRaw) { });
entry.exclude!.push({ } else if ("extendedTags" in excludeRaw) {
filter_type: { entry.exclude!.push({
$case: "categories", filter_type: {
value: { $case: "categories",
categories: excludeRaw.extendedTags value: {
.split(" ") categories: excludeRaw.extendedTags
.map( .split(" ")
(item) => .map(
blitzTankFilterDefinitionCategoryToBlitzkit[ (item) =>
item as BlitzTankFilterDefinitionCategory blitzTankFilterDefinitionCategoryToBlitzkit[
] item as BlitzTankFilterDefinitionCategory
), ]
}, ),
}, },
}); },
} else throw new SyntaxError("Unhandled exclude type"); });
} else throw new SyntaxError("Unhandled exclude type");
if (provision.vehicleFilter?.exclude?.nations) { if (provision.vehicleFilter?.exclude?.nations) {
entry.exclude!.push({ entry.exclude!.push({
filter_type: { filter_type: {
$case: "nations", $case: "nations",
value: { value: {
nations: provision.vehicleFilter.exclude.nations.split(" "), nations: provision.vehicleFilter.exclude.nations.split(" "),
},
}, },
}); },
} });
} }
} }
@@ -1,21 +0,0 @@
---
import type { GetStaticPaths } from "astro";
import { api } from "../../../core/api/dynamic";
export const getStaticPaths = (async () => {
const { tanks } = await api.tanks();
return tanks.map((tank) => ({
params: { slug: tank.slug },
props: { id: tank.id },
}));
}) satisfies GetStaticPaths;
const { id } = Astro.props;
---
<h1>Tank {id}</h1>
<code>
<pre>{JSON.stringify(await api.tank(id), null, 2)}</pre>
</code>
@@ -277,14 +277,14 @@ export function CompareTable({ stats }: CompareTableProps) {
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
<Flex align="center" justify="center" gap="3"> <Flex justify="center" gap="3">
<Popover.Root> <Popover.Root>
<Popover.Trigger> <Popover.Trigger>
<Button <Button
variant="ghost" variant="ghost"
radius="large" radius="large"
style={{ style={{
height: "100%", height: "2rem",
width: 12, width: 12,
position: "relative", position: "relative",
}} }}
@@ -345,7 +345,24 @@ export function CompareTable({ stats }: CompareTableProps) {
}); });
}} }}
/> />
<Flex justify="end" mt="4">
<Flex justify="end" mt="4" gap="3">
<Button
variant="ghost"
onClick={() => {
CompareEphemeral.mutate((draft) => {
draft.members.map((member) => {
member.provisions = [];
});
});
}}
>
{
strings.website.tools.compare.table.provisions
.clear_all
}
</Button>
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
@@ -473,7 +490,7 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="ghost" variant="ghost"
radius="large" radius="large"
style={{ style={{
height: "100%", height: "2rem",
width: 12, width: 12,
position: "relative", position: "relative",
}} }}
@@ -536,7 +553,23 @@ export function CompareTable({ stats }: CompareTableProps) {
} }
/> />
<Flex justify="end" mt="4"> <Flex justify="end" mt="4" gap="4">
<Button
variant="ghost"
onClick={() => {
CompareEphemeral.mutate((draft) => {
draft.members.map((member) => {
member.consumables = [];
});
});
}}
>
{
strings.website.tools.compare.table.consumables
.clear_all
}
</Button>
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
@@ -1,6 +1,6 @@
import type { Consumable } from '@blitzkit/core'; import type { Consumable } from "@blitzkit/core";
import { Flex, type FlexProps } from '@radix-ui/themes'; import { Flex, type FlexProps } from "@radix-ui/themes";
import { ConsumableButton } from './ModuleButtons/ConsumableButton'; import { ConsumableButton } from "./ModuleButtons/ConsumableButton";
type ConsumablesManagerProps = FlexProps & { type ConsumablesManagerProps = FlexProps & {
consumables: Consumable[]; consumables: Consumable[];
@@ -27,6 +27,8 @@ export function ConsumablesManager({
return ( return (
<Flex wrap="wrap" gap="2" {...props}> <Flex wrap="wrap" gap="2" {...props}>
{consumables.map((consumable) => { {consumables.map((consumable) => {
if (consumable.game_mode_exclusive) return null;
const isSelected = selected.some((id) => id === consumable.id); const isSelected = selected.some((id) => id === consumable.id);
return ( return (
@@ -9,12 +9,15 @@ const CONTRAST = 2 ** 2;
export function GuessRenderer() { export function GuessRenderer() {
const guessState = Guess.use((state) => state.guessState); const guessState = Guess.use((state) => state.guessState);
const isRevealed = guessState !== GuessState.NotGuessed; const helpingReveal = Guess.use((state) => state.helpingReveal);
const isRevealed = helpingReveal || guessState !== GuessState.NotGuessed;
return ( return (
<SmartCanvas <SmartCanvas
style={{ style={{
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`, filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${
isRevealed ? 1 : CONTRAST
}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
transitionDuration: isRevealed ? "2s" : undefined, transitionDuration: isRevealed ? "2s" : undefined,
transitionProperty: "filter", transitionProperty: "filter",
}} }}
+190 -55
View File
@@ -7,20 +7,27 @@ import {
import { literals } from "@blitzkit/i18n"; import { literals } from "@blitzkit/i18n";
import { import {
ArrowRightIcon, ArrowRightIcon,
EyeOpenIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
MixerVerticalIcon,
PaperPlaneIcon, PaperPlaneIcon,
StarFilledIcon,
TrashIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { import {
AlertDialog,
Box, Box,
Button, Button,
Card, Card,
DropdownMenu,
Flex, Flex,
IconButton,
Spinner, Spinner,
Text, Text,
TextField, TextField,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import { debounce } from "lodash-es"; import { debounce, times } from "lodash-es";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
import { awaitableTankNames } from "../core/awaitables/tankNames"; import { awaitableTankNames } from "../core/awaitables/tankNames";
@@ -43,11 +50,14 @@ export function Guesser() {
const guessState = Guess.use((state) => state.guessState); const guessState = Guess.use((state) => state.guessState);
const correctGuesses = Guess.use((state) => state.correctGuesses); const correctGuesses = Guess.use((state) => state.correctGuesses);
const totalGuesses = Guess.use((state) => state.totalGuesses); const totalGuesses = Guess.use((state) => state.totalGuesses);
const helpingReveal = Guess.use((state) => state.helpingReveal);
const streak = Guess.use((state) => state.streak);
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [results, setResults] = useState<TankDefinition[] | null>(null); const [results, setResults] = useState<TankDefinition[] | null>(null);
const [selected, setSelected] = useState<TankDefinition | null>(null); const [selected, setSelected] = useState<TankDefinition | null>(null);
const tiers = Guess.use((state) => state.tiers);
const requestSearch = useCallback(() => { const requestSearch = useCallback(() => {
setSelected(null); setSelected(null);
@@ -92,76 +102,150 @@ export function Guesser() {
width="100%" width="100%"
p="4" p="4"
maxWidth="25rem" maxWidth="25rem"
gap="4" gap="3"
> >
{results !== null && ( <Box position="relative">
<Card variant="classic"> {results !== null && (
<Box py="2" px="3"> <Card variant="classic">
{results.length === 0 && ( <Box py="2" px="3">
<Flex justify="center"> {results.length === 0 && (
<Text color="gray"> <Flex justify="center">
{strings.website.tools.guess.search.no_results} <Text color="gray">
</Text> {strings.website.tools.guess.search.no_results}
</Flex> </Text>
)} </Flex>
)}
<SearchResults.Root> <SearchResults.Root>
{results.map((result) => { {results.map((result) => {
const Icon = classIcons[result.class]; const Icon = classIcons[result.class];
return ( return (
<SearchResults.Item <SearchResults.Item
key={result.id} key={result.id}
onClick={() => { onClick={() => {
if (!input.current) return; if (!input.current) return;
input.current.value = unwrap(result.name); input.current.value = unwrap(result.name);
setSelected(result); setSelected(result);
setResults(null); setResults(null);
}} }}
text={unwrap(result.name)} text={unwrap(result.name)}
prefix={ prefix={
<img <img
style={{ width: "1em", height: "1em" }} style={{ width: "1em", height: "1em" }}
src={asset(`flags/circle/${result.nation}.webp`)} src={asset(`flags/circle/${result.nation}.webp`)}
/> />
} }
discriminator={ discriminator={
<Flex <Flex
align="center" align="center"
gap="1" gap="1"
width="38px" width="38px"
justify="center" justify="center"
> >
<Icon width="1em" height="1em" /> <Icon width="1em" height="1em" />
{TIER_ROMAN_NUMERALS[result.tier]} {TIER_ROMAN_NUMERALS[result.tier]}
</Flex> </Flex>
} }
/> />
); );
})} })}
</SearchResults.Root> </SearchResults.Root>
</Box> </Box>
</Card> </Card>
)} )}
</Box>
<Flex justify="center" style={{ userSelect: "none" }}> <Flex justify="center" style={{ userSelect: "none" }}>
<Text> <Text>
{literals(strings.website.tools.guess.stats, { {literals(strings.website.tools.guess.stats, {
correct: correctGuesses, correct: correctGuesses,
total: totalGuesses, total: totalGuesses,
streak: streak,
})} })}
</Text> </Text>
</Flex> </Flex>
<Flex gap="3"> <Flex gap={{ initial: "2", sm: "3" }}>
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger>
<IconButton
size={{ initial: "2", sm: "3" }}
variant="surface"
color="gray"
>
<MixerVerticalIcon />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{times(10, (index) => {
const tier = 10 - index;
const isSelected = tiers.includes(tier);
return (
<DropdownMenu.CheckboxItem
checked={isSelected}
key={tier}
onClick={(event) => {
event.preventDefault();
Guess.mutate((draft) => {
if (isSelected) {
if (draft.tiers.length === 1) return;
draft.tiers = draft.tiers.filter((t) => t !== tier);
} else {
draft.tiers = [...draft.tiers, tier];
}
draft.streak = 0;
});
}}
>
{literals(strings.website.tools.guess.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
})}
</DropdownMenu.CheckboxItem>
);
})}
<DropdownMenu.Separator />
<DropdownMenu.Item
onClick={(event) => {
event.preventDefault();
Guess.mutate((draft) => {
draft.tiers = Guess.initial.tiers;
});
}}
>
<StarFilledIcon /> {strings.website.tools.guess.select_all}
</DropdownMenu.Item>
<DropdownMenu.Item
color="red"
onClick={(event) => {
event.preventDefault();
Guess.mutate((draft) => {
draft.tiers = [];
});
}}
>
<TrashIcon /> {strings.website.tools.guess.clear}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<TextField.Root <TextField.Root
disabled={guessState !== GuessState.NotGuessed} disabled={guessState !== GuessState.NotGuessed}
ref={input} ref={input}
onChange={requestSearch} onChange={requestSearch}
style={{ flex: 1 }} style={{ flex: 1, maxWidth: "14rem" }}
placeholder={strings.website.tools.guess.search.placeholder} placeholder={strings.website.tools.guess.search.placeholder}
size="3" size={{ initial: "2", sm: "3" }}
variant="classic" variant="classic"
> >
<TextField.Slot> <TextField.Slot>
@@ -169,8 +253,50 @@ export function Guesser() {
</TextField.Slot> </TextField.Slot>
</TextField.Root> </TextField.Root>
<AlertDialog.Root>
<AlertDialog.Trigger>
<IconButton
size={{ initial: "2", sm: "3" }}
disabled={guessState !== GuessState.NotGuessed}
>
<EyeOpenIcon />
</IconButton>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Title>
{strings.website.tools.guess.help.title}
</AlertDialog.Title>
<AlertDialog.Description>
{strings.website.tools.guess.help.description}
</AlertDialog.Description>
<Flex justify="end" gap="2">
<AlertDialog.Cancel>
<Button variant="outline">
{strings.website.tools.guess.help.cancel}
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button
color="tomato"
onClick={() => {
Guess.mutate((draft) => {
draft.helpingReveal = true;
draft.streak = 0;
});
}}
>
{strings.website.tools.guess.help.reveal}
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<Button <Button
size="3" size={{ initial: "2", sm: "3" }}
color={ color={
guessState === GuessState.NotGuessed && selected === null guessState === GuessState.NotGuessed && selected === null
? "red" ? "red"
@@ -178,7 +304,8 @@ export function Guesser() {
} }
onClick={() => { onClick={() => {
if (guessState === GuessState.NotGuessed) { if (guessState === GuessState.NotGuessed) {
const correct = selected !== null && selected.id === tank.id; const correct =
selected !== null && selected.id === tank.id && !helpingReveal;
Guess.mutate((draft) => { Guess.mutate((draft) => {
draft.guessState = correct draft.guessState = correct
@@ -186,9 +313,16 @@ export function Guesser() {
: GuessState.Incorrect; : GuessState.Incorrect;
draft.totalGuesses++; draft.totalGuesses++;
draft.correctGuesses += correct ? 1 : 0; draft.correctGuesses += correct ? 1 : 0;
draft.streak = correct ? draft.streak + 1 : 0;
}); });
} else { } else {
const id = Number(ids[Math.floor(Math.random() * ids.length)]); const filteredIds = ids.filter((id) => {
const tank = tankDefinitions.tanks[Number(id)];
return tiers.includes(tank.tier);
});
const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)]
);
const tank = tankDefinitions.tanks[id]; const tank = tankDefinitions.tanks[id];
Guess.mutate((draft) => { Guess.mutate((draft) => {
@@ -196,6 +330,7 @@ export function Guesser() {
draft.tank = tank; draft.tank = tank;
draft.guessState = GuessState.NotGuessed; draft.guessState = GuessState.NotGuessed;
draft.helpingReveal = false;
input.current.value = ""; input.current.value = "";
}); });
@@ -1,5 +1,6 @@
import { Flex } from '@radix-ui/themes'; import { Flex } from "@radix-ui/themes";
import { ProvisionButton } from './ModuleButtons/ProvisionButton'; import { awaitableProvisionDefinitions } from "../core/awaitables/provisionDefinitions";
import { ProvisionButton } from "./ModuleButtons/ProvisionButton";
interface ProvisionsManagerProps { interface ProvisionsManagerProps {
provisions: number[]; provisions: number[];
@@ -8,6 +9,8 @@ interface ProvisionsManagerProps {
onChange?: (provisions: number[]) => void; onChange?: (provisions: number[]) => void;
} }
const provisionDefinitions = await awaitableProvisionDefinitions;
export function ProvisionsManager({ export function ProvisionsManager({
provisions, provisions,
selected, selected,
@@ -15,15 +18,18 @@ export function ProvisionsManager({
disabled, disabled,
}: ProvisionsManagerProps) { }: ProvisionsManagerProps) {
return ( return (
<Flex wrap="wrap" gap="2" justify={{ initial: 'center', sm: 'start' }}> <Flex wrap="wrap" gap="2" justify={{ initial: "center", sm: "start" }}>
{provisions.map((provision) => { {provisions.map((id) => {
const isSelected = selected.includes(provision); const isSelected = selected.includes(id);
const provision = provisionDefinitions.provisions[id];
if (provision.game_mode_exclusive) return null;
return ( return (
<ProvisionButton <ProvisionButton
key={provision} key={id}
disabled={disabled && !isSelected} disabled={disabled && !isSelected}
provision={provision} provision={id}
selected={isSelected} selected={isSelected}
onClick={() => { onClick={() => {
if (!onChange) return; if (!onChange) return;
@@ -31,9 +37,9 @@ export function ProvisionsManager({
const draft = [...selected]; const draft = [...selected];
if (isSelected) { if (isSelected) {
draft.splice(draft.indexOf(provision), 1); draft.splice(draft.indexOf(id), 1);
} else { } else {
draft.push(provision); draft.push(id);
} }
onChange(draft); onChange(draft);
@@ -41,7 +41,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
case 'fire.damage': case 'fire.damage':
return shell0.armor_damage.toFixed(0); return shell0.armor_damage.toFixed(0);
case 'fire.moduleDamage': case 'fire.moduleDamage':
return shell0.armor_damage.toFixed(0); return shell0.module_damage.toFixed(0);
case 'fire.dispersionMoving': case 'fire.dispersionMoving':
return tracks.dispersion_move.toFixed(3); return tracks.dispersion_move.toFixed(3);
case 'fire.dispersionStill': case 'fire.dispersionStill':
File diff suppressed because it is too large. Load diff
@@ -429,7 +429,7 @@ export const TankSearch = memo<TankSearchProps>(
}, [tankFilters, tankopediaSort]); }, [tankFilters, tankopediaSort]);
return ( return (
<Flex direction="column" gap="4" flexGrow="1" {...props}> <Flex direction="column" gap="2" flexGrow="1" {...props}>
<SearchBar <SearchBar
skeleton={skeleton} skeleton={skeleton}
topResult={tanks?.[0]} topResult={tanks?.[0]}
@@ -176,11 +176,17 @@ export function Modules() {
(turret) => turret.id === unlock.id (turret) => turret.id === unlock.id
)!; )!;
if ( // Check if current gun exists in new turret
!draft.protagonist.turret.guns.some( const gunInNewTurret = draft.protagonist.turret.guns.find(
(gun) => gun.id === draft.protagonist.gun.id (gun) => gun.id === draft.protagonist.gun.id
) );
) {
if (gunInNewTurret) {
// Gun exists - get it FROM this turret
draft.protagonist.gun = gunInNewTurret;
draft.protagonist.shell = gunInNewTurret.shells[0];
} else {
// Gun doesn't exist - select top gun from new turret
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!; draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
draft.protagonist.shell = draft.protagonist.gun.shells[0]; draft.protagonist.shell = draft.protagonist.gun.shells[0];
} }
@@ -1,10 +1,10 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
import { Box, Flex, Heading, Text } from '@radix-ui/themes'; import { Box, Flex, Heading, Text } from "@radix-ui/themes";
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions'; import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions'; import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from "../../hooks/useLocale";
import { Duel } from '../../stores/duel'; import { Duel } from "../../stores/duel";
const [gameDefinitions, consumableDefinitions, provisionDefinitions] = const [gameDefinitions, consumableDefinitions, provisionDefinitions] =
await Promise.all([ await Promise.all([
@@ -39,22 +39,22 @@ export function GameModeSection() {
overflow="hidden" overflow="hidden"
direction="column" direction="column"
style={{ style={{
borderRadius: 'var(--radius-3)', borderRadius: "var(--radius-3)",
background: `url(${asset( background: `url(${asset(
`icons/game_mode_banners/${id}.webp`, `icons/game_mode_banners/${id}.webp`
)})`, )})`,
backgroundSize: 'cover', backgroundSize: "cover",
backgroundPosition: 'center', backgroundPosition: "center",
}} }}
> >
<Box <Box
flexGrow="1" flexGrow="1"
style={{ style={{
background: `url(${asset( background: `url(${asset(
`icons/game_mode_banners/${id}.webp`, `icons/game_mode_banners/${id}.webp`
)})`, )})`,
backgroundSize: 'cover', backgroundSize: "cover",
backgroundPosition: 'center', backgroundPosition: "center",
}} }}
/> />
@@ -64,8 +64,8 @@ export function GameModeSection() {
height="98px" height="98px"
flexShrink="0" flexShrink="0"
style={{ style={{
backdropFilter: 'blur(4rem)', backdropFilter: "blur(4rem)",
WebkitBackdropFilter: 'blur(4rem)', WebkitBackdropFilter: "blur(4rem)",
}} }}
align="center" align="center"
justify="center" justify="center"
@@ -83,9 +83,9 @@ export function GameModeSection() {
<Flex align="center" gap="1"> <Flex align="center" gap="1">
<img <img
style={{ style={{
width: '1.5em', width: "1.5em",
height: '1.5em', height: "1.5em",
objectFit: 'contain', objectFit: "contain",
}} }}
src={asset(`icons/provisions/${id}.webp`)} src={asset(`icons/provisions/${id}.webp`)}
alt={unwrap(provisions.name)} alt={unwrap(provisions.name)}
@@ -95,6 +95,7 @@ export function GameModeSection() {
</Text> </Text>
); );
})} })}
{gameDefinitions.roles[value].consumables.map((id) => { {gameDefinitions.roles[value].consumables.map((id) => {
const consumable = consumableDefinitions.consumables[id]; const consumable = consumableDefinitions.consumables[id];
@@ -103,9 +104,9 @@ export function GameModeSection() {
<Flex align="center" gap="1"> <Flex align="center" gap="1">
<img <img
style={{ style={{
width: '1.5em', width: "1.5em",
height: '1.5em', height: "1.5em",
objectFit: 'contain', objectFit: "contain",
}} }}
src={asset(`icons/consumables/${id}.webp`)} src={asset(`icons/consumables/${id}.webp`)}
alt={unwrap(consumable.name)} alt={unwrap(consumable.name)}
@@ -108,10 +108,10 @@ export function CustomShellButton() {
{strings.common.shells.ap} {strings.common.shells.ap}
</Select.Item> </Select.Item>
<Select.Item value={`${ShellType.APCR}`}> <Select.Item value={`${ShellType.APCR}`}>
{strings.common.shells.apcr} {strings.common.shells.ap_cr}
</Select.Item> </Select.Item>
<Select.Item value={`${ShellType.HEAT}`}> <Select.Item value={`${ShellType.HEAT}`}>
{strings.common.shells.heat} {strings.common.shells.hc}
</Select.Item> </Select.Item>
<Select.Item value={`${ShellType.HE}`}> <Select.Item value={`${ShellType.HE}`}>
{strings.common.shells.he} {strings.common.shells.he}
@@ -150,6 +150,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
return ( return (
<SmartCanvas <SmartCanvas
resize={{ debounce: 0 }}
ref={canvas} ref={canvas}
scene={{ fog: new Fog("black", 30, 50) }} scene={{ fog: new Fog("black", 30, 50) }}
gl={{ gl={{
@@ -1,9 +1,17 @@
import type { TankDefinition } from "@blitzkit/core"; import type { TankDefinition } from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import type { TankFilters } from "../../stores/tankFilters"; import type { TankFilters } from "../../stores/tankFilters";
import { awaitableGameDefinitions } from "../awaitables/gameDefinitions";
import { awaitableProvisionDefinitions } from "../awaitables/provisionDefinitions";
const SHELLS = times(3, (index) => index); const SHELLS = times(3, (index) => index);
const consumableDefinitions = await awaitableConsumableDefinitions;
const provisionDefinitions = await awaitableProvisionDefinitions;
const gameDefinitions = await awaitableGameDefinitions;
export async function filterTank( export async function filterTank(
filters: TankFilters, filters: TankFilters,
tank: TankDefinition, tank: TankDefinition,
@@ -40,6 +48,42 @@ export async function filterTank(
gun.shells[index].type === filters.shells[index] gun.shells[index].type === filters.shells[index]
) )
) )
) ) &&
(filters.consumables.length === 0 ||
filters.consumables.every((consumable) =>
tank.turrets.some((turret) =>
turret.guns.some((gun) =>
checkConsumableProvisionInclusivity(
consumableDefinitions.consumables[consumable],
tank,
gun
)
)
)
)) &&
(filters.provisions.length === 0 ||
filters.provisions.every((provision) =>
tank.turrets.some((turret) =>
turret.guns.some((gun) =>
checkConsumableProvisionInclusivity(
provisionDefinitions.provisions[provision],
tank,
gun
)
)
)
)) &&
(filters.abilities.length === 0 ||
Object.values(tank.roles).some((id) =>
gameDefinitions.roles[id as unknown as number].consumables.some(
(consumable) => filters.abilities.includes(consumable)
)
)) &&
(filters.powers.length === 0 ||
Object.values(tank.roles).some((id) =>
gameDefinitions.roles[id as unknown as number].provisions.some(
(consumable) => filters.powers.includes(consumable)
)
))
); );
} }
@@ -19,9 +19,9 @@ import {
type TankDefinition, type TankDefinition,
type TrackDefinition, type TrackDefinition,
type TurretDefinition, type TurretDefinition,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { coefficient } from '@blitzkit/core/src/blitzkit/coefficient'; import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
import type { EquipmentMatrix } from '../../stores/duel'; import type { EquipmentMatrix } from "../../stores/duel";
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>; export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
export type TankCharacteristicsKey = keyof TankCharacteristics; export type TankCharacteristicsKey = keyof TankCharacteristics;
@@ -76,7 +76,7 @@ export function tankCharacteristics(
tankModelDefinition: ModelDefinition; tankModelDefinition: ModelDefinition;
equipmentDefinitions: EquipmentDefinitions; equipmentDefinitions: EquipmentDefinitions;
provisionDefinitions: ProvisionDefinitions; provisionDefinitions: ProvisionDefinitions;
}, }
) { ) {
const preset = equipmentDefinitions.presets[tank.equipment_preset]; const preset = equipmentDefinitions.presets[tank.equipment_preset];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id];
@@ -86,7 +86,7 @@ export function tankCharacteristics(
return preset.slots.some((slot, index) => { return preset.slots.some((slot, index) => {
const choice = equipmentMatrix[Math.floor(index / 3)][index % 3]; const choice = equipmentMatrix[Math.floor(index / 3)][index % 3];
if (choice === 0) return false; if (choice === 0) return false;
const equipped = slot[choice === -1 ? 'left' : 'right']; const equipped = slot[choice === -1 ? "left" : "right"];
return equipped === id; return equipped === id;
}); });
} }
@@ -138,7 +138,7 @@ export function tankCharacteristics(
provisionDefinitions.provisions[provision].crew provisionDefinitions.provisions[provision].crew
? total + provisionDefinitions.provisions[provision].crew! / 100 ? total + provisionDefinitions.provisions[provision].crew! / 100
: total, : total,
0, 0
) + (hasImprovedVentilation ? 0.08 : 0); ) + (hasImprovedVentilation ? 0.08 : 0);
const commanderMastery = 1 + provisionCrewBonus; const commanderMastery = 1 + provisionCrewBonus;
const loaderMastery = const loaderMastery =
@@ -158,19 +158,19 @@ export function tankCharacteristics(
coefficient( coefficient(
[applyReactiveArmor && shell.type !== ShellType.HE, -0.27], [applyReactiveArmor && shell.type !== ShellType.HE, -0.27],
[applyDynamicArmor, -0.1], [applyDynamicArmor, -0.1],
[applySpallLiner && shell.type === ShellType.HE, -0.2], [applySpallLiner && shell.type === ShellType.HE, -0.2]
); );
const assaultDamageCoefficient = const assaultDamageCoefficient =
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type) gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
? (gun.assault_ranges.ranges.find( ? gun.assault_ranges.ranges.find(
({ distance }) => distance >= assaultDistance, ({ distance }) => distance >= assaultDistance
)?.factor ?? 0) )?.factor ?? 0
: 1; : 1;
const moduleDamageCoefficient = coefficient([hasTungsten, 0.15]); const moduleDamageCoefficient = coefficient([hasTungsten, 0.15]);
const reloadCoefficient = const reloadCoefficient =
(coefficient([hasGunRammer, -0.05]) * (coefficient([hasGunRammer, -0.05]) *
coefficient([true, degressiveStat(loaderMastery)])) / coefficient([true, degressiveStat(loaderMastery)])) /
coefficient([hasAdrenaline && gun.gun_type!.$case === 'regular', 0.17]); coefficient([hasAdrenaline && gun.gun_type!.$case === "regular", 0.17]);
const penetrationCoefficient = coefficient([ const penetrationCoefficient = coefficient([
hasCalibratedShells, hasCalibratedShells,
resolvePenetrationCoefficient(true, shell.type) - 1, resolvePenetrationCoefficient(true, shell.type) - 1,
@@ -178,16 +178,16 @@ export function tankCharacteristics(
const healthCoefficient = coefficient( const healthCoefficient = coefficient(
[hasSandbagArmor, 0.03], [hasSandbagArmor, 0.03],
[hasEnhancedSandbagArmor, 0.06], [hasEnhancedSandbagArmor, 0.06],
[hasImprovedAssembly, 0.04], [hasImprovedAssembly, 0.04]
); );
const shellVelocityCoefficient = coefficient( const shellVelocityCoefficient = coefficient(
[hasSupercharger, 0.35], [hasSupercharger, 0.35],
[hasImprovedGunPowder, 0.3], [hasImprovedGunPowder, 0.3]
); );
const aimTimeCoefficient = const aimTimeCoefficient =
coefficient( coefficient(
[hasEnhancedGunLayingDrive, -0.15], [hasEnhancedGunLayingDrive, -0.15],
[hasReticleCalibration, -0.3], [hasReticleCalibration, -0.3]
) * coefficient([true, degressiveStat(gunnerMastery)]); ) * coefficient([true, degressiveStat(gunnerMastery)]);
const dispersionStillCoefficient = const dispersionStillCoefficient =
coefficient([hasRefinedGun, -0.15], [hasReticleCalibration, -0.3]) * coefficient([hasRefinedGun, -0.15], [hasReticleCalibration, -0.3]) *
@@ -198,19 +198,19 @@ export function tankCharacteristics(
]); ]);
const dispersionHullTraverseCoefficient = coefficient( const dispersionHullTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15], [hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turn / 100], [true, -crewSkills.smooth_turn / 100]
); );
const dispersionAfterShotCoefficient = coefficient( const dispersionAfterShotCoefficient = coefficient(
[hasVerticalStabilizer, -0.15], [hasVerticalStabilizer, -0.15],
[true, -crewSkills.soft_recoil / 100], [true, -crewSkills.soft_recoil / 100]
); );
const dispersionMovingCoefficient = coefficient( const dispersionMovingCoefficient = coefficient(
[hasVerticalStabilizer, -0.15], [hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_driving / 100], [true, -crewSkills.smooth_driving / 100]
); );
const dispersionTurretTraverseCoefficient = coefficient( const dispersionTurretTraverseCoefficient = coefficient(
[hasVerticalStabilizer, -0.15], [hasVerticalStabilizer, -0.15],
[true, -crewSkills.smooth_turret * (2 / 100)], [true, -crewSkills.smooth_turret * (2 / 100)]
); );
const enginePowerCoefficient = coefficient( const enginePowerCoefficient = coefficient(
[ [
@@ -225,7 +225,7 @@ export function tankCharacteristics(
[hasStandardFuel, 0.03], [hasStandardFuel, 0.03],
[hasImprovedFuel, 0.1], [hasImprovedFuel, 0.1],
[hasGearOil, 0.03], [hasGearOil, 0.03],
[hasImprovedGearOil, 0.06], [hasImprovedGearOil, 0.06]
); );
const turretTraverseCoefficient = const turretTraverseCoefficient =
coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) * coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) *
@@ -234,36 +234,41 @@ export function tankCharacteristics(
coefficient( coefficient(
[hasImprovedControl, 0.11], [hasImprovedControl, 0.11],
[hasImprovedEnginePowerBoost, 0.01], [hasImprovedEnginePowerBoost, 0.01],
[true, crewSkills.virtuoso / 100], [true, crewSkills.virtuoso / 100]
) * coefficient([true, progressiveStat(driverMastery)]); ) * coefficient([true, progressiveStat(driverMastery)]);
const resistanceHardCoefficient = coefficient([hasImprovedSuspension, -0.2]);
const resistanceMediumCoefficient = coefficient([ const resistanceHardCoefficient =
hasImprovedSuspension, coefficient([hasImprovedSuspension, -0.2]) *
-0.15, coefficient([true, degressiveStat(driverMastery)]);
]); const resistanceMediumCoefficient =
const resistanceSoftCoefficient = coefficient([hasImprovedSuspension, -0.3]); coefficient([hasImprovedSuspension, -0.15]) *
coefficient([true, degressiveStat(driverMastery)]);
const resistanceSoftCoefficient =
coefficient([hasImprovedSuspension, -0.3]) *
coefficient([true, degressiveStat(driverMastery)]);
const viewRangeCoefficient = const viewRangeCoefficient =
coefficient([ coefficient([
hasImprovedOptics, hasImprovedOptics,
tank.class === TankClass.HEAVY tank.class === TankClass.HEAVY
? 0.06 ? 0.06
: tank.class === TankClass.MEDIUM : tank.class === TankClass.MEDIUM
? 0.08 ? 0.08
: tank.class === TankClass.LIGHT : tank.class === TankClass.LIGHT
? 0.11 ? 0.11
: 0.04, : 0.04,
]) * coefficient([true, progressiveStat(commanderMastery)]); ]) * coefficient([true, progressiveStat(commanderMastery)]);
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
const speedForwardsSum = sum( const speedForwardsSum = sum(
[hasImprovedEnginePowerBoost, 4], [hasImprovedEnginePowerBoost, 4],
[hasGearOil, 2], [hasGearOil, 2],
[hasImprovedGearOil, 4], [hasImprovedGearOil, 4]
); );
const speedBackwardsSum = sum( const speedBackwardsSum = sum(
[hasImprovedEnginePowerBoost, 8], [hasImprovedEnginePowerBoost, 8],
[hasGearOil, 2], [hasGearOil, 2],
[hasImprovedGearOil, 4], [hasImprovedGearOil, 4]
); );
const camouflageCoefficientMoving = sum( const camouflageCoefficientMoving = sum(
[ [
@@ -273,8 +278,8 @@ export function tankCharacteristics(
tank.class === TankClass.TANK_DESTROYER tank.class === TankClass.TANK_DESTROYER
? 0.04 ? 0.04
: tank.class === TankClass.HEAVY : tank.class === TankClass.HEAVY
? 0.03 ? 0.03
: 0.02, : 0.02,
]), ]),
], ],
[ [
@@ -282,11 +287,11 @@ export function tankCharacteristics(
tank.class === TankClass.HEAVY tank.class === TankClass.HEAVY
? 0.02 ? 0.02
: tank.class === TankClass.MEDIUM : tank.class === TankClass.MEDIUM
? 0.04 ? 0.04
: tank.class === TankClass.LIGHT : tank.class === TankClass.LIGHT
? 0.07 ? 0.07
: 0.1, : 0.1,
], ]
); );
const camouflageCoefficientStill = sum( const camouflageCoefficientStill = sum(
[ [
@@ -297,10 +302,10 @@ export function tankCharacteristics(
tank.class === TankClass.TANK_DESTROYER tank.class === TankClass.TANK_DESTROYER
? 0.04 ? 0.04
: tank.class === TankClass.HEAVY : tank.class === TankClass.HEAVY
? 0.03 ? 0.03
: 0.02, : 0.02,
], ],
[true, crewSkills.camouflage * (3 / 100)], [true, crewSkills.camouflage * (3 / 100)]
), ),
], ],
[ [
@@ -308,18 +313,18 @@ export function tankCharacteristics(
tank.class === TankClass.HEAVY tank.class === TankClass.HEAVY
? 0.04 ? 0.04
: tank.class === TankClass.MEDIUM : tank.class === TankClass.MEDIUM
? 0.08 ? 0.08
: tank.class === TankClass.LIGHT : tank.class === TankClass.LIGHT
? 0.14 ? 0.14
: 0.2, : 0.2,
], ]
); );
const size = normalizeBoundingBox( const size = normalizeBoundingBox(
unionBoundingBox( unionBoundingBox(
tankModelDefinition.bounding_box, tankModelDefinition.bounding_box,
turretModelDefinition.bounding_box, turretModelDefinition.bounding_box
), )
); );
const weightKg = const weightKg =
tank.weight + engine.weight + track.weight + turret.weight + gun.weight; tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
@@ -339,14 +344,14 @@ export function tankCharacteristics(
shell, shell,
damageCoefficient, damageCoefficient,
reloadCoefficient, reloadCoefficient,
intraClipCoefficient, intraClipCoefficient
); );
const intraClip = const intraClip =
gun.gun_type?.$case === 'regular' gun.gun_type?.$case === "regular"
? undefined ? undefined
: gun.gun_type!.value.intra_clip * intraClipCoefficient; : gun.gun_type!.value.intra_clip * intraClipCoefficient;
const mostOptimalShellIndex = const mostOptimalShellIndex =
gun.gun_type!.$case === 'auto_reloader' gun.gun_type!.$case === "auto_reloader"
? gun.gun_type!.value.shell_reloads.reduce<null | { ? gun.gun_type!.value.shell_reloads.reduce<null | {
index: number; index: number;
reload: number; reload: number;
@@ -364,7 +369,7 @@ export function tankCharacteristics(
shell.armor_damage * damageCoefficientWithoutAssault; shell.armor_damage * damageCoefficientWithoutAssault;
const damage = shell.armor_damage * damageCoefficient; const damage = shell.armor_damage * damageCoefficient;
const dpmEffective = const dpmEffective =
gun.gun_type!.$case === 'auto_reloader' gun.gun_type!.$case === "auto_reloader"
? gun.gun_type!.value.shell_reloads[0] > ? gun.gun_type!.value.shell_reloads[0] >
gun.gun_type!.value.shell_reloads[1] + gun.gun_type!.value.intra_clip gun.gun_type!.value.shell_reloads[1] + gun.gun_type!.value.intra_clip
? ((armorDamageCoefficient * shell.armor_damage) / ? ((armorDamageCoefficient * shell.armor_damage) /
@@ -389,23 +394,23 @@ export function tankCharacteristics(
gun.gun_type!.value.shell_reloads.slice(1).length gun.gun_type!.value.shell_reloads.slice(1).length
: undefined; : undefined;
const shells = const shells =
gun.gun_type!.$case === 'regular' ? 1 : gun.gun_type!.value.shell_count; gun.gun_type!.$case === "regular" ? 1 : gun.gun_type!.value.shell_count;
const shellReloads = const shellReloads =
gun.gun_type!.$case === 'auto_reloader' gun.gun_type!.$case === "auto_reloader"
? gun.gun_type!.value.shell_reloads.map( ? gun.gun_type!.value.shell_reloads.map(
(reload) => reload * reloadCoefficient, (reload) => reload * reloadCoefficient
) )
: undefined; : undefined;
const shellReload = const shellReload =
gun.gun_type!.$case === 'auto_reloader' gun.gun_type!.$case === "auto_reloader"
? undefined ? undefined
: (gun.gun_type!.$case === 'regular' : (gun.gun_type!.$case === "regular"
? gun.gun_type!.value.reload ? gun.gun_type!.value.reload
: gun.gun_type!.value.clip_reload) * reloadCoefficient; : gun.gun_type!.value.clip_reload) * reloadCoefficient;
const caliber = shell.caliber; const caliber = shell.caliber;
const penetration = shell.penetration.near * penetrationCoefficient; const penetration = shell.penetration.near * penetrationCoefficient;
const clipDamage = const clipDamage =
gun.gun_type!.$case === 'regular' gun.gun_type!.$case === "regular"
? undefined ? undefined
: gun.gun_type!.value.shell_count * damage; : gun.gun_type!.value.shell_count * damage;
const moduleDamage = shell.module_damage * moduleDamageCoefficient; const moduleDamage = shell.module_damage * moduleDamageCoefficient;
@@ -599,45 +604,45 @@ export const characteristicsOrder: {
items: TankCharacteristicsKey[]; items: TankCharacteristicsKey[];
}[] = [ }[] = [
{ {
name: 'Firepower', name: "Firepower",
items: [ items: [
'gunType', "gunType",
'dpm', "dpm",
'shells', "shells",
'shellReload', "shellReload",
'shellReloads', "shellReloads",
'intraClip', "intraClip",
'penetration', "penetration",
'penetrationAt250m', "penetrationAt250m",
'damage', "damage",
'clipDamage', "clipDamage",
'moduleDamage', "moduleDamage",
'caliber', "caliber",
'shellNormalization', "shellNormalization",
'shellRicochet', "shellRicochet",
'shellVelocity', "shellVelocity",
'shellRange', "shellRange",
'shellCapacity', "shellCapacity",
'aimTime', "aimTime",
'dispersion', "dispersion",
'dispersionMoving', "dispersionMoving",
'dispersionHullTraversing', "dispersionHullTraversing",
'dispersionTurretTraversing', "dispersionTurretTraversing",
'dispersionShooting', "dispersionShooting",
'dispersionGunDamaged', "dispersionGunDamaged",
'gunDepression', "gunDepression",
'gunElevation', "gunElevation",
'gunFrontalDepression', "gunFrontalDepression",
'gunFrontalElevation', "gunFrontalElevation",
'gunRearDepression', "gunRearDepression",
'gunRearElevation', "gunRearElevation",
'azimuthLeft', "azimuthLeft",
'azimuthRight', "azimuthRight",
], ],
}, },
{ {
name: 'Crew', name: "Crew",
items: ['crewCount'], items: ["crewCount"],
}, },
]; ];
@@ -1,5 +1,5 @@
import { Box, Flex, Heading } from "@radix-ui/themes"; import { Box, Flex, Heading } from "@radix-ui/themes";
import { Suspense } from "react"; import { Suspense, useEffect } from "react";
import { GuessBackground } from "../../../components/GuessBackground"; import { GuessBackground } from "../../../components/GuessBackground";
import { Guesser } from "../../../components/Guesser"; import { Guesser } from "../../../components/Guesser";
import { GuessRenderer } from "../../../components/GuessRenderer"; import { GuessRenderer } from "../../../components/GuessRenderer";
@@ -62,6 +62,31 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
const name = unwrap(tank.name); const name = unwrap(tank.name);
const fontSize = `min(48vh, ${55 / name.length}vw)`; const fontSize = `min(48vh, ${55 / name.length}vw)`;
const transitionDuration = isRevealed ? "2s" : undefined; const transitionDuration = isRevealed ? "2s" : undefined;
const tiers = Guess.use((state) => state.tiers);
useEffect(() => {
const isTankAlreadyCompliant = tiers.includes(tank.tier);
if (isTankAlreadyCompliant) return;
const filteredIds = ids.filter((id) => {
const tank = tankDefinitions.tanks[Number(id)];
return tiers.includes(tank.tier);
});
if (filteredIds.length === 0) return;
const id = Number(
filteredIds[Math.floor(Math.random() * filteredIds.length)]
);
const newTank = tankDefinitions.tanks[id];
Guess.mutate((draft) => {
draft.tank = newTank;
draft.guessState = GuessState.NotGuessed;
draft.helpingReveal = false;
});
}, [tiers]);
return ( return (
<Flex flexGrow="1" position="relative" overflow="hidden"> <Flex flexGrow="1" position="relative" overflow="hidden">
@@ -1,23 +1,31 @@
import { asset } from "@blitzkit/core"; import {
import { CaretRightIcon, UpdateIcon } from "@radix-ui/react-icons"; asset,
GunDefinition,
TankDefinition,
TIER_ROMAN_NUMERALS,
TurretDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { CaretRightIcon, CaretUpIcon, UpdateIcon } from "@radix-ui/react-icons";
import { import {
Box, Box,
Button, Button,
Dialog, Dialog,
Flex, Flex,
IconButton,
Popover,
Skeleton, Skeleton,
Text, Text,
type ButtonProps, type ButtonProps,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { MixerScene } from "../../../components/MixerScene"; import { MixerScene } from "../../../components/MixerScene";
import { ModuleButton } from "../../../components/ModuleButtons/ModuleButton";
import { PageWrapper } from "../../../components/PageWrapper"; import { PageWrapper } from "../../../components/PageWrapper";
import { ScreenshotButton } from "../../../components/ScreenshotButton"; import { ScreenshotButton } from "../../../components/ScreenshotButton";
import { import { TankSearch } from "../../../components/TankSearch";
TankSearch,
type TankSearchProps,
} from "../../../components/TankSearch";
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions"; import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { curateMixer } from "../../../core/blitzkit/curateMixer"; import { curateMixer } from "../../../core/blitzkit/curateMixer";
import { import {
LocaleProvider, LocaleProvider,
@@ -31,6 +39,7 @@ import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonCo
const modelDefinition = await awaitableModelDefinitions.then( const modelDefinition = await awaitableModelDefinitions.then(
({ models }) => models[1] ({ models }) => models[1]
); );
const tankDefinitions = await awaitableTankDefinitions;
export function Page({ export function Page({
locale, locale,
@@ -50,57 +59,147 @@ export function Page({
); );
} }
function ModuleButton({ type ModuleButtonProps = Omit<ButtonProps, "onSelect"> &
MaybeSkeletonComponentProps & {
tank: number;
onSelect: (
tank: TankDefinition,
turret?: TurretDefinition,
gun?: GunDefinition
) => void;
turret?: number;
gun?: number;
};
function ModuleChooser({
skeleton, skeleton,
tank, tank,
onSelect, onSelect,
turret,
gun,
...props ...props
}: Omit<ButtonProps, "onSelect"> & }: ModuleButtonProps) {
MaybeSkeletonComponentProps & {
tank: number;
onSelect: TankSearchProps["onSelect"];
}) {
const { strings } = useLocale(); const { strings } = useLocale();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const tankDefinition = tankDefinitions.tanks[tank];
const turretDefinition = tankDefinition.turrets.find((t) => t.id === turret);
const gunDefinition = turretDefinition?.guns.find((g) => g.id === gun);
const subSelector =
turret !== undefined &&
(tankDefinition.turrets.length > 1 ||
(gun !== undefined && turretDefinition!.guns.length > 1));
return ( return (
<Dialog.Root open={open} onOpenChange={setOpen}> <Flex>
<Dialog.Trigger> <Dialog.Root open={open} onOpenChange={setOpen}>
<Button <Dialog.Trigger>
variant="surface" <Button
size="4" variant="surface"
style={{ overflow: "clip" }} size={{ initial: "3", md: "4" }}
{...props} style={{
> overflow: "clip",
{skeleton ? ( borderTopRightRadius: subSelector ? 0 : undefined,
<Skeleton width="4em" height="2em" /> borderBottomRightRadius: subSelector ? 0 : undefined,
) : ( }}
<img {...props}
>
{skeleton ? (
<Skeleton width="4em" height="2em" />
) : (
<img
style={{
width: "4em",
height: "4em",
objectFit: "contain",
}}
src={asset(`icons/tanks/big/${tank}.webp`)}
/>
)}
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>
{strings.website.common.tank_search.select_dialog_title}
</Dialog.Title>
<TankSearch
compact
onSelect={(tank) => {
onSelect?.(tank);
setOpen(false);
}}
/>
</Dialog.Content>
</Dialog.Root>
{subSelector && (
<Popover.Root>
<Popover.Trigger>
<IconButton
variant="surface"
size={{ initial: "3", md: "4" }}
style={{ style={{
width: "4em", overflow: "clip",
height: "4em", borderTopLeftRadius: 0,
objectFit: "contain", borderBottomLeftRadius: 0,
marginLeft: -1,
}} }}
src={asset(`icons/tanks/big/${tank}.webp`)} {...props}
/> >
)} <CaretUpIcon />
</Button> </IconButton>
</Dialog.Trigger> </Popover.Trigger>
<Dialog.Content> <Popover.Content>
<Dialog.Title> <Flex direction="column" gap="2">
{strings.website.common.tank_search.select_dialog_title} {tankDefinition.turrets.length > 1 && (
</Dialog.Title> <Flex gap="2">
{tankDefinition.turrets.map((turret) => (
<ModuleButton
variant={
turret.id === turretDefinition?.id ? "solid" : undefined
}
key={turret.id}
module="turret"
discriminator={TIER_ROMAN_NUMERALS[turret.tier]}
onClick={() => {
onSelect?.(tankDefinition, turret);
}}
/>
))}
</Flex>
)}
<TankSearch {turretDefinition && turretDefinition.guns.length > 1 && (
compact <Flex gap="2">
onSelect={(tank) => { {turretDefinition.guns.map((gun) => (
onSelect?.(tank); <ModuleButton
setOpen(false); variant={
}} gun.id === gunDefinition?.id ? "solid" : undefined
/> }
</Dialog.Content> key={gun.id}
</Dialog.Root> module="gun"
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
secondaryDiscriminator={
<Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, {
value: gun.shells[0].caliber.toFixed(0),
})}
</Text>
}
onClick={() => {
onSelect?.(tankDefinition, turretDefinition, gun);
}}
/>
))}
</Flex>
)}
</Flex>
</Popover.Content>
</Popover.Root>
)}
</Flex>
); );
} }
@@ -113,9 +212,12 @@ function ModuleSeparator() {
} }
function Content({ skeleton }: MaybeSkeletonComponentProps) { function Content({ skeleton }: MaybeSkeletonComponentProps) {
const hull = Mixer.use((state) => state.hull); const hullTankId = Mixer.use((state) => state.hull);
const turretId = Mixer.use((state) => state.turret.tank.id); const turretTankId = Mixer.use((state) => state.turret.tank.id);
const gunId = Mixer.use((state) => state.gun.tank.id); const turretTurretId = Mixer.use((state) => state.turret.turret.id);
const gunTankId = Mixer.use((state) => state.gun.tank.id);
const gunTurretId = Mixer.use((state) => state.gun.turret.id);
const gunGunId = Mixer.use((state) => state.gun.gun.id);
const canvas = useRef<HTMLCanvasElement>(null!); const canvas = useRef<HTMLCanvasElement>(null!);
const { strings } = useLocale(); const { strings } = useLocale();
@@ -136,7 +238,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
> >
<Flex gap="2"> <Flex gap="2">
<ScreenshotButton <ScreenshotButton
name={hull.slug} name={hullTankId.slug}
canvas={canvas} canvas={canvas}
highContrast highContrast
variant="surface" variant="surface"
@@ -160,9 +262,9 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
</Flex> </Flex>
<Flex gap="2" align="center"> <Flex gap="2" align="center">
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={hull.id} tank={hullTankId.id}
onSelect={(tank) => { onSelect={(tank) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.hull = tank; draft.hull = tank;
@@ -170,28 +272,34 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
}} }}
/> />
<ModuleSeparator /> <ModuleSeparator />
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={turretId} tank={turretTankId}
onSelect={(tank) => { turret={turretTurretId}
onSelect={(tank, turret) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.turret = { draft.turret = {
tank, tank,
turret: tank.turrets.at(-1)!, turret: turret ?? tank.turrets.at(-1)!,
}; };
}); });
}} }}
/> />
<ModuleSeparator /> <ModuleSeparator />
<ModuleButton <ModuleChooser
skeleton={skeleton} skeleton={skeleton}
tank={gunId} tank={gunTankId}
onSelect={(tank) => { turret={gunTurretId}
gun={gunGunId}
onSelect={(tank, turret, gun) => {
Mixer.mutate((draft) => { Mixer.mutate((draft) => {
draft.gun = { draft.gun = {
tank, tank,
turret: tank.turrets.at(-1)!, turret: turret ?? tank.turrets.at(-1)!,
gun: tank.turrets.at(-1)!.guns.at(-1)!, gun:
gun ??
turret?.guns.at(-1) ??
tank.turrets.at(-1)!.guns.at(-1)!,
}; };
}); });
}} }}
+7
View File
@@ -1,4 +1,5 @@
import type { TankDefinition } from "@blitzkit/core"; import type { TankDefinition } from "@blitzkit/core";
import { times } from "lodash-es";
import { Varuna } from "varuna"; import { Varuna } from "varuna";
export enum GuessState { export enum GuessState {
@@ -12,6 +13,9 @@ export interface Guess {
guessState: GuessState; guessState: GuessState;
totalGuesses: number; totalGuesses: number;
correctGuesses: number; correctGuesses: number;
streak: number;
helpingReveal: boolean;
tiers: number[];
} }
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
@@ -19,4 +23,7 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
guessState: GuessState.NotGuessed, guessState: GuessState.NotGuessed,
totalGuesses: 0, totalGuesses: 0,
correctGuesses: 0, correctGuesses: 0,
streak: 0,
helpingReveal: false,
tiers: times(10, (index) => index + 1),
})); }));
@@ -21,6 +21,10 @@ export interface TankFilters {
searching: boolean; searching: boolean;
gunType: CaseType<GunDefinition>[]; gunType: CaseType<GunDefinition>[];
shells: [ShellType | null, ShellType | null, ShellType | null]; shells: [ShellType | null, ShellType | null, ShellType | null];
consumables: number[];
provisions: number[];
abilities: number[];
powers: number[];
showTesting: boolean; showTesting: boolean;
showNonTesting: boolean; showNonTesting: boolean;
@@ -37,6 +41,10 @@ export const TankFilters = new Varuna<TankFilters>({
searching: false, searching: false,
gunType: [], gunType: [],
shells: [null, null, null], shells: [null, null, null],
consumables: [],
provisions: [],
abilities: [],
powers: [],
showTesting: true, showTesting: true,
showNonTesting: true, showNonTesting: true,