Compare commits

...
26 Commits
Author SHA1 Message Date
TrèsAbhi 50eac9eba6 New translations en.json (French) (#169) 2025-09-04 08:04:17 -05:00
TrèsAbhi 97fc386bc7 New Crowdin updates (#168)
* New translations en.json (Russian)

* 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)

* 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)

* New translations en.json (Russian)

* New translations en.json (Russian)
2025-08-31 09:54:49 -05:00
TrèsAbhi abfb3f8ddb fix: typo 2025-08-29 23:58:47 -05:00
TrèsAbhi 9cb5113f6e feat: haha 2025-08-29 23:58:14 -05:00
TrèsAbhi 7dd1c0c32a feat: logs 2025-08-29 23:57:33 -05:00
TrèsAbhi 03233f3fdc fix: align antagonist selector filters correctly 2025-08-29 23:46:57 -05:00
TrèsAbhi 377159c298 fix: consider tank models 2025-08-29 23:41:35 -05:00
TrèsAbhi d8431fb620 fix: gap alignemnt 2025-08-29 23:28:49 -05:00
TrèsAbhi 0546ddefbd fix: better shot card alignment 2025-08-29 23:24:12 -05:00
TrèsAbhi 26f6889012 feat: revert subtler advanced highlighting coloring 2025-08-29 23:10:20 -05:00
TrèsAbhi 3ad6088565 feat: skip button for reload visualizer 2025-08-29 23:08:44 -05:00
TrèsAbhi b6fd568cc6 fix: screenshot props 2025-08-29 23:00:02 -05:00
TrèsAbhi 3861b07aef fix: remove "show grid" 2025-08-29 22:53:04 -05:00
TrèsAbhi 2bf57e2c72 feat: better framing 2025-08-29 22:51:24 -05:00
TrèsAbhi 744c6654cf feat: cooler initial position 2025-08-29 22:39:34 -05:00
TrèsAbhi b82e905757 fix: dead code 2025-08-29 13:42:44 -05:00
TrèsAbhi c42e5b7dc3 feat: logs 2025-08-29 13:32:46 -05:00
TrèsAbhi b716218ecd fix: loading bar 2025-08-29 13:31:48 -05:00
TrèsAbhi 511346e4bb fix: select 2025-08-29 13:26:34 -05:00
TrèsAbhi 5c2e49453e feat: more lighting 2025-08-29 13:21:41 -05:00
TrèsAbhi 5d397de766 feat: more progress 2025-08-28 18:23:02 -05:00
TrèsAbhi aa763f65d1 feat: more progress 2025-08-28 17:59:31 -05:00
TrèsAbhi 94cbd95509 feat: start of new lighting 2025-08-28 16:52:23 -05:00
TrèsAbhi f3762e356f feat: start of mixer 2025-08-28 16:12:40 -05:00
TrèsAbhi 5747d7549f feat: tweak a few things and enable guess game 2025-08-27 17:43:42 -05:00
TrèsAbhi aea4905d20 fix: vars 2025-08-27 17:22:06 -05:00
61 changed files with 846 additions and 854 deletions

No files matched your search

