mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-12 20:26:10 +01:00
Compare commits
26
Commits
v1.27.17
...
50eac9eba6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50eac9eba6 | ||
|
|
97fc386bc7 | ||
|
|
abfb3f8ddb | ||
|
|
9cb5113f6e | ||
|
|
7dd1c0c32a | ||
|
|
03233f3fdc | ||
|
|
377159c298 | ||
|
|
d8431fb620 | ||
|
|
0546ddefbd | ||
|
|
26f6889012 | ||
|
|
3ad6088565 | ||
|
|
b6fd568cc6 | ||
|
|
3861b07aef | ||
|
|
2bf57e2c72 | ||
|
|
744c6654cf | ||
|
|
b82e905757 | ||
|
|
c42e5b7dc3 | ||
|
|
b716218ecd | ||
|
|
511346e4bb | ||
|
|
5c2e49453e | ||
|
|
5d397de766 | ||
|
|
aa763f65d1 | ||
|
|
94cbd95509 | ||
|
|
f3762e356f | ||
|
|
5747d7549f | ||
|
|
aea4905d20 |
No files matched your search
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "保存する",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "Предпросмотр",
|
||||||
|
|||||||
@@ -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": "Завантажити",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+7
-13
@@ -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,
|
||||||
|
|||||||
+2
-2
@@ -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 && (
|
||||||
|
|||||||
+5
-6
@@ -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={
|
||||||
|
|||||||
+4
-6
@@ -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,
|
||||||
|
|||||||
+8
-10
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
-30
@@ -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">
|
||||||
|
|||||||
+1
-6
@@ -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;
|
||||||
|
|||||||
+28
-5
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
-1
@@ -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);
|
||||||
|
|||||||
-1
@@ -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);
|
||||||
|
|||||||
+31
-129
@@ -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);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-2
@@ -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} />
|
||||||
|
|||||||
+11
-85
@@ -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
|
||||||
|
|||||||
+10
-8
@@ -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;
|
||||||
|
|||||||
-34
@@ -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" />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+49
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+30
-220
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+9
-7
@@ -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
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-3
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user