Compare commits

...
118 Commits
Author SHA1 Message Date
TrèsAbhi 907e71ce03 feat: turret rotation 2025-09-30 16:48:37 -05:00
TrèsAbhi 10942879a6 feat: start of rotation 2025-09-30 16:34:08 -05:00
TrèsAbhi d60551d30a fix: change blitzkit icons to big icons 2025-09-29 13:41:52 -05:00
TrèsAbhi b8c8528e1c Merge branch 'main' into dev 2025-09-29 13:39:13 -05:00
TrèsAbhi 0c9643016a feat: logs 2025-09-29 13:37:00 -05:00
TrèsAbhi 61d1ebbad6 fix: redirect 2025-09-29 13:35:47 -05:00
TrèsAbhi e9b7059cc0 feat: some linking 2025-09-29 13:30:10 -05:00
TrèsAbhi a359897a2a New Crowdin updates (#171) 2025-09-29 13:29:58 -05:00
TrèsAbhi e5f7d72df3 feat: rearrange 2025-09-28 14:11:33 -05:00
TrèsAbhi 57092d8df0 feat: env vars 2025-09-28 14:10:34 -05:00
TrèsAbhi 86508d5413 feat: submodules and packages 2025-09-28 13:44:15 -05:00
TrèsAbhi 33373c286f feat: logs 2025-09-28 13:13:08 -05:00
TrèsAbhi 7443f75347 fix: less aggressive light manager 2025-09-28 13:12:16 -05:00
TrèsAbhi d09f934b80 feat: start app 2025-09-28 13:09:03 -05:00
TrèsAbhi 7f919ebb87 feat: remove pwa 2025-09-28 13:02:48 -05:00
TrèsAbhi 0929825555 fix: glob and cache 2025-09-27 19:13:29 -05:00
TrèsAbhi 4de0fc5ec1 feat: concurrency 2025-09-27 18:37:51 -05:00
TrèsAbhi e66610f86e fix: side and out 2025-09-27 18:32:18 -05:00
TrèsAbhi 674abc4923 fix: dead code 2025-09-27 18:28:35 -05:00
TrèsAbhi 25cee8a36b fix: icons 2025-09-27 18:26:37 -05:00
TrèsAbhi dcbac5e017 fix: missing package 2025-09-27 18:16:05 -05:00
TrèsAbhi 0c94d42f3f fix: missing package 2025-09-27 18:15:00 -05:00
TrèsAbhi 7ab5e94323 fix: missing package 2025-09-27 18:13:59 -05:00
TrèsAbhi f9ea5ba74a fix: missing package 2025-09-27 18:13:00 -05:00
TrèsAbhi 1e7162d2da fix: missing package 2025-09-27 18:11:49 -05:00
TrèsAbhi cfba53a655 fix: missing package 2025-09-27 18:10:20 -05:00
TrèsAbhi 77a85641f0 fix: missing package 2025-09-27 18:09:10 -05:00
TrèsAbhi 5330a27e96 feat: logs 2025-09-27 18:07:43 -05:00
TrèsAbhi a791aff4a0 Merge branch 'main' into dev 2025-09-27 18:03:37 -05:00
TrèsAbhi df8a5967f0 feat: start of logs 2025-09-27 18:03:20 -05:00
TrèsAbhi 6261dfc9cd fix: more linting 2025-09-27 18:01:20 -05:00
TrèsAbhi 766801826c fix: canvases 2025-09-27 17:51:29 -05:00
TrèsAbhi 7b728596ca feat: storeDeferred 2025-09-27 17:32:20 -05:00
TrèsAbhi 0bf6ac33a5 feat: more promotion 2025-09-27 17:29:12 -05:00
TrèsAbhi 9a33b84b1c feat: install button 2025-09-27 17:05:06 -05:00
TrèsAbhi c14efe86cc fix: update errors 2025-09-27 16:07:45 -05:00
TrèsAbhi 4ff9a2ec7f feat: more pwa entries 2025-09-27 15:51:46 -05:00
TrèsAbhi 75d7b4554b feat: start of pew 2025-09-27 15:38:37 -05:00
TrèsAbhi 27c8b348bb feat: favicons 2025-09-27 13:39:08 -05:00
TrèsAbhi a55ab5e7ee feat: redo base setup 2025-09-27 13:34:11 -05:00
TrèsAbhi 12e90cc6ee feat: update dependencies 2025-09-26 14:51:49 -05:00
TrèsAbhi 01755a2a64 feat: start adding pwa 2025-09-26 14:46:07 -05:00
TrèsAbhi 080f0a0eb7 fix: make tankopedia hero section a bit smaller 2025-09-22 13:52:31 -05:00
TrèsAbhi a646cc1e57 feat: mixer buttons 2025-09-22 13:50:46 -05:00
TrèsAbhi d107813c37 feat: update closed 2025-09-22 13:27:38 -05:00
TrèsAbhi 27d3bbf346 feat: update closed 2025-09-22 13:20:47 -05:00
TrèsAbhi 675a613a0e New translations en.json (Russian) (#170) 2025-09-22 10:06:08 -05:00
TrèsAbhi 50c77e0ae1 fix: seo 2025-09-18 10:43:38 -05:00
TrèsAbhi 6e0429332d feat: mixer tank selector 2025-09-18 10:41:42 -05:00
TrèsAbhi 7afce6cbcb fix: keys 2025-09-18 10:19:41 -05:00
TrèsAbhi 17c7a5c7f5 feat: start of selector 2025-09-13 23:57:10 -05:00
TrèsAbhi f5aa2ff9de feat: reenable mixer 2025-09-13 23:42:26 -05:00
TrèsAbhi 96a8f225fc feat: start on api 2025-09-13 23:07:58 -05:00
TrèsAbhi d15a5aee69 fix: no overflow navbar on certain devices 2025-09-13 22:54:45 -05:00
TrèsAbhi 84dc5e4960 feat: data-driven tools 2025-09-13 22:53:29 -05:00
TrèsAbhi 58b5ee7f83 feat: data-driven tools 2025-09-13 21:54:43 -05:00
TrèsAbhi 8a99d34af9 fix: check intialization this mount 2025-09-13 21:33:16 -05:00
TrèsAbhi 7cd7bd83dc Merge branch 'main' into dev 2025-09-13 21:28:58 -05:00
TrèsAbhi c29d36a4b7 feat: update to main 2025-09-13 10:03:22 -05:00
TrèsAbhi 7d3b6221d7 feat: remake decodeAny 2025-09-13 10:02:46 -05:00
TrèsAbhi fd66f9ab17 feat: compiler based client 2025-09-12 16:49:26 -05:00
TrèsAbhi 6fbb0e81cb simplify error handling 2025-09-12 14:08:53 -05:00
TrèsAbhi 6dc82588c1 feat: cleanup 2025-09-12 13:42:33 -05:00
TrèsAbhi c6eae9ad51 feat: update closed 2025-09-12 13:29:10 -05:00
TrèsAbhi ebdd7629ff update 2025-09-12 13:02:43 -05:00
TrèsAbhi 646bccf370 feat: logs 2025-09-07 16:38:07 -05:00
TrèsAbhi 86206f2d89 Merge branch 'main' into dev 2025-09-07 16:29:28 -05:00
TrèsAbhi bc0dd87e31 Merge branch 'main' into dev 2025-09-07 16:29:09 -05:00
TrèsAbhi e26bb4efe0 feat: some changes to lighting... again 2025-09-06 12:40:35 -05:00
TrèsAbhi 66cf1fd223 feat: baked ao 2025-09-06 00:48:39 -05:00
TrèsAbhi 335cd4d699 feat: rewrite some systems 2025-09-05 23:34:59 -05:00
TrèsAbhi 95fa5bc1a5 fix: use base color map 2025-09-05 20:45:04 -05:00
TrèsAbhi 50eac9eba6 New translations en.json (French) (#169) 2025-09-04 08:04:17 -05:00
TrèsAbhi 1598ae973f feat: auto framerate 2025-09-02 22:44:46 -05:00
TrèsAbhi 69f3a95379 feat: use armor model 2025-09-02 22:16:48 -05:00
TrèsAbhi 3846cbab4a fix: save persistent data 2025-09-02 16:31:11 -05:00
TrèsAbhi 30e87743fc feat: upgrade rest of them 2025-09-02 16:24:13 -05:00
TrèsAbhi fe4afdcbf0 feat: upgrade CompareEphemeral 2025-09-01 23:05:22 -05:00
TrèsAbhi ef2a47e756 feat: upgrade ComparePersistent 2025-09-01 23:02:20 -05:00
TrèsAbhi c2d625e1ed feat: upgrade EmbedBreakdown 2025-09-01 23:00:44 -05:00
TrèsAbhi d33990bdb4 feat: upgrade Gallery 2025-09-01 22:58:26 -05:00
TrèsAbhi 96482d4268 feat: renaming 2025-09-01 22:56:39 -05:00
TrèsAbhi 8b527f2333 fix: last and next checking 2025-09-01 22:55:31 -05:00
TrèsAbhi f56c31fb48 feat: upgrade Guess 2025-09-01 19:22:38 -05:00
TrèsAbhi 7d164e415a feat: upgrade Mixer 2025-09-01 19:19:44 -05:00
TrèsAbhi b3a9f0889e feat: upgrade Session 2025-09-01 19:16:36 -05:00
TrèsAbhi 6d24ed92d0 feat: upgrade TankFilters 2025-09-01 19:13:08 -05:00
TrèsAbhi d287e66265 feat: upgrade tankopediaEphemeral 2025-09-01 19:01:01 -05:00
TrèsAbhi ac29d1ead0 feat: upgrade tankopediaEphemeral 2025-09-01 18:51:04 -05:00
TrèsAbhi b6c3fdc4e9 feat: upgrade performance 2025-09-01 18:16:41 -05:00
TrèsAbhi 4efdaaf7c9 feat: upgrade TankSort 2025-09-01 18:13:41 -05:00
TrèsAbhi 87b6a36520 feat: use custom state manager 2025-09-01 18:03:07 -05:00
TrèsAbhi cf38cb3a6e feat: optimize legacy redirect pages 2025-09-01 17:27:42 -05:00
TrèsAbhi 97fc386bc7 New Crowdin updates (#168)
* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

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

No files matched your search

+13
View File
@@ -22,6 +22,19 @@ env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
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:
host:
+27 -2
View File
@@ -24,7 +24,32 @@ public/sitemap*
target
# packages/core/src/protos/*.ts
/.vercel
/.astro
.astro
.vercel
/src
/packages/bot/src/prisma
/packages/bot/src/prisma
dev-dist
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-7
View File
@@ -1,7 +0,0 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 80
}
+8 -7
View File
@@ -21,6 +21,7 @@
"AYUV",
"battleresults",
"bbox",
"beforeinstallprompt",
"bepaint",
"betakrieg",
"BINORMAL",
@@ -76,7 +77,9 @@
"echarts",
"Everytime",
"FASTNAME",
"favicons",
"flexdirection",
"fontsource",
"FOURCC",
"frameloop",
"fsformat",
@@ -154,6 +157,7 @@
"Pyogenics",
"ratingleaderboard",
"raycaster",
"Redirector",
"researchable",
"Resvg",
"rizz",
@@ -167,6 +171,7 @@
"searchtanks",
"SHAREDEXP",
"SKLLD",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
@@ -186,6 +191,7 @@
"tankopedia",
"tankopedias",
"tankstats",
"tauri",
"texconv",
"TEXCOORD",
"tresabhi",
@@ -222,12 +228,7 @@
"yolo",
"youtuber",
"youtubers",
"zoomable",
"zustand"
"zoomable"
],
"deno.enable": false,
"deno.unstable": false,
"cSpell.enableFiletypes": ["glsl", "proto3"],
"typescript.tsdk": "node_modules\\typescript\\lib",
"liveServer.settings.multiRootWorkspaceName": "blitzkit"
"typescript.tsdk": "node_modules\\typescript\\lib"
}
+41 -7
View File
@@ -1,13 +1,47 @@
## BlitzKit
# BlitzKit
Your premier source for all things Blitz.
Tools for everything and anything World of Tanks Blitz.
### https://blitzkit.app/
This GitHub repository contains the code for BlitzKit and all its services. You may need to checkout sensitive closed-source submodules in order to locally develop some aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or talk about sensitive content like environment variables or submodules.
![BlitzKit website](https://i.imgur.com/KaxUxmt.png)
## Packages
## Invite the bot
| Package | Title | Source | Memo |
| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| [packages/app](packages/app) | BlitzKit | [Play Store](https://play.google.com/store/apps/details?id=com.tresabhi.blitzkit), [App Store](https://apps.apple.com/app/4321), [Microsoft Store](https://apps.microsoft.com/detail/zyxw) | The native app of BlitzKit for all platforms. Unavailable from stores at this time. |
| [packages/bot](packages/bot) | BlitzKit Discord Bot | [Discord Application Directory](https://discord.com/discovery/applications/1097673957865443370) | The one-in-all Discord bot for statistics. |
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website providing a wide range of tools, APIs, and interconnecting all other services. |
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
## Submodules
![BlitzKit application directory](https://i.imgur.com/YWCtOuJ.png)
| Submodule | Title | Source | Memo |
| ------------------------------------------------------------ | ----------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [submodules/blitzkit-closed](submodules/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. |
| [submodules/blitzkit-internal](submodules/blitzkit-internal) | InternalKit | [blitzkit/internal](https://github.com/blitzkit/internal) | Closed source, sensitive internal documentation hidden from the public to prevent bad actors from abusing the game. Access to this submodule is **not negotiable**. |
| [submodules/CUE4Parse](submodules/CUE4Parse) | CUE4Parse | [FabianFG/CUE4Parse](https://github.com/FabianFG/CUE4Parse) | An Unreal Engine archives and packages parsing library in C#. |
## Environment Variables
| Name | Source | BlitzKit Default | Memo |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating wether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
| `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. |
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
| `PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
| `PUBLIC_BRANCH` | | `main` | The branch that the website is operating on. This is completely nominal. |
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
| `SERVICING` | | `false` | Wether the website is down for servicing. |
| `DISCORD_CLIENT_ID` | [Discord Developer Portal](https://discord.com/developers/applications) | `1097673957865443370` | The BlitzKit Discord Bot's user ID. |
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
| `DISCORD_TOKEN` | [Discord Developer Portal](https://discord.com/developers/applications) | | The Discord API Key for the BlitzKit Discord Bot. |
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
| `WOTB_DLC_CDN` | | | |
+2984
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.27.18
## Features
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
- Switched to a new renderer for better lighting for the Tankopedia
## Fixes
- The loading bar for the Tankopedia sandbox now shows up again
- Removed a lot of dead code, improving performance
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.27.19
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
## Features
- The camera is now lower down in the Tankopedia, giving the tank a more heroic feel
- Tanks are better framed across all devices, minimizing whitespace
- Added a skip button for the reload visualizer to skip to the next event
## Fixes
- Removed the useless "show grid" setting
- Taking screenshots now no longer includes the scene props like the floor
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
- The shot display card is now properly aligned as a proper table again
- Tank modules are once again considered in the penetration tester
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
+13
View File
@@ -0,0 +1,13 @@
# BlitzKit 1.27.20
It's all technical updates! The HD tanks will be rolled out over the span of few days.
## Technical Changes
- Shadows are disabled if your framerate drops below 30fps
- Pages that redirect to other pages (like the old /tools/tankopedia) now load a few hundred times faster
- Replaced Zustand with a custom state manager called Varuna with the goal to rewrite a significant portion of the website to not require any React for rendering increasing performance
- The transition wireframe in the Tankopedia sandbox now uses a lower quality model for performance reasons
- Tanks with PBR now use the correct maximum resolution textures
- This is a significant update as you can now literally see individual splats of dust on tanks and scratches haha
- Baked the ambient occlusion maps into the textures, hence reducing the overall model load times even with higher resolution textures
+16
View File
@@ -0,0 +1,16 @@
# BlitzKit 1.28.0
BlitzKit, as an app and the new Mixer tool!
## Features
- Created the BlitzKit app that can be downloaded on most devices
- Re-did the BlitzKit logo to be a bit warmer
- Added the Mixer tool which lets you combine tank models into Frankensteins
- Updated all dependencies to the latest version for a decent performance boost
## Fixes
- BlitzKit no longer forgets persistent data if the app crashes without properly closing
- The tools list in the navbar no longer overflows into the neighboring elements on certain devices
- Added keying to a few places, speeding up re-rendering
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.28.1
## Fixes
- The lights no longer crash the Mixer tool
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.28.2
## Fixes
- BlitzKit no longer tries to redirect translated pages to the same page, over and over again
+15 -9
View File
@@ -1,7 +1,7 @@
{
"name": "blitzkit",
"private": true,
"version": "1.27.17",
"version": "1.28.2",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
@@ -16,11 +16,14 @@
"build:prisma": "prisma generate",
"build:sitemap": "bun scripts/sitemap.ts",
"build:docs": "vitepress build docs",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"build:app": "cd packages/app && bun run build",
"dev:bot": "bun run build:bot --dev && bun start:bot",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website && bun run dev",
"dev:app": "cd packages/app && bun run dev",
"dev": "bun run dev:website",
"dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
@@ -41,19 +44,22 @@
},
"license": "MIT",
"dependencies": {
"@types/xmpp__client": "^0.13.3",
"@xmpp/client": "^0.13.6",
"astro-favicons": "^3.1.5",
"cheerio": "^1.1.2",
"googleapis": "^144.0.0",
"googleapis": "^160.0.0",
"lodash-es": "^4.17.21",
"protobufjs": "^7.5.2",
"protobufjs": "^7.5.4",
"quicklime": "^1.2.0",
"sharp": "^0.33.5"
"sharp": "^0.34.4"
},
"devDependencies": {
"@types/bun": "latest",
"@types/bun": "^1.2.22",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"prettier": "^3.5.3",
"prisma": "^6.6.0",
"typescript": "^5.8.3"
"@types/node": "^24.5.2",
"prettier": "^3.6.2",
"prisma": "^6.16.2",
"typescript": "^5.9.2"
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@blitzkit/app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/cli": "^2",
"vite": "^6.0.3",
"typescript": "~5.6.2"
}
}
+7
View File
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
+5286
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "blitzkit"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "blitzkit_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
+14
View File
@@ -0,0 +1,14 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+27
View File
@@ -0,0 +1,27 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Command, Stdio};
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
let entry = app_handle
.path_resolver()
.resolve_resource("../../dist/website/server/entry.mjs")
.expect("failed to resolve server.ts");
Command::new("bun")
.arg("run")
.arg(entry)
.stderr(Stdio::null())
.spawn()
.expect("failed to start Bun server");
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running Tauri app");
}
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "blitzkit",
"version": "0.1.0",
"identifier": "com.tresabhi.blitzkit",
"build": {
"beforeDevCommand": "cd ../website && bun run dev",
"_beforeBuildCommand": "cd ../website && bun run build",
"frontendDist": "../../../dist/website/client",
"devUrl": "http://localhost:4321"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"url": "http://localhost:4321",
"title": "BlitzKit",
"fullscreen": false,
"resizable": true,
"maximized": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"extends": ["../../tsconfig.json"],
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
+5 -5
View File
@@ -6,14 +6,14 @@
"dependencies": {
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"satori": "^0.10.14",
"satori": "^0.18.3",
"@resvg/resvg-js": "^2.6.2",
"discord.js": "^14.16.3",
"react": "^18.3.1",
"discord.js": "^14.22.1",
"react": "^19.1.1",
"@radix-ui/colors": "^3.0.0",
"fuzzysort": "^2.0.4",
"fuzzysort": "^3.1.0",
"bepaint": "^0.1.0-beta.3",
"dotenv": "^16.4.5",
"dotenv": "^17.2.2",
"markdown-escape": "^2.0.0"
},
"scripts": {
+37 -37
View File
@@ -6,23 +6,23 @@ import {
getTankStats,
getWN8Percentile,
TankDefinition,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { chunk } from 'lodash-es';
import * as Breakdown from '../../components/Breakdown';
import { CommandWrapper } from '../../components/CommandWrapper';
import { NoData } from '../../components/NoData';
import { TitleBar } from '../../components/TitleBar';
import { filtersToDescription } from '../../core/blitzkit/filtersToDescription';
import { tankDefinitions } from '../../core/blitzkit/nonBlockingPromises';
import { UserError } from '../../core/blitzkit/userError';
import { filterStats, StatFilters } from '../../core/blitzstars/filterStats';
import { getStatsInPeriod } from '../../core/blitzstars/getStatsInPeriod';
import { blitzStarsTankAverages } from '../../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand';
import { translator } from '../../core/localization/translator';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { chunk } from "lodash-es";
import * as Breakdown from "../../components/Breakdown";
import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import { tankDefinitions } from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import { filterStats, StatFilters } from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import { blitzStarsTankAverages } from "../../core/blitzstars/tankAverages";
import { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
@@ -31,7 +31,7 @@ export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
locale: Locale
) {
const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
@@ -39,7 +39,7 @@ export async function renderBreakdown(
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ['clan']);
const clanData = await getClanAccountInfo(region, id, ["clan"]);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: BlitzStats[] = [];
@@ -74,63 +74,63 @@ export async function renderBreakdown(
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, tank.all)
: undefined,
: undefined
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0,
0
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0,
0
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0,
0
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0,
0
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0,
0
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0,
0
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0,
0
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0,
0
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0,
0
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0,
0
) / careerBattles;
const children: JSX.Element[] = [];
const children: React.JSX.Element[] = [];
if (filteredOrder.length > 0) {
children.push(
@@ -172,7 +172,7 @@ export async function renderBreakdown(
delta: currentDamage - careerDamage,
},
]}
/>,
/>
);
filteredOrder.forEach((id, index) => {
@@ -206,7 +206,7 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2,
2
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
@@ -230,24 +230,24 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(
current.damage_dealt / current.battles,
current.damage_dealt / current.battles
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
career.damage_dealt / career.battles
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>,
/>
);
});
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE)
);
if (filteredOrder.length > 0) {
+3 -3
View File
@@ -1,7 +1,7 @@
import { jsxToSvg } from './jsxToSvg';
import { svgToPng } from './svgToPng';
import { jsxToSvg } from "./jsxToSvg";
import { svgToPng } from "./svgToPng";
export async function jsxToPng(jsx: JSX.Element) {
export async function jsxToPng(jsx: React.JSX.Element) {
const svg = await jsxToSvg(jsx);
const png = svgToPng(svg);
@@ -1,10 +1,10 @@
import { jsxToSvg } from './jsxToSvg';
import { RenderConfiguration } from './renderConfiguration';
import { svgToPngThreaded } from './svgToPngThreaded';
import { jsxToSvg } from "./jsxToSvg";
import { RenderConfiguration } from "./renderConfiguration";
import { svgToPngThreaded } from "./svgToPngThreaded";
export async function jsxToPngThreaded(
jsx: JSX.Element,
renderConfiguration: RenderConfiguration,
jsx: React.JSX.Element,
renderConfiguration: RenderConfiguration
) {
const svg = await jsxToSvg(jsx, renderConfiguration);
const png = await svgToPngThreaded(svg);
@@ -1,9 +1,9 @@
import { readFile } from 'fs/promises';
import satori from 'satori';
import { RenderConfiguration } from '../renderConfiguration';
import robotoBlack from './Roboto-Black.ttf';
import robotoBold from './Roboto-Bold.ttf';
import roboto from './Roboto.ttf';
import { readFile } from "fs/promises";
import satori from "satori";
import { RenderConfiguration } from "../renderConfiguration";
import robotoBlack from "./Roboto-Black.ttf";
import robotoBold from "./Roboto-Bold.ttf";
import roboto from "./Roboto.ttf";
const fontsPromise = [
{ file: roboto, weight: 400 as const },
@@ -11,12 +11,12 @@ const fontsPromise = [
{ file: robotoBlack, weight: 900 as const },
].map(async ({ file, weight }) => {
const data = await readFile(`${__dirname}/${file}`);
return { name: 'Roboto', weight, data };
return { name: "Roboto", weight, data };
});
export async function jsxToSvg(
jsx: JSX.Element,
renderConfiguration = new RenderConfiguration(),
jsx: React.JSX.Element,
renderConfiguration = new RenderConfiguration()
) {
const fonts = await Promise.all(fontsPromise);
return await satori(jsx, { width: renderConfiguration.width, fonts });
@@ -6,14 +6,14 @@ import {
InteractionEditReplyOptions,
InteractionReplyOptions,
MessageEditOptions,
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate';
import { Writeable } from '../../types/writable';
import { jsxToPngThreaded } from '../blitzkit/jsxToPngThreaded';
import { RenderConfiguration } from '../blitzkit/renderConfiguration';
} from "discord.js";
import { InteractionReturnable } from "../../events/interactionCreate";
import { Writeable } from "../../types/writable";
import { jsxToPngThreaded } from "../blitzkit/jsxToPngThreaded";
import { RenderConfiguration } from "../blitzkit/renderConfiguration";
export async function normalizeInteractionReturnable(
returnable: InteractionReturnable,
returnable: InteractionReturnable
) {
const images: [number, Buffer][] = [];
const reply: InteractionEditReplyOptions &
@@ -27,7 +27,7 @@ export async function normalizeInteractionReturnable(
await Promise.all(
normalizedReturnable.map(async (item, index) => {
if (typeof item === 'string') {
if (typeof item === "string") {
reply.content = item;
} else if (item instanceof EmbedBuilder) {
if (!reply.embeds) reply.embeds = [];
@@ -36,7 +36,7 @@ export async function normalizeInteractionReturnable(
if (!reply.components)
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
(reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents(
item,
item
);
} else if (item instanceof AttachmentBuilder) {
if (!reply.files) reply.files = [];
@@ -49,14 +49,14 @@ export async function normalizeInteractionReturnable(
const image = await jsxToPngThreaded(item, renderConfiguration);
images.push([index, image]);
}
}),
})
);
if (images.length > 0) {
images.sort(([a], [b]) => b - a);
if (!reply.files) reply.files = [];
images.forEach(([, image]) =>
(reply.files as Writeable<typeof reply.files>)?.push(image),
(reply.files as Writeable<typeof reply.files>)?.push(image)
);
}
@@ -1,7 +1,7 @@
import { DEFAULT_LOCALE, SupportedLocale } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import locales from "@blitzkit/i18n/locales.json";
import { Locale } from "discord.js";
const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
const SUPPORTED_LOCALES_DISCORD_MAP: Record<string, Locale> = {
en: Locale.EnglishUS,
es: Locale.SpanishES,
pt: Locale.PortugueseBR,
@@ -14,12 +14,12 @@ const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
};
export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries(
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k]),
) as Record<Locale, SupportedLocale>;
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k])
) as Record<Locale, string>;
export const SUPPORTED_LOCALES_DISCORD = Object.values(
SUPPORTED_LOCALES_DISCORD_MAP,
SUPPORTED_LOCALES_DISCORD_MAP
);
export const DEFAULT_LOCALE_DISCORD =
SUPPORTED_LOCALES_DISCORD_MAP[DEFAULT_LOCALE];
SUPPORTED_LOCALES_DISCORD_MAP[locales.default];
+12 -14
View File
@@ -1,8 +1,8 @@
import { ReadStream } from '@blitzkit/core';
import AdmZip from 'adm-zip';
import { readFile, writeFile } from 'fs/promises';
import { Parser } from 'pickleparser';
import protobuf from 'protobufjs';
import { ReadStream } from "@blitzkit/core";
import AdmZip from "adm-zip";
import { readFile } from "fs/promises";
import { Parser } from "pickleparser";
import protobuf from "protobufjs";
interface WotbReplayMetaJson {
version: string;
@@ -24,26 +24,24 @@ export class WotbReplayReadStream extends ReadStream {
async wotbReplay() {
const pickleParser = new Parser();
const zip = new AdmZip(Buffer.from(this.buffer));
const metaJsonBuffer = zip.getEntry('meta.json')?.getData();
const battleResultsDat = zip.getEntry('battle_results.dat')?.getData();
const metaJsonBuffer = zip.getEntry("meta.json")?.getData();
const battleResultsDat = zip.getEntry("battle_results.dat")?.getData();
if (!metaJsonBuffer || !battleResultsDat)
throw new SyntaxError('meta.json not found');
throw new SyntaxError("meta.json not found");
const metaJson = JSON.parse(
metaJsonBuffer.toString(),
metaJsonBuffer.toString()
) as WotbReplayMetaJson;
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
const [, battleResultsString] = unpickledBattleResults as [number, string];
const root = await protobuf.load(
`${__dirname}/../../protos/BattleResultsClient.proto`,
`${__dirname}/../../protos/BattleResultsClient.proto`
);
const BattleResults = root.lookupType(
'BattleResultsGenerated.BattleResultsClient',
"BattleResultsGenerated.BattleResultsClient"
);
writeFile('test.dat', Buffer.from(battleResultsString));
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
// const decodedBattleResults = BattleResults.decodeDelimited(
@@ -53,7 +51,7 @@ export class WotbReplayReadStream extends ReadStream {
}
}
const replay = await readFile('temp/test3.wotbreplay');
const replay = await readFile("temp/test3.wotbreplay");
const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer);
stream.wotbReplay();
@@ -1,4 +1,4 @@
import { assertSecret } from '@blitzkit/core';
import { assertSecret } from "@blitzkit/core";
import {
AttachmentBuilder,
AutocompleteInteraction,
@@ -13,30 +13,30 @@ import {
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import { aboutCommand } from '../../commands/about';
import { breakdownCommand } from '../../commands/breakdown';
import { debugCommand } from '../../commands/debug';
import { evolutionCommand } from '../../commands/evolution';
import { fullStatsCommand } from '../../commands/fullStats';
import { inactiveCommand } from '../../commands/inactive';
import { verifyCommand } from '../../commands/link';
import { ownedTanksCommand } from '../../commands/ownedTanks';
import { permissionsCommand } from '../../commands/permissions';
import { pingCommand } from '../../commands/ping';
import { playerInfoCommand } from '../../commands/playerInfo';
import { ratingCommand } from '../../commands/rating';
import { ratingLeaderboardCommand } from '../../commands/ratingLeaderboard';
import { replayCommand } from '../../commands/replay';
import { researchCommand } from '../../commands/research';
import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers';
import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today';
import { RenderConfiguration } from '../../core/blitzkit/renderConfiguration';
import { handleAutocomplete } from './handlers/autocomplete';
import { handleButton } from './handlers/button';
import { handleChatInputCommand } from './handlers/chatInputCommand';
} from "discord.js";
import { aboutCommand } from "../../commands/about";
import { breakdownCommand } from "../../commands/breakdown";
import { debugCommand } from "../../commands/debug";
import { evolutionCommand } from "../../commands/evolution";
import { fullStatsCommand } from "../../commands/fullStats";
import { inactiveCommand } from "../../commands/inactive";
import { verifyCommand } from "../../commands/link";
import { ownedTanksCommand } from "../../commands/ownedTanks";
import { permissionsCommand } from "../../commands/permissions";
import { pingCommand } from "../../commands/ping";
import { playerInfoCommand } from "../../commands/playerInfo";
import { ratingCommand } from "../../commands/rating";
import { ratingLeaderboardCommand } from "../../commands/ratingLeaderboard";
import { replayCommand } from "../../commands/replay";
import { researchCommand } from "../../commands/research";
import { searchClansCommand } from "../../commands/searchClans";
import { searchPlayersCommand } from "../../commands/searchPlayers";
import { statsCommand } from "../../commands/stats";
import { todayCommand } from "../../commands/today";
import { RenderConfiguration } from "../../core/blitzkit/renderConfiguration";
import { handleAutocomplete } from "./handlers/autocomplete";
import { handleButton } from "./handlers/button";
import { handleChatInputCommand } from "./handlers/chatInputCommand";
export type InteractionRawReturnable =
| string
@@ -44,7 +44,7 @@ export type InteractionRawReturnable =
| ButtonBuilder
| AttachmentBuilder
| RenderConfiguration
| JSX.Element
| React.JSX.Element
| null;
export type InteractionIterableReturnable =
| InteractionRawReturnable
@@ -58,7 +58,7 @@ interface CommandRegistryBase {
| SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder
| SlashCommandOptionsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
| Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
@@ -72,7 +72,7 @@ interface CommandRegistryImplicit extends CommandRegistryBase {
interface CommandRegistryExplicit extends CommandRegistryBase {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
interaction: ChatInputCommandInteraction<CacheType>
) => InteractionReturnable;
}
@@ -104,16 +104,16 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry, index) => {
if (registry.status === 'rejected') {
if (registry.status === "rejected") {
console.warn(
`Command ${index} failed to load; skipping...`,
registry.reason,
registry.reason
);
return commands;
}
return { ...commands, [registry.value.command.name]: registry.value };
},
{},
{}
);
});
@@ -121,12 +121,12 @@ const rest = new REST().setToken(assertSecret(import.meta.env.DISCORD_TOKEN));
commands.then((awaitedCommands) => {
const body = Object.values(awaitedCommands).map((registry) =>
registry.command.toJSON(),
registry.command.toJSON()
);
rest.put(
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
{ body },
{ body }
);
});
+8 -8
View File
@@ -4,20 +4,20 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
"@octokit/plugin-throttling": "^8.2.0",
"@octokit/rest": "^20.1.1",
"@prisma/client": "^6.6.0",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^16.4.5",
"dotenv": "^17.2.2",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.4.0",
"ts-proto": "^2.2.2",
"yaml": "^2.5.0"
"protobufjs": "^7.5.4",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
},
"devDependencies": {
"@types/adm-zip": "^0.5.5",
"@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1"
}
}
+18 -16
View File
@@ -1,8 +1,8 @@
import { deburr } from 'lodash-es';
import { SUPPORTED_LOCALES } from '../../../i18n/src/strings';
import { I18nString } from '../protos';
import { fetchCamouflageDefinitions } from './camouflageDefinitions';
import { fetchTankDefinitions } from './tankDefinitions';
import locales from "@blitzkit/i18n/locales.json";
import { deburr } from "lodash-es";
import { I18nString } from "../protos";
import { fetchCamouflageDefinitions } from "./camouflageDefinitions";
import { fetchTankDefinitions } from "./tankDefinitions";
export async function fetchTankNames() {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
@@ -26,25 +26,27 @@ export async function fetchTankNames() {
searchableNameDeburr,
camouflages: tank.camouflages
?.map((id) =>
SUPPORTED_LOCALES.map(
(locale) =>
camouflageDefinitions.camouflages[id]?.name.locales[locale],
),
locales.supported.map(
({ locale }) =>
camouflageDefinitions.camouflages[id]?.name.locales[locale]
)
)
.flat()
.filter(Boolean)
.map(deburr)
.join(' '),
.join(" "),
treeType: tank.type,
};
}),
})
);
}
export const SEARCH_KEYS = [
...SUPPORTED_LOCALES.map((locale) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
]).flat(),
'camouflages',
...locales.supported
.map(({ locale }) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
])
.flat(),
"camouflages",
];
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
"ts-deepmerge": "^7.0.2"
"ts-deepmerge": "^7.0.3"
},
"devDependencies": {}
}
+4 -3
View File
@@ -1,3 +1,4 @@
export * from './literals';
export * from './strings';
export * from './unwrapper';
export * from "./literals";
export * from "./strings";
export * from "./unwrapper";
export * from "./wrapper";
+50 -82
View File
@@ -1,98 +1,66 @@
import { merge } from 'ts-deepmerge';
import en from '../strings/en.json';
import es from '../strings/es.json';
import fr from '../strings/fr.json';
import ja from '../strings/ja.json';
import pl from '../strings/pl.json';
import pt from '../strings/pt.json';
import ru from '../strings/ru.json';
import uk from '../strings/uk.json';
import zh from '../strings/zh.json';
import { merge } from "ts-deepmerge";
import locales from "../locales.json";
import en from "../strings/en.json";
import es from "../strings/es.json";
import fr from "../strings/fr.json";
import ja from "../strings/ja.json";
import pl from "../strings/pl.json";
import pt from "../strings/pt.json";
import ru from "../strings/ru.json";
import uk from "../strings/uk.json";
import zh from "../strings/zh.json";
export type BlitzKitStrings = typeof en;
const stringsRaw = { en, es, fr, ja, pl, pt, ru, uk, zh };
const stringsRaw: Record<string, TranslationTree> = {
en,
es,
fr,
ja,
pl,
pt,
ru,
uk,
zh,
};
export const SUPPORTED_LOCALES = Object.keys(stringsRaw) as SupportedLocale[];
export const DEFAULT_LOCALE = SUPPORTED_LOCALES[0];
function redact(string: string) {
const matches = [...string.matchAll(/%s\d+/g)];
const characters = string
.split('')
.map((char) => (char === ' ' ? ' ' : '█')) as string[];
matches.forEach((match) => {
const start = match.index;
const matchText = match[0];
for (let i = 0; i < matchText.length; i++) {
characters[start + i] = matchText[i];
}
});
return characters.join('');
}
function nuke(object: TranslationTree) {
for (const key in object) {
const typedKey = key as keyof typeof object;
if (typeof object[typedKey] === 'string') {
const value = object[typedKey];
object[typedKey] = redact(value as string);
} else nuke(object[typedKey] as TranslationTree);
}
}
const stringsPartial: Partial<Record<SupportedLocale, TranslationTree>> = {};
export const STRINGS: Record<string, BlitzKitStrings> = {};
for (const locale in stringsRaw) {
if (locale === DEFAULT_LOCALE) {
stringsPartial[DEFAULT_LOCALE] = stringsRaw[DEFAULT_LOCALE];
if (locale === locales.default) {
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
continue;
}
const localeStrings = stringsRaw[locale as SupportedLocale];
stringsPartial[locale as SupportedLocale] = merge(
stringsRaw[DEFAULT_LOCALE],
localeStrings,
);
if (import.meta.env.PUBLIC_DEBUG_MISSING_I18N === 'true') {
nuke(stringsPartial[locale as SupportedLocale]!);
}
const localeStrings = stringsRaw[locale];
STRINGS[locale] = merge(
stringsRaw[locales.default],
localeStrings
) as BlitzKitStrings;
}
export const STRINGS = stringsPartial as Record<
SupportedLocale,
BlitzKitStrings
>;
export type SupportedLocale = keyof typeof stringsRaw;
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<SupportedLocale, string> = {
en: 'en',
es: 'es',
fr: 'fr',
ja: 'ja',
pt: 'pt',
ru: 'ru',
uk: 'uk',
zh: 'zh-Hans',
pl: 'pl',
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
en: "en",
es: "es",
fr: "fr",
ja: "ja",
pt: "pt",
ru: "ru",
uk: "uk",
zh: "zh-Hans",
pl: "pl",
};
export const SUPPORTED_LOCALE_FLAGS: Record<SupportedLocale, string> = {
en: '🇺🇸',
es: '🇪🇸',
fr: '🇫🇷',
ja: '🇯🇵',
pt: '🇵🇹',
ru: '🇷🇺',
uk: '🇺🇦',
zh: '🇨🇳',
pl: '🇵🇱',
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
en: "🇺🇸",
es: "🇪🇸",
fr: "🇫🇷",
ja: "🇯🇵",
pt: "🇵🇹",
ru: "🇷🇺",
uk: "🇺🇦",
zh: "🇨🇳",
pl: "🇵🇱",
};
export type TranslationTree = { [key: string]: TranslationNode };
+4 -4
View File
@@ -1,12 +1,12 @@
import { I18nString } from '@blitzkit/core';
import { DEFAULT_LOCALE } from './strings';
import { I18nString } from "@blitzkit/core";
import locales from "@blitzkit/i18n/locales.json";
export function unwrapper(locale: string) {
export function unwrapper(locale = locales.default) {
return function (i18nString: I18nString) {
if (locale in i18nString.locales) {
return i18nString.locales[locale];
}
return i18nString.locales[DEFAULT_LOCALE];
return i18nString.locales[locales.default];
};
}
+15
View File
@@ -0,0 +1,15 @@
import locales from "../locales.json";
import { BlitzKitStrings, STRINGS } from "./strings";
export function wrapper(slicer: (strings: BlitzKitStrings) => string) {
const defaultString = slicer(STRINGS[locales.default] as BlitzKitStrings);
const object = { [locales.default]: defaultString };
for (const { locale } of locales.supported) {
const string = slicer(STRINGS[locale] as BlitzKitStrings);
if (string !== defaultString) object[locale] = string;
}
return object;
}
+32 -14
View File
@@ -6,6 +6,8 @@
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1m",
"mm": "%s1mm",
@@ -763,6 +765,8 @@
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
@@ -833,6 +837,15 @@
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -861,6 +874,15 @@
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -887,6 +909,10 @@
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -935,9 +961,7 @@
"green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
@@ -1437,17 +1461,11 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
+28 -14
View File
@@ -5,6 +5,7 @@
"fox": "El rápido zorro marrón salta sobre el perro perezoso."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1m",
"mm": "%s1mm",
@@ -671,6 +672,8 @@
"copied": "Copiado!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "Buscar tanques...",
"recent": "Visto recientemente",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -764,6 +775,14 @@
}
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -787,6 +806,9 @@
"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",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -829,9 +851,7 @@
"green_penetration": "Penetración verde",
"hide_model_under_armor": "Ocultar modelo bajo la armadura",
"opaque": "Opaco",
"dev_wireframe": "DEV: wireframe",
"environment": "Entorno",
"show_grid": "Mostrar cuadrícula"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
"download": "Descargar",
@@ -1307,17 +1327,11 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analizar ahora",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+69 -55
View File
@@ -5,6 +5,7 @@
"fox": "Le renard brun et rapide saute par-dessus le chien paresseux."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1m",
"mm": "%s1mm",
@@ -364,9 +365,9 @@
"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.",
"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.",
"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.",
"unordered": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank."
"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 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 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": {
@@ -380,7 +381,7 @@
},
"embeds": {
"name": "intègre",
"description": "How embeds work"
"description": "Comment fonctionnent les intégrations"
},
"introduction": {
"name": "introduction",
@@ -402,7 +403,7 @@
},
"breakdown": {
"name": "décomposition",
"description": "A period's breakdown by tanks played",
"description": "Répartition d'une période par chars joués",
"body": {
"total": "Total",
"battles": "Batailles",
@@ -471,7 +472,7 @@
"accuracy": "Précision"
},
"lethality": {
"title": "Lethality",
"title": "Létalité",
"average_damage": "Dégâts moyens",
"damage_per_tier": "Dégâts par tier",
"damage_ratio": "Rapport de dégâts",
@@ -499,20 +500,20 @@
},
"embed": {
"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": {
"name": "search-clans",
"description": "Search clans",
"name": "clans de recherche",
"description": "Rechercher des clans",
"options": {
"limit": {
"name": "limite",
"description": "The max number of clan results (default: %s1)"
"description": "Nombre maximum de résultats du clan (par défaut: %s1)"
}
},
"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é"
}
},
@@ -535,69 +536,69 @@
"description": "Statistiques des batailles de cotés",
"subcommands": {
"neighbors": {
"name": "neighbors",
"description": "Neighboring players on the leaderboard"
"name": "voisins",
"description": "Joueurs voisins dans le classement"
},
"league": {
"name": "ligue",
"description": "Top players of a league"
"description": "Meilleurs joueurs d'une ligue"
}
},
"options": {
"limit": {
"name": "limite",
"description": "How many neighbors to display (default: %s1)"
"description": "Combien de voisins afficher (par défaut : %s1)"
},
"season": {
"name": "season",
"description": "Season number (default: current)",
"name": "saison",
"description": "Numéro de saison (défaut : courant)",
"choices": {
"current": "%s1 (current)"
"current": "%s1 (actuel)"
}
},
"league": {
"name": "ligue",
"description": "The league to display"
"description": "La ligue à afficher"
}
},
"body": {
"top_players": "Top %s1 players",
"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_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted %s1",
"neighbors": "Rating neighbors",
"more": "+ %s1 more"
"top_players": "Top %s1 joueurs",
"no_participation": "%s1 n'a pas joué au classement dans la saison %s2",
"no_ongoing_season": "Pas de saison en cours\n\nIl n'y a pas de saison de notation en cours actuellement.",
"deleted_player": "%s1 supprimé",
"neighbors": "évaluer les voisins",
"more": "+ %s1 de plus"
}
},
"today": {
"name": "aujourd'hui",
"description": "Today's played tanks and statistics"
"description": "Les chars et les statistiques joués aujourd'hui"
},
"stats": {
"name": "statistiques",
"description": "Quick statistics for a period"
"description": "Statistiques rapides pour une période"
},
"owned_tanks": {
"name": "owned-tanks",
"description": "Shows all own tanks in a tier",
"name": "chars possédés",
"description": "Affiche tous les chars d'un niveau",
"body": {
"title": "# %s1's owned tier %s2 tanks\n\n"
"title": "# %s1 possédait des chars de rang %s2"
}
},
"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": {
"title": "# Permissions\n\n%s1",
"view_channels": "View channels: needed to find the message the refresh button was used on",
"read_message_history": "Read message history: needed to find the message to refresh",
"attach_files": "Attach files: needed to add images to image-based commands",
"external_emojis": "External emojis: needed to add national flags and tank class emojis"
"view_channels": "Afficher les canaux : nécessaire pour trouver le message sur lequel le bouton d'actualisation a été utilisé",
"read_message_history": "Lire l'historique des messages : nécessaire pour trouver le message à rafraîchir",
"attach_files": "Joindre des fichiers : nécessaire pour ajouter des images à des commandes basées sur l'image",
"external_emojis": "Émoticônes externes: nécessaire pour ajouter des drapeaux nationaux et des émojis de classe de char"
}
},
"ping": {
"name": "ping",
"description": "Check wether the bot is alive.",
"description": "Vérifiez si le bot est en vie.",
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms"
@@ -605,7 +606,7 @@
"subcommands": {
"blitzkit": {
"name": "blitzkit",
"description": "Ping BlitzKit"
"description": "Ping blitzkit"
},
"wotb": {
"name": "wotb",
@@ -614,10 +615,10 @@
}
},
"player_info": {
"name": "player-info",
"description": "Basic information about a player",
"name": "info-joueur",
"description": "Informations de base sur un joueur",
"body": {
"title": "%s1's information",
"title": "Informations de %s1",
"nickname": "Nickname",
"battles": "Batailles",
"winrate": "Taux de victoires",
@@ -635,7 +636,7 @@
"description": "Today's rating statistics"
},
"season": {
"name": "season",
"name": "saison",
"description": "This season's rating statistics"
}
},
@@ -671,6 +672,8 @@
"copied": "Copied!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -764,6 +775,14 @@
}
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -787,6 +806,9 @@
"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.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -829,9 +851,7 @@
"green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
"download": "Download",
@@ -1307,17 +1327,11 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+28 -14
View File
@@ -5,6 +5,7 @@
"fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。"
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1m",
"mm": "%s1mm",
@@ -671,6 +672,8 @@
"copied": "コピーしました!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "車輌名を入力…",
"recent": "最近の閲覧",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -764,6 +775,14 @@
}
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -787,6 +806,9 @@
"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.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -829,9 +851,7 @@
"green_penetration": "貫通可能域を緑色表示",
"hide_model_under_armor": "車輌モデルを非表示",
"opaque": "貫通アシストを不透明にする",
"dev_wireframe": "DEV: wireframe",
"environment": "環境",
"show_grid": "グリッドを表示"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
"download": "保存する",
@@ -1307,17 +1327,11 @@
"description": "さあ、BlitzKit を始めよう!",
"seo_description": "さあ、BlitzKit を始めよう!"
},
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "ゲーム内のセッション統計",
"seo_description": "In-game session stats in World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+28 -14
View File
@@ -5,6 +5,7 @@
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1m",
"mm": "%s1mm",
@@ -671,6 +672,8 @@
"copied": "Copied!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -764,6 +775,14 @@
}
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -787,6 +806,9 @@
"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.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -829,9 +851,7 @@
"green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
"download": "Download",
@@ -1307,17 +1327,11 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+28 -14
View File
@@ -5,6 +5,7 @@
"fox": "A ágil raposa castanha salta por cima do cão preguiçoso."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1m",
"mm": "%s1mm",
@@ -671,6 +672,8 @@
"copied": "Copiado!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "Procurar tanques...",
"recent": "Visto recentemente",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -764,6 +775,14 @@
}
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -787,6 +806,9 @@
"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.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -829,9 +851,7 @@
"green_penetration": "Penetração verde",
"hide_model_under_armor": "Ocultar modelo sob blindagem",
"opaque": "Opaco",
"dev_wireframe": "DEV: wireframe",
"environment": "Ambiente",
"show_grid": "Exibir grade"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
"download": "Download",
@@ -1307,17 +1327,11 @@
"description": "Comece com BlitzKit",
"seo_description": "Comece com BlitzKit"
},
"blitz_analysis": {
"name": "AnaliseBlitz[]",
"button": "Analise agora",
"description": "Estatísticas médias históricas ricas",
"seo_description": "Estatísticas médias históricas ricas no World of Tanks Blitz"
},
"blitz_insider": {
"name": "Assistente Blitz",
"button": "Descubra hoje",
"description": "Estatísticas da temporada dentro do jogo",
"seo_description": "Estatísticas da sessão no jogo World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+78 -64
View File
@@ -1,10 +1,11 @@
{
"test": {
"default": "тест",
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.",
"fox": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3."
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — %s1, потому что это цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.",
"fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1м",
"mm": "%s1мм",
@@ -283,15 +284,15 @@
"name": "уровень",
"description": "Уровень",
"choices": {
"tier": "Уровень %s - %s"
"tier": "Уровень %s1 - %s2"
}
}
},
"periods": {
"career": "Карьера",
"today": "Cегодня",
"days": "%s дней",
"custom": "%s - %s дней"
"days": "%s1 дней",
"custom": "%s1 - %s2 дней"
},
"filters": {
"none": "Без фильтров",
@@ -323,9 +324,9 @@
"description": "Танки доступные на аукционе сейчас",
"body": {
"title": "Blitz аукцион",
"subtitle": "%s доступные • следующие цены %s\nПокупка сейчас: %s",
"next": "Следующая цена: %s",
"available": "Доступно: %s / %s"
"subtitle": "%s1 доступные • следующие цены %s2\nПокупка сейчас: %s3",
"next": "Следующая цена: %s1",
"available": "Доступно: %s1 / %s2"
},
"errors": {
"no_auction": "# Аукцион сейчас не проходит\n\nНа данный момент нет танков для покупки на аукционе. Если считаете, что это ошибка, пожалуйста о несоответствии на [официальном сервере Discord](https://discord.gg/nDt7AjGJQH)."
@@ -354,19 +355,19 @@
"body": {
"subtitle": "Исследование",
"total": "Всего",
"research": "Исследование: %s",
"upgrades": "Улучшения: %s",
"purchase": "Покупка: %s",
"equipment": "Снаряжение: %s",
"research": "Исследование: %s1",
"upgrades": "Улучшения: %s1",
"purchase": "Покупка: %s1",
"equipment": "Снаряжение: %s1",
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
},
"errors": {
"non_tech_tree": "# %s отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.",
"tier_1": "# %s - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.",
"already_researched": "# %s уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
"start_end_equal": "# %s был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
"tanks_not_on_line": "# %s и %s находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
"unordered": "# У %s более высокий уровень чем у %s\n\nОбязательно укажите %s в качестве «starting-tank», так как это танк более низкого уровня."
"non_tech_tree": "# %s1 отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.",
"tier_1": "# %s1 - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.",
"already_researched": "# %s1 уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
"start_end_equal": "# %s1 был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
"tanks_not_on_line": "# %s1 и %s2 находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
"unordered": "# У %s1 более высокий уровень чем у %s2\n\nОбязательно укажите %s3 в качестве «starting-tank», так как это танк более низкого уровня."
}
},
"about": {
@@ -409,7 +410,7 @@
"wn8": "WN8",
"winrate": "Коэффициент побед",
"damage": "Урон",
"unknown_tank": "Неизвестный танк %s"
"unknown_tank": "Неизвестный танк %s1"
}
},
"debug": {
@@ -445,12 +446,12 @@
"options": {
"threshold": {
"name": "порог",
"description": "Количество дней не активности (по умолчанию: %s дней)"
"description": "Количество дней не активности (по умолчанию: %s1 дней)"
}
},
"body": {
"subtitle": "Неактивен %s+ дней",
"listing": "%s дней"
"subtitle": "Неактивен %s1+ дней",
"listing": "%s1 дней"
}
},
"full_stats": {
@@ -508,11 +509,11 @@
"options": {
"limit": {
"name": "лимит",
"description": "Максимальное количество результатов клана (по умолчанию: %s)"
"description": "Максимальное количество результатов клана (по умолчанию: %s1)"
}
},
"body": {
"title": "Поиск клана %s в регионе %s",
"title": "Поиск клана %s1 в регионе %s2",
"no_results": "Кланов не найдено"
}
},
@@ -522,11 +523,11 @@
"options": {
"limit": {
"name": "лимит",
"description": "Максимальное число результатов игрока (по умолчанию: %s)"
"description": "Максимальное число результатов игрока (по умолчанию: %s1)"
}
},
"body": {
"title": "Поиск игрока %s в регионе %s",
"title": "Поиск игрока %s1 в регионе %s2",
"no_results": "Игроков не найдено"
}
},
@@ -546,13 +547,13 @@
"options": {
"limit": {
"name": "лимит",
"description": "Количество отображаемых ближайших игроков (по умолчанию: %s)"
"description": "Количество отображаемых ближайших игроков (по умолчанию: %s1)"
},
"season": {
"name": "сезон",
"description": "Номер сезона (по умолчанию: текущий)",
"choices": {
"current": "%s (текущая)"
"current": "%s1 (текущая)"
}
},
"league": {
@@ -561,12 +562,12 @@
}
},
"body": {
"top_players": "Топ %s игроков",
"no_participation": "# %s не играл в рейтинг в сезоне %s\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
"top_players": "Топ %s1 игроков",
"no_participation": "# %s1 не играл в рейтинг в сезоне %s2\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
"no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.",
"deleted_player": "Удалено %s",
"deleted_player": "Удалено %s1",
"neighbors": "Рейтинг ближайших игроков",
"more": "+ ещё %s"
"more": "+ ещё %s1"
}
},
"today": {
@@ -581,14 +582,14 @@
"name": "cобственные-танки",
"description": "Показывает все принадлежащие танки на уровне",
"body": {
"title": "# %s принадлежал уровень %s танков\n\n"
"title": "# %s1 принадлежал уровень %s2 танков\n\n"
}
},
"permissions": {
"name": "разрешения",
"description": "Проверяет, есть ли у бота необходимые разрешения для работы",
"body": {
"title": "# Разрешения\n\n%s",
"title": "# Разрешения\n\n%s1",
"view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления",
"read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления",
"attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения",
@@ -600,7 +601,7 @@
"description": "Проверьте, что бот жив.",
"body": {
"pong": "Понг 🏓",
"pong_with_time": "Понг 🏓 - %sмс"
"pong_with_time": "Понг 🏓 - %s1мс"
},
"subcommands": {
"blitzkit": {
@@ -617,7 +618,7 @@
"name": "информация-об-игроке",
"description": "Основная информация о игроке",
"body": {
"title": "Информация о %s",
"title": "Информация о %s1",
"nickname": "Никнейм",
"battles": "Боёв",
"winrate": "Коэффициент побед",
@@ -646,7 +647,7 @@
}
},
"errors": {
"no_participation": "# %s не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
"no_participation": "# %s1 не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
"unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов."
}
}
@@ -671,12 +672,14 @@
"copied": "Скопировано!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Сбросить фильтры",
"search_bar_hint": "Поиск танков...",
"recent": "Недавние",
"select_all": "Выбрать всё",
"count_singular": "1 танк",
"count_plural": "%s1 танк",
"count_plural": "Танков: %s1",
"sorting_by": "Сортировать по %s1",
"no_tanks_found": {
"body": "Танков не найдено.",
@@ -693,7 +696,7 @@
"survivability.volume": "объём",
"survivability.length": "длина",
"fire.dpm": "базовый ДПМ",
"fire.dpmPremium": "улучшенный ДПМ",
"fire.dpmPremium": "премиумный ДПМ",
"fire.reload": "перезарядка",
"fire.caliber": "калибр",
"fire.standardPenetration": "базовое пробитие",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Отклонить"
},
"discord": {
"join_short": "Присоединяйтесь к крутым ребятам!",
"join_long": "Присоединяйся к супер-дупер крутым ребятам!",
@@ -764,6 +775,14 @@
}
},
"tools": {
"mixer": {
"name": "Смешиватель",
"button": "Смешать и собрать",
"description": "Модули смешанного танка",
"seo_description": "Смешивание и сбор корпусов, башен и пушек танков из World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Случайный выбор"
},
"guess": {
"name": "Угадайте танк",
"button": "Угадать",
@@ -784,9 +803,12 @@
"button": "Поиск танков",
"description": "Танковая энциклопедия 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 здоровье.",
"visualizers": {
"reload": {
"skip": "Пропуск"
},
"assault": {
"title": "Штурмовые снаряды"
},
@@ -806,13 +828,13 @@
"no_ricochet": "Разрывные снаряды не рикошетят"
},
"view_range": {
"camo": "Камо",
"camo": "Камуфляж",
"percentage": "%s1%",
"ghost_factory": "Промзона",
"dead_rail": "Эшелон"
},
"flexibility": {
"yaw": "Отклонение",
"yaw": "Поворот",
"pitch": "Наклон",
"min": "Вниз",
"max": "Вверх"
@@ -829,9 +851,7 @@
"green_penetration": "Зелёное пробитие",
"hide_model_under_armor": "Скрыть модель под броней",
"opaque": "Прозрачность",
"dev_wireframe": "DEV: wireframe",
"environment": "Окружение",
"show_grid": "Показать сетку"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
"download": "Загрузить",
@@ -902,7 +922,7 @@
"tier": "Уровень",
"type": "Тип",
"dev_id": "DEV: ID",
"purchase": "Стоимость покупки",
"purchase": "Цена покупки",
"restoration": "Цена восстановления",
"sale": "Цена продажи",
"research": "Необходимо опыта"
@@ -978,7 +998,7 @@
"dpmEffective": "Эффективность за 60с",
"shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Reloads",
"shellReloads": "Перезарядка",
"shellReload": "Перезарядка",
"intraClip": "Между снарядами",
"caliber": "Калибр",
@@ -1038,8 +1058,8 @@
"shellCapacity": "Боезапас",
"gunType": "Тип орудия",
"penetrationAt250m": "Пробитие на 250м",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"burstShells": "Кол-во разрывных снарядов",
"burstInterShell": "Взрыв между снарядами"
}
},
"firepower": {
@@ -1136,7 +1156,7 @@
"hits": "Уничтожен",
"damage_ratio": "Коэф. урона",
"damage_taken": "Получено урона",
"capture_points": "Ограничительные точки"
"capture_points": "Точки ограничения"
}
}
},
@@ -1148,7 +1168,7 @@
"last_updated": "Последнее обновление %s1",
"search": {
"hint": "Поиск аватаров...",
"results": "%s1 аватар"
"results": "Аватаров: %s1"
}
},
"session": {
@@ -1157,7 +1177,7 @@
"description": "Статистика сеанса игры",
"seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz",
"search": {
"lookup": "Искать игрока, чтобы начать",
"lookup": "Введите ник игрока",
"hint": "Поиск игроков...",
"searching": "Поиск...",
"no_results": "Игроков не найдено"
@@ -1198,7 +1218,7 @@
"button": "Создать",
"description": "Встраиваемые элементы для стримов",
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
"coming_soon": "Скоро появятся новые встраиваемые элементы.",
"coming_soon": "В скором времени появятся новые встраиваемые элементы.",
"suggest": "Предложить идеи.",
"preview": {
"title": "Предпросмотр",
@@ -1307,17 +1327,11 @@
"description": "Начните с BlitzKit",
"seo_description": "Начните с BlitzKit"
},
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Анализируй",
"description": "Богатая статистика",
"seo_description": "Богатые статистика по World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Открой для себя",
"description": "Статистика сеанса игры",
"seo_description": "Статистика сеанса игры в World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+28 -14
View File
@@ -5,6 +5,7 @@
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1м",
"mm": "%s1мм",
@@ -671,6 +672,8 @@
"copied": "Скопійовано!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"search_bar_hint": "Шукайте техніку...",
"recent": "Недавно переглянуті",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -764,6 +775,14 @@
}
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -787,6 +806,9 @@
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz",
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -829,9 +851,7 @@
"green_penetration": "Зелене пробиття",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
"download": "Завантажити",
@@ -1307,17 +1327,11 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+28 -14
View File
@@ -5,6 +5,7 @@
"fox": "褐色的狐狸迅速地跳过那只懒狗"
},
"common": {
"name": "BlitzKit",
"units": {
"m": "%s1公尺",
"mm": "%s1毫米",
@@ -671,6 +672,8 @@
"copied": "已拷贝!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "清除筛选",
"search_bar_hint": "搜寻战车……",
"recent": "最近查看的",
@@ -739,6 +742,14 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "加入超級酷的人群!",
"join_long": "加入我們的Discord社群,和超酷的人一起聊天!",
@@ -764,6 +775,14 @@
}
},
"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",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "猜坦克",
"button": "猜猜看",
@@ -787,6 +806,9 @@
"tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)",
"tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。",
"visualizers": {
"reload": {
"skip": "跳过"
},
"assault": {
"title": "Assault shells"
},
@@ -829,9 +851,7 @@
"green_penetration": "可击穿区域",
"hide_model_under_armor": "隐藏装甲下的模型",
"opaque": "不透明",
"dev_wireframe": "开发选项:网格",
"environment": "环境",
"show_grid": "显示网格"
"dev_wireframe": "开发选项:网格"
},
"screenshot": {
"download": "下载",
@@ -1307,17 +1327,11 @@
"description": "开始使用 BlitzKit",
"seo_description": "开始使用 BlitzKit"
},
"blitz_analysis": {
"name": "闪击战分析",
"button": "立即分析",
"description": "丰富的历史平均统计",
"seo_description": "战车世界闪击战中丰富的历史平均统计"
},
"blitz_insider": {
"name": "闪击战内嵌插件",
"button": "开始探索",
"description": "游戏内场次统计",
"seo_description": "战车世界闪击战内的游戏内场次统计"
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
}
},
"footer": {
+8 -7
View File
@@ -11,20 +11,21 @@
"build:ids": "bun src/discoverIds.ts",
"build:bot": "bun src/buildBot.ts",
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
"build:proxyclient": "bun src/buildProxyClient.ts",
"prunestrings": "bun src/pruneStrings.ts"
},
"dependencies": {
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@gltf-transform/core": "^4.0.10",
"@gltf-transform/extensions": "^4.0.10",
"@gltf-transform/functions": "^4.0.10",
"csv-parse": "^5.5.6",
"@gltf-transform/core": "^4.2.1",
"@gltf-transform/extensions": "^4.2.1",
"@gltf-transform/functions": "^4.2.1",
"csv-parse": "^6.1.0",
"decompress": "^4.2.1",
"esbuild": "^0.19.12",
"fast-xml-parser": "^4.4.1",
"esbuild": "^0.25.10",
"fast-xml-parser": "^5.2.5",
"progress": "^2.0.3",
"puppeteer": "^23.10.4"
"puppeteer": "^24.22.3"
},
"devDependencies": {
"@types/decompress": "^4.2.7",
@@ -1,11 +1,11 @@
import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from '@blitzkit/core';
import { launch } from 'puppeteer';
import sharp from 'sharp';
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from "@blitzkit/core";
import { launch } from "puppeteer";
import sharp from "sharp";
import { commitAssets } from "../core/github/commitAssets";
import { FileChange } from "../core/github/commitMultipleFiles";
export async function blitzkitTankIcons() {
console.log('Building blitzkit tank icons...');
console.log("Building blitzkit tank icons...");
const browser = await launch();
const page = await browser.newPage();
@@ -21,7 +21,7 @@ export async function blitzkitTankIcons() {
try {
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
waitUntil: 'networkidle0',
waitUntil: "networkidle0",
});
} catch (error) {
console.error(`${id} ${name} time out`, error);
@@ -29,21 +29,21 @@ export async function blitzkitTankIcons() {
}
const screenshot = await page.screenshot({
type: 'webp',
type: "webp",
omitBackground: true,
});
const content = await sharp(screenshot).trim({ threshold: 100 }).toBuffer();
index++;
console.log(
`Rendered ${id} (${name}) ${((index / tanks.length) * 100).toFixed(2)}%`,
`Rendered ${id} (${name}) ${((index / tanks.length) * 100).toFixed(2)}%`
);
files.push({ path: `icons/tanks/blitzkit/${id}.webp`, content });
files.push({ path: `icons/tanks/big/${id}.webp`, content });
}
await page.close();
await browser.close();
await commitAssets('tank icons', files);
await commitAssets("tank icons", files);
}
File diff suppressed because it is too large. Load diff
+25 -23
View File
@@ -1,50 +1,52 @@
import { toUniqueId } from '@blitzkit/core';
import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import { readdir } from 'fs/promises';
import { extractModel } from '../core/blitz/extractModel';
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
import { commitAssets } from '../core/github/commitAssets';
import { FileChange } from '../core/github/commitMultipleFiles';
import { DATA } from './constants';
import { VehicleDefinitionList } from './definitions';
import { TankParameters } from './tankIcons';
import { toUniqueId } from "@blitzkit/core";
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { readdir } from "fs/promises";
import { extractModel } from "../core/blitz/extractModel";
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { commitAssets } from "../core/github/commitAssets";
import { FileChange } from "../core/github/commitMultipleFiles";
import { DATA } from "./constants";
import { VehicleDefinitionList } from "./definitions";
import { TankParameters } from "./tankIcons";
export async function tankModels() {
console.log('Building tank models...');
console.log("Building tank models...");
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
(nations) => nations.filter((nation) => nation !== 'common'),
(nations) => nations.filter((nation) => nation !== "common")
);
for (const nationIndex in nations) {
const nation = nations[nationIndex];
const changes: FileChange[] = [];
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`,
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
);
const entries = Object.entries(tanks.root);
console.log(`Building models for ${nation}`);
for (const [tankKey, tank] of Object.entries(tanks.root)) {
if (tankKey.includes('tutorial_bot')) continue;
let index = 0;
for (const [tankKey, tank] of entries) {
console.log(` ${++index} / ${entries.length} : ${tankKey}`);
if (tankKey.includes("tutorial_bot")) continue;
const id = toUniqueId(nation, tank.id);
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
);
const model = await extractModel(
DATA,
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "")
);
const content = Buffer.from(await nodeIO.writeBinary(model));
changes.push({
path: `3d/tanks/models/${id}.glb`,
content: Buffer.from(await nodeIO.writeBinary(model)),
});
changes.push({ path: `3d/tanks/models/${id}.glb`, content });
}
await commitAssets(`tank models ${nation}`, changes);
+45
View File
@@ -0,0 +1,45 @@
import { readFile, writeFile } from "fs/promises";
const IMPORT = "@protos/auto_legacy_proxy_client";
const SOURCE =
"../../submodules/blitzkit-closed/protos/auto_legacy_proxy_client.proto";
const TARGET =
"../../submodules/blitzkit-closed/src/unreal/methodPopulatedProxyClient.ts";
const requestPattern = /message (\w+)Request {/g;
const source = await readFile(SOURCE).then((buffer) => buffer.toString());
const requests = source.matchAll(requestPattern);
const imports: string[] = [];
const methods: { name: string; method: string }[] = [];
for (const match of requests) {
const name = match[1];
// const method =
// name.charAt(0).toLowerCase() + name.slice(1).replace(/Async$/, "");
const method = name;
imports.push(`${name}Request`, `${name}Response`);
methods.push({ name, method });
}
let content = "";
content += "import {\n";
content += imports.map((i) => ` ${i},`).join("\n");
content += `\n} from "${IMPORT}";\n`;
content += 'import { BaseProxyClient } from "./baseProxyClient";\n\n';
content +=
"export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n";
content += methods
.map(({ name, method }) => {
return ` ${method}(data: ${name}Request) {
return this.request("${name}Request", ${name}Request, data, ${name}Response);
}\n`;
})
.join("");
content += "}\n";
await writeFile(TARGET, content);
@@ -1,13 +1,13 @@
import { VertexAttribute, vertexAttributeVectorSizes } from '@blitzkit/core';
import { VertexAttribute, vertexAttributeVectorSizes } from "@blitzkit/core";
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
{
[VertexAttribute.VERTEX]: 'POSITION',
[VertexAttribute.NORMAL]: 'NORMAL',
[VertexAttribute.TEXCOORD0]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD1]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD2]: 'TEXCOORD_0',
[VertexAttribute.TEXCOORD3]: 'TEXCOORD_0',
[VertexAttribute.VERTEX]: "POSITION",
[VertexAttribute.NORMAL]: "NORMAL",
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
};
export const vertexAttributeGltfVectorSizes = {
...vertexAttributeVectorSizes,
@@ -4,35 +4,35 @@ import {
ScgReadStream,
Textures,
VertexAttribute,
} from '@blitzkit/core';
import { Document, Material, Node, Scene } from '@gltf-transform/core';
import { dedup, prune } from '@gltf-transform/functions';
import { times } from 'lodash-es';
import { dirname } from 'path';
import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three';
import { readDVPLFile } from '../readDVPLFile';
import { readTexture } from '../readTexture';
import { TextureMutation } from '../readTexture/constants';
} from "@blitzkit/core";
import { Document, Material, Node, Scene } from "@gltf-transform/core";
import { dedup, prune } from "@gltf-transform/functions";
import { times } from "lodash-es";
import { dirname } from "path";
import { readBaseColor } from "../readBaseColor";
import { readDVPLFile } from "../readDVPLFile";
import { readNormal } from "../readNormal";
import { readRoughnessMetallic } from "../readRoughnessMetallic";
import {
vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes,
} from './constants';
} from "./constants";
const ERROR_ON_UNKNOWN_COMPONENT = false;
const omitMeshNames = {
start: ['chassis_chassis_', 'chassis_track_crash_', 'HP_'],
end: ['_POINT'],
start: ["chassis_chassis_", "chassis_track_crash_", "HP_"],
end: ["_POINT"],
};
export async function extractModel(data: string, path: string) {
const sc2Path = `${data}/3d/${path}.sc2`;
const scgPath = `${data}/3d/${path}.scg`;
const sc2 = new Sc2ReadStream(
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer,
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer
).sc2();
const scg = new ScgReadStream(
(await readDVPLFile(scgPath)).buffer as ArrayBuffer,
(await readDVPLFile(scgPath)).buffer as ArrayBuffer
).scg();
const document = new Document();
const scene = document.createScene();
@@ -41,8 +41,8 @@ export async function extractModel(data: string, path: string) {
// create materials
await Promise.all(
sc2['#dataNodes'].map(async (node) => {
const id = new DataView(node['#id']).getBigUint64(0, true);
sc2["#dataNodes"].map(async (node) => {
const id = new DataView(node["#id"]).getBigUint64(0, true);
if (node.parentMaterialKey !== undefined) {
/**
@@ -58,7 +58,7 @@ export async function extractModel(data: string, path: string) {
if (node.textures) {
textures = node.textures;
} else if (typeof node.configCount === 'number') {
} else if (typeof node.configCount === "number") {
textures = node.configArchive_0.textures;
}
@@ -66,26 +66,27 @@ export async function extractModel(data: string, path: string) {
material.setBaseColorTexture(
document
.createTexture(node.materialName)
.setMimeType('image/jpeg')
.setMimeType("image/jpeg")
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${textures.albedo}`,
TextureMutation.Albedo,
),
),
await readBaseColor(
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
textures.miscMap
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
: undefined
)
)
);
if (textures.baseRMMap) {
material.setMetallicRoughnessTexture(
document
.createTexture(node.materialName)
.setMimeType('image/jpeg')
.setMimeType("image/jpeg")
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`,
TextureMutation.RoughnessMetallicness,
),
),
await readRoughnessMetallic(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`
)
)
);
}
@@ -95,47 +96,33 @@ export async function extractModel(data: string, path: string) {
material.setNormalTexture(
document
.createTexture(node.materialName)
.setMimeType('image/jpeg')
.setMimeType("image/jpeg")
.setImage(
await readTexture(
await readNormal(
`${data}/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap
}`,
isBase ? TextureMutation.Normal : undefined,
),
),
);
}
if (textures.miscMap) {
material.setOcclusionTexture(
document
.createTexture(node.materialName)
.setMimeType('image/jpeg')
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
TextureMutation.Miscellaneous,
),
),
isBase
)
)
);
}
materials.set(
new DataView(node['#id']).getBigUint64(0, true),
material,
new DataView(node["#id"]).getBigUint64(0, true),
material
);
}
}),
})
);
// replace children materials with parents
materials.forEach((material, id) => {
if (typeof material !== 'bigint') return;
if (typeof material !== "bigint") return;
let resolvedMaterial: Material | bigint | undefined = material;
while (typeof resolvedMaterial === 'bigint') {
while (typeof resolvedMaterial === "bigint") {
const linkedParentMaterial = materials.get(resolvedMaterial);
resolvedMaterial = linkedParentMaterial;
@@ -155,36 +142,36 @@ export async function extractModel(data: string, path: string) {
const node = document.createNode(hierarchy.name);
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, '0')],
(index) => hierarchy.components[index.toString().padStart(4, "0")]
);
components.forEach((component) => {
switch (component['comp.typename']) {
case 'LodComponent':
switch (component["comp.typename"]) {
case "LodComponent":
// found and used later by transform component
// TODO: lod component always shows up before transform component; cache it before parsing transform
break;
case 'TransformComponent': {
node.setTranslation(component['tc.localTranslation']);
node.setRotation(component['tc.localRotation']);
node.setScale(component['tc.localScale']);
case "TransformComponent": {
node.setTranslation(component["tc.localTranslation"]);
node.setRotation(component["tc.localRotation"]);
node.setScale(component["tc.localScale"]);
break;
}
case 'RenderComponent': {
const renderObject = component['rc.renderObj'];
case "RenderComponent": {
const renderObject = component["rc.renderObj"];
times(renderObject['ro.batchCount'], (batchIndex): void => {
times(renderObject["ro.batchCount"], (batchIndex): void => {
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
if (lodIndex !== 0) return;
const batchKey = batchIndex.toString().padStart(4, '0');
const batch = renderObject['ro.batches'][batchKey];
const material = materials.get(batch['rb.nmatname']);
const polygonGroup = scg.get(batch['rb.datasource']);
const batchKey = batchIndex.toString().padStart(4, "0");
const batch = renderObject["ro.batches"][batchKey];
const material = materials.get(batch["rb.nmatname"]);
const polygonGroup = scg.get(batch["rb.datasource"]);
if (!(material instanceof Material)) {
// probably shadow material
@@ -192,14 +179,14 @@ export async function extractModel(data: string, path: string) {
}
if (polygonGroup === undefined) {
throw new Error(
`Missing polygon group ${batch['rb.datasource']}`,
`Missing polygon group ${batch["rb.datasource"]}`
);
}
const lodNode = document.createNode(batchKey);
const indicesAccessor = document
.createAccessor()
.setType('SCALAR')
.setType("SCALAR")
.setArray(new Uint16Array(polygonGroup.indices))
.setBuffer(buffer);
const primitive = document
@@ -233,7 +220,7 @@ export async function extractModel(data: string, path: string) {
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
const attributeAccessor = document
.createAccessor(name)
.setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`)
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
.setArray(new Float32Array(value.flat()))
.setBuffer(buffer);
@@ -241,7 +228,7 @@ export async function extractModel(data: string, path: string) {
});
const mesh = document
.createMesh(batch['##name'])
.createMesh(batch["##name"])
.addPrimitive(primitive);
lodNode.setMesh(mesh);
node.addChild(lodNode);
@@ -253,24 +240,24 @@ export async function extractModel(data: string, path: string) {
default: {
if (ERROR_ON_UNKNOWN_COMPONENT) {
throw new Error(
`Unhandled component type: ${component['comp.typename']}`,
`Unhandled component type: ${component["comp.typename"]}`
);
}
}
}
});
if (hierarchy['#hierarchy']) {
parseHierarchies(hierarchy['#hierarchy'], node);
if (hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy["#hierarchy"], node);
}
parent.addChild(node);
});
}
parseHierarchies(sc2['#hierarchy'], scene);
parseHierarchies(sc2["#hierarchy"], scene);
scene.addChild(document.createNode('test'));
scene.addChild(document.createNode("test"));
await document.transform(prune({ keepAttributes: true }), dedup());
@@ -0,0 +1,48 @@
import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readBaseColor(path: string, occlusionPath?: string) {
const baseRaw = await readTexture(path);
const occlusionRaw = occlusionPath
? await readTexture(occlusionPath)
: undefined;
const base = await sharp(baseRaw.data, { raw: baseRaw })
.removeAlpha()
.raw()
.toBuffer();
const occlusion = occlusionRaw
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
.extractChannel(3)
.raw()
.toBuffer()
: undefined;
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 3);
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
let c = 1;
if (occlusionRaw) {
const x = i % baseRaw.width;
const y = Math.floor(i / baseRaw.width);
const u = Math.floor(x * (occlusionRaw.width / baseRaw.width));
const v = Math.floor(y * (occlusionRaw.height / baseRaw.height));
const occlusionI = u + v * occlusionRaw.width;
c = occlusion![occlusionI] / 255;
}
combined[i * 3 + 0] = Math.round(base[i * 3 + 0] * c);
combined[i * 3 + 1] = Math.round(base[i * 3 + 1] * c);
combined[i * 3 + 2] = Math.round(base[i * 3 + 2] * c);
}
const image = await sharp(combined, {
raw: { width: baseRaw.width, height: baseRaw.height, channels: 3 },
})
.jpeg()
.toBuffer();
return image;
}
@@ -0,0 +1,29 @@
import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readNormal(path: string, isBase: boolean) {
const raw = await readTexture(path);
if (!isBase) {
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
}
const bytes = 4 * raw.width * raw.height;
for (let index = 0; index < bytes; index += 4) {
/**
* Red is always 255 and blue is always 0. Only alpha and green contain any
* sort of information.
*/
let x = raw.data[index + 3] * (2 / 255) - 1;
let y = raw.data[index + 1] * (2 / 255) - 1;
let z = Math.sqrt(Math.max(0, 1 - x ** 2 - y ** 2));
raw.data[index] = Math.round((x + 1) * (255 / 2));
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
raw.data[index + 3] = 255;
}
return await sharp(raw.data, { raw }).jpeg().toBuffer();
}
@@ -0,0 +1,24 @@
import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readRoughnessMetallic(path: string) {
const raw = await readTexture(path);
const blitz = sharp(raw.data, { raw: raw });
const metallicness = await blitz?.extractChannel(1).raw().toBuffer();
const roughness = await blitz?.extractChannel(3).raw().toBuffer();
const combined = Buffer.alloc(raw.width * raw.height * 3);
for (let i = 0; i < raw.width * raw.height; i++) {
combined[i * 3 + 1] = roughness[i];
combined[i * 3 + 2] = metallicness[i];
}
const image = await sharp(combined, {
raw: { width: raw.width, height: raw.height, channels: 3 },
})
.jpeg()
.toBuffer();
return image;
}
@@ -1,81 +1,83 @@
import { DdsReadStream, PvrReadStream } from '@blitzkit/core';
import { existsSync } from 'fs';
import sharp from 'sharp';
import { DATA_COMPRESSED } from '../../../buildAssets/constants';
import { readDVPLFile } from '../readDVPLFile';
import { TextureMutation } from './constants';
import { DdsReadStream, PvrReadStream } from "@blitzkit/core";
import { existsSync } from "fs";
import sharp from "sharp";
import { DATA_COMPRESSED } from "../../../buildAssets/constants";
import { readDVPLFile } from "../readDVPLFile";
import { TextureMutation } from "./constants";
export async function readTexture(path: string, mutation?: TextureMutation) {
const ddsTexturePath = path.replace('.tex', '.dx11.dds');
export async function readTexture(path: string) {
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
const isDds = existsSync(
`${ddsTexturePath}${DATA_COMPRESSED ? '.dvpl' : ''}`,
`${ddsTexturePath}${DATA_COMPRESSED ? ".dvpl" : ""}`
);
const resolvedTexturePath = isDds
? ddsTexturePath
: ddsTexturePath.replace('.dds', '.pvr');
: ddsTexturePath.replace(".dds", ".pvr");
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`);
const raw = isDds
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds()
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr();
switch (mutation) {
case TextureMutation.Normal: {
const bytes = 4 * raw.width * raw.height;
for (let index = 0; index < bytes; index += 4) {
/**
* Red is always 255 and blue is always 0. Only alpha and green contain any
* sort of information.
*/
let x = raw.data[index + 3] * (2 / 255) - 1;
let y = raw.data[index + 1] * (2 / 255) - 1;
const underSqrt = 1 - x ** 2 - y ** 2;
let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
return raw;
raw.data[index] = Math.round((x + 1) * (255 / 2));
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
raw.data[index + 3] = 255;
}
// switch (mutation) {
// case TextureMutation.Normal: {
// const bytes = 4 * raw.width * raw.height;
// for (let index = 0; index < bytes; index += 4) {
// /**
// * Red is always 255 and blue is always 0. Only alpha and green contain any
// * sort of information.
// */
// let x = raw.data[index + 3] * (2 / 255) - 1;
// let y = raw.data[index + 1] * (2 / 255) - 1;
// const underSqrt = 1 - x ** 2 - y ** 2;
// let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
return await sharp(raw.data, { raw }).jpeg().toBuffer();
}
// raw.data[index] = Math.round((x + 1) * (255 / 2));
// raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
// raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
// raw.data[index + 3] = 255;
// }
case TextureMutation.RoughnessMetallicness: {
const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
raw: { ...raw, channels: 1 },
});
const metallicness = sharp(raw.data, { raw })
.extractChannel('green')
.jpeg()
.toBuffer();
const roughness = sharp(raw.data, { raw })
.extractChannel('alpha')
.jpeg()
.toBuffer();
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
// }
return await newImage
.joinChannel(await Promise.all([roughness, metallicness]))
.jpeg()
.toBuffer();
}
// case TextureMutation.RoughnessMetallicness: {
// const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
// raw: { ...raw, channels: 1 },
// });
// const metallicness = sharp(raw.data, { raw })
// .extractChannel("green")
// .jpeg()
// .toBuffer();
// const roughness = sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
case TextureMutation.Miscellaneous: {
/**
* Alpha is ambient occlusion and green is emissive. But very few tanks
* use emissive so I am ignoring it for now.
*/
return await sharp(raw.data, { raw })
.extractChannel('alpha')
.jpeg()
.toBuffer();
}
// return await newImage
// .joinChannel(await Promise.all([roughness, metallicness]))
// .jpeg()
// .toBuffer();
// }
case TextureMutation.Albedo: {
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
}
// case TextureMutation.Miscellaneous: {
// /**
// * Alpha is ambient occlusion and green is emissive. But very few tanks
// * use emissive so I am ignoring it for now.
// */
// return await sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
// }
default:
return await sharp(raw.data, { raw }).jpeg().toBuffer();
}
// case TextureMutation.Albedo: {
// return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
// }
// default:
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
// }
}
+50 -36
View File
@@ -1,47 +1,61 @@
import { exec as execSync } from 'child_process';
import { existsSync } from 'fs';
import { readdir, rm, writeFile } from 'fs/promises';
import { promisify } from 'util';
import { exec as execSync } from "child_process";
import { existsSync } from "fs";
import { readdir, rm } from "fs/promises";
import { promisify } from "util";
const ROOT = 'packages/core/src/protos';
const MAX_COMMAND_LENGTH = 2 ** 11;
const roots = [
// "packages/core/src/protos",
"submodules/blitzkit-closed/protos",
];
const exec = promisify(execSync);
let executableFileExtension: string;
if (existsSync('node_modules/.bin/protoc-gen-ts_proto.exe')) {
executableFileExtension = '.exe';
if (existsSync("node_modules/.bin/protoc-gen-ts_proto.exe")) {
executableFileExtension = ".exe";
} else {
executableFileExtension = '';
executableFileExtension = "";
}
const filesRaw = await readdir(`${ROOT}`);
await Promise.all(
filesRaw.map(async (file) => {
if (!file.endsWith('.proto')) {
await rm(`${ROOT}/${file}`);
for (const root of roots) {
const filesRaw = await readdir(`${root}`);
const files: string[] = [];
for (const file of filesRaw) {
if (file.endsWith(".proto")) files.push(file);
if (file.endsWith(".ts")) await rm(`${root}/${file}`);
}
while (files.length > 0) {
let command = [
"protoc",
`--plugin=./node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`,
"--ts_proto_opt=esModuleInterop=true",
"--ts_proto_opt=oneof=unions-value",
// "--ts_proto_opt=removeEnumPrefix=true",
"--ts_proto_opt=unrecognizedEnum=false",
"--ts_proto_opt=snakeToCamel=false",
`--ts_proto_out=${root}`,
`-I=${root}`,
].join(" ");
while (true) {
if (files.length === 0) break;
const file = files.at(-1);
const newLine = ` ${root}/${file}`;
if (command.length + newLine.length < MAX_COMMAND_LENGTH) {
command += newLine;
files.pop();
} else {
break;
}
}
}),
);
const files = filesRaw.filter((file) => file.endsWith('.proto'));
const args = [
`--plugin=./node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`,
'--ts_proto_opt=esModuleInterop=true',
'--ts_proto_opt=oneof=unions-value',
'--ts_proto_opt=removeEnumPrefix=true',
'--ts_proto_opt=unrecognizedEnum=false',
'--ts_proto_opt=snakeToCamel=false',
'--ts_proto_out=.',
...files.map((file) => `${ROOT}/${file}`),
];
await exec(`protoc ${args.join(' ')}`);
await writeFile(
`${ROOT}/index.ts`,
'// @ts-nocheck\n' +
files
.map((file) => file.replace('.proto', ''))
.map((file) => `export * from './${file}.ts';`)
.join('\n'),
);
await exec(command);
}
}
+12 -15
View File
@@ -1,29 +1,26 @@
import {
DEFAULT_LOCALE,
SUPPORTED_LOCALES,
TranslationTree,
} from '@blitzkit/i18n';
import { writeFile } from 'fs/promises';
import { type TranslationTree } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import { writeFile } from "fs/promises";
const ROOT = '@blitzkit/i18n/strings';
const WORKING_ROOT = '../../packages/i18n/strings';
const ROOT = "@blitzkit/i18n/strings";
const WORKING_ROOT = "../../packages/i18n/strings";
const defaultStrings = require(`${ROOT}/${DEFAULT_LOCALE}.json`).default;
const nonDefaultLocales = SUPPORTED_LOCALES.filter(
(locale) => locale !== DEFAULT_LOCALE,
const defaultStrings = require(`${ROOT}/${locales.default}.json`).default;
const nonDefaultLocales = locales.supported.filter(
({ locale }) => locale !== locales.default
);
function prune(defaultStrings: TranslationTree, strings: TranslationTree) {
for (const key in strings) {
if (key in defaultStrings) {
if (typeof defaultStrings[key] === typeof strings[key]) {
if (typeof strings[key] === 'string') {
if (typeof strings[key] === "string") {
if (
// are equivalent regardless of %s flavor
(defaultStrings[key] as string).replaceAll(/%s\d/g, '%s') ===
(strings[key] as string).replaceAll(/%s\d/g, '%s') ||
(defaultStrings[key] as string).replaceAll(/%s\d/g, "%s") ===
(strings[key] as string).replaceAll(/%s\d/g, "%s") ||
// string is empty
strings[key] === '' ||
strings[key] === "" ||
// string has old flavor of %s
strings[key].match(/%s([^\d]|$)/g) !== null
) {
+8
View File
@@ -0,0 +1,8 @@
{
"name": "varuna",
"type": "module",
"main": "src/index.ts",
"dependencies": {
"react": "^19.1.1"
}
}
+122
View File
@@ -0,0 +1,122 @@
import { produce } from "immer";
import { debounce, merge } from "lodash-es";
import { useEffect, useRef, useState } from "react";
export class Varuna<Type, Arguments = void> {
private listeners = new Set<(state: Type) => void>();
private initialized = false;
private _initial?: Type;
private _state?: Type;
constructor(
private creator: Type | ((...args: Arguments[]) => Type),
private persistence?: string
) {
if (typeof creator !== "function") this.initialize(creator);
}
private initialize(initial: Type) {
let data = initial;
if (this.persistence && typeof localStorage !== "undefined") {
const dehydrated = localStorage.getItem(this.persistence);
if (dehydrated) {
const rehydrated = JSON.parse(dehydrated) as unknown;
data = merge(data, rehydrated);
}
window.addEventListener("beforeunload", this.store.bind(this));
}
this.initialized = true;
this._initial = data;
this._state = data;
}
useInitialization(...args: Arguments[]) {
const initializedThisMount = useRef(false);
if (initializedThisMount.current) return;
if (typeof this.creator !== "function") {
throw new Error("Provider must be used with a function creator");
}
this.initialize((this.creator as (...args: Arguments[]) => Type)(...args));
initializedThisMount.current = true;
}
private dispatch() {
const state = this.state;
this.listeners.forEach((callback) => callback(state));
}
on<Slice>(slicer: (state: Type) => Slice, callback: (state: Slice) => void) {
let last = slicer(this.state);
function listener(state: Type) {
const next = slicer(state);
if (next !== last) {
callback(next);
last = next;
}
}
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
mutate(mutator: (state: Type) => void) {
this.set(produce(this.state, mutator));
}
private assertInitialized() {
if (!this.initialized) {
throw new Error("Varuna store used before initialization");
}
}
get state() {
this.assertInitialized();
return this._state as Type;
}
set(state: Type) {
this.assertInitialized();
this._state = state;
this.dispatch();
this.storeDeferred();
}
get initial() {
this.assertInitialized();
return this._initial as Type;
}
use<Slice = Type>(
slicer: (state: Type) => Slice = (state) => state as unknown as Slice
) {
return this.useDeferred(slicer, slicer(this.state));
}
useDeferred<Slice = Type>(slicer: (state: Type) => Slice, initial: Slice) {
const instance = this;
const [slice, setSlice] = useState(initial);
useEffect(() => instance.on(slicer, setSlice), []);
return slice;
}
store() {
localStorage.setItem(this.persistence!, JSON.stringify(this.state));
}
storeDeferred = debounce(this.store.bind(this), 500);
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
-24
View File
@@ -1,24 +0,0 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
.vercel/
-35
View File
@@ -1,35 +0,0 @@
// @ts-check
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: [...SUPPORTED_LOCALES],
routing: {
prefixDefaultLocale: false,
},
},
devToolbar: { enabled: false },
output: 'static',
site: 'https://blitzkit.app',
outDir: '../../dist/website',
prefetch: true,
integrations: [react(), sitemap()],
vite: {
resolve: {
alias: {
'.prisma/client/index-browser':
'../../node_modules/.prisma/client/index-browser.js',
},
},
ssr: {
noExternal: [/^d3.*$/, /^@nivo.*$/],
},
},
});
+114
View File
@@ -0,0 +1,114 @@
import node from "@astrojs/node";
import react from "@astrojs/react";
import { wrapper } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import strings from "@blitzkit/i18n/strings/en.json";
import { mauve, mauveDark } from "@radix-ui/colors";
import AstroPWA from "@vite-pwa/astro";
import favicons from "astro-favicons";
import { defineConfig } from "astro/config";
import packageJSON from "../../package.json";
import { tools } from "./src/constants/tools";
const POSSIBLE_TARGETS = ["app", "web"];
const target = process.env.PUBLIC_TARGET;
if (!target || !POSSIBLE_TARGETS.includes(target)) {
throw new Error("Invalid target");
}
export default defineConfig({
devToolbar: { enabled: false },
vite: { server: { allowedHosts: [] } },
output: target === "web" ? "static" : "server",
site: "https://blitzkit.app",
outDir: "../../dist/website",
prefetch: { defaultStrategy: "hover", prefetchAll: true },
build: { concurrency: 4 },
adapter: target === "app" ? node({ mode: "standalone" }) : undefined,
integrations: [
favicons({
name: strings.common.name,
short_name: strings.common.name,
background: mauveDark.mauve1,
themes: [mauve.mauve1, mauveDark.mauve1],
appleStatusBarStyle: "black-translucent",
version: packageJSON.version,
shortcuts: Object.values(tools)
.filter((tool) => !tool.disabled && !tool.href)
.map((tool) => {
const stringId = (tool.strings ??
tool.id) as keyof typeof strings.website.tools;
return {
name: strings.website.tools[stringId].name,
name_localized: wrapper(
(strings) => strings.website.tools[stringId].name
),
description: strings.website.tools[stringId].description,
description_localized: wrapper(
(strings) => strings.website.tools[stringId].description
),
short_name: strings.website.tools[stringId].name,
short_name_localized: wrapper(
(strings) => strings.website.tools[stringId].name
),
url: `/${tool.id}`,
icon: `public/assets/images/tools/${tool.id}.webp`,
};
}),
manifest: {
description: strings.website.home.seo_description,
description_localized: wrapper(
(strings) => strings.website.home.seo_description
),
dir: "ltr",
categories: ["utilities", "games", "developer", "developer tools"],
id: "com.tresabhi.blitzkit",
start_url: "/",
launch_handler: { client_mode: "navigate-new" },
display: "fullscreen",
display_override: ["fullscreen", "standalone", "minimal-ui"],
lang: locales.default,
orientation: "any",
},
icons: {
windows: true,
android: true,
appleIcon: true,
appleStartup: true,
favicons: true,
yandex: true,
},
}),
AstroPWA({
manifest: false,
devOptions: { enabled: true },
workbox: {
globPatterns: ["**/*.{html,css,js,png,webp,svg,xml,json}"],
runtimeCaching: [
{
urlPattern: ({ url }) =>
url.href.startsWith(import.meta.env.PUBLIC_ASSET_BASE),
handler: "NetworkFirst",
},
],
},
}),
react(),
],
});
+30 -43
View File
@@ -2,53 +2,40 @@
"name": "@blitzkit/website",
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"dev": "PUBLIC_TARGET=web astro dev",
"build": "PUBLIC_TARGET=web astro build",
"preview": "PUBLIC_TARGET=web astro preview",
"lint": "astro check",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrolib/seo": "^1.0.0-beta.8",
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@fontsource/inter": "^5.2.5",
"@nanostores/persistent": "^0.10.2",
"@nanostores/react": "^0.7.3",
"@nivo/bar": "^0.87.0",
"@nivo/line": "^0.88.0",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^3.1.4",
"@react-three/drei": "^9.115.0",
"@react-three/fiber": "^8.17.10",
"compare-versions": "^6.1.1",
"echarts": "^5.5.1",
"immer": "^10.1.1",
"js-cookie": "^3.0.5",
"markdown-to-jsx": "^7.5.0",
"nanostores": "^0.11.3",
"quicklime": "^1.2.0",
"react": "^18.3.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.3.1",
"react-promise-suspense": "^0.3.4",
"three": "^0.169.0",
"urlon": "^3.1.0",
"zustand": "^4.5.5"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/react": "^3.6.2",
"@astrojs/sitemap": "^3.2.1",
"@playform/compress": "^0.1.9",
"@playform/inline": "^0.1.2",
"@types/js-cookie": "^3.0.6",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/three": "^0.160.0",
"astro": "^4.16.7",
"astro-compressor": "^1.0.0"
"@astrojs/node": "^9.4.4",
"@astrojs/react": "^4.4.0",
"@astrolib/seo": "^1.0.0-beta.8",
"@fontsource/inter": "^5.2.8",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/themes": "^3.2.1",
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.3.0",
"@types/react": "^19.1.14",
"@types/react-dom": "^19.1.9",
"@types/three": "^0.180.0",
"@vite-pwa/astro": "^1.1.0",
"astro": "^5.14.1",
"astro-favicons": "^3.1.5",
"compare-versions": "^6.1.1",
"fuzzysort": "^3.1.0",
"immer": "^10.1.3",
"markdown-to-jsx": "^7.7.13",
"pwa-install-handler": "^2.6.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-promise-suspense": "^0.3.4",
"three": "^0.180.0",
"three-stdlib": "^2.36.0",
"typescript": "^5.9.2",
"urlon": "^3.1.0"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

+46
View File
@@ -0,0 +1,46 @@
<svg width="426" height="426" viewBox="0 0 426 426" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M416.628 244.997L235.625 372.974C223.13 381.809 202.87 381.809 190.375 372.974L9.37173 244.997C3.1239 240.58 -3.95076e-06 234.79 0 229V196.6H426V229C426 234.79 422.876 240.58 416.628 244.997Z" fill="#0D2847"/>
<path d="M416.628 180.603L235.625 52.6262C223.13 43.7913 202.87 43.7913 190.375 52.6262L9.37172 180.603C-3.12391 189.438 -3.12391 203.762 9.37173 212.597L190.375 340.574C202.87 349.409 223.13 349.409 235.625 340.574L416.628 212.597C429.124 203.762 429.124 189.438 416.628 180.603Z" fill="#D9D9D9"/>
<path d="M416.628 180.603L235.625 52.6262C223.13 43.7913 202.87 43.7913 190.375 52.6262L9.37172 180.603C-3.12391 189.438 -3.12391 203.762 9.37173 212.597L190.375 340.574C202.87 349.409 223.13 349.409 235.625 340.574L416.628 212.597C429.124 203.762 429.124 189.438 416.628 180.603Z" fill="url(#paint0_linear_764_240)"/>
<path d="M416.628 180.603L235.625 52.6262C223.13 43.7913 202.87 43.7913 190.375 52.6262L9.37172 180.603C-3.12391 189.438 -3.12391 203.762 9.37173 212.597L190.375 340.574C202.87 349.409 223.13 349.409 235.625 340.574L416.628 212.597C429.124 203.762 429.124 189.438 416.628 180.603Z" fill="url(#paint1_linear_764_240)"/>
<g filter="url(#filter0_d_764_240)">
<path d="M197.056 150.108L201.368 153.051L290.596 213.945L306.6 202.599L310.832 199.599L289.153 184.23L288.86 177.31L335.236 182.298L351.706 170.622L359.102 165.378L288.049 158.111L286.355 118L263.968 133.871L265.867 167.722L217.292 135.762L197.056 150.108Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M192.905 289L74.2242 204.714L70 201.714L116.393 168.767C129.906 159.17 143.124 153.539 156.048 151.875C168.971 150.211 180.919 153.275 191.893 161.069C198.702 165.905 202.718 171.006 203.941 176.372C205.165 181.658 204.302 186.506 201.354 190.918L201.692 191.158C208.978 188.301 216.935 187.286 225.563 188.113C234.247 188.9 242.613 192.151 250.66 197.866C261.803 205.779 266.398 214.825 264.446 225.004C262.551 235.142 254.96 244.929 241.672 254.366L192.905 289ZM221.999 242.08L200.456 257.38L163.905 231.422L185.448 216.123C192.007 211.465 198.473 209.23 204.846 209.42C211.22 209.529 217.193 211.562 222.764 215.519C228.392 219.516 231.31 223.797 231.521 228.364C231.732 232.85 228.558 237.422 221.999 242.08ZM165.937 205.64L146.77 219.252L114.693 196.472L133.861 182.859C140.363 178.241 146.435 175.887 152.076 175.798C157.717 175.708 163.07 177.462 168.135 181.059C173.256 184.696 175.809 188.557 175.796 192.643C175.726 196.69 172.44 201.022 165.937 205.64Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
<path d="M197.056 156.108L290.596 219.945L310.832 205.599L306.6 202.599L289.153 190.23L288.86 183.31L335.236 188.298L359.102 171.378L351.706 170.622L288.049 164.111L286.355 124L263.968 139.871L265.867 173.722L217.292 141.762L201.368 153.051L197.056 156.108Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M192.905 295L70 207.714L74.2242 204.714L116.393 174.767C129.906 165.17 143.124 159.539 156.048 157.875C168.971 156.211 180.919 159.275 191.893 167.069C198.702 171.905 202.718 177.006 203.941 182.372C205.165 187.658 204.302 192.506 201.354 196.918L201.692 197.158C208.978 194.301 216.935 193.286 225.563 194.113C234.247 194.9 242.613 198.151 250.66 203.866C261.803 211.779 266.398 220.825 264.446 231.004C262.551 241.142 254.96 250.929 241.672 260.366L192.905 295ZM221.999 248.08L200.456 263.38L163.905 237.422L185.448 222.123C192.007 217.465 198.473 215.23 204.846 215.42C211.22 215.529 217.193 217.562 222.764 221.519C228.392 225.516 231.31 229.797 231.521 234.364C231.732 238.85 228.558 243.422 221.999 248.08ZM165.937 211.64L146.77 225.252L114.693 202.472L133.861 188.859C140.363 184.241 146.435 181.887 152.076 181.798C157.717 181.708 163.07 183.462 168.135 187.059C173.256 190.696 175.809 194.557 175.796 198.643C175.726 202.69 172.44 207.022 165.937 211.64Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
<path d="M310.832 199.599L306.6 202.599L310.832 205.599V199.599Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
<path d="M359.102 165.378L351.706 170.622L359.102 171.378V165.378Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
<path d="M201.368 153.051L197.056 150.108V156.108L201.368 153.051Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
<path d="M74.2242 204.714L70 201.714V207.714L74.2242 204.714Z" fill="#111927" fill-opacity="0.5" shape-rendering="crispEdges"/>
</g>
<path d="M197.056 150.108L290.596 213.945L310.832 199.599L289.153 184.23L288.86 177.31L335.236 182.298L359.102 165.378L288.049 158.111L286.355 118L263.968 133.871L265.867 167.722L217.292 135.762L197.056 150.108Z" fill="url(#paint2_linear_764_240)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M192.905 289L70 201.714L116.393 168.767C129.906 159.17 143.124 153.539 156.048 151.875C168.971 150.211 180.919 153.275 191.893 161.069C198.702 165.905 202.718 171.006 203.941 176.372C205.165 181.658 204.302 186.506 201.354 190.918L201.692 191.158C208.978 188.301 216.935 187.286 225.563 188.113C234.247 188.9 242.613 192.151 250.66 197.866C261.803 205.779 266.398 214.825 264.446 225.004C262.551 235.142 254.96 244.929 241.672 254.366L192.905 289ZM221.999 242.08L200.456 257.38L163.905 231.422L185.448 216.123C192.007 211.465 198.473 209.23 204.846 209.42C211.22 209.529 217.193 211.562 222.764 215.519C228.392 219.516 231.31 223.797 231.521 228.364C231.732 232.85 228.558 237.422 221.999 242.08ZM165.937 205.64L146.77 219.252L114.693 196.472L133.861 182.859C140.363 178.241 146.435 175.887 152.076 175.798C157.717 175.708 163.07 177.462 168.135 181.059C173.256 184.696 175.809 188.557 175.796 192.643C175.726 196.69 172.44 201.022 165.937 205.64Z" fill="url(#paint3_linear_764_240)"/>
<defs>
<filter id="filter0_d_764_240" x="66" y="116" width="297.103" height="185" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_764_240"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_764_240" result="shape"/>
</filter>
<linearGradient id="paint0_linear_764_240" x1="213" y1="46" x2="213" y2="347.2" gradientUnits="userSpaceOnUse">
<stop stop-color="#10243E"/>
<stop offset="1" stop-color="#0F1B2D"/>
</linearGradient>
<linearGradient id="paint1_linear_764_240" x1="127.008" y1="96.51" x2="241.48" y2="334.974" gradientUnits="userSpaceOnUse">
<stop stop-color="#262A65"/>
<stop offset="1" stop-color="#104D87"/>
</linearGradient>
<linearGradient id="paint2_linear_764_240" x1="215" y1="98.5" x2="299.5" y2="210.5" gradientUnits="userSpaceOnUse">
<stop offset="0.379808" stop-color="#C2E6FF"/>
<stop offset="0.706731" stop-color="#70B8FF"/>
</linearGradient>
<linearGradient id="paint3_linear_764_240" x1="215" y1="98.5" x2="299.5" y2="210.5" gradientUnits="userSpaceOnUse">
<stop offset="0.379808" stop-color="#C2E6FF"/>
<stop offset="0.706731" stop-color="#70B8FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

@@ -1,4 +1,4 @@
import { ReloadIcon } from '@radix-ui/react-icons';
import { ReloadIcon } from "@radix-ui/react-icons";
import {
Card,
Code,
@@ -6,26 +6,21 @@ import {
IconButton,
Text,
TextField,
} from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { useRef } from 'react';
import { radToDeg } from 'three/src/math/MathUtils.js';
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
import { App } from '../../../stores/app';
import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { layerTypeNames } from './ShotDisplayCard';
import { ArmorType } from './SpacedArmorScene';
} from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { useRef } from "react";
import { radToDeg } from "three/src/math/MathUtils.js";
import { resolveArmorIndex } from "../../../core/blitzkit/resolveArmorIndex";
import { App } from "../../../stores/app";
import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
import { layerTypeNames } from "./ShotDisplayCard";
import { ArmorType } from "./SpacedArmorScene";
export function ArmorPlateDisplay() {
const highlightArmor = TankopediaEphemeral.use(
(state) => state.highlightArmor,
);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
const developerMode = App.useDeferred((state) => state.developerMode, false);
const duelStore = Duel.useStore();
const input = useRef<HTMLInputElement>(null);
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
if (highlightArmor === undefined) return null;
@@ -33,20 +28,20 @@ export function ArmorPlateDisplay() {
<group position={highlightArmor.point}>
<Html
center
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
>
<Card
variant="classic"
mb="4"
style={{
whiteSpace: 'nowrap',
whiteSpace: "nowrap",
color: highlightArmor.color,
transform: 'translateY(-50%)',
transform: "translateY(-50%)",
}}
>
<Flex direction="column">
<Text weight="bold">
{layerTypeNames[highlightArmor.type]}{' '}
{layerTypeNames[highlightArmor.type]}{" "}
{highlightArmor.thickness.toFixed(0)}
<Text size="1" weight="regular">
mm
@@ -92,34 +87,34 @@ export function ArmorPlateDisplay() {
if (isNaN(thickness) || thickness < 0) return;
if (event.key === 'Enter') {
if (event.key === "Enter") {
input.current?.blur();
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
const { protagonist } = duelStore.getState();
const { protagonist } = Duel.state;
const tank = draft.model;
const turret = tank.turrets[protagonist.turret.id];
const gun = turret.guns[protagonist.gun.id];
const track = tank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith('hull_')) {
if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith('turret_')) {
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith('gun_')) {
if (highlightArmor.name.includes('_armor_')) {
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] = thickness;
} else {
gun.thickness = thickness;
}
} else if (highlightArmor.name.startsWith('chassis_')) {
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = thickness;
}
});
} else if (event.key === 'Escape') {
} else if (event.key === "Escape") {
input.current?.blur();
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}
@@ -137,7 +132,7 @@ export function ArmorPlateDisplay() {
mt="2"
color="red"
onClick={() => {
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
if (!highlightArmor.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name);
@@ -145,9 +140,8 @@ export function ArmorPlateDisplay() {
if (index === undefined) return;
draft.highlightArmor = undefined;
const initialState =
tankopediaEphemeralStore.getInitialState();
const { protagonist } = duelStore.getState();
const initialState = Tankopedia.initial;
const { protagonist } = Duel.state;
const tank = draft.model;
const initialTank = initialState.model;
const turret = tank.turrets[protagonist.turret.id];
@@ -159,20 +153,20 @@ export function ArmorPlateDisplay() {
const initialTrack =
initialTank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith('hull_')) {
if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] =
initialTank.armor.thickness[index];
} else if (highlightArmor.name.startsWith('turret_')) {
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] =
initialTurret.armor.thickness[index];
} else if (highlightArmor.name.startsWith('gun_')) {
if (highlightArmor.name.includes('_armor_')) {
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] =
initialGun.armor.thickness[index];
} else {
gun.thickness = initialGun.thickness;
}
} else if (highlightArmor.name.startsWith('chassis_')) {
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = initialTrack.thickness;
}
});
@@ -1,7 +1,8 @@
import type { GroupProps } from '@react-three/fiber';
import { forwardRef } from 'react';
import { Group } from 'three';
import { forwardRef, type ComponentProps } from "react";
import { Group } from "three";
export const ModelTankWrapper = forwardRef<Group, GroupProps>((props, ref) => {
return <group rotation={[-Math.PI / 2, 0, 0]} ref={ref} {...props} />;
});
export const ModelTankWrapper = forwardRef<Group, ComponentProps<"group">>(
(props, ref) => {
return <group rotation={[-Math.PI / 2, 0, 0]} ref={ref} {...props} />;
}
);
@@ -1,20 +1,19 @@
import { memo, useRef } from 'react';
import { Group } from 'three';
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
import { useArmor } from '../../../hooks/useArmor';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import { Duel } from '../../../stores/duel';
import { ModelTankWrapper } from './ModelTankWrapper';
import { PrimaryArmorSceneComponent } from './PrimaryArmorSceneComponent';
import { memo, useRef } from "react";
import { Group } from "three";
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
import { useArmor } from "../../../hooks/useArmor";
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
import { useTankTransform } from "../../../hooks/useTankTransform";
import { Duel } from "../../../stores/duel";
import { ModelTankWrapper } from "./ModelTankWrapper";
import { PrimaryArmorSceneComponent } from "./PrimaryArmorSceneComponent";
export const PrimaryArmorScene = memo(() => {
const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
// const protagonist = Duel.use((draft) => draft.protagonist);
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
@@ -29,7 +28,7 @@ export const PrimaryArmorScene = memo(() => {
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73),
state.protagonist.consumables.includes(73)
);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -38,20 +37,20 @@ export const PrimaryArmorScene = memo(() => {
<ModelTankWrapper ref={wrapper}>
<group position={hullOrigin}>
{armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_');
const isHull = node.name.startsWith("hull_");
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId,
armorId
);
if (
!isVisible ||
spaced ||
thickness === undefined ||
(isDynamicArmorActive && node.name.includes('state_01')) ||
(!isDynamicArmorActive && node.name.includes('state_00'))
(isDynamicArmorActive && node.name.includes("state_01")) ||
(!isDynamicArmorActive && node.name.includes("state_00"))
)
return null;
@@ -68,21 +67,21 @@ export const PrimaryArmorScene = memo(() => {
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`,
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId,
armorId
);
if (
!isVisible ||
spaced ||
thickness === undefined ||
(isDynamicArmorActive && node.name.includes('state_01')) ||
(!isDynamicArmorActive && node.name.includes('state_00'))
(isDynamicArmorActive && node.name.includes("state_01")) ||
(!isDynamicArmorActive && node.name.includes("state_00"))
)
return null;
@@ -102,21 +101,21 @@ export const PrimaryArmorScene = memo(() => {
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`,
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId,
armorId
);
if (
!isVisible ||
spaced ||
thickness === undefined ||
(isDynamicArmorActive && node.name.includes('state_01')) ||
(!isDynamicArmorActive && node.name.includes('state_00'))
(isDynamicArmorActive && node.name.includes("state_01")) ||
(!isDynamicArmorActive && node.name.includes("state_00"))
)
return null;
@@ -2,9 +2,9 @@ import {
canSplash,
isExplosive,
resolvePenetrationCoefficient,
} from '@blitzkit/core';
import { invalidate, useFrame } from '@react-three/fiber';
import { useEffect } from 'react';
} from "@blitzkit/core";
import { invalidate, useFrame } from "@react-three/fiber";
import { useEffect } from "react";
import {
MeshBasicMaterial,
NormalBlending,
@@ -14,16 +14,16 @@ import {
UniformsLib,
UniformsUtils,
Vector2,
} from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
import { Duel, type EquipmentMatrix } from '../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl?raw';
import { spacedArmorRenderTarget } from './target';
} from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw";
import { spacedArmorRenderTarget } from "./target";
interface PrimaryArmorSceneComponentProps {
thickness: number;
@@ -41,9 +41,6 @@ export function PrimaryArmorSceneComponent({
node,
thickness,
}: PrimaryArmorSceneComponentProps) {
const tankopediaPersistentStore = TankopediaPersistent.useStore();
const duelStore = Duel.useStore();
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const material = new ShaderMaterial({
fragmentShader,
vertexShader,
@@ -80,30 +77,30 @@ export function PrimaryArmorSceneComponent({
});
useEffect(() => {
async function handleShellChange() {
const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
function handleShellChange() {
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!,
isExplosive(shell.type) ? 90 : shell.ricochet!
);
material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0,
shell.normalization ?? 0
);
material.uniforms.isExplosive.value = isExplosive(shell.type);
material.uniforms.canSplash.value = canSplash(shell.type);
material.uniforms.damage.value = shell.armor_damage;
material.uniforms.explosionRadius.value = shell.explosion_radius;
await handleProtagonistEquipmentChange(
duel.protagonist.equipmentMatrix,
true,
handleProtagonistEquipmentChange(
Duel.state.protagonist.equipmentMatrix,
true
);
await handleAntagonistEquipmentChange(
duel.antagonist.equipmentMatrix,
true,
handleAntagonistEquipmentChange(
Duel.state.antagonist.equipmentMatrix,
true
);
invalidate();
@@ -121,15 +118,14 @@ export function PrimaryArmorSceneComponent({
function handleWireframeChange(wireframe: boolean) {
material.wireframe = wireframe;
}
async function handleProtagonistEquipmentChange(
function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix,
noInvalidate = false,
noInvalidate = false
) {
const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment(
const hasEnhancedArmor = hasEquipment(
110,
duel.protagonist.tank.equipment_preset,
equipment,
Duel.state.protagonist.tank.equipment_preset,
equipment
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03
@@ -137,18 +133,18 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate();
}
async function handleAntagonistEquipmentChange(
function handleAntagonistEquipmentChange(
equipment: EquipmentMatrix,
noInvalidate = false,
noInvalidate = false
) {
const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near;
const hasCalibratedShells = await hasEquipment(
const hasCalibratedShells = hasEquipment(
103,
duel.antagonist.tank.equipment_preset,
equipment,
Duel.state.antagonist.tank.equipment_preset,
equipment
);
material.uniforms.penetration.value =
@@ -158,45 +154,35 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate();
}
const initialState = tankopediaPersistentStore.getState();
handleShellChange();
handleGreenPenetrationChange(initialState.greenPenetration);
handleAdvancedHighlightingChange(initialState.advancedHighlighting);
handleOpaqueChange(initialState.opaque);
handleWireframeChange(initialState.wireframe);
handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix,
);
handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix,
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange(
TankopediaPersistent.state.advancedHighlighting
);
handleOpaqueChange(TankopediaPersistent.state.opaque);
handleWireframeChange(TankopediaPersistent.state.wireframe);
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
tankopediaEphemeralStore.subscribe(
(state) => state.customShell,
handleShellChange,
),
tankopediaPersistentStore.subscribe(
Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange),
TankopediaPersistent.on(
(state) => state.greenPenetration,
handleGreenPenetrationChange,
handleGreenPenetrationChange
),
tankopediaPersistentStore.subscribe(
TankopediaPersistent.on(
(state) => state.advancedHighlighting,
handleAdvancedHighlightingChange,
handleAdvancedHighlightingChange
),
tankopediaPersistentStore.subscribe(
(state) => state.opaque,
handleOpaqueChange,
),
duelStore.subscribe(
TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange),
Duel.on(
(state) => state.protagonist.equipmentMatrix,
(equipment) => handleProtagonistEquipmentChange(equipment),
(equipment) => handleProtagonistEquipmentChange(equipment)
),
duelStore.subscribe(
Duel.on(
(state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment),
(equipment) => handleAntagonistEquipmentChange(equipment)
),
];
@@ -207,7 +193,7 @@ export function PrimaryArmorSceneComponent({
useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
gl.getPixelRatio(),
gl.getPixelRatio()
);
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
material.uniforms.spacedArmorDepth.value =
@@ -37,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
vec3 getPenetrationColor(bool isThreeCalibersRule, bool 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);
@@ -107,7 +107,7 @@ void main() {
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
if (advancedHighlighting && didRicochet) {
baseColor = vec3(1.0, baseColor.g, 0.5);
baseColor = vec3(1.0, baseColor.g, 1.0);
}
if (greenPenetration || advancedHighlighting) {
@@ -1,9 +1,9 @@
import { J_HAT, K_HAT } from '@blitzkit/core';
import { Box, Flex, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { invalidate, useFrame } from '@react-three/fiber';
import { clamp } from 'lodash-es';
import { useEffect, useRef, type ComponentProps } from 'react';
import { J_HAT, K_HAT } from "@blitzkit/core";
import { Box, Flex, Text } from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { invalidate, useFrame } from "@react-three/fiber";
import { clamp } from "lodash-es";
import { useEffect, useRef, type ComponentProps } from "react";
import {
BufferGeometry,
DoubleSide,
@@ -15,23 +15,24 @@ import {
MeshBasicMaterial,
Path,
Quaternion,
} from 'three';
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
} from "three";
import { isHalloween } from "../../../core/blitzkit/isHalloween";
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
import {
TankopediaEphemeral,
Tankopedia,
type ShotLayerNonExternal,
type ShotStatus,
} from '../../../stores/tankopediaEphemeral';
import { ShotDisplayCard } from './ShotDisplayCard';
} from "../../../stores/tankopedia";
import { ShotDisplayCard } from "./ShotDisplayCard";
export const shotStatusColors: Record<
ShotStatus,
ComponentProps<typeof Text>['color']
ComponentProps<typeof Text>["color"]
> = {
blocked: 'red',
penetration: 'green',
ricochet: 'yellow',
splash: 'orange',
blocked: "red",
penetration: "green",
ricochet: "yellow",
splash: "orange",
};
const SURFACE_POINT_SIZE = 12;
@@ -40,7 +41,7 @@ const TRACER_THICK = 1 / 32;
const TRACER_THIN = TRACER_THICK / 2;
export function ShotDisplay() {
const shot = TankopediaEphemeral.use((state) => state.shot);
const shot = Tankopedia.use((state) => state.shot);
const inTracer = useRef<Mesh>(null);
const outTracer = useRef<Mesh>(null);
const splashRadiusWrapper = useRef<Group>(null);
@@ -51,16 +52,16 @@ export function ShotDisplay() {
useEffect(() => {
if (shot?.splashRadius !== undefined) {
const outlineGeometry = new BufferGeometry().setFromPoints(
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50),
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50)
);
const outlineMaterial = new LineDashedMaterial({
color: 'orange',
color: "orange",
dashSize: 0.05,
gapSize: 0.025,
});
const outline = new Line(
outlineGeometry,
outlineMaterial,
outlineMaterial
).computeLineDistances();
splashRadiusWrapper.current?.add(outline);
@@ -72,15 +73,12 @@ export function ShotDisplay() {
});
useEffect(() => {
const data = new Date();
const isHalloween = data.getMonth() === 9 && data.getDate() === 31;
if (
isHalloween &&
isHalloween() &&
shot?.splashRadius !== undefined &&
shot.in.status === 'penetration'
shot.in.status === "penetration"
) {
const audio = new Audio('/assets/audio/lotta-damage.mp3');
const audio = new Audio("/assets/audio/lotta-damage.mp3");
audio.currentTime = 0.7;
audio.volume = 0.25;
@@ -136,11 +134,11 @@ export function ShotDisplay() {
const outLength =
shot.out && shot.out.layers.length > 0
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
(shot.out.layers.at(-1) as ShotLayerNonExternal).point,
(shot.out.layers.at(-1) as ShotLayerNonExternal).point
)
: SHOT_DISPLAY_LENGTH_INFINITY;
const inLast = shot.in.layers.findLast(
(layer) => layer.type !== null,
(layer) => layer.type !== null
) as ShotLayerNonExternal;
const outTitleColor = shot.out
? shotStatusColors[shot.out.status]
@@ -156,7 +154,7 @@ export function ShotDisplay() {
position={inLast.point}
ref={splashRadiusWrapper}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
)}
>
<mesh>
@@ -174,13 +172,13 @@ export function ShotDisplay() {
<Html position={inLast.point} center>
<Box
style={{
pointerEvents: 'none',
pointerEvents: "none",
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
}}
mb={tracerGoingUp ? '0' : '9'}
pb={tracerGoingUp ? '0' : '4'}
mt={tracerGoingUp ? '9' : '0'}
pt={tracerGoingUp ? '4' : '0'}
mb={tracerGoingUp ? "0" : "9"}
pb={tracerGoingUp ? "0" : "4"}
mt={tracerGoingUp ? "9" : "0"}
pt={tracerGoingUp ? "4" : "0"}
>
<LocaleProvider locale={locale}>
<ShotDisplayCard shot={shot} />
@@ -192,7 +190,7 @@ export function ShotDisplay() {
if (layer.type === null) return null;
const shotStatusColor =
shot.in.status === 'splash'
shot.in.status === "splash"
? shotStatusColors.splash
: shotStatusColors[layer.status];
@@ -201,8 +199,8 @@ export function ShotDisplay() {
position={layer.point}
key={index}
style={{
pointerEvents: 'none',
transform: 'translateY(-50%)',
pointerEvents: "none",
transform: "translateY(-50%)",
}}
>
<Flex
@@ -227,7 +225,7 @@ export function ShotDisplay() {
<group
position={inLast.point}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal),
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal)
)}
>
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
@@ -255,8 +253,8 @@ export function ShotDisplay() {
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(
J_HAT,
shot.out.surfaceNormal.clone(),
),
shot.out.surfaceNormal.clone()
)
)}
>
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
@@ -1,4 +1,4 @@
import { literals } from '@blitzkit/i18n/src/literals';
import { literals } from "@blitzkit/i18n/src/literals";
import {
Card,
Flex,
@@ -6,19 +6,19 @@ import {
Separator,
Text,
type CardProps,
} from '@radix-ui/themes';
import type { ComponentProps } from 'react';
import { radToDeg } from 'three/src/math/MathUtils.js';
import { useLocale } from '../../../hooks/useLocale';
import type { Shot, ShotLayer } from '../../../stores/tankopediaEphemeral';
import { shotStatusColors } from './ShotDisplay';
import { ArmorType } from './SpacedArmorScene';
} from "@radix-ui/themes";
import type { ComponentProps } from "react";
import { radToDeg } from "three/src/math/MathUtils.js";
import { useLocale } from "../../../hooks/useLocale";
import type { Shot, ShotLayer } from "../../../stores/tankopedia";
import { shotStatusColors } from "./ShotDisplay";
import { ArmorType } from "./SpacedArmorScene";
export const layerTypeNames = {
[ArmorType.Primary]: 'primary',
[ArmorType.Spaced]: 'spaced',
[ArmorType.External]: 'external',
null: 'gap',
[ArmorType.Primary]: "primary",
[ArmorType.Spaced]: "spaced",
[ArmorType.External]: "external",
null: "gap",
} as const;
function LayerEntry({
@@ -29,7 +29,7 @@ function LayerEntry({
angle,
}: {
layerIndex?: string;
shotStatusColor: ComponentProps<typeof Text>['color'];
shotStatusColor: ComponentProps<typeof Text>["color"];
layerName: string;
layer: ShotLayer;
angle?: number;
@@ -37,44 +37,38 @@ function LayerEntry({
const { locale, strings } = useLocale();
return (
<Flex align="center" gap="2">
<Text size="1" color="gray" style={{ width: 12 }}>
{layerIndex}
</Text>
<Text size="2" color={shotStatusColor} style={{ width: 48 }}>
{layerName}
</Text>
<tr>
<td>
<Text size="1" color="gray">
{layerIndex}
</Text>
</td>
{layer.type === null && (
<>
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
<td>
<Text size="2" color={shotStatusColor}>
{layerName}
</Text>
</td>
<td>
{layer.type === null && (
<Text size="2" color={shotStatusColor}>
{literals(strings.common.units.mm, [
Math.round(layer.distance * 1000).toLocaleString(locale),
])}
</Text>
)}
<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)],
)}
{layer.type === ArmorType.External && (
<Text size="2" color={shotStatusColor}>
{literals(strings.common.units.mm, [
Math.round(layer.thickness).toLocaleString(locale),
])}
</Text>
</>
)}
{layer.type === ArmorType.External && (
<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 }}>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<Text size="2" color={shotStatusColor}>
{literals(
angle === undefined
? strings.common.units.mm
@@ -83,20 +77,35 @@ function LayerEntry({
[
Math.round(layer.thicknessAngled).toLocaleString(locale),
radToDeg(angle ?? 0).toFixed(0),
],
]
)}
</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">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.nominal,
[Math.round(layer.thickness).toLocaleString(locale)],
[Math.round(layer.thickness).toLocaleString(locale)]
)}
</Text>
</>
)}
</Flex>
)}
</td>
</tr>
);
}
@@ -115,7 +124,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
const inTitleColor = shotStatusColors[shot.in.status];
return (
<Card variant="classic" style={{ width: 300 }} {...props}>
<Card variant="classic" {...props}>
<Flex direction="column" gap="2">
<Flex direction="column" gap="1">
<Text color={inTitleColor} weight="bold">
@@ -123,11 +132,16 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
shot.in.status
],
[Math.round(shot.damage).toLocaleString(locale)],
[Math.round(shot.damage).toLocaleString(locale)]
)}
</Text>
<Flex direction="column">
<table
style={{
whiteSpace: "nowrap",
borderSpacing: "var(--space-1) 0",
}}
>
{shot.in.layers.map((layer, index) => {
const layerName =
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
@@ -141,7 +155,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
? undefined
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor =
shot.in.status === 'splash'
shot.in.status === "splash"
? shotStatusColors.splash
: shotStatusColors[layer.status];
@@ -161,7 +175,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
/>
);
})}
</Flex>
</table>
</Flex>
{shot.out && shot.out.layers.length > 0 && (
@@ -188,7 +202,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.status[shot.out.status],
[Math.round(shot.damage).toLocaleString(locale)],
[Math.round(shot.damage).toLocaleString(locale)]
)}
</Text>
@@ -210,7 +224,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
1
}.`;
const shotStatusColor =
shot.out!.status === 'splash'
shot.out!.status === "splash"
? shotStatusColors.splash
: shotStatusColors[layer.status];
@@ -1,15 +1,15 @@
import { memo, useRef } from 'react';
import { Group, Plane, Scene, Vector3 } from 'three';
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import { Duel } from '../../../stores/duel';
import { ModelTankWrapper } from './ModelTankWrapper';
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
import { memo, useRef } from "react";
import { Group, Plane, Scene, Vector3 } from "three";
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
import { useArmor } from "../../../hooks/useArmor";
import { useModel } from "../../../hooks/useModel";
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
import { useTankTransform } from "../../../hooks/useTankTransform";
import { Duel } from "../../../stores/duel";
import { ModelTankWrapper } from "./ModelTankWrapper";
import { SpacedArmorSceneComponent } from "./SpacedArmorSceneComponent";
export enum ArmorType {
Primary,
@@ -23,8 +23,8 @@ interface SpacedArmorSceneProps {
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
@@ -45,7 +45,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73),
state.protagonist.consumables.includes(73)
);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -54,19 +54,19 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
<ModelTankWrapper ref={wrapper}>
<group position={hullOrigin}>
{armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_');
const isHull = node.name.startsWith("hull_");
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId,
armorId
);
if (
!isVisible ||
thickness === undefined ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
(isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes("state_01"))
)
return null;
@@ -83,8 +83,8 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
</group>
{modelNodes.map((node) => {
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith("chassis_track_");
const isVisible = isWheel || isTrack;
if (!isVisible) return null;
@@ -104,20 +104,20 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`,
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId,
armorId
);
if (
!isVisible ||
thickness === undefined ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
(isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes("state_01"))
)
return null;
@@ -139,20 +139,20 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`,
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId,
armorId
);
if (
!isVisible ||
thickness === undefined ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
(isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes("state_01"))
)
return null;
@@ -173,7 +173,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
{modelNodes.map((node) => {
const gunString = `gun_${gunModelDefinition.model_id
.toString()
.padStart(2, '0')}`;
.padStart(2, "0")}`;
const isCurrentGun = gunModelDefinition.mask
? node.name.startsWith(gunString)
: node.name === gunString;
@@ -1,20 +1,20 @@
import { resolvePenetrationCoefficient } from '@blitzkit/core';
import { useEffect } from 'react';
import { resolvePenetrationCoefficient } from "@blitzkit/core";
import { useEffect } from "react";
import {
AdditiveBlending,
MeshBasicMaterial,
Object3D,
Plane,
ShaderMaterial,
} from 'three';
import type { ArmorUserData, ExternalModuleVariant } from '../..';
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { ArmorType } from '../../../SpacedArmorScene';
import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl?raw';
} from "three";
import type { ArmorUserData, ExternalModuleVariant } from "../..";
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { ArmorType } from "../../../SpacedArmorScene";
import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw";
interface SpacedArmorSubExternalProps {
node: Object3D;
@@ -29,8 +29,6 @@ export function SpacedArmorSubExternal({
variant,
clip,
}: SpacedArmorSubExternalProps) {
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const duelStore = Duel.useStore();
const material = new ShaderMaterial({
fragmentShader,
vertexShader,
@@ -49,33 +47,30 @@ export function SpacedArmorSubExternal({
useEffect(() => {
function handleShellChange() {
const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near;
}
async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix,
) {
const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment(
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
const hasEnhancedArmor = hasEquipment(
110,
duel.protagonist.tank.equipment_preset,
equipment,
Duel.state.protagonist.tank.equipment_preset,
equipment
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03
: thickness;
}
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near;
const hasCalibratedShells = await hasEquipment(
const hasCalibratedShells = hasEquipment(
103,
duel.antagonist.tank.equipment_preset,
equipment,
Duel.state.antagonist.tank.equipment_preset,
equipment
);
material.uniforms.penetration.value =
@@ -84,26 +79,19 @@ export function SpacedArmorSubExternal({
}
handleShellChange();
handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix,
);
handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix,
);
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
tankopediaEphemeralStore.subscribe(
(state) => state.customShell,
handleShellChange,
),
duelStore.subscribe(
Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange),
Duel.on(
(state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange,
handleProtagonistEquipmentChange
),
duelStore.subscribe(
Duel.on(
(state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange,
handleAntagonistEquipmentChange
),
];
@@ -125,6 +113,7 @@ export function SpacedArmorSubExternal({
return (
<mesh
{...props}
onClick={() => {}}
key={`${key}-spaced-sub-external-exclude`}
renderOrder={3}
material={
@@ -1,20 +1,20 @@
import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core';
import { useEffect } from 'react';
import { isExplosive, resolvePenetrationCoefficient } from "@blitzkit/core";
import { useEffect } from "react";
import {
AdditiveBlending,
MeshBasicMaterial,
ShaderMaterial,
type Object3D,
} from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
import type { ArmorUserData } from '../..';
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { ArmorType } from '../../../SpacedArmorScene';
import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl?raw';
} from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import type { ArmorUserData } from "../..";
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { ArmorType } from "../../../SpacedArmorScene";
import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw";
interface SpacedArmorSubSpacedProps {
node: Object3D;
@@ -30,8 +30,6 @@ export function SpacedArmorSubSpaced({
node,
thickness,
}: SpacedArmorSubSpacedProps) {
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const duelStore = Duel.useStore();
const material = new ShaderMaterial({
fragmentShader,
vertexShader,
@@ -51,41 +49,38 @@ export function SpacedArmorSubSpaced({
useEffect(() => {
function handleShellChange() {
const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near;
material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!,
isExplosive(shell.type) ? 90 : shell.ricochet!
);
material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0,
shell.normalization ?? 0
);
}
async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix,
) {
const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment(
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
const hasEnhancedArmor = hasEquipment(
110,
duel.protagonist.tank.equipment_preset,
equipment,
Duel.state.protagonist.tank.equipment_preset,
equipment
);
material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03
: thickness;
}
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const tankopediaEphemeral = Tankopedia.state;
const shell =
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
const penetration = shell.penetration.near;
const hasCalibratedShells = await hasEquipment(
const hasCalibratedShells = hasEquipment(
103,
duel.antagonist.tank.equipment_preset,
equipment,
Duel.state.antagonist.tank.equipment_preset,
equipment
);
material.uniforms.penetration.value =
@@ -94,26 +89,19 @@ export function SpacedArmorSubSpaced({
}
handleShellChange();
handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix,
);
handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix,
);
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
tankopediaEphemeralStore.subscribe(
(state) => state.customShell,
handleShellChange,
),
duelStore.subscribe(
Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange),
Duel.on(
(state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange,
handleProtagonistEquipmentChange
),
duelStore.subscribe(
Duel.on(
(state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange,
handleAntagonistEquipmentChange
),
];
@@ -2,9 +2,9 @@ import {
ShellType,
isExplosive,
resolvePenetrationCoefficient,
} from '@blitzkit/core';
import { useThree } from '@react-three/fiber';
import { useCallback } from 'react';
} from "@blitzkit/core";
import { useThree } from "@react-three/fiber";
import { useCallback } from "react";
import {
type Intersection,
MeshBasicMaterial,
@@ -14,21 +14,21 @@ import {
Raycaster,
Scene,
Vector3,
} from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
import { Duel } from '../../../../stores/duel';
} from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
import { Duel } from "../../../../stores/duel";
import {
type Shot,
type ShotLayerBase,
type ShotLayerNonExternal,
TankopediaEphemeral,
} from '../../../../stores/tankopediaEphemeral';
import { ArmorType } from '../SpacedArmorScene';
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
Tankopedia,
} from "../../../../stores/tankopedia";
import { ArmorType } from "../SpacedArmorScene";
import { SpacedArmorSubExternal } from "./components/SpacedArmorSubExternal";
import { SpacedArmorSubSpaced } from "./components/SpacedArmorSubSpaced";
type SpacedArmorSceneComponentProps = {
node: Object3D;
@@ -45,7 +45,7 @@ type SpacedArmorSceneComponentProps = {
}
);
export type ExternalModuleVariant = 'gun' | 'track';
export type ExternalModuleVariant = "gun" | "track";
export type ArmorUserData = {
thickness: number;
} & (
@@ -76,20 +76,17 @@ export function SpacedArmorSceneComponent({
clip,
...props
}: SpacedArmorSceneComponentProps) {
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const duelStore = Duel.useStore();
const camera = useThree((state) => state.camera);
const shoot = useCallback(
async (
(
point: Vector3,
intersections: Intersection[],
allowRicochet: boolean,
remainingPenetrationInput?: number,
remainingPenetrationInput?: number
) => {
const { antagonist, protagonist } = duelStore.getState();
const { customShell } = tankopediaEphemeralStore.getState();
const shell = customShell ?? antagonist.shell;
const { customShell } = Tankopedia.state;
const shell = customShell ?? Duel.state.antagonist.shell;
const cameraNormal = camera.position.clone().sub(point).normalize();
const shot: Shot = {
splashRadius:
@@ -99,19 +96,19 @@ export function SpacedArmorSceneComponent({
in: {
surfaceNormal: cameraNormal,
layers: [],
status: 'blocked',
status: "blocked",
},
};
const hasCalibratedShells = await hasEquipment(
const hasCalibratedShells = hasEquipment(
103,
antagonist.tank.equipment_preset,
antagonist.equipmentMatrix,
Duel.state.antagonist.tank.equipment_preset,
Duel.state.antagonist.equipmentMatrix
);
const hasEnhancedArmor = await hasEquipment(
const hasEnhancedArmor = hasEquipment(
110,
protagonist.tank.equipment_preset,
protagonist.equipmentMatrix,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
);
const penetration =
shell.penetration.near *
@@ -119,8 +116,8 @@ export function SpacedArmorSceneComponent({
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
const filteredIntersections = intersections.filter(
(intersection) =>
'type' in intersection.object.userData &&
!discardClippingPlane(intersection.object, intersection.point),
"type" in intersection.object.userData &&
!discardClippingPlane(intersection.object, intersection.point)
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
const noDuplicateIntersections: typeof filteredIntersections = [];
@@ -129,12 +126,12 @@ export function SpacedArmorSceneComponent({
if (intersection.object.userData.type === ArmorType.External) {
if (
!encounteredExternalModuleVariants.includes(
intersection.object.userData.variant,
intersection.object.userData.variant
)
) {
noDuplicateIntersections.push(intersection);
encounteredExternalModuleVariants.push(
intersection.object.userData.variant,
intersection.object.userData.variant
);
}
} else {
@@ -162,7 +159,7 @@ export function SpacedArmorSceneComponent({
const surfaceNormal = intersection
.normal!.clone()
.applyQuaternion(
intersection.object.getWorldQuaternion(new Quaternion()),
intersection.object.getWorldQuaternion(new Quaternion())
);
if (shell.type === ShellType.HEAT && index > 0) {
@@ -174,7 +171,7 @@ export function SpacedArmorSceneComponent({
shot.in.layers.push({
type: null,
distance,
status: blocked ? 'blocked' : 'penetration',
status: blocked ? "blocked" : "penetration",
});
if (blocked) break;
@@ -194,16 +191,16 @@ export function SpacedArmorSceneComponent({
surfaceNormal,
status:
shell.type === ShellType.HE
? 'blocked'
? "blocked"
: blocked
? 'blocked'
: 'penetration',
? "blocked"
: "penetration",
variant: layer.variant,
});
} else {
const thickness = layer.thickness * thicknessCoefficient;
const ricochet = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!,
isExplosive(shell.type) ? 90 : shell.ricochet!
);
const angle = surfaceNormal.angleTo(cameraNormal);
@@ -227,7 +224,7 @@ export function SpacedArmorSceneComponent({
thicknessAngled: finalThickness,
point: intersection.point,
surfaceNormal,
status: 'ricochet',
status: "ricochet",
angle,
});
@@ -245,10 +242,10 @@ export function SpacedArmorSceneComponent({
surfaceNormal: intersection.face!.normal,
status:
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
? 'blocked'
? "blocked"
: blocked
? 'blocked'
: 'penetration',
? "blocked"
: "penetration",
angle,
});
}
@@ -282,10 +279,10 @@ export function SpacedArmorSceneComponent({
return acc + layer.thicknessAngled;
},
0,
0
);
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
lastLayer.point,
lastLayer.point
);
const finalDamage = Math.max(
0,
@@ -294,7 +291,7 @@ export function SpacedArmorSceneComponent({
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
1.1 *
(lastLayer.thicknessAngled +
Math.min(penetration, totalSpacedArmorThickness)),
Math.min(penetration, totalSpacedArmorThickness))
);
/**
@@ -308,19 +305,19 @@ export function SpacedArmorSceneComponent({
* for parity's sake, I am capping the value here.
*/
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
if (shot.in.layers.length > 1 || lastLayer.status === "blocked") {
shot.in.status = finalDamage > 0 ? "splash" : "blocked";
shot.damage = finalDamage;
} else {
shot.in.status = 'penetration';
shot.in.status = "penetration";
shot.damage = shell.armor_damage;
}
} else {
if (lastLayer.status === 'blocked') {
shot.in.status = 'blocked';
if (lastLayer.status === "blocked") {
shot.in.status = "blocked";
shot.damage = 0;
} else if (lastLayer.status === 'penetration') {
shot.in.status = 'penetration';
} else if (lastLayer.status === "penetration") {
shot.in.status = "penetration";
shot.damage = shell.armor_damage;
} else {
const caster = new Raycaster();
@@ -331,29 +328,29 @@ export function SpacedArmorSceneComponent({
.sub(
lastLayer.surfaceNormal
.clone()
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal))
);
caster.set(lastLayer.point, ricochetNormal);
const ricochetIntersections = caster.intersectObjects(
props.scene.children,
true,
true
);
const outShot = await shoot(
const outShot = shoot(
lastLayer.point,
ricochetIntersections,
false,
remainingPenetration,
remainingPenetration
);
shot.in.status = 'ricochet';
shot.in.status = "ricochet";
if (outShot === null) {
shot.damage = 0;
shot.out = {
surfaceNormal: ricochetNormal,
layers: [],
status: 'ricochet',
status: "ricochet",
};
} else {
shot.damage = outShot.damage;
@@ -366,7 +363,7 @@ export function SpacedArmorSceneComponent({
return shot;
},
[camera],
[camera]
);
return (
@@ -386,14 +383,14 @@ export function SpacedArmorSceneComponent({
thickness,
} satisfies ArmorUserData
}
onClick={async (event) => {
onClick={(event) => {
event.stopPropagation();
const shot = (await shoot(
event.point,
event.intersections,
true,
))!;
tankopediaEphemeralStore.setState({ shot });
const shot = shoot(event.point, event.intersections, true)!;
Tankopedia.mutate((draft) => {
draft.shot = shot;
});
}}
/>
);
@@ -1,22 +1,22 @@
import { useThree, type ThreeEvent } from '@react-three/fiber';
import { memo, useRef } from 'react';
import { Group, Plane, Vector2, Vector3 } from 'three';
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits';
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
import { hasEquipment } from '../../../core/blitzkit/hasEquipment';
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform';
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
import { ModelTankWrapper } from './ModelTankWrapper';
import { ArmorType } from './SpacedArmorScene';
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent';
import { useThree, type ThreeEvent } from "@react-three/fiber";
import { memo, useRef } from "react";
import { Group, Plane, Vector2, Vector3 } from "three";
import { applyPitchYawLimits } from "../../../core/blitz/applyPitchYawLimits";
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
import { hasEquipment } from "../../../core/blitzkit/hasEquipment";
import { modelTransformEvent } from "../../../core/blitzkit/modelTransform";
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
import { useArmor } from "../../../hooks/useArmor";
import { useModel } from "../../../hooks/useModel";
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
import { useTankTransform } from "../../../hooks/useTankTransform";
import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
import { ModelTankWrapper } from "./ModelTankWrapper";
import { ArmorType } from "./SpacedArmorScene";
import { StaticArmorSceneComponent } from "./StaticArmorSceneComponent";
interface ArmorSceneProps {
thicknessRange: ThicknessRange;
@@ -28,9 +28,8 @@ export interface ThicknessRange {
export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
const protagonist = Duel.use((draft) => draft.protagonist);
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
@@ -46,25 +45,23 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const mutateDuel = Duel.useMutation();
const canvas = useThree((state) => state.gl.domElement);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const duelStore = Duel.useStore();
const maskOrigin =
gunModelDefinition.mask === undefined
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor,
(state) => state.showPrimaryArmor
);
const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor,
(state) => state.showSpacedArmor
);
const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules,
(state) => state.showExternalModules
);
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73),
state.protagonist.consumables.includes(73)
);
useTankTransform(track, turret, turretContainer, gunContainer);
@@ -73,12 +70,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<ModelTankWrapper ref={wrapper}>
<group position={hullOrigin}>
{armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_');
const isHull = node.name.startsWith("hull_");
const isVisible = isHull;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId,
armorId
);
if (
@@ -86,8 +83,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
(isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes("state_01"))
)
return null;
@@ -106,8 +103,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
</group>
{modelNodes.map((node) => {
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith("chassis_track_");
const isVisible = isWheel || isTrack;
if (!isVisible || !showExternalModules) return null;
@@ -129,13 +126,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`,
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId,
armorId
);
if (
@@ -143,8 +140,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
(isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes("state_01"))
) {
return null;
}
@@ -157,27 +154,26 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
position.set(event.clientX, event.clientY);
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
);
const hasDownImprovedVerticalStabilizer = await hasEquipment(
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
);
const boundingRect = canvas.getBoundingClientRect();
@@ -191,16 +187,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
);
modelTransformEvent.dispatch({ pitch, yaw });
}
function handlePointerUp() {
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
return (
@@ -209,7 +205,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<StaticArmorSceneComponent
name={node.name}
thicknessRange={thicknessRange}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
thickness={thickness}
node={node}
@@ -223,13 +218,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`,
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId,
armorId
);
if (
@@ -237,8 +232,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
(isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes("state_01"))
)
return null;
@@ -248,30 +243,29 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
position.set(event.clientX, event.clientY);
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
);
const hasDownImprovedVerticalStabilizer = await hasEquipment(
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
@@ -285,16 +279,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
);
modelTransformEvent.dispatch({ pitch, yaw });
}
function handlePointerUp() {
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
return (
@@ -316,7 +310,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
{modelNodes.map((node) => {
const gunString = `gun_${gunModelDefinition.model_id
.toString()
.padStart(2, '0')}`;
.padStart(2, "0")}`;
const isCurrentGun = gunModelDefinition.mask
? node.name.startsWith(gunString)
: node.name === gunString;
@@ -330,29 +324,28 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
position.set(event.clientX, event.clientY);
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = hasEquipment(
122,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
);
const hasDownImprovedVerticalStabilizer = await hasEquipment(
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
Duel.state.protagonist.tank.equipment_preset,
Duel.state.protagonist.equipmentMatrix
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
@@ -366,17 +359,17 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer
);
modelTransformEvent.dispatch({ pitch, yaw });
}
function handlePointerUp() {
mutateTankopediaEphemeral((draft) => {
Tankopedia.mutate((draft) => {
draft.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
return (
@@ -1,8 +1,8 @@
import { I_HAT, J_HAT } from '@blitzkit/core';
import { type MeshProps, useThree } from '@react-three/fiber';
import { clamp } from 'lodash-es';
import type { QuicklimeEvent } from 'quicklime';
import { useEffect, useMemo } from 'react';
import { I_HAT, J_HAT } from "@blitzkit/core";
import { useThree } from "@react-three/fiber";
import { clamp } from "lodash-es";
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useMemo, type ComponentProps } from "react";
import {
Box3,
Color,
@@ -10,33 +10,33 @@ import {
FrontSide,
LineBasicMaterial,
Mesh,
MeshStandardMaterial,
MeshBasicMaterial,
Object3D,
Plane,
Quaternion,
Vector3,
} from 'three';
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
import { jsxTree } from '../../../core/blitzkit/jsxTree';
} from "three";
import { unrotateDavaVector } from "../../../core/blitz/unrotateDavaVector";
import { jsxTree } from "../../../core/blitzkit/jsxTree";
import {
type ModelTransformEventData,
modelTransformEvent,
} from '../../../core/blitzkit/modelTransform';
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { ArmorType } from './SpacedArmorScene';
type ModelTransformEventData,
} from "../../../core/blitzkit/modelTransform";
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
import { Tankopedia } from "../../../stores/tankopedia";
import { ArmorType } from "./SpacedArmorScene";
import type {
ArmorUserData,
ExternalModuleVariant,
} from './SpacedArmorSceneComponent';
import type { ThicknessRange } from './StaticArmor';
} from "./SpacedArmorSceneComponent";
import type { ThicknessRange } from "./StaticArmor";
type StaticArmorSceneComponentProps = {
name: string;
thickness: number;
thicknessRange: ThicknessRange;
node: Object3D;
onPointerDown?: MeshProps['onPointerDown'];
onPointerDown?: ComponentProps<"mesh">["onPointerDown"];
} & (
| {
type: Exclude<ArmorType, ArmorType.External>;
@@ -69,8 +69,6 @@ export function StaticArmorSceneComponent({
onPointerDown,
...props
}: StaticArmorSceneComponentProps) {
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const camera = useThree((state) => state.camera);
const x = thickness / thicknessRange.value;
const xClamped = clamp(x, 0, 1);
@@ -92,7 +90,7 @@ export function StaticArmorSceneComponent({
color = new Color(
clamp(1 - (7 / 8) * xClamped, 0, 1),
0,
clamp(1 - (1 / 8) * xClamped, 0, 1),
clamp(1 - (1 / 8) * xClamped, 0, 1)
);
opacity = clamp(x + 1 / 2, 0, 1);
break;
@@ -108,14 +106,14 @@ export function StaticArmorSceneComponent({
const surfaceMaterial = useMemo(
() =>
new MeshStandardMaterial({
new MeshBasicMaterial({
color,
opacity,
transparent: opacity < 1,
depthWrite,
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
}),
[thickness],
[thickness]
);
const outlineMaterial = useMemo(
() =>
@@ -124,7 +122,7 @@ export function StaticArmorSceneComponent({
.clone()
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
}),
[thickness],
[thickness]
);
useEffect(() => {
@@ -139,11 +137,11 @@ export function StaticArmorSceneComponent({
outlineMaterial.visible = true;
} else if (
selectedName === name ||
(name.startsWith('chassis_') && selectedName.startsWith('chassis_')) ||
(name.startsWith('gun_') &&
selectedName.startsWith('gun_') &&
!name.includes('_armor_') &&
!selectedName.includes('_armor_'))
(name.startsWith("chassis_") && selectedName.startsWith("chassis_")) ||
(name.startsWith("gun_") &&
selectedName.startsWith("gun_") &&
!name.includes("_armor_") &&
!selectedName.includes("_armor_"))
) {
// this selected, stand out!
surfaceMaterial.opacity = 1;
@@ -166,9 +164,9 @@ export function StaticArmorSceneComponent({
}
const unsubscribes = [
tankopediaEphemeralStore.subscribe(
Tankopedia.on(
(state) => state.highlightArmor?.name,
handleHighlightArmor,
handleHighlightArmor
),
];
@@ -181,14 +179,14 @@ export function StaticArmorSceneComponent({
const { clip } = props;
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
unrotateDavaVector(props.turretOrigin.clone()),
unrotateDavaVector(props.turretOrigin.clone())
);
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
const point = new Vector3();
function handleModelTransform(
event: QuicklimeEvent<ModelTransformEventData>,
event: QuicklimeEvent<ModelTransformEventData>
) {
clip.normal
.set(0, 0, -1)
@@ -229,7 +227,7 @@ export function StaticArmorSceneComponent({
{
type: props.type,
variant:
props.type === ArmorType.External ? props.variant : 'gun',
props.type === ArmorType.External ? props.variant : "gun",
thickness,
} satisfies ArmorUserData
}
@@ -238,7 +236,7 @@ export function StaticArmorSceneComponent({
event.stopPropagation();
const { editStatic } = tankopediaEphemeralStore.getState();
const { editStatic } = Tankopedia.state;
const bounds = new Box3().setFromObject(event.object);
const point = bounds.min
@@ -253,13 +251,13 @@ export function StaticArmorSceneComponent({
const surfaceNormal = event
.normal!.clone()
.applyQuaternion(
event.object.getWorldQuaternion(new Quaternion()),
event.object.getWorldQuaternion(new Quaternion())
);
const angle = surfaceNormal.angleTo(cameraNormal);
const thicknessAngled =
thickness / Math.sin(Math.PI / 2 - angle);
mutateTankopediaEphemeralStore((draft) => {
Tankopedia.mutate((draft) => {
draft.highlightArmor = {
type: props.type,
name,
@@ -1,5 +1,5 @@
import { Theme, type ThemeProps } from '@radix-ui/themes';
import type { ReactNode } from 'react';
import { Theme, type ThemeProps } from "@radix-ui/themes";
import type { ReactNode } from "react";
interface BlitzKitThemeProps extends ThemeProps {
children: ReactNode;
@@ -13,13 +13,13 @@ export function BlitzKitTheme({
}: BlitzKitThemeProps) {
return (
<Theme
style={{ minHeight: 'unset', display: 'contents', ...style }}
style={{ minHeight: "unset", display: "contents", ...style }}
className={`blitzkit-theme ${className}`}
grayColor="mauve"
appearance="dark"
panelBackground="solid"
radius="full"
accentColor="amber"
accentColor="indigo"
suppressHydrationWarning
suppressContentEditableWarning
{...props}
@@ -1,19 +1,19 @@
import { assertSecret, idToRegion } from '@blitzkit/core';
import { Button, Flex, Heading, Text } from '@radix-ui/themes';
import { useEffect, useState } from 'react';
import { assertSecret, idToRegion } from "@blitzkit/core";
import { Button, Flex, Heading, Text } from "@radix-ui/themes";
import { useEffect, useState } from "react";
import {
LocaleProvider,
useLocale,
type LocaleAcceptorProps,
} from '../../hooks/useLocale';
import { App } from '../../stores/app';
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../../stores/app/constants';
import { LinkI18n } from '../LinkI18n';
import './index.css';
} from "../../hooks/useLocale";
import { App } from "../../stores/app";
import { CURRENT_POLICIES_AGREEMENT_INDEX } from "../../stores/app/constants";
import { LinkI18n } from "../LinkI18n";
import "./index.css";
type Extension =
| {
status: 'ok';
status: "ok";
data: {
access_token: string;
account_id: number;
@@ -21,15 +21,13 @@ type Extension =
};
}
| {
status: 'error';
status: "error";
};
export function Checks({ locale }: LocaleAcceptorProps) {
return (
<LocaleProvider locale={locale}>
<App.Provider>
<Content />
</App.Provider>
<Content />
</LocaleProvider>
);
}
@@ -40,19 +38,17 @@ export function Checks({ locale }: LocaleAcceptorProps) {
function Content() {
const { locale } = useLocale();
const logins = App.use((state) => state.logins);
const mutateApp = App.useMutation();
const appStore = App.useStore();
const policiesAgreementIndex = App.use(
(state) => state.policiesAgreementIndex,
(state) => state.policiesAgreementIndex
);
const isLegal = window.location.pathname.startsWith('/legal');
const isLegal = window.location.pathname.startsWith("/legal");
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
useEffect(() => {
function unsubscribe() {
window.removeEventListener('scroll', show);
window.removeEventListener('pointermove', show);
window.removeEventListener('pointerdown', show);
window.removeEventListener("scroll", show);
window.removeEventListener("pointermove", show);
window.removeEventListener("pointerdown", show);
}
function show() {
@@ -64,9 +60,9 @@ function Content() {
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
!isLegal
) {
window.addEventListener('scroll', show);
window.addEventListener('pointermove', show);
window.addEventListener('pointerdown', show);
window.addEventListener("scroll", show);
window.addEventListener("pointermove", show);
window.addEventListener("pointerdown", show);
}
return unsubscribe;
@@ -78,25 +74,25 @@ function Content() {
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
if (expiresInDays < 0) {
mutateApp((draft) => {
App.mutate((draft) => {
draft.logins.wargaming = undefined;
});
} else if (expiresInDays < 7) {
const { wargaming } = appStore.getState().logins;
const { wargaming } = App.state.logins;
if (!wargaming) return;
fetch(
`https://api.worldoftanks.${idToRegion(
wargaming.id,
wargaming.id
)}/wot/auth/prolongate/?application_id=${assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID,
)}&access_token=${wargaming.token}`,
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
)}&access_token=${wargaming.token}`
)
.then((response) => response.json() as Promise<Extension>)
.then((json) => {
mutateApp((draft) => {
if (json.status === 'error') {
App.mutate((draft) => {
if (json.status === "error") {
draft.logins.wargaming = undefined;
} else {
draft.logins.wargaming = {
@@ -126,11 +122,11 @@ function Content() {
<Text>
This website utilizes cookies to perform analytics and personalize
your experience. You can learn more through{' '}
your experience. You can learn more through{" "}
<LinkI18n locale={locale} href="/docs/legal/privacy-policy">
our privacy policy
</LinkI18n>
. By using BlitzKit, you also agree to our{' '}
. By using BlitzKit, you also agree to our{" "}
<LinkI18n locale={locale} href="/docs/legal/terms-of-service">
terms of service
</LinkI18n>
@@ -141,8 +137,8 @@ function Content() {
<Button
mt="2"
onClick={() => {
appStore.setState({
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
App.mutate((draft) => {
draft.policiesAgreementIndex = CURRENT_POLICIES_AGREEMENT_INDEX;
});
setShowPoliciesAgreement(false);
}}
@@ -1,11 +1,11 @@
import { asset } from '@blitzkit/core';
import { Flex, Heading, IconButton, Table } from '@radix-ui/themes';
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
import { useLocale } from '../../hooks/useLocale';
import { CompareEphemeral } from '../../stores/compareEphemeral';
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
import { CompareRow } from './CompareRow';
import { CompareSectionTitle } from './CompareSectionTitle';
import { asset } from "@blitzkit/core";
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { CompareRow } from "./CompareRow";
import { CompareSectionTitle } from "./CompareSectionTitle";
interface BodyProps {
stats: TankCharacteristics[];
@@ -14,10 +14,9 @@ interface BodyProps {
export function Body({ stats }: BodyProps) {
const members = CompareEphemeral.use((state) => state.members);
const hasNonRegularGun = members.some(
({ gun }) => gun.gun_type!.$case !== 'regular',
({ gun }) => gun.gun_type!.$case !== "regular"
);
const { unwrap, strings } = useLocale();
const mutateCompareEphemeral = CompareEphemeral.useMutation();
return (
<>
@@ -35,7 +34,7 @@ export function Body({ stats }: BodyProps) {
<Flex>
{gun.shells.map((thisShell, shellIndex) => (
<IconButton
color={thisShell.id === shell.id ? undefined : 'gray'}
color={thisShell.id === shell.id ? undefined : "gray"}
variant="soft"
key={thisShell.id}
style={{
@@ -49,7 +48,7 @@ export function Body({ stats }: BodyProps) {
marginLeft: shellIndex === 0 ? 0 : -1,
}}
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].shell = thisShell;
});
}}
@@ -88,7 +87,7 @@ export function Body({ stats }: BodyProps) {
}
display={(stats) =>
stats.shellReload?.toFixed(2) ??
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(', ')
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(", ")
}
deltaNominalDisplay={(delta) => delta.toFixed(2)}
/>
@@ -2,21 +2,21 @@ import {
CaretLeftIcon,
CaretRightIcon,
CaretSortIcon,
} from '@radix-ui/react-icons';
import { Flex, IconButton, Table, Text } from '@radix-ui/themes';
import { useRef } from 'react';
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
import { Var } from '../../core/radix/var';
import { useLocale } from '../../hooks/useLocale';
import { CompareEphemeral } from '../../stores/compareEphemeral';
import { ComparePersistent } from '../../stores/comparePersistent';
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
} from "@radix-ui/react-icons";
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
import { useRef } from "react";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { Var } from "../../core/radix/var";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { ComparePersistent } from "../../stores/comparePersistent";
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
type CompareRowProps = {
display?: (
member: Awaited<TankCharacteristics>,
member: Awaited<TankCharacteristics>
) => number | string | undefined;
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
deltaType?: "higherIsBetter" | "lowerIsBetter";
decimals?: number;
deltaNominalDisplay?: (delta: number) => string | number;
indent?: boolean;
@@ -33,7 +33,7 @@ type CompareRowProps = {
export function CompareRow({
display,
deltaType = 'higherIsBetter',
deltaType = "higherIsBetter",
decimals,
deltaNominalDisplay,
indent,
@@ -41,17 +41,16 @@ export function CompareRow({
...props
}: CompareRowProps) {
const { strings } = useLocale();
const mutateCompareEphemeral = CompareEphemeral.useMutation();
const sorting = CompareEphemeral.use((state) => state.sorting);
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
const values = stats.map((stat) =>
typeof props.value === 'function'
typeof props.value === "function"
? props.value(stat)!
: (stat[props.value] as number),
: (stat[props.value] as number)
);
const id = useRef(Math.random());
const name =
typeof props.value === 'string'
typeof props.value === "string"
? strings.website.tools.tankopedia.characteristics.values[props.value]
: props.name;
@@ -60,21 +59,21 @@ export function CompareRow({
<StickyRowHeaderCell>
<Flex
align="center"
style={{ whiteSpace: 'nowrap' }}
gap={indent ? { initial: '4', sm: '6' } : { initial: '1', sm: '2' }}
style={{ whiteSpace: "nowrap" }}
gap={indent ? { initial: "4", sm: "6" } : { initial: "1", sm: "2" }}
>
<IconButton
color={sorting?.by === id.current ? undefined : 'gray'}
color={sorting?.by === id.current ? undefined : "gray"}
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members.sort((memberA, memberB) => {
const indexA = draft.members.indexOf(memberA);
const indexB = draft.members.indexOf(memberB);
const valueA = values[indexA];
const valueB = values[indexB];
return draft.sorting?.direction === 'ascending' &&
return draft.sorting?.direction === "ascending" &&
draft.sorting.by === id.current
? valueA - valueB
: valueB - valueA;
@@ -83,26 +82,26 @@ export function CompareRow({
draft.sorting = {
by: id.current,
direction:
draft.sorting?.direction === 'ascending' &&
draft.sorting?.direction === "ascending" &&
draft.sorting.by === id.current
? 'descending'
: 'ascending',
? "descending"
: "ascending",
};
});
}}
>
{(sorting === undefined || sorting.by !== id.current) && (
<CaretSortIcon style={{ transform: 'rotate(90deg)' }} />
<CaretSortIcon style={{ transform: "rotate(90deg)" }} />
)}
{sorting?.by === id.current && (
<>
{sorting.direction === 'ascending' && <CaretRightIcon />}
{sorting.direction === 'descending' && <CaretLeftIcon />}
{sorting.direction === "ascending" && <CaretRightIcon />}
{sorting.direction === "descending" && <CaretLeftIcon />}
</>
)}
</IconButton>
<Text size={{ initial: '1', sm: '2' }}>{name}</Text>
<Text size={{ initial: "1", sm: "2" }}>{name}</Text>
</Flex>
</StickyRowHeaderCell>
@@ -110,7 +109,7 @@ export function CompareRow({
const delta = value - values[0];
const deltaPercentage = value / values[0] - 1;
const normalizedDeltaPercentage = Math.round(
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25),
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
);
const resolvedDisplayValue = display
? display(stats[index])
@@ -129,12 +128,12 @@ export function CompareRow({
value === values[0]
? undefined
: (
deltaType === 'higherIsBetter'
deltaType === "higherIsBetter"
? value > values[0]
: value < values[0]
)
? `color-mix(in srgb, ${Var('green-7')} ${normalizedDeltaPercentage}%, ${Var('green-3')})`
: `color-mix(in srgb, ${Var('red-7')} ${normalizedDeltaPercentage}%, ${Var('red-3')})`,
? `color-mix(in srgb, ${Var("green-7")} ${normalizedDeltaPercentage}%, ${Var("green-3")})`
: `color-mix(in srgb, ${Var("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
}}
>
<Flex
@@ -142,11 +141,11 @@ export function CompareRow({
justify="center"
gap="2"
style={{
width: '100%',
height: '100%',
width: "100%",
height: "100%",
}}
>
<Text style={{ textAlign: 'center' }} wrap="nowrap">
<Text style={{ textAlign: "center" }} wrap="nowrap">
{resolvedDisplayValue}
</Text>
@@ -154,10 +153,10 @@ export function CompareRow({
resolvedDisplayValue !== undefined &&
values[0] !== undefined && (
<>
{deltaMode === 'absolute' && (
{deltaMode === "absolute" && (
<Text color="gray">
(
{`${delta > 0 ? '+' : ''}${
{`${delta > 0 ? "+" : ""}${
deltaNominalDisplay
? deltaNominalDisplay(delta)
: decimals === undefined
@@ -168,11 +167,11 @@ export function CompareRow({
</Text>
)}
{deltaMode === 'percentage' && (
{deltaMode === "percentage" && (
<Text color="gray">
(
{`${deltaPercentage > 0 ? '+' : ''}${Math.round(
deltaPercentage * 100,
{`${deltaPercentage > 0 ? "+" : ""}${Math.round(
deltaPercentage * 100
)}%`}
)
</Text>
@@ -1,28 +1,28 @@
import { asset, availableProvisions, permanentSkills } from '@blitzkit/core';
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity';
import { ComponentPlaceholderIcon } from '@radix-ui/react-icons';
import { Button, Flex, IconButton, Popover, Table } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
import { awaitableEquipmentDefinitions } from '../../core/awaitables/equipmentDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { awaitableSkillDefinitions } from '../../core/awaitables/skillDefinitions';
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
import { Var } from '../../core/radix/var';
import { useLocale } from '../../hooks/useLocale';
import { CompareEphemeral } from '../../stores/compareEphemeral';
import type { EquipmentMatrix } from '../../stores/duel';
import { BlitzkitButtonGrayIcon } from '../BlitzkitButtonGrayIcon';
import { ConsumablesManager } from '../ConsumablesManager';
import { CrewSkillManager } from '../CrewSkillManager';
import { EquipmentManager } from '../EquipmentManager';
import { ModuleManager } from '../ModuleManager';
import { ProvisionsManager } from '../ProvisionsManager';
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
import { StickyTableRoot } from '../StickyTableRoot';
import { Body } from './Body';
import { InsertionMarker } from './IntersectionMarker';
import { TankCard } from './TankCard';
import { asset, availableProvisions, permanentSkills } from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
import { Button, Flex, IconButton, Popover, Table } from "@radix-ui/themes";
import { times } from "lodash-es";
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { awaitableSkillDefinitions } from "../../core/awaitables/skillDefinitions";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { Var } from "../../core/radix/var";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import type { EquipmentMatrix } from "../../stores/duel";
import { BlitzkitButtonGrayIcon } from "../BlitzkitButtonGrayIcon";
import { ConsumablesManager } from "../ConsumablesManager";
import { CrewSkillManager } from "../CrewSkillManager";
import { EquipmentManager } from "../EquipmentManager";
import { ModuleManager } from "../ModuleManager";
import { ProvisionsManager } from "../ProvisionsManager";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { StickyTableRoot } from "../StickyTableRoot";
import { Body } from "./Body";
import { InsertionMarker } from "./IntersectionMarker";
import { TankCard } from "./TankCard";
interface CompareTableProps {
stats: TankCharacteristics[];
@@ -43,7 +43,6 @@ const [
export function CompareTable({ stats }: CompareTableProps) {
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
const members = CompareEphemeral.use((state) => state.members);
const mutateCompareEphemeral = CompareEphemeral.useMutation();
const { unwrap, strings } = useLocale();
return (
@@ -51,20 +50,20 @@ export function CompareTable({ stats }: CompareTableProps) {
size="1"
variant="surface"
style={{
position: 'absolute',
maxWidth: '100%',
maxHeight: '100%',
height: '100%',
position: "absolute",
maxWidth: "100%",
maxHeight: "100%",
height: "100%",
// height: 'calc(100vh - 128px)'
}}
>
<Table.Header>
<Table.Row>
<StickyColumnHeaderCell width="0" style={{ height: '100%' }}>
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
<Flex
style={{
width: '100%',
height: '100%',
width: "100%",
height: "100%",
}}
align="center"
justify="center"
@@ -82,11 +81,11 @@ export function CompareTable({ stats }: CompareTableProps) {
<Table.Body>
<Table.Row>
<Table.Cell style={{ height: '100%' }}>
<Table.Cell style={{ height: "100%" }}>
<Flex
style={{
width: '100%',
height: '100%',
width: "100%",
height: "100%",
}}
align="center"
justify="center"
@@ -107,15 +106,15 @@ export function CompareTable({ stats }: CompareTableProps) {
borderRadius: 2,
backgroundColor:
crewSkills[skill] === 0
? Var('gray-11')
? Var("gray-11")
: permanentSkills.includes(skill)
? Var('crimson-11')
: Var('amber-11'),
? Var("crimson-11")
: Var("amber-11"),
}}
/>
))}
</Flex>
),
)
)}
</Flex>
</Button>
@@ -125,7 +124,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<CrewSkillManager
skillLevels={crewSkills}
onChange={(skills) => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.crewSkills = skills;
});
}}
@@ -135,7 +134,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
Object.keys(draft.crewSkills).forEach((skill) => {
draft.crewSkills[skill] = 0;
});
@@ -147,7 +146,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
Object.keys(draft.crewSkills).forEach((skill) => {
draft.crewSkills[skill] = 7;
});
@@ -177,19 +176,19 @@ export function CompareTable({ stats }: CompareTableProps) {
consumables,
camouflage,
},
index,
index
) => {
const equipmentPreset =
equipmentDefinitions.presets[tank.equipment_preset];
const provisionsList = availableProvisions(
tank,
gun,
provisionDefinitions,
provisionDefinitions
);
const consumablesList = Object.values(
consumableDefinitions.consumables,
consumableDefinitions.consumables
).filter((consumable) =>
checkConsumableProvisionInclusivity(consumable, tank, gun),
checkConsumableProvisionInclusivity(consumable, tank, gun)
);
return (
@@ -205,7 +204,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button variant="ghost">
<Flex align="center">
{(
['turret', 'gun', 'engine', 'chassis'] as const
["turret", "gun", "engine", "chassis"] as const
).map((module, index) => (
<img
key={module}
@@ -215,7 +214,7 @@ export function CompareTable({ stats }: CompareTableProps) {
src={asset(`icons/modules/${module}.webp`)}
style={{
marginLeft: index > 0 ? -8 : 0,
objectFit: 'contain',
objectFit: "contain",
}}
/>
))}
@@ -232,7 +231,7 @@ export function CompareTable({ stats }: CompareTableProps) {
engine={engine}
track={track}
onChange={(modules) => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index] = {
...draft.members[index],
...modules,
@@ -245,7 +244,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
const member = draft.members[index];
member.turret = member.tank.turrets[0];
@@ -261,7 +260,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
const member = draft.members[index];
member.turret = member.tank.turrets.at(-1)!;
@@ -288,9 +287,9 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="ghost"
radius="large"
style={{
height: '100%',
height: "100%",
width: 12,
position: 'relative',
position: "relative",
}}
>
<Flex direction="column" justify="center">
@@ -298,14 +297,14 @@ export function CompareTable({ stats }: CompareTableProps) {
<img
alt={unwrap(
provisionDefinitions.provisions[provision]
.name,
.name
)}
key={provision}
src={asset(
`/icons/provisions/${provision}.webp`,
`/icons/provisions/${provision}.webp`
)}
style={{
left: '50%',
left: "50%",
/**
* max = 100% - 16px - 4px
* min = 4px
@@ -320,11 +319,11 @@ export function CompareTable({ stats }: CompareTableProps) {
? 0.5
: index / (provisions.length - 1)
} * (100% - 24px))`,
transform: 'translateX(-50%)',
position: 'absolute',
transform: "translateX(-50%)",
position: "absolute",
width: 16,
height: 16,
objectFit: 'contain',
objectFit: "contain",
}}
/>
))}
@@ -339,12 +338,12 @@ export function CompareTable({ stats }: CompareTableProps) {
<Popover.Content>
<ProvisionsManager
provisions={provisionsList.map(
(provision) => provision.id,
(provision) => provision.id
)}
selected={provisions}
disabled={tank.max_provisions === provisions.length}
onChange={(provisions) => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].provisions = provisions;
});
}}
@@ -353,7 +352,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].provisions = [];
});
}}
@@ -394,8 +393,8 @@ export function CompareTable({ stats }: CompareTableProps) {
height: 8,
backgroundColor:
equipment === -1
? Var('crimson-11')
: Var('gray-11'),
? Var("crimson-11")
: Var("gray-11"),
borderRadius: 2,
}}
/>
@@ -405,8 +404,8 @@ export function CompareTable({ stats }: CompareTableProps) {
height: 8,
backgroundColor:
equipment === 1
? Var('crimson-11')
: Var('gray-11'),
? Var("crimson-11")
: Var("gray-11"),
borderRadius: 2,
}}
/>
@@ -424,7 +423,7 @@ export function CompareTable({ stats }: CompareTableProps) {
matrix={equipmentMatrix}
preset={equipmentPreset}
onChange={(matrix) => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].equipmentMatrix = matrix;
draft.sorting = undefined;
});
@@ -436,10 +435,10 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="ghost"
color="red"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].equipmentMatrix = times(
3,
() => times(3, () => 0),
() => times(3, () => 0)
) as EquipmentMatrix;
draft.sorting = undefined;
});
@@ -453,7 +452,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members.forEach((member) => {
member.equipmentMatrix =
draft.members[index].equipmentMatrix;
@@ -477,9 +476,9 @@ export function CompareTable({ stats }: CompareTableProps) {
variant="ghost"
radius="large"
style={{
height: '100%',
height: "100%",
width: 12,
position: 'relative',
position: "relative",
}}
>
<Flex direction="column" justify="center">
@@ -488,14 +487,14 @@ export function CompareTable({ stats }: CompareTableProps) {
alt={unwrap(
consumableDefinitions.consumables[
consumable
].name,
].name
)}
key={consumable}
src={asset(
`/icons/consumables/${consumable}.webp`,
`/icons/consumables/${consumable}.webp`
)}
style={{
left: '50%',
left: "50%",
/**
* max = 100% - 16px - 4px
* min = 4px
@@ -510,11 +509,11 @@ export function CompareTable({ stats }: CompareTableProps) {
? 0.5
: index / (consumables.length - 1)
} * (100% - 24px))`,
transform: 'translateX(-50%)',
position: 'absolute',
transform: "translateX(-50%)",
position: "absolute",
width: 16,
height: 16,
objectFit: 'contain',
objectFit: "contain",
}}
/>
))}
@@ -531,7 +530,7 @@ export function CompareTable({ stats }: CompareTableProps) {
consumables={consumablesList}
selected={consumables}
onConsumablesChange={(consumables) => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].consumables = consumables;
});
}}
@@ -544,7 +543,7 @@ export function CompareTable({ stats }: CompareTableProps) {
<Button
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].consumables = [];
});
}}
@@ -562,18 +561,18 @@ export function CompareTable({ stats }: CompareTableProps) {
{!tank.fixed_camouflage && (
<IconButton
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index].camouflage = !camouflage;
});
}}
variant={camouflage ? 'solid' : 'outline'}
variant={camouflage ? "solid" : "outline"}
>
<img
src={asset('icons/camo.webp')}
src={asset("icons/camo.webp")}
style={{
width: '1em',
height: '1em',
transform: 'scale(2) translate(15%, 15%)',
width: "1em",
height: "1em",
transform: "scale(2) translate(15%, 15%)",
}}
/>
</IconButton>
@@ -581,7 +580,7 @@ export function CompareTable({ stats }: CompareTableProps) {
</Flex>
</Table.Cell>
);
},
}
)}
</Table.Row>
</Table.Body>
@@ -1,33 +1,27 @@
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale';
import { CompareEphemeral } from '../../stores/compareEphemeral';
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import {
ComparePersistent,
type DeltaMode,
} from '../../stores/comparePersistent';
import { TankSearch } from '../TankSearch';
} from "../../stores/comparePersistent";
import { TankSearch } from "../TankSearch";
interface ControlsProps {
addTankDialogOpen: boolean;
onAddTankDialogOpenChange: (open: boolean) => void;
}
const [provisionDefinitions, modelDefinitions] = await Promise.all([
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
const provisionDefinitions = await awaitableProvisionDefinitions;
export function Controls({
addTankDialogOpen,
onAddTankDialogOpenChange,
}: ControlsProps) {
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
const mutateCompareEphemeral = CompareEphemeral.useMutation();
const mutateComparePersistent = ComparePersistent.useMutation();
const { strings } = useLocale();
return (
@@ -60,28 +54,20 @@ export function Controls({
<TankSearch
compact
onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members.push(
tankToCompareMember(tank, model, provisionDefinitions),
tankToCompareMember(tank, provisionDefinitions)
);
draft.sorting = undefined;
});
onAddTankDialogOpenChange(false);
}}
onSelectAll={(tanks) => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members.push(
...tanks.map((tank) => {
const model = modelDefinitions.models[tank.id];
return tankToCompareMember(
tank,
model,
provisionDefinitions,
);
}),
return tankToCompareMember(tank, provisionDefinitions);
})
);
draft.sorting = undefined;
});
@@ -97,7 +83,7 @@ export function Controls({
variant="soft"
color="red"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members = [];
draft.sorting = undefined;
});
@@ -110,18 +96,18 @@ export function Controls({
variant="classic"
value={deltaMode}
onValueChange={(value) => {
mutateComparePersistent((draft) => {
ComparePersistent.mutate((draft) => {
draft.deltaMode = value as DeltaMode;
});
}}
>
<SegmentedControl.Item value={'none' satisfies DeltaMode}>
<SegmentedControl.Item value={"none" satisfies DeltaMode}>
{strings.website.tools.compare.actions.deltas.none}
</SegmentedControl.Item>
<SegmentedControl.Item value={'percentage' satisfies DeltaMode}>
<SegmentedControl.Item value={"percentage" satisfies DeltaMode}>
{strings.website.tools.compare.actions.deltas.percentage}
</SegmentedControl.Item>
<SegmentedControl.Item value={'absolute' satisfies DeltaMode}>
<SegmentedControl.Item value={"absolute" satisfies DeltaMode}>
{strings.website.tools.compare.actions.deltas.absolute}
</SegmentedControl.Item>
</SegmentedControl.Root>
@@ -1,13 +1,13 @@
import { type TankDefinition, tankIcon } from '@blitzkit/core';
import { Flex, Text } from '@radix-ui/themes';
import { useEffect, useRef } from 'react';
import { Vector2 } from 'three';
import { useLocale } from '../../hooks/useLocale';
import { CompareEphemeral } from '../../stores/compareEphemeral';
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
import { InsertionMarker } from './IntersectionMarker';
import { insertionMarkers } from './IntersectionMarker/constants';
import { TankControl } from './TankControl';
import { type TankDefinition, tankIcon } from "@blitzkit/core";
import { Flex, Text } from "@radix-ui/themes";
import { useEffect, useRef } from "react";
import { Vector2 } from "three";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { InsertionMarker } from "./IntersectionMarker";
import { insertionMarkers } from "./IntersectionMarker/constants";
import { TankControl } from "./TankControl";
interface TankCardProps {
index: number;
@@ -16,7 +16,6 @@ interface TankCardProps {
export function TankCard({ index, tank }: TankCardProps) {
const draggable = useRef<HTMLDivElement>(null);
const mutateCompareEphemeral = CompareEphemeral.useMutation();
const { unwrap } = useLocale();
useEffect(() => {
@@ -34,8 +33,8 @@ export function TankCard({ index, tank }: TankCardProps) {
delta.set(0, 0);
current.copy(initial.set(event.clientX, event.clientY));
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
event.preventDefault();
@@ -43,7 +42,7 @@ export function TankCard({ index, tank }: TankCardProps) {
current.set(event.clientX, event.clientY);
delta.copy(current).sub(initial);
insertionMarkers.forEach(({ element }) => {
element.style.opacity = '0';
element.style.opacity = "0";
});
const distances = insertionMarkers.map((insertionMarker) => {
@@ -56,34 +55,34 @@ export function TankCard({ index, tank }: TankCardProps) {
};
});
const closest = distances.reduce((a, b) =>
a.distance < b.distance ? a : b,
a.distance < b.distance ? a : b
);
dropIndex = closest.index;
closest.element.style.opacity = '1';
draggable.current!.style.position = 'fixed';
draggable.current!.style.cursor = 'grabbing';
draggable.current!.style.zIndex = '1';
closest.element.style.opacity = "1";
draggable.current!.style.position = "fixed";
draggable.current!.style.cursor = "grabbing";
draggable.current!.style.zIndex = "1";
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
}
function handlePointerUp(event: PointerEvent) {
event.preventDefault();
draggable.current!.style.position = 'static';
draggable.current!.style.zIndex = 'unset';
draggable.current!.style.cursor = 'grab';
draggable.current!.style.position = "static";
draggable.current!.style.zIndex = "unset";
draggable.current!.style.cursor = "grab";
insertionMarkers.forEach(({ element }) => {
element.style.opacity = '0';
element.style.opacity = "0";
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
if (dropIndex === -1) return;
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
const fromIndex = index;
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
@@ -95,10 +94,10 @@ export function TankCard({ index, tank }: TankCardProps) {
dropIndex = -1;
}
draggable.current?.addEventListener('pointerdown', handlePointerDown);
draggable.current?.addEventListener("pointerdown", handlePointerDown);
return () => {
draggable.current?.removeEventListener('pointerdown', handlePointerDown);
draggable.current?.removeEventListener("pointerdown", handlePointerDown);
};
}, []);
@@ -114,9 +113,9 @@ export function TankCard({ index, tank }: TankCardProps) {
justify="between"
gap="2"
style={{
userSelect: 'none',
touchAction: 'none',
cursor: 'grab',
userSelect: "none",
touchAction: "none",
cursor: "grab",
}}
>
<img
@@ -126,16 +125,16 @@ export function TankCard({ index, tank }: TankCardProps) {
height={64}
draggable={false}
style={{
objectFit: 'contain',
objectFit: "contain",
}}
/>
<Text
style={{
whiteSpace: 'nowrap',
whiteSpace: "nowrap",
maxWidth: 128,
overflow: 'hidden',
textOverflow: 'ellipsis',
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{unwrap(tank.name)}
@@ -1,35 +1,30 @@
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
import { useState } from 'react';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale';
import { CompareEphemeral } from '../../stores/compareEphemeral';
import { LinkI18n } from '../LinkI18n';
import { TankSearch } from '../TankSearch';
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
import { Dialog, Flex, IconButton } from "@radix-ui/themes";
import { useState } from "react";
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import { LinkI18n } from "../LinkI18n";
import { TankSearch } from "../TankSearch";
interface TankControlProps {
index: number;
slug: string;
}
const [provisionDefinitions, modelDefinitions] = await Promise.all([
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
const provisionDefinitions = await awaitableProvisionDefinitions;
export function TankControl({ index, slug }: TankControlProps) {
const { locale } = useLocale();
const { locale, strings } = useLocale();
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
const mutateCompareEphemeral = CompareEphemeral.useMutation();
return (
<Flex gap="3" justify="center" style={{ width: '100%' }}>
<Flex gap="3" justify="center" style={{ width: "100%" }}>
<IconButton
variant="ghost"
onClick={() => {
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members.splice(index, 1);
draft.sorting = undefined;
});
@@ -49,7 +44,9 @@ export function TankControl({ index, slug }: TankControlProps) {
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title align="center">Switch tanks</Dialog.Title>
<Dialog.Title align="center">
{strings.website.common.tank_search.swap_dialog_title}
</Dialog.Title>
<Flex gap="4" direction="column">
<Flex
@@ -61,13 +58,10 @@ export function TankControl({ index, slug }: TankControlProps) {
<TankSearch
compact
onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => {
CompareEphemeral.mutate((draft) => {
draft.members[index] = tankToCompareMember(
tank,
model,
provisionDefinitions,
provisionDefinitions
);
draft.sorting = undefined;
});
Loaded 100 of 284 files, more files were not shown because too many files have changed in this diff. Show more