+13
View File
@@ -22,6 +22,19 @@ env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }} PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }} PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }} PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
jobs: jobs:
host: host:
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.27.18
## Features
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
- Switched to a new renderer for better lighting for the Tankopedia
## Fixes
- The loading bar for the Tankopedia sandbox now shows up again
- Removed a lot of dead code, improving performance
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.27.19
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
## Features
- The camera is now lower down in the Tankopedia, giving the tank a more heroic feel
- Tanks are better framed across all devices, minimizing whitespace
- Added a skip button for the reload visualizer to skip to the next event
## Fixes
- Removed the useless "show grid" setting
- Taking screenshots now no longer includes the scene props like the floor
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
- The shot display card is now properly aligned as a proper table again
- Tank modules are once again considered in the penetration tester
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"private": true, "private": true,
"version": "1.27.17", "version": "1.27.19",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module", "type": "module",
"types": "./types.d.ts", "types": "./types.d.ts",
+12 -3
View File
@@ -861,6 +861,13 @@
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -887,6 +894,10 @@
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.", "tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -935,9 +946,7 @@
"green_penetration": "Green penetration", "green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
+10 -3
View File
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -787,6 +793,9 @@
"tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 blindaje, estadisticas, equipamiento, proviciones y mas.\n%s2 DPM, %3hp alfa, %s4mm penetracion, %s5kph velocidad maxima, %s6 hp salud", "tank_seo_description": "%s1 blindaje, estadisticas, equipamiento, proviciones y mas.\n%s2 DPM, %3hp alfa, %s4mm penetracion, %s5kph velocidad maxima, %s6 hp salud",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -829,9 +838,7 @@
"green_penetration": "Penetración verde", "green_penetration": "Penetración verde",
"hide_model_under_armor": "Ocultar modelo bajo la armadura", "hide_model_under_armor": "Ocultar modelo bajo la armadura",
"opaque": "Opaco", "opaque": "Opaco",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Entorno",
"show_grid": "Mostrar cuadrícula"
}, },
"screenshot": { "screenshot": {
"download": "Descargar", "download": "Descargar",
+51 -44
View File
@@ -364,9 +364,9 @@
"non_tech_tree": "# %s1 n'est pas dans l'arbre technologique\n\nVous ne pouvez pas rechercher un char prenium/de collection.", "non_tech_tree": "# %s1 n'est pas dans l'arbre technologique\n\nVous ne pouvez pas rechercher un char prenium/de collection.",
"tier_1": "# %s1 est un char de tier I\n\nLe char de tier 1 sont déverrouillé gratuitement pour tous les joueurs.", "tier_1": "# %s1 est un char de tier I\n\nLe char de tier 1 sont déverrouillé gratuitement pour tous les joueurs.",
"already_researched": "# %s1 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.", "already_researched": "# %s1 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": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "start_end_equal": "# %s1 a été fourni deux fois\n\nAssurez-vous que `starting-tank` et `target-tank` sont différents.",
"tanks_not_on_line": "# %s1 and %s2 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": "%s1 et %s2 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": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank." "unordered": "# %s1 niveau supérieur à %s2\n\nAssurez-vous de fournir %s3 comme le `starting-tank` à la place car c'est le réservoir de niveau inférieur."
} }
}, },
"about": { "about": {
@@ -380,7 +380,7 @@
}, },
"embeds": { "embeds": {
"name": "intègre", "name": "intègre",
"description": "How embeds work" "description": "Comment fonctionnent les intégrations"
}, },
"introduction": { "introduction": {
"name": "introduction", "name": "introduction",
@@ -402,7 +402,7 @@
}, },
"breakdown": { "breakdown": {
"name": "décomposition", "name": "décomposition",
"description": "A period's breakdown by tanks played", "description": "Répartition d'une période par chars joués",
"body": { "body": {
"total": "Total", "total": "Total",
"battles": "Batailles", "battles": "Batailles",
@@ -471,7 +471,7 @@
"accuracy": "Précision" "accuracy": "Précision"
}, },
"lethality": { "lethality": {
"title": "Lethality", "title": "Létalité",
"average_damage": "Dégâts moyens", "average_damage": "Dégâts moyens",
"damage_per_tier": "Dégâts par tier", "damage_per_tier": "Dégâts par tier",
"damage_ratio": "Rapport de dégâts", "damage_ratio": "Rapport de dégâts",
@@ -499,20 +499,20 @@
}, },
"embed": { "embed": {
"title": "Compte invalide ?", "title": "Compte invalide ?",
"description": "Use the command again and select a username from the search results." "description": "Utilisez à nouveau la commande et sélectionnez un nom d'utilisateur à partir des résultats de recherche."
} }
}, },
"search_clans": { "search_clans": {
"name": "search-clans", "name": "clans de recherche",
"description": "Search clans", "description": "Rechercher des clans",
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "The max number of clan results (default: %s1)" "description": "Nombre maximum de résultats du clan (par défaut: %s1)"
} }
}, },
"body": { "body": {
"title": "Clan search for %s1 in region %s2", "title": "Recherche de clan %s1 dans la région %s2",
"no_results": "Pas de clan trouvé" "no_results": "Pas de clan trouvé"
} }
}, },
@@ -535,69 +535,69 @@
"description": "Statistiques des batailles de cotés", "description": "Statistiques des batailles de cotés",
"subcommands": { "subcommands": {
"neighbors": { "neighbors": {
"name": "neighbors", "name": "voisins",
"description": "Neighboring players on the leaderboard" "description": "Joueurs voisins dans le classement"
}, },
"league": { "league": {
"name": "ligue", "name": "ligue",
"description": "Top players of a league" "description": "Meilleurs joueurs d'une ligue"
} }
}, },
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "How many neighbors to display (default: %s1)" "description": "Combien de voisins afficher (par défaut : %s1)"
}, },
"season": { "season": {
"name": "season", "name": "saison",
"description": "Season number (default: current)", "description": "Numéro de saison (défaut : courant)",
"choices": { "choices": {
"current": "%s1 (current)" "current": "%s1 (actuel)"
} }
}, },
"league": { "league": {
"name": "ligue", "name": "ligue",
"description": "The league to display" "description": "La ligue à afficher"
} }
}, },
"body": { "body": {
"top_players": "Top %s1 players", "top_players": "Top %s1 joueurs",
"no_participation": "# %s1 didn't play rating in season %s2\n\nThis player did not participate in this season or did not get past calibration.", "no_participation": "%s1 n'a pas joué au classement dans la saison %s2",
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "Pas de saison en cours\n\nIl n'y a pas de saison de notation en cours actuellement.",
"deleted_player": "Deleted %s1", "deleted_player": "%s1 supprimé",
"neighbors": "Rating neighbors", "neighbors": "évaluer les voisins",
"more": "+ %s1 more" "more": "+ %s1 de plus"
} }
}, },
"today": { "today": {
"name": "aujourd'hui", "name": "aujourd'hui",
"description": "Today's played tanks and statistics" "description": "Les chars et les statistiques joués aujourd'hui"
}, },
"stats": { "stats": {
"name": "statistiques", "name": "statistiques",
"description": "Quick statistics for a period" "description": "Statistiques rapides pour une période"
}, },
"owned_tanks": { "owned_tanks": {
"name": "owned-tanks", "name": "chars possédés",
"description": "Shows all own tanks in a tier", "description": "Affiche tous les chars d'un niveau",
"body": { "body": {
"title": "# %s1's owned tier %s2 tanks\n\n" "title": "# %s1 possédait des chars de rang %s2"
} }
}, },
"permissions": { "permissions": {
"name": "permissions", "name": "permissions",
"description": "Checks wether the bot has the required permissions to function", "description": "Vérifie si le bot a les permissions nécessaires pour fonctionner",
"body": { "body": {
"title": "# Permissions\n\n%s1", "title": "# Permissions\n\n%s1",
"view_channels": "View channels: needed to find the message the refresh button was used on", "view_channels": "Afficher les canaux : nécessaire pour trouver le message sur lequel le bouton d'actualisation a été utilisé",
"read_message_history": "Read message history: needed to find the message to refresh", "read_message_history": "Lire l'historique des messages : nécessaire pour trouver le message à rafraîchir",
"attach_files": "Attach files: needed to add images to image-based commands", "attach_files": "Joindre des fichiers : nécessaire pour ajouter des images à des commandes basées sur l'image",
"external_emojis": "External emojis: needed to add national flags and tank class emojis" "external_emojis": "Émoticônes externes: nécessaire pour ajouter des drapeaux nationaux et des émojis de classe de char"
} }
}, },
"ping": { "ping": {
"name": "ping", "name": "ping",
"description": "Check wether the bot is alive.", "description": "Vérifiez si le bot est en vie.",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - %s1ms"
@@ -605,7 +605,7 @@
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
"name": "blitzkit", "name": "blitzkit",
"description": "Ping BlitzKit" "description": "Ping blitzkit"
}, },
"wotb": { "wotb": {
"name": "wotb", "name": "wotb",
@@ -614,10 +614,10 @@
} }
}, },
"player_info": { "player_info": {
"name": "player-info", "name": "info-joueur",
"description": "Basic information about a player", "description": "Informations de base sur un joueur",
"body": { "body": {
"title": "%s1's information", "title": "Informations de %s1",
"nickname": "Nickname", "nickname": "Nickname",
"battles": "Batailles", "battles": "Batailles",
"winrate": "Taux de victoires", "winrate": "Taux de victoires",
@@ -635,7 +635,7 @@
"description": "Today's rating statistics" "description": "Today's rating statistics"
}, },
"season": { "season": {
"name": "season", "name": "saison",
"description": "This season's rating statistics" "description": "This season's rating statistics"
} }
}, },
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -787,6 +793,9 @@
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.", "tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -829,9 +838,7 @@
"green_penetration": "Green penetration", "green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
"download": "Download", "download": "Download",
+10 -3
View File
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -787,6 +793,9 @@
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.", "tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -829,9 +838,7 @@
"green_penetration": "貫通可能域を緑色表示", "green_penetration": "貫通可能域を緑色表示",
"hide_model_under_armor": "車輌モデルを非表示", "hide_model_under_armor": "車輌モデルを非表示",
"opaque": "貫通アシストを不透明にする", "opaque": "貫通アシストを不透明にする",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "環境",
"show_grid": "グリッドを表示"
}, },
"screenshot": { "screenshot": {
"download": "保存する", "download": "保存する",
+10 -3
View File
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -787,6 +793,9 @@
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.", "tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -829,9 +838,7 @@
"green_penetration": "Green penetration", "green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
"download": "Download", "download": "Download",
+10 -3
View File
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -787,6 +793,9 @@
"tank_seo_title": "%s1 - Nível %s2 %s3 %s4 | %s5 Tanquepedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "%s1 - Nível %s2 %s3 %s4 | %s5 Tanquepedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 blindagem, estatísticas, equipamentos, provisões e mais. %s2 DPM, %s3hp alfa, %s4mm de penetração, %s5km/h velocidade máxima, %s6hp vida.", "tank_seo_description": "%s1 blindagem, estatísticas, equipamentos, provisões e mais. %s2 DPM, %s3hp alfa, %s4mm de penetração, %s5km/h velocidade máxima, %s6hp vida.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -829,9 +838,7 @@
"green_penetration": "Penetração verde", "green_penetration": "Penetração verde",
"hide_model_under_armor": "Ocultar modelo sob blindagem", "hide_model_under_armor": "Ocultar modelo sob blindagem",
"opaque": "Opaco", "opaque": "Opaco",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Ambiente",
"show_grid": "Exibir grade"
}, },
"screenshot": { "screenshot": {
"download": "Download", "download": "Download",
+59 -52
View File
@@ -1,8 +1,8 @@
{ {
"test": { "test": {
"default": "тест", "default": "тест",
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.", "long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — %s1, потому что это цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.",
"fox": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3." "fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
}, },
"common": { "common": {
"units": { "units": {
@@ -283,15 +283,15 @@
"name": "уровень", "name": "уровень",
"description": "Уровень", "description": "Уровень",
"choices": { "choices": {
"tier": "Уровень %s - %s" "tier": "Уровень %s1 - %s2"
} }
} }
}, },
"periods": { "periods": {
"career": "Карьера", "career": "Карьера",
"today": "Cегодня", "today": "Cегодня",
"days": "%s дней", "days": "%s1 дней",
"custom": "%s - %s дней" "custom": "%s1 - %s2 дней"
}, },
"filters": { "filters": {
"none": "Без фильтров", "none": "Без фильтров",
@@ -323,9 +323,9 @@
"description": "Танки доступные на аукционе сейчас", "description": "Танки доступные на аукционе сейчас",
"body": { "body": {
"title": "Blitz аукцион", "title": "Blitz аукцион",
"subtitle": "%s доступные • следующие цены %s\nПокупка сейчас: %s", "subtitle": "%s1 доступные • следующие цены %s2\nПокупка сейчас: %s3",
"next": "Следующая цена: %s", "next": "Следующая цена: %s1",
"available": "Доступно: %s / %s" "available": "Доступно: %s1 / %s2"
}, },
"errors": { "errors": {
"no_auction": "# Аукцион сейчас не проходит\n\nНа данный момент нет танков для покупки на аукционе. Если считаете, что это ошибка, пожалуйста о несоответствии на [официальном сервере Discord](https://discord.gg/nDt7AjGJQH)." "no_auction": "# Аукцион сейчас не проходит\n\nНа данный момент нет танков для покупки на аукционе. Если считаете, что это ошибка, пожалуйста о несоответствии на [официальном сервере Discord](https://discord.gg/nDt7AjGJQH)."
@@ -354,19 +354,19 @@
"body": { "body": {
"subtitle": "Исследование", "subtitle": "Исследование",
"total": "Всего", "total": "Всего",
"research": "Исследование: %s", "research": "Исследование: %s1",
"upgrades": "Улучшения: %s", "upgrades": "Улучшения: %s1",
"purchase": "Покупка: %s", "purchase": "Покупка: %s1",
"equipment": "Снаряжение: %s", "equipment": "Снаряжение: %s1",
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете." "estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
}, },
"errors": { "errors": {
"non_tech_tree": "# %s отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.", "non_tech_tree": "# %s1 отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.",
"tier_1": "# %s - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.", "tier_1": "# %s1 - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.",
"already_researched": "# %s уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.", "already_researched": "# %s1 уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
"start_end_equal": "# %s был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.", "start_end_equal": "# %s1 был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
"tanks_not_on_line": "# %s и %s находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.", "tanks_not_on_line": "# %s1 и %s2 находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
"unordered": "# У %s более высокий уровень чем у %s\n\nОбязательно укажите %s в качестве «starting-tank», так как это танк более низкого уровня." "unordered": "# У %s1 более высокий уровень чем у %s2\n\nОбязательно укажите %s3 в качестве «starting-tank», так как это танк более низкого уровня."
} }
}, },
"about": { "about": {
@@ -409,7 +409,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Коэффициент побед", "winrate": "Коэффициент побед",
"damage": "Урон", "damage": "Урон",
"unknown_tank": "Неизвестный танк %s" "unknown_tank": "Неизвестный танк %s1"
} }
}, },
"debug": { "debug": {
@@ -445,12 +445,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "порог", "name": "порог",
"description": "Количество дней не активности (по умолчанию: %s дней)" "description": "Количество дней не активности (по умолчанию: %s1 дней)"
} }
}, },
"body": { "body": {
"subtitle": "Неактивен %s+ дней", "subtitle": "Неактивен %s1+ дней",
"listing": "%s дней" "listing": "%s1 дней"
} }
}, },
"full_stats": { "full_stats": {
@@ -508,11 +508,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "лимит", "name": "лимит",
"description": "Максимальное количество результатов клана (по умолчанию: %s)" "description": "Максимальное количество результатов клана (по умолчанию: %s1)"
} }
}, },
"body": { "body": {
"title": "Поиск клана %s в регионе %s", "title": "Поиск клана %s1 в регионе %s2",
"no_results": "Кланов не найдено" "no_results": "Кланов не найдено"
} }
}, },
@@ -522,11 +522,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "лимит", "name": "лимит",
"description": "Максимальное число результатов игрока (по умолчанию: %s)" "description": "Максимальное число результатов игрока (по умолчанию: %s1)"
} }
}, },
"body": { "body": {
"title": "Поиск игрока %s в регионе %s", "title": "Поиск игрока %s1 в регионе %s2",
"no_results": "Игроков не найдено" "no_results": "Игроков не найдено"
} }
}, },
@@ -546,13 +546,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "лимит", "name": "лимит",
"description": "Количество отображаемых ближайших игроков (по умолчанию: %s)" "description": "Количество отображаемых ближайших игроков (по умолчанию: %s1)"
}, },
"season": { "season": {
"name": "сезон", "name": "сезон",
"description": "Номер сезона (по умолчанию: текущий)", "description": "Номер сезона (по умолчанию: текущий)",
"choices": { "choices": {
"current": "%s (текущая)" "current": "%s1 (текущая)"
} }
}, },
"league": { "league": {
@@ -561,12 +561,12 @@
} }
}, },
"body": { "body": {
"top_players": "Топ %s игроков", "top_players": "Топ %s1 игроков",
"no_participation": "# %s не играл в рейтинг в сезоне %s\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.", "no_participation": "# %s1 не играл в рейтинг в сезоне %s2\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
"no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.", "no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.",
"deleted_player": "Удалено %s", "deleted_player": "Удалено %s1",
"neighbors": "Рейтинг ближайших игроков", "neighbors": "Рейтинг ближайших игроков",
"more": "+ ещё %s" "more": "+ ещё %s1"
} }
}, },
"today": { "today": {
@@ -581,14 +581,14 @@
"name": "cобственные-танки", "name": "cобственные-танки",
"description": "Показывает все принадлежащие танки на уровне", "description": "Показывает все принадлежащие танки на уровне",
"body": { "body": {
"title": "# %s принадлежал уровень %s танков\n\n" "title": "# %s1 принадлежал уровень %s2 танков\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "разрешения", "name": "разрешения",
"description": "Проверяет, есть ли у бота необходимые разрешения для работы", "description": "Проверяет, есть ли у бота необходимые разрешения для работы",
"body": { "body": {
"title": "# Разрешения\n\n%s", "title": "# Разрешения\n\n%s1",
"view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления", "view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления",
"read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления", "read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления",
"attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения", "attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения",
@@ -600,7 +600,7 @@
"description": "Проверьте, что бот жив.", "description": "Проверьте, что бот жив.",
"body": { "body": {
"pong": "Понг 🏓", "pong": "Понг 🏓",
"pong_with_time": "Понг 🏓 - %sмс" "pong_with_time": "Понг 🏓 - %s1мс"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +617,7 @@
"name": "информация-об-игроке", "name": "информация-об-игроке",
"description": "Основная информация о игроке", "description": "Основная информация о игроке",
"body": { "body": {
"title": "Информация о %s", "title": "Информация о %s1",
"nickname": "Никнейм", "nickname": "Никнейм",
"battles": "Боёв", "battles": "Боёв",
"winrate": "Коэффициент побед", "winrate": "Коэффициент побед",
@@ -646,7 +646,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.", "no_participation": "# %s1 не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
"unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов." "unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов."
} }
} }
@@ -676,7 +676,7 @@
"recent": "Недавние", "recent": "Недавние",
"select_all": "Выбрать всё", "select_all": "Выбрать всё",
"count_singular": "1 танк", "count_singular": "1 танк",
"count_plural": "%s1 танк", "count_plural": "Танков: %s1",
"sorting_by": "Сортировать по %s1", "sorting_by": "Сортировать по %s1",
"no_tanks_found": { "no_tanks_found": {
"body": "Танков не найдено.", "body": "Танков не найдено.",
@@ -693,7 +693,7 @@
"survivability.volume": "объём", "survivability.volume": "объём",
"survivability.length": "длина", "survivability.length": "длина",
"fire.dpm": "базовый ДПМ", "fire.dpm": "базовый ДПМ",
"fire.dpmPremium": "улучшенный ДПМ", "fire.dpmPremium": "премиумный ДПМ",
"fire.reload": "перезарядка", "fire.reload": "перезарядка",
"fire.caliber": "калибр", "fire.caliber": "калибр",
"fire.standardPenetration": "базовое пробитие", "fire.standardPenetration": "базовое пробитие",
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Смешиватель",
"button": "Смешать и собрать",
"description": "Модули смешанного танка",
"seo_description": "Смешивание и сбор корпусов, башен и пушек танков из World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Угадайте танк", "name": "Угадайте танк",
"button": "Угадать", "button": "Угадать",
@@ -784,9 +790,12 @@
"button": "Поиск танков", "button": "Поиск танков",
"description": "Танковая энциклопедия Blitz", "description": "Танковая энциклопедия Blitz",
"seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz", "seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz",
"tank_seo_title": "%s1 - %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)", "tank_seo_title": "%s1 — %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.", "tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Пропуск"
},
"assault": { "assault": {
"title": "Штурмовые снаряды" "title": "Штурмовые снаряды"
}, },
@@ -812,7 +821,7 @@
"dead_rail": "Эшелон" "dead_rail": "Эшелон"
}, },
"flexibility": { "flexibility": {
"yaw": "Отклонение", "yaw": "Поворот",
"pitch": "Наклон", "pitch": "Наклон",
"min": "Вниз", "min": "Вниз",
"max": "Вверх" "max": "Вверх"
@@ -829,9 +838,7 @@
"green_penetration": "Зелёное пробитие", "green_penetration": "Зелёное пробитие",
"hide_model_under_armor": "Скрыть модель под броней", "hide_model_under_armor": "Скрыть модель под броней",
"opaque": "Прозрачность", "opaque": "Прозрачность",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Окружение",
"show_grid": "Показать сетку"
}, },
"screenshot": { "screenshot": {
"download": "Загрузить", "download": "Загрузить",
@@ -902,7 +909,7 @@
"tier": "Уровень", "tier": "Уровень",
"type": "Тип", "type": "Тип",
"dev_id": "DEV: ID", "dev_id": "DEV: ID",
"purchase": "Стоимость покупки", "purchase": "Цена покупки",
"restoration": "Цена восстановления", "restoration": "Цена восстановления",
"sale": "Цена продажи", "sale": "Цена продажи",
"research": "Необходимо опыта" "research": "Необходимо опыта"
@@ -978,7 +985,7 @@
"dpmEffective": "Эффективность за 60с", "dpmEffective": "Эффективность за 60с",
"shells": "Снарядов в барабане", "shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд", "mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Reloads", "shellReloads": "Перезарядка",
"shellReload": "Перезарядка", "shellReload": "Перезарядка",
"intraClip": "Между снарядами", "intraClip": "Между снарядами",
"caliber": "Калибр", "caliber": "Калибр",
@@ -1038,8 +1045,8 @@
"shellCapacity": "Боезапас", "shellCapacity": "Боезапас",
"gunType": "Тип орудия", "gunType": "Тип орудия",
"penetrationAt250m": "Пробитие на 250м", "penetrationAt250m": "Пробитие на 250м",
"burstShells": "Burst shell count", "burstShells": "Кол-во разрывных снарядов",
"burstInterShell": "Burst inter-shell" "burstInterShell": "Взрыв между снарядами"
} }
}, },
"firepower": { "firepower": {
@@ -1136,7 +1143,7 @@
"hits": "Уничтожен", "hits": "Уничтожен",
"damage_ratio": "Коэф. урона", "damage_ratio": "Коэф. урона",
"damage_taken": "Получено урона", "damage_taken": "Получено урона",
"capture_points": "Ограничительные точки" "capture_points": "Точки ограничения"
} }
} }
}, },
@@ -1148,7 +1155,7 @@
"last_updated": "Последнее обновление %s1", "last_updated": "Последнее обновление %s1",
"search": { "search": {
"hint": "Поиск аватаров...", "hint": "Поиск аватаров...",
"results": "%s1 аватар" "results": "Аватаров: %s1"
} }
}, },
"session": { "session": {
@@ -1157,7 +1164,7 @@
"description": "Статистика сеанса игры", "description": "Статистика сеанса игры",
"seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz", "seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz",
"search": { "search": {
"lookup": "Искать игрока, чтобы начать", "lookup": "Введите ник игрока",
"hint": "Поиск игроков...", "hint": "Поиск игроков...",
"searching": "Поиск...", "searching": "Поиск...",
"no_results": "Игроков не найдено" "no_results": "Игроков не найдено"
@@ -1198,7 +1205,7 @@
"button": "Создать", "button": "Создать",
"description": "Встраиваемые элементы для стримов", "description": "Встраиваемые элементы для стримов",
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации", "seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
"coming_soon": "Скоро появятся новые встраиваемые элементы.", "coming_soon": "В скором времени появятся новые встраиваемые элементы.",
"suggest": "Предложить идеи.", "suggest": "Предложить идеи.",
"preview": { "preview": {
"title": "Предпросмотр", "title": "Предпросмотр",
+10 -3
View File
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -787,6 +793,9 @@
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz", "tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz",
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.", "tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -829,9 +838,7 @@
"green_penetration": "Зелене пробиття", "green_penetration": "Зелене пробиття",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
"download": "Завантажити", "download": "Завантажити",
+10 -3
View File
@@ -764,6 +764,12 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
},
"guess": { "guess": {
"name": "猜坦克", "name": "猜坦克",
"button": "猜猜看", "button": "猜猜看",
@@ -787,6 +793,9 @@
"tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)", "tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)",
"tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。", "tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。",
"visualizers": { "visualizers": {
"reload": {
"skip": "跳过"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -829,9 +838,7 @@
"green_penetration": "可击穿区域", "green_penetration": "可击穿区域",
"hide_model_under_armor": "隐藏装甲下的模型", "hide_model_under_armor": "隐藏装甲下的模型",
"opaque": "不透明", "opaque": "不透明",
"dev_wireframe": "开发选项:网格", "dev_wireframe": "开发选项:网格"
"environment": "环境",
"show_grid": "显示网格"
}, },
"screenshot": { "screenshot": {
"download": "下载", "download": "下载",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

After

Width:  |  Height:  |  Size: 669 KiB

@@ -14,7 +14,6 @@ export const PrimaryArmorScene = memo(() => {
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null); const gunContainer = useRef<Group>(null);
// const protagonist = Duel.use((draft) => draft.protagonist);
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track); const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
@@ -80,7 +80,7 @@ export function PrimaryArmorSceneComponent({
}); });
useEffect(() => { useEffect(() => {
async function handleShellChange() { function handleShellChange() {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState(); const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
@@ -97,14 +97,8 @@ export function PrimaryArmorSceneComponent({
material.uniforms.damage.value = shell.armor_damage; material.uniforms.damage.value = shell.armor_damage;
material.uniforms.explosionRadius.value = shell.explosion_radius; material.uniforms.explosionRadius.value = shell.explosion_radius;
await handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix, true);
duel.protagonist.equipmentMatrix, handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix, true);
true,
);
await handleAntagonistEquipmentChange(
duel.antagonist.equipmentMatrix,
true,
);
invalidate(); invalidate();
} }
@@ -121,12 +115,12 @@ export function PrimaryArmorSceneComponent({
function handleWireframeChange(wireframe: boolean) { function handleWireframeChange(wireframe: boolean) {
material.wireframe = wireframe; material.wireframe = wireframe;
} }
async function handleProtagonistEquipmentChange( function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false, noInvalidate = false,
) { ) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
equipment, equipment,
@@ -137,7 +131,7 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate(); if (!noInvalidate) invalidate();
} }
async function handleAntagonistEquipmentChange( function handleAntagonistEquipmentChange(
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false, noInvalidate = false,
) { ) {
@@ -145,7 +139,7 @@ export function PrimaryArmorSceneComponent({
const tankopediaEphemeral = tankopediaEphemeralStore.getState(); const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = await hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, duel.antagonist.tank.equipment_preset,
equipment, equipment,
@@ -37,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) { vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
if (advancedHighlighting && couldHaveRicochet) { if (advancedHighlighting && couldHaveRicochet) {
return vec3(0.0, 1.0, isThreeCalibersRule ? 0.5 : 0.0); return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
} }
return vec3(0.0, 1.0, 0.0); return vec3(0.0, 1.0, 0.0);
@@ -107,7 +107,7 @@ void main() {
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0); vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
if (advancedHighlighting && didRicochet) { if (advancedHighlighting && didRicochet) {
baseColor = vec3(1.0, baseColor.g, 0.5); baseColor = vec3(1.0, baseColor.g, 1.0);
} }
if (greenPenetration || advancedHighlighting) { if (greenPenetration || advancedHighlighting) {
@@ -16,6 +16,7 @@ import {
Path, Path,
Quaternion, Quaternion,
} from 'three'; } from 'three';
import { isHalloween } from '../../../core/blitzkit/isHalloween';
import { LocaleProvider, useLocale } from '../../../hooks/useLocale'; import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
import { import {
TankopediaEphemeral, TankopediaEphemeral,
@@ -72,11 +73,8 @@ export function ShotDisplay() {
}); });
useEffect(() => { useEffect(() => {
const data = new Date();
const isHalloween = data.getMonth() === 9 && data.getDate() === 31;
if ( if (
isHalloween && isHalloween() &&
shot?.splashRadius !== undefined && shot?.splashRadius !== undefined &&
shot.in.status === 'penetration' shot.in.status === 'penetration'
) { ) {
@@ -37,44 +37,38 @@ function LayerEntry({
const { locale, strings } = useLocale(); const { locale, strings } = useLocale();
return ( return (
<Flex align="center" gap="2"> <tr>
<Text size="1" color="gray" style={{ width: 12 }}> <td>
{layerIndex} <Text size="1" color="gray">
</Text> {layerIndex}
<Text size="2" color={shotStatusColor} style={{ width: 48 }}> </Text>
{layerName} </td>
</Text>
{layer.type === null && ( <td>
<> <Text size="2" color={shotStatusColor}>
<Text size="2" color={shotStatusColor} style={{ width: 64 }}> {layerName}
</Text>
</td>
<td>
{layer.type === null && (
<Text size="2" color={shotStatusColor}>
{literals(strings.common.units.mm, [ {literals(strings.common.units.mm, [
Math.round(layer.distance * 1000).toLocaleString(locale), Math.round(layer.distance * 1000).toLocaleString(locale),
])} ])}
</Text> </Text>
)}
<Text size="2" color="gray"> {layer.type === ArmorType.External && (
{literals( <Text size="2" color={shotStatusColor}>
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats {literals(strings.common.units.mm, [
.ricochet_loss, Math.round(layer.thickness).toLocaleString(locale),
[Math.max(-100, -50 * layer.distance).toFixed(0)], ])}
)}
</Text> </Text>
</> )}
)} {(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
{layer.type === ArmorType.External && ( <Text size="2" color={shotStatusColor}>
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
{literals(strings.common.units.mm, [
Math.round(layer.thickness).toLocaleString(locale),
])}
</Text>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<>
<Text size="2" color={shotStatusColor} style={{ width: 80 }}>
{literals( {literals(
angle === undefined angle === undefined
? strings.common.units.mm ? strings.common.units.mm
@@ -86,7 +80,22 @@ function LayerEntry({
], ],
)} )}
</Text> </Text>
)}
</td>
<td>
{layer.type === null && (
<Text size="2" color="gray">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.ricochet_loss,
[Math.max(-100, -50 * layer.distance).toFixed(0)],
)}
</Text>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<Text size="2" color="gray"> <Text size="2" color="gray">
{literals( {literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
@@ -94,9 +103,9 @@ function LayerEntry({
[Math.round(layer.thickness).toLocaleString(locale)], [Math.round(layer.thickness).toLocaleString(locale)],
)} )}
</Text> </Text>
</> )}
)} </td>
</Flex> </tr>
); );
} }
@@ -115,7 +124,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
const inTitleColor = shotStatusColors[shot.in.status]; const inTitleColor = shotStatusColors[shot.in.status];
return ( return (
<Card variant="classic" style={{ width: 300 }} {...props}> <Card variant="classic" {...props}>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Flex direction="column" gap="1"> <Flex direction="column" gap="1">
<Text color={inTitleColor} weight="bold"> <Text color={inTitleColor} weight="bold">
@@ -127,7 +136,12 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
)} )}
</Text> </Text>
<Flex direction="column"> <table
style={{
whiteSpace: 'nowrap',
borderSpacing: 'var(--space-1) 0',
}}
>
{shot.in.layers.map((layer, index) => { {shot.in.layers.map((layer, index) => {
const layerName = const layerName =
strings.website.tools.tankopedia.sandbox.dynamic.shot_card strings.website.tools.tankopedia.sandbox.dynamic.shot_card
@@ -161,7 +175,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
/> />
); );
})} })}
</Flex> </table>
</Flex> </Flex>
{shot.out && shot.out.layers.length > 0 && ( {shot.out && shot.out.layers.length > 0 && (
@@ -54,11 +54,9 @@ export function SpacedArmorSubExternal({
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near; material.uniforms.penetration.value = shell.penetration.near;
} }
async function handleProtagonistEquipmentChange( function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
equipment: EquipmentMatrix,
) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
equipment, equipment,
@@ -67,12 +65,12 @@ export function SpacedArmorSubExternal({
? thickness * 1.03 ? thickness * 1.03
: thickness; : thickness;
} }
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState(); const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = await hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, duel.antagonist.tank.equipment_preset,
equipment, equipment,
@@ -125,6 +123,7 @@ export function SpacedArmorSubExternal({
return ( return (
<mesh <mesh
{...props} {...props}
onClick={() => {}}
key={`${key}-spaced-sub-external-exclude`} key={`${key}-spaced-sub-external-exclude`}
renderOrder={3} renderOrder={3}
material={ material={
@@ -64,11 +64,9 @@ export function SpacedArmorSubSpaced({
shell.normalization ?? 0, shell.normalization ?? 0,
); );
} }
async function handleProtagonistEquipmentChange( function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
equipment: EquipmentMatrix,
) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
equipment, equipment,
@@ -77,12 +75,12 @@ export function SpacedArmorSubSpaced({
? thickness * 1.03 ? thickness * 1.03
: thickness; : thickness;
} }
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState(); const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = await hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, duel.antagonist.tank.equipment_preset,
equipment, equipment,
@@ -81,7 +81,7 @@ export function SpacedArmorSceneComponent({
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const shoot = useCallback( const shoot = useCallback(
async ( (
point: Vector3, point: Vector3,
intersections: Intersection[], intersections: Intersection[],
allowRicochet: boolean, allowRicochet: boolean,
@@ -103,12 +103,12 @@ export function SpacedArmorSceneComponent({
}, },
}; };
const hasCalibratedShells = await hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
antagonist.tank.equipment_preset, antagonist.tank.equipment_preset,
antagonist.equipmentMatrix, antagonist.equipmentMatrix,
); );
const hasEnhancedArmor = await hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
protagonist.tank.equipment_preset, protagonist.tank.equipment_preset,
protagonist.equipmentMatrix, protagonist.equipmentMatrix,
@@ -340,7 +340,7 @@ export function SpacedArmorSceneComponent({
props.scene.children, props.scene.children,
true, true,
); );
const outShot = await shoot( const outShot = shoot(
lastLayer.point, lastLayer.point,
ricochetIntersections, ricochetIntersections,
false, false,
@@ -386,13 +386,11 @@ export function SpacedArmorSceneComponent({
thickness, thickness,
} satisfies ArmorUserData } satisfies ArmorUserData
} }
onClick={async (event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
const shot = (await shoot(
event.point, const shot = shoot(event.point, event.intersections, true)!;
event.intersections,
true,
))!;
tankopediaEphemeralStore.setState({ shot }); tankopediaEphemeralStore.setState({ shot });
}} }}
/> />
@@ -30,7 +30,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null); const gunContainer = useRef<Group>(null);
const protagonist = Duel.use((draft) => draft.protagonist);
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track); const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
@@ -46,7 +45,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hullOrigin = correctZYTuple(trackModelDefinition.origin); const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const mutateDuel = Duel.useMutation();
const canvas = useThree((state) => state.gl.domElement); const canvas = useThree((state) => state.gl.domElement);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
@@ -167,14 +165,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
async function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = await hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
@@ -209,7 +207,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<StaticArmorSceneComponent <StaticArmorSceneComponent
name={node.name} name={node.name}
thicknessRange={thicknessRange} thicknessRange={thicknessRange}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Primary} type={spaced ? ArmorType.Spaced : ArmorType.Primary}
thickness={thickness} thickness={thickness}
node={node} node={node}
@@ -261,14 +258,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
async function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = await hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
@@ -342,14 +339,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
async function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = await hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
@@ -10,7 +10,7 @@ import {
FrontSide, FrontSide,
LineBasicMaterial, LineBasicMaterial,
Mesh, Mesh,
MeshStandardMaterial, MeshBasicMaterial,
Object3D, Object3D,
Plane, Plane,
Quaternion, Quaternion,
@@ -108,7 +108,7 @@ export function StaticArmorSceneComponent({
const surfaceMaterial = useMemo( const surfaceMaterial = useMemo(
() => () =>
new MeshStandardMaterial({ new MeshBasicMaterial({
color, color,
opacity, opacity,
transparent: opacity < 1, transparent: opacity < 1,
@@ -1,6 +1,5 @@
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons'; import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes'; import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember'; import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from '../../hooks/useLocale';
@@ -16,10 +15,7 @@ interface ControlsProps {
onAddTankDialogOpenChange: (open: boolean) => void; onAddTankDialogOpenChange: (open: boolean) => void;
} }
const [provisionDefinitions, modelDefinitions] = await Promise.all([ const provisionDefinitions = await awaitableProvisionDefinitions;
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
export function Controls({ export function Controls({
addTankDialogOpen, addTankDialogOpen,
@@ -60,11 +56,9 @@ export function Controls({
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => { mutateCompareEphemeral((draft) => {
draft.members.push( draft.members.push(
tankToCompareMember(tank, model, provisionDefinitions), tankToCompareMember(tank, provisionDefinitions),
); );
draft.sorting = undefined; draft.sorting = undefined;
}); });
@@ -74,13 +68,7 @@ export function Controls({
mutateCompareEphemeral((draft) => { mutateCompareEphemeral((draft) => {
draft.members.push( draft.members.push(
...tanks.map((tank) => { ...tanks.map((tank) => {
const model = modelDefinitions.models[tank.id]; return tankToCompareMember(tank, provisionDefinitions);
return tankToCompareMember(
tank,
model,
provisionDefinitions,
);
}), }),
); );
draft.sorting = undefined; draft.sorting = undefined;
@@ -1,7 +1,6 @@
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons'; import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
import { Dialog, Flex, IconButton } from '@radix-ui/themes'; import { Dialog, Flex, IconButton } from '@radix-ui/themes';
import { useState } from 'react'; import { useState } from 'react';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember'; import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from '../../hooks/useLocale';
@@ -14,10 +13,7 @@ interface TankControlProps {
slug: string; slug: string;
} }
const [provisionDefinitions, modelDefinitions] = await Promise.all([ const provisionDefinitions = await awaitableProvisionDefinitions;
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
export function TankControl({ index, slug }: TankControlProps) { export function TankControl({ index, slug }: TankControlProps) {
const { locale } = useLocale(); const { locale } = useLocale();
@@ -61,12 +57,9 @@ export function TankControl({ index, slug }: TankControlProps) {
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => { mutateCompareEphemeral((draft) => {
draft.members[index] = tankToCompareMember( draft.members[index] = tankToCompareMember(
tank, tank,
model,
provisionDefinitions, provisionDefinitions,
); );
draft.sorting = undefined; draft.sorting = undefined;
@@ -22,8 +22,7 @@ export function GuessBackground() {
useEffect(() => { useEffect(() => {
mutateDuel((state) => { mutateDuel((state) => {
const model = modelDefinitions.models[tank.id]; state.protagonist = tankToDuelMember(tank, provisionDefinitions);
state.protagonist = tankToDuelMember(tank, model, provisionDefinitions);
}); });
mutateTankopediaEphemeral((state) => { mutateTankopediaEphemeral((state) => {
state.model = modelDefinitions.models[tank.id]; state.model = modelDefinitions.models[tank.id];
@@ -1,11 +1,10 @@
import { GuessEphemeral } from '../stores/guessEphemeral'; import { GuessEphemeral } from '../stores/guessEphemeral';
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
import { SmartCanvas } from './SmartCanvas'; import { SmartCanvas } from './SmartCanvas';
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control'; import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting'; import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel'; import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
const BRIGHTNESS = 2 ** -5; const BRIGHTNESS = 2 ** -5.5;
const CONTRAST = 2 ** 2; const CONTRAST = 2 ** 2;
export function GuessRenderer() { export function GuessRenderer() {
@@ -16,14 +15,13 @@ export function GuessRenderer() {
<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})`,
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
transitionDuration: isRevealed ? '2s' : undefined, transitionDuration: isRevealed ? '2s' : undefined,
transitionProperty: 'filter', transitionProperty: 'filter',
}} }}
camera={{ position: [-5, 5, -5] }} camera={{ position: [-5, 5, -5] }}
> >
<Controls distanceScale={2} zoomable={false} /> <Controls zoomable={false} />
<Lighting display={TankopediaDisplay.Model} /> <Lighting />
<TankModel /> <TankModel />
</SmartCanvas> </SmartCanvas>
); );
+1 -1
View File
@@ -138,7 +138,7 @@ export function Guesser() {
</Card> </Card>
)} )}
<Flex justify="center"> <Flex justify="center" style={{ userSelect: 'none' }}>
<Text> <Text>
{literals(strings.website.tools.guess.stats, [ {literals(strings.website.tools.guess.stats, [
`${correctGuesses}`, `${correctGuesses}`,
@@ -0,0 +1,21 @@
import type { SpotLightProps } from '@react-three/fiber';
import { memo } from 'react';
interface Props extends SpotLightProps {
debug?: boolean;
}
export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
return (
<>
{debug && (
<mesh position={props.position}>
<icosahedronGeometry args={[0.1, 2]} />
<meshBasicMaterial color={props.color} />
</mesh>
)}
<spotLight {...props} />
</>
);
});
@@ -0,0 +1,125 @@
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
import { jsxTree } from '../../../core/blitzkit/jsxTree';
import { useModel } from '../../../hooks/useModel';
import { MixerEphemeral } from '../../../stores/mixerEphemeral';
const modelDefinitions = await awaitableModelDefinitions;
export function Model() {
const hull = MixerEphemeral.use((state) => state.hull);
const turret = MixerEphemeral.use((state) => state.turret);
const gun = MixerEphemeral.use((state) => state.gun);
const hullGltf = useModel(hull.id);
const turretGltf = useModel(turret.tank.id);
const gunGltf = useModel(gun.tank.id);
const hullNodes = Object.values(hullGltf.gltf.nodes);
const turretNodes = Object.values(turretGltf.gltf.nodes);
const gunNodes = Object.values(gunGltf.gltf.nodes);
const tankModel = modelDefinitions.models[hull.id];
const trackModel = tankModel.tracks[hull.tracks.at(-1)!.id];
const turretTankModel = modelDefinitions.models[turret.tank.id];
const turretTrackModel =
turretTankModel.tracks[turret.tank.tracks.at(-1)!.id];
const turretModel = turretTankModel.turrets[turret.turret.id];
const gunTankModel = modelDefinitions.models[gun.tank.id];
const gunTrackModel = gunTankModel.tracks[gun.tank.tracks.at(-1)!.id];
const gunTurretModel = gunTankModel.turrets[gun.turret.id];
const gunModel = gunTurretModel.guns[gun.gun.id];
return (
<group rotation={[-Math.PI / 2, 0, 0]}>
{hullNodes.map((node) => {
const isHull = node.name === 'hull';
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isHull || isWheel || isTrack;
if (!isVisible) return null;
return jsxTree(node, {
mesh(_, props, key) {
return <mesh {...props} key={key} castShadow receiveShadow />;
},
});
})}
<group
position={[
tankModel.turret_origin.x +
trackModel.origin.x -
turretTankModel.turret_origin.x -
turretTrackModel.origin.x,
tankModel.turret_origin.z +
trackModel.origin.z -
turretTankModel.turret_origin.z -
turretTrackModel.origin.z,
tankModel.turret_origin.y +
trackModel.origin.y -
turretTankModel.turret_origin.y -
turretTrackModel.origin.y,
]}
>
{turretNodes.map((node) => {
const isCurrentTurret =
node.name ===
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
const isVisible = isCurrentTurret;
if (!isVisible) return null;
return jsxTree(node, {
mesh(_, props, key) {
return <mesh {...props} key={key} castShadow receiveShadow />;
},
});
})}
<group
position={[
turretTankModel.turret_origin.x +
turretTrackModel.origin.x +
turretModel.gun_origin.x -
gunTankModel.turret_origin.x -
gunTrackModel.origin.x -
gunTurretModel.gun_origin.x,
turretTankModel.turret_origin.z +
turretTrackModel.origin.z +
turretModel.gun_origin.z -
gunTankModel.turret_origin.z -
gunTrackModel.origin.z -
gunTurretModel.gun_origin.z,
turretTankModel.turret_origin.y +
turretTrackModel.origin.y +
turretModel.gun_origin.y -
gunTankModel.turret_origin.y -
gunTrackModel.origin.y -
gunTurretModel.gun_origin.y,
]}
>
{gunNodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModel.model_id.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun || isCurrentMantlet;
if (!isVisible) return null;
return jsxTree(node, {
mesh(_, props, key) {
return <mesh {...props} key={key} castShadow receiveShadow />;
},
});
})}
</group>
</group>
</group>
);
}
@@ -0,0 +1,25 @@
import { OrbitControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { Suspense } from 'react';
import { Model } from './components/Model';
export function MixerScene() {
return (
<Canvas
shadows
camera={{
fov: 45,
position: [5, 5, -5],
}}
>
<OrbitControls />
<directionalLight position={[10, 10, 10]} intensity={5} castShadow />
<directionalLight position={[-10, -10, -10]} intensity={3} castShadow />
<Suspense fallback={null}>
<Model />
</Suspense>
</Canvas>
);
}
@@ -307,7 +307,7 @@ export function FilterControl({ compact }: FilterControlProps) {
</Flex> </Flex>
<Flex <Flex
direction={{ initial: 'row', md: 'column' }} direction={compact ? 'row' : { initial: 'row', md: 'column' }}
align="start" align="start"
gap={{ initial: '2', md: '1' }} gap={{ initial: '2', md: '1' }}
> >
@@ -1,14 +1,6 @@
import { import { asset, isExplosive } from '@blitzkit/core';
asset,
coefficient,
isExplosive,
resolvePenetrationCoefficient,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n'; import { literals } from '@blitzkit/i18n';
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes'; import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes';
import { debounce } from 'lodash-es';
import { useEffect, useRef, useState } from 'react';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from '../../../../../../hooks/useLocale';
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition'; import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
import { Duel } from '../../../../../../stores/duel'; import { Duel } from '../../../../../../stores/duel';
@@ -28,34 +20,13 @@ export function Firepower({
}: StatsAcceptorProps & MaybeSkeletonComponentProps) { }: StatsAcceptorProps & MaybeSkeletonComponentProps) {
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const [penetrationDistance, setPenetrationDistance] = useState(250);
const setPenetrationDistanceDebounced = debounce((value: number) => {
setPenetrationDistance(value);
}, 500);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
const shell = Duel.use((state) => state.protagonist.shell); const shell = Duel.use((state) => state.protagonist.shell);
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
const hasSupercharger = useEquipment(107);
const hasCalibratedShells = useEquipment(103);
const penetrationCoefficient = coefficient([
hasCalibratedShells,
resolvePenetrationCoefficient(true, shell.type) - 1,
]);
const penetrationLossOverDistanceCoefficient = coefficient([
hasSupercharger,
-0.6,
]);
const tankModelDefinition = useTankModelDefinition(); const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id]; const gunModelDefinition = turretModelDefinition.guns[gun.id];
useEffect(() => {
if (penetrationDistanceInput.current) {
penetrationDistanceInput.current.value = `${penetrationDistance}`;
}
}, [penetrationDistance]);
return ( return (
<StatsTableWrapper> <StatsTableWrapper>
<Flex align="center" gap="4"> <Flex align="center" gap="4">
@@ -80,12 +80,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
relativeAgainst === TankopediaRelativeAgainst.All, relativeAgainst === TankopediaRelativeAgainst.All,
) )
.map((tank) => { .map((tank) => {
const thisTankModel = modelDefinitions.models[tank.id]; const member = tankToDuelMember(tank, provisionDefinitions);
const member = tankToDuelMember(
tank,
thisTankModel,
provisionDefinitions,
);
if (member.gun.shells[shellIndex] === undefined) { if (member.gun.shells[shellIndex] === undefined) {
return undefined; return undefined;
@@ -1,8 +1,10 @@
import { Box, Code } from '@radix-ui/themes'; import { DoubleArrowRightIcon } from '@radix-ui/react-icons';
import { Box, Button, Code } from '@radix-ui/themes';
import { times } from 'lodash-es'; import { times } from 'lodash-es';
import { Quicklime, type QuicklimeCallback } from 'quicklime'; import { Quicklime, type QuicklimeCallback } from 'quicklime';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Var } from '../../../../../../../core/radix/var'; import { Var } from '../../../../../../../core/radix/var';
import { useLocale } from '../../../../../../../hooks/useLocale';
import type { StatsAcceptorProps } from '../TraverseVisualizer'; import type { StatsAcceptorProps } from '../TraverseVisualizer';
import { VisualizerCard } from '../VisualizerCard'; import { VisualizerCard } from '../VisualizerCard';
import { Shell } from './components/Shell'; import { Shell } from './components/Shell';
@@ -19,6 +21,8 @@ export interface ReloadUpdateData {
export const reloadUpdate = new Quicklime<ReloadUpdateData>(); export const reloadUpdate = new Quicklime<ReloadUpdateData>();
export function ReloadVisualizer({ stats }: StatsAcceptorProps) { export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
const { strings } = useLocale();
const reloadCircle = useRef<HTMLDivElement>(null); const reloadCircle = useRef<HTMLDivElement>(null);
const reloadCore = useRef<HTMLDivElement>(null); const reloadCore = useRef<HTMLDivElement>(null);
const reloadGlow = useRef<HTMLDivElement>(null); const reloadGlow = useRef<HTMLDivElement>(null);
@@ -246,7 +250,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
<Code <Code
ref={progressTime} ref={progressTime}
size="1" size="2"
variant="ghost" variant="ghost"
weight="bold" weight="bold"
color="gray" color="gray"
@@ -255,14 +259,14 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
right: 'calc(50% + var(--space-8))', right: 'calc(50% + var(--space-8))',
transform: 'translateX(50%)', transform: 'translate(50%, -50%)',
}} }}
> >
0.00 0.00
</Code> </Code>
<Code <Code
ref={totalTime} ref={totalTime}
size="1" size="2"
variant="ghost" variant="ghost"
weight="bold" weight="bold"
color="gray" color="gray"
@@ -270,7 +274,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: 'calc(50% + var(--space-8))', left: 'calc(50% + var(--space-8))',
transform: 'translateX(-50%)', transform: 'translate(-50%, -50%)',
}} }}
> >
0.00 0.00
@@ -290,6 +294,25 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
width="100%" width="100%"
height="100%" height="100%"
/> />
<Button
size="1"
variant="outline"
color="gray"
highContrast
style={{
position: 'absolute',
top: '68%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
onClick={() => {
state.current = Math.floor(state.current + 1);
}}
>
{strings.website.tools.tankopedia.visualizers.reload.skip}
<DoubleArrowRightIcon />
</Button>
</VisualizerCard> </VisualizerCard>
); );
} }
@@ -21,7 +21,6 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
]); ]);
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) { export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
const mutateDuel = Duel.useMutation();
const crewSkills = TankopediaEphemeral.use((state) => state.skills); const crewSkills = TankopediaEphemeral.use((state) => state.skills);
const provisions = Duel.use((state) => state.protagonist.provisions); const provisions = Duel.use((state) => state.protagonist.provisions);
const consumables = Duel.use((state) => state.protagonist.consumables); const consumables = Duel.use((state) => state.protagonist.consumables);
@@ -14,7 +14,6 @@ import { Duel } from '../../../../../../../stores/duel';
import './index.css'; import './index.css';
export function QuickInputs() { export function QuickInputs() {
const mutateDuel = Duel.useMutation();
const protagonist = Duel.use((state) => state.protagonist); const protagonist = Duel.use((state) => state.protagonist);
const yawInput = useRef<HTMLInputElement>(null); const yawInput = useRef<HTMLInputElement>(null);
const pitchInput = useRef<HTMLInputElement>(null); const pitchInput = useRef<HTMLInputElement>(null);
@@ -12,7 +12,6 @@ import {
EnterFullScreenIcon, EnterFullScreenIcon,
ExitFullScreenIcon, ExitFullScreenIcon,
EyeOpenIcon, EyeOpenIcon,
GearIcon,
} from '@radix-ui/react-icons'; } from '@radix-ui/react-icons';
import { import {
Button, Button,
@@ -33,7 +32,6 @@ import { useEquipment } from '../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../hooks/useFullScreen'; import { useFullScreen } from '../../../../../hooks/useFullScreen';
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability'; import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
import { useLocale } from '../../../../../hooks/useLocale'; import { useLocale } from '../../../../../hooks/useLocale';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel'; import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
@@ -43,6 +41,7 @@ import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton'; import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
import { SmallTankIcon } from '../../../../SmallTankIcon'; import { SmallTankIcon } from '../../../../SmallTankIcon';
import { TankSearch } from '../../../../TankSearch'; import { TankSearch } from '../../../../TankSearch';
import { screenshotReadyEvent } from '../TankSandbox/components/SceneProps';
import { CustomShellButton } from './components/CustomShellButton'; import { CustomShellButton } from './components/CustomShellButton';
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher'; import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
import { QuickInputs } from './components/QuickInputs'; import { QuickInputs } from './components/QuickInputs';
@@ -59,20 +58,10 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
); );
const display = TankopediaEphemeral.use((state) => state.display); const display = TankopediaEphemeral.use((state) => state.display);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const showGrid = TankopediaPersistent.use((state) => state.showGrid);
const greenPenetration = TankopediaPersistent.use(
(state) => state.greenPenetration,
);
const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor,
);
const advancedHighlighting = TankopediaPersistent.use( const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting, (state) => state.advancedHighlighting,
); );
const wireframe = TankopediaPersistent.use((state) => state.wireframe);
const opaque = TankopediaPersistent.use((state) => state.opaque);
const fullScreenAvailable = useFullscreenAvailability(true); const fullScreenAvailable = useFullscreenAvailability(true);
const developerMode = App.use((state) => state.developerMode);
const protagonistTank = Duel.use((state) => state.protagonist.tank); const protagonistTank = Duel.use((state) => state.protagonist.tank);
const antagonistGun = Duel.use((state) => state.antagonist.gun); const antagonistGun = Duel.use((state) => state.antagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell); const antagonistShell = Duel.use((state) => state.antagonist.shell);
@@ -330,6 +319,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Text color="gray" size="2"> <Text color="gray" size="2">
{strings.website.tools.tankopedia.sandbox.dynamic.shooter} {strings.website.tools.tankopedia.sandbox.dynamic.shooter}
</Text> </Text>
<Dialog.Root <Dialog.Root
open={antagonistSelectorOpen} open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen} onOpenChange={setAntagonistSelectorOpen}
@@ -451,106 +441,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton variant="ghost" color="gray">
<GearIcon />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{display === TankopediaDisplay.DynamicArmor && (
<>
<DropdownMenu.Label>
{
strings.website.tools.tankopedia.sandbox.settings
.armor
}
</DropdownMenu.Label>
<DropdownMenu.CheckboxItem
checked={greenPenetration}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.greenPenetration = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.green_penetration
}
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={hideTankModelUnderArmor}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.hideTankModelUnderArmor = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.hide_model_under_armor
}
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={opaque}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.opaque = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.opaque
}
</DropdownMenu.CheckboxItem>
{developerMode && (
<DropdownMenu.CheckboxItem
checked={wireframe}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.wireframe = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.dev_wireframe
}
</DropdownMenu.CheckboxItem>
)}
</>
)}
<DropdownMenu.Label>
{
strings.website.tools.tankopedia.sandbox.settings
.environment
}
</DropdownMenu.Label>
<DropdownMenu.CheckboxItem
checked={showGrid}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.showGrid = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.show_grid
}
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Popover.Root> <Popover.Root>
<Popover.Trigger> <Popover.Trigger>
<IconButton color="gray" variant="ghost"> <IconButton color="gray" variant="ghost">
@@ -563,19 +453,25 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Popover.Close> <Popover.Close>
<Button <Button
onClick={() => { onClick={() => {
if (!canvas.current) return; screenshotReadyEvent.dispatch(true);
const anchor = document.createElement('a'); requestAnimationFrame(() => {
if (!canvas.current) return;
anchor.setAttribute( const anchor = document.createElement('a');
'download',
`${protagonistTank.name}.png`, anchor.setAttribute(
); 'download',
anchor.setAttribute( `${protagonistTank.name}.png`,
'href', );
canvas.current.toDataURL('image/png'), anchor.setAttribute(
); 'href',
anchor.click(); canvas.current.toDataURL('image/png'),
);
anchor.click();
screenshotReadyEvent.dispatch(false);
});
}} }}
> >
<DownloadIcon /> <DownloadIcon />
@@ -589,14 +485,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
if (!canvas.current) return; screenshotReadyEvent.dispatch(true);
canvas.current.toBlob((blob) => { requestAnimationFrame(() => {
if (!blob) return; if (!canvas.current) return;
navigator.clipboard.write([ canvas.current.toBlob((blob) => {
new ClipboardItem({ 'image/png': blob }), if (!blob) return;
]);
navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob }),
]);
});
screenshotReadyEvent.dispatch(false);
}); });
}} }}
> >
@@ -13,10 +13,10 @@ export function Tracker({ fontSize }: Props) {
<Box <Box
position="absolute" position="absolute"
left="50%" left="50%"
bottom="0" top="50%"
width="min(100%, 16rem)" width="min(100%, 16rem)"
style={{ style={{
transform: `translate(-50%, calc(${fontSize} / 2))`, transform: `translate(-50%, calc(${fontSize} / 2 + var(--space-4)))`,
}} }}
> >
<Progress color="gray" value={progress} /> <Progress color="gray" value={progress} />
@@ -1,92 +1,19 @@
import { TankType } from '@blitzkit/core';
import { Flex, Heading } from '@radix-ui/themes'; import { Flex, Heading } from '@radix-ui/themes';
import { Var } from '../../../../../../core/radix/var';
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from '../../../../../../hooks/useLocale';
import { Duel } from '../../../../../../stores/duel'; import { Duel } from '../../../../../../stores/duel';
import type { RadixColor } from '../../../../../../stores/embedState';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { Tracker } from './components/Tracker'; import { Tracker } from './components/Tracker';
interface TitleProps { export function Title() {
foreground?: boolean;
}
export const NATION_COLORS: Record<
string,
{
text: RadixColor;
background: RadixColor[];
tint: RadixColor;
}
> = {
france: {
text: 'mint',
tint: 'purple',
background: ['mint', 'cyan', 'jade'],
},
germany: {
text: 'sage',
tint: 'blue',
background: ['slate', 'sage', 'mauve'],
},
usa: {
text: 'gold',
tint: 'red',
background: ['brown', 'gold', 'gray'],
},
european: {
text: 'sky',
tint: 'violet',
background: ['cyan', 'blue', 'indigo'],
},
ussr: {
text: 'bronze',
tint: 'red',
background: ['bronze', 'tomato', 'bronze'],
},
uk: {
text: 'gold',
tint: 'red',
background: ['bronze', 'gold', 'brown'],
},
japan: {
text: 'mint',
tint: 'violet',
background: ['green', 'teal', 'jade'],
},
china: {
text: 'mint',
tint: 'pink',
background: ['green', 'teal', 'jade'],
},
other: {
text: 'amber',
tint: 'sky',
background: ['amber', 'yellow', 'gray'],
},
};
export function Title({ foreground }: TitleProps) {
const { unwrap } = useLocale(); const { unwrap } = useLocale();
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
const revealed = TankopediaEphemeral.use((state) => state.revealed); const revealed = TankopediaEphemeral.use((state) => state.revealed);
const nationColors = NATION_COLORS[protagonist.nation];
const color = Var(
disturbed
? protagonist.type === TankType.COLLECTOR
? 'blue-11'
: protagonist.type === TankType.PREMIUM
? 'amber-11'
: 'gray-12'
: `${nationColors.text}-12`,
);
const name = unwrap(protagonist.name); const name = unwrap(protagonist.name);
const fontSize = revealed const fontSize = revealed
? disturbed ? revealed
? '2rem' ? '2rem'
: `min(65vh, ${125 / name.length}vw)` : `min(16vh, ${125 / name.length}vw)`
: `min(48vh, ${75 / name.length}vw)`; : `min(12vh, ${75 / name.length}vw)`;
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
return ( return (
@@ -95,19 +22,19 @@ export function Title({ foreground }: TitleProps) {
event.preventDefault(); event.preventDefault();
mutateTankopediaEphemeral((draft) => { mutateTankopediaEphemeral((draft) => {
draft.disturbed = true; draft.revealed = true;
}); });
}} }}
direction="column" direction="column"
position="absolute" position="absolute"
align="center" align="center"
justify="center" justify="center"
top={disturbed ? '6rem' : '50%'} top={revealed ? '6rem' : '50%'}
left="50%" left="50%"
width="100%" width="100%"
height={disturbed ? fontSize : '100%'} height={revealed ? fontSize : '100%'}
style={{ style={{
pointerEvents: disturbed ? 'none' : undefined, pointerEvents: revealed ? 'none' : undefined,
transitionDuration: '1s', transitionDuration: '1s',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}} }}
@@ -119,11 +46,10 @@ export function Title({ foreground }: TitleProps) {
pointerEvents: 'none', pointerEvents: 'none',
fontSize, fontSize,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
color: foreground ? Var('gray-a10') : color, opacity: revealed ? 1 : 0.5,
opacity: foreground && disturbed ? 0 : 1, letterSpacing: revealed || !revealed ? 0 : '-0.03em',
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
transition: ` transition: `
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'}, letter-spacing 1.5s ${revealed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
font-size 1s, font-size 1s,
-webkit-text-stroke 2s, -webkit-text-stroke 2s,
opacity 1s opacity 1s
@@ -24,7 +24,6 @@ interface ControlsProps {
autoRotate?: boolean; autoRotate?: boolean;
zoomable?: boolean; zoomable?: boolean;
enableRotate?: boolean; enableRotate?: boolean;
distanceScale?: number;
} }
export function Controls({ export function Controls({
@@ -32,7 +31,6 @@ export function Controls({
autoRotate = true, autoRotate = true,
zoomable = true, zoomable = true,
enableRotate = true, enableRotate = true,
distanceScale = 1,
}: ControlsProps) { }: ControlsProps) {
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
@@ -84,9 +82,9 @@ export function Controls({
protagonistGunOrigin.y; protagonistGunOrigin.y;
const inspectModeInitialPosition = new Vector3( const inspectModeInitialPosition = new Vector3(
-8, -8,
gunHeight + (naked ? 10 : 2), naked ? gunHeight + 10 : 2,
-13, -13,
).multiplyScalar(distanceScale); );
const t0 = useRef(Date.now() / 1000); const t0 = useRef(Date.now() / 1000);
useFrame(() => { useFrame(() => {
@@ -111,14 +109,14 @@ export function Controls({
}, },
); );
async function handlePoseEvent(event: Pose) { function handlePoseEvent(event: Pose) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = await hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
@@ -240,7 +238,11 @@ export function Controls({
orbitControls.current.rotateSpeed = 0.25; orbitControls.current.rotateSpeed = 0.25;
camera.position.copy(inspectModeInitialPosition); camera.position.copy(inspectModeInitialPosition);
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0); orbitControls.current.target.set(
0,
gunHeight * (naked ? 1 / 4 : 3 / 4),
0,
);
orbitControls.current.enablePan = true; orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true; orbitControls.current.enableZoom = true;
camera.fov = naked ? 20 : 25; camera.fov = naked ? 20 : 25;
@@ -1,34 +0,0 @@
import { Environment } from '@react-three/drei';
import { Euler } from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
import { useModel } from '../../../../../../hooks/useModel';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
const LIGHTS_COUNT = 5;
interface LightingProps {
display: TankopediaDisplay;
}
export function Lighting({ display }: LightingProps) {
const protagonist = Duel.use((state) => state.protagonist);
const { hasPbr } = useModel(protagonist.tank.id);
const isBrighterLighting =
!hasPbr && display !== TankopediaDisplay.StaticArmor;
return (
<>
{display !== TankopediaDisplay.StaticArmor && (
<Environment
files="https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/winter_river_1k.hdr"
environmentIntensity={isBrighterLighting ? 1.5 : 1.25}
environmentRotation={new Euler(0, degToRad(180), 0)}
/>
)}
{display === TankopediaDisplay.StaticArmor && (
<Environment preset="lobby" />
)}
</>
);
}
@@ -0,0 +1,49 @@
import { useHelper } from '@react-three/drei';
import { times } from 'lodash-es';
import { useRef } from 'react';
import { Color, SpotLight, SpotLightHelper } from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
import { isHalloween } from '../../../../../../../core/blitzkit/isHalloween';
import { HelpingSpotLight } from '../../../../../../HelpingSpotLight';
const LIGHTS_COUNT = 5;
const LIGHT_DISTANCE = 13;
const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 2;
const INTENSITY = 30;
export function Lighting() {
const topLight = useRef<SpotLight>(null!);
const frontLight = useRef<SpotLight>(null!);
useHelper(topLight, SpotLightHelper, 'red');
useHelper(frontLight, SpotLightHelper, 'green');
return (
<>
{times(LIGHTS_COUNT, (index) => {
const x = index / (LIGHTS_COUNT - 1);
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152);
const position = [
LIGHT_DISTANCE * Math.sin(theta),
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x,
LIGHT_DISTANCE * Math.cos(theta),
] as const;
const color = new Color();
if (isHalloween()) color.set(1, 0.25, 0.25);
return (
<HelpingSpotLight
position={position}
intensity={INTENSITY}
penumbra={1}
castShadow
decay={1}
color={color}
/>
);
})}
</>
);
}
@@ -1,240 +1,50 @@
import { asset, I_HAT, J_HAT } from '@blitzkit/core'; import { invalidate, useFrame } from '@react-three/fiber';
import { useFrame, useLoader } from '@react-three/fiber';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { useRef } from 'react'; import { Quicklime, type QuicklimeEvent } from 'quicklime';
import { import { useEffect, useRef } from 'react';
Group, import { Mesh, MeshStandardMaterial } from 'three';
MeshStandardMaterial,
PerspectiveCamera,
TextureLoader,
Vector2,
Vector3,
} from 'three';
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
import { awaitableEquipmentDefinitions } from '../../../../../../../core/awaitables/equipmentDefinitions';
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../../../../../../core/awaitables/provisionDefinitions';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
import { tankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
import { Duel } from '../../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
import { aimTarget } from './constants';
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] = const SIZE = 2 ** 5;
await Promise.all([
awaitableModelDefinitions,
awaitableEquipmentDefinitions,
awaitableProvisionDefinitions,
]);
const emptyVector = new Vector2(); export const screenshotReadyEvent = new Quicklime(false);
export function SceneProps() { export function SceneProps() {
const disturbed = TankopediaEphemeral.use((state) => state.disturbed); const mesh = useRef<Mesh>(null!);
const targetCircleWrapper = useRef<Group>(null);
const clientTargetCircle = useRef<HTMLDivElement>(null);
const serverTargetCircle = useRef<HTMLDivElement>(null);
const material = useRef<MeshStandardMaterial>(null); const material = useRef<MeshStandardMaterial>(null);
const playground = useRef<Group>(null);
const turret = Duel.use((state) => state.protagonist.turret);
const track = Duel.use((state) => state.protagonist.track);
const tank = Duel.use((state) => state.protagonist.tank);
const gun = Duel.use((state) => state.protagonist.gun);
const shell = Duel.use((state) => state.protagonist.shell);
const engine = Duel.use((state) => state.protagonist.engine);
const tankModelDefinition = modelDefinitions.models[tank.id];
const trackModelDefinition =
modelDefinitions.models[tank.id].tracks[track.id];
const gunModelDefinition =
tankModelDefinition.turrets[turret.id].guns[gun.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const hullOrigin = new Vector3(
trackModelDefinition.origin.x,
trackModelDefinition.origin.y,
-trackModelDefinition.origin.z,
);
const turretOrigin = new Vector3(
tankModelDefinition.turret_origin.x,
tankModelDefinition.turret_origin.y,
-tankModelDefinition.turret_origin.z,
);
const gunOriginOnlyY = new Vector3(0, turretModelDefinition.gun_origin.y, 0);
const shellOrigin = hullOrigin.clone().add(turretOrigin).add(gunOriginOnlyY);
const camouflage = Duel.use((state) => state.protagonist.camouflage);
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
const consumables = Duel.use((state) => state.protagonist.consumables);
const provisions = Duel.use((state) => state.protagonist.provisions);
const assaultDistance = Duel.use(
(state) => state.protagonist.assaultDistance,
);
const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix,
);
const characteristics = tankCharacteristics(
{
tank,
turret,
gun,
shell,
consumables,
applyDynamicArmor: false,
applyReactiveArmor: false,
applySpallLiner: false,
camouflage,
crewSkills,
engine,
equipmentMatrix,
provisions,
stockEngine: tank.engines[0],
stockGun: tank.turrets[0].guns[0],
stockTurret: tank.turrets[0],
stockTrack: tank.tracks[0],
track,
assaultDistance,
},
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition },
);
let { dispersion } = characteristics;
const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed);
const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`));
const texture = useLoader(TextureLoader, '/assets/images/3d/grid.png');
const path = new Vector3();
const direction = new Vector3();
texture.anisotropy = 2;
useFrame(({ camera }) => { useFrame(({ camera }) => {
if (!material.current) return; if (!material.current) return;
material.current.opacity = clamp(0.5 * camera.position.y, 0, 1); material.current.opacity = clamp(0.5 * camera.position.y, 0, 1);
}); });
let lastTime = 0; useEffect(() => {
let DEBUG_disable = false; function handleScreenshotReady(event: QuicklimeEvent<boolean>) {
mesh.current.visible = !event.data;
useFrame(({ raycaster, camera, clock, gl }) => { invalidate();
if (
!playground.current ||
!targetCircleWrapper.current ||
DEBUG_disable ||
!clientTargetCircle.current ||
!serverTargetCircle.current
) {
return;
} }
raycaster.setFromCamera(emptyVector, camera); screenshotReadyEvent.on(handleScreenshotReady);
const cameraIntersections = raycaster.intersectObjects( return () => {
playground.current.children, screenshotReadyEvent.off(handleScreenshotReady);
true, };
); }, []);
let length: number;
if (cameraIntersections.length === 0) {
length = characteristics.shellRange;
direction.set(0, 0, -1).applyQuaternion(camera.quaternion);
aimTarget.copy(shellOrigin).addScaledVector(direction, length);
} else {
aimTarget.copy(cameraIntersections[0].point);
path.copy(aimTarget).sub(shellOrigin);
length = path.length();
direction.copy(path).normalize();
}
const deltaT = clock.elapsedTime - lastTime;
lastTime = clock.elapsedTime;
const desiredPitch = Math.asin(direction.y);
const desiredYaw = Math.atan2(-direction.x, -direction.z);
const deltaPitch = desiredPitch - modelTransformEvent.last!.pitch;
const deltaYaw = desiredYaw - modelTransformEvent.last!.yaw;
const deltaAngle = Math.sqrt(deltaPitch ** 2 + deltaYaw ** 2);
const maxPossibleRotationInTime = gunRotationSpeed * deltaT;
const clampedDeltaAngle = Math.min(maxPossibleRotationInTime, deltaAngle);
const clampedDeltaPitch =
deltaAngle === 0 ? 0 : deltaPitch * (clampedDeltaAngle / deltaAngle);
const clampedDeltaYaw =
deltaAngle === 0 ? 0 : deltaYaw * (clampedDeltaAngle / deltaAngle);
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch + clampedDeltaPitch,
modelTransformEvent.last!.yaw + clampedDeltaYaw,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
);
const actualDeltaPitch = pitch - modelTransformEvent.last!.pitch;
const actualDeltaYaw = yaw - modelTransformEvent.last!.yaw;
const deltaRotation = Math.abs(actualDeltaPitch) + Math.abs(actualDeltaYaw);
const rotationSpeed = deltaRotation / deltaT;
const turretTraversePenalty =
characteristics.dispersionTurretTraversing * radToDeg(rotationSpeed);
const penalty = Math.sqrt(1 + turretTraversePenalty ** 2);
if (penalty === 1) {
const dispersionMod = dispersion - characteristics.dispersion;
const dispersionModDischarged =
dispersionMod * Math.exp(-deltaT / characteristics.aimTime) -
dispersionMod;
dispersion += dispersionModDischarged;
} else {
dispersion = Math.max(characteristics.dispersion * penalty, dispersion);
}
modelTransformEvent.dispatch({ pitch, yaw });
const gunDirection = new Vector3(0, 0, -1)
.applyAxisAngle(I_HAT, pitch)
.applyAxisAngle(J_HAT, yaw);
raycaster.set(shellOrigin, gunDirection);
const gunIntersections = raycaster.intersectObjects(
playground.current.children,
true,
);
let gunTarget: Vector3;
if (gunIntersections.length === 0) {
gunTarget = shellOrigin
.clone()
.add(gunDirection.multiplyScalar(characteristics.shellRange));
} else {
gunTarget = gunIntersections[0].point;
}
targetCircleWrapper.current.position.copy(gunTarget);
const distanceToTarget = gunTarget.distanceTo(shellOrigin);
const dispersionRadius = dispersion * (distanceToTarget / 100);
const dispersionSize = 2 * dispersionRadius;
const targetDistanceFromCamera = gunTarget.distanceTo(camera.position);
const angularSize =
2 * Math.atan(dispersionSize / (2 * targetDistanceFromCamera));
const verticalScreenSize =
2 * Math.tan(degToRad((camera as PerspectiveCamera).fov) / 2);
const screenSpaceSize =
gl.domElement.clientHeight * (angularSize / verticalScreenSize);
const targetCircleDotsSize = `${screenSpaceSize}px`;
clientTargetCircle.current.style.width = targetCircleDotsSize;
clientTargetCircle.current.style.height = targetCircleDotsSize;
serverTargetCircle.current.style.width = targetCircleDotsSize;
serverTargetCircle.current.style.height = targetCircleDotsSize;
});
if (!disturbed) return null;
return ( return (
<mesh rotation={[-Math.PI / 2, 0, 0]}> <mesh
<planeGeometry args={[10, 10]} /> rotation={[-Math.PI / 2, 0, 0]}
<meshStandardMaterial ref={material} map={texture} transparent /> receiveShadow
visible={!screenshotReadyEvent.last!}
ref={mesh}
>
<planeGeometry args={[SIZE, SIZE]} />
<meshStandardMaterial
transparent
ref={material}
color="black"
roughness={1}
metalness={1}
/>
</mesh> </mesh>
); );
} }
@@ -14,7 +14,6 @@ import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper'; import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
export function TankModel() { export function TankModel() {
const mutateDuel = Duel.useMutation();
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
const protagonist = Duel.use((draft) => draft.protagonist); const protagonist = Duel.use((draft) => draft.protagonist);
const tankopediaEphemeralStore = TankopediaEphemeral.useStore(); const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
@@ -105,6 +104,7 @@ export function TankModel() {
key={key} key={key}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
castShadow castShadow
receiveShadow
/> />
); );
}, },
@@ -140,14 +140,14 @@ export function TankModel() {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
async function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = await hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
@@ -184,6 +184,7 @@ export function TankModel() {
key={key} key={key}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
castShadow castShadow
receiveShadow
/> />
); );
}, },
@@ -227,14 +228,14 @@ export function TankModel() {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
async function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
); );
const hasDownImprovedVerticalStabilizer = await hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix,
@@ -269,6 +270,7 @@ export function TankModel() {
key={key} key={key}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
castShadow castShadow
receiveShadow
/> />
); );
}, },
@@ -55,7 +55,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const hasDownImprovedVerticalStabilizer = useEquipment(124); const hasDownImprovedVerticalStabilizer = useEquipment(124);
const protagonistGun = Duel.use((state) => state.protagonist.gun); const protagonistGun = Duel.use((state) => state.protagonist.gun);
const protagonistTurret = Duel.use((state) => state.protagonist.turret); const protagonistTurret = Duel.use((state) => state.protagonist.turret);
const mutateDuel = Duel.useMutation();
const tankModelDefinition = useTankModelDefinition(); const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[protagonistTurret.id]; tankModelDefinition.turrets[protagonistTurret.id];
@@ -199,7 +198,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
localClippingEnabled: true, localClippingEnabled: true,
preserveDrawingBuffer: true, preserveDrawingBuffer: true,
}} }}
shadows shadows="soft"
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
onPointerMissed={() => { onPointerMissed={() => {
mutateTankopediaEphemeral((draft) => { mutateTankopediaEphemeral((draft) => {
@@ -232,7 +231,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
{display === TankopediaDisplay.StaticArmor && ( {display === TankopediaDisplay.StaticArmor && (
<StaticArmor thicknessRange={thicknessRange} /> <StaticArmor thicknessRange={thicknessRange} />
)} )}
<Lighting display={display} /> <Lighting />
</Suspense> </Suspense>
<InitialAligner /> <InitialAligner />
@@ -2,7 +2,6 @@ import { Box, Flex } from '@radix-ui/themes';
import { times } from 'lodash-es'; import { times } from 'lodash-es';
import { Suspense, useEffect, useMemo, useRef } from 'react'; import { Suspense, useEffect, useMemo, useRef } from 'react';
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
import { Var } from '../../../core/radix/var';
import { useFullScreen } from '../../../hooks/useFullScreen'; import { useFullScreen } from '../../../hooks/useFullScreen';
import { Duel } from '../../../stores/duel'; import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
@@ -10,7 +9,7 @@ import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonCo
import type { ThicknessRange } from '../../Armor/components/StaticArmor'; import type { ThicknessRange } from '../../Armor/components/StaticArmor';
import { Options } from './components/Options'; import { Options } from './components/Options';
import { TankSandbox } from './components/TankSandbox'; import { TankSandbox } from './components/TankSandbox';
import { NATION_COLORS, Title } from './components/TankSandbox/Title'; import { Title } from './components/TankSandbox/Title';
const tankDefinitions = await awaitableTankDefinitions; const tankDefinitions = await awaitableTankDefinitions;
@@ -40,7 +39,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const nationColors = NATION_COLORS[protagonist.nation];
useEffect(() => { useEffect(() => {
if (disturbed) { if (disturbed) {
@@ -85,9 +83,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
left="0" left="0"
style={{ style={{
opacity: disturbed ? 0 : 1, opacity: disturbed ? 0 : 1,
background: `linear-gradient(${nationColors.background background: 'black',
.map((color) => Var(`${color}-2`))
.join(',')})`,
transitionDuration: '1s', transitionDuration: '1s',
}} }}
/> />
@@ -101,8 +97,16 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
transitionDuration: '1s', transitionDuration: '1s',
background: isFullScreen ? 'black' : undefined, background: isFullScreen ? 'black' : undefined,
}} }}
height={!isFullScreen ? 'calc(100svh - 6rem)' : '100vh'} height={
maxHeight={isFullScreen ? undefined : '60rem'} isFullScreen
? '100vh'
: disturbed
? 'calc(100svh - 6rem)'
: {
initial: '28rem',
md: 'calc(100svh - 12rem)',
}
}
maxWidth={isFullScreen ? undefined : '120rem'} maxWidth={isFullScreen ? undefined : '120rem'}
flexGrow="1" flexGrow="1"
width={isFullScreen ? '100vw' : undefined} width={isFullScreen ? '100vw' : undefined}
@@ -118,8 +122,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
position="relative" position="relative"
> >
<Box position="absolute" width="100%" height="100%" overflow="hidden"> <Box position="absolute" width="100%" height="100%" overflow="hidden">
<Title />
<Box <Box
width="100%" width="100%"
height="100%" height="100%"
@@ -145,7 +147,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
</Box> </Box>
</Box> </Box>
<Title foreground /> <Title />
<Options <Options
skeleton={skeleton} skeleton={skeleton}
+8 -1
View File
@@ -127,11 +127,18 @@ export const guessTool: Tool = {
branches: ['main', 'dev', 'opentest', 'preview'], branches: ['main', 'dev', 'opentest', 'preview'],
}; };
export const mixerTool: Tool = {
id: 'mixer',
button: { color: 'red' },
branches: ['main', 'dev', 'opentest', 'preview'],
};
export const TOOLS: Tool[] = [ export const TOOLS: Tool[] = [
tankopediaTool, tankopediaTool,
compareTool, compareTool,
performanceTool, performanceTool,
// guessTool, guessTool,
// mixerTool,
// chartsTool, // chartsTool,
galleryTool, galleryTool,
sessionTool, sessionTool,
@@ -0,0 +1,4 @@
export function isHalloween() {
const data = new Date();
return data.getMonth() === 9 && data.getDate() === 31;
}
@@ -1,19 +1,14 @@
import type { import type { ProvisionDefinitions, TankDefinition } from '@blitzkit/core';
ModelDefinition,
ProvisionDefinitions,
TankDefinition,
} from '@blitzkit/core';
import { uniqueId } from 'lodash-es'; import { uniqueId } from 'lodash-es';
import type { CompareMember } from '../../stores/compareEphemeral'; import type { CompareMember } from '../../stores/compareEphemeral';
import { tankToDuelMember } from './tankToDuelMember'; import { tankToDuelMember } from './tankToDuelMember';
export function tankToCompareMember( export function tankToCompareMember(
tank: TankDefinition, tank: TankDefinition,
model: ModelDefinition,
provisionDefinitions: ProvisionDefinitions, provisionDefinitions: ProvisionDefinitions,
) { ) {
return { return {
...tankToDuelMember(tank, model, provisionDefinitions), ...tankToDuelMember(tank, provisionDefinitions),
key: uniqueId(), key: uniqueId(),
} satisfies CompareMember; } satisfies CompareMember;
} }
@@ -1,6 +1,5 @@
import { import {
createDefaultProvisions, createDefaultProvisions,
ModelDefinition,
type ProvisionDefinitions, type ProvisionDefinitions,
type TankDefinition, type TankDefinition,
} from '@blitzkit/core'; } from '@blitzkit/core';
@@ -9,7 +8,6 @@ import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
export function tankToDuelMember( export function tankToDuelMember(
tank: TankDefinition, tank: TankDefinition,
model: ModelDefinition,
provisionDefinitions: ProvisionDefinitions, provisionDefinitions: ProvisionDefinitions,
) { ) {
const turret = tank.turrets.at(-1)!; const turret = tank.turrets.at(-1)!;
@@ -15,7 +15,6 @@ import {
modelTransformEvent, modelTransformEvent,
type ModelTransformEventData, type ModelTransformEventData,
} from '../core/blitzkit/modelTransform'; } from '../core/blitzkit/modelTransform';
import { Duel } from '../stores/duel';
import { useTankModelDefinition } from './useTankModelDefinition'; import { useTankModelDefinition } from './useTankModelDefinition';
export function useTankTransform( export function useTankTransform(
@@ -25,7 +24,6 @@ export function useTankTransform(
gunContainer: RefObject<Group>, gunContainer: RefObject<Group>,
) { ) {
const tankModelDefinition = useTankModelDefinition(); const tankModelDefinition = useTankModelDefinition();
const duelStore = Duel.useStore();
useEffect(() => { useEffect(() => {
const trackModelDefinition = tankModelDefinition.tracks[track.id]; const trackModelDefinition = tankModelDefinition.tracks[track.id];
@@ -114,11 +114,7 @@ function Content() {
if (tank === undefined) return; if (tank === undefined) return;
const model = modelDefinitions.models[tank.id]; draft.members.push(tankToCompareMember(tank, provisionDefinitions));
draft.members.push(
tankToCompareMember(tank, model, provisionDefinitions),
);
}); });
}); });
}, []); }, []);
@@ -7,7 +7,6 @@ import { GuessRendererLoader } from '../../../components/GuessRendererLoader';
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from '../../../core/awaitables/provisionDefinitions';
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
import { Var } from '../../../core/radix/var';
import { import {
type LocaleAcceptorProps, type LocaleAcceptorProps,
LocaleProvider, LocaleProvider,
@@ -33,13 +32,13 @@ export function Page({ locale }: LocaleAcceptorProps) {
return ( return (
<GuessEphemeral.Provider data={initialTank}> <GuessEphemeral.Provider data={initialTank}>
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<Page2 /> <Container />
</LocaleProvider> </LocaleProvider>
</GuessEphemeral.Provider> </GuessEphemeral.Provider>
); );
} }
function Page2() { function Container() {
const tank = GuessEphemeral.use((state) => state.tank); const tank = GuessEphemeral.use((state) => state.tank);
const model = modelDefinitions.models[tank.id]; const model = modelDefinitions.models[tank.id];
@@ -57,65 +56,62 @@ function Content() {
const tank = GuessEphemeral.use((state) => state.tank); const tank = GuessEphemeral.use((state) => state.tank);
const guessState = GuessEphemeral.use((state) => state.guessState); const guessState = GuessEphemeral.use((state) => state.guessState);
const isRevealed = guessState !== null; const isRevealed = guessState !== null;
const name = unwrap(tank.name);
const fontSize = `min(48vh, ${55 / name.length}vw)`;
const transitionDuration = isRevealed ? '2s' : undefined;
return ( return (
<Flex flexGrow="1" position="relative" overflow="hidden"> <Flex flexGrow="1" position="relative" overflow="hidden">
<GuessBackground /> <GuessBackground />
<Flex <Heading
position="absolute"
left="50%"
top="50%"
ml="9"
style={{ style={{
transitionDuration,
opacity: isRevealed ? 1 : 0, opacity: isRevealed ? 1 : 0,
transitionDuration: isRevealed ? '2s' : undefined, fontWeight: 900,
transform: 'translateY(-50%)', position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
fontSize,
userSelect: 'none',
whiteSpace: 'nowrap',
pointerEvents: 'none',
}} }}
> >
<Heading weight="medium" size="9"> {name}
{unwrap(tank.name)} </Heading>
</Heading>
</Flex>
<Box <Box
position="absolute" position="absolute"
top="0"
left="0"
width="100%" width="100%"
height="100%" height="100%"
top="0" style={{ transitionDuration }}
right={isRevealed ? '16rem' : '0'}
style={{
transitionDuration: isRevealed ? '2s' : undefined,
}}
> >
<Suspense fallback={<GuessRendererLoader />}> <Suspense fallback={<GuessRendererLoader />}>
<GuessRenderer /> <GuessRenderer />
</Suspense> </Suspense>
</Box> </Box>
<Flex <Heading
position="absolute"
left="50%"
top="50%"
ml="9"
style={{ style={{
opacity: isRevealed ? 1 : 0, transitionDuration,
transitionDuration: isRevealed ? '2s' : undefined, opacity: isRevealed ? 0.5 : 0,
transform: 'translateY(-50%)', fontWeight: 900,
color: 'transparent', position: 'absolute',
WebkitTextStroke: `1px ${Var('gray-12')}`, left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
fontSize,
userSelect: 'none',
whiteSpace: 'nowrap',
pointerEvents: 'none',
}} }}
> >
<Heading {name}
style={{ </Heading>
WebkitTextStroke: `1px inherit`,
}}
weight="medium"
size="9"
>
{unwrap(tank.name)}
</Heading>
</Flex>
<Guesser /> <Guesser />
</Flex> </Flex>
@@ -7,7 +7,6 @@ import { Box, Flex, Heading, Link } from '@radix-ui/themes';
import type { GetStaticPaths } from 'astro'; import type { GetStaticPaths } from 'astro';
import { AesonPlug } from '../../components/AesonPlug'; import { AesonPlug } from '../../components/AesonPlug';
import HomeHero from '../../components/HomeHero.astro'; import HomeHero from '../../components/HomeHero.astro';
import { HomePageHotTanks } from '../../components/HomePageHotTanks';
import { Plugs } from '../../components/Plugs'; import { Plugs } from '../../components/Plugs';
import ToolCard from '../../components/ToolCard.astro'; import ToolCard from '../../components/ToolCard.astro';
import { import {
@@ -15,6 +14,7 @@ import {
discordTool, discordTool,
embedTool, embedTool,
galleryTool, galleryTool,
guessTool,
moreTool, moreTool,
performanceTool, performanceTool,
sessionTool, sessionTool,
@@ -25,7 +25,6 @@ import { getStrings } from '../../core/i18n/getStrings';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_BRANCH); const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_BRANCH);
const MODE = assertSecret(import.meta.env.MODE);
const locale = Astro.params.locale ?? DEFAULT_LOCALE; const locale = Astro.params.locale ?? DEFAULT_LOCALE;
const strings = getStrings(Astro.params.locale); const strings = getStrings(Astro.params.locale);
@@ -110,22 +109,22 @@ export const getStaticPaths = (() => {
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}> <Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<!-- <ToolCard tool={chartsTool} /> --> <!-- <ToolCard tool={chartsTool} /> -->
<!-- <ToolCard tool={guessTool} /> --> <ToolCard tool={guessTool} />
<ToolCard tool={galleryTool} /> <ToolCard tool={galleryTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard tool={sessionTool} /> <ToolCard tool={sessionTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard tool={discordTool} /> <ToolCard tool={discordTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard tool={tierListTool} /> <ToolCard tool={tierListTool} />
</Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard tool={embedTool} /> <ToolCard tool={embedTool} />
<ToolCard tool={moreTool} />
</Flex> </Flex>
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}> <Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
<ToolCard tool={moreTool} />
<Box flexGrow="1" /> <Box flexGrow="1" />
</Flex> </Flex>
@@ -0,0 +1,64 @@
import { Box } from '@radix-ui/themes';
import { useRef } from 'react';
import { MixerScene } from '../../../components/MixerScene';
import { PageWrapper } from '../../../components/PageWrapper';
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
import {
LocaleProvider,
type LocaleAcceptorProps,
} from '../../../hooks/useLocale';
import { MixerEphemeral } from '../../../stores/mixerEphemeral';
const tankDefinitions = await awaitableTankDefinitions;
const tanks = Object.values(tankDefinitions.tanks);
export function Page({ locale }: LocaleAcceptorProps) {
const hull = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
const turret = useRef(
turretTank.current.turrets[
Math.floor(Math.random() * turretTank.current.turrets.length)
],
);
const gunTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
const gunTurret = useRef(
gunTank.current.turrets[
Math.floor(Math.random() * gunTank.current.turrets.length)
],
);
const gun = useRef(
gunTurret.current.guns[
Math.floor(Math.random() * gunTurret.current.guns.length)
],
);
return (
<LocaleProvider locale={locale}>
<MixerEphemeral.Provider
data={{
hull: hull.current,
turret: { tank: turretTank.current, turret: turret.current },
gun: {
tank: gunTank.current,
turret: gunTurret.current,
gun: gun.current,
},
}}
>
<Content />
</MixerEphemeral.Provider>
</LocaleProvider>
);
}
function Content() {
return (
<PageWrapper p="0">
<Box flexGrow="1" position="relative">
<Box position="absolute" width="100%" height="100%" top="0" left="0">
<MixerScene />
</Box>
</Box>
</PageWrapper>
);
}
@@ -0,0 +1,15 @@
---
import { PageWrapper } from '../../../components/PageWrapper';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Page } from './_index';
export { getStaticPaths } from '../index.astro';
const { locale } = Astro.params;
---
<BaseLayout>
<PageWrapper p="0" maxWidth="unset">
<Page {locale} client:load />
</PageWrapper>
</BaseLayout>
+1 -1
View File
@@ -55,7 +55,7 @@ export const Duel = createContextualStore(
model: ModelDefinition; model: ModelDefinition;
provisionDefinitions: ProvisionDefinitions; provisionDefinitions: ProvisionDefinitions;
}) => { }) => {
const protagonist = tankToDuelMember(tank, model, provisionDefinitions); const protagonist = tankToDuelMember(tank, provisionDefinitions);
return create<DuelStore>()( return create<DuelStore>()(
subscribeWithSelector<DuelStore>(() => ({ subscribeWithSelector<DuelStore>(() => ({
@@ -0,0 +1,24 @@
import type {
GunDefinition,
TankDefinition,
TurretDefinition,
} from '@blitzkit/core';
import { create } from 'zustand';
import { createContextualStore } from '../core/zustand/createContextualStore';
export interface MixerEphemeral {
hull: TankDefinition;
turret: {
tank: TankDefinition;
turret: TurretDefinition;
};
gun: {
tank: TankDefinition;
turret: TurretDefinition;
gun: GunDefinition;
};
}
export const MixerEphemeral = createContextualStore((data: MixerEphemeral) =>
create<MixerEphemeral>()(() => data),
);
@@ -12,7 +12,6 @@ export interface TankopediaPersistentStore {
wireframe: boolean; wireframe: boolean;
opaque: boolean; opaque: boolean;
advancedHighlighting: boolean; advancedHighlighting: boolean;
showGrid: boolean;
greenPenetration: boolean; greenPenetration: boolean;
showSpacedArmor: boolean; showSpacedArmor: boolean;
showExternalModules: boolean; showExternalModules: boolean;
@@ -34,7 +33,6 @@ export const TankopediaPersistent = createContextualStore(() =>
wireframe: false, wireframe: false,
opaque: false, opaque: false,
advancedHighlighting: false, advancedHighlighting: false,
showGrid: true,
greenPenetration: false, greenPenetration: false,
showSpacedArmor: true, showSpacedArmor: true,
showExternalModules: true, showExternalModules: true,