Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
907e71ce03 | ||
|
|
10942879a6 | ||
|
|
d60551d30a | ||
|
|
b8c8528e1c | ||
|
|
0c9643016a | ||
|
|
61d1ebbad6 | ||
|
|
e9b7059cc0 | ||
|
|
a359897a2a | ||
|
|
e5f7d72df3 | ||
|
|
57092d8df0 | ||
|
|
86508d5413 | ||
|
|
33373c286f | ||
|
|
7443f75347 | ||
|
|
d09f934b80 | ||
|
|
7f919ebb87 | ||
|
|
0929825555 | ||
|
|
4de0fc5ec1 | ||
|
|
e66610f86e | ||
|
|
674abc4923 | ||
|
|
25cee8a36b | ||
|
|
dcbac5e017 | ||
|
|
0c94d42f3f | ||
|
|
7ab5e94323 | ||
|
|
f9ea5ba74a | ||
|
|
1e7162d2da | ||
|
|
cfba53a655 | ||
|
|
77a85641f0 | ||
|
|
5330a27e96 | ||
|
|
a791aff4a0 | ||
|
|
df8a5967f0 | ||
|
|
6261dfc9cd | ||
|
|
766801826c | ||
|
|
7b728596ca | ||
|
|
0bf6ac33a5 | ||
|
|
9a33b84b1c | ||
|
|
c14efe86cc | ||
|
|
4ff9a2ec7f | ||
|
|
75d7b4554b | ||
|
|
27c8b348bb | ||
|
|
a55ab5e7ee | ||
|
|
12e90cc6ee | ||
|
|
01755a2a64 | ||
|
|
080f0a0eb7 | ||
|
|
a646cc1e57 | ||
|
|
d107813c37 | ||
|
|
27d3bbf346 | ||
|
|
675a613a0e | ||
|
|
50c77e0ae1 | ||
|
|
6e0429332d | ||
|
|
7afce6cbcb | ||
|
|
17c7a5c7f5 | ||
|
|
f5aa2ff9de | ||
|
|
96a8f225fc | ||
|
|
d15a5aee69 | ||
|
|
84dc5e4960 | ||
|
|
58b5ee7f83 | ||
|
|
8a99d34af9 | ||
|
|
7cd7bd83dc | ||
|
|
c29d36a4b7 | ||
|
|
7d3b6221d7 | ||
|
|
fd66f9ab17 | ||
|
|
6fbb0e81cb | ||
|
|
6dc82588c1 | ||
|
|
c6eae9ad51 | ||
|
|
ebdd7629ff | ||
|
|
646bccf370 | ||
|
|
86206f2d89 | ||
|
|
bc0dd87e31 | ||
|
|
e26bb4efe0 | ||
|
|
66cf1fd223 | ||
|
|
335cd4d699 | ||
|
|
95fa5bc1a5 | ||
|
|
50eac9eba6 | ||
|
|
1598ae973f | ||
|
|
69f3a95379 | ||
|
|
3846cbab4a | ||
|
|
30e87743fc | ||
|
|
fe4afdcbf0 | ||
|
|
ef2a47e756 | ||
|
|
c2d625e1ed | ||
|
|
d33990bdb4 | ||
|
|
96482d4268 | ||
|
|
8b527f2333 | ||
|
|
f56c31fb48 | ||
|
|
7d164e415a | ||
|
|
b3a9f0889e | ||
|
|
6d24ed92d0 | ||
|
|
d287e66265 | ||
|
|
ac29d1ead0 | ||
|
|
b6c3fdc4e9 | ||
|
|
4efdaaf7c9 | ||
|
|
87b6a36520 | ||
|
|
cf38cb3a6e | ||
|
|
97fc386bc7 | ||
|
|
abfb3f8ddb | ||
|
|
9cb5113f6e | ||
|
|
7dd1c0c32a | ||
|
|
03233f3fdc | ||
|
|
377159c298 | ||
|
|
d8431fb620 | ||
|
|
0546ddefbd | ||
|
|
26f6889012 | ||
|
|
3ad6088565 | ||
|
|
b6fd568cc6 | ||
|
|
3861b07aef | ||
|
|
2bf57e2c72 | ||
|
|
744c6654cf | ||
|
|
b82e905757 | ||
|
|
c42e5b7dc3 | ||
|
|
b716218ecd | ||
|
|
511346e4bb | ||
|
|
5c2e49453e | ||
|
|
5d397de766 | ||
|
|
aa763f65d1 | ||
|
|
94cbd95509 | ||
|
|
f3762e356f | ||
|
|
5747d7549f | ||
|
|
aea4905d20 |
No files matched your search
@@ -22,6 +22,19 @@ env:
|
|||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
||||||
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
||||||
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
|
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
|
||||||
|
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
||||||
|
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
|
||||||
|
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
||||||
|
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||||
|
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||||
|
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
|
||||||
|
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
|
||||||
|
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||||
|
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
||||||
|
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
||||||
|
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
|
||||||
|
SERVICING: ${{ secrets.SERVICING }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
host:
|
host:
|
||||||
|
|||||||
@@ -24,7 +24,32 @@ public/sitemap*
|
|||||||
target
|
target
|
||||||
# packages/core/src/protos/*.ts
|
# packages/core/src/protos/*.ts
|
||||||
/.vercel
|
/.vercel
|
||||||
/.astro
|
.astro
|
||||||
.vercel
|
.vercel
|
||||||
/src
|
/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?
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"AYUV",
|
"AYUV",
|
||||||
"battleresults",
|
"battleresults",
|
||||||
"bbox",
|
"bbox",
|
||||||
|
"beforeinstallprompt",
|
||||||
"bepaint",
|
"bepaint",
|
||||||
"betakrieg",
|
"betakrieg",
|
||||||
"BINORMAL",
|
"BINORMAL",
|
||||||
@@ -76,7 +77,9 @@
|
|||||||
"echarts",
|
"echarts",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
"FASTNAME",
|
"FASTNAME",
|
||||||
|
"favicons",
|
||||||
"flexdirection",
|
"flexdirection",
|
||||||
|
"fontsource",
|
||||||
"FOURCC",
|
"FOURCC",
|
||||||
"frameloop",
|
"frameloop",
|
||||||
"fsformat",
|
"fsformat",
|
||||||
@@ -154,6 +157,7 @@
|
|||||||
"Pyogenics",
|
"Pyogenics",
|
||||||
"ratingleaderboard",
|
"ratingleaderboard",
|
||||||
"raycaster",
|
"raycaster",
|
||||||
|
"Redirector",
|
||||||
"researchable",
|
"researchable",
|
||||||
"Resvg",
|
"Resvg",
|
||||||
"rizz",
|
"rizz",
|
||||||
@@ -167,6 +171,7 @@
|
|||||||
"searchtanks",
|
"searchtanks",
|
||||||
"SHAREDEXP",
|
"SHAREDEXP",
|
||||||
"SKLLD",
|
"SKLLD",
|
||||||
|
"smoothstep",
|
||||||
"Smri",
|
"Smri",
|
||||||
"Specularity",
|
"Specularity",
|
||||||
"SSAO",
|
"SSAO",
|
||||||
@@ -186,6 +191,7 @@
|
|||||||
"tankopedia",
|
"tankopedia",
|
||||||
"tankopedias",
|
"tankopedias",
|
||||||
"tankstats",
|
"tankstats",
|
||||||
|
"tauri",
|
||||||
"texconv",
|
"texconv",
|
||||||
"TEXCOORD",
|
"TEXCOORD",
|
||||||
"tresabhi",
|
"tresabhi",
|
||||||
@@ -222,12 +228,7 @@
|
|||||||
"yolo",
|
"yolo",
|
||||||
"youtuber",
|
"youtuber",
|
||||||
"youtubers",
|
"youtubers",
|
||||||
"zoomable",
|
"zoomable"
|
||||||
"zustand"
|
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
"deno.unstable": false,
|
|
||||||
"cSpell.enableFiletypes": ["glsl", "proto3"],
|
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
|
||||||
"liveServer.settings.multiRootWorkspaceName": "blitzkit"
|
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
|
|
||||||

|
## 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
|
||||||
|
|
||||||

|
| 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` | | | |
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# BlitzKit 1.27.18
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
|
||||||
|
- Switched to a new renderer for better lighting for the Tankopedia
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The loading bar for the Tankopedia sandbox now shows up again
|
||||||
|
- Removed a lot of dead code, improving performance
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# BlitzKit 1.27.19
|
||||||
|
|
||||||
|
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The camera is now lower down in the Tankopedia, giving the tank a more heroic feel
|
||||||
|
- Tanks are better framed across all devices, minimizing whitespace
|
||||||
|
- Added a skip button for the reload visualizer to skip to the next event
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed the useless "show grid" setting
|
||||||
|
- Taking screenshots now no longer includes the scene props like the floor
|
||||||
|
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
|
||||||
|
- The shot display card is now properly aligned as a proper table again
|
||||||
|
- Tank modules are once again considered in the penetration tester
|
||||||
|
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# BlitzKit 1.28.1
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The lights no longer crash the Mixer tool
|
||||||
@@ -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
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.17",
|
"version": "1.28.2",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types.d.ts",
|
"types": "./types.d.ts",
|
||||||
@@ -16,11 +16,14 @@
|
|||||||
"build:prisma": "prisma generate",
|
"build:prisma": "prisma generate",
|
||||||
"build:sitemap": "bun scripts/sitemap.ts",
|
"build:sitemap": "bun scripts/sitemap.ts",
|
||||||
"build:docs": "vitepress build docs",
|
"build:docs": "vitepress build docs",
|
||||||
|
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
|
||||||
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
|
"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",
|
"dev:bot": "bun run build:bot --dev && bun start:bot",
|
||||||
"start:bot": "bun dist/bot/main.js",
|
"start:bot": "bun dist/bot/main.js",
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"dev:website": "cd packages/website && bun run dev",
|
"dev:website": "cd packages/website && bun run dev",
|
||||||
|
"dev:app": "cd packages/app && bun run dev",
|
||||||
"dev": "bun run dev:website",
|
"dev": "bun run dev:website",
|
||||||
"dev:api": "bun --filter=@blitzkit/api run dev",
|
"dev:api": "bun --filter=@blitzkit/api run dev",
|
||||||
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
|
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
|
||||||
@@ -41,19 +44,22 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/xmpp__client": "^0.13.3",
|
||||||
|
"@xmpp/client": "^0.13.6",
|
||||||
|
"astro-favicons": "^3.1.5",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.1.2",
|
||||||
"googleapis": "^144.0.0",
|
"googleapis": "^160.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"protobufjs": "^7.5.2",
|
"protobufjs": "^7.5.4",
|
||||||
"quicklime": "^1.2.0",
|
"quicklime": "^1.2.0",
|
||||||
"sharp": "^0.33.5"
|
"sharp": "^0.34.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "^1.2.22",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.14.1",
|
"@types/node": "^24.5.2",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.2",
|
||||||
"prisma": "^6.6.0",
|
"prisma": "^6.16.2",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
"satori": "^0.10.14",
|
"satori": "^0.18.3",
|
||||||
"@resvg/resvg-js": "^2.6.2",
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"discord.js": "^14.16.3",
|
"discord.js": "^14.22.1",
|
||||||
"react": "^18.3.1",
|
"react": "^19.1.1",
|
||||||
"@radix-ui/colors": "^3.0.0",
|
"@radix-ui/colors": "^3.0.0",
|
||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^3.1.0",
|
||||||
"bepaint": "^0.1.0-beta.3",
|
"bepaint": "^0.1.0-beta.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^17.2.2",
|
||||||
"markdown-escape": "^2.0.0"
|
"markdown-escape": "^2.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ import {
|
|||||||
getTankStats,
|
getTankStats,
|
||||||
getWN8Percentile,
|
getWN8Percentile,
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from "@blitzkit/i18n";
|
||||||
import { Locale } from 'discord.js';
|
import { Locale } from "discord.js";
|
||||||
import { chunk } from 'lodash-es';
|
import { chunk } from "lodash-es";
|
||||||
import * as Breakdown from '../../components/Breakdown';
|
import * as Breakdown from "../../components/Breakdown";
|
||||||
import { CommandWrapper } from '../../components/CommandWrapper';
|
import { CommandWrapper } from "../../components/CommandWrapper";
|
||||||
import { NoData } from '../../components/NoData';
|
import { NoData } from "../../components/NoData";
|
||||||
import { TitleBar } from '../../components/TitleBar';
|
import { TitleBar } from "../../components/TitleBar";
|
||||||
import { filtersToDescription } from '../../core/blitzkit/filtersToDescription';
|
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
|
||||||
import { tankDefinitions } from '../../core/blitzkit/nonBlockingPromises';
|
import { tankDefinitions } from "../../core/blitzkit/nonBlockingPromises";
|
||||||
import { UserError } from '../../core/blitzkit/userError';
|
import { UserError } from "../../core/blitzkit/userError";
|
||||||
import { filterStats, StatFilters } from '../../core/blitzstars/filterStats';
|
import { filterStats, StatFilters } from "../../core/blitzstars/filterStats";
|
||||||
import { getStatsInPeriod } from '../../core/blitzstars/getStatsInPeriod';
|
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
|
||||||
import { blitzStarsTankAverages } from '../../core/blitzstars/tankAverages';
|
import { blitzStarsTankAverages } from "../../core/blitzstars/tankAverages";
|
||||||
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
|
import { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
|
||||||
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand';
|
import { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
|
||||||
import { translator } from '../../core/localization/translator';
|
import { translator } from "../../core/localization/translator";
|
||||||
|
|
||||||
const ROWS_PER_PAGE = 8;
|
const ROWS_PER_PAGE = 8;
|
||||||
const MAX_PAGES = 9;
|
const MAX_PAGES = 9;
|
||||||
@@ -31,7 +31,7 @@ export async function renderBreakdown(
|
|||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
{ start, end, name }: ResolvedPeriod,
|
{ start, end, name }: ResolvedPeriod,
|
||||||
filters: StatFilters,
|
filters: StatFilters,
|
||||||
locale: Locale,
|
locale: Locale
|
||||||
) {
|
) {
|
||||||
const { strings, unwrap } = translator(locale);
|
const { strings, unwrap } = translator(locale);
|
||||||
const awaitedTankDefinitions = await tankDefinitions;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
@@ -39,7 +39,7 @@ export async function renderBreakdown(
|
|||||||
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
|
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
|
||||||
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
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 tankStats = await getTankStats(region, id);
|
||||||
const filterDescriptions = await filtersToDescription(filters, locale);
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
const orderedCurrentStats: BlitzStats[] = [];
|
const orderedCurrentStats: BlitzStats[] = [];
|
||||||
@@ -74,63 +74,63 @@ export async function renderBreakdown(
|
|||||||
orderedCareerWN8Full.push(
|
orderedCareerWN8Full.push(
|
||||||
tankAveragesEntry
|
tankAveragesEntry
|
||||||
? calculateWN8(tankAveragesEntry.all, tank.all)
|
? calculateWN8(tankAveragesEntry.all, tank.all)
|
||||||
: undefined,
|
: undefined
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentBattles = orderedCurrentStats.reduce(
|
const currentBattles = orderedCurrentStats.reduce(
|
||||||
(accumulator, current) => accumulator + current.battles,
|
(accumulator, current) => accumulator + current.battles,
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const careerBattles = tankStats.reduce(
|
const careerBattles = tankStats.reduce(
|
||||||
(accumulator, current) => accumulator + current.all.battles,
|
(accumulator, current) => accumulator + current.all.battles,
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const currentWinrate =
|
const currentWinrate =
|
||||||
orderedCurrentStats.reduce(
|
orderedCurrentStats.reduce(
|
||||||
(accumulator, current) => accumulator + current.wins,
|
(accumulator, current) => accumulator + current.wins,
|
||||||
0,
|
0
|
||||||
) / currentBattles;
|
) / currentBattles;
|
||||||
const careerWinrate =
|
const careerWinrate =
|
||||||
tankStats.reduce(
|
tankStats.reduce(
|
||||||
(accumulator, current) => accumulator + current.all.wins,
|
(accumulator, current) => accumulator + current.all.wins,
|
||||||
0,
|
0
|
||||||
) / careerBattles;
|
) / careerBattles;
|
||||||
const currentWN8 =
|
const currentWN8 =
|
||||||
orderedCurrentWN8.reduce<number>(
|
orderedCurrentWN8.reduce<number>(
|
||||||
(accumulator, current, index) =>
|
(accumulator, current, index) =>
|
||||||
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
|
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
|
||||||
0,
|
0
|
||||||
) /
|
) /
|
||||||
orderedCurrentStats.reduce(
|
orderedCurrentStats.reduce(
|
||||||
(accumulator, current, index) =>
|
(accumulator, current, index) =>
|
||||||
accumulator +
|
accumulator +
|
||||||
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
|
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const careerWN8 =
|
const careerWN8 =
|
||||||
orderedCareerWN8Full.reduce<number>(
|
orderedCareerWN8Full.reduce<number>(
|
||||||
(accumulator, current) => accumulator + (current ?? 0),
|
(accumulator, current) => accumulator + (current ?? 0),
|
||||||
0,
|
0
|
||||||
) /
|
) /
|
||||||
tankStats.reduce(
|
tankStats.reduce(
|
||||||
(accumulator, current, index) =>
|
(accumulator, current, index) =>
|
||||||
accumulator +
|
accumulator +
|
||||||
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
|
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const currentDamage =
|
const currentDamage =
|
||||||
orderedCurrentStats.reduce(
|
orderedCurrentStats.reduce(
|
||||||
(accumulator, current) => accumulator + current.damage_dealt,
|
(accumulator, current) => accumulator + current.damage_dealt,
|
||||||
0,
|
0
|
||||||
) / currentBattles;
|
) / currentBattles;
|
||||||
const careerDamage =
|
const careerDamage =
|
||||||
tankStats.reduce(
|
tankStats.reduce(
|
||||||
(accumulator, current) => accumulator + current.all.damage_dealt,
|
(accumulator, current) => accumulator + current.all.damage_dealt,
|
||||||
0,
|
0
|
||||||
) / careerBattles;
|
) / careerBattles;
|
||||||
|
|
||||||
const children: JSX.Element[] = [];
|
const children: React.JSX.Element[] = [];
|
||||||
|
|
||||||
if (filteredOrder.length > 0) {
|
if (filteredOrder.length > 0) {
|
||||||
children.push(
|
children.push(
|
||||||
@@ -172,7 +172,7 @@ export async function renderBreakdown(
|
|||||||
delta: currentDamage - careerDamage,
|
delta: currentDamage - careerDamage,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
filteredOrder.forEach((id, index) => {
|
filteredOrder.forEach((id, index) => {
|
||||||
@@ -206,7 +206,7 @@ export async function renderBreakdown(
|
|||||||
{
|
{
|
||||||
title: strings.bot.commands.breakdown.body.winrate,
|
title: strings.bot.commands.breakdown.body.winrate,
|
||||||
current: `${(100 * (current.wins / current.battles)).toFixed(
|
current: `${(100 * (current.wins / current.battles)).toFixed(
|
||||||
2,
|
2
|
||||||
)}%`,
|
)}%`,
|
||||||
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
|
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
|
||||||
delta:
|
delta:
|
||||||
@@ -230,24 +230,24 @@ export async function renderBreakdown(
|
|||||||
{
|
{
|
||||||
title: strings.bot.commands.breakdown.body.damage,
|
title: strings.bot.commands.breakdown.body.damage,
|
||||||
current: Math.round(
|
current: Math.round(
|
||||||
current.damage_dealt / current.battles,
|
current.damage_dealt / current.battles
|
||||||
).toLocaleString(),
|
).toLocaleString(),
|
||||||
career: Math.round(
|
career: Math.round(
|
||||||
career.damage_dealt / career.battles,
|
career.damage_dealt / career.battles
|
||||||
).toLocaleString(),
|
).toLocaleString(),
|
||||||
delta:
|
delta:
|
||||||
current.damage_dealt / current.battles -
|
current.damage_dealt / current.battles -
|
||||||
career.damage_dealt / career.battles,
|
career.damage_dealt / career.battles,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages = chunk(
|
const pages = chunk(
|
||||||
children,
|
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) {
|
if (filteredOrder.length > 0) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { jsxToSvg } from './jsxToSvg';
|
import { jsxToSvg } from "./jsxToSvg";
|
||||||
import { svgToPng } from './svgToPng';
|
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 svg = await jsxToSvg(jsx);
|
||||||
const png = svgToPng(svg);
|
const png = svgToPng(svg);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { jsxToSvg } from './jsxToSvg';
|
import { jsxToSvg } from "./jsxToSvg";
|
||||||
import { RenderConfiguration } from './renderConfiguration';
|
import { RenderConfiguration } from "./renderConfiguration";
|
||||||
import { svgToPngThreaded } from './svgToPngThreaded';
|
import { svgToPngThreaded } from "./svgToPngThreaded";
|
||||||
|
|
||||||
export async function jsxToPngThreaded(
|
export async function jsxToPngThreaded(
|
||||||
jsx: JSX.Element,
|
jsx: React.JSX.Element,
|
||||||
renderConfiguration: RenderConfiguration,
|
renderConfiguration: RenderConfiguration
|
||||||
) {
|
) {
|
||||||
const svg = await jsxToSvg(jsx, renderConfiguration);
|
const svg = await jsxToSvg(jsx, renderConfiguration);
|
||||||
const png = await svgToPngThreaded(svg);
|
const png = await svgToPngThreaded(svg);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { readFile } from 'fs/promises';
|
import { readFile } from "fs/promises";
|
||||||
import satori from 'satori';
|
import satori from "satori";
|
||||||
import { RenderConfiguration } from '../renderConfiguration';
|
import { RenderConfiguration } from "../renderConfiguration";
|
||||||
import robotoBlack from './Roboto-Black.ttf';
|
import robotoBlack from "./Roboto-Black.ttf";
|
||||||
import robotoBold from './Roboto-Bold.ttf';
|
import robotoBold from "./Roboto-Bold.ttf";
|
||||||
import roboto from './Roboto.ttf';
|
import roboto from "./Roboto.ttf";
|
||||||
|
|
||||||
const fontsPromise = [
|
const fontsPromise = [
|
||||||
{ file: roboto, weight: 400 as const },
|
{ file: roboto, weight: 400 as const },
|
||||||
@@ -11,12 +11,12 @@ const fontsPromise = [
|
|||||||
{ file: robotoBlack, weight: 900 as const },
|
{ file: robotoBlack, weight: 900 as const },
|
||||||
].map(async ({ file, weight }) => {
|
].map(async ({ file, weight }) => {
|
||||||
const data = await readFile(`${__dirname}/${file}`);
|
const data = await readFile(`${__dirname}/${file}`);
|
||||||
return { name: 'Roboto', weight, data };
|
return { name: "Roboto", weight, data };
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function jsxToSvg(
|
export async function jsxToSvg(
|
||||||
jsx: JSX.Element,
|
jsx: React.JSX.Element,
|
||||||
renderConfiguration = new RenderConfiguration(),
|
renderConfiguration = new RenderConfiguration()
|
||||||
) {
|
) {
|
||||||
const fonts = await Promise.all(fontsPromise);
|
const fonts = await Promise.all(fontsPromise);
|
||||||
return await satori(jsx, { width: renderConfiguration.width, fonts });
|
return await satori(jsx, { width: renderConfiguration.width, fonts });
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import {
|
|||||||
InteractionEditReplyOptions,
|
InteractionEditReplyOptions,
|
||||||
InteractionReplyOptions,
|
InteractionReplyOptions,
|
||||||
MessageEditOptions,
|
MessageEditOptions,
|
||||||
} from 'discord.js';
|
} from "discord.js";
|
||||||
import { InteractionReturnable } from '../../events/interactionCreate';
|
import { InteractionReturnable } from "../../events/interactionCreate";
|
||||||
import { Writeable } from '../../types/writable';
|
import { Writeable } from "../../types/writable";
|
||||||
import { jsxToPngThreaded } from '../blitzkit/jsxToPngThreaded';
|
import { jsxToPngThreaded } from "../blitzkit/jsxToPngThreaded";
|
||||||
import { RenderConfiguration } from '../blitzkit/renderConfiguration';
|
import { RenderConfiguration } from "../blitzkit/renderConfiguration";
|
||||||
|
|
||||||
export async function normalizeInteractionReturnable(
|
export async function normalizeInteractionReturnable(
|
||||||
returnable: InteractionReturnable,
|
returnable: InteractionReturnable
|
||||||
) {
|
) {
|
||||||
const images: [number, Buffer][] = [];
|
const images: [number, Buffer][] = [];
|
||||||
const reply: InteractionEditReplyOptions &
|
const reply: InteractionEditReplyOptions &
|
||||||
@@ -27,7 +27,7 @@ export async function normalizeInteractionReturnable(
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
normalizedReturnable.map(async (item, index) => {
|
normalizedReturnable.map(async (item, index) => {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === "string") {
|
||||||
reply.content = item;
|
reply.content = item;
|
||||||
} else if (item instanceof EmbedBuilder) {
|
} else if (item instanceof EmbedBuilder) {
|
||||||
if (!reply.embeds) reply.embeds = [];
|
if (!reply.embeds) reply.embeds = [];
|
||||||
@@ -36,7 +36,7 @@ export async function normalizeInteractionReturnable(
|
|||||||
if (!reply.components)
|
if (!reply.components)
|
||||||
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
|
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
|
||||||
(reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents(
|
(reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents(
|
||||||
item,
|
item
|
||||||
);
|
);
|
||||||
} else if (item instanceof AttachmentBuilder) {
|
} else if (item instanceof AttachmentBuilder) {
|
||||||
if (!reply.files) reply.files = [];
|
if (!reply.files) reply.files = [];
|
||||||
@@ -49,14 +49,14 @@ export async function normalizeInteractionReturnable(
|
|||||||
const image = await jsxToPngThreaded(item, renderConfiguration);
|
const image = await jsxToPngThreaded(item, renderConfiguration);
|
||||||
images.push([index, image]);
|
images.push([index, image]);
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
images.sort(([a], [b]) => b - a);
|
images.sort(([a], [b]) => b - a);
|
||||||
if (!reply.files) reply.files = [];
|
if (!reply.files) reply.files = [];
|
||||||
images.forEach(([, image]) =>
|
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 locales from "@blitzkit/i18n/locales.json";
|
||||||
import { Locale } from 'discord.js';
|
import { Locale } from "discord.js";
|
||||||
|
|
||||||
const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
|
const SUPPORTED_LOCALES_DISCORD_MAP: Record<string, Locale> = {
|
||||||
en: Locale.EnglishUS,
|
en: Locale.EnglishUS,
|
||||||
es: Locale.SpanishES,
|
es: Locale.SpanishES,
|
||||||
pt: Locale.PortugueseBR,
|
pt: Locale.PortugueseBR,
|
||||||
@@ -14,12 +14,12 @@ const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries(
|
export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries(
|
||||||
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k]),
|
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k])
|
||||||
) as Record<Locale, SupportedLocale>;
|
) as Record<Locale, string>;
|
||||||
|
|
||||||
export const SUPPORTED_LOCALES_DISCORD = Object.values(
|
export const SUPPORTED_LOCALES_DISCORD = Object.values(
|
||||||
SUPPORTED_LOCALES_DISCORD_MAP,
|
SUPPORTED_LOCALES_DISCORD_MAP
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DEFAULT_LOCALE_DISCORD =
|
export const DEFAULT_LOCALE_DISCORD =
|
||||||
SUPPORTED_LOCALES_DISCORD_MAP[DEFAULT_LOCALE];
|
SUPPORTED_LOCALES_DISCORD_MAP[locales.default];
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ReadStream } from '@blitzkit/core';
|
import { ReadStream } from "@blitzkit/core";
|
||||||
import AdmZip from 'adm-zip';
|
import AdmZip from "adm-zip";
|
||||||
import { readFile, writeFile } from 'fs/promises';
|
import { readFile } from "fs/promises";
|
||||||
import { Parser } from 'pickleparser';
|
import { Parser } from "pickleparser";
|
||||||
import protobuf from 'protobufjs';
|
import protobuf from "protobufjs";
|
||||||
|
|
||||||
interface WotbReplayMetaJson {
|
interface WotbReplayMetaJson {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -24,26 +24,24 @@ export class WotbReplayReadStream extends ReadStream {
|
|||||||
async wotbReplay() {
|
async wotbReplay() {
|
||||||
const pickleParser = new Parser();
|
const pickleParser = new Parser();
|
||||||
const zip = new AdmZip(Buffer.from(this.buffer));
|
const zip = new AdmZip(Buffer.from(this.buffer));
|
||||||
const metaJsonBuffer = zip.getEntry('meta.json')?.getData();
|
const metaJsonBuffer = zip.getEntry("meta.json")?.getData();
|
||||||
const battleResultsDat = zip.getEntry('battle_results.dat')?.getData();
|
const battleResultsDat = zip.getEntry("battle_results.dat")?.getData();
|
||||||
|
|
||||||
if (!metaJsonBuffer || !battleResultsDat)
|
if (!metaJsonBuffer || !battleResultsDat)
|
||||||
throw new SyntaxError('meta.json not found');
|
throw new SyntaxError("meta.json not found");
|
||||||
|
|
||||||
const metaJson = JSON.parse(
|
const metaJson = JSON.parse(
|
||||||
metaJsonBuffer.toString(),
|
metaJsonBuffer.toString()
|
||||||
) as WotbReplayMetaJson;
|
) as WotbReplayMetaJson;
|
||||||
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
|
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
|
||||||
const [, battleResultsString] = unpickledBattleResults as [number, string];
|
const [, battleResultsString] = unpickledBattleResults as [number, string];
|
||||||
const root = await protobuf.load(
|
const root = await protobuf.load(
|
||||||
`${__dirname}/../../protos/BattleResultsClient.proto`,
|
`${__dirname}/../../protos/BattleResultsClient.proto`
|
||||||
);
|
);
|
||||||
const BattleResults = root.lookupType(
|
const BattleResults = root.lookupType(
|
||||||
'BattleResultsGenerated.BattleResultsClient',
|
"BattleResultsGenerated.BattleResultsClient"
|
||||||
);
|
);
|
||||||
|
|
||||||
writeFile('test.dat', Buffer.from(battleResultsString));
|
|
||||||
|
|
||||||
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
|
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
|
||||||
|
|
||||||
// const decodedBattleResults = BattleResults.decodeDelimited(
|
// 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);
|
const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer);
|
||||||
|
|
||||||
stream.wotbReplay();
|
stream.wotbReplay();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { assertSecret } from '@blitzkit/core';
|
import { assertSecret } from "@blitzkit/core";
|
||||||
import {
|
import {
|
||||||
AttachmentBuilder,
|
AttachmentBuilder,
|
||||||
AutocompleteInteraction,
|
AutocompleteInteraction,
|
||||||
@@ -13,30 +13,30 @@ import {
|
|||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
SlashCommandOptionsOnlyBuilder,
|
SlashCommandOptionsOnlyBuilder,
|
||||||
SlashCommandSubcommandsOnlyBuilder,
|
SlashCommandSubcommandsOnlyBuilder,
|
||||||
} from 'discord.js';
|
} from "discord.js";
|
||||||
import { aboutCommand } from '../../commands/about';
|
import { aboutCommand } from "../../commands/about";
|
||||||
import { breakdownCommand } from '../../commands/breakdown';
|
import { breakdownCommand } from "../../commands/breakdown";
|
||||||
import { debugCommand } from '../../commands/debug';
|
import { debugCommand } from "../../commands/debug";
|
||||||
import { evolutionCommand } from '../../commands/evolution';
|
import { evolutionCommand } from "../../commands/evolution";
|
||||||
import { fullStatsCommand } from '../../commands/fullStats';
|
import { fullStatsCommand } from "../../commands/fullStats";
|
||||||
import { inactiveCommand } from '../../commands/inactive';
|
import { inactiveCommand } from "../../commands/inactive";
|
||||||
import { verifyCommand } from '../../commands/link';
|
import { verifyCommand } from "../../commands/link";
|
||||||
import { ownedTanksCommand } from '../../commands/ownedTanks';
|
import { ownedTanksCommand } from "../../commands/ownedTanks";
|
||||||
import { permissionsCommand } from '../../commands/permissions';
|
import { permissionsCommand } from "../../commands/permissions";
|
||||||
import { pingCommand } from '../../commands/ping';
|
import { pingCommand } from "../../commands/ping";
|
||||||
import { playerInfoCommand } from '../../commands/playerInfo';
|
import { playerInfoCommand } from "../../commands/playerInfo";
|
||||||
import { ratingCommand } from '../../commands/rating';
|
import { ratingCommand } from "../../commands/rating";
|
||||||
import { ratingLeaderboardCommand } from '../../commands/ratingLeaderboard';
|
import { ratingLeaderboardCommand } from "../../commands/ratingLeaderboard";
|
||||||
import { replayCommand } from '../../commands/replay';
|
import { replayCommand } from "../../commands/replay";
|
||||||
import { researchCommand } from '../../commands/research';
|
import { researchCommand } from "../../commands/research";
|
||||||
import { searchClansCommand } from '../../commands/searchClans';
|
import { searchClansCommand } from "../../commands/searchClans";
|
||||||
import { searchPlayersCommand } from '../../commands/searchPlayers';
|
import { searchPlayersCommand } from "../../commands/searchPlayers";
|
||||||
import { statsCommand } from '../../commands/stats';
|
import { statsCommand } from "../../commands/stats";
|
||||||
import { todayCommand } from '../../commands/today';
|
import { todayCommand } from "../../commands/today";
|
||||||
import { RenderConfiguration } from '../../core/blitzkit/renderConfiguration';
|
import { RenderConfiguration } from "../../core/blitzkit/renderConfiguration";
|
||||||
import { handleAutocomplete } from './handlers/autocomplete';
|
import { handleAutocomplete } from "./handlers/autocomplete";
|
||||||
import { handleButton } from './handlers/button';
|
import { handleButton } from "./handlers/button";
|
||||||
import { handleChatInputCommand } from './handlers/chatInputCommand';
|
import { handleChatInputCommand } from "./handlers/chatInputCommand";
|
||||||
|
|
||||||
export type InteractionRawReturnable =
|
export type InteractionRawReturnable =
|
||||||
| string
|
| string
|
||||||
@@ -44,7 +44,7 @@ export type InteractionRawReturnable =
|
|||||||
| ButtonBuilder
|
| ButtonBuilder
|
||||||
| AttachmentBuilder
|
| AttachmentBuilder
|
||||||
| RenderConfiguration
|
| RenderConfiguration
|
||||||
| JSX.Element
|
| React.JSX.Element
|
||||||
| null;
|
| null;
|
||||||
export type InteractionIterableReturnable =
|
export type InteractionIterableReturnable =
|
||||||
| InteractionRawReturnable
|
| InteractionRawReturnable
|
||||||
@@ -58,7 +58,7 @@ interface CommandRegistryBase {
|
|||||||
| SlashCommandBuilder
|
| SlashCommandBuilder
|
||||||
| SlashCommandSubcommandsOnlyBuilder
|
| SlashCommandSubcommandsOnlyBuilder
|
||||||
| SlashCommandOptionsOnlyBuilder
|
| SlashCommandOptionsOnlyBuilder
|
||||||
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
| Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
|
||||||
|
|
||||||
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
||||||
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
||||||
@@ -72,7 +72,7 @@ interface CommandRegistryImplicit extends CommandRegistryBase {
|
|||||||
interface CommandRegistryExplicit extends CommandRegistryBase {
|
interface CommandRegistryExplicit extends CommandRegistryBase {
|
||||||
handlesInteraction?: false;
|
handlesInteraction?: false;
|
||||||
handler: (
|
handler: (
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>
|
||||||
) => InteractionReturnable;
|
) => InteractionReturnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,16 +104,16 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
|
|||||||
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
|
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
|
||||||
return rawCommands.reduce<Record<string, CommandRegistry>>(
|
return rawCommands.reduce<Record<string, CommandRegistry>>(
|
||||||
(commands, registry, index) => {
|
(commands, registry, index) => {
|
||||||
if (registry.status === 'rejected') {
|
if (registry.status === "rejected") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Command ${index} failed to load; skipping...`,
|
`Command ${index} failed to load; skipping...`,
|
||||||
registry.reason,
|
registry.reason
|
||||||
);
|
);
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
return { ...commands, [registry.value.command.name]: registry.value };
|
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) => {
|
commands.then((awaitedCommands) => {
|
||||||
const body = Object.values(awaitedCommands).map((registry) =>
|
const body = Object.values(awaitedCommands).map((registry) =>
|
||||||
registry.command.toJSON(),
|
registry.command.toJSON()
|
||||||
);
|
);
|
||||||
|
|
||||||
rest.put(
|
rest.put(
|
||||||
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
|
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
|
||||||
{ body },
|
{ body }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,20 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/plugin-throttling": "^8.2.0",
|
"@octokit/plugin-throttling": "^11.0.1",
|
||||||
"@octokit/rest": "^20.1.1",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.16.2",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"crc": "^4.3.2",
|
"crc": "^4.3.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^17.2.2",
|
||||||
"lz4js": "^0.2.0",
|
"lz4js": "^0.2.0",
|
||||||
"pickleparser": "^0.2.1",
|
"pickleparser": "^0.2.1",
|
||||||
"protobufjs": "^7.4.0",
|
"protobufjs": "^7.5.4",
|
||||||
"ts-proto": "^2.2.2",
|
"ts-proto": "^2.7.7",
|
||||||
"yaml": "^2.5.0"
|
"yaml": "^2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/lz4js": "^0.2.1"
|
"@types/lz4js": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { deburr } from 'lodash-es';
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
import { SUPPORTED_LOCALES } from '../../../i18n/src/strings';
|
import { deburr } from "lodash-es";
|
||||||
import { I18nString } from '../protos';
|
import { I18nString } from "../protos";
|
||||||
import { fetchCamouflageDefinitions } from './camouflageDefinitions';
|
import { fetchCamouflageDefinitions } from "./camouflageDefinitions";
|
||||||
import { fetchTankDefinitions } from './tankDefinitions';
|
import { fetchTankDefinitions } from "./tankDefinitions";
|
||||||
|
|
||||||
export async function fetchTankNames() {
|
export async function fetchTankNames() {
|
||||||
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
|
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
|
||||||
@@ -26,25 +26,27 @@ export async function fetchTankNames() {
|
|||||||
searchableNameDeburr,
|
searchableNameDeburr,
|
||||||
camouflages: tank.camouflages
|
camouflages: tank.camouflages
|
||||||
?.map((id) =>
|
?.map((id) =>
|
||||||
SUPPORTED_LOCALES.map(
|
locales.supported.map(
|
||||||
(locale) =>
|
({ locale }) =>
|
||||||
camouflageDefinitions.camouflages[id]?.name.locales[locale],
|
camouflageDefinitions.camouflages[id]?.name.locales[locale]
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.flat()
|
.flat()
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(deburr)
|
.map(deburr)
|
||||||
.join(' '),
|
.join(" "),
|
||||||
treeType: tank.type,
|
treeType: tank.type,
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SEARCH_KEYS = [
|
export const SEARCH_KEYS = [
|
||||||
...SUPPORTED_LOCALES.map((locale) => [
|
...locales.supported
|
||||||
`searchableName.locales.${locale}`,
|
.map(({ locale }) => [
|
||||||
`searchableNameDeburr.locales.${locale}`,
|
`searchableName.locales.${locale}`,
|
||||||
]).flat(),
|
`searchableNameDeburr.locales.${locale}`,
|
||||||
'camouflages',
|
])
|
||||||
|
.flat(),
|
||||||
|
"camouflages",
|
||||||
];
|
];
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ts-deepmerge": "^7.0.2"
|
"ts-deepmerge": "^7.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './literals';
|
export * from "./literals";
|
||||||
export * from './strings';
|
export * from "./strings";
|
||||||
export * from './unwrapper';
|
export * from "./unwrapper";
|
||||||
|
export * from "./wrapper";
|
||||||
@@ -1,98 +1,66 @@
|
|||||||
import { merge } from 'ts-deepmerge';
|
import { merge } from "ts-deepmerge";
|
||||||
import en from '../strings/en.json';
|
import locales from "../locales.json";
|
||||||
import es from '../strings/es.json';
|
import en from "../strings/en.json";
|
||||||
import fr from '../strings/fr.json';
|
import es from "../strings/es.json";
|
||||||
import ja from '../strings/ja.json';
|
import fr from "../strings/fr.json";
|
||||||
import pl from '../strings/pl.json';
|
import ja from "../strings/ja.json";
|
||||||
import pt from '../strings/pt.json';
|
import pl from "../strings/pl.json";
|
||||||
import ru from '../strings/ru.json';
|
import pt from "../strings/pt.json";
|
||||||
import uk from '../strings/uk.json';
|
import ru from "../strings/ru.json";
|
||||||
import zh from '../strings/zh.json';
|
import uk from "../strings/uk.json";
|
||||||
|
import zh from "../strings/zh.json";
|
||||||
|
|
||||||
export type BlitzKitStrings = typeof en;
|
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 STRINGS: Record<string, BlitzKitStrings> = {};
|
||||||
|
|
||||||
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>> = {};
|
|
||||||
|
|
||||||
for (const locale in stringsRaw) {
|
for (const locale in stringsRaw) {
|
||||||
if (locale === DEFAULT_LOCALE) {
|
if (locale === locales.default) {
|
||||||
stringsPartial[DEFAULT_LOCALE] = stringsRaw[DEFAULT_LOCALE];
|
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeStrings = stringsRaw[locale as SupportedLocale];
|
const localeStrings = stringsRaw[locale];
|
||||||
stringsPartial[locale as SupportedLocale] = merge(
|
STRINGS[locale] = merge(
|
||||||
stringsRaw[DEFAULT_LOCALE],
|
stringsRaw[locales.default],
|
||||||
localeStrings,
|
localeStrings
|
||||||
);
|
) as BlitzKitStrings;
|
||||||
|
|
||||||
if (import.meta.env.PUBLIC_DEBUG_MISSING_I18N === 'true') {
|
|
||||||
nuke(stringsPartial[locale as SupportedLocale]!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STRINGS = stringsPartial as Record<
|
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
|
||||||
SupportedLocale,
|
en: "en",
|
||||||
BlitzKitStrings
|
es: "es",
|
||||||
>;
|
fr: "fr",
|
||||||
|
ja: "ja",
|
||||||
export type SupportedLocale = keyof typeof stringsRaw;
|
pt: "pt",
|
||||||
|
ru: "ru",
|
||||||
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<SupportedLocale, string> = {
|
uk: "uk",
|
||||||
en: 'en',
|
zh: "zh-Hans",
|
||||||
es: 'es',
|
pl: "pl",
|
||||||
fr: 'fr',
|
|
||||||
ja: 'ja',
|
|
||||||
pt: 'pt',
|
|
||||||
ru: 'ru',
|
|
||||||
uk: 'uk',
|
|
||||||
zh: 'zh-Hans',
|
|
||||||
pl: 'pl',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SUPPORTED_LOCALE_FLAGS: Record<SupportedLocale, string> = {
|
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
|
||||||
en: '🇺🇸',
|
en: "🇺🇸",
|
||||||
es: '🇪🇸',
|
es: "🇪🇸",
|
||||||
fr: '🇫🇷',
|
fr: "🇫🇷",
|
||||||
ja: '🇯🇵',
|
ja: "🇯🇵",
|
||||||
pt: '🇵🇹',
|
pt: "🇵🇹",
|
||||||
ru: '🇷🇺',
|
ru: "🇷🇺",
|
||||||
uk: '🇺🇦',
|
uk: "🇺🇦",
|
||||||
zh: '🇨🇳',
|
zh: "🇨🇳",
|
||||||
pl: '🇵🇱',
|
pl: "🇵🇱",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TranslationTree = { [key: string]: TranslationNode };
|
export type TranslationTree = { [key: string]: TranslationNode };
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { I18nString } from '@blitzkit/core';
|
import { I18nString } from "@blitzkit/core";
|
||||||
import { DEFAULT_LOCALE } from './strings';
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
|
|
||||||
export function unwrapper(locale: string) {
|
export function unwrapper(locale = locales.default) {
|
||||||
return function (i18nString: I18nString) {
|
return function (i18nString: I18nString) {
|
||||||
if (locale in i18nString.locales) {
|
if (locale in i18nString.locales) {
|
||||||
return i18nString.locales[locale];
|
return i18nString.locales[locale];
|
||||||
}
|
}
|
||||||
|
|
||||||
return i18nString.locales[DEFAULT_LOCALE];
|
return i18nString.locales[locales.default];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
|
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1m",
|
"m": "%s1m",
|
||||||
"mm": "%s1mm",
|
"mm": "%s1mm",
|
||||||
@@ -763,6 +765,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Search tanks...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Recently viewed",
|
||||||
@@ -833,6 +837,15 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
|
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Join cool people!",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Join super duper cool people!",
|
||||||
@@ -861,6 +874,15 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
|
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"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.",
|
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
||||||
|
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
|
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -935,9 +961,7 @@
|
|||||||
"green_penetration": "Green penetration",
|
"green_penetration": "Green penetration",
|
||||||
"hide_model_under_armor": "Hide model under armor",
|
"hide_model_under_armor": "Hide model under armor",
|
||||||
"opaque": "Opaque",
|
"opaque": "Opaque",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Environment",
|
|
||||||
"show_grid": "Show grid"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
@@ -1437,17 +1461,11 @@
|
|||||||
"description": "Get started with BlitzKit",
|
"description": "Get started with BlitzKit",
|
||||||
"seo_description": "Get started with BlitzKit"
|
"seo_description": "Get started with BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "BlitzAnalysis[]",
|
"name": "API",
|
||||||
"button": "Analyze now",
|
"button": "To the docs",
|
||||||
"description": "Rich historic average stats",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Rich historic average stats in World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for 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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"fox": "El rápido zorro marrón salta sobre el perro perezoso."
|
"fox": "El rápido zorro marrón salta sobre el perro perezoso."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1m",
|
"m": "%s1m",
|
||||||
"mm": "%s1mm",
|
"mm": "%s1mm",
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"copied": "Copiado!"
|
"copied": "Copiado!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Buscar tanques...",
|
"search_bar_hint": "Buscar tanques...",
|
||||||
"recent": "Visto recientemente",
|
"recent": "Visto recientemente",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Join cool people!",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Join super duper cool people!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"button": "Guess away",
|
||||||
@@ -787,6 +806,9 @@
|
|||||||
"tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 blindaje, estadisticas, equipamiento, proviciones y mas.\n%s2 DPM, %3hp alfa, %s4mm penetracion, %s5kph velocidad maxima, %s6 hp salud",
|
"tank_seo_description": "%s1 blindaje, estadisticas, equipamiento, proviciones y mas.\n%s2 DPM, %3hp alfa, %s4mm penetracion, %s5kph velocidad maxima, %s6 hp salud",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "Penetración verde",
|
"green_penetration": "Penetración verde",
|
||||||
"hide_model_under_armor": "Ocultar modelo bajo la armadura",
|
"hide_model_under_armor": "Ocultar modelo bajo la armadura",
|
||||||
"opaque": "Opaco",
|
"opaque": "Opaco",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Entorno",
|
|
||||||
"show_grid": "Mostrar cuadrícula"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "Get started with BlitzKit",
|
"description": "Get started with BlitzKit",
|
||||||
"seo_description": "Get started with BlitzKit"
|
"seo_description": "Get started with BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "BlitzAnalysis[]",
|
"name": "API",
|
||||||
"button": "Analizar ahora",
|
"button": "To the docs",
|
||||||
"description": "Rich historic average stats",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Rich historic average stats in World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for 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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"fox": "Le renard brun et rapide saute par-dessus le chien paresseux."
|
"fox": "Le renard brun et rapide saute par-dessus le chien paresseux."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1m",
|
"m": "%s1m",
|
||||||
"mm": "%s1mm",
|
"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.",
|
"non_tech_tree": "# %s1 n'est pas dans l'arbre technologique\n\nVous ne pouvez pas rechercher un char prenium/de collection.",
|
||||||
"tier_1": "# %s1 est un char de tier I\n\nLe char de tier 1 sont déverrouillé gratuitement pour tous les joueurs.",
|
"tier_1": "# %s1 est un char de tier I\n\nLe char de tier 1 sont déverrouillé gratuitement pour tous les joueurs.",
|
||||||
"already_researched": "# %s1 est déjà recherché\n\nEssayez de fournir un `char de départ` en utilisant la commande pour voir la progression d'une lignée.",
|
"already_researched": "# %s1 est déjà recherché\n\nEssayez de fournir un `char de départ` en utilisant la commande pour voir la progression d'une lignée.",
|
||||||
"start_end_equal": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
|
"start_end_equal": "# %s1 a été fourni deux fois\n\nAssurez-vous que `starting-tank` et `target-tank` sont différents.",
|
||||||
"tanks_not_on_line": "# %s1 and %s2 are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
|
"tanks_not_on_line": "%s1 et %s2 ne sont pas sur la même ligne \n\nAssurez-vous que « starting-tank » et « target-tank » sont sur la même ligne d'arbre technologique.",
|
||||||
"unordered": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank."
|
"unordered": "# %s1 niveau supérieur à %s2\n\nAssurez-vous de fournir %s3 comme le `starting-tank` à la place car c'est le réservoir de niveau inférieur."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
@@ -380,7 +381,7 @@
|
|||||||
},
|
},
|
||||||
"embeds": {
|
"embeds": {
|
||||||
"name": "intègre",
|
"name": "intègre",
|
||||||
"description": "How embeds work"
|
"description": "Comment fonctionnent les intégrations"
|
||||||
},
|
},
|
||||||
"introduction": {
|
"introduction": {
|
||||||
"name": "introduction",
|
"name": "introduction",
|
||||||
@@ -402,7 +403,7 @@
|
|||||||
},
|
},
|
||||||
"breakdown": {
|
"breakdown": {
|
||||||
"name": "décomposition",
|
"name": "décomposition",
|
||||||
"description": "A period's breakdown by tanks played",
|
"description": "Répartition d'une période par chars joués",
|
||||||
"body": {
|
"body": {
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"battles": "Batailles",
|
"battles": "Batailles",
|
||||||
@@ -471,7 +472,7 @@
|
|||||||
"accuracy": "Précision"
|
"accuracy": "Précision"
|
||||||
},
|
},
|
||||||
"lethality": {
|
"lethality": {
|
||||||
"title": "Lethality",
|
"title": "Létalité",
|
||||||
"average_damage": "Dégâts moyens",
|
"average_damage": "Dégâts moyens",
|
||||||
"damage_per_tier": "Dégâts par tier",
|
"damage_per_tier": "Dégâts par tier",
|
||||||
"damage_ratio": "Rapport de dégâts",
|
"damage_ratio": "Rapport de dégâts",
|
||||||
@@ -499,20 +500,20 @@
|
|||||||
},
|
},
|
||||||
"embed": {
|
"embed": {
|
||||||
"title": "Compte invalide ?",
|
"title": "Compte invalide ?",
|
||||||
"description": "Use the command again and select a username from the search results."
|
"description": "Utilisez à nouveau la commande et sélectionnez un nom d'utilisateur à partir des résultats de recherche."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_clans": {
|
"search_clans": {
|
||||||
"name": "search-clans",
|
"name": "clans de recherche",
|
||||||
"description": "Search clans",
|
"description": "Rechercher des clans",
|
||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "limite",
|
"name": "limite",
|
||||||
"description": "The max number of clan results (default: %s1)"
|
"description": "Nombre maximum de résultats du clan (par défaut: %s1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Clan search for %s1 in region %s2",
|
"title": "Recherche de clan %s1 dans la région %s2",
|
||||||
"no_results": "Pas de clan trouvé"
|
"no_results": "Pas de clan trouvé"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -535,69 +536,69 @@
|
|||||||
"description": "Statistiques des batailles de cotés",
|
"description": "Statistiques des batailles de cotés",
|
||||||
"subcommands": {
|
"subcommands": {
|
||||||
"neighbors": {
|
"neighbors": {
|
||||||
"name": "neighbors",
|
"name": "voisins",
|
||||||
"description": "Neighboring players on the leaderboard"
|
"description": "Joueurs voisins dans le classement"
|
||||||
},
|
},
|
||||||
"league": {
|
"league": {
|
||||||
"name": "ligue",
|
"name": "ligue",
|
||||||
"description": "Top players of a league"
|
"description": "Meilleurs joueurs d'une ligue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "limite",
|
"name": "limite",
|
||||||
"description": "How many neighbors to display (default: %s1)"
|
"description": "Combien de voisins afficher (par défaut : %s1)"
|
||||||
},
|
},
|
||||||
"season": {
|
"season": {
|
||||||
"name": "season",
|
"name": "saison",
|
||||||
"description": "Season number (default: current)",
|
"description": "Numéro de saison (défaut : courant)",
|
||||||
"choices": {
|
"choices": {
|
||||||
"current": "%s1 (current)"
|
"current": "%s1 (actuel)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"league": {
|
"league": {
|
||||||
"name": "ligue",
|
"name": "ligue",
|
||||||
"description": "The league to display"
|
"description": "La ligue à afficher"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"top_players": "Top %s1 players",
|
"top_players": "Top %s1 joueurs",
|
||||||
"no_participation": "# %s1 didn't play rating in season %s2\n\nThis player did not participate in this season or did not get past calibration.",
|
"no_participation": "%s1 n'a pas joué au classement dans la saison %s2",
|
||||||
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
|
"no_ongoing_season": "Pas de saison en cours\n\nIl n'y a pas de saison de notation en cours actuellement.",
|
||||||
"deleted_player": "Deleted %s1",
|
"deleted_player": "%s1 supprimé",
|
||||||
"neighbors": "Rating neighbors",
|
"neighbors": "évaluer les voisins",
|
||||||
"more": "+ %s1 more"
|
"more": "+ %s1 de plus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"today": {
|
"today": {
|
||||||
"name": "aujourd'hui",
|
"name": "aujourd'hui",
|
||||||
"description": "Today's played tanks and statistics"
|
"description": "Les chars et les statistiques joués aujourd'hui"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"name": "statistiques",
|
"name": "statistiques",
|
||||||
"description": "Quick statistics for a period"
|
"description": "Statistiques rapides pour une période"
|
||||||
},
|
},
|
||||||
"owned_tanks": {
|
"owned_tanks": {
|
||||||
"name": "owned-tanks",
|
"name": "chars possédés",
|
||||||
"description": "Shows all own tanks in a tier",
|
"description": "Affiche tous les chars d'un niveau",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# %s1's owned tier %s2 tanks\n\n"
|
"title": "# %s1 possédait des chars de rang %s2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"name": "permissions",
|
"name": "permissions",
|
||||||
"description": "Checks wether the bot has the required permissions to function",
|
"description": "Vérifie si le bot a les permissions nécessaires pour fonctionner",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# Permissions\n\n%s1",
|
"title": "# Permissions\n\n%s1",
|
||||||
"view_channels": "View channels: needed to find the message the refresh button was used on",
|
"view_channels": "Afficher les canaux : nécessaire pour trouver le message sur lequel le bouton d'actualisation a été utilisé",
|
||||||
"read_message_history": "Read message history: needed to find the message to refresh",
|
"read_message_history": "Lire l'historique des messages : nécessaire pour trouver le message à rafraîchir",
|
||||||
"attach_files": "Attach files: needed to add images to image-based commands",
|
"attach_files": "Joindre des fichiers : nécessaire pour ajouter des images à des commandes basées sur l'image",
|
||||||
"external_emojis": "External emojis: needed to add national flags and tank class emojis"
|
"external_emojis": "Émoticônes externes: nécessaire pour ajouter des drapeaux nationaux et des émojis de classe de char"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"name": "ping",
|
"name": "ping",
|
||||||
"description": "Check wether the bot is alive.",
|
"description": "Vérifiez si le bot est en vie.",
|
||||||
"body": {
|
"body": {
|
||||||
"pong": "Pong 🏓",
|
"pong": "Pong 🏓",
|
||||||
"pong_with_time": "Pong 🏓 - %s1ms"
|
"pong_with_time": "Pong 🏓 - %s1ms"
|
||||||
@@ -605,7 +606,7 @@
|
|||||||
"subcommands": {
|
"subcommands": {
|
||||||
"blitzkit": {
|
"blitzkit": {
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"description": "Ping BlitzKit"
|
"description": "Ping blitzkit"
|
||||||
},
|
},
|
||||||
"wotb": {
|
"wotb": {
|
||||||
"name": "wotb",
|
"name": "wotb",
|
||||||
@@ -614,10 +615,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player_info": {
|
"player_info": {
|
||||||
"name": "player-info",
|
"name": "info-joueur",
|
||||||
"description": "Basic information about a player",
|
"description": "Informations de base sur un joueur",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "%s1's information",
|
"title": "Informations de %s1",
|
||||||
"nickname": "Nickname",
|
"nickname": "Nickname",
|
||||||
"battles": "Batailles",
|
"battles": "Batailles",
|
||||||
"winrate": "Taux de victoires",
|
"winrate": "Taux de victoires",
|
||||||
@@ -635,7 +636,7 @@
|
|||||||
"description": "Today's rating statistics"
|
"description": "Today's rating statistics"
|
||||||
},
|
},
|
||||||
"season": {
|
"season": {
|
||||||
"name": "season",
|
"name": "saison",
|
||||||
"description": "This season's rating statistics"
|
"description": "This season's rating statistics"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"copied": "Copied!"
|
"copied": "Copied!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Search tanks...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Recently viewed",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Join cool people!",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Join super duper cool people!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"button": "Guess away",
|
||||||
@@ -787,6 +806,9 @@
|
|||||||
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "Green penetration",
|
"green_penetration": "Green penetration",
|
||||||
"hide_model_under_armor": "Hide model under armor",
|
"hide_model_under_armor": "Hide model under armor",
|
||||||
"opaque": "Opaque",
|
"opaque": "Opaque",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Environment",
|
|
||||||
"show_grid": "Show grid"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "Get started with BlitzKit",
|
"description": "Get started with BlitzKit",
|
||||||
"seo_description": "Get started with BlitzKit"
|
"seo_description": "Get started with BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "BlitzAnalysis[]",
|
"name": "API",
|
||||||
"button": "Analyze now",
|
"button": "To the docs",
|
||||||
"description": "Rich historic average stats",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Rich historic average stats in World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for 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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。"
|
"fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1m",
|
"m": "%s1m",
|
||||||
"mm": "%s1mm",
|
"mm": "%s1mm",
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"copied": "コピーしました!"
|
"copied": "コピーしました!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "車輌名を入力…",
|
"search_bar_hint": "車輌名を入力…",
|
||||||
"recent": "最近の閲覧",
|
"recent": "最近の閲覧",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Join cool people!",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Join super duper cool people!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"button": "Guess away",
|
||||||
@@ -787,6 +806,9 @@
|
|||||||
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "貫通可能域を緑色表示",
|
"green_penetration": "貫通可能域を緑色表示",
|
||||||
"hide_model_under_armor": "車輌モデルを非表示",
|
"hide_model_under_armor": "車輌モデルを非表示",
|
||||||
"opaque": "貫通アシストを不透明にする",
|
"opaque": "貫通アシストを不透明にする",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "環境",
|
|
||||||
"show_grid": "グリッドを表示"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "保存する",
|
"download": "保存する",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "さあ、BlitzKit を始めよう!",
|
"description": "さあ、BlitzKit を始めよう!",
|
||||||
"seo_description": "さあ、BlitzKit を始めよう!"
|
"seo_description": "さあ、BlitzKit を始めよう!"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "BlitzAnalysis[]",
|
"name": "API",
|
||||||
"button": "Analyze now",
|
"button": "To the docs",
|
||||||
"description": "Rich historic average stats",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Rich historic average stats in World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
|
||||||
},
|
|
||||||
"blitz_insider": {
|
|
||||||
"name": "Blitz Insider",
|
|
||||||
"button": "Discover today",
|
|
||||||
"description": "ゲーム内のセッション統計",
|
|
||||||
"seo_description": "In-game session stats in World of Tanks Blitz"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
|
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1m",
|
"m": "%s1m",
|
||||||
"mm": "%s1mm",
|
"mm": "%s1mm",
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"copied": "Copied!"
|
"copied": "Copied!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Search tanks...",
|
"search_bar_hint": "Search tanks...",
|
||||||
"recent": "Recently viewed",
|
"recent": "Recently viewed",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Join cool people!",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Join super duper cool people!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"button": "Guess away",
|
||||||
@@ -787,6 +806,9 @@
|
|||||||
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "Green penetration",
|
"green_penetration": "Green penetration",
|
||||||
"hide_model_under_armor": "Hide model under armor",
|
"hide_model_under_armor": "Hide model under armor",
|
||||||
"opaque": "Opaque",
|
"opaque": "Opaque",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Environment",
|
|
||||||
"show_grid": "Show grid"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "Get started with BlitzKit",
|
"description": "Get started with BlitzKit",
|
||||||
"seo_description": "Get started with BlitzKit"
|
"seo_description": "Get started with BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "BlitzAnalysis[]",
|
"name": "API",
|
||||||
"button": "Analyze now",
|
"button": "To the docs",
|
||||||
"description": "Rich historic average stats",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Rich historic average stats in World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for 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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"fox": "A ágil raposa castanha salta por cima do cão preguiçoso."
|
"fox": "A ágil raposa castanha salta por cima do cão preguiçoso."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1m",
|
"m": "%s1m",
|
||||||
"mm": "%s1mm",
|
"mm": "%s1mm",
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"copied": "Copiado!"
|
"copied": "Copiado!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Procurar tanques...",
|
"search_bar_hint": "Procurar tanques...",
|
||||||
"recent": "Visto recentemente",
|
"recent": "Visto recentemente",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Join cool people!",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Join super duper cool people!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"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_title": "%s1 - Nível %s2 %s3 %s4 | %s5 Tanquepedia | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 blindagem, estatísticas, equipamentos, provisões e mais. %s2 DPM, %s3hp alfa, %s4mm de penetração, %s5km/h velocidade máxima, %s6hp vida.",
|
"tank_seo_description": "%s1 blindagem, estatísticas, equipamentos, provisões e mais. %s2 DPM, %s3hp alfa, %s4mm de penetração, %s5km/h velocidade máxima, %s6hp vida.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "Penetração verde",
|
"green_penetration": "Penetração verde",
|
||||||
"hide_model_under_armor": "Ocultar modelo sob blindagem",
|
"hide_model_under_armor": "Ocultar modelo sob blindagem",
|
||||||
"opaque": "Opaco",
|
"opaque": "Opaco",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Ambiente",
|
|
||||||
"show_grid": "Exibir grade"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "Comece com BlitzKit",
|
"description": "Comece com BlitzKit",
|
||||||
"seo_description": "Comece com BlitzKit"
|
"seo_description": "Comece com BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "AnaliseBlitz[]",
|
"name": "API",
|
||||||
"button": "Analise agora",
|
"button": "To the docs",
|
||||||
"description": "Estatísticas médias históricas ricas",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Estatísticas médias históricas ricas no World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for 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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"test": {
|
"test": {
|
||||||
"default": "тест",
|
"default": "тест",
|
||||||
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.",
|
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — %s1, потому что это цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.",
|
||||||
"fox": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3."
|
"fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1м",
|
"m": "%s1м",
|
||||||
"mm": "%s1мм",
|
"mm": "%s1мм",
|
||||||
@@ -283,15 +284,15 @@
|
|||||||
"name": "уровень",
|
"name": "уровень",
|
||||||
"description": "Уровень",
|
"description": "Уровень",
|
||||||
"choices": {
|
"choices": {
|
||||||
"tier": "Уровень %s - %s"
|
"tier": "Уровень %s1 - %s2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"periods": {
|
"periods": {
|
||||||
"career": "Карьера",
|
"career": "Карьера",
|
||||||
"today": "Cегодня",
|
"today": "Cегодня",
|
||||||
"days": "%s дней",
|
"days": "%s1 дней",
|
||||||
"custom": "%s - %s дней"
|
"custom": "%s1 - %s2 дней"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"none": "Без фильтров",
|
"none": "Без фильтров",
|
||||||
@@ -323,9 +324,9 @@
|
|||||||
"description": "Танки доступные на аукционе сейчас",
|
"description": "Танки доступные на аукционе сейчас",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Blitz аукцион",
|
"title": "Blitz аукцион",
|
||||||
"subtitle": "%s доступные • следующие цены %s\nПокупка сейчас: %s",
|
"subtitle": "%s1 доступные • следующие цены %s2\nПокупка сейчас: %s3",
|
||||||
"next": "Следующая цена: %s",
|
"next": "Следующая цена: %s1",
|
||||||
"available": "Доступно: %s / %s"
|
"available": "Доступно: %s1 / %s2"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"no_auction": "# Аукцион сейчас не проходит\n\nНа данный момент нет танков для покупки на аукционе. Если считаете, что это ошибка, пожалуйста о несоответствии на [официальном сервере Discord](https://discord.gg/nDt7AjGJQH)."
|
"no_auction": "# Аукцион сейчас не проходит\n\nНа данный момент нет танков для покупки на аукционе. Если считаете, что это ошибка, пожалуйста о несоответствии на [официальном сервере Discord](https://discord.gg/nDt7AjGJQH)."
|
||||||
@@ -354,19 +355,19 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"subtitle": "Исследование",
|
"subtitle": "Исследование",
|
||||||
"total": "Всего",
|
"total": "Всего",
|
||||||
"research": "Исследование: %s",
|
"research": "Исследование: %s1",
|
||||||
"upgrades": "Улучшения: %s",
|
"upgrades": "Улучшения: %s1",
|
||||||
"purchase": "Покупка: %s",
|
"purchase": "Покупка: %s1",
|
||||||
"equipment": "Снаряжение: %s",
|
"equipment": "Снаряжение: %s1",
|
||||||
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
|
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"non_tech_tree": "# %s отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.",
|
"non_tech_tree": "# %s1 отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.",
|
||||||
"tier_1": "# %s - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.",
|
"tier_1": "# %s1 - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.",
|
||||||
"already_researched": "# %s уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
|
"already_researched": "# %s1 уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
|
||||||
"start_end_equal": "# %s был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
|
"start_end_equal": "# %s1 был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
|
||||||
"tanks_not_on_line": "# %s и %s находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
|
"tanks_not_on_line": "# %s1 и %s2 находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
|
||||||
"unordered": "# У %s более высокий уровень чем у %s\n\nОбязательно укажите %s в качестве «starting-tank», так как это танк более низкого уровня."
|
"unordered": "# У %s1 более высокий уровень чем у %s2\n\nОбязательно укажите %s3 в качестве «starting-tank», так как это танк более низкого уровня."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
@@ -409,7 +410,7 @@
|
|||||||
"wn8": "WN8",
|
"wn8": "WN8",
|
||||||
"winrate": "Коэффициент побед",
|
"winrate": "Коэффициент побед",
|
||||||
"damage": "Урон",
|
"damage": "Урон",
|
||||||
"unknown_tank": "Неизвестный танк %s"
|
"unknown_tank": "Неизвестный танк %s1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
@@ -445,12 +446,12 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"threshold": {
|
"threshold": {
|
||||||
"name": "порог",
|
"name": "порог",
|
||||||
"description": "Количество дней не активности (по умолчанию: %s дней)"
|
"description": "Количество дней не активности (по умолчанию: %s1 дней)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"subtitle": "Неактивен %s+ дней",
|
"subtitle": "Неактивен %s1+ дней",
|
||||||
"listing": "%s дней"
|
"listing": "%s1 дней"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"full_stats": {
|
"full_stats": {
|
||||||
@@ -508,11 +509,11 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "лимит",
|
"name": "лимит",
|
||||||
"description": "Максимальное количество результатов клана (по умолчанию: %s)"
|
"description": "Максимальное количество результатов клана (по умолчанию: %s1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Поиск клана %s в регионе %s",
|
"title": "Поиск клана %s1 в регионе %s2",
|
||||||
"no_results": "Кланов не найдено"
|
"no_results": "Кланов не найдено"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -522,11 +523,11 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "лимит",
|
"name": "лимит",
|
||||||
"description": "Максимальное число результатов игрока (по умолчанию: %s)"
|
"description": "Максимальное число результатов игрока (по умолчанию: %s1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Поиск игрока %s в регионе %s",
|
"title": "Поиск игрока %s1 в регионе %s2",
|
||||||
"no_results": "Игроков не найдено"
|
"no_results": "Игроков не найдено"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -546,13 +547,13 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "лимит",
|
"name": "лимит",
|
||||||
"description": "Количество отображаемых ближайших игроков (по умолчанию: %s)"
|
"description": "Количество отображаемых ближайших игроков (по умолчанию: %s1)"
|
||||||
},
|
},
|
||||||
"season": {
|
"season": {
|
||||||
"name": "сезон",
|
"name": "сезон",
|
||||||
"description": "Номер сезона (по умолчанию: текущий)",
|
"description": "Номер сезона (по умолчанию: текущий)",
|
||||||
"choices": {
|
"choices": {
|
||||||
"current": "%s (текущая)"
|
"current": "%s1 (текущая)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"league": {
|
"league": {
|
||||||
@@ -561,12 +562,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"top_players": "Топ %s игроков",
|
"top_players": "Топ %s1 игроков",
|
||||||
"no_participation": "# %s не играл в рейтинг в сезоне %s\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
|
"no_participation": "# %s1 не играл в рейтинг в сезоне %s2\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
|
||||||
"no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.",
|
"no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.",
|
||||||
"deleted_player": "Удалено %s",
|
"deleted_player": "Удалено %s1",
|
||||||
"neighbors": "Рейтинг ближайших игроков",
|
"neighbors": "Рейтинг ближайших игроков",
|
||||||
"more": "+ ещё %s"
|
"more": "+ ещё %s1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"today": {
|
"today": {
|
||||||
@@ -581,14 +582,14 @@
|
|||||||
"name": "cобственные-танки",
|
"name": "cобственные-танки",
|
||||||
"description": "Показывает все принадлежащие танки на уровне",
|
"description": "Показывает все принадлежащие танки на уровне",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# %s принадлежал уровень %s танков\n\n"
|
"title": "# %s1 принадлежал уровень %s2 танков\n\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"name": "разрешения",
|
"name": "разрешения",
|
||||||
"description": "Проверяет, есть ли у бота необходимые разрешения для работы",
|
"description": "Проверяет, есть ли у бота необходимые разрешения для работы",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "# Разрешения\n\n%s",
|
"title": "# Разрешения\n\n%s1",
|
||||||
"view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления",
|
"view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления",
|
||||||
"read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления",
|
"read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления",
|
||||||
"attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения",
|
"attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения",
|
||||||
@@ -600,7 +601,7 @@
|
|||||||
"description": "Проверьте, что бот жив.",
|
"description": "Проверьте, что бот жив.",
|
||||||
"body": {
|
"body": {
|
||||||
"pong": "Понг 🏓",
|
"pong": "Понг 🏓",
|
||||||
"pong_with_time": "Понг 🏓 - %sмс"
|
"pong_with_time": "Понг 🏓 - %s1мс"
|
||||||
},
|
},
|
||||||
"subcommands": {
|
"subcommands": {
|
||||||
"blitzkit": {
|
"blitzkit": {
|
||||||
@@ -617,7 +618,7 @@
|
|||||||
"name": "информация-об-игроке",
|
"name": "информация-об-игроке",
|
||||||
"description": "Основная информация о игроке",
|
"description": "Основная информация о игроке",
|
||||||
"body": {
|
"body": {
|
||||||
"title": "Информация о %s",
|
"title": "Информация о %s1",
|
||||||
"nickname": "Никнейм",
|
"nickname": "Никнейм",
|
||||||
"battles": "Боёв",
|
"battles": "Боёв",
|
||||||
"winrate": "Коэффициент побед",
|
"winrate": "Коэффициент побед",
|
||||||
@@ -646,7 +647,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"no_participation": "# %s не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
|
"no_participation": "# %s1 не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
|
||||||
"unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов."
|
"unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -671,12 +672,14 @@
|
|||||||
"copied": "Скопировано!"
|
"copied": "Скопировано!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Сбросить фильтры",
|
"clear_filters": "Сбросить фильтры",
|
||||||
"search_bar_hint": "Поиск танков...",
|
"search_bar_hint": "Поиск танков...",
|
||||||
"recent": "Недавние",
|
"recent": "Недавние",
|
||||||
"select_all": "Выбрать всё",
|
"select_all": "Выбрать всё",
|
||||||
"count_singular": "1 танк",
|
"count_singular": "1 танк",
|
||||||
"count_plural": "%s1 танк",
|
"count_plural": "Танков: %s1",
|
||||||
"sorting_by": "Сортировать по %s1",
|
"sorting_by": "Сортировать по %s1",
|
||||||
"no_tanks_found": {
|
"no_tanks_found": {
|
||||||
"body": "Танков не найдено.",
|
"body": "Танков не найдено.",
|
||||||
@@ -693,7 +696,7 @@
|
|||||||
"survivability.volume": "объём",
|
"survivability.volume": "объём",
|
||||||
"survivability.length": "длина",
|
"survivability.length": "длина",
|
||||||
"fire.dpm": "базовый ДПМ",
|
"fire.dpm": "базовый ДПМ",
|
||||||
"fire.dpmPremium": "улучшенный ДПМ",
|
"fire.dpmPremium": "премиумный ДПМ",
|
||||||
"fire.reload": "перезарядка",
|
"fire.reload": "перезарядка",
|
||||||
"fire.caliber": "калибр",
|
"fire.caliber": "калибр",
|
||||||
"fire.standardPenetration": "базовое пробитие",
|
"fire.standardPenetration": "базовое пробитие",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Отклонить"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Присоединяйтесь к крутым ребятам!",
|
"join_short": "Присоединяйтесь к крутым ребятам!",
|
||||||
"join_long": "Присоединяйся к супер-дупер крутым ребятам!",
|
"join_long": "Присоединяйся к супер-дупер крутым ребятам!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Смешиватель",
|
||||||
|
"button": "Смешать и собрать",
|
||||||
|
"description": "Модули смешанного танка",
|
||||||
|
"seo_description": "Смешивание и сбор корпусов, башен и пушек танков из World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Случайный выбор"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Угадайте танк",
|
"name": "Угадайте танк",
|
||||||
"button": "Угадать",
|
"button": "Угадать",
|
||||||
@@ -784,9 +803,12 @@
|
|||||||
"button": "Поиск танков",
|
"button": "Поиск танков",
|
||||||
"description": "Танковая энциклопедия Blitz",
|
"description": "Танковая энциклопедия Blitz",
|
||||||
"seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz",
|
"seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz",
|
||||||
"tank_seo_title": "%s1 - %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)",
|
"tank_seo_title": "%s1 — %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)",
|
||||||
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
|
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Пропуск"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Штурмовые снаряды"
|
"title": "Штурмовые снаряды"
|
||||||
},
|
},
|
||||||
@@ -806,13 +828,13 @@
|
|||||||
"no_ricochet": "Разрывные снаряды не рикошетят"
|
"no_ricochet": "Разрывные снаряды не рикошетят"
|
||||||
},
|
},
|
||||||
"view_range": {
|
"view_range": {
|
||||||
"camo": "Камо",
|
"camo": "Камуфляж",
|
||||||
"percentage": "%s1%",
|
"percentage": "%s1%",
|
||||||
"ghost_factory": "Промзона",
|
"ghost_factory": "Промзона",
|
||||||
"dead_rail": "Эшелон"
|
"dead_rail": "Эшелон"
|
||||||
},
|
},
|
||||||
"flexibility": {
|
"flexibility": {
|
||||||
"yaw": "Отклонение",
|
"yaw": "Поворот",
|
||||||
"pitch": "Наклон",
|
"pitch": "Наклон",
|
||||||
"min": "Вниз",
|
"min": "Вниз",
|
||||||
"max": "Вверх"
|
"max": "Вверх"
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "Зелёное пробитие",
|
"green_penetration": "Зелёное пробитие",
|
||||||
"hide_model_under_armor": "Скрыть модель под броней",
|
"hide_model_under_armor": "Скрыть модель под броней",
|
||||||
"opaque": "Прозрачность",
|
"opaque": "Прозрачность",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Окружение",
|
|
||||||
"show_grid": "Показать сетку"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "Загрузить",
|
"download": "Загрузить",
|
||||||
@@ -902,7 +922,7 @@
|
|||||||
"tier": "Уровень",
|
"tier": "Уровень",
|
||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"dev_id": "DEV: ID",
|
"dev_id": "DEV: ID",
|
||||||
"purchase": "Стоимость покупки",
|
"purchase": "Цена покупки",
|
||||||
"restoration": "Цена восстановления",
|
"restoration": "Цена восстановления",
|
||||||
"sale": "Цена продажи",
|
"sale": "Цена продажи",
|
||||||
"research": "Необходимо опыта"
|
"research": "Необходимо опыта"
|
||||||
@@ -978,7 +998,7 @@
|
|||||||
"dpmEffective": "Эффективность за 60с",
|
"dpmEffective": "Эффективность за 60с",
|
||||||
"shells": "Снарядов в барабане",
|
"shells": "Снарядов в барабане",
|
||||||
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
"mostOptimalShellIndex": "Самый эффективный снаряд",
|
||||||
"shellReloads": "Reloads",
|
"shellReloads": "Перезарядка",
|
||||||
"shellReload": "Перезарядка",
|
"shellReload": "Перезарядка",
|
||||||
"intraClip": "Между снарядами",
|
"intraClip": "Между снарядами",
|
||||||
"caliber": "Калибр",
|
"caliber": "Калибр",
|
||||||
@@ -1038,8 +1058,8 @@
|
|||||||
"shellCapacity": "Боезапас",
|
"shellCapacity": "Боезапас",
|
||||||
"gunType": "Тип орудия",
|
"gunType": "Тип орудия",
|
||||||
"penetrationAt250m": "Пробитие на 250м",
|
"penetrationAt250m": "Пробитие на 250м",
|
||||||
"burstShells": "Burst shell count",
|
"burstShells": "Кол-во разрывных снарядов",
|
||||||
"burstInterShell": "Burst inter-shell"
|
"burstInterShell": "Взрыв между снарядами"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"firepower": {
|
"firepower": {
|
||||||
@@ -1136,7 +1156,7 @@
|
|||||||
"hits": "Уничтожен",
|
"hits": "Уничтожен",
|
||||||
"damage_ratio": "Коэф. урона",
|
"damage_ratio": "Коэф. урона",
|
||||||
"damage_taken": "Получено урона",
|
"damage_taken": "Получено урона",
|
||||||
"capture_points": "Ограничительные точки"
|
"capture_points": "Точки ограничения"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1148,7 +1168,7 @@
|
|||||||
"last_updated": "Последнее обновление %s1",
|
"last_updated": "Последнее обновление %s1",
|
||||||
"search": {
|
"search": {
|
||||||
"hint": "Поиск аватаров...",
|
"hint": "Поиск аватаров...",
|
||||||
"results": "%s1 аватар"
|
"results": "Аватаров: %s1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
@@ -1157,7 +1177,7 @@
|
|||||||
"description": "Статистика сеанса игры",
|
"description": "Статистика сеанса игры",
|
||||||
"seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz",
|
"seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
"lookup": "Искать игрока, чтобы начать",
|
"lookup": "Введите ник игрока",
|
||||||
"hint": "Поиск игроков...",
|
"hint": "Поиск игроков...",
|
||||||
"searching": "Поиск...",
|
"searching": "Поиск...",
|
||||||
"no_results": "Игроков не найдено"
|
"no_results": "Игроков не найдено"
|
||||||
@@ -1198,7 +1218,7 @@
|
|||||||
"button": "Создать",
|
"button": "Создать",
|
||||||
"description": "Встраиваемые элементы для стримов",
|
"description": "Встраиваемые элементы для стримов",
|
||||||
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
|
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
|
||||||
"coming_soon": "Скоро появятся новые встраиваемые элементы.",
|
"coming_soon": "В скором времени появятся новые встраиваемые элементы.",
|
||||||
"suggest": "Предложить идеи.",
|
"suggest": "Предложить идеи.",
|
||||||
"preview": {
|
"preview": {
|
||||||
"title": "Предпросмотр",
|
"title": "Предпросмотр",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "Начните с BlitzKit",
|
"description": "Начните с BlitzKit",
|
||||||
"seo_description": "Начните с BlitzKit"
|
"seo_description": "Начните с BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "BlitzAnalysis[]",
|
"name": "API",
|
||||||
"button": "Анализируй",
|
"button": "To the docs",
|
||||||
"description": "Богатая статистика",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Богатые статистика по World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
|
||||||
},
|
|
||||||
"blitz_insider": {
|
|
||||||
"name": "Blitz Insider",
|
|
||||||
"button": "Открой для себя",
|
|
||||||
"description": "Статистика сеанса игры",
|
|
||||||
"seo_description": "Статистика сеанса игры в World of Tanks Blitz"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
|
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1м",
|
"m": "%s1м",
|
||||||
"mm": "%s1мм",
|
"mm": "%s1мм",
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"copied": "Скопійовано!"
|
"copied": "Скопійовано!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "Clear filters",
|
"clear_filters": "Clear filters",
|
||||||
"search_bar_hint": "Шукайте техніку...",
|
"search_bar_hint": "Шукайте техніку...",
|
||||||
"recent": "Недавно переглянуті",
|
"recent": "Недавно переглянуті",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "Join cool people!",
|
"join_short": "Join cool people!",
|
||||||
"join_long": "Join super duper cool people!",
|
"join_long": "Join super duper cool people!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"button": "Guess away",
|
||||||
@@ -787,6 +806,9 @@
|
|||||||
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz",
|
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz",
|
||||||
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.",
|
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "Зелене пробиття",
|
"green_penetration": "Зелене пробиття",
|
||||||
"hide_model_under_armor": "Hide model under armor",
|
"hide_model_under_armor": "Hide model under armor",
|
||||||
"opaque": "Opaque",
|
"opaque": "Opaque",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Environment",
|
|
||||||
"show_grid": "Show grid"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "Завантажити",
|
"download": "Завантажити",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "Get started with BlitzKit",
|
"description": "Get started with BlitzKit",
|
||||||
"seo_description": "Get started with BlitzKit"
|
"seo_description": "Get started with BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "BlitzAnalysis[]",
|
"name": "API",
|
||||||
"button": "Analyze now",
|
"button": "To the docs",
|
||||||
"description": "Rich historic average stats",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "Rich historic average stats in World of Tanks Blitz"
|
"seo_description": "BlitzKit hosted static API for 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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"fox": "褐色的狐狸迅速地跳过那只懒狗"
|
"fox": "褐色的狐狸迅速地跳过那只懒狗"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"name": "BlitzKit",
|
||||||
"units": {
|
"units": {
|
||||||
"m": "%s1公尺",
|
"m": "%s1公尺",
|
||||||
"mm": "%s1毫米",
|
"mm": "%s1毫米",
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"copied": "已拷贝!"
|
"copied": "已拷贝!"
|
||||||
},
|
},
|
||||||
"tank_search": {
|
"tank_search": {
|
||||||
|
"select_dialog_title": "Search for tanks",
|
||||||
|
"swap_dialog_title": "Swap tank",
|
||||||
"clear_filters": "清除筛选",
|
"clear_filters": "清除筛选",
|
||||||
"search_bar_hint": "搜寻战车……",
|
"search_bar_hint": "搜寻战车……",
|
||||||
"recent": "最近查看的",
|
"recent": "最近查看的",
|
||||||
@@ -739,6 +742,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"pwa": {
|
||||||
|
"title": "Install the BlitzKit App",
|
||||||
|
"new": "NEW",
|
||||||
|
"checking": "Checking compatibility...",
|
||||||
|
"instructions": "Instructions for Apple devices.",
|
||||||
|
"install": "Install",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"join_short": "加入超級酷的人群!",
|
"join_short": "加入超級酷的人群!",
|
||||||
"join_long": "加入我們的Discord社群,和超酷的人一起聊天!",
|
"join_long": "加入我們的Discord社群,和超酷的人一起聊天!",
|
||||||
@@ -764,6 +775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
|
||||||
|
"screenshot": "Screenshot",
|
||||||
|
"randomize": "Randomize"
|
||||||
|
},
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "猜坦克",
|
"name": "猜坦克",
|
||||||
"button": "猜猜看",
|
"button": "猜猜看",
|
||||||
@@ -787,6 +806,9 @@
|
|||||||
"tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)",
|
"tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)",
|
||||||
"tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。",
|
"tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。",
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "跳过"
|
||||||
|
},
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -829,9 +851,7 @@
|
|||||||
"green_penetration": "可击穿区域",
|
"green_penetration": "可击穿区域",
|
||||||
"hide_model_under_armor": "隐藏装甲下的模型",
|
"hide_model_under_armor": "隐藏装甲下的模型",
|
||||||
"opaque": "不透明",
|
"opaque": "不透明",
|
||||||
"dev_wireframe": "开发选项:网格",
|
"dev_wireframe": "开发选项:网格"
|
||||||
"environment": "环境",
|
|
||||||
"show_grid": "显示网格"
|
|
||||||
},
|
},
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
@@ -1307,17 +1327,11 @@
|
|||||||
"description": "开始使用 BlitzKit",
|
"description": "开始使用 BlitzKit",
|
||||||
"seo_description": "开始使用 BlitzKit"
|
"seo_description": "开始使用 BlitzKit"
|
||||||
},
|
},
|
||||||
"blitz_analysis": {
|
"api": {
|
||||||
"name": "闪击战分析",
|
"name": "API",
|
||||||
"button": "立即分析",
|
"button": "To the docs",
|
||||||
"description": "丰富的历史平均统计",
|
"description": "BlitzKit hosted API",
|
||||||
"seo_description": "战车世界闪击战中丰富的历史平均统计"
|
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
|
||||||
},
|
|
||||||
"blitz_insider": {
|
|
||||||
"name": "闪击战内嵌插件",
|
|
||||||
"button": "开始探索",
|
|
||||||
"description": "游戏内场次统计",
|
|
||||||
"seo_description": "战车世界闪击战内的游戏内场次统计"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -11,20 +11,21 @@
|
|||||||
"build:ids": "bun src/discoverIds.ts",
|
"build:ids": "bun src/discoverIds.ts",
|
||||||
"build:bot": "bun src/buildBot.ts",
|
"build:bot": "bun src/buildBot.ts",
|
||||||
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
|
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
|
||||||
|
"build:proxyclient": "bun src/buildProxyClient.ts",
|
||||||
"prunestrings": "bun src/pruneStrings.ts"
|
"prunestrings": "bun src/pruneStrings.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
"@gltf-transform/core": "^4.0.10",
|
"@gltf-transform/core": "^4.2.1",
|
||||||
"@gltf-transform/extensions": "^4.0.10",
|
"@gltf-transform/extensions": "^4.2.1",
|
||||||
"@gltf-transform/functions": "^4.0.10",
|
"@gltf-transform/functions": "^4.2.1",
|
||||||
"csv-parse": "^5.5.6",
|
"csv-parse": "^6.1.0",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"esbuild": "^0.19.12",
|
"esbuild": "^0.25.10",
|
||||||
"fast-xml-parser": "^4.4.1",
|
"fast-xml-parser": "^5.2.5",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
"puppeteer": "^23.10.4"
|
"puppeteer": "^24.22.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/decompress": "^4.2.7",
|
"@types/decompress": "^4.2.7",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from '@blitzkit/core';
|
import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from "@blitzkit/core";
|
||||||
import { launch } from 'puppeteer';
|
import { launch } from "puppeteer";
|
||||||
import sharp from 'sharp';
|
import sharp from "sharp";
|
||||||
import { commitAssets } from '../core/github/commitAssets';
|
import { commitAssets } from "../core/github/commitAssets";
|
||||||
import { FileChange } from '../core/github/commitMultipleFiles';
|
import { FileChange } from "../core/github/commitMultipleFiles";
|
||||||
|
|
||||||
export async function blitzkitTankIcons() {
|
export async function blitzkitTankIcons() {
|
||||||
console.log('Building blitzkit tank icons...');
|
console.log("Building blitzkit tank icons...");
|
||||||
|
|
||||||
const browser = await launch();
|
const browser = await launch();
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
@@ -21,7 +21,7 @@ export async function blitzkitTankIcons() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
|
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
|
||||||
waitUntil: 'networkidle0',
|
waitUntil: "networkidle0",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`${id} ${name} time out`, error);
|
console.error(`${id} ${name} time out`, error);
|
||||||
@@ -29,21 +29,21 @@ export async function blitzkitTankIcons() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const screenshot = await page.screenshot({
|
const screenshot = await page.screenshot({
|
||||||
type: 'webp',
|
type: "webp",
|
||||||
omitBackground: true,
|
omitBackground: true,
|
||||||
});
|
});
|
||||||
const content = await sharp(screenshot).trim({ threshold: 100 }).toBuffer();
|
const content = await sharp(screenshot).trim({ threshold: 100 }).toBuffer();
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
console.log(
|
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 page.close();
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
||||||
await commitAssets('tank icons', files);
|
await commitAssets("tank icons", files);
|
||||||
}
|
}
|
||||||
@@ -1,50 +1,52 @@
|
|||||||
import { toUniqueId } from '@blitzkit/core';
|
import { toUniqueId } from "@blitzkit/core";
|
||||||
import { NodeIO } from '@gltf-transform/core';
|
import { NodeIO } from "@gltf-transform/core";
|
||||||
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
import { readdir } from 'fs/promises';
|
import { readdir } from "fs/promises";
|
||||||
import { extractModel } from '../core/blitz/extractModel';
|
import { extractModel } from "../core/blitz/extractModel";
|
||||||
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
|
||||||
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
|
||||||
import { commitAssets } from '../core/github/commitAssets';
|
import { commitAssets } from "../core/github/commitAssets";
|
||||||
import { FileChange } from '../core/github/commitMultipleFiles';
|
import { FileChange } from "../core/github/commitMultipleFiles";
|
||||||
import { DATA } from './constants';
|
import { DATA } from "./constants";
|
||||||
import { VehicleDefinitionList } from './definitions';
|
import { VehicleDefinitionList } from "./definitions";
|
||||||
import { TankParameters } from './tankIcons';
|
import { TankParameters } from "./tankIcons";
|
||||||
|
|
||||||
export async function tankModels() {
|
export async function tankModels() {
|
||||||
console.log('Building tank models...');
|
console.log("Building tank models...");
|
||||||
|
|
||||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||||
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
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) {
|
for (const nationIndex in nations) {
|
||||||
const nation = nations[nationIndex];
|
const nation = nations[nationIndex];
|
||||||
const changes: FileChange[] = [];
|
const changes: FileChange[] = [];
|
||||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
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}`);
|
console.log(`Building models for ${nation}`);
|
||||||
|
|
||||||
for (const [tankKey, tank] of Object.entries(tanks.root)) {
|
let index = 0;
|
||||||
if (tankKey.includes('tutorial_bot')) continue;
|
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 id = toUniqueId(nation, tank.id);
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
|
||||||
);
|
);
|
||||||
const model = await extractModel(
|
const model = await extractModel(
|
||||||
DATA,
|
DATA,
|
||||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "")
|
||||||
);
|
);
|
||||||
|
const content = Buffer.from(await nodeIO.writeBinary(model));
|
||||||
|
|
||||||
changes.push({
|
changes.push({ path: `3d/tanks/models/${id}.glb`, content });
|
||||||
path: `3d/tanks/models/${id}.glb`,
|
|
||||||
content: Buffer.from(await nodeIO.writeBinary(model)),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await commitAssets(`tank models ${nation}`, changes);
|
await commitAssets(`tank models ${nation}`, changes);
|
||||||
|
|||||||
@@ -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>> =
|
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
||||||
{
|
{
|
||||||
[VertexAttribute.VERTEX]: 'POSITION',
|
[VertexAttribute.VERTEX]: "POSITION",
|
||||||
[VertexAttribute.NORMAL]: 'NORMAL',
|
[VertexAttribute.NORMAL]: "NORMAL",
|
||||||
[VertexAttribute.TEXCOORD0]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
|
||||||
[VertexAttribute.TEXCOORD1]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
|
||||||
[VertexAttribute.TEXCOORD2]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
|
||||||
[VertexAttribute.TEXCOORD3]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
|
||||||
};
|
};
|
||||||
export const vertexAttributeGltfVectorSizes = {
|
export const vertexAttributeGltfVectorSizes = {
|
||||||
...vertexAttributeVectorSizes,
|
...vertexAttributeVectorSizes,
|
||||||
|
|||||||
@@ -4,35 +4,35 @@ import {
|
|||||||
ScgReadStream,
|
ScgReadStream,
|
||||||
Textures,
|
Textures,
|
||||||
VertexAttribute,
|
VertexAttribute,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { Document, Material, Node, Scene } from '@gltf-transform/core';
|
import { Document, Material, Node, Scene } from "@gltf-transform/core";
|
||||||
import { dedup, prune } from '@gltf-transform/functions';
|
import { dedup, prune } from "@gltf-transform/functions";
|
||||||
import { times } from 'lodash-es';
|
import { times } from "lodash-es";
|
||||||
import { dirname } from 'path';
|
import { dirname } from "path";
|
||||||
import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three';
|
import { readBaseColor } from "../readBaseColor";
|
||||||
import { readDVPLFile } from '../readDVPLFile';
|
import { readDVPLFile } from "../readDVPLFile";
|
||||||
import { readTexture } from '../readTexture';
|
import { readNormal } from "../readNormal";
|
||||||
import { TextureMutation } from '../readTexture/constants';
|
import { readRoughnessMetallic } from "../readRoughnessMetallic";
|
||||||
import {
|
import {
|
||||||
vertexAttributeGLTFName,
|
vertexAttributeGLTFName,
|
||||||
vertexAttributeGltfVectorSizes,
|
vertexAttributeGltfVectorSizes,
|
||||||
} from './constants';
|
} from "./constants";
|
||||||
|
|
||||||
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
||||||
|
|
||||||
const omitMeshNames = {
|
const omitMeshNames = {
|
||||||
start: ['chassis_chassis_', 'chassis_track_crash_', 'HP_'],
|
start: ["chassis_chassis_", "chassis_track_crash_", "HP_"],
|
||||||
end: ['_POINT'],
|
end: ["_POINT"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function extractModel(data: string, path: string) {
|
export async function extractModel(data: string, path: string) {
|
||||||
const sc2Path = `${data}/3d/${path}.sc2`;
|
const sc2Path = `${data}/3d/${path}.sc2`;
|
||||||
const scgPath = `${data}/3d/${path}.scg`;
|
const scgPath = `${data}/3d/${path}.scg`;
|
||||||
const sc2 = new Sc2ReadStream(
|
const sc2 = new Sc2ReadStream(
|
||||||
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer,
|
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer
|
||||||
).sc2();
|
).sc2();
|
||||||
const scg = new ScgReadStream(
|
const scg = new ScgReadStream(
|
||||||
(await readDVPLFile(scgPath)).buffer as ArrayBuffer,
|
(await readDVPLFile(scgPath)).buffer as ArrayBuffer
|
||||||
).scg();
|
).scg();
|
||||||
const document = new Document();
|
const document = new Document();
|
||||||
const scene = document.createScene();
|
const scene = document.createScene();
|
||||||
@@ -41,8 +41,8 @@ export async function extractModel(data: string, path: string) {
|
|||||||
|
|
||||||
// create materials
|
// create materials
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
sc2['#dataNodes'].map(async (node) => {
|
sc2["#dataNodes"].map(async (node) => {
|
||||||
const id = new DataView(node['#id']).getBigUint64(0, true);
|
const id = new DataView(node["#id"]).getBigUint64(0, true);
|
||||||
|
|
||||||
if (node.parentMaterialKey !== undefined) {
|
if (node.parentMaterialKey !== undefined) {
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +58,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
|
|
||||||
if (node.textures) {
|
if (node.textures) {
|
||||||
textures = node.textures;
|
textures = node.textures;
|
||||||
} else if (typeof node.configCount === 'number') {
|
} else if (typeof node.configCount === "number") {
|
||||||
textures = node.configArchive_0.textures;
|
textures = node.configArchive_0.textures;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,26 +66,27 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setBaseColorTexture(
|
material.setBaseColorTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/jpeg')
|
.setMimeType("image/jpeg")
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readBaseColor(
|
||||||
`${data}/3d/${dirname(path)}/${textures.albedo}`,
|
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
|
||||||
TextureMutation.Albedo,
|
textures.miscMap
|
||||||
),
|
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
|
||||||
),
|
: undefined
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (textures.baseRMMap) {
|
if (textures.baseRMMap) {
|
||||||
material.setMetallicRoughnessTexture(
|
material.setMetallicRoughnessTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/jpeg')
|
.setMimeType("image/jpeg")
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readRoughnessMetallic(
|
||||||
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`,
|
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`
|
||||||
TextureMutation.RoughnessMetallicness,
|
)
|
||||||
),
|
)
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,47 +96,33 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setNormalTexture(
|
material.setNormalTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/jpeg')
|
.setMimeType("image/jpeg")
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readNormal(
|
||||||
`${data}/3d/${dirname(path)}/${
|
`${data}/3d/${dirname(path)}/${
|
||||||
textures.baseNormalMap ?? textures.normalmap
|
textures.baseNormalMap ?? textures.normalmap
|
||||||
}`,
|
}`,
|
||||||
isBase ? TextureMutation.Normal : undefined,
|
isBase
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textures.miscMap) {
|
|
||||||
material.setOcclusionTexture(
|
|
||||||
document
|
|
||||||
.createTexture(node.materialName)
|
|
||||||
.setMimeType('image/jpeg')
|
|
||||||
.setImage(
|
|
||||||
await readTexture(
|
|
||||||
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
|
|
||||||
TextureMutation.Miscellaneous,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
materials.set(
|
materials.set(
|
||||||
new DataView(node['#id']).getBigUint64(0, true),
|
new DataView(node["#id"]).getBigUint64(0, true),
|
||||||
material,
|
material
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// replace children materials with parents
|
// replace children materials with parents
|
||||||
materials.forEach((material, id) => {
|
materials.forEach((material, id) => {
|
||||||
if (typeof material !== 'bigint') return;
|
if (typeof material !== "bigint") return;
|
||||||
|
|
||||||
let resolvedMaterial: Material | bigint | undefined = material;
|
let resolvedMaterial: Material | bigint | undefined = material;
|
||||||
|
|
||||||
while (typeof resolvedMaterial === 'bigint') {
|
while (typeof resolvedMaterial === "bigint") {
|
||||||
const linkedParentMaterial = materials.get(resolvedMaterial);
|
const linkedParentMaterial = materials.get(resolvedMaterial);
|
||||||
|
|
||||||
resolvedMaterial = linkedParentMaterial;
|
resolvedMaterial = linkedParentMaterial;
|
||||||
@@ -155,36 +142,36 @@ export async function extractModel(data: string, path: string) {
|
|||||||
const node = document.createNode(hierarchy.name);
|
const node = document.createNode(hierarchy.name);
|
||||||
const components = times(
|
const components = times(
|
||||||
hierarchy.components.count,
|
hierarchy.components.count,
|
||||||
(index) => hierarchy.components[index.toString().padStart(4, '0')],
|
(index) => hierarchy.components[index.toString().padStart(4, "0")]
|
||||||
);
|
);
|
||||||
|
|
||||||
components.forEach((component) => {
|
components.forEach((component) => {
|
||||||
switch (component['comp.typename']) {
|
switch (component["comp.typename"]) {
|
||||||
case 'LodComponent':
|
case "LodComponent":
|
||||||
// found and used later by transform component
|
// found and used later by transform component
|
||||||
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'TransformComponent': {
|
case "TransformComponent": {
|
||||||
node.setTranslation(component['tc.localTranslation']);
|
node.setTranslation(component["tc.localTranslation"]);
|
||||||
node.setRotation(component['tc.localRotation']);
|
node.setRotation(component["tc.localRotation"]);
|
||||||
node.setScale(component['tc.localScale']);
|
node.setScale(component["tc.localScale"]);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'RenderComponent': {
|
case "RenderComponent": {
|
||||||
const renderObject = component['rc.renderObj'];
|
const renderObject = component["rc.renderObj"];
|
||||||
|
|
||||||
times(renderObject['ro.batchCount'], (batchIndex): void => {
|
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
||||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||||
|
|
||||||
if (lodIndex !== 0) return;
|
if (lodIndex !== 0) return;
|
||||||
|
|
||||||
const batchKey = batchIndex.toString().padStart(4, '0');
|
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||||
const batch = renderObject['ro.batches'][batchKey];
|
const batch = renderObject["ro.batches"][batchKey];
|
||||||
const material = materials.get(batch['rb.nmatname']);
|
const material = materials.get(batch["rb.nmatname"]);
|
||||||
const polygonGroup = scg.get(batch['rb.datasource']);
|
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||||
|
|
||||||
if (!(material instanceof Material)) {
|
if (!(material instanceof Material)) {
|
||||||
// probably shadow material
|
// probably shadow material
|
||||||
@@ -192,14 +179,14 @@ export async function extractModel(data: string, path: string) {
|
|||||||
}
|
}
|
||||||
if (polygonGroup === undefined) {
|
if (polygonGroup === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing polygon group ${batch['rb.datasource']}`,
|
`Missing polygon group ${batch["rb.datasource"]}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lodNode = document.createNode(batchKey);
|
const lodNode = document.createNode(batchKey);
|
||||||
const indicesAccessor = document
|
const indicesAccessor = document
|
||||||
.createAccessor()
|
.createAccessor()
|
||||||
.setType('SCALAR')
|
.setType("SCALAR")
|
||||||
.setArray(new Uint16Array(polygonGroup.indices))
|
.setArray(new Uint16Array(polygonGroup.indices))
|
||||||
.setBuffer(buffer);
|
.setBuffer(buffer);
|
||||||
const primitive = document
|
const primitive = document
|
||||||
@@ -233,7 +220,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||||
const attributeAccessor = document
|
const attributeAccessor = document
|
||||||
.createAccessor(name)
|
.createAccessor(name)
|
||||||
.setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`)
|
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||||
.setArray(new Float32Array(value.flat()))
|
.setArray(new Float32Array(value.flat()))
|
||||||
.setBuffer(buffer);
|
.setBuffer(buffer);
|
||||||
|
|
||||||
@@ -241,7 +228,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mesh = document
|
const mesh = document
|
||||||
.createMesh(batch['##name'])
|
.createMesh(batch["##name"])
|
||||||
.addPrimitive(primitive);
|
.addPrimitive(primitive);
|
||||||
lodNode.setMesh(mesh);
|
lodNode.setMesh(mesh);
|
||||||
node.addChild(lodNode);
|
node.addChild(lodNode);
|
||||||
@@ -253,24 +240,24 @@ export async function extractModel(data: string, path: string) {
|
|||||||
default: {
|
default: {
|
||||||
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled component type: ${component['comp.typename']}`,
|
`Unhandled component type: ${component["comp.typename"]}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hierarchy['#hierarchy']) {
|
if (hierarchy["#hierarchy"]) {
|
||||||
parseHierarchies(hierarchy['#hierarchy'], node);
|
parseHierarchies(hierarchy["#hierarchy"], node);
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.addChild(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());
|
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 { DdsReadStream, PvrReadStream } from "@blitzkit/core";
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from "fs";
|
||||||
import sharp from 'sharp';
|
import sharp from "sharp";
|
||||||
import { DATA_COMPRESSED } from '../../../buildAssets/constants';
|
import { DATA_COMPRESSED } from "../../../buildAssets/constants";
|
||||||
import { readDVPLFile } from '../readDVPLFile';
|
import { readDVPLFile } from "../readDVPLFile";
|
||||||
import { TextureMutation } from './constants';
|
import { TextureMutation } from "./constants";
|
||||||
|
|
||||||
export async function readTexture(path: string, mutation?: TextureMutation) {
|
export async function readTexture(path: string) {
|
||||||
const ddsTexturePath = path.replace('.tex', '.dx11.dds');
|
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
|
||||||
const isDds = existsSync(
|
const isDds = existsSync(
|
||||||
`${ddsTexturePath}${DATA_COMPRESSED ? '.dvpl' : ''}`,
|
`${ddsTexturePath}${DATA_COMPRESSED ? ".dvpl" : ""}`
|
||||||
);
|
);
|
||||||
const resolvedTexturePath = isDds
|
const resolvedTexturePath = isDds
|
||||||
? ddsTexturePath
|
? ddsTexturePath
|
||||||
: ddsTexturePath.replace('.dds', '.pvr');
|
: ddsTexturePath.replace(".dds", ".pvr");
|
||||||
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`);
|
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`);
|
||||||
|
|
||||||
const raw = isDds
|
const raw = isDds
|
||||||
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds()
|
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds()
|
||||||
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr();
|
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr();
|
||||||
|
|
||||||
switch (mutation) {
|
return raw;
|
||||||
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);
|
|
||||||
|
|
||||||
raw.data[index] = Math.round((x + 1) * (255 / 2));
|
// switch (mutation) {
|
||||||
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
// case TextureMutation.Normal: {
|
||||||
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
// const bytes = 4 * raw.width * raw.height;
|
||||||
raw.data[index + 3] = 255;
|
// 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: {
|
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||||
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 newImage
|
// case TextureMutation.RoughnessMetallicness: {
|
||||||
.joinChannel(await Promise.all([roughness, metallicness]))
|
// const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
|
||||||
.jpeg()
|
// raw: { ...raw, channels: 1 },
|
||||||
.toBuffer();
|
// });
|
||||||
}
|
// const metallicness = sharp(raw.data, { raw })
|
||||||
|
// .extractChannel("green")
|
||||||
|
// .jpeg()
|
||||||
|
// .toBuffer();
|
||||||
|
// const roughness = sharp(raw.data, { raw })
|
||||||
|
// .extractChannel("alpha")
|
||||||
|
// .jpeg()
|
||||||
|
// .toBuffer();
|
||||||
|
|
||||||
case TextureMutation.Miscellaneous: {
|
// return await newImage
|
||||||
/**
|
// .joinChannel(await Promise.all([roughness, metallicness]))
|
||||||
* Alpha is ambient occlusion and green is emissive. But very few tanks
|
// .jpeg()
|
||||||
* use emissive so I am ignoring it for now.
|
// .toBuffer();
|
||||||
*/
|
// }
|
||||||
return await sharp(raw.data, { raw })
|
|
||||||
.extractChannel('alpha')
|
|
||||||
.jpeg()
|
|
||||||
.toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
case TextureMutation.Albedo: {
|
// case TextureMutation.Miscellaneous: {
|
||||||
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
// /**
|
||||||
}
|
// * 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:
|
// case TextureMutation.Albedo: {
|
||||||
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
// return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// default:
|
||||||
|
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
@@ -1,47 +1,61 @@
|
|||||||
import { exec as execSync } from 'child_process';
|
import { exec as execSync } from "child_process";
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from "fs";
|
||||||
import { readdir, rm, writeFile } from 'fs/promises';
|
import { readdir, rm } from "fs/promises";
|
||||||
import { promisify } from 'util';
|
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);
|
const exec = promisify(execSync);
|
||||||
|
|
||||||
let executableFileExtension: string;
|
let executableFileExtension: string;
|
||||||
|
|
||||||
if (existsSync('node_modules/.bin/protoc-gen-ts_proto.exe')) {
|
if (existsSync("node_modules/.bin/protoc-gen-ts_proto.exe")) {
|
||||||
executableFileExtension = '.exe';
|
executableFileExtension = ".exe";
|
||||||
} else {
|
} else {
|
||||||
executableFileExtension = '';
|
executableFileExtension = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const filesRaw = await readdir(`${ROOT}`);
|
for (const root of roots) {
|
||||||
await Promise.all(
|
const filesRaw = await readdir(`${root}`);
|
||||||
filesRaw.map(async (file) => {
|
const files: string[] = [];
|
||||||
if (!file.endsWith('.proto')) {
|
|
||||||
await rm(`${ROOT}/${file}`);
|
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'));
|
await exec(command);
|
||||||
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'),
|
|
||||||
);
|
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
import {
|
import { type TranslationTree } from "@blitzkit/i18n";
|
||||||
DEFAULT_LOCALE,
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
SUPPORTED_LOCALES,
|
import { writeFile } from "fs/promises";
|
||||||
TranslationTree,
|
|
||||||
} from '@blitzkit/i18n';
|
|
||||||
import { writeFile } from 'fs/promises';
|
|
||||||
|
|
||||||
const ROOT = '@blitzkit/i18n/strings';
|
const ROOT = "@blitzkit/i18n/strings";
|
||||||
const WORKING_ROOT = '../../packages/i18n/strings';
|
const WORKING_ROOT = "../../packages/i18n/strings";
|
||||||
|
|
||||||
const defaultStrings = require(`${ROOT}/${DEFAULT_LOCALE}.json`).default;
|
const defaultStrings = require(`${ROOT}/${locales.default}.json`).default;
|
||||||
const nonDefaultLocales = SUPPORTED_LOCALES.filter(
|
const nonDefaultLocales = locales.supported.filter(
|
||||||
(locale) => locale !== DEFAULT_LOCALE,
|
({ locale }) => locale !== locales.default
|
||||||
);
|
);
|
||||||
|
|
||||||
function prune(defaultStrings: TranslationTree, strings: TranslationTree) {
|
function prune(defaultStrings: TranslationTree, strings: TranslationTree) {
|
||||||
for (const key in strings) {
|
for (const key in strings) {
|
||||||
if (key in defaultStrings) {
|
if (key in defaultStrings) {
|
||||||
if (typeof defaultStrings[key] === typeof strings[key]) {
|
if (typeof defaultStrings[key] === typeof strings[key]) {
|
||||||
if (typeof strings[key] === 'string') {
|
if (typeof strings[key] === "string") {
|
||||||
if (
|
if (
|
||||||
// are equivalent regardless of %s flavor
|
// are equivalent regardless of %s flavor
|
||||||
(defaultStrings[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') ||
|
(strings[key] as string).replaceAll(/%s\d/g, "%s") ||
|
||||||
// string is empty
|
// string is empty
|
||||||
strings[key] === '' ||
|
strings[key] === "" ||
|
||||||
// string has old flavor of %s
|
// string has old flavor of %s
|
||||||
strings[key].match(/%s([^\d]|$)/g) !== null
|
strings[key].match(/%s([^\d]|$)/g) !== null
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "varuna",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json"
|
||||||
|
}
|
||||||
@@ -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/
|
|
||||||
@@ -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.*$/],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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(),
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -2,53 +2,40 @@
|
|||||||
"name": "@blitzkit/website",
|
"name": "@blitzkit/website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "PUBLIC_TARGET=web astro dev",
|
||||||
"start": "astro dev",
|
"build": "PUBLIC_TARGET=web astro build",
|
||||||
|
"preview": "PUBLIC_TARGET=web astro preview",
|
||||||
"lint": "astro check",
|
"lint": "astro check",
|
||||||
"build": "astro check && astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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/check": "^0.9.4",
|
||||||
"@astrojs/react": "^3.6.2",
|
"@astrojs/node": "^9.4.4",
|
||||||
"@astrojs/sitemap": "^3.2.1",
|
"@astrojs/react": "^4.4.0",
|
||||||
"@playform/compress": "^0.1.9",
|
"@astrolib/seo": "^1.0.0-beta.8",
|
||||||
"@playform/inline": "^0.1.2",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@radix-ui/colors": "^3.0.0",
|
||||||
"@types/react": "^18.3.12",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@radix-ui/themes": "^3.2.1",
|
||||||
"@types/three": "^0.160.0",
|
"@react-three/drei": "^10.7.6",
|
||||||
"astro": "^4.16.7",
|
"@react-three/fiber": "^9.3.0",
|
||||||
"astro-compressor": "^1.0.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 490 KiB After Width: | Height: | Size: 669 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 692 B |
|
Before Width: | Height: | Size: 692 B |
@@ -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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Code,
|
Code,
|
||||||
@@ -6,26 +6,21 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from "@react-three/drei";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
|
import { resolveArmorIndex } from "../../../core/blitzkit/resolveArmorIndex";
|
||||||
import { App } from '../../../stores/app';
|
import { App } from "../../../stores/app";
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from "../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { layerTypeNames } from './ShotDisplayCard';
|
import { layerTypeNames } from "./ShotDisplayCard";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
|
|
||||||
export function ArmorPlateDisplay() {
|
export function ArmorPlateDisplay() {
|
||||||
const highlightArmor = TankopediaEphemeral.use(
|
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
|
||||||
(state) => state.highlightArmor,
|
|
||||||
);
|
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
|
|
||||||
if (highlightArmor === undefined) return null;
|
if (highlightArmor === undefined) return null;
|
||||||
|
|
||||||
@@ -33,20 +28,20 @@ export function ArmorPlateDisplay() {
|
|||||||
<group position={highlightArmor.point}>
|
<group position={highlightArmor.point}>
|
||||||
<Html
|
<Html
|
||||||
center
|
center
|
||||||
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
|
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
variant="classic"
|
variant="classic"
|
||||||
mb="4"
|
mb="4"
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
color: highlightArmor.color,
|
color: highlightArmor.color,
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
<Text weight="bold">
|
<Text weight="bold">
|
||||||
{layerTypeNames[highlightArmor.type]}{' '}
|
{layerTypeNames[highlightArmor.type]}{" "}
|
||||||
{highlightArmor.thickness.toFixed(0)}
|
{highlightArmor.thickness.toFixed(0)}
|
||||||
<Text size="1" weight="regular">
|
<Text size="1" weight="regular">
|
||||||
mm
|
mm
|
||||||
@@ -92,34 +87,34 @@ export function ArmorPlateDisplay() {
|
|||||||
|
|
||||||
if (isNaN(thickness) || thickness < 0) return;
|
if (isNaN(thickness) || thickness < 0) return;
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const { protagonist } = duelStore.getState();
|
const { protagonist } = Duel.state;
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
const gun = turret.guns[protagonist.gun.id];
|
const gun = turret.guns[protagonist.gun.id];
|
||||||
const track = tank.tracks[protagonist.track.id];
|
const track = tank.tracks[protagonist.track.id];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith('hull_')) {
|
if (highlightArmor.name.startsWith("hull_")) {
|
||||||
tank.armor.thickness[index] = thickness;
|
tank.armor.thickness[index] = thickness;
|
||||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||||
turret.armor.thickness[index] = thickness;
|
turret.armor.thickness[index] = thickness;
|
||||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||||
if (highlightArmor.name.includes('_armor_')) {
|
if (highlightArmor.name.includes("_armor_")) {
|
||||||
gun.armor.thickness[index] = thickness;
|
gun.armor.thickness[index] = thickness;
|
||||||
} else {
|
} else {
|
||||||
gun.thickness = thickness;
|
gun.thickness = thickness;
|
||||||
}
|
}
|
||||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||||
track.thickness = thickness;
|
track.thickness = thickness;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === "Escape") {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,7 +132,7 @@ export function ArmorPlateDisplay() {
|
|||||||
mt="2"
|
mt="2"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
if (!highlightArmor.editingPlate) return;
|
if (!highlightArmor.editingPlate) return;
|
||||||
|
|
||||||
const index = resolveArmorIndex(highlightArmor.name);
|
const index = resolveArmorIndex(highlightArmor.name);
|
||||||
@@ -145,9 +140,8 @@ export function ArmorPlateDisplay() {
|
|||||||
if (index === undefined) return;
|
if (index === undefined) return;
|
||||||
|
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const initialState =
|
const initialState = Tankopedia.initial;
|
||||||
tankopediaEphemeralStore.getInitialState();
|
const { protagonist } = Duel.state;
|
||||||
const { protagonist } = duelStore.getState();
|
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const initialTank = initialState.model;
|
const initialTank = initialState.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
@@ -159,20 +153,20 @@ export function ArmorPlateDisplay() {
|
|||||||
const initialTrack =
|
const initialTrack =
|
||||||
initialTank.tracks[protagonist.track.id];
|
initialTank.tracks[protagonist.track.id];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith('hull_')) {
|
if (highlightArmor.name.startsWith("hull_")) {
|
||||||
tank.armor.thickness[index] =
|
tank.armor.thickness[index] =
|
||||||
initialTank.armor.thickness[index];
|
initialTank.armor.thickness[index];
|
||||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||||
turret.armor.thickness[index] =
|
turret.armor.thickness[index] =
|
||||||
initialTurret.armor.thickness[index];
|
initialTurret.armor.thickness[index];
|
||||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||||
if (highlightArmor.name.includes('_armor_')) {
|
if (highlightArmor.name.includes("_armor_")) {
|
||||||
gun.armor.thickness[index] =
|
gun.armor.thickness[index] =
|
||||||
initialGun.armor.thickness[index];
|
initialGun.armor.thickness[index];
|
||||||
} else {
|
} else {
|
||||||
gun.thickness = initialGun.thickness;
|
gun.thickness = initialGun.thickness;
|
||||||
}
|
}
|
||||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||||
track.thickness = initialTrack.thickness;
|
track.thickness = initialTrack.thickness;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { GroupProps } from '@react-three/fiber';
|
import { forwardRef, type ComponentProps } from "react";
|
||||||
import { forwardRef } from 'react';
|
import { Group } from "three";
|
||||||
import { Group } from 'three';
|
|
||||||
|
|
||||||
export const ModelTankWrapper = forwardRef<Group, GroupProps>((props, ref) => {
|
export const ModelTankWrapper = forwardRef<Group, ComponentProps<"group">>(
|
||||||
return <group rotation={[-Math.PI / 2, 0, 0]} ref={ref} {...props} />;
|
(props, ref) => {
|
||||||
});
|
return <group rotation={[-Math.PI / 2, 0, 0]} ref={ref} {...props} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from "react";
|
||||||
import { Group } from 'three';
|
import { Group } from "three";
|
||||||
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
|
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
|
||||||
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
|
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
|
||||||
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
|
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
|
||||||
import { useArmor } from '../../../hooks/useArmor';
|
import { useArmor } from "../../../hooks/useArmor";
|
||||||
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
|
||||||
import { useTankTransform } from '../../../hooks/useTankTransform';
|
import { useTankTransform } from "../../../hooks/useTankTransform";
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from "../../../stores/duel";
|
||||||
import { ModelTankWrapper } from './ModelTankWrapper';
|
import { ModelTankWrapper } from "./ModelTankWrapper";
|
||||||
import { PrimaryArmorSceneComponent } from './PrimaryArmorSceneComponent';
|
import { PrimaryArmorSceneComponent } from "./PrimaryArmorSceneComponent";
|
||||||
|
|
||||||
export const PrimaryArmorScene = memo(() => {
|
export const PrimaryArmorScene = memo(() => {
|
||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null!);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null!);
|
||||||
// const protagonist = Duel.use((draft) => draft.protagonist);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
@@ -29,7 +28,7 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73),
|
state.protagonist.consumables.includes(73)
|
||||||
);
|
);
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
@@ -38,20 +37,20 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
<ModelTankWrapper ref={wrapper}>
|
<ModelTankWrapper ref={wrapper}>
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isHull = node.name.startsWith("hull_");
|
||||||
const isVisible = isHull;
|
const isVisible = isHull;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
tankModelDefinition.armor,
|
tankModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isVisible ||
|
!isVisible ||
|
||||||
spaced ||
|
spaced ||
|
||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_01')) ||
|
(isDynamicArmorActive && node.name.includes("state_01")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_00'))
|
(!isDynamicArmorActive && node.name.includes("state_00"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -68,21 +67,21 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
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 isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
turretModelDefinition.armor,
|
turretModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isVisible ||
|
!isVisible ||
|
||||||
spaced ||
|
spaced ||
|
||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_01')) ||
|
(isDynamicArmorActive && node.name.includes("state_01")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_00'))
|
(!isDynamicArmorActive && node.name.includes("state_00"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -102,21 +101,21 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
<group ref={gunContainer}>
|
<group ref={gunContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentGun = node.name.startsWith(
|
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 isVisible = isCurrentGun;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
gunModelDefinition.armor,
|
gunModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isVisible ||
|
!isVisible ||
|
||||||
spaced ||
|
spaced ||
|
||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_01')) ||
|
(isDynamicArmorActive && node.name.includes("state_01")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_00'))
|
(!isDynamicArmorActive && node.name.includes("state_00"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
canSplash,
|
canSplash,
|
||||||
isExplosive,
|
isExplosive,
|
||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { invalidate, useFrame } from '@react-three/fiber';
|
import { invalidate, useFrame } from "@react-three/fiber";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
NormalBlending,
|
NormalBlending,
|
||||||
@@ -14,16 +14,16 @@ import {
|
|||||||
UniformsLib,
|
UniformsLib,
|
||||||
UniformsUtils,
|
UniformsUtils,
|
||||||
Vector2,
|
Vector2,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||||
import { spacedArmorRenderTarget } from './target';
|
import { spacedArmorRenderTarget } from "./target";
|
||||||
|
|
||||||
interface PrimaryArmorSceneComponentProps {
|
interface PrimaryArmorSceneComponentProps {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
@@ -41,9 +41,6 @@ export function PrimaryArmorSceneComponent({
|
|||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
}: PrimaryArmorSceneComponentProps) {
|
}: PrimaryArmorSceneComponentProps) {
|
||||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -80,30 +77,30 @@ export function PrimaryArmorSceneComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(
|
material.uniforms.ricochet.value = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
material.uniforms.normalization.value = degToRad(
|
material.uniforms.normalization.value = degToRad(
|
||||||
shell.normalization ?? 0,
|
shell.normalization ?? 0
|
||||||
);
|
);
|
||||||
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||||
material.uniforms.canSplash.value = canSplash(shell.type);
|
material.uniforms.canSplash.value = canSplash(shell.type);
|
||||||
material.uniforms.damage.value = shell.armor_damage;
|
material.uniforms.damage.value = shell.armor_damage;
|
||||||
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
||||||
|
|
||||||
await handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
await handleAntagonistEquipmentChange(
|
handleAntagonistEquipmentChange(
|
||||||
duel.antagonist.equipmentMatrix,
|
Duel.state.antagonist.equipmentMatrix,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -121,15 +118,14 @@ export function PrimaryArmorSceneComponent({
|
|||||||
function handleWireframeChange(wireframe: boolean) {
|
function handleWireframeChange(wireframe: boolean) {
|
||||||
material.wireframe = wireframe;
|
material.wireframe = wireframe;
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(
|
||||||
equipment: EquipmentMatrix,
|
equipment: EquipmentMatrix,
|
||||||
noInvalidate = false,
|
noInvalidate = false
|
||||||
) {
|
) {
|
||||||
const duel = duelStore.getState();
|
const hasEnhancedArmor = hasEquipment(
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
@@ -137,18 +133,18 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
if (!noInvalidate) invalidate();
|
if (!noInvalidate) invalidate();
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(
|
function handleAntagonistEquipmentChange(
|
||||||
equipment: EquipmentMatrix,
|
equipment: EquipmentMatrix,
|
||||||
noInvalidate = false,
|
noInvalidate = false
|
||||||
) {
|
) {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -158,45 +154,35 @@ export function PrimaryArmorSceneComponent({
|
|||||||
if (!noInvalidate) invalidate();
|
if (!noInvalidate) invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = tankopediaPersistentStore.getState();
|
|
||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleGreenPenetrationChange(initialState.greenPenetration);
|
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
|
||||||
handleAdvancedHighlightingChange(initialState.advancedHighlighting);
|
handleAdvancedHighlightingChange(
|
||||||
handleOpaqueChange(initialState.opaque);
|
TankopediaPersistent.state.advancedHighlighting
|
||||||
handleWireframeChange(initialState.wireframe);
|
|
||||||
handleProtagonistEquipmentChange(
|
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
handleAntagonistEquipmentChange(
|
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
|
||||||
);
|
);
|
||||||
|
handleOpaqueChange(TankopediaPersistent.state.opaque);
|
||||||
|
handleWireframeChange(TankopediaPersistent.state.wireframe);
|
||||||
|
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||||
|
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
TankopediaPersistent.on(
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
tankopediaPersistentStore.subscribe(
|
|
||||||
(state) => state.greenPenetration,
|
(state) => state.greenPenetration,
|
||||||
handleGreenPenetrationChange,
|
handleGreenPenetrationChange
|
||||||
),
|
),
|
||||||
tankopediaPersistentStore.subscribe(
|
TankopediaPersistent.on(
|
||||||
(state) => state.advancedHighlighting,
|
(state) => state.advancedHighlighting,
|
||||||
handleAdvancedHighlightingChange,
|
handleAdvancedHighlightingChange
|
||||||
),
|
),
|
||||||
tankopediaPersistentStore.subscribe(
|
TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange),
|
||||||
(state) => state.opaque,
|
Duel.on(
|
||||||
handleOpaqueChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
(equipment) => handleProtagonistEquipmentChange(equipment),
|
(equipment) => handleProtagonistEquipmentChange(equipment)
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
Duel.on(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
(equipment) => handleAntagonistEquipmentChange(equipment),
|
(equipment) => handleAntagonistEquipmentChange(equipment)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -207,7 +193,7 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
useFrame(({ gl, camera }) => {
|
useFrame(({ gl, camera }) => {
|
||||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||||
gl.getPixelRatio(),
|
gl.getPixelRatio()
|
||||||
);
|
);
|
||||||
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
||||||
material.uniforms.spacedArmorDepth.value =
|
material.uniforms.spacedArmorDepth.value =
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
|
|||||||
|
|
||||||
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
||||||
if (advancedHighlighting && couldHaveRicochet) {
|
if (advancedHighlighting && couldHaveRicochet) {
|
||||||
return vec3(0.0, 1.0, isThreeCalibersRule ? 0.5 : 0.0);
|
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vec3(0.0, 1.0, 0.0);
|
return vec3(0.0, 1.0, 0.0);
|
||||||
@@ -107,7 +107,7 @@ void main() {
|
|||||||
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
||||||
|
|
||||||
if (advancedHighlighting && didRicochet) {
|
if (advancedHighlighting && didRicochet) {
|
||||||
baseColor = vec3(1.0, baseColor.g, 0.5);
|
baseColor = vec3(1.0, baseColor.g, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (greenPenetration || advancedHighlighting) {
|
if (greenPenetration || advancedHighlighting) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { J_HAT, K_HAT } from '@blitzkit/core';
|
import { J_HAT, K_HAT } from "@blitzkit/core";
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from "@react-three/drei";
|
||||||
import { invalidate, useFrame } from '@react-three/fiber';
|
import { invalidate, useFrame } from "@react-three/fiber";
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from "lodash-es";
|
||||||
import { useEffect, useRef, type ComponentProps } from 'react';
|
import { useEffect, useRef, type ComponentProps } from "react";
|
||||||
import {
|
import {
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
DoubleSide,
|
DoubleSide,
|
||||||
@@ -15,23 +15,24 @@ import {
|
|||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
Path,
|
Path,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
|
import { isHalloween } from "../../../core/blitzkit/isHalloween";
|
||||||
|
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
|
||||||
import {
|
import {
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
type ShotLayerNonExternal,
|
type ShotLayerNonExternal,
|
||||||
type ShotStatus,
|
type ShotStatus,
|
||||||
} from '../../../stores/tankopediaEphemeral';
|
} from "../../../stores/tankopedia";
|
||||||
import { ShotDisplayCard } from './ShotDisplayCard';
|
import { ShotDisplayCard } from "./ShotDisplayCard";
|
||||||
|
|
||||||
export const shotStatusColors: Record<
|
export const shotStatusColors: Record<
|
||||||
ShotStatus,
|
ShotStatus,
|
||||||
ComponentProps<typeof Text>['color']
|
ComponentProps<typeof Text>["color"]
|
||||||
> = {
|
> = {
|
||||||
blocked: 'red',
|
blocked: "red",
|
||||||
penetration: 'green',
|
penetration: "green",
|
||||||
ricochet: 'yellow',
|
ricochet: "yellow",
|
||||||
splash: 'orange',
|
splash: "orange",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SURFACE_POINT_SIZE = 12;
|
const SURFACE_POINT_SIZE = 12;
|
||||||
@@ -40,7 +41,7 @@ const TRACER_THICK = 1 / 32;
|
|||||||
const TRACER_THIN = TRACER_THICK / 2;
|
const TRACER_THIN = TRACER_THICK / 2;
|
||||||
|
|
||||||
export function ShotDisplay() {
|
export function ShotDisplay() {
|
||||||
const shot = TankopediaEphemeral.use((state) => state.shot);
|
const shot = Tankopedia.use((state) => state.shot);
|
||||||
const inTracer = useRef<Mesh>(null);
|
const inTracer = useRef<Mesh>(null);
|
||||||
const outTracer = useRef<Mesh>(null);
|
const outTracer = useRef<Mesh>(null);
|
||||||
const splashRadiusWrapper = useRef<Group>(null);
|
const splashRadiusWrapper = useRef<Group>(null);
|
||||||
@@ -51,16 +52,16 @@ export function ShotDisplay() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shot?.splashRadius !== undefined) {
|
if (shot?.splashRadius !== undefined) {
|
||||||
const outlineGeometry = new BufferGeometry().setFromPoints(
|
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({
|
const outlineMaterial = new LineDashedMaterial({
|
||||||
color: 'orange',
|
color: "orange",
|
||||||
dashSize: 0.05,
|
dashSize: 0.05,
|
||||||
gapSize: 0.025,
|
gapSize: 0.025,
|
||||||
});
|
});
|
||||||
const outline = new Line(
|
const outline = new Line(
|
||||||
outlineGeometry,
|
outlineGeometry,
|
||||||
outlineMaterial,
|
outlineMaterial
|
||||||
).computeLineDistances();
|
).computeLineDistances();
|
||||||
|
|
||||||
splashRadiusWrapper.current?.add(outline);
|
splashRadiusWrapper.current?.add(outline);
|
||||||
@@ -72,15 +73,12 @@ export function ShotDisplay() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const data = new Date();
|
|
||||||
const isHalloween = data.getMonth() === 9 && data.getDate() === 31;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isHalloween &&
|
isHalloween() &&
|
||||||
shot?.splashRadius !== undefined &&
|
shot?.splashRadius !== undefined &&
|
||||||
shot.in.status === 'penetration'
|
shot.in.status === "penetration"
|
||||||
) {
|
) {
|
||||||
const audio = new Audio('/assets/audio/lotta-damage.mp3');
|
const audio = new Audio("/assets/audio/lotta-damage.mp3");
|
||||||
|
|
||||||
audio.currentTime = 0.7;
|
audio.currentTime = 0.7;
|
||||||
audio.volume = 0.25;
|
audio.volume = 0.25;
|
||||||
@@ -136,11 +134,11 @@ export function ShotDisplay() {
|
|||||||
const outLength =
|
const outLength =
|
||||||
shot.out && shot.out.layers.length > 0
|
shot.out && shot.out.layers.length > 0
|
||||||
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
|
? (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;
|
: SHOT_DISPLAY_LENGTH_INFINITY;
|
||||||
const inLast = shot.in.layers.findLast(
|
const inLast = shot.in.layers.findLast(
|
||||||
(layer) => layer.type !== null,
|
(layer) => layer.type !== null
|
||||||
) as ShotLayerNonExternal;
|
) as ShotLayerNonExternal;
|
||||||
const outTitleColor = shot.out
|
const outTitleColor = shot.out
|
||||||
? shotStatusColors[shot.out.status]
|
? shotStatusColors[shot.out.status]
|
||||||
@@ -156,7 +154,7 @@ export function ShotDisplay() {
|
|||||||
position={inLast.point}
|
position={inLast.point}
|
||||||
ref={splashRadiusWrapper}
|
ref={splashRadiusWrapper}
|
||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
|
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh>
|
<mesh>
|
||||||
@@ -174,13 +172,13 @@ export function ShotDisplay() {
|
|||||||
<Html position={inLast.point} center>
|
<Html position={inLast.point} center>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
||||||
}}
|
}}
|
||||||
mb={tracerGoingUp ? '0' : '9'}
|
mb={tracerGoingUp ? "0" : "9"}
|
||||||
pb={tracerGoingUp ? '0' : '4'}
|
pb={tracerGoingUp ? "0" : "4"}
|
||||||
mt={tracerGoingUp ? '9' : '0'}
|
mt={tracerGoingUp ? "9" : "0"}
|
||||||
pt={tracerGoingUp ? '4' : '0'}
|
pt={tracerGoingUp ? "4" : "0"}
|
||||||
>
|
>
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<ShotDisplayCard shot={shot} />
|
<ShotDisplayCard shot={shot} />
|
||||||
@@ -192,7 +190,7 @@ export function ShotDisplay() {
|
|||||||
if (layer.type === null) return null;
|
if (layer.type === null) return null;
|
||||||
|
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.in.status === 'splash'
|
shot.in.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
@@ -201,8 +199,8 @@ export function ShotDisplay() {
|
|||||||
position={layer.point}
|
position={layer.point}
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -227,7 +225,7 @@ export function ShotDisplay() {
|
|||||||
<group
|
<group
|
||||||
position={inLast.point}
|
position={inLast.point}
|
||||||
rotation={new Euler().setFromQuaternion(
|
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}>
|
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
|
||||||
@@ -255,8 +253,8 @@ export function ShotDisplay() {
|
|||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(
|
new Quaternion().setFromUnitVectors(
|
||||||
J_HAT,
|
J_HAT,
|
||||||
shot.out.surfaceNormal.clone(),
|
shot.out.surfaceNormal.clone()
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
|
<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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -6,19 +6,19 @@ import {
|
|||||||
Separator,
|
Separator,
|
||||||
Text,
|
Text,
|
||||||
type CardProps,
|
type CardProps,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from "react";
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import type { Shot, ShotLayer } from '../../../stores/tankopediaEphemeral';
|
import type { Shot, ShotLayer } from "../../../stores/tankopedia";
|
||||||
import { shotStatusColors } from './ShotDisplay';
|
import { shotStatusColors } from "./ShotDisplay";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
|
|
||||||
export const layerTypeNames = {
|
export const layerTypeNames = {
|
||||||
[ArmorType.Primary]: 'primary',
|
[ArmorType.Primary]: "primary",
|
||||||
[ArmorType.Spaced]: 'spaced',
|
[ArmorType.Spaced]: "spaced",
|
||||||
[ArmorType.External]: 'external',
|
[ArmorType.External]: "external",
|
||||||
null: 'gap',
|
null: "gap",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function LayerEntry({
|
function LayerEntry({
|
||||||
@@ -29,7 +29,7 @@ function LayerEntry({
|
|||||||
angle,
|
angle,
|
||||||
}: {
|
}: {
|
||||||
layerIndex?: string;
|
layerIndex?: string;
|
||||||
shotStatusColor: ComponentProps<typeof Text>['color'];
|
shotStatusColor: ComponentProps<typeof Text>["color"];
|
||||||
layerName: string;
|
layerName: string;
|
||||||
layer: ShotLayer;
|
layer: ShotLayer;
|
||||||
angle?: number;
|
angle?: number;
|
||||||
@@ -37,44 +37,38 @@ function LayerEntry({
|
|||||||
const { locale, strings } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align="center" gap="2">
|
<tr>
|
||||||
<Text size="1" color="gray" style={{ width: 12 }}>
|
<td>
|
||||||
{layerIndex}
|
<Text size="1" color="gray">
|
||||||
</Text>
|
{layerIndex}
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 48 }}>
|
</Text>
|
||||||
{layerName}
|
</td>
|
||||||
</Text>
|
|
||||||
|
|
||||||
{layer.type === null && (
|
<td>
|
||||||
<>
|
<Text size="2" color={shotStatusColor}>
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
{layerName}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text size="2" color={shotStatusColor}>
|
||||||
{literals(strings.common.units.mm, [
|
{literals(strings.common.units.mm, [
|
||||||
Math.round(layer.distance * 1000).toLocaleString(locale),
|
Math.round(layer.distance * 1000).toLocaleString(locale),
|
||||||
])}
|
])}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text size="2" color="gray">
|
{layer.type === ArmorType.External && (
|
||||||
{literals(
|
<Text size="2" color={shotStatusColor}>
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
{literals(strings.common.units.mm, [
|
||||||
.ricochet_loss,
|
Math.round(layer.thickness).toLocaleString(locale),
|
||||||
[Math.max(-100, -50 * layer.distance).toFixed(0)],
|
])}
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
)}
|
||||||
)}
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) && (
|
||||||
{layer.type === ArmorType.External && (
|
<Text size="2" color={shotStatusColor}>
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
|
||||||
{literals(strings.common.units.mm, [
|
|
||||||
Math.round(layer.thickness).toLocaleString(locale),
|
|
||||||
])}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(layer.type === ArmorType.Primary ||
|
|
||||||
layer.type === ArmorType.Spaced) && (
|
|
||||||
<>
|
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 80 }}>
|
|
||||||
{literals(
|
{literals(
|
||||||
angle === undefined
|
angle === undefined
|
||||||
? strings.common.units.mm
|
? strings.common.units.mm
|
||||||
@@ -83,20 +77,35 @@ function LayerEntry({
|
|||||||
[
|
[
|
||||||
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
||||||
radToDeg(angle ?? 0).toFixed(0),
|
radToDeg(angle ?? 0).toFixed(0),
|
||||||
],
|
]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text size="2" color="gray">
|
||||||
|
{literals(
|
||||||
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||||
|
.ricochet_loss,
|
||||||
|
[Math.max(-100, -50 * layer.distance).toFixed(0)]
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) && (
|
||||||
<Text size="2" color="gray">
|
<Text size="2" color="gray">
|
||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||||
.nominal,
|
.nominal,
|
||||||
[Math.round(layer.thickness).toLocaleString(locale)],
|
[Math.round(layer.thickness).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
)}
|
||||||
)}
|
</td>
|
||||||
</Flex>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +124,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
const inTitleColor = shotStatusColors[shot.in.status];
|
const inTitleColor = shotStatusColors[shot.in.status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" style={{ width: 300 }} {...props}>
|
<Card variant="classic" {...props}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Flex direction="column" gap="1">
|
<Flex direction="column" gap="1">
|
||||||
<Text color={inTitleColor} weight="bold">
|
<Text color={inTitleColor} weight="bold">
|
||||||
@@ -123,11 +132,16 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
||||||
shot.in.status
|
shot.in.status
|
||||||
],
|
],
|
||||||
[Math.round(shot.damage).toLocaleString(locale)],
|
[Math.round(shot.damage).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Flex direction="column">
|
<table
|
||||||
|
style={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
borderSpacing: "var(--space-1) 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{shot.in.layers.map((layer, index) => {
|
{shot.in.layers.map((layer, index) => {
|
||||||
const layerName =
|
const layerName =
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
@@ -141,7 +155,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
? undefined
|
? undefined
|
||||||
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.in.status === 'splash'
|
shot.in.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
@@ -161,7 +175,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Flex>
|
</table>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{shot.out && shot.out.layers.length > 0 && (
|
{shot.out && shot.out.layers.length > 0 && (
|
||||||
@@ -188,7 +202,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
.status[shot.out.status],
|
.status[shot.out.status],
|
||||||
[Math.round(shot.damage).toLocaleString(locale)],
|
[Math.round(shot.damage).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -210,7 +224,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
1
|
1
|
||||||
}.`;
|
}.`;
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.out!.status === 'splash'
|
shot.out!.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from "react";
|
||||||
import { Group, Plane, Scene, Vector3 } from 'three';
|
import { Group, Plane, Scene, Vector3 } from "three";
|
||||||
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
|
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
|
||||||
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
|
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
|
||||||
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
|
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
|
||||||
import { useArmor } from '../../../hooks/useArmor';
|
import { useArmor } from "../../../hooks/useArmor";
|
||||||
import { useModel } from '../../../hooks/useModel';
|
import { useModel } from "../../../hooks/useModel";
|
||||||
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
|
||||||
import { useTankTransform } from '../../../hooks/useTankTransform';
|
import { useTankTransform } from "../../../hooks/useTankTransform";
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from "../../../stores/duel";
|
||||||
import { ModelTankWrapper } from './ModelTankWrapper';
|
import { ModelTankWrapper } from "./ModelTankWrapper";
|
||||||
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
|
import { SpacedArmorSceneComponent } from "./SpacedArmorSceneComponent";
|
||||||
|
|
||||||
export enum ArmorType {
|
export enum ArmorType {
|
||||||
Primary,
|
Primary,
|
||||||
@@ -23,8 +23,8 @@ interface SpacedArmorSceneProps {
|
|||||||
|
|
||||||
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null!);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null!);
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
@@ -45,7 +45,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
? undefined
|
? undefined
|
||||||
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73),
|
state.protagonist.consumables.includes(73)
|
||||||
);
|
);
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
@@ -54,19 +54,19 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
<ModelTankWrapper ref={wrapper}>
|
<ModelTankWrapper ref={wrapper}>
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isHull = node.name.startsWith("hull_");
|
||||||
const isVisible = isHull;
|
const isVisible = isHull;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
tankModelDefinition.armor,
|
tankModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isVisible ||
|
!isVisible ||
|
||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
const isTrack = node.name.startsWith("chassis_track_");
|
||||||
const isVisible = isWheel || isTrack;
|
const isVisible = isWheel || isTrack;
|
||||||
|
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
@@ -104,20 +104,20 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
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 isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
turretModelDefinition.armor,
|
turretModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isVisible ||
|
!isVisible ||
|
||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -139,20 +139,20 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
<group ref={gunContainer}>
|
<group ref={gunContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentGun = node.name.startsWith(
|
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 isVisible = isCurrentGun;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
gunModelDefinition.armor,
|
gunModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isVisible ||
|
!isVisible ||
|
||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
|||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const gunString = `gun_${gunModelDefinition.model_id
|
const gunString = `gun_${gunModelDefinition.model_id
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}`;
|
.padStart(2, "0")}`;
|
||||||
const isCurrentGun = gunModelDefinition.mask
|
const isCurrentGun = gunModelDefinition.mask
|
||||||
? node.name.startsWith(gunString)
|
? node.name.startsWith(gunString)
|
||||||
: node.name === gunString;
|
: node.name === gunString;
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { resolvePenetrationCoefficient } from '@blitzkit/core';
|
import { resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AdditiveBlending,
|
AdditiveBlending,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
Object3D,
|
Object3D,
|
||||||
Plane,
|
Plane,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import type { ArmorUserData, ExternalModuleVariant } from '../..';
|
import type { ArmorUserData, ExternalModuleVariant } from "../..";
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
import { ArmorType } from "../../../SpacedArmorScene";
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||||
|
|
||||||
interface SpacedArmorSubExternalProps {
|
interface SpacedArmorSubExternalProps {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -29,8 +29,6 @@ export function SpacedArmorSubExternal({
|
|||||||
variant,
|
variant,
|
||||||
clip,
|
clip,
|
||||||
}: SpacedArmorSubExternalProps) {
|
}: SpacedArmorSubExternalProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -49,33 +47,30 @@ export function SpacedArmorSubExternal({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
equipment: EquipmentMatrix,
|
const hasEnhancedArmor = hasEquipment(
|
||||||
) {
|
|
||||||
const duel = duelStore.getState();
|
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
: thickness;
|
: thickness;
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -84,26 +79,19 @@ export function SpacedArmorSubExternal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||||
);
|
|
||||||
handleAntagonistEquipmentChange(
|
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
Duel.on(
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
Duel.on(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
handleAntagonistEquipmentChange,
|
handleAntagonistEquipmentChange
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -125,6 +113,7 @@ export function SpacedArmorSubExternal({
|
|||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
{...props}
|
{...props}
|
||||||
|
onClick={() => {}}
|
||||||
key={`${key}-spaced-sub-external-exclude`}
|
key={`${key}-spaced-sub-external-exclude`}
|
||||||
renderOrder={3}
|
renderOrder={3}
|
||||||
material={
|
material={
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core';
|
import { isExplosive, resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AdditiveBlending,
|
AdditiveBlending,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
type Object3D,
|
type Object3D,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import type { ArmorUserData } from '../..';
|
import type { ArmorUserData } from "../..";
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
import { ArmorType } from "../../../SpacedArmorScene";
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||||
|
|
||||||
interface SpacedArmorSubSpacedProps {
|
interface SpacedArmorSubSpacedProps {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -30,8 +30,6 @@ export function SpacedArmorSubSpaced({
|
|||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
}: SpacedArmorSubSpacedProps) {
|
}: SpacedArmorSubSpacedProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -51,41 +49,38 @@ export function SpacedArmorSubSpaced({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(
|
material.uniforms.ricochet.value = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
material.uniforms.normalization.value = degToRad(
|
material.uniforms.normalization.value = degToRad(
|
||||||
shell.normalization ?? 0,
|
shell.normalization ?? 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
equipment: EquipmentMatrix,
|
const hasEnhancedArmor = hasEquipment(
|
||||||
) {
|
|
||||||
const duel = duelStore.getState();
|
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
: thickness;
|
: thickness;
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -94,26 +89,19 @@ export function SpacedArmorSubSpaced({
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||||
);
|
|
||||||
handleAntagonistEquipmentChange(
|
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
Duel.on(
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
Duel.on(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
handleAntagonistEquipmentChange,
|
handleAntagonistEquipmentChange
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
ShellType,
|
ShellType,
|
||||||
isExplosive,
|
isExplosive,
|
||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from "@react-three/fiber";
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
type Intersection,
|
type Intersection,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
@@ -14,21 +14,21 @@ import {
|
|||||||
Raycaster,
|
Raycaster,
|
||||||
Scene,
|
Scene,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||||
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
|
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import {
|
import {
|
||||||
type Shot,
|
type Shot,
|
||||||
type ShotLayerBase,
|
type ShotLayerBase,
|
||||||
type ShotLayerNonExternal,
|
type ShotLayerNonExternal,
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
} from '../../../../stores/tankopediaEphemeral';
|
} from "../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
import { ArmorType } from "../SpacedArmorScene";
|
||||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
import { SpacedArmorSubExternal } from "./components/SpacedArmorSubExternal";
|
||||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
import { SpacedArmorSubSpaced } from "./components/SpacedArmorSubSpaced";
|
||||||
|
|
||||||
type SpacedArmorSceneComponentProps = {
|
type SpacedArmorSceneComponentProps = {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -45,7 +45,7 @@ type SpacedArmorSceneComponentProps = {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ExternalModuleVariant = 'gun' | 'track';
|
export type ExternalModuleVariant = "gun" | "track";
|
||||||
export type ArmorUserData = {
|
export type ArmorUserData = {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
} & (
|
} & (
|
||||||
@@ -76,20 +76,17 @@ export function SpacedArmorSceneComponent({
|
|||||||
clip,
|
clip,
|
||||||
...props
|
...props
|
||||||
}: SpacedArmorSceneComponentProps) {
|
}: SpacedArmorSceneComponentProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
const shoot = useCallback(
|
const shoot = useCallback(
|
||||||
async (
|
(
|
||||||
point: Vector3,
|
point: Vector3,
|
||||||
intersections: Intersection[],
|
intersections: Intersection[],
|
||||||
allowRicochet: boolean,
|
allowRicochet: boolean,
|
||||||
remainingPenetrationInput?: number,
|
remainingPenetrationInput?: number
|
||||||
) => {
|
) => {
|
||||||
const { antagonist, protagonist } = duelStore.getState();
|
const { customShell } = Tankopedia.state;
|
||||||
const { customShell } = tankopediaEphemeralStore.getState();
|
const shell = customShell ?? Duel.state.antagonist.shell;
|
||||||
const shell = customShell ?? antagonist.shell;
|
|
||||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||||
const shot: Shot = {
|
const shot: Shot = {
|
||||||
splashRadius:
|
splashRadius:
|
||||||
@@ -99,19 +96,19 @@ export function SpacedArmorSceneComponent({
|
|||||||
in: {
|
in: {
|
||||||
surfaceNormal: cameraNormal,
|
surfaceNormal: cameraNormal,
|
||||||
layers: [],
|
layers: [],
|
||||||
status: 'blocked',
|
status: "blocked",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
antagonist.equipmentMatrix,
|
Duel.state.antagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
const hasEnhancedArmor = hasEquipment(
|
||||||
110,
|
110,
|
||||||
protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const penetration =
|
const penetration =
|
||||||
shell.penetration.near *
|
shell.penetration.near *
|
||||||
@@ -119,8 +116,8 @@ export function SpacedArmorSceneComponent({
|
|||||||
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
||||||
const filteredIntersections = intersections.filter(
|
const filteredIntersections = intersections.filter(
|
||||||
(intersection) =>
|
(intersection) =>
|
||||||
'type' in intersection.object.userData &&
|
"type" in intersection.object.userData &&
|
||||||
!discardClippingPlane(intersection.object, intersection.point),
|
!discardClippingPlane(intersection.object, intersection.point)
|
||||||
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
||||||
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
||||||
const noDuplicateIntersections: typeof filteredIntersections = [];
|
const noDuplicateIntersections: typeof filteredIntersections = [];
|
||||||
@@ -129,12 +126,12 @@ export function SpacedArmorSceneComponent({
|
|||||||
if (intersection.object.userData.type === ArmorType.External) {
|
if (intersection.object.userData.type === ArmorType.External) {
|
||||||
if (
|
if (
|
||||||
!encounteredExternalModuleVariants.includes(
|
!encounteredExternalModuleVariants.includes(
|
||||||
intersection.object.userData.variant,
|
intersection.object.userData.variant
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
noDuplicateIntersections.push(intersection);
|
noDuplicateIntersections.push(intersection);
|
||||||
encounteredExternalModuleVariants.push(
|
encounteredExternalModuleVariants.push(
|
||||||
intersection.object.userData.variant,
|
intersection.object.userData.variant
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -162,7 +159,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const surfaceNormal = intersection
|
const surfaceNormal = intersection
|
||||||
.normal!.clone()
|
.normal!.clone()
|
||||||
.applyQuaternion(
|
.applyQuaternion(
|
||||||
intersection.object.getWorldQuaternion(new Quaternion()),
|
intersection.object.getWorldQuaternion(new Quaternion())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shell.type === ShellType.HEAT && index > 0) {
|
if (shell.type === ShellType.HEAT && index > 0) {
|
||||||
@@ -174,7 +171,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
shot.in.layers.push({
|
shot.in.layers.push({
|
||||||
type: null,
|
type: null,
|
||||||
distance,
|
distance,
|
||||||
status: blocked ? 'blocked' : 'penetration',
|
status: blocked ? "blocked" : "penetration",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blocked) break;
|
if (blocked) break;
|
||||||
@@ -194,16 +191,16 @@ export function SpacedArmorSceneComponent({
|
|||||||
surfaceNormal,
|
surfaceNormal,
|
||||||
status:
|
status:
|
||||||
shell.type === ShellType.HE
|
shell.type === ShellType.HE
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: 'penetration',
|
: "penetration",
|
||||||
variant: layer.variant,
|
variant: layer.variant,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const thickness = layer.thickness * thicknessCoefficient;
|
const thickness = layer.thickness * thicknessCoefficient;
|
||||||
const ricochet = degToRad(
|
const ricochet = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
|
|
||||||
@@ -227,7 +224,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
thicknessAngled: finalThickness,
|
thicknessAngled: finalThickness,
|
||||||
point: intersection.point,
|
point: intersection.point,
|
||||||
surfaceNormal,
|
surfaceNormal,
|
||||||
status: 'ricochet',
|
status: "ricochet",
|
||||||
angle,
|
angle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,10 +242,10 @@ export function SpacedArmorSceneComponent({
|
|||||||
surfaceNormal: intersection.face!.normal,
|
surfaceNormal: intersection.face!.normal,
|
||||||
status:
|
status:
|
||||||
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
|
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: 'penetration',
|
: "penetration",
|
||||||
angle,
|
angle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -282,10 +279,10 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
return acc + layer.thicknessAngled;
|
return acc + layer.thicknessAngled;
|
||||||
},
|
},
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
||||||
lastLayer.point,
|
lastLayer.point
|
||||||
);
|
);
|
||||||
const finalDamage = Math.max(
|
const finalDamage = Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -294,7 +291,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
|
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
|
||||||
1.1 *
|
1.1 *
|
||||||
(lastLayer.thicknessAngled +
|
(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.
|
* for parity's sake, I am capping the value here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
|
if (shot.in.layers.length > 1 || lastLayer.status === "blocked") {
|
||||||
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
|
shot.in.status = finalDamage > 0 ? "splash" : "blocked";
|
||||||
shot.damage = finalDamage;
|
shot.damage = finalDamage;
|
||||||
} else {
|
} else {
|
||||||
shot.in.status = 'penetration';
|
shot.in.status = "penetration";
|
||||||
shot.damage = shell.armor_damage;
|
shot.damage = shell.armor_damage;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (lastLayer.status === 'blocked') {
|
if (lastLayer.status === "blocked") {
|
||||||
shot.in.status = 'blocked';
|
shot.in.status = "blocked";
|
||||||
shot.damage = 0;
|
shot.damage = 0;
|
||||||
} else if (lastLayer.status === 'penetration') {
|
} else if (lastLayer.status === "penetration") {
|
||||||
shot.in.status = 'penetration';
|
shot.in.status = "penetration";
|
||||||
shot.damage = shell.armor_damage;
|
shot.damage = shell.armor_damage;
|
||||||
} else {
|
} else {
|
||||||
const caster = new Raycaster();
|
const caster = new Raycaster();
|
||||||
@@ -331,29 +328,29 @@ export function SpacedArmorSceneComponent({
|
|||||||
.sub(
|
.sub(
|
||||||
lastLayer.surfaceNormal
|
lastLayer.surfaceNormal
|
||||||
.clone()
|
.clone()
|
||||||
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
|
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal))
|
||||||
);
|
);
|
||||||
|
|
||||||
caster.set(lastLayer.point, ricochetNormal);
|
caster.set(lastLayer.point, ricochetNormal);
|
||||||
|
|
||||||
const ricochetIntersections = caster.intersectObjects(
|
const ricochetIntersections = caster.intersectObjects(
|
||||||
props.scene.children,
|
props.scene.children,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
const outShot = await shoot(
|
const outShot = shoot(
|
||||||
lastLayer.point,
|
lastLayer.point,
|
||||||
ricochetIntersections,
|
ricochetIntersections,
|
||||||
false,
|
false,
|
||||||
remainingPenetration,
|
remainingPenetration
|
||||||
);
|
);
|
||||||
|
|
||||||
shot.in.status = 'ricochet';
|
shot.in.status = "ricochet";
|
||||||
if (outShot === null) {
|
if (outShot === null) {
|
||||||
shot.damage = 0;
|
shot.damage = 0;
|
||||||
shot.out = {
|
shot.out = {
|
||||||
surfaceNormal: ricochetNormal,
|
surfaceNormal: ricochetNormal,
|
||||||
layers: [],
|
layers: [],
|
||||||
status: 'ricochet',
|
status: "ricochet",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
shot.damage = outShot.damage;
|
shot.damage = outShot.damage;
|
||||||
@@ -366,7 +363,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
return shot;
|
return shot;
|
||||||
},
|
},
|
||||||
[camera],
|
[camera]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -386,14 +383,14 @@ export function SpacedArmorSceneComponent({
|
|||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData
|
} satisfies ArmorUserData
|
||||||
}
|
}
|
||||||
onClick={async (event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const shot = (await shoot(
|
|
||||||
event.point,
|
const shot = shoot(event.point, event.intersections, true)!;
|
||||||
event.intersections,
|
|
||||||
true,
|
Tankopedia.mutate((draft) => {
|
||||||
))!;
|
draft.shot = shot;
|
||||||
tankopediaEphemeralStore.setState({ shot });
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { useThree, type ThreeEvent } from '@react-three/fiber';
|
import { useThree, type ThreeEvent } from "@react-three/fiber";
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from "react";
|
||||||
import { Group, Plane, Vector2, Vector3 } from 'three';
|
import { Group, Plane, Vector2, Vector3 } from "three";
|
||||||
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from "../../../core/blitz/applyPitchYawLimits";
|
||||||
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
|
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
|
||||||
import { hasEquipment } from '../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../core/blitzkit/hasEquipment";
|
||||||
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform';
|
import { modelTransformEvent } from "../../../core/blitzkit/modelTransform";
|
||||||
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
|
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
|
||||||
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
|
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
|
||||||
import { useArmor } from '../../../hooks/useArmor';
|
import { useArmor } from "../../../hooks/useArmor";
|
||||||
import { useModel } from '../../../hooks/useModel';
|
import { useModel } from "../../../hooks/useModel";
|
||||||
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
|
||||||
import { useTankTransform } from '../../../hooks/useTankTransform';
|
import { useTankTransform } from "../../../hooks/useTankTransform";
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from "../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||||
import { ModelTankWrapper } from './ModelTankWrapper';
|
import { ModelTankWrapper } from "./ModelTankWrapper";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent';
|
import { StaticArmorSceneComponent } from "./StaticArmorSceneComponent";
|
||||||
|
|
||||||
interface ArmorSceneProps {
|
interface ArmorSceneProps {
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
@@ -28,9 +28,8 @@ export interface ThicknessRange {
|
|||||||
|
|
||||||
export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null!);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null!);
|
||||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
@@ -46,25 +45,23 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const canvas = useThree((state) => state.gl.domElement);
|
const canvas = useThree((state) => state.gl.domElement);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const maskOrigin =
|
const maskOrigin =
|
||||||
gunModelDefinition.mask === undefined
|
gunModelDefinition.mask === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
||||||
const showPrimaryArmor = TankopediaPersistent.use(
|
const showPrimaryArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showPrimaryArmor,
|
(state) => state.showPrimaryArmor
|
||||||
);
|
);
|
||||||
const showSpacedArmor = TankopediaPersistent.use(
|
const showSpacedArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showSpacedArmor,
|
(state) => state.showSpacedArmor
|
||||||
);
|
);
|
||||||
const showExternalModules = TankopediaPersistent.use(
|
const showExternalModules = TankopediaPersistent.use(
|
||||||
(state) => state.showExternalModules,
|
(state) => state.showExternalModules
|
||||||
);
|
);
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73),
|
state.protagonist.consumables.includes(73)
|
||||||
);
|
);
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
@@ -73,12 +70,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<ModelTankWrapper ref={wrapper}>
|
<ModelTankWrapper ref={wrapper}>
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isHull = node.name.startsWith("hull_");
|
||||||
const isVisible = isHull;
|
const isVisible = isHull;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
tankModelDefinition.armor,
|
tankModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -86,8 +83,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -106,8 +103,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
const isTrack = node.name.startsWith("chassis_track_");
|
||||||
const isVisible = isWheel || isTrack;
|
const isVisible = isWheel || isTrack;
|
||||||
|
|
||||||
if (!isVisible || !showExternalModules) return null;
|
if (!isVisible || !showExternalModules) return null;
|
||||||
@@ -129,13 +126,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
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 isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
turretModelDefinition.armor,
|
turretModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -143,8 +140,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -157,27 +154,26 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
@@ -191,16 +187,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -209,7 +205,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<StaticArmorSceneComponent
|
<StaticArmorSceneComponent
|
||||||
name={node.name}
|
name={node.name}
|
||||||
thicknessRange={thicknessRange}
|
thicknessRange={thicknessRange}
|
||||||
key={node.uuid}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -223,13 +218,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<group ref={gunContainer}>
|
<group ref={gunContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentGun = node.name.startsWith(
|
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 isVisible = isCurrentGun;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
gunModelDefinition.armor,
|
gunModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -237,8 +232,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -248,30 +243,29 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
@@ -285,16 +279,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -316,7 +310,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const gunString = `gun_${gunModelDefinition.model_id
|
const gunString = `gun_${gunModelDefinition.model_id
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}`;
|
.padStart(2, "0")}`;
|
||||||
const isCurrentGun = gunModelDefinition.mask
|
const isCurrentGun = gunModelDefinition.mask
|
||||||
? node.name.startsWith(gunString)
|
? node.name.startsWith(gunString)
|
||||||
: node.name === gunString;
|
: node.name === gunString;
|
||||||
@@ -330,29 +324,28 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
@@ -366,17 +359,17 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
import { I_HAT, J_HAT } from "@blitzkit/core";
|
||||||
import { type MeshProps, useThree } from '@react-three/fiber';
|
import { useThree } from "@react-three/fiber";
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from "lodash-es";
|
||||||
import type { QuicklimeEvent } from 'quicklime';
|
import type { QuicklimeEvent } from "quicklime";
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo, type ComponentProps } from "react";
|
||||||
import {
|
import {
|
||||||
Box3,
|
Box3,
|
||||||
Color,
|
Color,
|
||||||
@@ -10,33 +10,33 @@ import {
|
|||||||
FrontSide,
|
FrontSide,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshStandardMaterial,
|
MeshBasicMaterial,
|
||||||
Object3D,
|
Object3D,
|
||||||
Plane,
|
Plane,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
|
import { unrotateDavaVector } from "../../../core/blitz/unrotateDavaVector";
|
||||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
||||||
import {
|
import {
|
||||||
type ModelTransformEventData,
|
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
} from '../../../core/blitzkit/modelTransform';
|
type ModelTransformEventData,
|
||||||
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
|
} from "../../../core/blitzkit/modelTransform";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
import type {
|
import type {
|
||||||
ArmorUserData,
|
ArmorUserData,
|
||||||
ExternalModuleVariant,
|
ExternalModuleVariant,
|
||||||
} from './SpacedArmorSceneComponent';
|
} from "./SpacedArmorSceneComponent";
|
||||||
import type { ThicknessRange } from './StaticArmor';
|
import type { ThicknessRange } from "./StaticArmor";
|
||||||
|
|
||||||
type StaticArmorSceneComponentProps = {
|
type StaticArmorSceneComponentProps = {
|
||||||
name: string;
|
name: string;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
onPointerDown?: MeshProps['onPointerDown'];
|
onPointerDown?: ComponentProps<"mesh">["onPointerDown"];
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
@@ -69,8 +69,6 @@ export function StaticArmorSceneComponent({
|
|||||||
onPointerDown,
|
onPointerDown,
|
||||||
...props
|
...props
|
||||||
}: StaticArmorSceneComponentProps) {
|
}: StaticArmorSceneComponentProps) {
|
||||||
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const x = thickness / thicknessRange.value;
|
const x = thickness / thicknessRange.value;
|
||||||
const xClamped = clamp(x, 0, 1);
|
const xClamped = clamp(x, 0, 1);
|
||||||
@@ -92,7 +90,7 @@ export function StaticArmorSceneComponent({
|
|||||||
color = new Color(
|
color = new Color(
|
||||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||||
0,
|
0,
|
||||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
clamp(1 - (1 / 8) * xClamped, 0, 1)
|
||||||
);
|
);
|
||||||
opacity = clamp(x + 1 / 2, 0, 1);
|
opacity = clamp(x + 1 / 2, 0, 1);
|
||||||
break;
|
break;
|
||||||
@@ -108,14 +106,14 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
const surfaceMaterial = useMemo(
|
const surfaceMaterial = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new MeshStandardMaterial({
|
new MeshBasicMaterial({
|
||||||
color,
|
color,
|
||||||
opacity,
|
opacity,
|
||||||
transparent: opacity < 1,
|
transparent: opacity < 1,
|
||||||
depthWrite,
|
depthWrite,
|
||||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||||
}),
|
}),
|
||||||
[thickness],
|
[thickness]
|
||||||
);
|
);
|
||||||
const outlineMaterial = useMemo(
|
const outlineMaterial = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -124,7 +122,7 @@ export function StaticArmorSceneComponent({
|
|||||||
.clone()
|
.clone()
|
||||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||||
}),
|
}),
|
||||||
[thickness],
|
[thickness]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -139,11 +137,11 @@ export function StaticArmorSceneComponent({
|
|||||||
outlineMaterial.visible = true;
|
outlineMaterial.visible = true;
|
||||||
} else if (
|
} else if (
|
||||||
selectedName === name ||
|
selectedName === name ||
|
||||||
(name.startsWith('chassis_') && selectedName.startsWith('chassis_')) ||
|
(name.startsWith("chassis_") && selectedName.startsWith("chassis_")) ||
|
||||||
(name.startsWith('gun_') &&
|
(name.startsWith("gun_") &&
|
||||||
selectedName.startsWith('gun_') &&
|
selectedName.startsWith("gun_") &&
|
||||||
!name.includes('_armor_') &&
|
!name.includes("_armor_") &&
|
||||||
!selectedName.includes('_armor_'))
|
!selectedName.includes("_armor_"))
|
||||||
) {
|
) {
|
||||||
// this selected, stand out!
|
// this selected, stand out!
|
||||||
surfaceMaterial.opacity = 1;
|
surfaceMaterial.opacity = 1;
|
||||||
@@ -166,9 +164,9 @@ export function StaticArmorSceneComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on(
|
||||||
(state) => state.highlightArmor?.name,
|
(state) => state.highlightArmor?.name,
|
||||||
handleHighlightArmor,
|
handleHighlightArmor
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -181,14 +179,14 @@ export function StaticArmorSceneComponent({
|
|||||||
const { clip } = props;
|
const { clip } = props;
|
||||||
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
||||||
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
||||||
unrotateDavaVector(props.turretOrigin.clone()),
|
unrotateDavaVector(props.turretOrigin.clone())
|
||||||
);
|
);
|
||||||
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
||||||
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
||||||
const point = new Vector3();
|
const point = new Vector3();
|
||||||
|
|
||||||
function handleModelTransform(
|
function handleModelTransform(
|
||||||
event: QuicklimeEvent<ModelTransformEventData>,
|
event: QuicklimeEvent<ModelTransformEventData>
|
||||||
) {
|
) {
|
||||||
clip.normal
|
clip.normal
|
||||||
.set(0, 0, -1)
|
.set(0, 0, -1)
|
||||||
@@ -229,7 +227,7 @@ export function StaticArmorSceneComponent({
|
|||||||
{
|
{
|
||||||
type: props.type,
|
type: props.type,
|
||||||
variant:
|
variant:
|
||||||
props.type === ArmorType.External ? props.variant : 'gun',
|
props.type === ArmorType.External ? props.variant : "gun",
|
||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData
|
} satisfies ArmorUserData
|
||||||
}
|
}
|
||||||
@@ -238,7 +236,7 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
const { editStatic } = Tankopedia.state;
|
||||||
|
|
||||||
const bounds = new Box3().setFromObject(event.object);
|
const bounds = new Box3().setFromObject(event.object);
|
||||||
const point = bounds.min
|
const point = bounds.min
|
||||||
@@ -253,13 +251,13 @@ export function StaticArmorSceneComponent({
|
|||||||
const surfaceNormal = event
|
const surfaceNormal = event
|
||||||
.normal!.clone()
|
.normal!.clone()
|
||||||
.applyQuaternion(
|
.applyQuaternion(
|
||||||
event.object.getWorldQuaternion(new Quaternion()),
|
event.object.getWorldQuaternion(new Quaternion())
|
||||||
);
|
);
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
const thicknessAngled =
|
const thicknessAngled =
|
||||||
thickness / Math.sin(Math.PI / 2 - angle);
|
thickness / Math.sin(Math.PI / 2 - angle);
|
||||||
|
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = {
|
draft.highlightArmor = {
|
||||||
type: props.type,
|
type: props.type,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Theme, type ThemeProps } from '@radix-ui/themes';
|
import { Theme, type ThemeProps } from "@radix-ui/themes";
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
interface BlitzKitThemeProps extends ThemeProps {
|
interface BlitzKitThemeProps extends ThemeProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -13,13 +13,13 @@ export function BlitzKitTheme({
|
|||||||
}: BlitzKitThemeProps) {
|
}: BlitzKitThemeProps) {
|
||||||
return (
|
return (
|
||||||
<Theme
|
<Theme
|
||||||
style={{ minHeight: 'unset', display: 'contents', ...style }}
|
style={{ minHeight: "unset", display: "contents", ...style }}
|
||||||
className={`blitzkit-theme ${className}`}
|
className={`blitzkit-theme ${className}`}
|
||||||
grayColor="mauve"
|
grayColor="mauve"
|
||||||
appearance="dark"
|
appearance="dark"
|
||||||
panelBackground="solid"
|
panelBackground="solid"
|
||||||
radius="full"
|
radius="full"
|
||||||
accentColor="amber"
|
accentColor="indigo"
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { assertSecret, idToRegion } from '@blitzkit/core';
|
import { assertSecret, idToRegion } from "@blitzkit/core";
|
||||||
import { Button, Flex, Heading, Text } from '@radix-ui/themes';
|
import { Button, Flex, Heading, Text } from "@radix-ui/themes";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
useLocale,
|
useLocale,
|
||||||
type LocaleAcceptorProps,
|
type LocaleAcceptorProps,
|
||||||
} from '../../hooks/useLocale';
|
} from "../../hooks/useLocale";
|
||||||
import { App } from '../../stores/app';
|
import { App } from "../../stores/app";
|
||||||
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../../stores/app/constants';
|
import { CURRENT_POLICIES_AGREEMENT_INDEX } from "../../stores/app/constants";
|
||||||
import { LinkI18n } from '../LinkI18n';
|
import { LinkI18n } from "../LinkI18n";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
type Extension =
|
type Extension =
|
||||||
| {
|
| {
|
||||||
status: 'ok';
|
status: "ok";
|
||||||
data: {
|
data: {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
account_id: number;
|
account_id: number;
|
||||||
@@ -21,15 +21,13 @@ type Extension =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
status: 'error';
|
status: "error";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Checks({ locale }: LocaleAcceptorProps) {
|
export function Checks({ locale }: LocaleAcceptorProps) {
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<App.Provider>
|
<Content />
|
||||||
<Content />
|
|
||||||
</App.Provider>
|
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,19 +38,17 @@ export function Checks({ locale }: LocaleAcceptorProps) {
|
|||||||
function Content() {
|
function Content() {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const logins = App.use((state) => state.logins);
|
const logins = App.use((state) => state.logins);
|
||||||
const mutateApp = App.useMutation();
|
|
||||||
const appStore = App.useStore();
|
|
||||||
const policiesAgreementIndex = App.use(
|
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);
|
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function unsubscribe() {
|
function unsubscribe() {
|
||||||
window.removeEventListener('scroll', show);
|
window.removeEventListener("scroll", show);
|
||||||
window.removeEventListener('pointermove', show);
|
window.removeEventListener("pointermove", show);
|
||||||
window.removeEventListener('pointerdown', show);
|
window.removeEventListener("pointerdown", show);
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
@@ -64,9 +60,9 @@ function Content() {
|
|||||||
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
|
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
|
||||||
!isLegal
|
!isLegal
|
||||||
) {
|
) {
|
||||||
window.addEventListener('scroll', show);
|
window.addEventListener("scroll", show);
|
||||||
window.addEventListener('pointermove', show);
|
window.addEventListener("pointermove", show);
|
||||||
window.addEventListener('pointerdown', show);
|
window.addEventListener("pointerdown", show);
|
||||||
}
|
}
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
@@ -78,25 +74,25 @@ function Content() {
|
|||||||
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||||
|
|
||||||
if (expiresInDays < 0) {
|
if (expiresInDays < 0) {
|
||||||
mutateApp((draft) => {
|
App.mutate((draft) => {
|
||||||
draft.logins.wargaming = undefined;
|
draft.logins.wargaming = undefined;
|
||||||
});
|
});
|
||||||
} else if (expiresInDays < 7) {
|
} else if (expiresInDays < 7) {
|
||||||
const { wargaming } = appStore.getState().logins;
|
const { wargaming } = App.state.logins;
|
||||||
|
|
||||||
if (!wargaming) return;
|
if (!wargaming) return;
|
||||||
|
|
||||||
fetch(
|
fetch(
|
||||||
`https://api.worldoftanks.${idToRegion(
|
`https://api.worldoftanks.${idToRegion(
|
||||||
wargaming.id,
|
wargaming.id
|
||||||
)}/wot/auth/prolongate/?application_id=${assertSecret(
|
)}/wot/auth/prolongate/?application_id=${assertSecret(
|
||||||
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID,
|
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
|
||||||
)}&access_token=${wargaming.token}`,
|
)}&access_token=${wargaming.token}`
|
||||||
)
|
)
|
||||||
.then((response) => response.json() as Promise<Extension>)
|
.then((response) => response.json() as Promise<Extension>)
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
mutateApp((draft) => {
|
App.mutate((draft) => {
|
||||||
if (json.status === 'error') {
|
if (json.status === "error") {
|
||||||
draft.logins.wargaming = undefined;
|
draft.logins.wargaming = undefined;
|
||||||
} else {
|
} else {
|
||||||
draft.logins.wargaming = {
|
draft.logins.wargaming = {
|
||||||
@@ -126,11 +122,11 @@ function Content() {
|
|||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
This website utilizes cookies to perform analytics and personalize
|
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">
|
<LinkI18n locale={locale} href="/docs/legal/privacy-policy">
|
||||||
our privacy policy
|
our privacy policy
|
||||||
</LinkI18n>
|
</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">
|
<LinkI18n locale={locale} href="/docs/legal/terms-of-service">
|
||||||
terms of service
|
terms of service
|
||||||
</LinkI18n>
|
</LinkI18n>
|
||||||
@@ -141,8 +137,8 @@ function Content() {
|
|||||||
<Button
|
<Button
|
||||||
mt="2"
|
mt="2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
appStore.setState({
|
App.mutate((draft) => {
|
||||||
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
|
draft.policiesAgreementIndex = CURRENT_POLICIES_AGREEMENT_INDEX;
|
||||||
});
|
});
|
||||||
setShowPoliciesAgreement(false);
|
setShowPoliciesAgreement(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { asset } from '@blitzkit/core';
|
import { asset } from "@blitzkit/core";
|
||||||
import { Flex, Heading, IconButton, Table } from '@radix-ui/themes';
|
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
||||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { CompareRow } from './CompareRow';
|
import { CompareRow } from "./CompareRow";
|
||||||
import { CompareSectionTitle } from './CompareSectionTitle';
|
import { CompareSectionTitle } from "./CompareSectionTitle";
|
||||||
|
|
||||||
interface BodyProps {
|
interface BodyProps {
|
||||||
stats: TankCharacteristics[];
|
stats: TankCharacteristics[];
|
||||||
@@ -14,10 +14,9 @@ interface BodyProps {
|
|||||||
export function Body({ stats }: BodyProps) {
|
export function Body({ stats }: BodyProps) {
|
||||||
const members = CompareEphemeral.use((state) => state.members);
|
const members = CompareEphemeral.use((state) => state.members);
|
||||||
const hasNonRegularGun = members.some(
|
const hasNonRegularGun = members.some(
|
||||||
({ gun }) => gun.gun_type!.$case !== 'regular',
|
({ gun }) => gun.gun_type!.$case !== "regular"
|
||||||
);
|
);
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -35,7 +34,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<Flex>
|
<Flex>
|
||||||
{gun.shells.map((thisShell, shellIndex) => (
|
{gun.shells.map((thisShell, shellIndex) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
color={thisShell.id === shell.id ? undefined : "gray"}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
key={thisShell.id}
|
key={thisShell.id}
|
||||||
style={{
|
style={{
|
||||||
@@ -49,7 +48,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].shell = thisShell;
|
draft.members[index].shell = thisShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -88,7 +87,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
}
|
}
|
||||||
display={(stats) =>
|
display={(stats) =>
|
||||||
stats.shellReload?.toFixed(2) ??
|
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)}
|
deltaNominalDisplay={(delta) => delta.toFixed(2)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import {
|
|||||||
CaretLeftIcon,
|
CaretLeftIcon,
|
||||||
CaretRightIcon,
|
CaretRightIcon,
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
import { Flex, IconButton, Table, Text } from '@radix-ui/themes';
|
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from "../../core/radix/var";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { ComparePersistent } from '../../stores/comparePersistent';
|
import { ComparePersistent } from "../../stores/comparePersistent";
|
||||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||||
|
|
||||||
type CompareRowProps = {
|
type CompareRowProps = {
|
||||||
display?: (
|
display?: (
|
||||||
member: Awaited<TankCharacteristics>,
|
member: Awaited<TankCharacteristics>
|
||||||
) => number | string | undefined;
|
) => number | string | undefined;
|
||||||
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
|
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
deltaNominalDisplay?: (delta: number) => string | number;
|
deltaNominalDisplay?: (delta: number) => string | number;
|
||||||
indent?: boolean;
|
indent?: boolean;
|
||||||
@@ -33,7 +33,7 @@ type CompareRowProps = {
|
|||||||
|
|
||||||
export function CompareRow({
|
export function CompareRow({
|
||||||
display,
|
display,
|
||||||
deltaType = 'higherIsBetter',
|
deltaType = "higherIsBetter",
|
||||||
decimals,
|
decimals,
|
||||||
deltaNominalDisplay,
|
deltaNominalDisplay,
|
||||||
indent,
|
indent,
|
||||||
@@ -41,17 +41,16 @@ export function CompareRow({
|
|||||||
...props
|
...props
|
||||||
}: CompareRowProps) {
|
}: CompareRowProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const sorting = CompareEphemeral.use((state) => state.sorting);
|
const sorting = CompareEphemeral.use((state) => state.sorting);
|
||||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||||
const values = stats.map((stat) =>
|
const values = stats.map((stat) =>
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(stat)!
|
? props.value(stat)!
|
||||||
: (stat[props.value] as number),
|
: (stat[props.value] as number)
|
||||||
);
|
);
|
||||||
const id = useRef(Math.random());
|
const id = useRef(Math.random());
|
||||||
const name =
|
const name =
|
||||||
typeof props.value === 'string'
|
typeof props.value === "string"
|
||||||
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
||||||
: props.name;
|
: props.name;
|
||||||
|
|
||||||
@@ -60,21 +59,21 @@ export function CompareRow({
|
|||||||
<StickyRowHeaderCell>
|
<StickyRowHeaderCell>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
style={{ whiteSpace: 'nowrap' }}
|
style={{ whiteSpace: "nowrap" }}
|
||||||
gap={indent ? { initial: '4', sm: '6' } : { initial: '1', sm: '2' }}
|
gap={indent ? { initial: "4", sm: "6" } : { initial: "1", sm: "2" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={sorting?.by === id.current ? undefined : 'gray'}
|
color={sorting?.by === id.current ? undefined : "gray"}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.sort((memberA, memberB) => {
|
draft.members.sort((memberA, memberB) => {
|
||||||
const indexA = draft.members.indexOf(memberA);
|
const indexA = draft.members.indexOf(memberA);
|
||||||
const indexB = draft.members.indexOf(memberB);
|
const indexB = draft.members.indexOf(memberB);
|
||||||
const valueA = values[indexA];
|
const valueA = values[indexA];
|
||||||
const valueB = values[indexB];
|
const valueB = values[indexB];
|
||||||
|
|
||||||
return draft.sorting?.direction === 'ascending' &&
|
return draft.sorting?.direction === "ascending" &&
|
||||||
draft.sorting.by === id.current
|
draft.sorting.by === id.current
|
||||||
? valueA - valueB
|
? valueA - valueB
|
||||||
: valueB - valueA;
|
: valueB - valueA;
|
||||||
@@ -83,26 +82,26 @@ export function CompareRow({
|
|||||||
draft.sorting = {
|
draft.sorting = {
|
||||||
by: id.current,
|
by: id.current,
|
||||||
direction:
|
direction:
|
||||||
draft.sorting?.direction === 'ascending' &&
|
draft.sorting?.direction === "ascending" &&
|
||||||
draft.sorting.by === id.current
|
draft.sorting.by === id.current
|
||||||
? 'descending'
|
? "descending"
|
||||||
: 'ascending',
|
: "ascending",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(sorting === undefined || sorting.by !== id.current) && (
|
{(sorting === undefined || sorting.by !== id.current) && (
|
||||||
<CaretSortIcon style={{ transform: 'rotate(90deg)' }} />
|
<CaretSortIcon style={{ transform: "rotate(90deg)" }} />
|
||||||
)}
|
)}
|
||||||
{sorting?.by === id.current && (
|
{sorting?.by === id.current && (
|
||||||
<>
|
<>
|
||||||
{sorting.direction === 'ascending' && <CaretRightIcon />}
|
{sorting.direction === "ascending" && <CaretRightIcon />}
|
||||||
{sorting.direction === 'descending' && <CaretLeftIcon />}
|
{sorting.direction === "descending" && <CaretLeftIcon />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Text size={{ initial: '1', sm: '2' }}>{name}</Text>
|
<Text size={{ initial: "1", sm: "2" }}>{name}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</StickyRowHeaderCell>
|
</StickyRowHeaderCell>
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ export function CompareRow({
|
|||||||
const delta = value - values[0];
|
const delta = value - values[0];
|
||||||
const deltaPercentage = value / values[0] - 1;
|
const deltaPercentage = value / values[0] - 1;
|
||||||
const normalizedDeltaPercentage = Math.round(
|
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
|
const resolvedDisplayValue = display
|
||||||
? display(stats[index])
|
? display(stats[index])
|
||||||
@@ -129,12 +128,12 @@ export function CompareRow({
|
|||||||
value === values[0]
|
value === values[0]
|
||||||
? undefined
|
? undefined
|
||||||
: (
|
: (
|
||||||
deltaType === 'higherIsBetter'
|
deltaType === "higherIsBetter"
|
||||||
? value > values[0]
|
? value > values[0]
|
||||||
: value < values[0]
|
: value < values[0]
|
||||||
)
|
)
|
||||||
? `color-mix(in srgb, ${Var('green-7')} ${normalizedDeltaPercentage}%, ${Var('green-3')})`
|
? `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("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -142,11 +141,11 @@ export function CompareRow({
|
|||||||
justify="center"
|
justify="center"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ textAlign: 'center' }} wrap="nowrap">
|
<Text style={{ textAlign: "center" }} wrap="nowrap">
|
||||||
{resolvedDisplayValue}
|
{resolvedDisplayValue}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -154,10 +153,10 @@ export function CompareRow({
|
|||||||
resolvedDisplayValue !== undefined &&
|
resolvedDisplayValue !== undefined &&
|
||||||
values[0] !== undefined && (
|
values[0] !== undefined && (
|
||||||
<>
|
<>
|
||||||
{deltaMode === 'absolute' && (
|
{deltaMode === "absolute" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
(
|
(
|
||||||
{`${delta > 0 ? '+' : ''}${
|
{`${delta > 0 ? "+" : ""}${
|
||||||
deltaNominalDisplay
|
deltaNominalDisplay
|
||||||
? deltaNominalDisplay(delta)
|
? deltaNominalDisplay(delta)
|
||||||
: decimals === undefined
|
: decimals === undefined
|
||||||
@@ -168,11 +167,11 @@ export function CompareRow({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{deltaMode === 'percentage' && (
|
{deltaMode === "percentage" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
(
|
(
|
||||||
{`${deltaPercentage > 0 ? '+' : ''}${Math.round(
|
{`${deltaPercentage > 0 ? "+" : ""}${Math.round(
|
||||||
deltaPercentage * 100,
|
deltaPercentage * 100
|
||||||
)}%`}
|
)}%`}
|
||||||
)
|
)
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { asset, availableProvisions, permanentSkills } from '@blitzkit/core';
|
import { asset, availableProvisions, permanentSkills } from "@blitzkit/core";
|
||||||
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity';
|
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
||||||
import { ComponentPlaceholderIcon } from '@radix-ui/react-icons';
|
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Flex, IconButton, Popover, Table } from '@radix-ui/themes';
|
import { Button, Flex, IconButton, Popover, Table } from "@radix-ui/themes";
|
||||||
import { times } from 'lodash-es';
|
import { times } from "lodash-es";
|
||||||
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
|
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
|
||||||
import { awaitableEquipmentDefinitions } from '../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableSkillDefinitions } from '../../core/awaitables/skillDefinitions';
|
import { awaitableSkillDefinitions } from "../../core/awaitables/skillDefinitions";
|
||||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from "../../core/radix/var";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import type { EquipmentMatrix } from '../../stores/duel';
|
import type { EquipmentMatrix } from "../../stores/duel";
|
||||||
import { BlitzkitButtonGrayIcon } from '../BlitzkitButtonGrayIcon';
|
import { BlitzkitButtonGrayIcon } from "../BlitzkitButtonGrayIcon";
|
||||||
import { ConsumablesManager } from '../ConsumablesManager';
|
import { ConsumablesManager } from "../ConsumablesManager";
|
||||||
import { CrewSkillManager } from '../CrewSkillManager';
|
import { CrewSkillManager } from "../CrewSkillManager";
|
||||||
import { EquipmentManager } from '../EquipmentManager';
|
import { EquipmentManager } from "../EquipmentManager";
|
||||||
import { ModuleManager } from '../ModuleManager';
|
import { ModuleManager } from "../ModuleManager";
|
||||||
import { ProvisionsManager } from '../ProvisionsManager';
|
import { ProvisionsManager } from "../ProvisionsManager";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { StickyTableRoot } from '../StickyTableRoot';
|
import { StickyTableRoot } from "../StickyTableRoot";
|
||||||
import { Body } from './Body';
|
import { Body } from "./Body";
|
||||||
import { InsertionMarker } from './IntersectionMarker';
|
import { InsertionMarker } from "./IntersectionMarker";
|
||||||
import { TankCard } from './TankCard';
|
import { TankCard } from "./TankCard";
|
||||||
|
|
||||||
interface CompareTableProps {
|
interface CompareTableProps {
|
||||||
stats: TankCharacteristics[];
|
stats: TankCharacteristics[];
|
||||||
@@ -43,7 +43,6 @@ const [
|
|||||||
export function CompareTable({ stats }: CompareTableProps) {
|
export function CompareTable({ stats }: CompareTableProps) {
|
||||||
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
||||||
const members = CompareEphemeral.use((state) => state.members);
|
const members = CompareEphemeral.use((state) => state.members);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,20 +50,20 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
variant="surface"
|
variant="surface"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
maxHeight: '100%',
|
maxHeight: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
// height: 'calc(100vh - 128px)'
|
// height: 'calc(100vh - 128px)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<StickyColumnHeaderCell width="0" style={{ height: '100%' }}>
|
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -82,11 +81,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
|
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell style={{ height: '100%' }}>
|
<Table.Cell style={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -107,15 +106,15 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
crewSkills[skill] === 0
|
crewSkills[skill] === 0
|
||||||
? Var('gray-11')
|
? Var("gray-11")
|
||||||
: permanentSkills.includes(skill)
|
: permanentSkills.includes(skill)
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('amber-11'),
|
: Var("amber-11"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -125,7 +124,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<CrewSkillManager
|
<CrewSkillManager
|
||||||
skillLevels={crewSkills}
|
skillLevels={crewSkills}
|
||||||
onChange={(skills) => {
|
onChange={(skills) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.crewSkills = skills;
|
draft.crewSkills = skills;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -135,7 +134,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||||
draft.crewSkills[skill] = 0;
|
draft.crewSkills[skill] = 0;
|
||||||
});
|
});
|
||||||
@@ -147,7 +146,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||||
draft.crewSkills[skill] = 7;
|
draft.crewSkills[skill] = 7;
|
||||||
});
|
});
|
||||||
@@ -177,19 +176,19 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
consumables,
|
consumables,
|
||||||
camouflage,
|
camouflage,
|
||||||
},
|
},
|
||||||
index,
|
index
|
||||||
) => {
|
) => {
|
||||||
const equipmentPreset =
|
const equipmentPreset =
|
||||||
equipmentDefinitions.presets[tank.equipment_preset];
|
equipmentDefinitions.presets[tank.equipment_preset];
|
||||||
const provisionsList = availableProvisions(
|
const provisionsList = availableProvisions(
|
||||||
tank,
|
tank,
|
||||||
gun,
|
gun,
|
||||||
provisionDefinitions,
|
provisionDefinitions
|
||||||
);
|
);
|
||||||
const consumablesList = Object.values(
|
const consumablesList = Object.values(
|
||||||
consumableDefinitions.consumables,
|
consumableDefinitions.consumables
|
||||||
).filter((consumable) =>
|
).filter((consumable) =>
|
||||||
checkConsumableProvisionInclusivity(consumable, tank, gun),
|
checkConsumableProvisionInclusivity(consumable, tank, gun)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -205,7 +204,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
{(
|
{(
|
||||||
['turret', 'gun', 'engine', 'chassis'] as const
|
["turret", "gun", "engine", "chassis"] as const
|
||||||
).map((module, index) => (
|
).map((module, index) => (
|
||||||
<img
|
<img
|
||||||
key={module}
|
key={module}
|
||||||
@@ -215,7 +214,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
src={asset(`icons/modules/${module}.webp`)}
|
src={asset(`icons/modules/${module}.webp`)}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: index > 0 ? -8 : 0,
|
marginLeft: index > 0 ? -8 : 0,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -232,7 +231,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
engine={engine}
|
engine={engine}
|
||||||
track={track}
|
track={track}
|
||||||
onChange={(modules) => {
|
onChange={(modules) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index] = {
|
draft.members[index] = {
|
||||||
...draft.members[index],
|
...draft.members[index],
|
||||||
...modules,
|
...modules,
|
||||||
@@ -245,7 +244,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const member = draft.members[index];
|
const member = draft.members[index];
|
||||||
|
|
||||||
member.turret = member.tank.turrets[0];
|
member.turret = member.tank.turrets[0];
|
||||||
@@ -261,7 +260,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const member = draft.members[index];
|
const member = draft.members[index];
|
||||||
|
|
||||||
member.turret = member.tank.turrets.at(-1)!;
|
member.turret = member.tank.turrets.at(-1)!;
|
||||||
@@ -288,9 +287,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
radius="large"
|
radius="large"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: "100%",
|
||||||
width: 12,
|
width: 12,
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" justify="center">
|
<Flex direction="column" justify="center">
|
||||||
@@ -298,14 +297,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<img
|
<img
|
||||||
alt={unwrap(
|
alt={unwrap(
|
||||||
provisionDefinitions.provisions[provision]
|
provisionDefinitions.provisions[provision]
|
||||||
.name,
|
.name
|
||||||
)}
|
)}
|
||||||
key={provision}
|
key={provision}
|
||||||
src={asset(
|
src={asset(
|
||||||
`/icons/provisions/${provision}.webp`,
|
`/icons/provisions/${provision}.webp`
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: '50%',
|
left: "50%",
|
||||||
/**
|
/**
|
||||||
* max = 100% - 16px - 4px
|
* max = 100% - 16px - 4px
|
||||||
* min = 4px
|
* min = 4px
|
||||||
@@ -320,11 +319,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
? 0.5
|
? 0.5
|
||||||
: index / (provisions.length - 1)
|
: index / (provisions.length - 1)
|
||||||
} * (100% - 24px))`,
|
} * (100% - 24px))`,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -339,12 +338,12 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Popover.Content>
|
<Popover.Content>
|
||||||
<ProvisionsManager
|
<ProvisionsManager
|
||||||
provisions={provisionsList.map(
|
provisions={provisionsList.map(
|
||||||
(provision) => provision.id,
|
(provision) => provision.id
|
||||||
)}
|
)}
|
||||||
selected={provisions}
|
selected={provisions}
|
||||||
disabled={tank.max_provisions === provisions.length}
|
disabled={tank.max_provisions === provisions.length}
|
||||||
onChange={(provisions) => {
|
onChange={(provisions) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].provisions = provisions;
|
draft.members[index].provisions = provisions;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -353,7 +352,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].provisions = [];
|
draft.members[index].provisions = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -394,8 +393,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
equipment === -1
|
equipment === -1
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('gray-11'),
|
: Var("gray-11"),
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -405,8 +404,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
equipment === 1
|
equipment === 1
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('gray-11'),
|
: Var("gray-11"),
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -424,7 +423,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
matrix={equipmentMatrix}
|
matrix={equipmentMatrix}
|
||||||
preset={equipmentPreset}
|
preset={equipmentPreset}
|
||||||
onChange={(matrix) => {
|
onChange={(matrix) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].equipmentMatrix = matrix;
|
draft.members[index].equipmentMatrix = matrix;
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -436,10 +435,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].equipmentMatrix = times(
|
draft.members[index].equipmentMatrix = times(
|
||||||
3,
|
3,
|
||||||
() => times(3, () => 0),
|
() => times(3, () => 0)
|
||||||
) as EquipmentMatrix;
|
) as EquipmentMatrix;
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -453,7 +452,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.forEach((member) => {
|
draft.members.forEach((member) => {
|
||||||
member.equipmentMatrix =
|
member.equipmentMatrix =
|
||||||
draft.members[index].equipmentMatrix;
|
draft.members[index].equipmentMatrix;
|
||||||
@@ -477,9 +476,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
radius="large"
|
radius="large"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: "100%",
|
||||||
width: 12,
|
width: 12,
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" justify="center">
|
<Flex direction="column" justify="center">
|
||||||
@@ -488,14 +487,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
alt={unwrap(
|
alt={unwrap(
|
||||||
consumableDefinitions.consumables[
|
consumableDefinitions.consumables[
|
||||||
consumable
|
consumable
|
||||||
].name,
|
].name
|
||||||
)}
|
)}
|
||||||
key={consumable}
|
key={consumable}
|
||||||
src={asset(
|
src={asset(
|
||||||
`/icons/consumables/${consumable}.webp`,
|
`/icons/consumables/${consumable}.webp`
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: '50%',
|
left: "50%",
|
||||||
/**
|
/**
|
||||||
* max = 100% - 16px - 4px
|
* max = 100% - 16px - 4px
|
||||||
* min = 4px
|
* min = 4px
|
||||||
@@ -510,11 +509,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
? 0.5
|
? 0.5
|
||||||
: index / (consumables.length - 1)
|
: index / (consumables.length - 1)
|
||||||
} * (100% - 24px))`,
|
} * (100% - 24px))`,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -531,7 +530,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
consumables={consumablesList}
|
consumables={consumablesList}
|
||||||
selected={consumables}
|
selected={consumables}
|
||||||
onConsumablesChange={(consumables) => {
|
onConsumablesChange={(consumables) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].consumables = consumables;
|
draft.members[index].consumables = consumables;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -544,7 +543,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].consumables = [];
|
draft.members[index].consumables = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -562,18 +561,18 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
{!tank.fixed_camouflage && (
|
{!tank.fixed_camouflage && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].camouflage = !camouflage;
|
draft.members[index].camouflage = !camouflage;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
variant={camouflage ? 'solid' : 'outline'}
|
variant={camouflage ? "solid" : "outline"}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={asset('icons/camo.webp')}
|
src={asset("icons/camo.webp")}
|
||||||
style={{
|
style={{
|
||||||
width: '1em',
|
width: "1em",
|
||||||
height: '1em',
|
height: "1em",
|
||||||
transform: 'scale(2) translate(15%, 15%)',
|
transform: "scale(2) translate(15%, 15%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -581,7 +580,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
)}
|
)}
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
|
|||||||
@@ -1,33 +1,27 @@
|
|||||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
|
||||||
import {
|
import {
|
||||||
ComparePersistent,
|
ComparePersistent,
|
||||||
type DeltaMode,
|
type DeltaMode,
|
||||||
} from '../../stores/comparePersistent';
|
} from "../../stores/comparePersistent";
|
||||||
import { TankSearch } from '../TankSearch';
|
import { TankSearch } from "../TankSearch";
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
addTankDialogOpen: boolean;
|
addTankDialogOpen: boolean;
|
||||||
onAddTankDialogOpenChange: (open: boolean) => void;
|
onAddTankDialogOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
awaitableProvisionDefinitions,
|
|
||||||
awaitableModelDefinitions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function Controls({
|
export function Controls({
|
||||||
addTankDialogOpen,
|
addTankDialogOpen,
|
||||||
onAddTankDialogOpenChange,
|
onAddTankDialogOpenChange,
|
||||||
}: ControlsProps) {
|
}: ControlsProps) {
|
||||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const mutateComparePersistent = ComparePersistent.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,28 +54,20 @@ export function Controls({
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
CompareEphemeral.mutate((draft) => {
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
tankToCompareMember(tank, model, provisionDefinitions),
|
tankToCompareMember(tank, provisionDefinitions)
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
onAddTankDialogOpenChange(false);
|
onAddTankDialogOpenChange(false);
|
||||||
}}
|
}}
|
||||||
onSelectAll={(tanks) => {
|
onSelectAll={(tanks) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
...tanks.map((tank) => {
|
...tanks.map((tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
return tankToCompareMember(tank, provisionDefinitions);
|
||||||
|
})
|
||||||
return tankToCompareMember(
|
|
||||||
tank,
|
|
||||||
model,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -97,7 +83,7 @@ export function Controls({
|
|||||||
variant="soft"
|
variant="soft"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members = [];
|
draft.members = [];
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -110,18 +96,18 @@ export function Controls({
|
|||||||
variant="classic"
|
variant="classic"
|
||||||
value={deltaMode}
|
value={deltaMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateComparePersistent((draft) => {
|
ComparePersistent.mutate((draft) => {
|
||||||
draft.deltaMode = value as DeltaMode;
|
draft.deltaMode = value as DeltaMode;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SegmentedControl.Item value={'none' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"none" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.none}
|
{strings.website.tools.compare.actions.deltas.none}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={'percentage' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"percentage" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.percentage}
|
{strings.website.tools.compare.actions.deltas.percentage}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={'absolute' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"absolute" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.absolute}
|
{strings.website.tools.compare.actions.deltas.absolute}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
</SegmentedControl.Root>
|
</SegmentedControl.Root>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { type TankDefinition, tankIcon } from '@blitzkit/core';
|
import { type TankDefinition, tankIcon } from "@blitzkit/core";
|
||||||
import { Flex, Text } from '@radix-ui/themes';
|
import { Flex, Text } from "@radix-ui/themes";
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from "react";
|
||||||
import { Vector2 } from 'three';
|
import { Vector2 } from "three";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { InsertionMarker } from './IntersectionMarker';
|
import { InsertionMarker } from "./IntersectionMarker";
|
||||||
import { insertionMarkers } from './IntersectionMarker/constants';
|
import { insertionMarkers } from "./IntersectionMarker/constants";
|
||||||
import { TankControl } from './TankControl';
|
import { TankControl } from "./TankControl";
|
||||||
|
|
||||||
interface TankCardProps {
|
interface TankCardProps {
|
||||||
index: number;
|
index: number;
|
||||||
@@ -16,7 +16,6 @@ interface TankCardProps {
|
|||||||
|
|
||||||
export function TankCard({ index, tank }: TankCardProps) {
|
export function TankCard({ index, tank }: TankCardProps) {
|
||||||
const draggable = useRef<HTMLDivElement>(null);
|
const draggable = useRef<HTMLDivElement>(null);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const { unwrap } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,8 +33,8 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
|
|
||||||
delta.set(0, 0);
|
delta.set(0, 0);
|
||||||
current.copy(initial.set(event.clientX, event.clientY));
|
current.copy(initial.set(event.clientX, event.clientY));
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -43,7 +42,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
current.set(event.clientX, event.clientY);
|
current.set(event.clientX, event.clientY);
|
||||||
delta.copy(current).sub(initial);
|
delta.copy(current).sub(initial);
|
||||||
insertionMarkers.forEach(({ element }) => {
|
insertionMarkers.forEach(({ element }) => {
|
||||||
element.style.opacity = '0';
|
element.style.opacity = "0";
|
||||||
});
|
});
|
||||||
|
|
||||||
const distances = insertionMarkers.map((insertionMarker) => {
|
const distances = insertionMarkers.map((insertionMarker) => {
|
||||||
@@ -56,34 +55,34 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const closest = distances.reduce((a, b) =>
|
const closest = distances.reduce((a, b) =>
|
||||||
a.distance < b.distance ? a : b,
|
a.distance < b.distance ? a : b
|
||||||
);
|
);
|
||||||
|
|
||||||
dropIndex = closest.index;
|
dropIndex = closest.index;
|
||||||
closest.element.style.opacity = '1';
|
closest.element.style.opacity = "1";
|
||||||
draggable.current!.style.position = 'fixed';
|
draggable.current!.style.position = "fixed";
|
||||||
draggable.current!.style.cursor = 'grabbing';
|
draggable.current!.style.cursor = "grabbing";
|
||||||
draggable.current!.style.zIndex = '1';
|
draggable.current!.style.zIndex = "1";
|
||||||
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
|
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
|
||||||
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
|
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
|
||||||
}
|
}
|
||||||
function handlePointerUp(event: PointerEvent) {
|
function handlePointerUp(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
draggable.current!.style.position = 'static';
|
draggable.current!.style.position = "static";
|
||||||
draggable.current!.style.zIndex = 'unset';
|
draggable.current!.style.zIndex = "unset";
|
||||||
draggable.current!.style.cursor = 'grab';
|
draggable.current!.style.cursor = "grab";
|
||||||
|
|
||||||
insertionMarkers.forEach(({ element }) => {
|
insertionMarkers.forEach(({ element }) => {
|
||||||
element.style.opacity = '0';
|
element.style.opacity = "0";
|
||||||
});
|
});
|
||||||
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
|
|
||||||
if (dropIndex === -1) return;
|
if (dropIndex === -1) return;
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const fromIndex = index;
|
const fromIndex = index;
|
||||||
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
|
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
|
||||||
|
|
||||||
@@ -95,10 +94,10 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
dropIndex = -1;
|
dropIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
draggable.current?.addEventListener('pointerdown', handlePointerDown);
|
draggable.current?.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
draggable.current?.removeEventListener('pointerdown', handlePointerDown);
|
draggable.current?.removeEventListener("pointerdown", handlePointerDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -114,9 +113,9 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
justify="between"
|
justify="between"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{
|
style={{
|
||||||
userSelect: 'none',
|
userSelect: "none",
|
||||||
touchAction: 'none',
|
touchAction: "none",
|
||||||
cursor: 'grab',
|
cursor: "grab",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -126,16 +125,16 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
height={64}
|
height={64}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{
|
style={{
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
maxWidth: 128,
|
maxWidth: 128,
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{unwrap(tank.name)}
|
{unwrap(tank.name)}
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
import { Dialog, Flex, IconButton } from "@radix-ui/themes";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { LinkI18n } from "../LinkI18n";
|
||||||
import { LinkI18n } from '../LinkI18n';
|
import { TankSearch } from "../TankSearch";
|
||||||
import { TankSearch } from '../TankSearch';
|
|
||||||
|
|
||||||
interface TankControlProps {
|
interface TankControlProps {
|
||||||
index: number;
|
index: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
awaitableProvisionDefinitions,
|
|
||||||
awaitableModelDefinitions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function TankControl({ index, slug }: TankControlProps) {
|
export function TankControl({ index, slug }: TankControlProps) {
|
||||||
const { locale } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap="3" justify="center" style={{ width: '100%' }}>
|
<Flex gap="3" justify="center" style={{ width: "100%" }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.splice(index, 1);
|
draft.members.splice(index, 1);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -49,7 +44,9 @@ export function TankControl({ index, slug }: TankControlProps) {
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
|
|
||||||
<Dialog.Content>
|
<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 gap="4" direction="column">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -61,13 +58,10 @@ export function TankControl({ index, slug }: TankControlProps) {
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
CompareEphemeral.mutate((draft) => {
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
|
||||||
draft.members[index] = tankToCompareMember(
|
draft.members[index] = tankToCompareMember(
|
||||||
tank,
|
tank,
|
||||||
model,
|
provisionDefinitions
|
||||||
provisionDefinitions,
|
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
|
|||||||