mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54ceb944e | ||
|
|
37a2ba3566 | ||
|
|
caff7bb6d0 | ||
|
|
01c491aee0 | ||
|
|
fd408d6777 | ||
|
|
6d1ed1e56e | ||
|
|
46f9d605f7 | ||
|
|
a65ea42b81 | ||
|
|
40f8c6a915 | ||
|
|
4678359f36 | ||
|
|
f91bd63646 | ||
|
|
c1872262f5 | ||
|
|
082e112758 | ||
|
|
4cb7d50eb5 | ||
|
|
8c050add23 | ||
|
|
2610697d86 | ||
|
|
64f1de37ad | ||
|
|
58f6862279 | ||
|
|
b47f6fdb32 | ||
|
|
ce5a53d0ff | ||
|
|
6ee1fa13d3 | ||
|
|
196f780bb3 | ||
|
|
088d5a2941 | ||
|
|
47abdaed6f | ||
|
|
30f6055a69 | ||
|
|
9d4b750dfd | ||
|
|
d1308bc625 | ||
|
|
db40256bb4 | ||
|
|
70f1ebadfc | ||
|
|
43adaf85f5 | ||
|
|
8d5e78a510 | ||
|
|
9d6d893373 | ||
|
|
9840982aa2 | ||
|
|
8e216ebe46 | ||
|
|
889ae39e3b | ||
|
|
265eff98bf | ||
|
|
ef7d1dcfb3 | ||
|
|
607ebc0025 | ||
|
|
0f0bc9569d | ||
|
|
169e892678 | ||
|
|
fe249e2704 | ||
|
|
b8be5d5e38 | ||
|
|
77d6c93b90 | ||
|
|
4fedaf1cea | ||
|
|
b2220f885e | ||
|
|
3181f9c4e7 | ||
|
|
153d0d326d | ||
|
|
7f9b349de6 | ||
|
|
b3a50f2b27 | ||
|
|
fb49628375 | ||
|
|
394a74c3a0 | ||
|
|
370bf826c0 | ||
|
|
38ca1499e2 | ||
|
|
2deced699b | ||
|
|
6405af7ec7 | ||
|
|
fa2bdfca12 | ||
|
|
872e7ce704 | ||
|
|
110d3c1007 | ||
|
|
7a27b70c29 | ||
|
|
2ffe5a137c | ||
|
|
bf0246ec26 | ||
|
|
b71feca802 | ||
|
|
da1fbeeca4 | ||
|
|
53174db351 | ||
|
|
19d2ad4cb2 | ||
|
|
3325536bbc | ||
|
|
4220177b02 | ||
|
|
796aca21cf | ||
|
|
3510768786 | ||
|
|
6197642d5e | ||
|
|
b60f8bfd57 | ||
|
|
9437d16908 | ||
|
|
de9077e0d2 | ||
|
|
4a6e946809 | ||
|
|
935c578ef6 | ||
|
|
0e06ff52ba | ||
|
|
fa4590d8f6 | ||
|
|
5620b118f4 | ||
|
|
d53b31ca40 | ||
|
|
a4a5d53f66 | ||
|
|
9f4c8c9088 | ||
|
|
472172c504 | ||
|
|
bc08656806 | ||
|
|
bd6f1f3570 | ||
|
|
716b446272 | ||
|
|
96a9e5b938 | ||
|
|
83e31e6352 | ||
|
|
49d0dbe86d | ||
|
|
e141a7ca3f | ||
|
|
adad668157 | ||
|
|
267f23cad8 | ||
|
|
f605801555 | ||
|
|
19e81ff78a | ||
|
|
9232a95e00 | ||
|
|
6dcef6b199 | ||
|
|
e355bd10b1 | ||
|
|
ceb4ed52c0 | ||
|
|
227750efad | ||
|
|
8a76558f01 | ||
|
|
db7c865c76 | ||
|
|
8e35e0d6d9 | ||
|
|
c9d50f39b5 | ||
|
|
77ac893a7c | ||
|
|
ac60a436f1 | ||
|
|
2ba9f0f4c8 | ||
|
|
47c3aa4842 | ||
|
|
d2edf4b89d | ||
|
|
11a0c246dc | ||
|
|
cba93b3d55 | ||
|
|
fa9b6c6d97 | ||
|
|
bef48eca62 | ||
|
|
e42ff03c4b | ||
|
|
ec628e4a0d | ||
|
|
8865a48936 | ||
|
|
d3a9c3bd95 | ||
|
|
90bff59af3 | ||
|
|
9efe9860ae | ||
|
|
f6a418499b | ||
|
|
66740bcb8d | ||
|
|
035e257926 | ||
|
|
e63549158b | ||
|
|
7fb64e690f | ||
|
|
54798c30b1 | ||
|
|
4c0f7b61e4 | ||
|
|
206ac373d2 | ||
|
|
c626e84f7d | ||
|
|
90ec5fa80b | ||
|
|
d496b46b6f | ||
|
|
7af1fdcf6b | ||
|
|
199e156e53 | ||
|
|
f5578ee788 | ||
|
|
c0a3628b3b | ||
|
|
fa8922e0a6 | ||
|
|
bf6991ce06 | ||
|
|
7f49245f15 | ||
|
|
f55313e0cd | ||
|
|
69c388f243 | ||
|
|
35788657a8 | ||
|
|
d531698867 | ||
|
|
cc4177bf14 | ||
|
|
d377067441 | ||
|
|
b2f026c212 | ||
|
|
07cdb0788c | ||
|
|
01f919ddd3 | ||
|
|
4b1fda9680 | ||
|
|
79d03233ae | ||
|
|
522b40aff1 | ||
|
|
06c878dd2c | ||
|
|
f5c589373d | ||
|
|
2689568650 | ||
|
|
fabf649608 | ||
|
|
080af86e1b | ||
|
|
e5b58cd5ff | ||
|
|
3500389628 | ||
|
|
1129f0d60b | ||
|
|
b3a6a47a6a | ||
|
|
bdb3844bcc | ||
|
|
f0bb25efc1 |
No files matched your search
@@ -2,10 +2,10 @@ name: CD Build Assets
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: dev
|
branches: [dev, main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
# schedule:
|
schedule:
|
||||||
# - cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -50,4 +50,5 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||||
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||||
|
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
|
||||||
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
name: CI Version
|
|
||||||
|
|
||||||
on: push
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
host:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Use node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install Yarn
|
|
||||||
run: npm install --location=global yarn
|
|
||||||
|
|
||||||
- name: Use node_modules cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: '**/node_modules'
|
|
||||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn
|
|
||||||
|
|
||||||
- name: Generate leaderboard
|
|
||||||
run: yarn lint:version
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "submodules/closedkrieg"]
|
||||||
|
path = submodules/closedkrieg
|
||||||
|
url = https://github.com/tresabhi/closedkrieg
|
||||||
Vendored
+9
-1
@@ -5,6 +5,7 @@
|
|||||||
"accountinfo",
|
"accountinfo",
|
||||||
"aeson",
|
"aeson",
|
||||||
"alphatest",
|
"alphatest",
|
||||||
|
"APAC",
|
||||||
"apcr",
|
"apcr",
|
||||||
"artefacts",
|
"artefacts",
|
||||||
"atgm",
|
"atgm",
|
||||||
@@ -40,8 +41,10 @@
|
|||||||
"donotremove",
|
"donotremove",
|
||||||
"drei",
|
"drei",
|
||||||
"Droodles",
|
"Droodles",
|
||||||
|
"dvpd",
|
||||||
"dvpl",
|
"dvpl",
|
||||||
"DVPLCRC",
|
"DVPLCRC",
|
||||||
|
"dvpm",
|
||||||
"dxgi",
|
"dxgi",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
"FASTNAME",
|
"FASTNAME",
|
||||||
@@ -54,6 +57,7 @@
|
|||||||
"glsl",
|
"glsl",
|
||||||
"GLTF",
|
"GLTF",
|
||||||
"haha",
|
"haha",
|
||||||
|
"highp",
|
||||||
"iife",
|
"iife",
|
||||||
"immer",
|
"immer",
|
||||||
"Inclusivity",
|
"Inclusivity",
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
"lods",
|
"lods",
|
||||||
"metallicness",
|
"metallicness",
|
||||||
"minitelrose",
|
"minitelrose",
|
||||||
|
"Minotauro",
|
||||||
"MIPMAPCOUNT",
|
"MIPMAPCOUNT",
|
||||||
"modeltransform",
|
"modeltransform",
|
||||||
"MURICA",
|
"MURICA",
|
||||||
@@ -128,6 +133,7 @@
|
|||||||
"TYPELESS",
|
"TYPELESS",
|
||||||
"ubjson",
|
"ubjson",
|
||||||
"Unicum",
|
"Unicum",
|
||||||
|
"UNLOCALIZED",
|
||||||
"UNORM",
|
"UNORM",
|
||||||
"Unregistering",
|
"Unregistering",
|
||||||
"vickers",
|
"vickers",
|
||||||
@@ -137,6 +143,7 @@
|
|||||||
"webp",
|
"webp",
|
||||||
"winrate",
|
"winrate",
|
||||||
"wireframe",
|
"wireframe",
|
||||||
|
"WOTB",
|
||||||
"wotblitz",
|
"wotblitz",
|
||||||
"xffn",
|
"xffn",
|
||||||
"xkdkw",
|
"xkdkw",
|
||||||
@@ -145,5 +152,6 @@
|
|||||||
"zustand"
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
"deno.unstable": false
|
"deno.unstable": false,
|
||||||
|
"cSpell.enableFiletypes": ["glsl"]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
files:
|
||||||
|
- source: /src/lang/en-US.json
|
||||||
|
translation: /src/lang/%locale%.json
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Blitzkrieg 1.16.1
|
||||||
|
|
||||||
|
Armor sandbox gains 25% rendering speed and 40% memory efficiency with added accuracy and features.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Multiple layers of spaced armors are now calculated properly by the visual armor highlighting
|
||||||
|
- Added opaque mode to view armor highlighting without any transparency
|
||||||
|
- Added breakdown on layers of armor penetrated
|
||||||
|
- Penetration of each layer is shown represented by a color and an index
|
||||||
|
- Green: penetration
|
||||||
|
- Red: blocked
|
||||||
|
- Yellow: ricochet
|
||||||
|
- This also shows layers encountered after a ricochet
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Accuracy now doesn't drop the further the shot is placed from the origin
|
||||||
|
- HE shells no longer loose penetration like how HEAT shells do when traveling between layers of armor
|
||||||
|
- Persistent settings are now merged in the correct order making them truly persistent now
|
||||||
|
- Enhanced armor and calibrated shells are now properly synced from the get go
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Spaced armor and external module scenes have been merged
|
||||||
|
- They now use additive blending
|
||||||
|
- Colors are now normalized by the penetration value instead of maximum armor plate thickness
|
||||||
|
- Shell normalization now doesn't violate arccosine's domain
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Blitzkrieg 1.16.2
|
||||||
|
|
||||||
|
¡Hola desde el mundo del español!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added translations for Spanish to the bot
|
||||||
|
- Your language will be inferred automatically from your Discord settings
|
||||||
|
- Any untranslated languages and words we forgot to translate will be left in English
|
||||||
|
- If you have your language set to Spanish and see English in any places, please let us know on [the official Discord server](https://discord.gg/nDt7AjGJQH)
|
||||||
|
- All `/about` commands except for `commands` remain in English
|
||||||
|
- Tank names are still in English
|
||||||
|
- The website remains unchanged
|
||||||
|
- The title bar (featured as the big text on top of most commands) has now been made more compact
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- "Tank type" is now called "tank class"
|
||||||
|
- Removed `/player-achievements` and `search-tanks`
|
||||||
|
- They will be reintroduced in the future in a far better way
|
||||||
|
- Also removed a `SKLLD` server exclusive `/eligible` command
|
||||||
|
- This will be generalized for all clans in the future
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed the red glowing floor underneath the tank sandbox
|
||||||
|
- This has been replaced with a static image
|
||||||
|
- The refresh button is now properly removed when lacking permissions
|
||||||
|
- Tank names that start with a number are now properly inferred from command inputs
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Command registration is now error tolerant meaning that if a single command fails to initiate, it will be discarded until the next restart
|
||||||
|
- We are currently looking into how we can allow the command to retry registration after a fixed amount of time
|
||||||
|
- Due to the implications of the code added which can be very easily exploited by hackers, a single file has been added to the repository as a closed source submodule and will remain closed unless a third party open-source implementation is made public
|
||||||
|
- Removed preview commands
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Blitzkrieg 1.16.2
|
||||||
|
|
||||||
|
¡Hola desde el mundo del español!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added translations for Spanish to the bot
|
||||||
|
- Your language will be inferred automatically from your Discord settings
|
||||||
|
- Any untranslated languages and words we forgot to translate will be left in English
|
||||||
|
- If you have your language set to Spanish and see English in any places, please let us know on [the official Discord server](https://discord.gg/nDt7AjGJQH)
|
||||||
|
- All `/about` commands except for `commands` remain in English
|
||||||
|
- Tank names are still in English
|
||||||
|
- The website remains unchanged
|
||||||
|
- The title bar (featured as the big text on top of most commands) has now been made more compact
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- "Tank type" is now called "tank class"
|
||||||
|
- Removed `/player-achievements` and `search-tanks`
|
||||||
|
- They will be reintroduced in the future in a far better way
|
||||||
|
- Also removed a `SKLLD` server exclusive `/eligible` command
|
||||||
|
- This will be generalized for all clans in the future
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed the red glowing floor underneath the tank sandbox
|
||||||
|
- This has been replaced with a static image
|
||||||
|
- The refresh button is now properly removed when lacking permissions
|
||||||
|
- Tank names that start with a number are now properly inferred from command inputs
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Command registration is now error tolerant meaning that if a single command fails to initiate, it will be discarded until the next restart
|
||||||
|
- We are currently looking into how we can allow the command to retry registration after a fixed amount of time
|
||||||
|
- Due to the implications of the code added which can be very easily exploited by hackers, a single file has been added to the repository as a closed source submodule and will remain closed unless a third party open-source implementation is made public
|
||||||
|
- Removed preview commands
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Blitzkrieg 1.16.3
|
||||||
|
|
||||||
|
Further polishing and corrections for the Tankopedia.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added a few more stats to view for the Tankopedia
|
||||||
|
- Added the count of shells in the magazine for auto-reloaders
|
||||||
|
- The optimal shell index was moved below this stats
|
||||||
|
- Shell reloads for auto-reloaders now abide crew stats
|
||||||
|
- Intra-clip is now also shown for auto-reloaders
|
||||||
|
- Added 1 more decimal place worth of precision when viewing and changing gun pitch and yaw
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- "Inter-clip" is now correctly spelled as "intra-clip"
|
||||||
|
- Tanks with initial gun pitches (like the Minotauro) are now handled correctly
|
||||||
|
- The stats now reflect the fact that the gun is naturally pitched downwards
|
||||||
|
- The view model now compensates for the natural pitch
|
||||||
|
- Tanks with the improved vertical stabilizer equipment are now handled correctly
|
||||||
|
- The view model now allows you to pitch the gun further as intended when the equipment is on
|
||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.16.0",
|
"version": "1.16.2",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
|
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
|
||||||
"knip": "knip",
|
"knip": "knip",
|
||||||
"update": "yarn upgrade --latest",
|
"update": "yarn upgrade --latest",
|
||||||
"setup:depotdownloader": "vite-node --script scripts/setupDepotDownloader"
|
"setup:depotdownloader": "vite-node --script scripts/setupDepotDownloader",
|
||||||
|
"tunnel": "cloudflared tunnel --url http://localhost:3000"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { readdir } from 'fs/promises';
|
|||||||
import { argv } from 'process';
|
import { argv } from 'process';
|
||||||
|
|
||||||
const isDepot = argv.includes('--depot');
|
const isDepot = argv.includes('--depot');
|
||||||
|
export const WOTB_WIN32_DEPOT = 444202;
|
||||||
export const DATA = isDepot
|
export const DATA = isDepot
|
||||||
? await (async () => {
|
? await (async () => {
|
||||||
const [installationVersion] = await readdir('depots/444202');
|
const [installationVersion] = await readdir(`depots/${WOTB_WIN32_DEPOT}`);
|
||||||
return `depots/444202/${installationVersion}/Data`;
|
return `depots/${WOTB_WIN32_DEPOT}/${installationVersion}/Data`;
|
||||||
})()
|
})()
|
||||||
: 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
|
: 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { readdir } from 'fs/promises';
|
|||||||
import { parse as parsePath } from 'path';
|
import { parse as parsePath } from 'path';
|
||||||
import { Vector3Tuple } from 'three';
|
import { Vector3Tuple } from 'three';
|
||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
import { TankType } from '../../src/components/Tanks';
|
import { TankClass } from '../../src/components/Tanks';
|
||||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { TankParameters } from './tankIcons';
|
import { TankParameters } from './tankIcons';
|
||||||
|
|
||||||
type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
|
|
||||||
interface Strings {
|
interface Strings {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
@@ -306,19 +305,6 @@ type ConsumablesVehicleFilter =
|
|||||||
| { name: string }
|
| { name: string }
|
||||||
| { extendedTags: string };
|
| { extendedTags: string };
|
||||||
|
|
||||||
const consumableProvisionEffectSuffixes = [
|
|
||||||
'Increase',
|
|
||||||
'Factor',
|
|
||||||
'Bias',
|
|
||||||
'Coef',
|
|
||||||
'Percent',
|
|
||||||
];
|
|
||||||
const blitzTankTypeToBlitzkrieg: Record<BlitzTankType, TankType> = {
|
|
||||||
'AT-SPG': 'tankDestroyer',
|
|
||||||
lightTank: 'light',
|
|
||||||
mediumTank: 'medium',
|
|
||||||
heavyTank: 'heavy',
|
|
||||||
};
|
|
||||||
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
||||||
ARMOR_PIERCING: 'ap',
|
ARMOR_PIERCING: 'ap',
|
||||||
ARMOR_PIERCING_CR: 'ap_cr',
|
ARMOR_PIERCING_CR: 'ap_cr',
|
||||||
@@ -502,7 +488,7 @@ export async function definitions(production: boolean) {
|
|||||||
? 'premium'
|
? 'premium'
|
||||||
: 'researchable',
|
: 'researchable',
|
||||||
tier: tank.level as Tier,
|
tier: tank.level as Tier,
|
||||||
type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType],
|
class: tankTags[0] as TankClass,
|
||||||
testing: tankTags.includes('testTank') ? true : undefined,
|
testing: tankTags.includes('testTank') ? true : undefined,
|
||||||
price: tankPrice,
|
price: tankPrice,
|
||||||
camouflage: {
|
camouflage: {
|
||||||
@@ -688,7 +674,7 @@ export async function definitions(production: boolean) {
|
|||||||
: turretGunEntry.reloadTime;
|
: turretGunEntry.reloadTime;
|
||||||
const gunClipCount =
|
const gunClipCount =
|
||||||
gunType === 'regular' ? undefined : turretGunEntry.clip!.count;
|
gunType === 'regular' ? undefined : turretGunEntry.clip!.count;
|
||||||
const gunInterClip =
|
const gunIntraClip =
|
||||||
gunType === 'regular'
|
gunType === 'regular'
|
||||||
? undefined
|
? undefined
|
||||||
: 60 / turretGunEntry.clip!.rate;
|
: 60 / turretGunEntry.clip!.rate;
|
||||||
@@ -736,7 +722,7 @@ export async function definitions(production: boolean) {
|
|||||||
type: gunType,
|
type: gunType,
|
||||||
reload: gunReload,
|
reload: gunReload,
|
||||||
count: gunClipCount,
|
count: gunClipCount,
|
||||||
interClip: gunInterClip,
|
intraClip: gunIntraClip,
|
||||||
camouflageLoss:
|
camouflageLoss:
|
||||||
typeof turretGunEntry.invisibilityFactorAtShot === 'number'
|
typeof turretGunEntry.invisibilityFactorAtShot === 'number'
|
||||||
? turretGunEntry.invisibilityFactorAtShot
|
? turretGunEntry.invisibilityFactorAtShot
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
|
|||||||
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
|
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
|
||||||
}
|
}
|
||||||
type="tank"
|
type="tank"
|
||||||
tankType={tankDefinitions?.type}
|
tankType={tankDefinitions?.class}
|
||||||
treeType={tankDefinitions?.treeType}
|
treeType={tankDefinitions?.treeType}
|
||||||
stats={session.customColumns.map((customColumn) => {
|
stats={session.customColumns.map((customColumn) => {
|
||||||
const rowStat = rowStats[customColumn.display];
|
const rowStat = rowStats[customColumn.display];
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
paddingTop: isEmbed ? 0 : '3.25rem',
|
paddingTop: isEmbed ? 0 : '3.25rem',
|
||||||
|
backgroundColor: isEmbed ? 'transparent' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
@@ -100,6 +101,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
|
|
||||||
<Flex direction="column">{children}</Flex>
|
<Flex direction="column">{children}</Flex>
|
||||||
|
|
||||||
|
{!isEmbed && (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -153,6 +155,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</a>
|
</a>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
)}
|
||||||
</Theme>
|
</Theme>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ import PageWrapper from '../../../components/PageWrapper';
|
|||||||
import { Skeleton } from '../../../components/Skeleton';
|
import { Skeleton } from '../../../components/Skeleton';
|
||||||
import { LEAGUES } from '../../../constants/leagues';
|
import { LEAGUES } from '../../../constants/leagues';
|
||||||
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings';
|
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings';
|
||||||
import { REGIONS, REGION_NAMES, Region } from '../../../constants/regions';
|
import {
|
||||||
|
REGIONS,
|
||||||
|
Region,
|
||||||
|
UNLOCALIZED_REGION_NAMES,
|
||||||
|
} from '../../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||||
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
|
||||||
@@ -556,7 +560,7 @@ export default function Page() {
|
|||||||
<Select.Content>
|
<Select.Content>
|
||||||
{REGIONS.map((region) => (
|
{REGIONS.map((region) => (
|
||||||
<Select.Item key={region} value={region}>
|
<Select.Item key={region} value={region}>
|
||||||
{REGION_NAMES[region]}
|
{UNLOCALIZED_REGION_NAMES[region]}
|
||||||
</Select.Item>
|
</Select.Item>
|
||||||
))}
|
))}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
import PageWrapper from '../../../components/PageWrapper';
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
import { REGION_NAMES } from '../../../constants/regions';
|
import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
|
||||||
import searchPlayersAcrossRegions, {
|
import searchPlayersAcrossRegions, {
|
||||||
AccountListWithServer,
|
AccountListWithServer,
|
||||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||||
@@ -107,7 +107,7 @@ export default function Page() {
|
|||||||
setShowSearchResults(false);
|
setShowSearchResults(false);
|
||||||
setSession(region, id, nickname);
|
setSession(region, id, nickname);
|
||||||
}}
|
}}
|
||||||
shortcut={REGION_NAMES[region]}
|
shortcut={UNLOCALIZED_REGION_NAMES[region]}
|
||||||
>
|
>
|
||||||
{nickname}
|
{nickname}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { debounce } from 'lodash';
|
|||||||
import { use, useEffect, useRef, useState } from 'react';
|
import { use, useEffect, useRef, useState } from 'react';
|
||||||
import { lerp } from 'three/src/math/MathUtils';
|
import { lerp } from 'three/src/math/MathUtils';
|
||||||
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
|
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
|
||||||
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
||||||
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
||||||
import { coefficient } from '../../../../../../core/blitzkrieg/coefficient';
|
import { coefficient } from '../../../../../../core/blitzkrieg/coefficient';
|
||||||
@@ -56,6 +57,7 @@ export function Characteristics() {
|
|||||||
turretModelDefinition.boundingBox,
|
turretModelDefinition.boundingBox,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
|
||||||
const weight =
|
const weight =
|
||||||
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
|
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
|
||||||
const weightTons = weight / 1000;
|
const weightTons = weight / 1000;
|
||||||
@@ -113,7 +115,7 @@ export function Characteristics() {
|
|||||||
// TODO: add max rolls
|
// TODO: add max rolls
|
||||||
[hasTungsten, 0.15],
|
[hasTungsten, 0.15],
|
||||||
);
|
);
|
||||||
const interClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
|
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
|
||||||
const shellVelocityCoefficient = coefficient(
|
const shellVelocityCoefficient = coefficient(
|
||||||
[hasSupercharger, 0.3],
|
[hasSupercharger, 0.3],
|
||||||
[hasImprovedGunPowder, 0.3],
|
[hasImprovedGunPowder, 0.3],
|
||||||
@@ -136,12 +138,13 @@ export function Characteristics() {
|
|||||||
]);
|
]);
|
||||||
const enginePowerCoefficient = coefficient(
|
const enginePowerCoefficient = coefficient(
|
||||||
[
|
[
|
||||||
hasEngineAccelerator && (tank.type === 'light' || tank.type === 'medium'),
|
hasEngineAccelerator &&
|
||||||
|
(tank.class === 'lightTank' || tank.class === 'mediumTank'),
|
||||||
0.05,
|
0.05,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
hasEngineAccelerator &&
|
hasEngineAccelerator &&
|
||||||
(tank.type === 'heavy' || tank.type === 'tankDestroyer'),
|
(tank.class === 'heavyTank' || tank.class === 'AT-SPG'),
|
||||||
0.07,
|
0.07,
|
||||||
],
|
],
|
||||||
[hasEnginePowerBoost, 0.2],
|
[hasEnginePowerBoost, 0.2],
|
||||||
@@ -163,7 +166,7 @@ export function Characteristics() {
|
|||||||
const viewRangeCoefficient =
|
const viewRangeCoefficient =
|
||||||
coefficient([
|
coefficient([
|
||||||
hasImprovedOptics,
|
hasImprovedOptics,
|
||||||
tank.type === 'tankDestroyer' ? 0.05 : tank.type === 'heavy' ? 0.07 : 0.1,
|
tank.class === 'AT-SPG' ? 0.05 : tank.class === 'heavyTank' ? 0.07 : 0.1,
|
||||||
]) * coefficient([true, progressiveStat(commanderMastery)]);
|
]) * coefficient([true, progressiveStat(commanderMastery)]);
|
||||||
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
|
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
|
||||||
|
|
||||||
@@ -180,38 +183,26 @@ export function Characteristics() {
|
|||||||
const camouflageSumMoving = sum(
|
const camouflageSumMoving = sum(
|
||||||
[
|
[
|
||||||
hasCamouflageNet,
|
hasCamouflageNet,
|
||||||
tank.type === 'heavy'
|
tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05,
|
||||||
? 0.03
|
|
||||||
: tank.type === 'tankDestroyer'
|
|
||||||
? 0.07
|
|
||||||
: 0.05,
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
camouflage,
|
camouflage,
|
||||||
tank.type === 'tankDestroyer'
|
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
|
||||||
? 0.04
|
|
||||||
: tank.type === 'heavy'
|
|
||||||
? 0.03
|
|
||||||
: 0.02,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const camouflageSumStill = sum(
|
const camouflageSumStill = sum(
|
||||||
[
|
[
|
||||||
hasCamouflageNet,
|
hasCamouflageNet,
|
||||||
2 *
|
2 *
|
||||||
(tank.type === 'heavy'
|
(tank.class === 'heavyTank'
|
||||||
? 0.03
|
? 0.03
|
||||||
: tank.type === 'tankDestroyer'
|
: tank.class === 'AT-SPG'
|
||||||
? 0.07
|
? 0.07
|
||||||
: 0.05),
|
: 0.05),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
camouflage,
|
camouflage,
|
||||||
tank.type === 'tankDestroyer'
|
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
|
||||||
? 0.04
|
|
||||||
: tank.type === 'heavy'
|
|
||||||
? 0.03
|
|
||||||
: 0.02,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -221,7 +212,7 @@ export function Characteristics() {
|
|||||||
shell,
|
shell,
|
||||||
damageCoefficient,
|
damageCoefficient,
|
||||||
reloadCoefficient,
|
reloadCoefficient,
|
||||||
interClipCoefficient,
|
intraClipCoefficient,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -229,6 +220,17 @@ export function Characteristics() {
|
|||||||
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
||||||
}
|
}
|
||||||
}, [penetrationDistance]);
|
}, [penetrationDistance]);
|
||||||
|
useEffect(() => {
|
||||||
|
mutateDuel((draft) => {
|
||||||
|
[draft.protagonist!.pitch, draft.protagonist!.yaw] = applyPitchYawLimits(
|
||||||
|
draft.protagonist!.pitch,
|
||||||
|
draft.protagonist!.yaw,
|
||||||
|
gunModelDefinition.pitch,
|
||||||
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [hasImprovedVerticalStabilizer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="8" style={{ width: '100%' }}>
|
<Flex direction="column" gap="8" style={{ width: '100%' }}>
|
||||||
@@ -266,7 +268,7 @@ export function Characteristics() {
|
|||||||
<>
|
<>
|
||||||
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
|
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
|
||||||
{gun.reload.at(-1)! < gun.reload.at(-2)!
|
{gun.reload.at(-1)! < gun.reload.at(-2)!
|
||||||
? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) *
|
? (shell.damage.armor / (gun.reload.at(-1)! + gun.intraClip)) *
|
||||||
60
|
60
|
||||||
: (shell.damage.armor / gun.reload[0]) * 60}
|
: (shell.damage.armor / gun.reload[0]) * 60}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
@@ -277,12 +279,12 @@ export function Characteristics() {
|
|||||||
unit="hp / min"
|
unit="hp / min"
|
||||||
>
|
>
|
||||||
{gun.reload.at(-1)! < gun.reload.at(-2)!
|
{gun.reload.at(-1)! < gun.reload.at(-2)!
|
||||||
? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) *
|
? (shell.damage.armor / (gun.reload.at(-1)! + gun.intraClip)) *
|
||||||
(60 -
|
(60 -
|
||||||
(gun.reload.slice(0, -1).length - 1) * gun.interClip) +
|
(gun.reload.slice(0, -1).length - 1) * gun.intraClip) +
|
||||||
shell.damage.armor * gun.reload.slice(0, -1).length
|
shell.damage.armor * gun.reload.slice(0, -1).length
|
||||||
: (shell.damage.armor / (gun.reload[0] + gun.interClip)) *
|
: (shell.damage.armor / (gun.reload[0] + gun.intraClip)) *
|
||||||
(60 - (gun.reload.slice(1).length - 1) * gun.interClip) +
|
(60 - (gun.reload.slice(1).length - 1) * gun.intraClip) +
|
||||||
shell.damage.armor * gun.reload.slice(1).length}
|
shell.damage.armor * gun.reload.slice(1).length}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info
|
<Info
|
||||||
@@ -296,14 +298,15 @@ export function Characteristics() {
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Info indent name="Optimal shell index">
|
|
||||||
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1}
|
|
||||||
</Info>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{gun.type === 'autoReloader' ? (
|
{gun.type === 'autoReloader' ? (
|
||||||
<>
|
<>
|
||||||
<Info name="Reload" unit="s" />
|
<InfoWithDelta name="Shells">{gun.reload.length}</InfoWithDelta>
|
||||||
|
<Info indent name="Most optimal">
|
||||||
|
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1}
|
||||||
|
</Info>
|
||||||
|
<Info name="Shell reloads" unit="s" />
|
||||||
{gun.reload.map((reload, index) => (
|
{gun.reload.map((reload, index) => (
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
key={index}
|
key={index}
|
||||||
@@ -312,7 +315,7 @@ export function Characteristics() {
|
|||||||
decimals={2}
|
decimals={2}
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{reload}
|
{reload * reloadCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -326,17 +329,18 @@ export function Characteristics() {
|
|||||||
{gun.reload * reloadCoefficient}
|
{gun.reload * reloadCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
)}
|
)}
|
||||||
{gun.type === 'autoLoader' && (
|
{gun.type === 'autoLoader' ||
|
||||||
|
(gun.type === 'autoReloader' && (
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
indent
|
indent
|
||||||
decimals={2}
|
decimals={2}
|
||||||
name="Inter-clip reload"
|
name="Intra-clip"
|
||||||
unit="s"
|
unit="s"
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{gun.interClip * interClipCoefficient}
|
{gun.intraClip * intraClipCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
)}
|
))}
|
||||||
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
||||||
{shell.caliber}
|
{shell.caliber}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
@@ -468,10 +472,12 @@ export function Characteristics() {
|
|||||||
<Info name="Gun flexibility" unit="°" />
|
<Info name="Gun flexibility" unit="°" />
|
||||||
<InfoWithDelta decimals={1} indent name="Depression">
|
<InfoWithDelta decimals={1} indent name="Depression">
|
||||||
{gunModelDefinition.pitch.max +
|
{gunModelDefinition.pitch.max +
|
||||||
|
gunDefaultPitch +
|
||||||
(hasImprovedVerticalStabilizer ? 3 : 0)}
|
(hasImprovedVerticalStabilizer ? 3 : 0)}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta decimals={1} indent name="Elevation">
|
<InfoWithDelta decimals={1} indent name="Elevation">
|
||||||
{-gunModelDefinition.pitch.min +
|
{-gunModelDefinition.pitch.min -
|
||||||
|
gunDefaultPitch +
|
||||||
(hasImprovedVerticalStabilizer ? 3 : 0)}
|
(hasImprovedVerticalStabilizer ? 3 : 0)}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
{gunModelDefinition.pitch.front && (
|
{gunModelDefinition.pitch.front && (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react';
|
|||||||
import { Vector3 } from 'three';
|
import { Vector3 } from 'three';
|
||||||
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
||||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { hasEquipment } from '../../../../../core/blitzkrieg/hasEquipment';
|
||||||
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
||||||
import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose';
|
import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose';
|
||||||
import { useAwait } from '../../../../../hooks/useAwait';
|
import { useAwait } from '../../../../../hooks/useAwait';
|
||||||
@@ -67,7 +68,9 @@ export function Controls() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function handlePoseEvent(event: Pose) {
|
async function handlePoseEvent(event: Pose) {
|
||||||
|
const hasImprovedVerticalStabilizer = await hasEquipment(122);
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case Pose.HullDown: {
|
case Pose.HullDown: {
|
||||||
const [pitch] = applyPitchYawLimits(
|
const [pitch] = applyPitchYawLimits(
|
||||||
@@ -75,6 +78,7 @@ export function Controls() {
|
|||||||
0,
|
0,
|
||||||
protagonistGunModelDefinition.pitch,
|
protagonistGunModelDefinition.pitch,
|
||||||
protagonistTurretModelDefinition.yaw,
|
protagonistTurretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
|
|
||||||
camera.position
|
camera.position
|
||||||
@@ -106,6 +110,7 @@ export function Controls() {
|
|||||||
0,
|
0,
|
||||||
protagonistGunModelDefinition.pitch,
|
protagonistGunModelDefinition.pitch,
|
||||||
protagonistTurretModelDefinition.yaw,
|
protagonistTurretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
|
|
||||||
camera.position
|
camera.position
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { PerspectiveCamera } from '@react-three/drei';
|
|||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
||||||
import { isMobile } from 'react-device-detect';
|
import { isMobile } from 'react-device-detect';
|
||||||
import { ShotDisplay } from '../../../../../../components/ShotDisplay';
|
import { Armor } from '../../../../../../components/Armor';
|
||||||
|
import { ShotDisplay } from '../../../../../../components/Armor/components/ShotDisplay';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
||||||
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
||||||
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
|
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
|
||||||
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
|
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
|
||||||
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
@@ -24,13 +26,11 @@ import { RotationInputs } from '../QuickInputs';
|
|||||||
import { SceneProps } from '../SceneProps';
|
import { SceneProps } from '../SceneProps';
|
||||||
import { ModelLoader } from './components/ModelLoader';
|
import { ModelLoader } from './components/ModelLoader';
|
||||||
import { Options } from './components/Options';
|
import { Options } from './components/Options';
|
||||||
import { TankArmor } from './components/TankArmor';
|
|
||||||
import { ExternalModuleMask } from './components/TankArmor/components/ExternalModuleMask';
|
|
||||||
import { SpacedArmorDepth } from './components/TankArmor/components/SpacedArmorDepth';
|
|
||||||
import { TankModel } from './components/TankModel';
|
import { TankModel } from './components/TankModel';
|
||||||
|
|
||||||
export function TankSandbox() {
|
export function TankSandbox() {
|
||||||
const canvas = useRef<HTMLCanvasElement>(null);
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
const awaitedModelDefinitions = use(modelDefinitions);
|
const awaitedModelDefinitions = use(modelDefinitions);
|
||||||
const canvasWrapper = useRef<HTMLDivElement>(null);
|
const canvasWrapper = useRef<HTMLDivElement>(null);
|
||||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||||
@@ -72,6 +72,7 @@ export function TankSandbox() {
|
|||||||
0,
|
0,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
@@ -89,6 +90,7 @@ export function TankSandbox() {
|
|||||||
0,
|
0,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
@@ -106,6 +108,7 @@ export function TankSandbox() {
|
|||||||
0,
|
0,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
@@ -133,6 +136,7 @@ export function TankSandbox() {
|
|||||||
draft.protagonist!.yaw,
|
draft.protagonist!.yaw,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [protagonist.gun, protagonist.turret]);
|
}, [protagonist.gun, protagonist.turret]);
|
||||||
@@ -198,12 +202,11 @@ export function TankSandbox() {
|
|||||||
<SceneProps />
|
<SceneProps />
|
||||||
|
|
||||||
<Suspense fallback={<ModelLoader />}>
|
<Suspense fallback={<ModelLoader />}>
|
||||||
<ShotDisplay />
|
{/* TODO: add rotation cube to the top right */}
|
||||||
<Lighting />
|
<Lighting />
|
||||||
<TankModel />
|
<TankModel />
|
||||||
<TankArmor />
|
<Armor />
|
||||||
<ExternalModuleMask ornamental />
|
<ShotDisplay />
|
||||||
<SpacedArmorDepth ornamental />
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
const wireframe = useTankopediaPersistent(
|
const wireframe = useTankopediaPersistent(
|
||||||
(state) => state.model.visual.wireframe,
|
(state) => state.model.visual.wireframe,
|
||||||
);
|
);
|
||||||
|
const opaque = useTankopediaPersistent((state) => state.model.visual.opaque);
|
||||||
const fullScreenAvailable =
|
const fullScreenAvailable =
|
||||||
typeof document !== 'undefined' && document.fullscreenEnabled;
|
typeof document !== 'undefined' && document.fullscreenEnabled;
|
||||||
const environment = useTankopediaPersistent(
|
const environment = useTankopediaPersistent(
|
||||||
@@ -78,6 +79,8 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Label>Armor</DropdownMenu.Label>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={greenPenetration}
|
checked={greenPenetration}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
@@ -89,6 +92,30 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
Green penetration
|
Green penetration
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={wireframe}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.model.visual.wireframe = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Wireframe
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={opaque}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.model.visual.opaque = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Opaque
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
|
<DropdownMenu.Label>Environment</DropdownMenu.Label>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={showGrid}
|
checked={showGrid}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
@@ -132,17 +159,6 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
|
|||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={wireframe}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.model.visual.wireframe = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Wireframe
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)}
|
)}
|
||||||
|
|||||||
-286
@@ -1,286 +0,0 @@
|
|||||||
import { memo, useEffect, useRef } from 'react';
|
|
||||||
import { Euler, Group, Vector3 } from 'three';
|
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
|
||||||
import { ArmorMeshExternalModuleMask } from '../../../../../../../../../components/ArmorMesh';
|
|
||||||
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis';
|
|
||||||
import {
|
|
||||||
ModelTransformEventData,
|
|
||||||
modelTransformEvent,
|
|
||||||
} from '../../../../../../../../../core/blitzkrieg/modelTransform';
|
|
||||||
import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId';
|
|
||||||
import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness';
|
|
||||||
import { useArmor } from '../../../../../../../../../hooks/useArmor';
|
|
||||||
import { useModel } from '../../../../../../../../../hooks/useModel';
|
|
||||||
import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions';
|
|
||||||
import { useDuel } from '../../../../../../../../../stores/duel';
|
|
||||||
import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia';
|
|
||||||
|
|
||||||
interface ExternalModuleMaskProps {
|
|
||||||
ornamental?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When rendered, generates a mask and thickness buffer for external modules.
|
|
||||||
* - R: 1 means external module
|
|
||||||
* - G: normalized thickness; multiply with max thickness to get nominal thickness
|
|
||||||
* - B: no data
|
|
||||||
* - A: 1 means is armor; 0 is background
|
|
||||||
*/
|
|
||||||
export const ExternalModuleMask = memo<ExternalModuleMaskProps>(
|
|
||||||
({ ornamental = false }) => {
|
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
|
||||||
const wrapper = useRef<Group>(null);
|
|
||||||
const modelDefinitions = useModelDefinitions();
|
|
||||||
const turretContainer = useRef<Group>(null);
|
|
||||||
const gunContainer = useRef<Group>(null);
|
|
||||||
const initialTankopediaState = useTankopediaPersistent.getState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hullOrigin = new Vector3(
|
|
||||||
trackModelDefinition.origin[0],
|
|
||||||
trackModelDefinition.origin[1],
|
|
||||||
-trackModelDefinition.origin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretOrigin = new Vector3(
|
|
||||||
tankModelDefinition.turretOrigin[0],
|
|
||||||
tankModelDefinition.turretOrigin[1],
|
|
||||||
-tankModelDefinition.turretOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const gunOrigin = new Vector3(
|
|
||||||
turretModelDefinition.gunOrigin[0],
|
|
||||||
turretModelDefinition.gunOrigin[1],
|
|
||||||
-turretModelDefinition.gunOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretPosition = new Vector3();
|
|
||||||
const turretRotation = new Euler();
|
|
||||||
const gunPosition = new Vector3();
|
|
||||||
const gunRotation = new Euler();
|
|
||||||
|
|
||||||
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
|
||||||
gunPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.sub(gunOrigin)
|
|
||||||
.applyAxisAngle(I_HAT, pitch)
|
|
||||||
.add(gunOrigin)
|
|
||||||
.add(turretOrigin)
|
|
||||||
.add(hullOrigin);
|
|
||||||
gunRotation.set(pitch, 0, 0);
|
|
||||||
gunContainer.current?.position.copy(gunPosition);
|
|
||||||
gunContainer.current?.rotation.copy(gunRotation);
|
|
||||||
|
|
||||||
if (yaw === undefined) return;
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
|
||||||
turretRotation.set(0, 0, yaw);
|
|
||||||
|
|
||||||
if (tankModelDefinition.turretRotation) {
|
|
||||||
const initialPitch = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.pitch,
|
|
||||||
);
|
|
||||||
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
|
||||||
const initialRoll = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.roll,
|
|
||||||
);
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.applyAxisAngle(I_HAT, initialPitch)
|
|
||||||
.applyAxisAngle(J_HAT, initialRoll)
|
|
||||||
.applyAxisAngle(K_HAT, initialYaw);
|
|
||||||
turretRotation.x += initialPitch;
|
|
||||||
turretRotation.y += initialRoll;
|
|
||||||
turretRotation.z += initialYaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
turretPosition.add(turretOrigin).add(hullOrigin);
|
|
||||||
turretContainer.current?.position.copy(turretPosition);
|
|
||||||
turretContainer.current?.rotation.copy(turretRotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModelTransform(protagonist);
|
|
||||||
modelTransformEvent.on(handleModelTransform);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
modelTransformEvent.off(handleModelTransform);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.mode,
|
|
||||||
(mode) => {
|
|
||||||
if (wrapper.current) wrapper.current.visible = mode === 'armor';
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
});
|
|
||||||
|
|
||||||
const armorGltf = useArmor(protagonist.tank.id);
|
|
||||||
const { gltf: modelGltf } = useModel(protagonist.tank.id);
|
|
||||||
|
|
||||||
const armorNodes = Object.values(armorGltf.nodes);
|
|
||||||
const modelNodes = Object.values(modelGltf.nodes);
|
|
||||||
const tankModelDefinition = modelDefinitions[protagonist.tank.id];
|
|
||||||
const trackModelDefinition =
|
|
||||||
tankModelDefinition.tracks[protagonist.track.id];
|
|
||||||
const turretModelDefinition =
|
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
|
||||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
|
||||||
const maxThickness = Math.max(
|
|
||||||
trackModelDefinition.thickness,
|
|
||||||
gunModelDefinition.thickness,
|
|
||||||
...armorNodes
|
|
||||||
.map((node) => {
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
return resolveArmor(tankModelDefinition.armor, armorId).thickness;
|
|
||||||
})
|
|
||||||
.filter(Boolean),
|
|
||||||
);
|
|
||||||
const hullOrigin = new Vector3(
|
|
||||||
trackModelDefinition.origin[0],
|
|
||||||
trackModelDefinition.origin[1],
|
|
||||||
-trackModelDefinition.origin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretOrigin = new Vector3(
|
|
||||||
tankModelDefinition.turretOrigin[0],
|
|
||||||
tankModelDefinition.turretOrigin[1],
|
|
||||||
-tankModelDefinition.turretOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const gunOrigin = new Vector3(
|
|
||||||
turretModelDefinition.gunOrigin[0],
|
|
||||||
turretModelDefinition.gunOrigin[1],
|
|
||||||
-turretModelDefinition.gunOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group
|
|
||||||
ref={wrapper}
|
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
|
||||||
visible={initialTankopediaState.mode === 'armor'}
|
|
||||||
>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isHull = node.name.startsWith('hull_');
|
|
||||||
const isVisible = isHull;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { thickness, spaced } = resolveArmor(
|
|
||||||
tankModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined || spaced) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArmorMeshExternalModuleMask
|
|
||||||
ornamental={ornamental}
|
|
||||||
exclude
|
|
||||||
key={node.uuid}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
|
||||||
const isVisible = isWheel || isTrack;
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArmorMeshExternalModuleMask
|
|
||||||
ornamental={ornamental}
|
|
||||||
maxThickness={maxThickness}
|
|
||||||
thickness={trackModelDefinition.thickness}
|
|
||||||
key={node.uuid}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<group ref={turretContainer}>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentTurret = node.name.startsWith(
|
|
||||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentTurret;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { thickness, spaced } = resolveArmor(
|
|
||||||
turretModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined || spaced) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group key={node.uuid} position={turretOrigin}>
|
|
||||||
<ArmorMeshExternalModuleMask
|
|
||||||
ornamental={ornamental}
|
|
||||||
exclude
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group ref={gunContainer}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentGun = node.name.startsWith(
|
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { thickness, spaced } = resolveArmor(
|
|
||||||
gunModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined || spaced) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group
|
|
||||||
key={node.uuid}
|
|
||||||
position={turretOrigin.clone().add(gunOrigin)}
|
|
||||||
>
|
|
||||||
<ArmorMeshExternalModuleMask
|
|
||||||
ornamental={ornamental}
|
|
||||||
exclude
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
|
||||||
const isCurrentGun =
|
|
||||||
node.name ===
|
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArmorMeshExternalModuleMask
|
|
||||||
ornamental={ornamental}
|
|
||||||
maxThickness={maxThickness}
|
|
||||||
thickness={gunModelDefinition.thickness}
|
|
||||||
key={node.uuid}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
-299
@@ -1,299 +0,0 @@
|
|||||||
import { memo, useEffect, useRef } from 'react';
|
|
||||||
import { Euler, Group, Vector3 } from 'three';
|
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
|
||||||
import { ArmorMeshSpacedArmorDepth } from '../../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth';
|
|
||||||
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis';
|
|
||||||
import {
|
|
||||||
ModelTransformEventData,
|
|
||||||
modelTransformEvent,
|
|
||||||
} from '../../../../../../../../../core/blitzkrieg/modelTransform';
|
|
||||||
import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId';
|
|
||||||
import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness';
|
|
||||||
import { useArmor } from '../../../../../../../../../hooks/useArmor';
|
|
||||||
import { useModel } from '../../../../../../../../../hooks/useModel';
|
|
||||||
import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions';
|
|
||||||
import { useDuel } from '../../../../../../../../../stores/duel';
|
|
||||||
import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia';
|
|
||||||
|
|
||||||
interface SpacedArmorDepthProps {
|
|
||||||
ornamental?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When rendered, this component provides the depth buffer for the armor which
|
|
||||||
* when compared to the depth of the core armor will give you the distance
|
|
||||||
* between the first point of contact and the core armor. The color channels
|
|
||||||
* hold information about the thickness of the spaced armor (external modules
|
|
||||||
* not included).
|
|
||||||
* - R: angle of the armor where 0 is 0 and 1 is 90
|
|
||||||
* - G: normalized thickness; multiply with max thickness to get nominal thickness
|
|
||||||
* - B: no data
|
|
||||||
* - A: 1 means is armor; 0 is background
|
|
||||||
*/
|
|
||||||
export const SpacedArmorDepth = memo<SpacedArmorDepthProps>(
|
|
||||||
({ ornamental = false }) => {
|
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
|
||||||
const wrapper = useRef<Group>(null);
|
|
||||||
const modelDefinitions = useModelDefinitions();
|
|
||||||
const turretContainer = useRef<Group>(null);
|
|
||||||
const gunContainer = useRef<Group>(null);
|
|
||||||
const initialTankopediaState = useTankopediaPersistent.getState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!modelDefinitions) return;
|
|
||||||
|
|
||||||
const hullOrigin = new Vector3(
|
|
||||||
trackModelDefinition.origin[0],
|
|
||||||
trackModelDefinition.origin[1],
|
|
||||||
-trackModelDefinition.origin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretOrigin = new Vector3(
|
|
||||||
tankModelDefinition.turretOrigin[0],
|
|
||||||
tankModelDefinition.turretOrigin[1],
|
|
||||||
-tankModelDefinition.turretOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const gunOrigin = new Vector3(
|
|
||||||
turretModelDefinition.gunOrigin[0],
|
|
||||||
turretModelDefinition.gunOrigin[1],
|
|
||||||
-turretModelDefinition.gunOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretPosition = new Vector3();
|
|
||||||
const turretRotation = new Euler();
|
|
||||||
const gunPosition = new Vector3();
|
|
||||||
const gunRotation = new Euler();
|
|
||||||
|
|
||||||
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
|
||||||
gunPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.sub(gunOrigin)
|
|
||||||
.applyAxisAngle(I_HAT, pitch)
|
|
||||||
.add(gunOrigin)
|
|
||||||
.add(turretOrigin)
|
|
||||||
.add(hullOrigin);
|
|
||||||
gunRotation.set(pitch, 0, 0);
|
|
||||||
gunContainer.current?.position.copy(gunPosition);
|
|
||||||
gunContainer.current?.rotation.copy(gunRotation);
|
|
||||||
|
|
||||||
if (yaw === undefined) return;
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
|
||||||
turretRotation.set(0, 0, yaw);
|
|
||||||
|
|
||||||
if (tankModelDefinition.turretRotation) {
|
|
||||||
const initialPitch = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.pitch,
|
|
||||||
);
|
|
||||||
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
|
||||||
const initialRoll = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.roll,
|
|
||||||
);
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.applyAxisAngle(I_HAT, initialPitch)
|
|
||||||
.applyAxisAngle(J_HAT, initialRoll)
|
|
||||||
.applyAxisAngle(K_HAT, initialYaw);
|
|
||||||
turretRotation.x += initialPitch;
|
|
||||||
turretRotation.y += initialRoll;
|
|
||||||
turretRotation.z += initialYaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
turretPosition.add(turretOrigin).add(hullOrigin);
|
|
||||||
turretContainer.current?.position.copy(turretPosition);
|
|
||||||
turretContainer.current?.rotation.copy(turretRotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModelTransform(protagonist);
|
|
||||||
modelTransformEvent.on(handleModelTransform);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
modelTransformEvent.off(handleModelTransform);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.mode,
|
|
||||||
(mode) => {
|
|
||||||
if (wrapper.current) wrapper.current.visible = mode === 'armor';
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
});
|
|
||||||
|
|
||||||
const armorGltf = useArmor(protagonist.tank.id);
|
|
||||||
const { gltf: modelGltf } = useModel(protagonist.tank.id);
|
|
||||||
|
|
||||||
const armorNodes = Object.values(armorGltf.nodes);
|
|
||||||
const modelNodes = Object.values(modelGltf.nodes);
|
|
||||||
const tankModelDefinition = modelDefinitions[protagonist.tank.id];
|
|
||||||
const trackModelDefinition =
|
|
||||||
tankModelDefinition.tracks[protagonist.track.id];
|
|
||||||
const turretModelDefinition =
|
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
|
||||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
|
||||||
const maxThickness = Math.max(
|
|
||||||
trackModelDefinition.thickness,
|
|
||||||
gunModelDefinition.thickness,
|
|
||||||
...armorNodes
|
|
||||||
.map((node) => {
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
return resolveArmor(tankModelDefinition.armor, armorId).thickness;
|
|
||||||
})
|
|
||||||
.filter(Boolean),
|
|
||||||
);
|
|
||||||
const hullOrigin = new Vector3(
|
|
||||||
trackModelDefinition.origin[0],
|
|
||||||
trackModelDefinition.origin[1],
|
|
||||||
-trackModelDefinition.origin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretOrigin = new Vector3(
|
|
||||||
tankModelDefinition.turretOrigin[0],
|
|
||||||
tankModelDefinition.turretOrigin[1],
|
|
||||||
-tankModelDefinition.turretOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const gunOrigin = new Vector3(
|
|
||||||
turretModelDefinition.gunOrigin[0],
|
|
||||||
turretModelDefinition.gunOrigin[1],
|
|
||||||
-turretModelDefinition.gunOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group
|
|
||||||
ref={wrapper}
|
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
|
||||||
visible={initialTankopediaState.mode === 'armor'}
|
|
||||||
>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isHull = node.name.startsWith('hull_');
|
|
||||||
const isVisible = isHull;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
tankModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArmorMeshSpacedArmorDepth
|
|
||||||
ornamental={ornamental}
|
|
||||||
include={spaced}
|
|
||||||
isExternalModule={false}
|
|
||||||
thickness={thickness}
|
|
||||||
maxThickness={maxThickness}
|
|
||||||
key={node.uuid}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
{modelNodes.map((node) => {
|
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
|
||||||
const isVisible = isWheel || isTrack;
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArmorMeshSpacedArmorDepth
|
|
||||||
ornamental={ornamental}
|
|
||||||
isExternalModule
|
|
||||||
key={node.uuid}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<group ref={turretContainer}>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentTurret = node.name.startsWith(
|
|
||||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentTurret;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
turretModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group key={node.uuid} position={turretOrigin}>
|
|
||||||
<ArmorMeshSpacedArmorDepth
|
|
||||||
ornamental={ornamental}
|
|
||||||
include={spaced}
|
|
||||||
isExternalModule={false}
|
|
||||||
thickness={thickness}
|
|
||||||
maxThickness={maxThickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
<group ref={gunContainer}>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentGun = node.name.startsWith(
|
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
gunModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group
|
|
||||||
key={node.uuid}
|
|
||||||
position={turretOrigin.clone().add(gunOrigin)}
|
|
||||||
>
|
|
||||||
<ArmorMeshSpacedArmorDepth
|
|
||||||
ornamental={ornamental}
|
|
||||||
include={spaced}
|
|
||||||
isExternalModule={false}
|
|
||||||
thickness={thickness}
|
|
||||||
maxThickness={maxThickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
|
||||||
const isCurrentGun =
|
|
||||||
node.name ===
|
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArmorMeshSpacedArmorDepth
|
|
||||||
ornamental={ornamental}
|
|
||||||
isExternalModule
|
|
||||||
key={node.uuid}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { createPortal, useFrame } from '@react-three/fiber';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { DepthTexture, Scene, Vector2 } from 'three';
|
|
||||||
import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh';
|
|
||||||
import { spacedArmorDepthRenderTarget } from '../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth';
|
|
||||||
import { ArmorHighlighting } from './components/ArmorHighlighting';
|
|
||||||
import { ExternalModuleMask } from './components/ExternalModuleMask';
|
|
||||||
import { SpacedArmorDepth } from './components/SpacedArmorDepth';
|
|
||||||
|
|
||||||
export const TankArmor = memo(() => {
|
|
||||||
const externalModuleMaskScene = new Scene();
|
|
||||||
const spacedArmorDepthScene = new Scene();
|
|
||||||
const armorHighlightingScene = new Scene();
|
|
||||||
const externalModuleMaskPortal = createPortal(
|
|
||||||
<ExternalModuleMask />,
|
|
||||||
externalModuleMaskScene,
|
|
||||||
);
|
|
||||||
const spacedArmorDepthPortal = createPortal(
|
|
||||||
<SpacedArmorDepth />,
|
|
||||||
spacedArmorDepthScene,
|
|
||||||
);
|
|
||||||
const armorHighlightingPortal = createPortal(
|
|
||||||
<ArmorHighlighting />,
|
|
||||||
armorHighlightingScene,
|
|
||||||
);
|
|
||||||
const renderSize = new Vector2();
|
|
||||||
const newRenderSize = new Vector2();
|
|
||||||
|
|
||||||
useFrame(({ gl, scene, camera }) => {
|
|
||||||
const pixelRatio = gl.getPixelRatio();
|
|
||||||
gl.autoClear = true;
|
|
||||||
|
|
||||||
gl.getSize(newRenderSize).multiplyScalar(pixelRatio);
|
|
||||||
if (!newRenderSize.equals(renderSize)) {
|
|
||||||
renderSize.copy(newRenderSize);
|
|
||||||
spacedArmorDepthRenderTarget.depthTexture = new DepthTexture(
|
|
||||||
renderSize.x,
|
|
||||||
renderSize.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
externalModuleMaskRenderTarget.setSize(renderSize.x, renderSize.y);
|
|
||||||
gl.setRenderTarget(externalModuleMaskRenderTarget);
|
|
||||||
gl.render(externalModuleMaskScene, camera);
|
|
||||||
|
|
||||||
gl.clearDepth();
|
|
||||||
spacedArmorDepthRenderTarget.setSize(renderSize.x, renderSize.y * 0.5);
|
|
||||||
gl.setRenderTarget(spacedArmorDepthRenderTarget);
|
|
||||||
gl.render(spacedArmorDepthScene, camera);
|
|
||||||
|
|
||||||
gl.autoClear = false;
|
|
||||||
|
|
||||||
gl.setRenderTarget(null);
|
|
||||||
gl.render(scene, camera);
|
|
||||||
|
|
||||||
gl.clearDepth();
|
|
||||||
gl.render(armorHighlightingScene, camera);
|
|
||||||
}, 1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{externalModuleMaskPortal}
|
|
||||||
{spacedArmorDepthPortal}
|
|
||||||
{armorHighlightingPortal}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -4,13 +4,14 @@ import { Euler, Group, Vector3 } from 'three';
|
|||||||
import { degToRad } from 'three/src/math/MathUtils';
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
|
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
|
||||||
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
|
import { hasEquipment } from '../../../../../../../core/blitzkrieg/hasEquipment';
|
||||||
import { jsxTree } from '../../../../../../../core/blitzkrieg/jsxTree';
|
import { jsxTree } from '../../../../../../../core/blitzkrieg/jsxTree';
|
||||||
import { modelDefinitions } from '../../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../../core/blitzkrieg/modelDefinitions';
|
||||||
import {
|
import {
|
||||||
ModelTransformEventData,
|
ModelTransformEventData,
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
} from '../../../../../../../core/blitzkrieg/modelTransform';
|
} from '../../../../../../../core/blitzkrieg/modelTransform';
|
||||||
import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180';
|
import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngleRad';
|
||||||
import { useAwait } from '../../../../../../../hooks/useAwait';
|
import { useAwait } from '../../../../../../../hooks/useAwait';
|
||||||
import { useModel } from '../../../../../../../hooks/useModel';
|
import { useModel } from '../../../../../../../hooks/useModel';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
@@ -106,7 +107,6 @@ export const TankModel = memo(() => {
|
|||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
tankModelDefinition.turrets[protagonist.turret.id];
|
||||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
|
|
||||||
const { gltf } = useModel(protagonist.tank.id);
|
const { gltf } = useModel(protagonist.tank.id);
|
||||||
const nodes = Object.values(gltf.nodes);
|
const nodes = Object.values(gltf.nodes);
|
||||||
|
|
||||||
@@ -161,19 +161,21 @@ export const TankModel = memo(() => {
|
|||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handlePointerMove(event: PointerEvent) {
|
async function handlePointerMove(event: PointerEvent) {
|
||||||
|
const hasImprovedVerticalStabilizer = await hasEquipment(122);
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
[pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch,
|
pitch,
|
||||||
yaw + event.movementX * (Math.PI / canvas.width),
|
yaw + event.movementX * (Math.PI / canvas.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.protagonist!.pitch = normalizeAnglePI(pitch);
|
draft.protagonist!.pitch = normalizeAngleRad(pitch);
|
||||||
draft.protagonist!.yaw = normalizeAnglePI(yaw);
|
draft.protagonist!.yaw = normalizeAngleRad(yaw);
|
||||||
});
|
});
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.model.visual.controlsEnabled = true;
|
draft.model.visual.controlsEnabled = true;
|
||||||
@@ -220,12 +222,14 @@ export const TankModel = memo(() => {
|
|||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
}
|
}
|
||||||
function handlePointerMove(event: PointerEvent) {
|
async function handlePointerMove(event: PointerEvent) {
|
||||||
|
const hasImprovedVerticalStabilizer = await hasEquipment(122);
|
||||||
[pitch, yaw] = applyPitchYawLimits(
|
[pitch, yaw] = applyPitchYawLimits(
|
||||||
pitch - event.movementY * (Math.PI / canvas.height),
|
pitch - event.movementY * (Math.PI / canvas.height),
|
||||||
yaw + event.movementX * (Math.PI / canvas.width),
|
yaw + event.movementX * (Math.PI / canvas.width),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
}
|
}
|
||||||
@@ -234,8 +238,8 @@ export const TankModel = memo(() => {
|
|||||||
draft.model.visual.controlsEnabled = true;
|
draft.model.visual.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
mutateDuel((draft) => {
|
mutateDuel((draft) => {
|
||||||
draft.protagonist!.pitch = normalizeAnglePI(pitch);
|
draft.protagonist!.pitch = normalizeAngleRad(pitch);
|
||||||
draft.protagonist!.yaw = normalizeAnglePI(yaw);
|
draft.protagonist!.yaw = normalizeAngleRad(yaw);
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Flex, TextField } from '@radix-ui/themes';
|
import { Flex, TextField } from '@radix-ui/themes';
|
||||||
import { use, useEffect, useRef } from 'react';
|
import { use, useEffect, useRef } from 'react';
|
||||||
|
import { degToRad, radToDeg } from 'three/src/math/MathUtils';
|
||||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||||
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
|
||||||
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
|
||||||
@@ -20,22 +21,21 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
const mode = useTankopediaPersistent((state) => state.mode);
|
const mode = useTankopediaPersistent((state) => state.mode);
|
||||||
const hasCalibratedShells = useEquipment(103, true);
|
const hasCalibratedShells = useEquipment(103, true);
|
||||||
const hasEnhancedArmor = useEquipment(110);
|
const hasEnhancedArmor = useEquipment(110);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
turretYawInput.current!.value = `${-Math.round(
|
|
||||||
protagonist.yaw * (180 / Math.PI),
|
|
||||||
)}`;
|
|
||||||
}, [protagonist.yaw]);
|
|
||||||
useEffect(() => {
|
|
||||||
gunPitchInput.current!.value = `${-Math.round(
|
|
||||||
protagonist.pitch * (180 / Math.PI),
|
|
||||||
)}`;
|
|
||||||
}, [protagonist.pitch]);
|
|
||||||
|
|
||||||
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
||||||
const turretModelDefinition =
|
const turretModelDefinition =
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
tankModelDefinition.turrets[protagonist.turret.id];
|
||||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||||
|
const initialGunPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
|
||||||
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
|
||||||
|
}, [protagonist.yaw]);
|
||||||
|
useEffect(() => {
|
||||||
|
gunPitchInput.current!.value = (
|
||||||
|
-radToDeg(protagonist.pitch) + initialGunPitch
|
||||||
|
).toFixed(1);
|
||||||
|
}, [protagonist.pitch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -68,22 +68,22 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
Yaw
|
Yaw
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
defaultValue={Math.round(protagonist.yaw * (180 / Math.PI))}
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
protagonist.pitch,
|
protagonist.pitch,
|
||||||
-Number(turretYawInput.current?.value) * (Math.PI / 180),
|
degToRad(Number(turretYawInput.current!.value)),
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
mutateDuel((state) => {
|
mutateDuel((state) => {
|
||||||
state.protagonist!.pitch = pitch;
|
state.protagonist!.pitch = pitch;
|
||||||
state.protagonist!.yaw = yaw;
|
state.protagonist!.yaw = yaw;
|
||||||
});
|
});
|
||||||
turretYawInput.current!.value = `${Math.round(
|
turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(
|
||||||
yaw * (180 / Math.PI),
|
1,
|
||||||
)}`;
|
);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
@@ -116,22 +116,24 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
|
|||||||
Pitch
|
Pitch
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
defaultValue={-Math.round(protagonist.pitch * (180 / Math.PI))}
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
const [pitch, yaw] = applyPitchYawLimits(
|
const [pitch, yaw] = applyPitchYawLimits(
|
||||||
-Number(gunPitchInput.current?.value) * (Math.PI / 180),
|
degToRad(
|
||||||
|
-Number(gunPitchInput.current!.value) + initialGunPitch,
|
||||||
|
),
|
||||||
protagonist.yaw,
|
protagonist.yaw,
|
||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
|
hasImprovedVerticalStabilizer,
|
||||||
);
|
);
|
||||||
modelTransformEvent.emit({ pitch, yaw });
|
modelTransformEvent.emit({ pitch, yaw });
|
||||||
mutateDuel((state) => {
|
mutateDuel((state) => {
|
||||||
state.protagonist!.pitch = pitch;
|
state.protagonist!.pitch = pitch;
|
||||||
state.protagonist!.yaw = yaw;
|
state.protagonist!.yaw = yaw;
|
||||||
});
|
});
|
||||||
gunPitchInput.current!.value = `${Math.round(
|
gunPitchInput.current!.value = (
|
||||||
pitch * (180 / Math.PI),
|
-radToDeg(protagonist.pitch) + initialGunPitch
|
||||||
)}`;
|
).toFixed(1);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
import { Color } from 'three';
|
import { useFrame, useLoader } from '@react-three/fiber';
|
||||||
import InfiniteGridHelper from '../../../../../components/InfiniteGridHelper';
|
import { clamp } from 'lodash';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { MeshStandardMaterial, TextureLoader } from 'three';
|
||||||
import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
|
import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
|
||||||
|
|
||||||
export function SceneProps() {
|
export function SceneProps() {
|
||||||
const showGrid = useTankopediaPersistent(
|
const show = useTankopediaPersistent(
|
||||||
(state) => state.model.visual.showGrid,
|
(state) =>
|
||||||
);
|
state.model.visual.showGrid && !state.model.visual.showEnvironment,
|
||||||
const showEnvironment = useTankopediaPersistent(
|
|
||||||
(state) => state.model.visual.showEnvironment,
|
|
||||||
);
|
);
|
||||||
|
const texture = useLoader(TextureLoader, 'https://i.imgur.com/C28Z8nU.png');
|
||||||
|
const material = useRef<MeshStandardMaterial>(null);
|
||||||
|
|
||||||
if (!showGrid || showEnvironment) return null;
|
useFrame(({ camera }) => {
|
||||||
|
if (!material.current) return;
|
||||||
|
material.current.opacity = clamp(0.1 * camera.position.y, 0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
texture.anisotropy = 2;
|
||||||
|
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
<InfiniteGridHelper
|
<planeGeometry args={[10, 10]} />
|
||||||
size1={1 / 5}
|
<meshStandardMaterial ref={material} map={texture} transparent />
|
||||||
size2={1}
|
</mesh>
|
||||||
distance={20}
|
|
||||||
color={new Color('#ffffff')}
|
|
||||||
/>
|
|
||||||
<InfiniteGridHelper
|
|
||||||
position={[0, 1e-4, 0]}
|
|
||||||
size1={0}
|
|
||||||
size2={100}
|
|
||||||
distance={25}
|
|
||||||
color={new Color('red')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export function Title() {
|
|||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
TREE_TYPE_ICONS[protagonist.tank.treeType][
|
TREE_TYPE_ICONS[protagonist.tank.treeType][
|
||||||
protagonist.tank.type
|
protagonist.tank.class
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
};
|
};
|
||||||
}, [initialId, assigned]);
|
}, [initialId, assigned]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="purple" size="double">
|
<PageWrapper color="purple" size="double">
|
||||||
{assigned && (
|
{assigned && (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
|||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { Flag } from '../../../../../../../components/Flag';
|
import { Flag } from '../../../../../../../components/Flag';
|
||||||
import {
|
import {
|
||||||
TANK_TYPES,
|
TANK_CLASSES,
|
||||||
TREE_TYPES,
|
TREE_TYPES,
|
||||||
TREE_TYPE_ICONS,
|
TREE_TYPE_ICONS,
|
||||||
TREE_TYPE_IMAGES,
|
TREE_TYPE_IMAGES,
|
||||||
@@ -121,7 +121,7 @@ export function Options() {
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex>
|
<Flex>
|
||||||
{[...TANK_TYPES].map((type, index) => (
|
{[...TANK_CLASSES].map((type, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={type}
|
key={type}
|
||||||
color={filters.types.includes(type) ? undefined : 'gray'}
|
color={filters.types.includes(type) ? undefined : 'gray'}
|
||||||
@@ -132,9 +132,9 @@ export function Options() {
|
|||||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||||
borderTopRightRadius:
|
borderTopRightRadius:
|
||||||
index === TANK_TYPES.length - 1 ? undefined : 0,
|
index === TANK_CLASSES.length - 1 ? undefined : 0,
|
||||||
borderBottomRightRadius:
|
borderBottomRightRadius:
|
||||||
index === TANK_TYPES.length - 1 ? undefined : 0,
|
index === TANK_CLASSES.length - 1 ? undefined : 0,
|
||||||
}}
|
}}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import { use, useEffect, useMemo, useRef, useState } from 'react';
|
import { use, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { TankType, TreeType } from '../../../../../components/Tanks';
|
import { TANK_CLASSES, TreeType } from '../../../../../components/Tanks';
|
||||||
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
|
||||||
import { asset } from '../../../../../core/blitzkrieg/asset';
|
import { asset } from '../../../../../core/blitzkrieg/asset';
|
||||||
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
||||||
@@ -49,7 +49,6 @@ interface TankSearchProps {
|
|||||||
onSelect?: (tank: TankDefinition) => void;
|
onSelect?: (tank: TankDefinition) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tankTypeOrder: TankType[] = ['light', 'medium', 'heavy', 'tankDestroyer'];
|
|
||||||
const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector'];
|
const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector'];
|
||||||
|
|
||||||
export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
||||||
@@ -70,7 +69,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
)
|
)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(tankTypeOrder.indexOf(a.type) - tankTypeOrder.indexOf(b.type)) *
|
(TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class)) *
|
||||||
(sort.direction === 'descending' ? -1 : 1),
|
(sort.direction === 'descending' ? -1 : 1),
|
||||||
)
|
)
|
||||||
.sort(
|
.sort(
|
||||||
@@ -89,7 +88,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
: filters.tiers.includes(tank.tier)) &&
|
: filters.tiers.includes(tank.tier)) &&
|
||||||
(filters.types.length === 0
|
(filters.types.length === 0
|
||||||
? true
|
? true
|
||||||
: filters.types.includes(tank.type)) &&
|
: filters.types.includes(tank.class)) &&
|
||||||
(filters.treeTypes.length === 0
|
(filters.treeTypes.length === 0
|
||||||
? true
|
? true
|
||||||
: filters.treeTypes.includes(tank.treeType)) &&
|
: filters.treeTypes.includes(tank.treeType)) &&
|
||||||
@@ -844,7 +843,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
|
|||||||
? TANK_ICONS_COLLECTOR
|
? TANK_ICONS_COLLECTOR
|
||||||
: tank.treeType === 'premium'
|
: tank.treeType === 'premium'
|
||||||
? TANK_ICONS_PREMIUM
|
? TANK_ICONS_PREMIUM
|
||||||
: TANK_ICONS)[tank.type]
|
: TANK_ICONS)[tank.class]
|
||||||
}
|
}
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: '1em', height: '1em' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+39
-37
@@ -1,67 +1,69 @@
|
|||||||
import {
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
SlashCommandBuilder,
|
import { translator } from '../core/localization/translator';
|
||||||
SlashCommandSubcommandsOnlyBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
|
import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
|
const subcommands = [
|
||||||
const DOCS = {
|
'embeds',
|
||||||
|
'introduction',
|
||||||
|
'invite',
|
||||||
|
'timezones',
|
||||||
|
'creators',
|
||||||
|
] as const;
|
||||||
|
type Subcommand = (typeof subcommands)[number];
|
||||||
|
|
||||||
|
const DOCS: Record<Subcommand, string> = {
|
||||||
embeds: 'guide/embeds',
|
embeds: 'guide/embeds',
|
||||||
introduction: 'guide/introduction',
|
introduction: 'guide/introduction',
|
||||||
invite: 'guide/invite',
|
invite: 'guide/invite',
|
||||||
timezones: 'guide/timezones',
|
timezones: 'guide/timezones',
|
||||||
creators: 'guide/creators',
|
creators: 'guide/creators',
|
||||||
};
|
};
|
||||||
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
|
||||||
embeds: 'How embeds work',
|
|
||||||
introduction: 'About the bot',
|
|
||||||
invite: 'How to invite the bot',
|
|
||||||
timezones: 'How Blitzkrieg infers and uses your timezone',
|
|
||||||
creators: 'Who created the bot',
|
|
||||||
};
|
|
||||||
|
|
||||||
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
Object.entries(DOC_DESCRIPTIONS).forEach(([key, value]) => {
|
resolve({
|
||||||
option.addSubcommand((sub) => sub.setName(key).setDescription(value));
|
|
||||||
});
|
|
||||||
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const aboutCommand: CommandRegistry = {
|
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: addDocsSubcommands(
|
command: createLocalizedCommand('about', [
|
||||||
new SlashCommandBuilder()
|
{ subcommand: 'commands' },
|
||||||
.setName('about')
|
...subcommands.map((subcommand) => ({ subcommand })),
|
||||||
.setDescription('All the info you need about the bot')
|
]),
|
||||||
.addSubcommand((option) =>
|
|
||||||
option
|
|
||||||
.setName('commands')
|
|
||||||
.setDescription('List of all commands and their descriptions'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
const { t } = translator(interaction.locale);
|
||||||
|
|
||||||
if (subcommand === 'commands') {
|
if (subcommand === 'commands') {
|
||||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
return t`bot.commands.about.subcommands.commands.body${(
|
||||||
await Promise.all(COMMANDS_RAW)
|
await Promise.all(COMMANDS_RAW)
|
||||||
)
|
)
|
||||||
.filter((registry) => registry.inPublic && registry.inProduction)
|
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||||
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
|
.sort((a, b) =>
|
||||||
|
(a.command.name_localizations?.[interaction.locale] ??
|
||||||
|
a.command.name) <
|
||||||
|
(b.command.name_localizations?.[interaction.locale] ??
|
||||||
|
b.command.name)
|
||||||
|
? -1
|
||||||
|
: 1,
|
||||||
|
)
|
||||||
.map(
|
.map(
|
||||||
(registry) =>
|
(registry) =>
|
||||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
`- \`/${
|
||||||
|
registry.command.name_localizations?.[interaction.locale] ??
|
||||||
|
registry.command.name
|
||||||
|
}\`: ${
|
||||||
|
registry.command.description_localizations?.[
|
||||||
|
interaction.locale
|
||||||
|
] ?? registry.command.description
|
||||||
|
}`,
|
||||||
)
|
)
|
||||||
.join('\n')}`;
|
.join('\n')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${RAW_PATH}${DOCS[subcommand as keyof typeof DOCS]}.md`;
|
const url = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/${DOCS[subcommand as Subcommand]}.md`;
|
||||||
const content = fetch(url).then((response) => response.text());
|
const content = fetch(url).then((response) => response.text());
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+51
-38
@@ -1,9 +1,9 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
import { chunk } from 'lodash';
|
import { chunk } from 'lodash';
|
||||||
import * as Breakdown from '../components/Breakdown';
|
import * as Breakdown from '../components/Breakdown';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
|
import NoData from '../components/NoData';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
|
||||||
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
@@ -19,8 +19,9 @@ import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
|
|||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import buttonPrimary from '../core/discord/buttonPrimary';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import commandToURL from '../core/discord/commandToURL';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
@@ -32,10 +33,11 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
|||||||
import resolvePlayerFromCommand, {
|
import resolvePlayerFromCommand, {
|
||||||
ResolvedPlayer,
|
ResolvedPlayer,
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import calculateWN8 from '../core/statistics/calculateWN8';
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
||||||
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
const ROWS_PER_PAGE = 8;
|
const ROWS_PER_PAGE = 8;
|
||||||
const MAX_PAGES = 9;
|
const MAX_PAGES = 9;
|
||||||
@@ -44,15 +46,17 @@ export async function renderBreakdown(
|
|||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
{ start, end, name }: ResolvedPeriod,
|
{ start, end, name }: ResolvedPeriod,
|
||||||
filters: StatFilters,
|
filters: StatFilters,
|
||||||
|
locale: Locale,
|
||||||
) {
|
) {
|
||||||
|
const { t, translate } = translator(locale);
|
||||||
const awaitedTankDefinitions = await tankDefinitions;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const awaitedTankAverages = await tankAverages;
|
const awaitedTankAverages = await tankAverages;
|
||||||
const statsInPeriod = await getStatsInPeriod(region, id, start, end);
|
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, locale);
|
||||||
const filterDescriptions = await filtersToDescription(filters);
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
const orderedCurrentStats: AllStats[] = [];
|
const orderedCurrentStats: AllStats[] = [];
|
||||||
const orderedCareerStats: AllStats[] = [];
|
const orderedCareerStats: AllStats[] = [];
|
||||||
const orderedCurrentWN8: (number | undefined)[] = [];
|
const orderedCurrentWN8: (number | undefined)[] = [];
|
||||||
@@ -144,21 +148,21 @@ export async function renderBreakdown(
|
|||||||
<Breakdown.Row
|
<Breakdown.Row
|
||||||
key={id}
|
key={id}
|
||||||
type="summary"
|
type="summary"
|
||||||
title="Total"
|
title={t`bot.commands.breakdown.body.total`}
|
||||||
stats={[
|
stats={[
|
||||||
{
|
{
|
||||||
title: 'Battles',
|
title: t`bot.commands.breakdown.body.battles`,
|
||||||
current: currentBattles,
|
current: currentBattles,
|
||||||
career: careerBattles,
|
career: careerBattles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Winrate',
|
title: t`bot.commands.breakdown.body.winrate`,
|
||||||
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
||||||
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||||
delta: currentWinrate - careerWinrate,
|
delta: currentWinrate - careerWinrate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'WN8',
|
title: t`bot.commands.breakdown.body.wn8`,
|
||||||
current:
|
current:
|
||||||
currentWN8 === undefined
|
currentWN8 === undefined
|
||||||
? undefined
|
? undefined
|
||||||
@@ -173,7 +177,7 @@ export async function renderBreakdown(
|
|||||||
: getWN8Percentile(currentWN8),
|
: getWN8Percentile(currentWN8),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Damage',
|
title: t`bot.commands.breakdown.body.damage`,
|
||||||
current: Math.round(currentDamage).toLocaleString(),
|
current: Math.round(currentDamage).toLocaleString(),
|
||||||
career: Math.round(careerDamage).toLocaleString(),
|
career: Math.round(careerDamage).toLocaleString(),
|
||||||
delta: currentDamage - careerDamage,
|
delta: currentDamage - careerDamage,
|
||||||
@@ -195,17 +199,20 @@ export async function renderBreakdown(
|
|||||||
<Breakdown.Row
|
<Breakdown.Row
|
||||||
key={id}
|
key={id}
|
||||||
type="tank"
|
type="tank"
|
||||||
tankType={tankDefinition?.type}
|
tankType={tankDefinition?.class}
|
||||||
treeType={tankDefinition?.treeType}
|
treeType={tankDefinition?.treeType}
|
||||||
title={tankDefinition?.name ?? `Unknown tank ${id}`}
|
title={
|
||||||
|
tankDefinition?.name ??
|
||||||
|
translate('bot.commands.breakdown.body.unknown_tank', [`${id}`])
|
||||||
|
}
|
||||||
stats={[
|
stats={[
|
||||||
{
|
{
|
||||||
title: 'Battles',
|
title: t`bot.commands.breakdown.body.battles`,
|
||||||
current: current.battles.toLocaleString(),
|
current: current.battles.toLocaleString(),
|
||||||
career: career.battles.toLocaleString(),
|
career: career.battles.toLocaleString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Winrate',
|
title: t`bot.commands.breakdown.body.winrate`,
|
||||||
current: `${(100 * (current.wins / current.battles)).toFixed(
|
current: `${(100 * (current.wins / current.battles)).toFixed(
|
||||||
2,
|
2,
|
||||||
)}%`,
|
)}%`,
|
||||||
@@ -214,7 +221,7 @@ export async function renderBreakdown(
|
|||||||
current.wins / current.battles - career.wins / career.battles,
|
current.wins / current.battles - career.wins / career.battles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'WN8',
|
title: t`bot.commands.breakdown.body.wn8`,
|
||||||
current:
|
current:
|
||||||
currentWN8 === undefined
|
currentWN8 === undefined
|
||||||
? undefined
|
? undefined
|
||||||
@@ -229,7 +236,7 @@ export async function renderBreakdown(
|
|||||||
: getWN8Percentile(currentWN8),
|
: getWN8Percentile(currentWN8),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Damage',
|
title: t`bot.commands.breakdown.body.damage`,
|
||||||
current: Math.round(
|
current: Math.round(
|
||||||
current.damage_dealt / current.battles,
|
current.damage_dealt / current.battles,
|
||||||
).toLocaleString(),
|
).toLocaleString(),
|
||||||
@@ -254,10 +261,10 @@ export async function renderBreakdown(
|
|||||||
if (filteredOrder.length > 0) {
|
if (filteredOrder.length > 0) {
|
||||||
return pages
|
return pages
|
||||||
.map((page, index) => (
|
.map((page, index) => (
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo.nickname}
|
title={accountInfo.nickname}
|
||||||
image={
|
image={
|
||||||
clanData?.clan
|
clanData?.clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||||
@@ -268,14 +275,14 @@ export async function renderBreakdown(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Breakdown.Root>{page}</Breakdown.Root>
|
<Breakdown.Root>{page}</Breakdown.Root>
|
||||||
</Wrapper>
|
</CommandWrapper>
|
||||||
))
|
))
|
||||||
.reverse();
|
.reverse();
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo.nickname}
|
title={accountInfo.nickname}
|
||||||
image={
|
image={
|
||||||
clanData?.clan
|
clanData?.clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||||
@@ -284,26 +291,22 @@ export async function renderBreakdown(
|
|||||||
description={`${name} • ${filterDescriptions}`}
|
description={`${name} • ${filterDescriptions}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NoData type={NoDataType.BattlesInPeriod} />
|
<NoData type="battles_in_period" locale={locale} />
|
||||||
</Wrapper>,
|
</CommandWrapper>,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const breakdownCommand = new Promise<CommandRegistryPromisable>(
|
export const breakdownCommand = new Promise<CommandRegistry>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addPeriodicFilterOptions(
|
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('breakdown')
|
|
||||||
.setDescription("A period's breakdown by tanks played"),
|
|
||||||
(option) => option.addStringOption(addUsernameChoices),
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command,
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand('breakdown'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
@@ -316,8 +319,13 @@ export const breakdownCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(await renderBreakdown(player, period, filters)),
|
...(await renderBreakdown(
|
||||||
buttonPrimary(path, 'Refresh'),
|
player,
|
||||||
|
period,
|
||||||
|
filters,
|
||||||
|
interaction.locale,
|
||||||
|
)),
|
||||||
|
buttonRefresh(interaction, path),
|
||||||
await getBlitzStarsLinkButton(player.region, player.id),
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -332,7 +340,12 @@ export const breakdownCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
const filters = getFiltersFromButton(interaction);
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await renderBreakdown(player, period, filters);
|
return await renderBreakdown(
|
||||||
|
player,
|
||||||
|
period,
|
||||||
|
filters,
|
||||||
|
interaction.locale,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+18
-14
@@ -1,36 +1,40 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
|
||||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||||
import { client } from '../core/discord/client';
|
import { client } from '../core/discord/client';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
const executionStart = new Date().getTime();
|
const executionStart = new Date().getTime();
|
||||||
|
|
||||||
export const debugCommand: CommandRegistry = {
|
export const debugCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('debug'),
|
||||||
.setName('debug')
|
|
||||||
.setDescription('Debug information about the bot'),
|
|
||||||
|
|
||||||
async handler() {
|
async handler(interaction) {
|
||||||
|
const { t } = translator(interaction.locale);
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
const uptime = currentTime - executionStart;
|
const uptime = currentTime - executionStart;
|
||||||
const list = [
|
const list = [
|
||||||
['Version', packageJSON.version],
|
[t`bot.commands.debug.body.version`, packageJSON.version],
|
||||||
['Shard', client.shard?.ids[0] ?? 'default'],
|
|
||||||
[
|
[
|
||||||
'Uptime',
|
t`bot.commands.debug.body.shard`,
|
||||||
|
client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
t`bot.commands.debug.body.uptime`,
|
||||||
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
|
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
|
||||||
(uptime / 1000 / 60) % 60,
|
(uptime / 1000 / 60) % 60,
|
||||||
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
|
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
|
||||||
uptime % 1000,
|
uptime % 1000,
|
||||||
)}ms`,
|
)}ms`,
|
||||||
],
|
],
|
||||||
]
|
];
|
||||||
|
return t`bot.commands.debug.body.title${list
|
||||||
.map(([key, value]) => `- ${key}: ${value}`)
|
.map(([key, value]) => `- ${key}: ${value}`)
|
||||||
.join('\n');
|
.join('\n')}`;
|
||||||
|
|
||||||
return `# Debug\n\n${list}`;
|
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+185
-178
@@ -1,198 +1,205 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
|
||||||
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
// | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
|
||||||
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
// | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
|
||||||
import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions';
|
// _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
// |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
|
||||||
import embedPositive from '../core/discord/embedPositive';
|
|
||||||
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
|
||||||
import calculateWN8 from '../core/statistics/calculateWN8';
|
|
||||||
import sumStats from '../core/statistics/sumStats';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
|
||||||
|
|
||||||
export type SkilledClan = 'SKLLD' | 'SMRI';
|
// import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
// import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
||||||
|
// import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
||||||
|
// import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions';
|
||||||
|
// import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
// import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
// import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
// import embedNegative from '../core/discord/embedNegative';
|
||||||
|
// import embedPositive from '../core/discord/embedPositive';
|
||||||
|
// import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||||
|
// import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
// import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
|
// import sumStats from '../core/statistics/sumStats';
|
||||||
|
// import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
// import { PossiblyPromise } from '../types/possiblyPromise';
|
||||||
|
|
||||||
export const SKILLED_CLANS: Record<SkilledClan, string> = {
|
// export type SkilledClan = 'SKLLD' | 'SMRI';
|
||||||
SKLLD: 'Skilled',
|
|
||||||
SMRI: 'Samurai',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const eligibleCommand: CommandRegistry = {
|
// export const SKILLED_CLANS: Record<SkilledClan, string> = {
|
||||||
inProduction: true,
|
// SKLLD: 'Skilled',
|
||||||
inPublic: false,
|
// SMRI: 'Samurai',
|
||||||
|
// };
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
// export const eligibleCommand: CommandRegistry = {
|
||||||
.setName('eligible')
|
// inProduction: true,
|
||||||
.setDescription('Checks eligibility for any Skilled clan')
|
// inPublic: false,
|
||||||
.addStringOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('clan')
|
|
||||||
.setDescription('The clan to check')
|
|
||||||
.setChoices(
|
|
||||||
{
|
|
||||||
name: SKILLED_CLANS.SKLLD,
|
|
||||||
value: 'SKLLD' satisfies SkilledClan,
|
|
||||||
},
|
|
||||||
{ name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan },
|
|
||||||
)
|
|
||||||
.setRequired(true),
|
|
||||||
)
|
|
||||||
.addStringOption(addUsernameChoices),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
// command: new SlashCommandBuilder()
|
||||||
const clan = interaction.options.getString('clan') as SkilledClan;
|
// .setName('eligible')
|
||||||
const { id, region: region } = await resolvePlayerFromCommand(interaction);
|
// .setDescription('Checks eligibility for any Skilled clan')
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
// .addStringOption((option) =>
|
||||||
const problems: TableInput = [];
|
// option
|
||||||
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
// .setName('clan')
|
||||||
let isEligible = false;
|
// .setDescription('The clan to check')
|
||||||
let body = '';
|
// .setChoices(
|
||||||
|
// {
|
||||||
|
// name: SKILLED_CLANS.SKLLD,
|
||||||
|
// value: 'SKLLD' satisfies SkilledClan,
|
||||||
|
// },
|
||||||
|
// { name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan },
|
||||||
|
// )
|
||||||
|
// .setRequired(true),
|
||||||
|
// )
|
||||||
|
// .addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
if (clan === 'SKLLD') {
|
// async handler(interaction) {
|
||||||
const { diff: diffed } = await getStatsInPeriod(
|
// const clan = interaction.options.getString('clan') as SkilledClan;
|
||||||
region,
|
// const { id, region: region } = await resolvePlayerFromCommand(interaction);
|
||||||
id,
|
// const accountInfo = await getAccountInfo(region, id);
|
||||||
getTimeDaysAgo(region, 30),
|
// const problems: TableInput = [];
|
||||||
getPeriodNow(),
|
// const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
||||||
);
|
// let isEligible = false;
|
||||||
const entries = Object.entries(diffed);
|
// let body = '';
|
||||||
const stats = sumStats(entries.map(([, stats]) => stats));
|
|
||||||
const averageTier =
|
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
|
||||||
const tankId = parseInt(tankIdString);
|
|
||||||
const tankDefinition = (await tankDefinitions)[tankId];
|
|
||||||
|
|
||||||
if (!tankDefinition) return accumulator;
|
// if (clan === 'SKLLD') {
|
||||||
|
// const { diff: diffed } = await getStatsInPeriod(
|
||||||
|
// region,
|
||||||
|
// id,
|
||||||
|
// getTimeDaysAgo(region, 30),
|
||||||
|
// getPeriodNow(),
|
||||||
|
// );
|
||||||
|
// const entries = Object.entries(diffed);
|
||||||
|
// const stats = sumStats(entries.map(([, stats]) => stats));
|
||||||
|
// const averageTier =
|
||||||
|
// (await entries.reduce<PossiblyPromise<number>>(
|
||||||
|
// async (accumulator, [tankIdString, stats]) => {
|
||||||
|
// const tankId = parseInt(tankIdString);
|
||||||
|
// const tankDefinition = (await tankDefinitions)[tankId];
|
||||||
|
|
||||||
const tankTier = tankDefinition.tier;
|
// if (!tankDefinition) return accumulator;
|
||||||
|
|
||||||
return (await accumulator) + tankTier * stats.battles;
|
// const tankTier = tankDefinition.tier;
|
||||||
},
|
|
||||||
0,
|
|
||||||
)) /
|
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
|
||||||
const tankId = parseInt(tankIdString);
|
|
||||||
const tankDefinition = (await tankDefinitions)[tankId];
|
|
||||||
|
|
||||||
if (!tankDefinition) return accumulator;
|
// return (await accumulator) + tankTier * stats.battles;
|
||||||
|
// },
|
||||||
|
// 0,
|
||||||
|
// )) /
|
||||||
|
// (await entries.reduce<PossiblyPromise<number>>(
|
||||||
|
// async (accumulator, [tankIdString, stats]) => {
|
||||||
|
// const tankId = parseInt(tankIdString);
|
||||||
|
// const tankDefinition = (await tankDefinitions)[tankId];
|
||||||
|
|
||||||
return stats.battles;
|
// if (!tankDefinition) return accumulator;
|
||||||
},
|
|
||||||
0,
|
|
||||||
));
|
|
||||||
const WN8 =
|
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
|
||||||
const tankId = parseInt(tankIdString);
|
|
||||||
|
|
||||||
// edge case where new tanks don't have averages
|
// return stats.battles;
|
||||||
if ((await tankAverages)[tankId]) {
|
// },
|
||||||
const tankWN8 = calculateWN8(
|
// 0,
|
||||||
(await tankAverages)[tankId].all,
|
// ));
|
||||||
stats,
|
// const WN8 =
|
||||||
);
|
// (await entries.reduce<PossiblyPromise<number>>(
|
||||||
|
// async (accumulator, [tankIdString, stats]) => {
|
||||||
|
// const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
if (isNaN(tankWN8)) return accumulator;
|
// // edge case where new tanks don't have averages
|
||||||
return (await accumulator) + tankWN8 * stats.battles;
|
// if ((await tankAverages)[tankId]) {
|
||||||
} else return accumulator;
|
// const tankWN8 = calculateWN8(
|
||||||
},
|
// (await tankAverages)[tankId].all,
|
||||||
0,
|
// stats,
|
||||||
)) / stats.battles;
|
// );
|
||||||
const tier10Damage =
|
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
|
||||||
const tankId = parseInt(tankIdString);
|
|
||||||
|
|
||||||
if ((await tankDefinitions)[tankId]?.tier === 10) {
|
// if (isNaN(tankWN8)) return accumulator;
|
||||||
const tankStats = diffed[tankId];
|
// return (await accumulator) + tankWN8 * stats.battles;
|
||||||
return (await accumulator) + tankStats.damage_dealt;
|
// } else return accumulator;
|
||||||
} else return accumulator;
|
// },
|
||||||
},
|
// 0,
|
||||||
0,
|
// )) / stats.battles;
|
||||||
)) /
|
// const tier10Damage =
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
// (await entries.reduce<PossiblyPromise<number>>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
// async (accumulator, [tankIdString, stats]) => {
|
||||||
const tankId = parseInt(tankIdString);
|
// const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
if ((await tankDefinitions)[tankId]?.tier === 10) {
|
// if ((await tankDefinitions)[tankId]?.tier === 10) {
|
||||||
const tankStats = diffed[tankId];
|
// const tankStats = diffed[tankId];
|
||||||
return (await accumulator) + tankStats.battles;
|
// return (await accumulator) + tankStats.damage_dealt;
|
||||||
} else return accumulator;
|
// } else return accumulator;
|
||||||
},
|
// },
|
||||||
0,
|
// 0,
|
||||||
));
|
// )) /
|
||||||
|
// (await entries.reduce<PossiblyPromise<number>>(
|
||||||
|
// async (accumulator, [tankIdString, stats]) => {
|
||||||
|
// const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
if (stats.battles < 126) {
|
// if ((await tankDefinitions)[tankId]?.tier === 10) {
|
||||||
problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]);
|
// const tankStats = diffed[tankId];
|
||||||
}
|
// return (await accumulator) + tankStats.battles;
|
||||||
if (averageTier < 8) {
|
// } else return accumulator;
|
||||||
problems.push([
|
// },
|
||||||
'Average tier',
|
// 0,
|
||||||
`${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`,
|
// ));
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (stats.wins / stats.battles < 0.6) {
|
|
||||||
problems.push([
|
|
||||||
'Winrate',
|
|
||||||
`${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
|
|
||||||
100 *
|
|
||||||
(stats.wins / stats.battles - 0.6)
|
|
||||||
).toFixed(2)}%)`,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (WN8 < 2450) {
|
|
||||||
problems.push([
|
|
||||||
'WN8',
|
|
||||||
`${WN8.toFixed(0)} (${(WN8 - 2450).toFixed(0)})`,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (tier10Damage < 1250) {
|
|
||||||
problems.push([
|
|
||||||
'Tier 10 damage',
|
|
||||||
`${tier10Damage.toFixed(0)} (${tier10Damage - 1250})`,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
isEligible = problems.length === 0;
|
// if (stats.battles < 126) {
|
||||||
body = isEligible
|
// problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]);
|
||||||
? 'Player is fully eligible.'
|
// }
|
||||||
: `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable(
|
// if (averageTier < 8) {
|
||||||
problems,
|
// problems.push([
|
||||||
)}`;
|
// 'Average tier',
|
||||||
} else if (clan === 'SMRI') {
|
// `${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`,
|
||||||
const stats = accountInfo.statistics.all;
|
// ]);
|
||||||
|
// }
|
||||||
|
// if (stats.wins / stats.battles < 0.6) {
|
||||||
|
// problems.push([
|
||||||
|
// 'Winrate',
|
||||||
|
// `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
|
||||||
|
// 100 *
|
||||||
|
// (stats.wins / stats.battles - 0.6)
|
||||||
|
// ).toFixed(2)}%)`,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
// if (WN8 < 2450) {
|
||||||
|
// problems.push([
|
||||||
|
// 'WN8',
|
||||||
|
// `${WN8.toFixed(0)} (${(WN8 - 2450).toFixed(0)})`,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
// if (tier10Damage < 1250) {
|
||||||
|
// problems.push([
|
||||||
|
// 'Tier 10 damage',
|
||||||
|
// `${tier10Damage.toFixed(0)} (${tier10Damage - 1250})`,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
if (stats.battles < 1500) {
|
// isEligible = problems.length === 0;
|
||||||
problems.push([
|
// body = isEligible
|
||||||
'Battles',
|
// ? 'Player is fully eligible.'
|
||||||
`${stats.battles} (${stats.battles - 1500})`,
|
// : `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable(
|
||||||
]);
|
// problems,
|
||||||
}
|
// )}`;
|
||||||
if (stats.wins / stats.battles <= 0.53) {
|
// } else if (clan === 'SMRI') {
|
||||||
problems.push([
|
// const stats = accountInfo.statistics.all;
|
||||||
'Winrate',
|
|
||||||
`${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
|
|
||||||
100 *
|
|
||||||
(stats.wins / stats.battles - 0.53)
|
|
||||||
).toFixed(2)}%)`,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
isEligible = problems.length === 0;
|
// if (stats.battles < 1500) {
|
||||||
body = isEligible
|
// problems.push([
|
||||||
? 'Player is fully eligible.'
|
// 'Battles',
|
||||||
: `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable(
|
// `${stats.battles} (${stats.battles - 1500})`,
|
||||||
problems,
|
// ]);
|
||||||
)}`;
|
// }
|
||||||
}
|
// if (stats.wins / stats.battles <= 0.53) {
|
||||||
|
// problems.push([
|
||||||
|
// 'Winrate',
|
||||||
|
// `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
|
||||||
|
// 100 *
|
||||||
|
// (stats.wins / stats.battles - 0.53)
|
||||||
|
// ).toFixed(2)}%)`,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
return (isEligible ? embedPositive : embedNegative)(title, body);
|
// isEligible = problems.length === 0;
|
||||||
},
|
// body = isEligible
|
||||||
};
|
// ? 'Player is fully eligible.'
|
||||||
|
// : `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable(
|
||||||
|
// problems,
|
||||||
|
// )}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (isEligible ? embedPositive : embedNegative)(title, body);
|
||||||
|
// },
|
||||||
|
// };
|
||||||
+48
-52
@@ -1,11 +1,8 @@
|
|||||||
import {
|
import { Locale, SlashCommandSubcommandGroupBuilder } from 'discord.js';
|
||||||
SlashCommandBuilder,
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
SlashCommandSubcommandGroupBuilder,
|
|
||||||
} from 'discord.js';
|
|
||||||
import * as Graph from '../components/Graph';
|
import * as Graph from '../components/Graph';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData from '../components/NoData';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
@@ -13,13 +10,14 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
|||||||
import getTankHistories from '../core/blitzkrieg/getTankHistories';
|
import getTankHistories from '../core/blitzkrieg/getTankHistories';
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
||||||
import addPeriodSubCommands from '../core/discord/addPeriodSubCommands';
|
import { addPeriodSubCommands } from '../core/discord/addPeriodSubCommands';
|
||||||
import addTankChoices from '../core/discord/addTankChoices';
|
import addTankChoices from '../core/discord/addTankChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import buttonPrimary from '../core/discord/buttonPrimary';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import commandToURL from '../core/discord/commandToURL';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
import resolvePeriodFromCommand, {
|
import resolvePeriodFromCommand, {
|
||||||
@@ -29,7 +27,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
|||||||
import resolvePlayerFromCommand, {
|
import resolvePlayerFromCommand, {
|
||||||
ResolvedPlayer,
|
ResolvedPlayer,
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
type EvolutionStatType = 'player' | 'tank';
|
type EvolutionStatType = 'player' | 'tank';
|
||||||
|
|
||||||
@@ -38,6 +36,7 @@ async function render(
|
|||||||
{ start, end, name }: ResolvedPeriod,
|
{ start, end, name }: ResolvedPeriod,
|
||||||
type: 'player' | 'tank',
|
type: 'player' | 'tank',
|
||||||
tankId: number | null,
|
tankId: number | null,
|
||||||
|
locale: Locale,
|
||||||
) {
|
) {
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
@@ -49,7 +48,7 @@ async function render(
|
|||||||
includeLatestHistories: true,
|
includeLatestHistories: true,
|
||||||
includePreviousHistories: true,
|
includePreviousHistories: true,
|
||||||
})
|
})
|
||||||
: getTankHistories(region, id, {
|
: getTankHistories(region, id, locale, {
|
||||||
tankId: tankId!,
|
tankId: tankId!,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
@@ -74,8 +73,8 @@ async function render(
|
|||||||
const minX = Math.min(...xs);
|
const minX = Math.min(...xs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
<TitleBar name={accountInfo.nickname} image={logo} description={name} />
|
<TitleBar title={accountInfo.nickname} image={logo} description={name} />
|
||||||
|
|
||||||
{plot.length > 0 && (
|
{plot.length > 0 && (
|
||||||
<Graph.Root
|
<Graph.Root
|
||||||
@@ -90,8 +89,8 @@ async function render(
|
|||||||
max: maxX,
|
max: maxX,
|
||||||
precision: 0,
|
precision: 0,
|
||||||
}}
|
}}
|
||||||
xMinLabel={new Date(minTime * 1000).toDateString()}
|
xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
|
||||||
xMaxLabel={new Date(maxTime * 1000).toDateString()}
|
xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
|
||||||
>
|
>
|
||||||
{Graph.Line({
|
{Graph.Line({
|
||||||
color: Graph.LineColor.Red,
|
color: Graph.LineColor.Red,
|
||||||
@@ -102,47 +101,37 @@ async function render(
|
|||||||
</Graph.Root>
|
</Graph.Root>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
{plot.length === 0 && <NoData type="battles_in_period" locale={locale} />}
|
||||||
</Wrapper>
|
</CommandWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const evolutionCommand = new Promise<CommandRegistryPromisable>(
|
export const evolutionCommand = new Promise<CommandRegistry>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
|
||||||
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
|
||||||
const command = new SlashCommandBuilder()
|
|
||||||
.setName('evolution')
|
|
||||||
.setDescription('Evolution of statistics')
|
|
||||||
.addSubcommandGroup((option) => {
|
|
||||||
playerGroup = option
|
|
||||||
.setName('player')
|
|
||||||
.setDescription('Player statistics');
|
|
||||||
|
|
||||||
return playerGroup;
|
|
||||||
})
|
|
||||||
.addSubcommandGroup((option) => {
|
|
||||||
tankGroup = option.setName('tank').setDescription('Tank statistics');
|
|
||||||
|
|
||||||
return tankGroup;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
addPeriodSubCommands(playerGroup!, (option) =>
|
|
||||||
option.addStringOption(addUsernameChoices),
|
|
||||||
),
|
|
||||||
addPeriodSubCommands(tankGroup!, (option) =>
|
|
||||||
option
|
|
||||||
.addStringOption(addTankChoices)
|
|
||||||
.addStringOption(addUsernameChoices),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command,
|
command: createLocalizedCommand('evolution', [
|
||||||
|
{
|
||||||
|
group: 'player',
|
||||||
|
modify(option: SlashCommandSubcommandGroupBuilder) {
|
||||||
|
addPeriodSubCommands(option, (option) =>
|
||||||
|
option.addStringOption(addUsernameChoices),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'tank',
|
||||||
|
modify(option: SlashCommandSubcommandGroupBuilder) {
|
||||||
|
addPeriodSubCommands(option, (option) =>
|
||||||
|
option
|
||||||
|
.addStringOption(addTankChoices)
|
||||||
|
.addStringOption(addUsernameChoices),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const commandGroup =
|
const commandGroup =
|
||||||
@@ -151,18 +140,24 @@ export const evolutionCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
const tankId =
|
const tankId =
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
commandGroup === 'tank'
|
||||||
|
? await resolveTankId(tankIdRaw, interaction.locale)
|
||||||
|
: null;
|
||||||
const path = commandToURL(interaction, {
|
const path = commandToURL(interaction, {
|
||||||
...player,
|
...player,
|
||||||
...getCustomPeriodParams(interaction),
|
...getCustomPeriodParams(interaction),
|
||||||
tankId,
|
tankId,
|
||||||
});
|
});
|
||||||
|
|
||||||
getBlitzStarsLinkButton;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
await render(player, period, commandGroup, tankId),
|
await render(
|
||||||
buttonPrimary(path, 'Refresh'),
|
player,
|
||||||
|
period,
|
||||||
|
commandGroup,
|
||||||
|
tankId,
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
|
buttonRefresh(interaction, path),
|
||||||
await getBlitzStarsLinkButton(
|
await getBlitzStarsLinkButton(
|
||||||
player.region,
|
player.region,
|
||||||
player.id,
|
player.id,
|
||||||
@@ -188,6 +183,7 @@ export const evolutionCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
period,
|
period,
|
||||||
commandGroup,
|
commandGroup,
|
||||||
parseInt(url.searchParams.get('tankId') ?? '0') || null,
|
parseInt(url.searchParams.get('tankId') ?? '0') || null,
|
||||||
|
interaction.locale,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+32
-22
@@ -1,9 +1,9 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
import GenericAllStats from '../components/GenericAllStats';
|
import GenericAllStats from '../components/GenericAllStats';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData from '../components/NoData';
|
||||||
import TierWeights from '../components/TierWeights';
|
import TierWeights from '../components/TierWeights';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
@@ -16,6 +16,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks';
|
|||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import commandToURL from '../core/discord/commandToURL';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
@@ -29,55 +30,64 @@ import resolvePlayerFromCommand, {
|
|||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { getTierWeights } from '../core/statistics/getTierWeights';
|
import { getTierWeights } from '../core/statistics/getTierWeights';
|
||||||
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
async function render(
|
async function render(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
{ start, end, name }: ResolvedPeriod,
|
{ start, end, name }: ResolvedPeriod,
|
||||||
filters: StatFilters,
|
filters: StatFilters,
|
||||||
|
locale: Locale,
|
||||||
) {
|
) {
|
||||||
const { nickname } = await getAccountInfo(region, id);
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
|
const diffedTankStats = await getStatsInPeriod(
|
||||||
|
region,
|
||||||
|
id,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
locale,
|
||||||
|
);
|
||||||
const { stats, supplementary, filteredOrder } = await filterStats(
|
const { stats, supplementary, filteredOrder } = await filterStats(
|
||||||
diffedTankStats,
|
diffedTankStats,
|
||||||
filters,
|
filters,
|
||||||
);
|
);
|
||||||
const filterDescriptions = await filtersToDescription(filters);
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
|
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={nickname}
|
title={nickname}
|
||||||
image={clanImage}
|
image={clanImage}
|
||||||
description={`${name} • ${filterDescriptions}`}
|
description={`${name} • ${filterDescriptions}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
{stats.battles === 0 && (
|
||||||
|
<NoData type="battles_in_period" locale={locale} />
|
||||||
|
)}
|
||||||
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
|
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
|
||||||
{stats.battles > 0 && (
|
{stats.battles > 0 && (
|
||||||
<GenericAllStats stats={stats} supplementaryStats={supplementary} />
|
<GenericAllStats
|
||||||
|
stats={stats}
|
||||||
|
supplementaryStats={supplementary}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</CommandWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
|
export const fullStatsCommand = new Promise<CommandRegistry>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addPeriodicFilterOptions(
|
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('full-stats')
|
|
||||||
.setDescription('Full in-game statistics'),
|
|
||||||
(option) => option.addStringOption(addUsernameChoices),
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command,
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand('full-stats'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
@@ -90,7 +100,7 @@ export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
render(player, period, filters),
|
render(player, period, filters, interaction.locale),
|
||||||
buttonRefresh(interaction, path),
|
buttonRefresh(interaction, path),
|
||||||
getBlitzStarsLinkButton(player.region, player.id),
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
]);
|
]);
|
||||||
@@ -106,7 +116,7 @@ export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
const filters = getFiltersFromButton(interaction);
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await render(player, period, filters);
|
return await render(player, period, filters, interaction.locale);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+41
-19
@@ -1,35 +1,48 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
import GenericStats from '../components/GenericStats';
|
import GenericStats from '../components/GenericStats';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData from '../components/NoData';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanInfo } from '../core/blitz/getClanInfo';
|
import { getClanInfo } from '../core/blitz/getClanInfo';
|
||||||
import addClanChoices from '../core/discord/addClanChoices';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { localizationObject } from '../core/discord/localizationObject';
|
||||||
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
const DEFAULT_THRESHOLD = 7;
|
const DEFAULT_THRESHOLD = 7;
|
||||||
|
|
||||||
export const inactiveCommand: CommandRegistry = {
|
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('inactive')
|
||||||
.setName('inactive')
|
|
||||||
.setDescription('Lists all inactive players')
|
|
||||||
.addStringOption(addClanChoices)
|
.addStringOption(addClanChoices)
|
||||||
.addNumberOption((option) =>
|
.addNumberOption((option) => {
|
||||||
option
|
const { translate } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
.setName('threshold')
|
.setName('threshold')
|
||||||
.setDescription(
|
.setNameLocalizations(
|
||||||
`The number of days inactive (default: ${DEFAULT_THRESHOLD})`,
|
localizationObject('bot.commands.inactive.options.threshold'),
|
||||||
)
|
)
|
||||||
.setMinValue(0),
|
.setDescription(
|
||||||
|
translate('bot.commands.inactive.options.threshold.description'),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.inactive.options.threshold.description',
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.setMinValue(0);
|
||||||
|
}),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { translate } = translator(interaction.locale);
|
||||||
const { region, id } = await resolveClanFromCommand(interaction);
|
const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
const threshold =
|
const threshold =
|
||||||
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
||||||
@@ -47,23 +60,32 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.map(
|
.map(
|
||||||
([name, days]) => [name, `${days.toFixed(0)} days`] as [string, string],
|
([name, days]) =>
|
||||||
|
[
|
||||||
|
name,
|
||||||
|
translate('bot.commands.inactive.body.listing', [
|
||||||
|
days.toFixed(0),
|
||||||
|
]),
|
||||||
|
] as [string, string],
|
||||||
);
|
);
|
||||||
const hasInactiveMembers = inactive.length > 0;
|
const hasInactiveMembers = inactive.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={clanInfo.name}
|
title={clanInfo.name}
|
||||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
||||||
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
description={`${translate('bot.commands.inactive.body.subtitle', [`${threshold}`])} • ${new Date().toLocaleDateString(interaction.locale)}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
{!hasInactiveMembers && (
|
||||||
|
<NoData type="players_in_period" locale={interaction.locale} />
|
||||||
|
)}
|
||||||
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
||||||
</Wrapper>
|
</CommandWrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: autocompleteClan,
|
autocomplete: autocompleteClan,
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+29
-18
@@ -1,31 +1,33 @@
|
|||||||
import { teal } from '@radix-ui/colors';
|
import { teal } from '@radix-ui/colors';
|
||||||
import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js';
|
import { GuildMemberRoleManager } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import discord from '../../discord.json' assert { type: 'json' };
|
import discord from '../../discord.json' assert { type: 'json' };
|
||||||
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
||||||
import Wrapper from '../components/Wrapper';
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
import { REGION_NAMES } from '../constants/regions';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export const verifyCommand: CommandRegistry = {
|
export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('link').addStringOption((option) =>
|
||||||
.setName('link')
|
addUsernameChoices(option).setRequired(true),
|
||||||
.setDescription('Links your Blitz and Discord account')
|
),
|
||||||
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
const { id, region } = await resolvePlayerFromCommand(interaction);
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
const discordId = parseInt(interaction.user.id);
|
const discordId = parseInt(interaction.user.id);
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
@@ -36,8 +38,14 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
if (interaction.guildId === discord.sklld_guild_id) {
|
if (interaction.guildId === discord.sklld_guild_id) {
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
||||||
return embedNegative(
|
return embedNegative(
|
||||||
`${markdownEscape(interaction.user.username)} failed to verify`,
|
translate(translate('bot.commands.link.body.no_role_permissions'), [
|
||||||
"I don't have the permission to change your manage roles.",
|
markdownEscape(interaction.user.username),
|
||||||
|
]),
|
||||||
|
translate(
|
||||||
|
translate(
|
||||||
|
'bot.commands.link.body.no_role_permissions.description',
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +58,7 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<Wrapper fat>
|
<CommandWrapper fat>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -137,7 +145,9 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
height: 16,
|
height: 16,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: 16 }}>Accounts linked</span>
|
<span
|
||||||
|
style={{ fontSize: 16 }}
|
||||||
|
>{t`bot.commands.link.body.accounts_linked`}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -186,9 +196,9 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{clanAccountInfo?.clan
|
{clanAccountInfo?.clan
|
||||||
? `[${clanAccountInfo?.clan?.tag}] - `
|
? `[${clanAccountInfo?.clan?.tag}] • `
|
||||||
: ''}
|
: ''}
|
||||||
{REGION_NAMES[region]}
|
{translate(`common.regions.normal.${region}`)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,14 +206,15 @@ export const verifyCommand: CommandRegistry = {
|
|||||||
|
|
||||||
<Glow color={teal.teal9} rotation={-90} />
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>,
|
</CommandWrapper>,
|
||||||
|
|
||||||
embedInfo(
|
embedInfo(
|
||||||
'Incorrect account?',
|
t`bot.commands.link.embed.title`,
|
||||||
'Use the command again and select a username from the search results.',
|
t`bot.commands.link.embed.description`,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+24
-19
@@ -1,9 +1,7 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData from '../components/NoData';
|
||||||
import * as Tanks from '../components/Tanks';
|
import * as Tanks from '../components/Tanks';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
|
||||||
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
@@ -18,25 +16,27 @@ import { tankIcon } from '../core/blitzkrieg/tankIcon';
|
|||||||
import addTierChoices from '../core/discord/addTierChoices';
|
import addTierChoices from '../core/discord/addTierChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const ownedTanksCommand: CommandRegistry = {
|
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('owned-tanks')
|
||||||
.setName('owned-tanks')
|
|
||||||
.setDescription("Shows a player's owned tanks")
|
|
||||||
.addStringOption(addTierChoices)
|
.addStringOption(addTierChoices)
|
||||||
.addStringOption(addUsernameChoices),
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { translate } = translator(interaction.locale);
|
||||||
const tier = Number(interaction.options.getString('tier'));
|
const tier = Number(interaction.options.getString('tier'));
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const { id, region: server } = account;
|
||||||
const accountInfo = await getAccountInfo(server, id);
|
const accountInfo = await getAccountInfo(server, id);
|
||||||
const tankStats = await getTankStats(server, id);
|
const tankStats = await getTankStats(server, id, interaction.locale);
|
||||||
const filteredTanks = (
|
const filteredTanks = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tankStats.map(async (tankData) => ({
|
tankStats.map(async (tankData) => ({
|
||||||
@@ -61,18 +61,22 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
nations.sort();
|
nations.sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo.nickname}
|
title={accountInfo.nickname}
|
||||||
image={
|
image={
|
||||||
clanAccountInfo?.clan
|
clanAccountInfo?.clan
|
||||||
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
|
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`}
|
description={translate('bot.commands.owned_tanks.body.subtitle', [
|
||||||
|
TIER_ROMAN_NUMERALS[tier as Tier],
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
{filteredTanks.length === 0 && (
|
||||||
|
<NoData type="tanks_found" locale={interaction.locale} />
|
||||||
|
)}
|
||||||
|
|
||||||
{filteredTanks.length > 0 &&
|
{filteredTanks.length > 0 &&
|
||||||
(await Promise.all(
|
(await Promise.all(
|
||||||
@@ -83,9 +87,9 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
const rightColumn = tanks.slice(leftColumnSize);
|
const rightColumn = tanks.slice(leftColumnSize);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tanks.Root>
|
<Tanks.Root key={nation}>
|
||||||
<Tanks.Title>
|
<Tanks.Title>
|
||||||
{(await encyclopediaInfo).vehicle_nations[nation]}
|
{translate(`common.nations.${nation}`)}
|
||||||
</Tanks.Title>
|
</Tanks.Title>
|
||||||
|
|
||||||
<Tanks.Row>
|
<Tanks.Row>
|
||||||
@@ -95,7 +99,7 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
<Tanks.Item
|
<Tanks.Item
|
||||||
key={tank.id}
|
key={tank.id}
|
||||||
name={tank.name}
|
name={tank.name}
|
||||||
tankType={tank.type}
|
tankType={tank.class}
|
||||||
image={tankIcon(tank.id)}
|
image={tankIcon(tank.id)}
|
||||||
treeType={tank.treeType}
|
treeType={tank.treeType}
|
||||||
/>
|
/>
|
||||||
@@ -108,7 +112,7 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
<Tanks.Item
|
<Tanks.Item
|
||||||
key={tank.id}
|
key={tank.id}
|
||||||
name={tank.name}
|
name={tank.name}
|
||||||
tankType={tank.type}
|
tankType={tank.class}
|
||||||
image={tankIcon(tank.id)}
|
image={tankIcon(tank.id)}
|
||||||
treeType={tank.treeType}
|
treeType={tank.treeType}
|
||||||
/>
|
/>
|
||||||
@@ -120,9 +124,10 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
))}
|
))}
|
||||||
</Wrapper>
|
</CommandWrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+12
-10
@@ -1,30 +1,31 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const permissionsCommand: CommandRegistry = {
|
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('permissions'),
|
||||||
.setName('permissions')
|
|
||||||
.setDescription('Checks wether the bot has the required permissions'),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
// friday the 13th permissions easter egg
|
// friday the 13th permissions easter egg
|
||||||
const isFridayThe13th =
|
const isFridayThe13th =
|
||||||
new Date().getDay() === 5 && new Date().getDate() === 13;
|
new Date().getDay() === 5 && new Date().getDate() === 13;
|
||||||
const permissions = [
|
const permissions = [
|
||||||
[
|
[
|
||||||
interaction.appPermissions?.has('ViewChannel'),
|
interaction.appPermissions?.has('ViewChannel'),
|
||||||
'View channels: needed for the refresh button to work',
|
t`bot.commands.permissions.body.view_channels`,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
interaction.appPermissions?.has('ReadMessageHistory'),
|
interaction.appPermissions?.has('ReadMessageHistory'),
|
||||||
'Read message history: also needed for the refresh button to work',
|
t`bot.commands.permissions.body.read_message_history`,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
interaction.appPermissions?.has('AttachFiles'),
|
interaction.appPermissions?.has('AttachFiles'),
|
||||||
'Attach files: needed for image-based commands to work',
|
t`bot.commands.permissions.body.attach_files`,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
const body = permissions
|
const body = permissions
|
||||||
@@ -37,6 +38,7 @@ export const permissionsCommand: CommandRegistry = {
|
|||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
return `# Permissions\n${body}`;
|
return translate('bot.commands.permissions.body.title', [body]);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+17
-14
@@ -1,32 +1,35 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const pingCommand: CommandRegistry = {
|
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
handlesInteraction: true,
|
handlesInteraction: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('ping', [
|
||||||
.setName('ping')
|
{ subcommand: 'blitzkrieg' },
|
||||||
.setDescription('Check if the bot is alive')
|
{ subcommand: 'wotb' },
|
||||||
.addSubcommand((option) =>
|
]),
|
||||||
option.setName('blitzkrieg').setDescription('Ping Blitzkrieg'),
|
|
||||||
)
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
option.setName('blitz').setDescription('Ping Blitz'),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const executionStart = Date.now();
|
const executionStart = Date.now();
|
||||||
|
|
||||||
if (subcommand === 'blitzkrieg') {
|
if (subcommand === 'blitzkrieg') {
|
||||||
await interaction.editReply('Pong 🏓');
|
await interaction.editReply(t`bot.commands.ping.body.pong`);
|
||||||
} else {
|
} else {
|
||||||
await fetch('https://api.wotblitz.com/');
|
await fetch('https://api.wotblitz.com/');
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionTime = Date.now() - executionStart;
|
const executionTime = Date.now() - executionStart;
|
||||||
interaction.editReply(`Pong 🏓 - ${executionTime}ms`);
|
interaction.editReply(
|
||||||
|
translate('bot.commands.ping.body.pong_with_time', [
|
||||||
|
`${Math.round(executionTime)}`,
|
||||||
|
]),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
@@ -1,68 +1,80 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
|
||||||
import { startCase } from 'lodash';
|
// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
|
||||||
import markdownEscape from 'markdown-escape';
|
// | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
|
||||||
import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
// | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
// _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
// |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
|
||||||
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
|
|
||||||
type SortBy = 'name' | 'count';
|
// import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
// import { startCase } from 'lodash';
|
||||||
|
// import markdownEscape from 'markdown-escape';
|
||||||
|
// import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
||||||
|
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
// import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
// import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
// import embedInfo from '../core/discord/embedInfo';
|
||||||
|
// import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
||||||
|
// import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
// import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const playerAchievementsCommand: CommandRegistry = {
|
// type SortBy = 'name' | 'count';
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
// export const playerAchievementsCommand = new Promise<CommandRegistry>(
|
||||||
.setName('player-achievements')
|
// (resolve) => {
|
||||||
.setDescription("All the player's achievements")
|
// resolve({
|
||||||
.addStringOption(addUsernameChoices)
|
// inProduction: true,
|
||||||
.addStringOption((option) =>
|
// inPublic: true,
|
||||||
option
|
|
||||||
.setName('sort')
|
|
||||||
.setDescription('What to sort by (default: name)')
|
|
||||||
.addChoices(
|
|
||||||
{ name: 'By name', value: 'name' satisfies SortBy },
|
|
||||||
{ name: 'By count', value: 'count' satisfies SortBy },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
// command: new SlashCommandBuilder()
|
||||||
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
// .setName('player-achievements')
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
// .setDescription("All the player's achievements")
|
||||||
const { id, region: region } = account;
|
// .addStringOption(addUsernameChoices)
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
// .addStringOption((option) =>
|
||||||
const accountsAchievements = await getAccountAchievements(region, id);
|
// option
|
||||||
const accountAchievements = accountsAchievements;
|
// .setName('sort')
|
||||||
const compound = {
|
// .setDescription('What to sort by (default: name)')
|
||||||
...accountAchievements.achievements,
|
// .addChoices(
|
||||||
...accountAchievements.max_series,
|
// { name: 'By name', value: 'name' satisfies SortBy },
|
||||||
};
|
// { name: 'By count', value: 'count' satisfies SortBy },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
return embedInfo(
|
// async handler(interaction) {
|
||||||
`${markdownEscape(accountInfo.nickname)}'s information`,
|
// const sortBy = (interaction.options.getString('sort') ??
|
||||||
|
// 'name') as SortBy;
|
||||||
|
// const account = await resolvePlayerFromCommand(interaction);
|
||||||
|
// const { id, region: region } = account;
|
||||||
|
// const accountInfo = await getAccountInfo(region, id);
|
||||||
|
// const accountsAchievements = await getAccountAchievements(region, id);
|
||||||
|
// const accountAchievements = accountsAchievements;
|
||||||
|
// const compound = {
|
||||||
|
// ...accountAchievements.achievements,
|
||||||
|
// ...accountAchievements.max_series,
|
||||||
|
// };
|
||||||
|
|
||||||
markdownTable(
|
// return embedInfo(
|
||||||
Object.keys(compound)
|
// `${markdownEscape(accountInfo.nickname)}'s information`,
|
||||||
.filter((achievement) => compound[achievement] > 0)
|
|
||||||
.sort(
|
|
||||||
sortBy === 'count'
|
|
||||||
? (a, b) => compound[b] - compound[a]
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(achievement) =>
|
|
||||||
[
|
|
||||||
startCase(achievement),
|
|
||||||
compound[achievement],
|
|
||||||
] as TableInputEntry,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
// markdownTable(
|
||||||
};
|
// Object.keys(compound)
|
||||||
|
// .filter((achievement) => compound[achievement] > 0)
|
||||||
|
// .sort(
|
||||||
|
// sortBy === 'count'
|
||||||
|
// ? (a, b) => compound[b] - compound[a]
|
||||||
|
// : undefined,
|
||||||
|
// )
|
||||||
|
// .map(
|
||||||
|
// (achievement) =>
|
||||||
|
// [
|
||||||
|
// startCase(achievement),
|
||||||
|
// compound[achievement],
|
||||||
|
// ] as TableInputEntry,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
|
||||||
|
// autocomplete: autocompleteUsername,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// );
|
||||||
+33
-16
@@ -1,35 +1,44 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable from '../core/discord/markdownTable';
|
import markdownTable from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const playerInfoCommand: CommandRegistry = {
|
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command:
|
||||||
.setName('player-info')
|
createLocalizedCommand('player-info').addStringOption(addUsernameChoices),
|
||||||
.setDescription('Basic information about a player')
|
|
||||||
.addStringOption(addUsernameChoices),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: region } = account;
|
const { id, region: region } = account;
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`${markdownEscape(accountInfo.nickname)}'s information`,
|
translate('bot.commands.player_info.body.title', [
|
||||||
|
markdownEscape(accountInfo.nickname),
|
||||||
|
]),
|
||||||
|
|
||||||
markdownTable([
|
markdownTable([
|
||||||
['Nickname', `${accountInfo.nickname}`],
|
|
||||||
['Battles', `${accountInfo.statistics.all.battles}`],
|
|
||||||
[
|
[
|
||||||
'Winrate',
|
t`bot.commands.player_info.body.nickname`,
|
||||||
|
`${accountInfo.nickname}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
t`bot.commands.player_info.body.battles`,
|
||||||
|
`${accountInfo.statistics.all.battles}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
t`bot.commands.player_info.body.winrate`,
|
||||||
`${(
|
`${(
|
||||||
100 *
|
100 *
|
||||||
(accountInfo.statistics.all.wins /
|
(accountInfo.statistics.all.wins /
|
||||||
@@ -37,18 +46,26 @@ export const playerInfoCommand: CommandRegistry = {
|
|||||||
).toFixed(2)}%`,
|
).toFixed(2)}%`,
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
['Account ID', `${accountInfo.account_id}`],
|
|
||||||
[
|
[
|
||||||
'Created',
|
t`bot.commands.player_info.body.account_id`,
|
||||||
`${new Date(accountInfo.created_at * 1000).toDateString()}`,
|
`${accountInfo.account_id}`,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Last battle',
|
t`bot.commands.player_info.body.created`,
|
||||||
`${new Date(accountInfo.last_battle_time * 1000).toDateString()}`,
|
new Date(accountInfo.created_at * 1000).toLocaleDateString(
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
t`bot.commands.player_info.body.last_battle`,
|
||||||
|
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+169
-119
@@ -1,17 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
APIApplicationCommandOptionChoice,
|
APIApplicationCommandOptionChoice,
|
||||||
SlashCommandBuilder,
|
Locale,
|
||||||
SlashCommandSubcommandBuilder,
|
SlashCommandSubcommandBuilder,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||||
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
import * as Leaderboard from '../components/Leaderboard';
|
import * as Leaderboard from '../components/Leaderboard';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
|
||||||
import { LEAGUES } from '../constants/leagues';
|
import { LEAGUES } from '../constants/leagues';
|
||||||
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings';
|
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings';
|
||||||
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
import { Region } from '../constants/regions';
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||||
@@ -25,9 +25,11 @@ import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchive
|
|||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { localizationObject } from '../core/discord/localizationObject';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
import { translator } from '../core/localization/translator';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { UserError } from '../hooks/userError';
|
import { UserError } from '../hooks/userError';
|
||||||
|
|
||||||
export interface RatingsPlayer {
|
export interface RatingsPlayer {
|
||||||
@@ -117,91 +119,6 @@ export interface RatingsNeighbors {
|
|||||||
neighbors: RatingsPlayer[];
|
neighbors: RatingsPlayer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const noOngoingSeason = embedNegative(
|
|
||||||
'No ongoing season',
|
|
||||||
"Wargaming didn't provide any data for this season.",
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|
||||||
async (resolve) => {
|
|
||||||
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
|
||||||
const onGoingSeason = await isOnGoingRatingsSeason();
|
|
||||||
const seasonNumbers = range(
|
|
||||||
FIRST_ARCHIVED_RATINGS_SEASON,
|
|
||||||
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
function addSubcommands(option: SlashCommandSubcommandBuilder) {
|
|
||||||
return option
|
|
||||||
.addStringOption(addUsernameChoices)
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('limit')
|
|
||||||
.setDescription('How many neighbors to display (default: 10)')
|
|
||||||
.setRequired(false)
|
|
||||||
.setMinValue(5)
|
|
||||||
.setMaxValue(30),
|
|
||||||
)
|
|
||||||
.addStringOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('season')
|
|
||||||
.setDescription('Season number')
|
|
||||||
.setRequired(false)
|
|
||||||
.addChoices(
|
|
||||||
...seasonNumbers
|
|
||||||
.map(
|
|
||||||
(number, index) =>
|
|
||||||
({
|
|
||||||
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
|
|
||||||
value: `${number}`,
|
|
||||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
|
||||||
)
|
|
||||||
.reverse(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = new SlashCommandBuilder()
|
|
||||||
.setName('ratings')
|
|
||||||
.setDescription('Ratings battles statistics')
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
addSubcommands(
|
|
||||||
option
|
|
||||||
.setName('neighbors')
|
|
||||||
.setDescription('Shows neighboring players on the leaderboard'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
addSubcommands(
|
|
||||||
option
|
|
||||||
.setName('league')
|
|
||||||
.setDescription('Shows top players from a league')
|
|
||||||
.addStringOption(addRegionChoices)
|
|
||||||
.addStringOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('league')
|
|
||||||
.setDescription('The league to display')
|
|
||||||
.addChoices(
|
|
||||||
...LEAGUES.map(
|
|
||||||
({ name }, value) =>
|
|
||||||
({
|
|
||||||
name,
|
|
||||||
value: `${value}`,
|
|
||||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setRequired(true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command,
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
interface SimplifiedPlayer {
|
interface SimplifiedPlayer {
|
||||||
id: number;
|
id: number;
|
||||||
score: number;
|
score: number;
|
||||||
@@ -210,10 +127,143 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
nickname: string;
|
nickname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT = 10;
|
||||||
|
|
||||||
|
export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
|
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
||||||
|
const onGoingSeason = await isOnGoingRatingsSeason();
|
||||||
|
const seasonNumbers = range(
|
||||||
|
FIRST_ARCHIVED_RATINGS_SEASON,
|
||||||
|
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
function addOptions(option: SlashCommandSubcommandBuilder) {
|
||||||
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(t`bot.commands.ratings.options.limit`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject('bot.commands.ratings.options.limit'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate('bot.commands.ratings.options.limit.description', [
|
||||||
|
`${DEFAULT_LIMIT}`,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.ratings.options.limit.description',
|
||||||
|
[`${DEFAULT_LIMIT}`],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(t`bot.commands.ratings.options.season`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject('bot.commands.ratings.options.season'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate('bot.commands.ratings.options.season.description'),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.ratings.options.season.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
...seasonNumbers
|
||||||
|
.map((number, index) => {
|
||||||
|
const current =
|
||||||
|
index === seasonNumbers.length - 1 && onGoingSeason;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: current
|
||||||
|
? translate(
|
||||||
|
'bot.commands.ratings.options.season.choices.current',
|
||||||
|
[`${number}`],
|
||||||
|
)
|
||||||
|
: `${number}`,
|
||||||
|
value: `${number}`,
|
||||||
|
name_localizations: current
|
||||||
|
? localizationObject(
|
||||||
|
'bot.commands.ratings.options.season.choices.current',
|
||||||
|
[`${number}`],
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
} satisfies APIApplicationCommandOptionChoice<string>;
|
||||||
|
})
|
||||||
|
.reverse(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: createLocalizedCommand('ratings', [
|
||||||
|
{
|
||||||
|
subcommand: 'neighbors',
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option.addStringOption(addUsernameChoices);
|
||||||
|
addOptions(option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subcommand: 'league',
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option
|
||||||
|
.addStringOption((option) => {
|
||||||
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.setName(t`bot.commands.ratings.options.league`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject('bot.commands.ratings.options.league'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate('bot.commands.ratings.options.league.description'),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.ratings.options.league.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addChoices(
|
||||||
|
...LEAGUES.map(
|
||||||
|
({ name }, value) =>
|
||||||
|
({
|
||||||
|
name: translate(`common.leagues.${name}`),
|
||||||
|
value: `${value}`,
|
||||||
|
name_localizations: localizationObject(
|
||||||
|
`common.leagues.${name}`,
|
||||||
|
),
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(true);
|
||||||
|
})
|
||||||
|
.addStringOption(addRegionChoices);
|
||||||
|
addOptions(option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
// BIG TODO: title seems to be in english
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
const subcommand = interaction.options.getSubcommand(true) as
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
| 'league'
|
| 'league'
|
||||||
| 'neighbors';
|
| 'neighbors';
|
||||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
|
||||||
const seasonOption = interaction.options.getString('season');
|
const seasonOption = interaction.options.getString('season');
|
||||||
const season = seasonOption
|
const season = seasonOption
|
||||||
? parseInt(seasonOption)
|
? parseInt(seasonOption)
|
||||||
@@ -224,7 +274,6 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
let titleName: string;
|
let titleName: string;
|
||||||
let titleImage: string | undefined;
|
let titleImage: string | undefined;
|
||||||
let titleDescription: string;
|
let titleDescription: string;
|
||||||
let playersBefore: number;
|
|
||||||
let playersAfter: number;
|
let playersAfter: number;
|
||||||
let players: SimplifiedPlayer[] | undefined;
|
let players: SimplifiedPlayer[] | undefined;
|
||||||
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
||||||
@@ -232,15 +281,14 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
let regionRatingsInfo: RatingsInfo;
|
let regionRatingsInfo: RatingsInfo;
|
||||||
|
|
||||||
if (subcommand === 'league') {
|
if (subcommand === 'league') {
|
||||||
const leagueIndex = parseInt(
|
const leagueIndex = parseInt(interaction.options.getString('league')!);
|
||||||
interaction.options.getString('league')!,
|
|
||||||
);
|
|
||||||
const region = interaction.options.getString('region') as Region;
|
const region = interaction.options.getString('region') as Region;
|
||||||
regionRatingsInfo = await (isCurrentSeason
|
regionRatingsInfo = await (isCurrentSeason
|
||||||
? getRatingsInfo(region)
|
? getRatingsInfo(region)
|
||||||
: getArchivedRatingsInfo(region, season!));
|
: getArchivedRatingsInfo(region, season!));
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
if (regionRatingsInfo.detail !== undefined)
|
||||||
|
return t`bot.commands.ratings.body.no_ongoing_season`;
|
||||||
|
|
||||||
const result = isCurrentSeason
|
const result = isCurrentSeason
|
||||||
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
|
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
|
||||||
@@ -284,7 +332,9 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
clan: clanData[player.id]?.clan?.tag,
|
clan: clanData[player.id]?.clan?.tag,
|
||||||
nickname:
|
nickname:
|
||||||
clanData[index]?.account_name ??
|
clanData[index]?.account_name ??
|
||||||
`Deleted ${player.id}`,
|
translate('bot.commands.ratings.body.deleted_player', [
|
||||||
|
`${player.id}`,
|
||||||
|
]),
|
||||||
}) satisfies SimplifiedPlayer,
|
}) satisfies SimplifiedPlayer,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -298,23 +348,27 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
players = result;
|
players = result;
|
||||||
playersBefore = result ? result[0].position - 1 : 0;
|
|
||||||
playersAfter =
|
playersAfter =
|
||||||
regionRatingsInfo.count - result[result.length - 1].position;
|
regionRatingsInfo.count - result[result.length - 1].position;
|
||||||
titleName = `${
|
titleName = `${
|
||||||
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
leagueInfo
|
||||||
} - ${REGION_NAMES_SHORT[region]}`;
|
? translate(`common.leagues.${LEAGUES[leagueInfo.index].name}`)
|
||||||
|
: ''
|
||||||
|
} - ${translate(`common.regions.short.${region}`)}`;
|
||||||
titleImage = leagueInfo.big_icon.startsWith('http')
|
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||||
? leagueInfo.big_icon
|
? leagueInfo.big_icon
|
||||||
: `https:${leagueInfo.big_icon}`;
|
: `https:${leagueInfo.big_icon}`;
|
||||||
titleDescription = `Top ${limit} players`;
|
titleDescription = translate('bot.commands.ratings.body.top_players', [
|
||||||
|
`${limit}`,
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
regionRatingsInfo = isCurrentSeason
|
regionRatingsInfo = isCurrentSeason
|
||||||
? await getRatingsInfo(region)
|
? await getRatingsInfo(region)
|
||||||
: await getArchivedRatingsInfo(region, season!);
|
: await getArchivedRatingsInfo(region, season!);
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
if (regionRatingsInfo.detail !== undefined)
|
||||||
|
return t`bot.commands.ratings.body.no_ongoing_season`;
|
||||||
|
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
@@ -340,7 +394,10 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
|
|
||||||
if (playerIndex === -1) {
|
if (playerIndex === -1) {
|
||||||
throw new UserError(
|
throw new UserError(
|
||||||
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`,
|
translate('bot.commands.ratings.body.no_participation', [
|
||||||
|
markdownEscape(accountInfo.nickname),
|
||||||
|
`${season}`,
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,11 +434,8 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
titleImage = clan
|
titleImage = clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
: undefined;
|
: undefined;
|
||||||
titleName = neighbors
|
titleName = neighbors ? accountInfo.nickname : '';
|
||||||
? accountInfo.nickname
|
titleDescription = t`bot.commands.ratings.body.neighbors`;
|
||||||
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
|
||||||
titleDescription = 'Ratings neighbors';
|
|
||||||
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
|
||||||
playersAfter =
|
playersAfter =
|
||||||
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
||||||
playerId = id;
|
playerId = id;
|
||||||
@@ -423,33 +477,29 @@ export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={titleName}
|
title={titleName}
|
||||||
image={titleImage}
|
image={titleImage}
|
||||||
description={titleDescription}
|
description={titleDescription}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{items && (
|
{items && (
|
||||||
<Leaderboard.Root>
|
<Leaderboard.Root>
|
||||||
{playersBefore > 0 && (
|
|
||||||
<Leaderboard.Gap
|
|
||||||
message={`+ ${playersBefore.toLocaleString()} more`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{items}
|
{items}
|
||||||
{playersAfter > 0 && (
|
{playersAfter > 0 && (
|
||||||
<Leaderboard.Gap
|
<Leaderboard.Gap
|
||||||
message={`+ ${playersAfter.toLocaleString()} more`}
|
message={translate('bot.commands.ratings.body.more', [
|
||||||
|
`${playersAfter.toLocaleString(interaction.locale)}`,
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Leaderboard.Root>
|
</Leaderboard.Root>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</CommandWrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
+43
-17
@@ -1,46 +1,72 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { REGION_NAMES, Region } from '../constants/regions';
|
import { Region } from '../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||||
import fetchBlitz from '../core/blitz/fetchBlitz';
|
import fetchBlitz from '../core/blitz/fetchBlitz';
|
||||||
import addClanChoices from '../core/discord/addClanChoices';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
|
import { localizationObject } from '../core/discord/localizationObject';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { ClanList } from '../types/clanList';
|
import { ClanList } from '../types/clanList';
|
||||||
|
|
||||||
export const searchClansCommand: CommandRegistry = {
|
const DEFAULT_LIMIT = 25;
|
||||||
|
|
||||||
|
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('search-clans')
|
||||||
.setName('search-clans')
|
|
||||||
.setDescription('Search clans')
|
|
||||||
.addStringOption(addRegionChoices)
|
.addStringOption(addRegionChoices)
|
||||||
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
.addStringOption((option) =>
|
||||||
.addIntegerOption((option) =>
|
addClanChoices(option).setAutocomplete(false),
|
||||||
option
|
)
|
||||||
.setName('limit')
|
.addIntegerOption((option) => {
|
||||||
.setDescription('The size of the search result (default: 25)')
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
.setMinValue(1)
|
|
||||||
.setMaxValue(100),
|
return option
|
||||||
|
.setName(t`bot.commands.search_clans.options.limit`)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject('bot.commands.search_clans.options.limit'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate('bot.commands.search_clans.options.limit.description', [
|
||||||
|
`${DEFAULT_LIMIT}`,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.search_clans.options.limit.description',
|
||||||
|
[`${DEFAULT_LIMIT}`],
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(100);
|
||||||
|
}),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { translate } = translator(interaction.locale);
|
||||||
const server = interaction.options.getString('region') as Region;
|
const server = interaction.options.getString('region') as Region;
|
||||||
const clan = interaction.options.getString('clan')!;
|
const clan = interaction.options.getString('clan')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
|
||||||
const clanList = await fetchBlitz<ClanList>(
|
const clanList = await fetchBlitz<ClanList>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
|
translate('bot.commands.search_clans.body.title', [
|
||||||
|
markdownEscape(clan),
|
||||||
|
translate(`common.regions.normal.${server}`),
|
||||||
|
]),
|
||||||
clanList.length === 0
|
clanList.length === 0
|
||||||
? 'No clans found.'
|
? translate('bot.commands.search_clans.body.no_results')
|
||||||
: `\`\`\`\n${clanList
|
: `\`\`\`\n${clanList
|
||||||
.map((clan) => `${clan.name} [${clan.tag}]`)
|
.map((clan) => `${clan.name} [${clan.tag}]`)
|
||||||
.join('\n')}\n\`\`\``,
|
.join('\n')}\n\`\`\``,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { REGION_NAMES, Region } from '../constants/regions';
|
import { Region } from '../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||||
import fetchBlitz from '../core/blitz/fetchBlitz';
|
import fetchBlitz from '../core/blitz/fetchBlitz';
|
||||||
import {
|
import {
|
||||||
@@ -9,29 +9,49 @@ import {
|
|||||||
} from '../core/blitz/searchPlayersAcrossRegions';
|
} from '../core/blitz/searchPlayersAcrossRegions';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
|
import { localizationObject } from '../core/discord/localizationObject';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const searchPlayersCommand: CommandRegistry = {
|
const DEFAULT_LIMIT = 25;
|
||||||
|
|
||||||
|
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('search-players')
|
||||||
.setName('search-players')
|
|
||||||
.setDescription('Search players in a Blitz server')
|
|
||||||
.addStringOption(addRegionChoices)
|
.addStringOption(addRegionChoices)
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
|
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
|
||||||
)
|
)
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('limit')
|
.setName(t`bot.commands.search_players.options.limit`)
|
||||||
.setDescription('The size of the search result (default: 25)')
|
.setNameLocalizations(
|
||||||
|
localizationObject('bot.commands.search_players.options.limit'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate('bot.commands.search_players.options.limit.description', [
|
||||||
|
`${DEFAULT_LIMIT}`,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.search_players.options.limit.description',
|
||||||
|
[`${DEFAULT_LIMIT}`],
|
||||||
|
),
|
||||||
|
)
|
||||||
.setMinValue(1)
|
.setMinValue(1)
|
||||||
.setMaxValue(100),
|
.setMaxValue(100),
|
||||||
),
|
),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
|
const { translate } = translator(interaction.locale);
|
||||||
const server = interaction.options.getString('region') as Region;
|
const server = interaction.options.getString('region') as Region;
|
||||||
const name = interaction.options.getString('username')!;
|
const name = interaction.options.getString('username')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
@@ -45,12 +65,16 @@ export const searchPlayersCommand: CommandRegistry = {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
|
translate('bot.commands.search_players.body.title', [
|
||||||
|
markdownEscape(trimmedSearch),
|
||||||
|
translate(`common.regions.normal.${server}`),
|
||||||
|
]),
|
||||||
`\`\`\`${
|
`\`\`\`${
|
||||||
players.length === 0
|
players.length === 0
|
||||||
? 'No players found.'
|
? translate('bot.commands.search_players.body.no_results')
|
||||||
: players.map((player) => player.nickname).join('\n')
|
: players.map((player) => player.nickname).join('\n')
|
||||||
}\`\`\``,
|
}\`\`\``,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+47
-36
@@ -1,40 +1,51 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
|
||||||
import { go } from 'fuzzysort';
|
// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
|
||||||
import markdownEscape from 'markdown-escape';
|
// | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
|
||||||
import { tankNames } from '../core/blitzkrieg/tankDefinitions';
|
// | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
|
||||||
import addTankChoices from '../core/discord/addTankChoices';
|
// _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
// |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
|
|
||||||
export const searchTanksCommand: CommandRegistry = {
|
// import { SlashCommandBuilder } from 'discord.js';
|
||||||
inProduction: true,
|
// import { go } from 'fuzzysort';
|
||||||
inPublic: true,
|
// import markdownEscape from 'markdown-escape';
|
||||||
|
// import { tankNames } from '../core/blitzkrieg/tankDefinitions';
|
||||||
|
// import addTankChoices from '../core/discord/addTankChoices';
|
||||||
|
// import embedInfo from '../core/discord/embedInfo';
|
||||||
|
// import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
// export const searchTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
.setName('search-tanks')
|
// resolve({
|
||||||
.setDescription('Search tanks')
|
// inProduction: true,
|
||||||
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
|
// inPublic: true,
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('limit')
|
|
||||||
.setDescription('The size of the search result (default: 25)')
|
|
||||||
.setMinValue(1)
|
|
||||||
.setMaxValue(100),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
// command: new SlashCommandBuilder()
|
||||||
const tank = interaction.options.getString('tank')!;
|
// .setName('search-tanks')
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
// .setDescription('Search tanks')
|
||||||
const results = go(tank, await tankNames, {
|
// .addStringOption((option) =>
|
||||||
limit,
|
// addTankChoices(option).setAutocomplete(false),
|
||||||
keys: ['combined'],
|
// )
|
||||||
}).map((result) => result.obj.original);
|
// .addIntegerOption((option) =>
|
||||||
|
// option
|
||||||
|
// .setName('limit')
|
||||||
|
// .setDescription('The size of the search result (default: 25)')
|
||||||
|
// .setMinValue(1)
|
||||||
|
// .setMaxValue(100),
|
||||||
|
// ),
|
||||||
|
|
||||||
return embedInfo(
|
// async handler(interaction) {
|
||||||
`Tank search for "${markdownEscape(tank)}"`,
|
// const tank = interaction.options.getString('tank')!;
|
||||||
results.length === 0
|
// const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
? 'No tanks found.'
|
// const results = go(tank, await tankNames, {
|
||||||
: `\`\`\`\n${results.join('\n')}\n\`\`\``,
|
// limit,
|
||||||
);
|
// keys: ['combined'],
|
||||||
},
|
// }).map((result) => result.obj.original);
|
||||||
};
|
|
||||||
|
// return embedInfo(
|
||||||
|
// `Tank search for "${markdownEscape(tank)}"`,
|
||||||
|
// results.length === 0
|
||||||
|
// ? 'No tanks found.'
|
||||||
|
// : `\`\`\`\n${results.join('\n')}\n\`\`\``,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// });
|
||||||
+31
-25
@@ -1,8 +1,8 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { Locale } from 'discord.js';
|
||||||
import AllStatsOverview from '../components/AllStatsOverview';
|
import AllStatsOverview from '../components/AllStatsOverview';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import CommandWrapper from '../components/CommandWrapper';
|
||||||
|
import NoData from '../components/NoData';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
|
||||||
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
@@ -15,6 +15,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks';
|
|||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import commandToURL from '../core/discord/commandToURL';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
@@ -27,50 +28,56 @@ import resolvePlayerFromCommand, {
|
|||||||
ResolvedPlayer,
|
ResolvedPlayer,
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
async function render(
|
async function render(
|
||||||
{ region, id }: ResolvedPlayer,
|
{ region, id }: ResolvedPlayer,
|
||||||
{ start, end, name }: ResolvedPeriod,
|
{ start, end, name }: ResolvedPeriod,
|
||||||
filters: StatFilters,
|
filters: StatFilters,
|
||||||
|
locale: Locale,
|
||||||
) {
|
) {
|
||||||
const { nickname } = await getAccountInfo(region, id);
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
|
const diffedTankStats = await getStatsInPeriod(
|
||||||
|
region,
|
||||||
|
id,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
locale,
|
||||||
|
);
|
||||||
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
|
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
|
||||||
const filterDescriptions = await filtersToDescription(filters);
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<CommandWrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={nickname}
|
title={nickname}
|
||||||
image={clanImage}
|
image={clanImage}
|
||||||
description={`${name} • ${filterDescriptions}`}
|
description={`${name} • ${filterDescriptions}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!stats.battles && <NoData type={NoDataType.BattlesInPeriod} />}
|
{!stats.battles && <NoData type="battles_in_period" locale={locale} />}
|
||||||
{stats.battles > 0 && (
|
{stats.battles > 0 && (
|
||||||
<AllStatsOverview stats={stats} supplementaryStats={supplementary} />
|
<AllStatsOverview
|
||||||
|
locale={locale}
|
||||||
|
stats={stats}
|
||||||
|
supplementaryStats={supplementary}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</CommandWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const statsCommand = new Promise<CommandRegistryPromisable>(
|
export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
async (resolve) => {
|
|
||||||
const command = await addPeriodicFilterOptions(
|
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('stats')
|
|
||||||
.setDescription('Regular battles statistics'),
|
|
||||||
(option) => option.addStringOption(addUsernameChoices),
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command,
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand('stats'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
@@ -83,7 +90,7 @@ export const statsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
render(player, period, filters),
|
render(player, period, filters, interaction.locale),
|
||||||
buttonRefresh(interaction, path),
|
buttonRefresh(interaction, path),
|
||||||
getBlitzStarsLinkButton(player.region, player.id),
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
]);
|
]);
|
||||||
@@ -99,8 +106,7 @@ export const statsCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
const filters = getFiltersFromButton(interaction);
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await render(player, period, filters);
|
return await render(player, period, filters, interaction.locale);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
+14
-20
@@ -1,12 +1,12 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { NATIONS } from '../core/blitzkrieg/tankDefinitions';
|
||||||
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
|
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import { addFilterOptions } from '../core/discord/addFilterOptions';
|
import { addFilterOptions } from '../core/discord/addFilterOptions';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import buttonPrimary from '../core/discord/buttonPrimary';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import commandToURL from '../core/discord/commandToURL';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
@@ -14,24 +14,19 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
|||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { renderBreakdown } from './breakdown';
|
import { renderBreakdown } from './breakdown';
|
||||||
|
|
||||||
export const todayCommand = new Promise<CommandRegistryPromisable>(
|
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
async (resolve) => {
|
const nations = await NATIONS;
|
||||||
const awaitedEncyclopediaInfo = await encyclopediaInfo;
|
|
||||||
const command = addFilterOptions(
|
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('today')
|
|
||||||
.setDescription("Today's played tanks and statistics"),
|
|
||||||
awaitedEncyclopediaInfo,
|
|
||||||
).addStringOption(addUsernameChoices);
|
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command,
|
command: addFilterOptions(
|
||||||
|
createLocalizedCommand('today'),
|
||||||
|
nations,
|
||||||
|
).addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
@@ -48,8 +43,8 @@ export const todayCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(await renderBreakdown(player, period, filters)),
|
...(await renderBreakdown(player, period, filters, interaction.locale)),
|
||||||
buttonPrimary(path, 'Refresh'),
|
buttonRefresh(interaction, path),
|
||||||
await getBlitzStarsLinkButton(player.region, player.id),
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -64,8 +59,7 @@ export const todayCommand = new Promise<CommandRegistryPromisable>(
|
|||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
const filters = getFiltersFromButton(interaction);
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await renderBreakdown(player, period, filters);
|
return await renderBreakdown(player, period, filters, interaction.locale);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
@@ -1,30 +1,21 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
import { Percentile } from '../../../../constants/percentiles';
|
import { Percentile } from '../../../../constants/percentiles';
|
||||||
|
import { translator } from '../../../../core/localization/translator';
|
||||||
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
|
||||||
import { theme } from '../../../../stitches.config';
|
import { theme } from '../../../../stitches.config';
|
||||||
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
|
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
|
||||||
import { Glow } from './components/Glow';
|
import { Glow } from './components/Glow';
|
||||||
|
|
||||||
export interface WN8DisplayProps {
|
export interface WN8DisplayProps {
|
||||||
WN8?: number;
|
wn8?: number;
|
||||||
|
locale: Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PERCENTILE_NAMES = {
|
export function WN8Display({ wn8, locale }: WN8DisplayProps) {
|
||||||
[Percentile.VeryBad]: 'Very Bad',
|
|
||||||
[Percentile.Bad]: 'Bad',
|
|
||||||
[Percentile.BelowAverage]: 'Below Average',
|
|
||||||
[Percentile.Average]: 'Average',
|
|
||||||
[Percentile.AboveAverage]: 'Above Average',
|
|
||||||
[Percentile.Good]: 'Good',
|
|
||||||
[Percentile.VeryGood]: 'Very Good',
|
|
||||||
[Percentile.Great]: 'Great',
|
|
||||||
[Percentile.Unicum]: 'Unicum',
|
|
||||||
[Percentile.SuperUnicum]: 'Super Unicum',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|
||||||
const percentile =
|
const percentile =
|
||||||
WN8 === undefined ? Percentile.VeryBad : getWN8Percentile(WN8);
|
wn8 === undefined ? Percentile.VeryBad : getWN8Percentile(wn8);
|
||||||
const color = PERCENTILE_COLORS[percentile];
|
const color = PERCENTILE_COLORS[percentile];
|
||||||
|
const { translate } = translator(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -59,7 +50,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{WN8 === undefined ? '--' : WN8.toFixed(0)}
|
{wn8 === undefined ? '--' : wn8.toFixed(0)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -67,7 +58,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{PERCENTILE_NAMES[percentile]}
|
{translate(`common.wn8_percentile.${percentile}`)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function WN8SurroundingStat({
|
|||||||
style={{
|
style={{
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: 700,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||||
|
import { translator } from '../../core/localization/translator';
|
||||||
import { WN8Display } from './components/WN8Display';
|
import { WN8Display } from './components/WN8Display';
|
||||||
import {
|
import {
|
||||||
WN8SurroundingStat,
|
WN8SurroundingStat,
|
||||||
@@ -8,12 +10,16 @@ import {
|
|||||||
export interface AllStatsOverviewProps {
|
export interface AllStatsOverviewProps {
|
||||||
stats: AllStats;
|
stats: AllStats;
|
||||||
supplementaryStats: SupplementaryStats;
|
supplementaryStats: SupplementaryStats;
|
||||||
|
locale: Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AllStatsOverview({
|
export default function AllStatsOverview({
|
||||||
stats,
|
stats,
|
||||||
|
locale,
|
||||||
supplementaryStats,
|
supplementaryStats,
|
||||||
}: AllStatsOverviewProps) {
|
}: AllStatsOverviewProps) {
|
||||||
|
const { t } = translator(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -32,18 +38,18 @@ export default function AllStatsOverview({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Battles"
|
label={t`bot.commands.stats.body.battles`}
|
||||||
value={stats.battles.toLocaleString()}
|
value={stats.battles.toLocaleString()}
|
||||||
align={WN8SurroundingStatAlign.Right}
|
align={WN8SurroundingStatAlign.Right}
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Winrate"
|
label={t`bot.commands.stats.body.winrate`}
|
||||||
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
|
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
|
||||||
align={WN8SurroundingStatAlign.Right}
|
align={WN8SurroundingStatAlign.Right}
|
||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Damage"
|
label={t`bot.commands.stats.body.damage`}
|
||||||
value={Math.round(
|
value={Math.round(
|
||||||
stats.damage_dealt / stats.battles,
|
stats.damage_dealt / stats.battles,
|
||||||
).toLocaleString()}
|
).toLocaleString()}
|
||||||
@@ -51,13 +57,13 @@ export default function AllStatsOverview({
|
|||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Dmg. ratio"
|
label={t`bot.commands.stats.body.damage_ratio`}
|
||||||
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
|
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
|
||||||
align={WN8SurroundingStatAlign.Right}
|
align={WN8SurroundingStatAlign.Right}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WN8Display WN8={supplementaryStats.WN8} />
|
<WN8Display locale={locale} wn8={supplementaryStats.WN8} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -68,25 +74,25 @@ export default function AllStatsOverview({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Tier"
|
label={t`bot.commands.stats.body.tier`}
|
||||||
value={
|
value={
|
||||||
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
|
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Survival"
|
label={t`bot.commands.stats.body.survival`}
|
||||||
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
||||||
2,
|
2,
|
||||||
)}%`}
|
)}%`}
|
||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Accuracy"
|
label={t`bot.commands.stats.body.accuracy`}
|
||||||
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
|
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
|
||||||
padded
|
padded
|
||||||
/>
|
/>
|
||||||
<WN8SurroundingStat
|
<WN8SurroundingStat
|
||||||
label="Kill ratio"
|
label={t`bot.commands.stats.body.kill_ratio`}
|
||||||
value={(
|
value={(
|
||||||
stats.frags /
|
stats.frags /
|
||||||
(stats.battles - stats.survived_battles)
|
(stats.battles - stats.survived_battles)
|
||||||
|
|||||||
+65
-59
@@ -1,28 +1,29 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { memo, useEffect, useRef } from 'react';
|
||||||
import { Euler, Group, Mesh, Vector3 } from 'three';
|
import { Euler, Group, Vector3 } from 'three';
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
import { ArmorMesh } from '../../../../../../../../../components/ArmorMesh';
|
import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis';
|
||||||
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../../../constants/axis';
|
|
||||||
import {
|
import {
|
||||||
ModelTransformEventData,
|
ModelTransformEventData,
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
} from '../../../../../../../../../core/blitzkrieg/modelTransform';
|
} from '../../../core/blitzkrieg/modelTransform';
|
||||||
import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameToArmorId';
|
import { nameToArmorId } from '../../../core/blitzkrieg/nameToArmorId';
|
||||||
import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness';
|
import { resolveArmor } from '../../../core/blitzkrieg/resolveThickness';
|
||||||
import { useArmor } from '../../../../../../../../../hooks/useArmor';
|
import { useArmor } from '../../../hooks/useArmor';
|
||||||
import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions';
|
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
|
||||||
import { useDuel } from '../../../../../../../../../stores/duel';
|
import { useDuel } from '../../../stores/duel';
|
||||||
import { useTankopediaPersistent } from '../../../../../../../../../stores/tankopedia';
|
import { useTankopediaPersistent } from '../../../stores/tankopedia';
|
||||||
|
import { CoreArmorSceneComponent } from './CoreArmorSceneComponent';
|
||||||
|
|
||||||
export function ArmorHighlighting() {
|
export const CoreArmorScene = memo(() => {
|
||||||
const protagonist = useDuel((state) => state.protagonist!);
|
|
||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const modelDefinitions = useModelDefinitions();
|
const modelDefinitions = useModelDefinitions();
|
||||||
const gunContainer = useRef<Group>(null);
|
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
|
const gunContainer = useRef<Group>(null);
|
||||||
const initialTankopediaState = useTankopediaPersistent.getState();
|
const initialTankopediaState = useTankopediaPersistent.getState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!modelDefinitions) return;
|
||||||
|
|
||||||
const hullOrigin = new Vector3(
|
const hullOrigin = new Vector3(
|
||||||
trackModelDefinition.origin[0],
|
trackModelDefinition.origin[0],
|
||||||
trackModelDefinition.origin[1],
|
trackModelDefinition.origin[1],
|
||||||
@@ -33,14 +34,17 @@ export function ArmorHighlighting() {
|
|||||||
tankModelDefinition.turretOrigin[1],
|
tankModelDefinition.turretOrigin[1],
|
||||||
-tankModelDefinition.turretOrigin[2],
|
-tankModelDefinition.turretOrigin[2],
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const gunOrigin = new Vector3(
|
||||||
|
turretModelDefinition.gunOrigin[0],
|
||||||
|
turretModelDefinition.gunOrigin[1],
|
||||||
|
-turretModelDefinition.gunOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
const turretPosition = new Vector3();
|
const turretPosition = new Vector3();
|
||||||
const turretRotation = new Euler();
|
const turretRotation = new Euler();
|
||||||
const gunPosition = new Vector3();
|
const gunPosition = new Vector3();
|
||||||
const gunRotation = new Euler();
|
const gunRotation = new Euler();
|
||||||
|
|
||||||
function handleModelTransform({ pitch, yaw }: ModelTransformEventData) {
|
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
||||||
if (yaw === undefined) return;
|
|
||||||
|
|
||||||
gunPosition
|
gunPosition
|
||||||
.set(0, 0, 0)
|
.set(0, 0, 0)
|
||||||
.sub(hullOrigin)
|
.sub(hullOrigin)
|
||||||
@@ -53,6 +57,9 @@ export function ArmorHighlighting() {
|
|||||||
gunRotation.set(pitch, 0, 0);
|
gunRotation.set(pitch, 0, 0);
|
||||||
gunContainer.current?.position.copy(gunPosition);
|
gunContainer.current?.position.copy(gunPosition);
|
||||||
gunContainer.current?.rotation.copy(gunRotation);
|
gunContainer.current?.rotation.copy(gunRotation);
|
||||||
|
|
||||||
|
if (yaw === undefined) return;
|
||||||
|
|
||||||
turretPosition
|
turretPosition
|
||||||
.set(0, 0, 0)
|
.set(0, 0, 0)
|
||||||
.sub(hullOrigin)
|
.sub(hullOrigin)
|
||||||
@@ -80,6 +87,16 @@ export function ArmorHighlighting() {
|
|||||||
turretContainer.current?.position.copy(turretPosition);
|
turretContainer.current?.position.copy(turretPosition);
|
||||||
turretContainer.current?.rotation.copy(turretRotation);
|
turretContainer.current?.rotation.copy(turretRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleModelTransform(useDuel.getState().protagonist!);
|
||||||
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modelTransformEvent.off(handleModelTransform);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const unsubscribe = useTankopediaPersistent.subscribe(
|
const unsubscribe = useTankopediaPersistent.subscribe(
|
||||||
(state) => state.mode,
|
(state) => state.mode,
|
||||||
(mode) => {
|
(mode) => {
|
||||||
@@ -87,27 +104,24 @@ export function ArmorHighlighting() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
handleModelTransform(protagonist);
|
return unsubscribe;
|
||||||
modelTransformEvent.on(handleModelTransform);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
modelTransformEvent.off(handleModelTransform);
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const armorGltf = useArmor(protagonist.tank.id);
|
const tank = useDuel((state) => state.protagonist!.tank);
|
||||||
|
const track = useDuel((state) => state.protagonist!.track);
|
||||||
|
const turret = useDuel((state) => state.protagonist!.turret);
|
||||||
|
const gun = useDuel((state) => state.protagonist!.gun);
|
||||||
|
const armorGltf = useArmor(tank.id);
|
||||||
const armorNodes = Object.values(armorGltf.nodes);
|
const armorNodes = Object.values(armorGltf.nodes);
|
||||||
const tankModelDefinition = modelDefinitions[protagonist.tank.id];
|
const tankModelDefinition = modelDefinitions[tank.id];
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[protagonist.track.id];
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
const turretModelDefinition =
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
tankModelDefinition.turrets[protagonist.turret.id];
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
|
||||||
const hullOrigin = new Vector3(
|
const hullOrigin = new Vector3(
|
||||||
trackModelDefinition.origin[0],
|
trackModelDefinition.origin[0],
|
||||||
trackModelDefinition.origin[1],
|
trackModelDefinition.origin[1],
|
||||||
-trackModelDefinition.origin[2],
|
-trackModelDefinition.origin[2],
|
||||||
);
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
const turretOrigin = new Vector3(
|
const turretOrigin = new Vector3(
|
||||||
tankModelDefinition.turretOrigin[0],
|
tankModelDefinition.turretOrigin[0],
|
||||||
tankModelDefinition.turretOrigin[1],
|
tankModelDefinition.turretOrigin[1],
|
||||||
@@ -118,24 +132,14 @@ export function ArmorHighlighting() {
|
|||||||
turretModelDefinition.gunOrigin[1],
|
turretModelDefinition.gunOrigin[1],
|
||||||
-turretModelDefinition.gunOrigin[2],
|
-turretModelDefinition.gunOrigin[2],
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
const maxThickness = Math.max(
|
|
||||||
trackModelDefinition.thickness,
|
|
||||||
gunModelDefinition.thickness,
|
|
||||||
...armorNodes
|
|
||||||
.map((node) => {
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
return resolveArmor(tankModelDefinition.armor, armorId).thickness;
|
|
||||||
})
|
|
||||||
.filter(Boolean),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group
|
||||||
ref={wrapper}
|
ref={wrapper}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
visible={initialTankopediaState.mode === 'armor'}
|
visible={initialTankopediaState.mode === 'armor'}
|
||||||
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;
|
||||||
@@ -145,19 +149,20 @@ export function ArmorHighlighting() {
|
|||||||
armorId,
|
armorId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined || spaced) return null;
|
if (!isVisible || spaced || thickness === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArmorMesh
|
<CoreArmorSceneComponent
|
||||||
maxThickness={maxThickness}
|
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</group>
|
||||||
|
|
||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
const isCurrentTurret = node.name.startsWith(
|
||||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
@@ -169,20 +174,21 @@ export function ArmorHighlighting() {
|
|||||||
armorId,
|
armorId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined || spaced) return null;
|
if (!isVisible || spaced || thickness === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArmorMesh
|
<group key={node.uuid} position={turretOrigin}>
|
||||||
maxThickness={maxThickness}
|
<CoreArmorSceneComponent
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
|
||||||
position={turretOrigin}
|
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
/>
|
/>
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</group>
|
||||||
<group ref={gunContainer}>
|
<group ref={gunContainer}>
|
||||||
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentGun = node.name.startsWith(
|
const isCurrentGun = node.name.startsWith(
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
@@ -194,20 +200,20 @@ export function ArmorHighlighting() {
|
|||||||
armorId,
|
armorId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined || spaced) return null;
|
if (!isVisible || spaced || thickness === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArmorMesh
|
<group
|
||||||
maxThickness={maxThickness}
|
|
||||||
key={node.uuid}
|
key={node.uuid}
|
||||||
geometry={(node as Mesh).geometry}
|
|
||||||
position={turretOrigin.clone().add(gunOrigin)}
|
position={turretOrigin.clone().add(gunOrigin)}
|
||||||
thickness={thickness}
|
>
|
||||||
/>
|
<CoreArmorSceneComponent thickness={thickness} node={node} />
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
MeshBasicMaterial,
|
||||||
|
Object3D,
|
||||||
|
ShaderMaterial,
|
||||||
|
Vector2,
|
||||||
|
WebGLRenderTarget,
|
||||||
|
} from 'three';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { canSplash } from '../../../../core/blitz/canSplash';
|
||||||
|
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
||||||
|
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
||||||
|
import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment';
|
||||||
|
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
|
||||||
|
import { ShellDefinition } from '../../../../core/blitzkrieg/tankDefinitions';
|
||||||
|
import { EquipmentMatrix, useDuel } from '../../../../stores/duel';
|
||||||
|
import {
|
||||||
|
TankopediaPersistent,
|
||||||
|
useTankopediaPersistent,
|
||||||
|
} from '../../../../stores/tankopedia';
|
||||||
|
import fragmentShader from './shaders/fragment.glsl';
|
||||||
|
import vertexShader from './shaders/vertex.glsl';
|
||||||
|
|
||||||
|
export const spacedArmorRenderTarget = new WebGLRenderTarget();
|
||||||
|
|
||||||
|
interface CoreArmorSceneComponentProps {
|
||||||
|
thickness: number;
|
||||||
|
node: Object3D;
|
||||||
|
}
|
||||||
|
|
||||||
|
const excludeMaterial = new MeshBasicMaterial({
|
||||||
|
colorWrite: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function CoreArmorSceneComponent({
|
||||||
|
node,
|
||||||
|
thickness,
|
||||||
|
}: CoreArmorSceneComponentProps) {
|
||||||
|
const material = new ShaderMaterial({
|
||||||
|
fragmentShader,
|
||||||
|
vertexShader,
|
||||||
|
|
||||||
|
transparent: true,
|
||||||
|
|
||||||
|
uniforms: {
|
||||||
|
thickness: { value: null },
|
||||||
|
penetration: { value: null },
|
||||||
|
caliber: { value: null },
|
||||||
|
ricochet: { value: null },
|
||||||
|
normalization: { value: null },
|
||||||
|
isExplosive: { value: null },
|
||||||
|
canSplash: { value: null },
|
||||||
|
damage: { value: null },
|
||||||
|
explosionRadius: { value: null },
|
||||||
|
greenPenetration: { value: null },
|
||||||
|
opaque: { value: null },
|
||||||
|
|
||||||
|
resolution: { value: new Vector2() },
|
||||||
|
spacedArmorBuffer: { value: null },
|
||||||
|
spacedArmorDepth: { value: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleShellChange(shell: ShellDefinition) {
|
||||||
|
material.uniforms.penetration.value = resolveNearPenetration(
|
||||||
|
shell.penetration,
|
||||||
|
);
|
||||||
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
|
material.uniforms.ricochet.value = degToRad(shell.ricochet ?? 90);
|
||||||
|
material.uniforms.normalization.value = degToRad(
|
||||||
|
shell.normalization ?? 0,
|
||||||
|
);
|
||||||
|
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||||
|
material.uniforms.canSplash.value = canSplash(shell.type);
|
||||||
|
material.uniforms.damage.value = shell.damage.armor;
|
||||||
|
material.uniforms.explosionRadius.value = shell.explosionRadius;
|
||||||
|
}
|
||||||
|
function handleVisualChange(
|
||||||
|
visual: TankopediaPersistent['model']['visual'],
|
||||||
|
) {
|
||||||
|
material.uniforms.greenPenetration.value = visual.greenPenetration;
|
||||||
|
material.uniforms.opaque.value = visual.opaque || visual.wireframe;
|
||||||
|
material.wireframe = visual.wireframe;
|
||||||
|
}
|
||||||
|
async function handleProtagonistEquipmentChange(
|
||||||
|
equipment: EquipmentMatrix,
|
||||||
|
) {
|
||||||
|
const hasEnhancedArmor = await hasEquipment(110, false, equipment);
|
||||||
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
|
? thickness * 1.04
|
||||||
|
: thickness;
|
||||||
|
}
|
||||||
|
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
|
const penetration = resolveNearPenetration(shell.penetration);
|
||||||
|
const hasCalibratedShells = await hasEquipment(103, true, equipment);
|
||||||
|
material.uniforms.penetration.value = hasCalibratedShells
|
||||||
|
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
|
||||||
|
: penetration;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShellChange(useDuel.getState().antagonist!.shell);
|
||||||
|
handleVisualChange(useTankopediaPersistent.getState().model.visual);
|
||||||
|
handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment);
|
||||||
|
handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment);
|
||||||
|
|
||||||
|
const unsubscribes = [
|
||||||
|
useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange),
|
||||||
|
useTankopediaPersistent.subscribe(
|
||||||
|
(state) => state.model.visual,
|
||||||
|
handleVisualChange,
|
||||||
|
),
|
||||||
|
useDuel.subscribe(
|
||||||
|
(state) => state.protagonist!.equipment,
|
||||||
|
handleProtagonistEquipmentChange,
|
||||||
|
),
|
||||||
|
useDuel.subscribe(
|
||||||
|
(state) => state.antagonist!.equipment,
|
||||||
|
handleAntagonistEquipmentChange,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame(({ gl }) => {
|
||||||
|
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||||
|
gl.getPixelRatio(),
|
||||||
|
);
|
||||||
|
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
||||||
|
material.uniforms.spacedArmorDepth.value =
|
||||||
|
spacedArmorRenderTarget.depthTexture;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsxTree(node, {
|
||||||
|
renderOrder: 0,
|
||||||
|
material: excludeMaterial,
|
||||||
|
})}
|
||||||
|
{jsxTree(node, {
|
||||||
|
renderOrder: 1,
|
||||||
|
material,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
varying vec3 vNormal;
|
||||||
|
varying vec3 vViewPosition;
|
||||||
|
varying mat4 vProjectionMatrix;
|
||||||
|
|
||||||
|
uniform float thickness;
|
||||||
|
uniform float penetration;
|
||||||
|
uniform float caliber;
|
||||||
|
uniform float ricochet;
|
||||||
|
uniform float normalization;
|
||||||
|
uniform vec2 resolution;
|
||||||
|
uniform bool isExplosive;
|
||||||
|
uniform bool canSplash;
|
||||||
|
uniform float damage;
|
||||||
|
uniform float explosionRadius;
|
||||||
|
uniform bool greenPenetration;
|
||||||
|
uniform bool opaque;
|
||||||
|
|
||||||
|
uniform highp sampler2D spacedArmorBuffer;
|
||||||
|
uniform highp sampler2D spacedArmorDepth;
|
||||||
|
|
||||||
|
float depthToDistance(float depth) {
|
||||||
|
vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
|
||||||
|
vec4 eyePosition = inverse(vProjectionMatrix) * clipPosition;
|
||||||
|
return length(eyePosition.xyz / eyePosition.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float penetrationChance = -1.0;
|
||||||
|
float splashChance = -1.0;
|
||||||
|
|
||||||
|
vec2 screenCoordinates = gl_FragCoord.xy / resolution;
|
||||||
|
float angle = acos(dot(vNormal, -vViewPosition) / length(vViewPosition));
|
||||||
|
|
||||||
|
vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates);
|
||||||
|
bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0;
|
||||||
|
|
||||||
|
bool threeCalibersRule = caliber > thickness * 3.0 || isUnderSpacedArmor;
|
||||||
|
if (!threeCalibersRule && angle >= ricochet) {
|
||||||
|
penetrationChance = 0.0;
|
||||||
|
} else {
|
||||||
|
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
|
||||||
|
float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization;
|
||||||
|
float finalThickness = thickness / cos(max(0.0, angle - finalNormalization));
|
||||||
|
float remainingPenetration = penetration;
|
||||||
|
|
||||||
|
// world space is in meters
|
||||||
|
vec4 spacedArmorDepthFragment = texture2D(spacedArmorDepth, screenCoordinates);
|
||||||
|
float spacedArmorDistance = depthToDistance(spacedArmorDepthFragment.r);
|
||||||
|
float coreArmorDistance = depthToDistance(gl_FragCoord.z);
|
||||||
|
float distanceFromSpacedArmor = coreArmorDistance - spacedArmorDistance;
|
||||||
|
|
||||||
|
if (isUnderSpacedArmor) {
|
||||||
|
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
|
||||||
|
remainingPenetration -= spacedArmorThickness;
|
||||||
|
|
||||||
|
if (isExplosive && !canSplash && remainingPenetration > 0.0) {
|
||||||
|
// there is a 50% penetration loss per meter for HEAT shells
|
||||||
|
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingPenetration = max(0.0, remainingPenetration);
|
||||||
|
float delta = finalThickness - remainingPenetration;
|
||||||
|
float randomization = remainingPenetration * 0.05;
|
||||||
|
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
||||||
|
|
||||||
|
if (canSplash) {
|
||||||
|
// only allow splashing if the damage equation deals more than 0 damage
|
||||||
|
float nominalArmorThickness = finalThickness - remainingPenetration;
|
||||||
|
float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * nominalArmorThickness;
|
||||||
|
splashChance = finalDamage > 0.0 ? 1.0 : 0.0; // TODO: add randomization here?
|
||||||
|
} else {
|
||||||
|
splashChance = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float alpha = opaque ? 1.0 : 0.5;
|
||||||
|
vec3 color = vec3(1.0, splashChance * 0.392, 0.0);
|
||||||
|
if (greenPenetration) {
|
||||||
|
// non-trigonometric red-green mixing: https://www.desmos.com/calculator/ceo1oq7l67
|
||||||
|
float falling = -pow(penetrationChance, 2.0) + 1.0;
|
||||||
|
float gaining = -pow(penetrationChance - 1.0, 2.0) + 1.0;
|
||||||
|
gl_FragColor = vec4(falling * color + gaining * vec3(0.0, 1.0, 0.0), alpha);
|
||||||
|
} else {
|
||||||
|
gl_FragColor = vec4(color, (1.0 - penetrationChance) * alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,306 @@
|
|||||||
|
import { Card, Flex, Inset, Text } from '@radix-ui/themes';
|
||||||
|
import { Html } from '@react-three/drei';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Mesh } from 'three';
|
||||||
|
import { radToDeg } from 'three/src/math/MathUtils';
|
||||||
|
import { isExplosive } from '../../../core/blitz/isExplosive';
|
||||||
|
import { NaNFallback } from '../../../core/math/NaNFallback';
|
||||||
|
import { normalToEuler } from '../../../core/math/normalToEuler';
|
||||||
|
import { AngledPenetration } from '../../../icons/AngledPenetration';
|
||||||
|
import { Block } from '../../../icons/Block';
|
||||||
|
import { NominalPenetration } from '../../../icons/NominalPenetration';
|
||||||
|
import { Ricochet } from '../../../icons/Ricochet';
|
||||||
|
import { useDuel } from '../../../stores/duel';
|
||||||
|
import {
|
||||||
|
ShotLayer,
|
||||||
|
ShotLayerBase,
|
||||||
|
ShotLayerGap,
|
||||||
|
ShotLayerNonExternal,
|
||||||
|
mutateTankopediaTemporary,
|
||||||
|
useTankopediaTemporary,
|
||||||
|
} from '../../../stores/tankopedia';
|
||||||
|
import { ArmorType } from './SpacedArmorScene';
|
||||||
|
|
||||||
|
const layerTypeNames: Record<ArmorType | 'null', string> = {
|
||||||
|
[ArmorType.Core]: 'Core',
|
||||||
|
[ArmorType.Spaced]: 'Spaced',
|
||||||
|
[ArmorType.External]: 'External',
|
||||||
|
null: 'Gap',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LENGTH_INFINITY = 4;
|
||||||
|
const TRACER_THICK = 1 / 64;
|
||||||
|
const TRACER_THIN = TRACER_THICK / 2;
|
||||||
|
|
||||||
|
export function ShotDisplay() {
|
||||||
|
const shot = useTankopediaTemporary((state) => state.shot);
|
||||||
|
const preMidPointTracer = useRef<Mesh>(null);
|
||||||
|
const postMidPointTracer = useRef<Mesh>(null);
|
||||||
|
const hasMultipleLayers = shot?.layers.some(
|
||||||
|
(layer, index) => index !== 0 && layer.type !== null,
|
||||||
|
);
|
||||||
|
const { shell } = useDuel.getState().antagonist!;
|
||||||
|
const explosive = isExplosive(shell.type);
|
||||||
|
const midPointLayer = (shot?.layers.findLast(
|
||||||
|
(layer) => layer.type !== null && layer.status === 'ricochet',
|
||||||
|
) ?? shot?.layers.findLast((layer) => layer.type !== null)) as ShotLayerBase;
|
||||||
|
const firstLayer = shot?.layers[0] as Exclude<ShotLayer, ShotLayerGap>;
|
||||||
|
const lastLayer = shot?.layers.at(-1)!;
|
||||||
|
const distanceFromSpacedArmor = shot
|
||||||
|
? firstLayer.point.distanceTo(shot.point)
|
||||||
|
: undefined;
|
||||||
|
const preMidPointLayers = shot?.layers.slice(
|
||||||
|
0,
|
||||||
|
shot.layers.indexOf(midPointLayer as ShotLayer),
|
||||||
|
);
|
||||||
|
const postMidPointLayers = shot?.layers.slice(
|
||||||
|
shot.layers.indexOf(midPointLayer as ShotLayer),
|
||||||
|
shot.layers.length,
|
||||||
|
);
|
||||||
|
const preMidPointGap = shot
|
||||||
|
? preMidPointLayers!.reduce((accumulator, layer) => {
|
||||||
|
if (layer.type === null) return accumulator + layer.distance;
|
||||||
|
return accumulator;
|
||||||
|
}, 0) + LENGTH_INFINITY
|
||||||
|
: undefined;
|
||||||
|
const postMidPointGap = shot
|
||||||
|
? postMidPointLayers!.reduce((accumulator, layer) => {
|
||||||
|
if (layer.type === null) return accumulator + layer.distance;
|
||||||
|
return accumulator;
|
||||||
|
}, 0) || LENGTH_INFINITY
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
if (!shot || !preMidPointTracer.current) return;
|
||||||
|
|
||||||
|
const t1 = clock.elapsedTime % 1;
|
||||||
|
const t2 = (clock.elapsedTime + 0.5) % 1;
|
||||||
|
|
||||||
|
preMidPointTracer.current.position.set(0, (1 - t1) * preMidPointGap!, 0);
|
||||||
|
preMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t1 - 0.5), 1);
|
||||||
|
|
||||||
|
if (!postMidPointTracer.current) return;
|
||||||
|
|
||||||
|
postMidPointTracer.current.position.set(0, t2 * postMidPointGap!, 0);
|
||||||
|
postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function resetShot() {
|
||||||
|
mutateTankopediaTemporary((draft) => {
|
||||||
|
draft.shot = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribes = [
|
||||||
|
useDuel.subscribe((state) => state.protagonist?.tank, resetShot),
|
||||||
|
useDuel.subscribe((state) => state.antagonist?.tank, resetShot),
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shot) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<group
|
||||||
|
position={midPointLayer.point}
|
||||||
|
rotation={normalToEuler(midPointLayer.shellNormal)}
|
||||||
|
>
|
||||||
|
<mesh position={[0, preMidPointGap! / 2, 0]}>
|
||||||
|
<cylinderGeometry args={[TRACER_THIN, TRACER_THIN, preMidPointGap]} />
|
||||||
|
<meshBasicMaterial depthTest={false} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh position={[0, preMidPointGap! / 2, 0]} ref={preMidPointTracer}>
|
||||||
|
<cylinderGeometry
|
||||||
|
args={[TRACER_THICK, TRACER_THICK, preMidPointGap]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial
|
||||||
|
color={
|
||||||
|
lastLayer.status === 'blocked'
|
||||||
|
? 0xff4040
|
||||||
|
: lastLayer.status === 'penetration'
|
||||||
|
? 0x00ff00
|
||||||
|
: 0xffff00
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{midPointLayer.status === 'ricochet' && (
|
||||||
|
<group
|
||||||
|
position={midPointLayer.point}
|
||||||
|
rotation={normalToEuler(
|
||||||
|
midPointLayer.shellNormal
|
||||||
|
.clone()
|
||||||
|
.reflect(midPointLayer.surfaceNormal)
|
||||||
|
.multiplyScalar(-1),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<mesh position={[0, postMidPointGap! / 2, 0]}>
|
||||||
|
<cylinderGeometry
|
||||||
|
args={[TRACER_THIN, TRACER_THIN, postMidPointGap]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial depthTest={false} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh
|
||||||
|
position={[0, postMidPointGap! / 2, 0]}
|
||||||
|
ref={postMidPointTracer}
|
||||||
|
>
|
||||||
|
<cylinderGeometry
|
||||||
|
args={[TRACER_THICK, TRACER_THICK, postMidPointGap]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial color={0xffff00} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shot.layers.map((layer, index) => {
|
||||||
|
if (layer.type === null) return;
|
||||||
|
|
||||||
|
const shellRotation = normalToEuler(layer.shellNormal);
|
||||||
|
const nextLayer = shot.layers[index + 1] as ShotLayerGap | undefined;
|
||||||
|
const trueStatus =
|
||||||
|
nextLayer?.status === 'blocked' ? 'blocked' : layer.status;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<group
|
||||||
|
key={`${index}-line`}
|
||||||
|
position={layer.point}
|
||||||
|
rotation={shellRotation}
|
||||||
|
>
|
||||||
|
<Html
|
||||||
|
center
|
||||||
|
style={{
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
trueStatus === 'penetration'
|
||||||
|
? '#00ff00c0'
|
||||||
|
: trueStatus === 'blocked'
|
||||||
|
? '#ff0000c0'
|
||||||
|
: '#ffff00c0',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Inset>
|
||||||
|
{hasMultipleLayers && (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center',
|
||||||
|
color:
|
||||||
|
trueStatus === 'ricochet' ? 'black' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<b>{layer.index + 1}</b>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Inset>
|
||||||
|
</Card>
|
||||||
|
</Html>
|
||||||
|
</group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Html
|
||||||
|
position={midPointLayer.point}
|
||||||
|
style={{
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
marginLeft: 32,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card style={{ width: 256 + 32 }}>
|
||||||
|
<Flex direction="column">
|
||||||
|
<Text>
|
||||||
|
{lastLayer.status[0].toUpperCase()}
|
||||||
|
{lastLayer.status.slice(1)}
|
||||||
|
{lastLayer.status === 'penetration' &&
|
||||||
|
`: ${Math.round(shell.damage.armor)}hp`}
|
||||||
|
{lastLayer.status === 'blocked' &&
|
||||||
|
shell.type === 'he' &&
|
||||||
|
`: ${NaNFallback(
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.round(
|
||||||
|
0.5 *
|
||||||
|
shell.damage.armor *
|
||||||
|
(1 -
|
||||||
|
distanceFromSpacedArmor! / shell.explosionRadius!) -
|
||||||
|
1.1 *
|
||||||
|
(midPointLayer as ShotLayerNonExternal)
|
||||||
|
.thicknessAngled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)}hp`}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{shot.layers.map((layer, index) => {
|
||||||
|
if (layer.type === null && !explosive) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex key={index} align="center" gap="2">
|
||||||
|
<Flex>
|
||||||
|
{hasMultipleLayers && (
|
||||||
|
<Text size="2" style={{ width: 16 }}>
|
||||||
|
{layer.type === null ? null : `${layer.index + 1}.`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text size="2">{layerTypeNames[`${layer.type}`]}</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{layer.type !== null && (
|
||||||
|
<>
|
||||||
|
<Flex align="center" style={{ width: 64 }}>
|
||||||
|
<NominalPenetration width={16} height={16} />
|
||||||
|
<Text size="2">{Math.round(layer.thickness)}mm</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{layer.type !== ArmorType.External && (
|
||||||
|
<Flex align="center">
|
||||||
|
{layer.status === 'ricochet' ? (
|
||||||
|
<Ricochet width={16} height={16} />
|
||||||
|
) : layer.status === 'blocked' ? (
|
||||||
|
<Block width={16} height={16} />
|
||||||
|
) : (
|
||||||
|
<AngledPenetration width={16} height={16} />
|
||||||
|
)}
|
||||||
|
<Text size="2">
|
||||||
|
{Math.round(layer.thicknessAngled)}mm (
|
||||||
|
{Math.round(radToDeg(layer.angle))}°)
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text size="2">
|
||||||
|
{Math.round(layer.distance * 1000)}mm (-
|
||||||
|
{Math.round(Math.min(100, layer.distance * 50))}%
|
||||||
|
penetration)
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Html>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import { memo, useEffect, useRef } from 'react';
|
||||||
|
import { Euler, Group, Scene, Vector3 } from 'three';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { I_HAT, J_HAT, K_HAT } from '../../../constants/axis';
|
||||||
|
import {
|
||||||
|
ModelTransformEventData,
|
||||||
|
modelTransformEvent,
|
||||||
|
} from '../../../core/blitzkrieg/modelTransform';
|
||||||
|
import { nameToArmorId } from '../../../core/blitzkrieg/nameToArmorId';
|
||||||
|
import { resolveArmor } from '../../../core/blitzkrieg/resolveThickness';
|
||||||
|
import { useArmor } from '../../../hooks/useArmor';
|
||||||
|
import { useModel } from '../../../hooks/useModel';
|
||||||
|
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
|
||||||
|
import { useDuel } from '../../../stores/duel';
|
||||||
|
import { useTankopediaPersistent } from '../../../stores/tankopedia';
|
||||||
|
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
|
||||||
|
|
||||||
|
export enum ArmorType {
|
||||||
|
Core,
|
||||||
|
Spaced,
|
||||||
|
External,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpacedArmorSceneProps {
|
||||||
|
scene: Scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||||
|
const wrapper = useRef<Group>(null);
|
||||||
|
const modelDefinitions = useModelDefinitions();
|
||||||
|
const turretContainer = useRef<Group>(null);
|
||||||
|
const gunContainer = useRef<Group>(null);
|
||||||
|
const initialTankopediaState = useTankopediaPersistent.getState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!modelDefinitions) return;
|
||||||
|
|
||||||
|
const hullOrigin = new Vector3(
|
||||||
|
trackModelDefinition.origin[0],
|
||||||
|
trackModelDefinition.origin[1],
|
||||||
|
-trackModelDefinition.origin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const turretOrigin = new Vector3(
|
||||||
|
tankModelDefinition.turretOrigin[0],
|
||||||
|
tankModelDefinition.turretOrigin[1],
|
||||||
|
-tankModelDefinition.turretOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const gunOrigin = new Vector3(
|
||||||
|
turretModelDefinition.gunOrigin[0],
|
||||||
|
turretModelDefinition.gunOrigin[1],
|
||||||
|
-turretModelDefinition.gunOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const turretPosition = new Vector3();
|
||||||
|
const turretRotation = new Euler();
|
||||||
|
const gunPosition = new Vector3();
|
||||||
|
const gunRotation = new Euler();
|
||||||
|
|
||||||
|
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
||||||
|
gunPosition
|
||||||
|
.set(0, 0, 0)
|
||||||
|
.sub(hullOrigin)
|
||||||
|
.sub(turretOrigin)
|
||||||
|
.sub(gunOrigin)
|
||||||
|
.applyAxisAngle(I_HAT, pitch)
|
||||||
|
.add(gunOrigin)
|
||||||
|
.add(turretOrigin)
|
||||||
|
.add(hullOrigin);
|
||||||
|
gunRotation.set(pitch, 0, 0);
|
||||||
|
gunContainer.current?.position.copy(gunPosition);
|
||||||
|
gunContainer.current?.rotation.copy(gunRotation);
|
||||||
|
|
||||||
|
if (yaw === undefined) return;
|
||||||
|
|
||||||
|
turretPosition
|
||||||
|
.set(0, 0, 0)
|
||||||
|
.sub(hullOrigin)
|
||||||
|
.sub(turretOrigin)
|
||||||
|
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||||
|
turretRotation.set(0, 0, yaw);
|
||||||
|
|
||||||
|
if (tankModelDefinition.turretRotation) {
|
||||||
|
const initialPitch = -degToRad(
|
||||||
|
tankModelDefinition.turretRotation.pitch,
|
||||||
|
);
|
||||||
|
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
||||||
|
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
|
||||||
|
|
||||||
|
turretPosition
|
||||||
|
.applyAxisAngle(I_HAT, initialPitch)
|
||||||
|
.applyAxisAngle(J_HAT, initialRoll)
|
||||||
|
.applyAxisAngle(K_HAT, initialYaw);
|
||||||
|
turretRotation.x += initialPitch;
|
||||||
|
turretRotation.y += initialRoll;
|
||||||
|
turretRotation.z += initialYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
turretPosition.add(turretOrigin).add(hullOrigin);
|
||||||
|
turretContainer.current?.position.copy(turretPosition);
|
||||||
|
turretContainer.current?.rotation.copy(turretRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModelTransform(useDuel.getState().protagonist!);
|
||||||
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modelTransformEvent.off(handleModelTransform);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = useTankopediaPersistent.subscribe(
|
||||||
|
(state) => state.mode,
|
||||||
|
(mode) => {
|
||||||
|
if (wrapper.current) wrapper.current.visible = mode === 'armor';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tank = useDuel((state) => state.protagonist!.tank);
|
||||||
|
const track = useDuel((state) => state.protagonist!.track);
|
||||||
|
const turret = useDuel((state) => state.protagonist!.turret);
|
||||||
|
const gun = useDuel((state) => state.protagonist!.gun);
|
||||||
|
const armorGltf = useArmor(tank.id);
|
||||||
|
const { gltf: modelGltf } = useModel(tank.id);
|
||||||
|
const armorNodes = Object.values(armorGltf.nodes);
|
||||||
|
const modelNodes = Object.values(modelGltf.nodes);
|
||||||
|
const tankModelDefinition = modelDefinitions[tank.id];
|
||||||
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
|
const hullOrigin = new Vector3(
|
||||||
|
trackModelDefinition.origin[0],
|
||||||
|
trackModelDefinition.origin[1],
|
||||||
|
-trackModelDefinition.origin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const turretOrigin = new Vector3(
|
||||||
|
tankModelDefinition.turretOrigin[0],
|
||||||
|
tankModelDefinition.turretOrigin[1],
|
||||||
|
-tankModelDefinition.turretOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const gunOrigin = new Vector3(
|
||||||
|
turretModelDefinition.gunOrigin[0],
|
||||||
|
turretModelDefinition.gunOrigin[1],
|
||||||
|
-turretModelDefinition.gunOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group
|
||||||
|
ref={wrapper}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
visible={initialTankopediaState.mode === 'armor'}
|
||||||
|
>
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
{armorNodes.map((node) => {
|
||||||
|
const isHull = node.name.startsWith('hull_');
|
||||||
|
const isVisible = isHull;
|
||||||
|
const armorId = nameToArmorId(node.name);
|
||||||
|
const { spaced, thickness } = resolveArmor(
|
||||||
|
tankModelDefinition.armor,
|
||||||
|
armorId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isVisible || thickness === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
|
key={node.uuid}
|
||||||
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
{modelNodes.map((node) => {
|
||||||
|
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||||
|
const isTrack = node.name.startsWith('chassis_track_');
|
||||||
|
const isVisible = isWheel || isTrack;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
|
key={node.uuid}
|
||||||
|
type={ArmorType.External}
|
||||||
|
thickness={trackModelDefinition.thickness}
|
||||||
|
variant="track"
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group ref={turretContainer}>
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
{armorNodes.map((node) => {
|
||||||
|
const isCurrentTurret = node.name.startsWith(
|
||||||
|
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
const isVisible = isCurrentTurret;
|
||||||
|
const armorId = nameToArmorId(node.name);
|
||||||
|
const { spaced, thickness } = resolveArmor(
|
||||||
|
turretModelDefinition.armor,
|
||||||
|
armorId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isVisible || thickness === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group key={node.uuid} position={turretOrigin}>
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
|
key={node.uuid}
|
||||||
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
<group ref={gunContainer}>
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
{armorNodes.map((node) => {
|
||||||
|
const isCurrentGun = node.name.startsWith(
|
||||||
|
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
const isVisible = isCurrentGun;
|
||||||
|
const armorId = nameToArmorId(node.name);
|
||||||
|
const { spaced, thickness } = resolveArmor(
|
||||||
|
gunModelDefinition.armor,
|
||||||
|
armorId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isVisible || thickness === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group
|
||||||
|
key={node.uuid}
|
||||||
|
position={turretOrigin.clone().add(gunOrigin)}
|
||||||
|
>
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{modelNodes.map((node) => {
|
||||||
|
const isCurrentGun =
|
||||||
|
node.name ===
|
||||||
|
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
||||||
|
const isVisible = isCurrentGun;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
|
scene={scene}
|
||||||
|
key={node.uuid}
|
||||||
|
type={ArmorType.External}
|
||||||
|
thickness={gunModelDefinition.thickness}
|
||||||
|
variant="gun"
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
});
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
AdditiveBlending,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
Object3D,
|
||||||
|
ShaderMaterial,
|
||||||
|
} from 'three';
|
||||||
|
import { ArmorUserData, ExternalModuleVariant } from '../..';
|
||||||
|
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
||||||
|
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
||||||
|
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
||||||
|
import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree';
|
||||||
|
import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
||||||
|
import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel';
|
||||||
|
import { ArmorType } from '../../../SpacedArmorScene';
|
||||||
|
import fragmentShader from './shaders/fragment.glsl';
|
||||||
|
import vertexShader from './shaders/vertex.glsl';
|
||||||
|
|
||||||
|
interface SpacedArmorSubExternalProps {
|
||||||
|
node: Object3D;
|
||||||
|
thickness: number;
|
||||||
|
variant: ExternalModuleVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpacedArmorSubExternal({
|
||||||
|
node,
|
||||||
|
thickness,
|
||||||
|
variant,
|
||||||
|
}: SpacedArmorSubExternalProps) {
|
||||||
|
const material = new ShaderMaterial({
|
||||||
|
fragmentShader,
|
||||||
|
vertexShader,
|
||||||
|
|
||||||
|
depthTest: true,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: AdditiveBlending,
|
||||||
|
|
||||||
|
uniforms: {
|
||||||
|
thickness: { value: null },
|
||||||
|
penetration: { value: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleShellChange(shell: ShellDefinition) {
|
||||||
|
material.uniforms.penetration.value = resolveNearPenetration(
|
||||||
|
shell.penetration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function handleProtagonistEquipmentChange(
|
||||||
|
equipment: EquipmentMatrix,
|
||||||
|
) {
|
||||||
|
const hasEnhancedArmor = await hasEquipment(110, false, equipment);
|
||||||
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
|
? thickness * 1.04
|
||||||
|
: thickness;
|
||||||
|
}
|
||||||
|
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
|
const penetration = resolveNearPenetration(shell.penetration);
|
||||||
|
const hasCalibratedShells = await hasEquipment(103, true, equipment);
|
||||||
|
material.uniforms.penetration.value = hasCalibratedShells
|
||||||
|
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
|
||||||
|
: penetration;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShellChange(useDuel.getState().antagonist!.shell);
|
||||||
|
handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment);
|
||||||
|
handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment);
|
||||||
|
|
||||||
|
const unsubscribes = [
|
||||||
|
useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange),
|
||||||
|
useDuel.subscribe(
|
||||||
|
(state) => state.protagonist!.equipment,
|
||||||
|
handleProtagonistEquipmentChange,
|
||||||
|
),
|
||||||
|
useDuel.subscribe(
|
||||||
|
(state) => state.antagonist!.equipment,
|
||||||
|
handleAntagonistEquipmentChange,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsxTree(node, {
|
||||||
|
renderOrder: 3,
|
||||||
|
material: new MeshBasicMaterial({
|
||||||
|
colorWrite: false,
|
||||||
|
depthTest: true,
|
||||||
|
depthWrite: true,
|
||||||
|
}),
|
||||||
|
onClick() {},
|
||||||
|
userData: {
|
||||||
|
type: ArmorType.External,
|
||||||
|
thickness,
|
||||||
|
variant,
|
||||||
|
} satisfies ArmorUserData,
|
||||||
|
})}
|
||||||
|
{jsxTree(node, {
|
||||||
|
renderOrder: 4,
|
||||||
|
material,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
uniform float thickness;
|
||||||
|
uniform float penetration;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(thickness / penetration, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
void main() {
|
||||||
|
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
|
||||||
|
gl_Position = projectionMatrix * vViewPosition4;
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
AdditiveBlending,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
Object3D,
|
||||||
|
ShaderMaterial,
|
||||||
|
} from 'three';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { ArmorUserData } from '../..';
|
||||||
|
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
||||||
|
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
||||||
|
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
||||||
|
import { jsxTree } from '../../../../../../core/blitzkrieg/jsxTree';
|
||||||
|
import { ShellDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
||||||
|
import { EquipmentMatrix, useDuel } from '../../../../../../stores/duel';
|
||||||
|
import { ArmorType } from '../../../SpacedArmorScene';
|
||||||
|
import fragmentShader from './shaders/fragment.glsl';
|
||||||
|
import vertexShader from './shaders/vertex.glsl';
|
||||||
|
|
||||||
|
interface SpacedArmorSubSpacedProps {
|
||||||
|
node: Object3D;
|
||||||
|
thickness: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const depthWriteMaterial = new MeshBasicMaterial({
|
||||||
|
depthWrite: true,
|
||||||
|
colorWrite: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function SpacedArmorSubSpaced({
|
||||||
|
node,
|
||||||
|
thickness,
|
||||||
|
}: SpacedArmorSubSpacedProps) {
|
||||||
|
const material = new ShaderMaterial({
|
||||||
|
fragmentShader,
|
||||||
|
vertexShader,
|
||||||
|
|
||||||
|
depthTest: true,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: AdditiveBlending,
|
||||||
|
|
||||||
|
uniforms: {
|
||||||
|
thickness: { value: null },
|
||||||
|
penetration: { value: null },
|
||||||
|
caliber: { value: null },
|
||||||
|
ricochet: { value: null },
|
||||||
|
normalization: { value: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleShellChange(shell: ShellDefinition) {
|
||||||
|
material.uniforms.penetration.value = resolveNearPenetration(
|
||||||
|
shell.penetration,
|
||||||
|
);
|
||||||
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
|
material.uniforms.ricochet.value = degToRad(shell.ricochet ?? 90);
|
||||||
|
material.uniforms.normalization.value = degToRad(
|
||||||
|
shell.normalization ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function handleProtagonistEquipmentChange(
|
||||||
|
equipment: EquipmentMatrix,
|
||||||
|
) {
|
||||||
|
const hasEnhancedArmor = await hasEquipment(110, false, equipment);
|
||||||
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
|
? thickness * 1.04
|
||||||
|
: thickness;
|
||||||
|
}
|
||||||
|
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
|
const penetration = resolveNearPenetration(shell.penetration);
|
||||||
|
const hasCalibratedShells = await hasEquipment(103, true, equipment);
|
||||||
|
material.uniforms.penetration.value = hasCalibratedShells
|
||||||
|
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
|
||||||
|
: penetration;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShellChange(useDuel.getState().antagonist!.shell);
|
||||||
|
handleProtagonistEquipmentChange(useDuel.getState().protagonist!.equipment);
|
||||||
|
handleAntagonistEquipmentChange(useDuel.getState().antagonist!.equipment);
|
||||||
|
|
||||||
|
const unsubscribes = [
|
||||||
|
useDuel.subscribe((state) => state.antagonist!.shell, handleShellChange),
|
||||||
|
useDuel.subscribe(
|
||||||
|
(state) => state.protagonist!.equipment,
|
||||||
|
handleProtagonistEquipmentChange,
|
||||||
|
),
|
||||||
|
useDuel.subscribe(
|
||||||
|
(state) => state.antagonist!.equipment,
|
||||||
|
handleAntagonistEquipmentChange,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsxTree(node, {
|
||||||
|
renderOrder: 2,
|
||||||
|
material,
|
||||||
|
onClick() {},
|
||||||
|
userData: {
|
||||||
|
type: ArmorType.Spaced,
|
||||||
|
thickness,
|
||||||
|
} satisfies ArmorUserData,
|
||||||
|
})}
|
||||||
|
{jsxTree(node, {
|
||||||
|
renderOrder: 5,
|
||||||
|
material: depthWriteMaterial,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
varying vec3 vNormal;
|
||||||
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
|
uniform float thickness;
|
||||||
|
uniform float penetration;
|
||||||
|
uniform float caliber;
|
||||||
|
uniform float ricochet;
|
||||||
|
uniform float normalization;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float angle = acos(dot(vNormal, -vViewPosition) / length(vViewPosition));
|
||||||
|
|
||||||
|
bool threeCalibersRule = caliber > thickness * 3.0;
|
||||||
|
if (!threeCalibersRule && angle >= ricochet) {
|
||||||
|
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
|
||||||
|
float finalNormalization = twoCalibersRule ? ((1.4 * normalization * caliber) / (2.0 * thickness)) : normalization;
|
||||||
|
float finalThickness = thickness / cos(max(0.0, angle - finalNormalization));
|
||||||
|
|
||||||
|
gl_FragColor = vec4(finalThickness / penetration, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
varying vec3 vViewPosition;
|
|
||||||
varying vec3 vNormal;
|
varying vec3 vNormal;
|
||||||
|
varying vec3 vViewPosition;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
|
vec4 vViewPosition4 = modelViewMatrix * vec4(position, 1.0);
|
||||||
|
|
||||||
vViewPosition = vViewPosition4.xyz;
|
vViewPosition = vViewPosition4.xyz;
|
||||||
vNormal = normalMatrix * normal;
|
vNormal = normalMatrix * normal;
|
||||||
|
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
gl_Position = projectionMatrix * vViewPosition4;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import { useThree } from '@react-three/fiber';
|
||||||
|
import {
|
||||||
|
Intersection,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
Object3D,
|
||||||
|
Quaternion,
|
||||||
|
Raycaster,
|
||||||
|
Scene,
|
||||||
|
Vector3,
|
||||||
|
} from 'three';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { canSplash } from '../../../../core/blitz/canSplash';
|
||||||
|
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
||||||
|
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
||||||
|
import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment';
|
||||||
|
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
|
||||||
|
import { useDuel } from '../../../../stores/duel';
|
||||||
|
import {
|
||||||
|
Shot,
|
||||||
|
ShotLayerBase,
|
||||||
|
useTankopediaTemporary,
|
||||||
|
} from '../../../../stores/tankopedia';
|
||||||
|
import { ArmorType } from '../SpacedArmorScene';
|
||||||
|
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
||||||
|
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
||||||
|
|
||||||
|
type SpacedArmorSceneComponentProps = {
|
||||||
|
node: Object3D;
|
||||||
|
thickness: number;
|
||||||
|
scene: Scene;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ArmorType.External;
|
||||||
|
variant: ExternalModuleVariant;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ExternalModuleVariant = 'gun' | 'track';
|
||||||
|
export type ArmorUserData = {
|
||||||
|
thickness: number;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ArmorType.External;
|
||||||
|
variant: ExternalModuleVariant;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
type ArmorMeshIntersection = Intersection & {
|
||||||
|
object: Object3D & { userData: ArmorUserData };
|
||||||
|
};
|
||||||
|
|
||||||
|
const omitMaterial = new MeshBasicMaterial({
|
||||||
|
colorWrite: false,
|
||||||
|
depthWrite: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render orders allocations:
|
||||||
|
* - 0 : core omission
|
||||||
|
* - 1-2: spaced without depth buffer write but do test to allow additive blending
|
||||||
|
* - 3-4: external with depth buffer write in first pass and read in second to simplify geometry
|
||||||
|
* - 5 : spaced depth write
|
||||||
|
*/
|
||||||
|
export function SpacedArmorSceneComponent({
|
||||||
|
node,
|
||||||
|
thickness,
|
||||||
|
scene,
|
||||||
|
...props
|
||||||
|
}: SpacedArmorSceneComponentProps) {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.type === ArmorType.Core &&
|
||||||
|
jsxTree(node, {
|
||||||
|
renderOrder: 0,
|
||||||
|
material: omitMaterial,
|
||||||
|
userData: {
|
||||||
|
type: ArmorType.Core,
|
||||||
|
thickness,
|
||||||
|
} satisfies ArmorUserData,
|
||||||
|
|
||||||
|
async onClick(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const hasCalibratedShells = await hasEquipment(103, true);
|
||||||
|
const hasEnhancedArmor = await hasEquipment(110);
|
||||||
|
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
||||||
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
|
const explosive = isExplosive(shell.type);
|
||||||
|
const splashing = canSplash(shell.type);
|
||||||
|
const penetration =
|
||||||
|
resolveNearPenetration(shell.penetration) *
|
||||||
|
(hasCalibratedShells
|
||||||
|
? isExplosive(shell.type)
|
||||||
|
? 1.1
|
||||||
|
: 1.05
|
||||||
|
: 1);
|
||||||
|
const shot: Shot = {
|
||||||
|
layers: [],
|
||||||
|
point: event.point,
|
||||||
|
}; // TODO: type this
|
||||||
|
let remainingPenetration = penetration;
|
||||||
|
|
||||||
|
const shellNormal = camera.position
|
||||||
|
.clone()
|
||||||
|
.sub(event.point)
|
||||||
|
.normalize();
|
||||||
|
|
||||||
|
pushLogs(shellNormal, filterIntersections(event.intersections), 0);
|
||||||
|
|
||||||
|
function filterIntersections(intersectionsRaw: Intersection[]) {
|
||||||
|
const intersectionsAll = intersectionsRaw.filter(
|
||||||
|
({ object }) => object.userData.type !== undefined,
|
||||||
|
) as ArmorMeshIntersection[];
|
||||||
|
const intersectionsWithDuplicates = intersectionsAll.slice(
|
||||||
|
0,
|
||||||
|
intersectionsAll.findIndex(
|
||||||
|
({ object }) => object.userData.type === ArmorType.Core,
|
||||||
|
) + 1,
|
||||||
|
);
|
||||||
|
const firstGunIntersectionIndex =
|
||||||
|
intersectionsWithDuplicates.findIndex(
|
||||||
|
({ object }) =>
|
||||||
|
object.userData.type === ArmorType.External &&
|
||||||
|
object.userData.variant === 'gun',
|
||||||
|
);
|
||||||
|
const firstTrackIntersectionIndex =
|
||||||
|
intersectionsWithDuplicates.findIndex(
|
||||||
|
({ object }) =>
|
||||||
|
object.userData.type === ArmorType.External &&
|
||||||
|
object.userData.variant === 'track',
|
||||||
|
);
|
||||||
|
let intersections = intersectionsWithDuplicates;
|
||||||
|
|
||||||
|
if (firstGunIntersectionIndex !== -1) {
|
||||||
|
intersections = intersections.filter(({ object }, index) => {
|
||||||
|
return object.userData.type === ArmorType.External &&
|
||||||
|
object.userData.variant === 'gun'
|
||||||
|
? index === firstGunIntersectionIndex
|
||||||
|
: true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (firstTrackIntersectionIndex !== -1) {
|
||||||
|
intersections = intersections.filter(({ object }, index) => {
|
||||||
|
return object.userData.type === ArmorType.External &&
|
||||||
|
object.userData.variant === 'track'
|
||||||
|
? index === firstTrackIntersectionIndex
|
||||||
|
: true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushLogs(
|
||||||
|
shellNormal: Vector3,
|
||||||
|
intersections: ArmorMeshIntersection[],
|
||||||
|
startingIndex: number,
|
||||||
|
) {
|
||||||
|
let layerIndex = startingIndex;
|
||||||
|
let loopIndex = 0;
|
||||||
|
|
||||||
|
if (layerIndex !== 0 && intersections.length > 0) {
|
||||||
|
const distance = (
|
||||||
|
shot.layers.at(-1) as ShotLayerBase
|
||||||
|
).point.distanceTo(intersections[0].point);
|
||||||
|
if (explosive && !splashing)
|
||||||
|
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
||||||
|
const blocked = remainingPenetration < 0;
|
||||||
|
|
||||||
|
shot.layers.push({
|
||||||
|
type: null,
|
||||||
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
|
distance,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blocked) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const intersection of intersections) {
|
||||||
|
const surfaceNormal = intersection
|
||||||
|
.normal!.clone()
|
||||||
|
.applyQuaternion(
|
||||||
|
event.object.getWorldQuaternion(new Quaternion()),
|
||||||
|
);
|
||||||
|
const thickness =
|
||||||
|
thicknessCoefficient * intersection.object.userData.thickness;
|
||||||
|
|
||||||
|
if (layerIndex !== 0 && startingIndex === 0) {
|
||||||
|
const previousIntersection = intersections[layerIndex - 1];
|
||||||
|
const distance =
|
||||||
|
intersection.distance - previousIntersection.distance;
|
||||||
|
if (explosive && !splashing)
|
||||||
|
remainingPenetration -=
|
||||||
|
0.5 * remainingPenetration * distance;
|
||||||
|
const wasted = remainingPenetration < 0;
|
||||||
|
|
||||||
|
shot.layers.push({
|
||||||
|
type: null,
|
||||||
|
status: wasted ? 'blocked' : 'penetration',
|
||||||
|
distance,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wasted) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersection.object.userData.type === ArmorType.External) {
|
||||||
|
remainingPenetration -= thickness;
|
||||||
|
const blocked = remainingPenetration < 0;
|
||||||
|
|
||||||
|
shot.layers.push({
|
||||||
|
type: ArmorType.External,
|
||||||
|
index: layerIndex,
|
||||||
|
shellNormal,
|
||||||
|
surfaceNormal,
|
||||||
|
point: intersection.point,
|
||||||
|
thickness: intersection.object.userData.thickness,
|
||||||
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blocked) break;
|
||||||
|
} else {
|
||||||
|
const type = intersection.object.userData.type;
|
||||||
|
const angle = surfaceNormal.angleTo(shellNormal);
|
||||||
|
const ricochet = degToRad(shell.ricochet ?? 90);
|
||||||
|
const normalization = degToRad(shell.normalization ?? 0);
|
||||||
|
const threeCalibersRule =
|
||||||
|
shell.caliber > thickness * 3 || layerIndex > 0;
|
||||||
|
const twoCalibersRule = shell.caliber > thickness * 2;
|
||||||
|
const finalNormalization = twoCalibersRule
|
||||||
|
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
|
||||||
|
: normalization;
|
||||||
|
const finalThickness =
|
||||||
|
thickness === 0
|
||||||
|
? 0
|
||||||
|
: thickness /
|
||||||
|
Math.cos(Math.max(0, angle - finalNormalization));
|
||||||
|
|
||||||
|
if (!threeCalibersRule && angle >= ricochet) {
|
||||||
|
shot.layers.push({
|
||||||
|
type,
|
||||||
|
index: layerIndex,
|
||||||
|
shellNormal,
|
||||||
|
surfaceNormal,
|
||||||
|
point: intersection.point,
|
||||||
|
thickness,
|
||||||
|
thicknessAngled: finalThickness,
|
||||||
|
angle,
|
||||||
|
status: 'ricochet',
|
||||||
|
});
|
||||||
|
|
||||||
|
remainingPenetration *= 0.75;
|
||||||
|
|
||||||
|
const raycaster = new Raycaster();
|
||||||
|
const newShellNormal = shellNormal
|
||||||
|
.clone()
|
||||||
|
.reflect(surfaceNormal);
|
||||||
|
const newCameraNormal = newShellNormal
|
||||||
|
.clone()
|
||||||
|
.multiplyScalar(-1);
|
||||||
|
|
||||||
|
raycaster.set(intersection.point, newCameraNormal);
|
||||||
|
pushLogs(
|
||||||
|
newShellNormal,
|
||||||
|
filterIntersections(
|
||||||
|
raycaster.intersectObjects(scene.children, true),
|
||||||
|
),
|
||||||
|
layerIndex + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
remainingPenetration -= finalThickness;
|
||||||
|
const blocked = remainingPenetration < 0;
|
||||||
|
|
||||||
|
shot.layers.push({
|
||||||
|
type,
|
||||||
|
index: layerIndex,
|
||||||
|
shellNormal,
|
||||||
|
surfaceNormal,
|
||||||
|
point: intersection.point,
|
||||||
|
thickness,
|
||||||
|
thicknessAngled: finalThickness,
|
||||||
|
angle,
|
||||||
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layerIndex++;
|
||||||
|
loopIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useTankopediaTemporary.setState({ shot });
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
|
||||||
|
{props.type === ArmorType.Spaced && (
|
||||||
|
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.type === ArmorType.External && (
|
||||||
|
<SpacedArmorSubExternal
|
||||||
|
node={node}
|
||||||
|
thickness={thickness}
|
||||||
|
variant={props.variant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { createPortal, useFrame } from '@react-three/fiber';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { DepthTexture, Scene, Vector2 } from 'three';
|
||||||
|
import { CoreArmorScene } from './components/CoreArmorScene';
|
||||||
|
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
|
||||||
|
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||||
|
|
||||||
|
export const Armor = memo(() => {
|
||||||
|
const [spacedArmorScene] = useState(() => new Scene());
|
||||||
|
const [coreArmorScene] = useState(() => new Scene());
|
||||||
|
const spacedArmorPortal = createPortal(
|
||||||
|
<SpacedArmorScene scene={spacedArmorScene} />,
|
||||||
|
spacedArmorScene,
|
||||||
|
);
|
||||||
|
const coreArmorPortal = createPortal(<CoreArmorScene />, coreArmorScene);
|
||||||
|
|
||||||
|
const renderSize = new Vector2();
|
||||||
|
const newRenderSize = new Vector2();
|
||||||
|
|
||||||
|
useFrame(({ gl, camera, scene }) => {
|
||||||
|
gl.autoClear = true;
|
||||||
|
|
||||||
|
gl.getSize(newRenderSize).multiplyScalar(gl.getPixelRatio());
|
||||||
|
|
||||||
|
if (!newRenderSize.equals(renderSize)) {
|
||||||
|
renderSize.copy(newRenderSize);
|
||||||
|
spacedArmorRenderTarget.depthTexture = new DepthTexture(
|
||||||
|
renderSize.x,
|
||||||
|
renderSize.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
spacedArmorRenderTarget.setSize(renderSize.x, renderSize.y);
|
||||||
|
gl.setRenderTarget(spacedArmorRenderTarget);
|
||||||
|
gl.render(spacedArmorScene, camera);
|
||||||
|
|
||||||
|
gl.autoClear = false;
|
||||||
|
|
||||||
|
gl.setRenderTarget(null);
|
||||||
|
gl.render(scene, camera);
|
||||||
|
|
||||||
|
gl.clearDepth();
|
||||||
|
gl.render(coreArmorScene, camera);
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{spacedArmorPortal}
|
||||||
|
{coreArmorPortal}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
import { MeshProps, useFrame, useThree } from '@react-three/fiber';
|
|
||||||
import { memo, useEffect, useRef } from 'react';
|
|
||||||
import {
|
|
||||||
IUniform,
|
|
||||||
Mesh,
|
|
||||||
Quaternion,
|
|
||||||
Raycaster,
|
|
||||||
ShaderMaterial,
|
|
||||||
Vector2,
|
|
||||||
Vector3,
|
|
||||||
} from 'three';
|
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
|
||||||
import { canRicochet } from '../../../../core/blitz/canRicochet';
|
|
||||||
import { canSplash } from '../../../../core/blitz/canSplash';
|
|
||||||
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
|
||||||
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
|
||||||
import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment';
|
|
||||||
import { useAwait } from '../../../../hooks/useAwait';
|
|
||||||
import { useDuel } from '../../../../stores/duel';
|
|
||||||
import {
|
|
||||||
ArmorPiercingLayer,
|
|
||||||
Shot,
|
|
||||||
mutateTankopediaTemporary,
|
|
||||||
useTankopediaPersistent,
|
|
||||||
} from '../../../../stores/tankopedia';
|
|
||||||
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
|
|
||||||
import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
|
|
||||||
import fragmentShader from './shaders/fragment.glsl';
|
|
||||||
import vertexShader from './shaders/vertex.glsl';
|
|
||||||
|
|
||||||
interface ArmorMeshProps extends MeshProps {
|
|
||||||
thickness: number;
|
|
||||||
maxThickness: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ArmorMeshUserData = {
|
|
||||||
type: 'coreArmor' | 'spacedArmor' | 'externalModule';
|
|
||||||
thickness: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ArmorMesh = memo<ArmorMeshProps>(
|
|
||||||
({ thickness, maxThickness, ...props }) => {
|
|
||||||
const camera = useThree((state) => state.camera);
|
|
||||||
const initialShell = useDuel.getState().antagonist!.shell;
|
|
||||||
const initialTankopedia = useTankopediaPersistent.getState();
|
|
||||||
const material = useRef<ShaderMaterial>(null);
|
|
||||||
const explosionCapable = isExplosive(initialShell.type);
|
|
||||||
const ricochetCapable = canRicochet(initialShell.type);
|
|
||||||
const cameraNormal = new Vector3();
|
|
||||||
const shellNormal = new Vector3();
|
|
||||||
const surfaceNormal = new Vector3();
|
|
||||||
const mesh = useRef<Mesh>(null);
|
|
||||||
const scene = useThree((state) => state.scene);
|
|
||||||
const hasCalibratedShells = useAwait(hasEquipment(103, true));
|
|
||||||
const hasEnhancedArmor = useAwait(hasEquipment(110));
|
|
||||||
const wireframe = initialTankopedia.model.visual.wireframe;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function updateQuickEquipments() {
|
|
||||||
const shell = useDuel.getState().antagonist!.shell;
|
|
||||||
const nearPenetration = resolveNearPenetration(shell.penetration);
|
|
||||||
const hasCalibratedShells = await hasEquipment(103, true);
|
|
||||||
const hasEnhancedArmor = await hasEquipment(110);
|
|
||||||
|
|
||||||
if (material.current) {
|
|
||||||
material.current.uniforms.thickness.value =
|
|
||||||
thickness * (hasEnhancedArmor ? 1.04 : 1);
|
|
||||||
material.current.uniforms.penetration.value =
|
|
||||||
nearPenetration *
|
|
||||||
(hasCalibratedShells ? (isExplosive(shell.type) ? 1.1 : 1.05) : 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateShellProperties() {
|
|
||||||
const shell = useDuel.getState().antagonist!.shell;
|
|
||||||
|
|
||||||
if (material.current) {
|
|
||||||
material.current.uniforms.canRicochet.value = canRicochet(shell.type);
|
|
||||||
material.current.uniforms.isExplosive.value = isExplosive(shell.type);
|
|
||||||
material.current.uniforms.canSplash.value = canSplash(shell.type);
|
|
||||||
material.current.uniforms.caliber.value = shell.caliber;
|
|
||||||
material.current.uniforms.ricochetAngle.value = degToRad(
|
|
||||||
shell.ricochet ?? 90,
|
|
||||||
);
|
|
||||||
material.current.uniforms.damage.value = shell.damage.armor;
|
|
||||||
material.current.uniforms.explosionRadius.value =
|
|
||||||
shell.explosionRadius ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQuickEquipments();
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribes = [
|
|
||||||
useDuel.subscribe(
|
|
||||||
(state) => state.protagonist!.equipment,
|
|
||||||
updateQuickEquipments,
|
|
||||||
),
|
|
||||||
useDuel.subscribe(
|
|
||||||
(state) => state.antagonist!.equipment,
|
|
||||||
updateQuickEquipments,
|
|
||||||
),
|
|
||||||
useDuel.subscribe(
|
|
||||||
(state) => state.antagonist!.shell,
|
|
||||||
updateShellProperties,
|
|
||||||
),
|
|
||||||
useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.model.visual.greenPenetration,
|
|
||||||
(greenPenetration) => {
|
|
||||||
if (material.current) {
|
|
||||||
material.current.uniforms.greenPenetration.value =
|
|
||||||
greenPenetration;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.model.visual.wireframe,
|
|
||||||
(wireframe) => {
|
|
||||||
if (material.current) {
|
|
||||||
material.current.wireframe = wireframe;
|
|
||||||
material.current.uniforms.wireframe.value = wireframe;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useFrame(({ gl, camera }) => {
|
|
||||||
if (material.current) {
|
|
||||||
gl.getSize(material.current.uniforms.resolution.value);
|
|
||||||
|
|
||||||
(
|
|
||||||
material.current.uniforms.resolution as IUniform<Vector2>
|
|
||||||
).value.multiplyScalar(gl.getPixelRatio());
|
|
||||||
|
|
||||||
material.current.uniforms.externalModuleMask.value =
|
|
||||||
externalModuleMaskRenderTarget.texture;
|
|
||||||
material.current.uniforms.spacedArmorDepth.value =
|
|
||||||
spacedArmorDepthRenderTarget.depthTexture;
|
|
||||||
material.current.uniforms.spacedArmorMask.value =
|
|
||||||
spacedArmorDepthRenderTarget.texture;
|
|
||||||
material.current.uniforms.projectionMatrixInverse.value =
|
|
||||||
camera.projectionMatrixInverse;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<mesh {...props} renderOrder={0}>
|
|
||||||
<meshBasicMaterial colorWrite={false} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<mesh
|
|
||||||
{...props}
|
|
||||||
renderOrder={1}
|
|
||||||
userData={
|
|
||||||
{
|
|
||||||
type: 'coreArmor',
|
|
||||||
thickness,
|
|
||||||
} satisfies ArmorMeshUserData
|
|
||||||
}
|
|
||||||
ref={mesh}
|
|
||||||
onClick={async (event) => {
|
|
||||||
// if (useTankopediaTemporary.getState().mode !== 'armor') return;
|
|
||||||
|
|
||||||
shellNormal.copy(
|
|
||||||
cameraNormal.copy(camera.position).sub(event.point).normalize(),
|
|
||||||
);
|
|
||||||
surfaceNormal
|
|
||||||
.copy(event.normal!)
|
|
||||||
.applyQuaternion(
|
|
||||||
event.object.getWorldQuaternion(new Quaternion()),
|
|
||||||
);
|
|
||||||
const angle = Math.acos(surfaceNormal.dot(shellNormal));
|
|
||||||
const shell = useDuel.getState().antagonist!.shell;
|
|
||||||
const hasCalibratedShells = await hasEquipment(103, true);
|
|
||||||
const resolvedPenetration =
|
|
||||||
resolveNearPenetration(shell.penetration) *
|
|
||||||
(hasCalibratedShells
|
|
||||||
? isExplosive(shell.type)
|
|
||||||
? 1.1
|
|
||||||
: 1.05
|
|
||||||
: 1);
|
|
||||||
const indexedObjects = new Set<ArmorMeshUserData['type']>();
|
|
||||||
const intersections = event.intersections
|
|
||||||
.filter(({ object }) => Boolean(object.userData.type))
|
|
||||||
.map((event) => {
|
|
||||||
const typedEvent = event as typeof event & {
|
|
||||||
object: (typeof event)['object'] & {
|
|
||||||
userData: ArmorMeshUserData;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return typedEvent;
|
|
||||||
})
|
|
||||||
.filter(({ object }) => {
|
|
||||||
if (indexedObjects.has(object.userData.type)) return false;
|
|
||||||
indexedObjects.add(object.userData.type);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const intersectionsTillFirstCoreArmor = intersections.slice(
|
|
||||||
0,
|
|
||||||
intersections.findIndex(
|
|
||||||
({ object }) => object.userData.type === 'coreArmor',
|
|
||||||
) + 1,
|
|
||||||
);
|
|
||||||
const coreArmorMesh =
|
|
||||||
intersectionsTillFirstCoreArmor.at(-1)!.object;
|
|
||||||
const hasEnhancedArmor = await hasEquipment(110);
|
|
||||||
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
|
||||||
|
|
||||||
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
|
|
||||||
|
|
||||||
let hasRicochet = false;
|
|
||||||
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
|
|
||||||
.map((event, index) => {
|
|
||||||
if (hasRicochet) return null;
|
|
||||||
|
|
||||||
switch (event.object.userData.type) {
|
|
||||||
case 'externalModule': {
|
|
||||||
return {
|
|
||||||
nominal:
|
|
||||||
event.object.userData.thickness * thicknessCoefficient,
|
|
||||||
angled:
|
|
||||||
event.object.userData.thickness * thicknessCoefficient,
|
|
||||||
type: 'external',
|
|
||||||
distance: event.distance,
|
|
||||||
ricochet: false,
|
|
||||||
} satisfies ArmorPiercingLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'spacedArmor':
|
|
||||||
case 'coreArmor': {
|
|
||||||
const threeCalibersRule =
|
|
||||||
shell.caliber >
|
|
||||||
3 *
|
|
||||||
(event.object.userData.thickness *
|
|
||||||
thicknessCoefficient);
|
|
||||||
|
|
||||||
if (
|
|
||||||
index === 0 &&
|
|
||||||
canRicochet(shell.type) &&
|
|
||||||
!threeCalibersRule &&
|
|
||||||
angle >= degToRad(shell.ricochet!)
|
|
||||||
) {
|
|
||||||
hasRicochet = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const twoCalibersRule =
|
|
||||||
shell.caliber >
|
|
||||||
2 *
|
|
||||||
(event.object.userData.thickness *
|
|
||||||
thicknessCoefficient);
|
|
||||||
const normalization =
|
|
||||||
twoCalibersRule && event.object.userData.thickness > 0
|
|
||||||
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
|
|
||||||
(2 *
|
|
||||||
event.object.userData.thickness *
|
|
||||||
thicknessCoefficient)
|
|
||||||
: shell.normalization ?? 0;
|
|
||||||
const angled =
|
|
||||||
(event.object.userData.thickness * thicknessCoefficient) /
|
|
||||||
Math.cos(angle - degToRad(normalization));
|
|
||||||
|
|
||||||
return {
|
|
||||||
ricochet: hasRicochet,
|
|
||||||
nominal:
|
|
||||||
event.object.userData.thickness * thicknessCoefficient,
|
|
||||||
angled,
|
|
||||||
type:
|
|
||||||
event.object.userData.type === 'coreArmor'
|
|
||||||
? 'core'
|
|
||||||
: 'spaced',
|
|
||||||
distance: event.distance,
|
|
||||||
} satisfies ArmorPiercingLayer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => {
|
|
||||||
if (aIndex === armorPiercingLayersRaw.length - 1) return null;
|
|
||||||
const layerB = armorPiercingLayersRaw[aIndex + 1];
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'gap',
|
|
||||||
gap: Math.abs(layerB.distance - layerA.distance),
|
|
||||||
} satisfies ArmorPiercingLayer;
|
|
||||||
});
|
|
||||||
const armorPiercingLayers: ArmorPiercingLayer[] = [];
|
|
||||||
|
|
||||||
armorPiercingLayersRaw.forEach((layer, index) => {
|
|
||||||
const gap = gaps[index];
|
|
||||||
|
|
||||||
armorPiercingLayers.push(layer);
|
|
||||||
if (gap) armorPiercingLayers.push(gap);
|
|
||||||
});
|
|
||||||
|
|
||||||
let remainingPenetration = resolvedPenetration;
|
|
||||||
const explosiveCapable = isExplosive(shell.type);
|
|
||||||
|
|
||||||
armorPiercingLayers.forEach((layer) => {
|
|
||||||
if (layer.type !== 'gap') {
|
|
||||||
if (layer.ricochet) {
|
|
||||||
remainingPenetration *= 0.75;
|
|
||||||
} else {
|
|
||||||
remainingPenetration -= layer.angled;
|
|
||||||
}
|
|
||||||
} else if (explosiveCapable) {
|
|
||||||
// there is a 50% penetration loss per meter for HE based shells
|
|
||||||
remainingPenetration -= 0.5 * remainingPenetration * layer.gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingPenetration = Math.max(0, remainingPenetration);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasPenetrated = remainingPenetration > 0;
|
|
||||||
let ricochet: Shot['ricochet'] = undefined;
|
|
||||||
|
|
||||||
if (hasRicochet) {
|
|
||||||
const raycaster = new Raycaster();
|
|
||||||
const bounceNormal = shellNormal
|
|
||||||
.clone()
|
|
||||||
.multiplyScalar(-1)
|
|
||||||
.sub(
|
|
||||||
surfaceNormal
|
|
||||||
.clone()
|
|
||||||
.multiplyScalar(
|
|
||||||
2 *
|
|
||||||
surfaceNormal.dot(
|
|
||||||
shellNormal.clone().multiplyScalar(-1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
raycaster.set(event.point, bounceNormal);
|
|
||||||
const intersections = raycaster
|
|
||||||
.intersectObjects(scene.children, true)
|
|
||||||
.filter(
|
|
||||||
(intersection) =>
|
|
||||||
typeof intersection.object.userData?.type === 'string',
|
|
||||||
);
|
|
||||||
const indexOfCoreArmor = intersections.findIndex(
|
|
||||||
({ object }) => object.userData.type === 'coreArmor',
|
|
||||||
);
|
|
||||||
const intersectionsTillFirstCoreArmor = intersections.slice(
|
|
||||||
0,
|
|
||||||
indexOfCoreArmor + 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (intersectionsTillFirstCoreArmor.length > 0) {
|
|
||||||
intersectionsTillFirstCoreArmor.forEach((intersection) => {
|
|
||||||
switch (intersection.object.userData.type) {
|
|
||||||
case 'externalModule': {
|
|
||||||
remainingPenetration -=
|
|
||||||
intersection.object.userData.thickness;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'spacedArmor':
|
|
||||||
case 'coreArmor': {
|
|
||||||
const angle = bounceNormal.angleTo(
|
|
||||||
intersection.object.position.clone().sub(event.point),
|
|
||||||
);
|
|
||||||
const twoCalibersRule =
|
|
||||||
shell.caliber >
|
|
||||||
2 *
|
|
||||||
(intersection.object.userData.thickness *
|
|
||||||
thicknessCoefficient);
|
|
||||||
const normalization = twoCalibersRule
|
|
||||||
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
|
|
||||||
(2 *
|
|
||||||
intersection.object.userData.thickness *
|
|
||||||
thicknessCoefficient)
|
|
||||||
: shell.normalization ?? 0;
|
|
||||||
const angled =
|
|
||||||
(intersection.object.userData.thickness *
|
|
||||||
thicknessCoefficient) /
|
|
||||||
Math.cos(angle - degToRad(normalization));
|
|
||||||
|
|
||||||
remainingPenetration -= angled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ricochet = {
|
|
||||||
point: intersectionsTillFirstCoreArmor
|
|
||||||
.at(-1)!
|
|
||||||
.point.toArray(),
|
|
||||||
distance: intersectionsTillFirstCoreArmor.at(-1)!.distance,
|
|
||||||
penetration: remainingPenetration > 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateTankopediaTemporary((draft) => {
|
|
||||||
draft.shot = {
|
|
||||||
type: hasRicochet
|
|
||||||
? 'ricochet'
|
|
||||||
: hasPenetrated
|
|
||||||
? 'penetration'
|
|
||||||
: 'block',
|
|
||||||
point: event.point.toArray(),
|
|
||||||
thicknesses: armorPiercingLayers,
|
|
||||||
shellNormal: shellNormal.toArray(),
|
|
||||||
surfaceNormal: surfaceNormal.toArray(),
|
|
||||||
angle,
|
|
||||||
ricochet,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<shaderMaterial
|
|
||||||
wireframe={wireframe}
|
|
||||||
ref={material}
|
|
||||||
fragmentShader={fragmentShader}
|
|
||||||
vertexShader={vertexShader}
|
|
||||||
transparent
|
|
||||||
depthWrite={false}
|
|
||||||
uniforms={{
|
|
||||||
resolution: { value: new Vector2() },
|
|
||||||
externalModuleMask: { value: null },
|
|
||||||
spacedArmorDepth: { value: null },
|
|
||||||
spacedArmorMask: { value: null },
|
|
||||||
projectionMatrixInverse: { value: null },
|
|
||||||
zNear: { value: camera.near },
|
|
||||||
zFar: { value: camera.far },
|
|
||||||
greenPenetration: {
|
|
||||||
value: initialTankopedia.model.visual.greenPenetration,
|
|
||||||
},
|
|
||||||
maxThickness: { value: maxThickness },
|
|
||||||
isExplosive: { value: explosionCapable },
|
|
||||||
canRicochet: { value: ricochetCapable },
|
|
||||||
canSplash: { value: canSplash(initialShell.type) },
|
|
||||||
thickness: {
|
|
||||||
value: thickness * (hasEnhancedArmor ? 1.04 : 1),
|
|
||||||
},
|
|
||||||
penetration: {
|
|
||||||
value:
|
|
||||||
resolveNearPenetration(initialShell.penetration) *
|
|
||||||
(hasCalibratedShells ? (explosionCapable ? 1.1 : 1.05) : 1),
|
|
||||||
},
|
|
||||||
caliber: { value: initialShell.caliber },
|
|
||||||
ricochetAngle: { value: degToRad(initialShell.ricochet ?? 90) },
|
|
||||||
normalization: {
|
|
||||||
value: degToRad(initialShell.normalization ?? 0),
|
|
||||||
},
|
|
||||||
damage: { value: initialShell.damage.armor },
|
|
||||||
explosionRadius: { value: initialShell.explosionRadius ?? 0 },
|
|
||||||
wireframe: { value: wireframe },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
() => true,
|
|
||||||
);
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
#define PI 3.1415926535897932384626433832795
|
|
||||||
|
|
||||||
varying vec3 vViewPosition;
|
|
||||||
varying vec3 vNormal;
|
|
||||||
uniform bool isExplosive;
|
|
||||||
uniform bool canRicochet;
|
|
||||||
uniform bool canSplash;
|
|
||||||
uniform float thickness;
|
|
||||||
uniform float penetration;
|
|
||||||
uniform float caliber;
|
|
||||||
uniform float ricochetAngle;
|
|
||||||
uniform float normalization;
|
|
||||||
uniform sampler2D externalModuleMask;
|
|
||||||
uniform highp sampler2D spacedArmorDepth;
|
|
||||||
uniform sampler2D spacedArmorMask;
|
|
||||||
uniform vec2 resolution;
|
|
||||||
varying mat4 vProjectionMatrix;
|
|
||||||
uniform float maxThickness;
|
|
||||||
uniform bool greenPenetration;
|
|
||||||
uniform float damage;
|
|
||||||
uniform float explosionRadius;
|
|
||||||
uniform bool wireframe;
|
|
||||||
|
|
||||||
float depthToDistance(float depth) {
|
|
||||||
mat4 projectionMatrixInverse = inverse(vProjectionMatrix);
|
|
||||||
vec4 clipPosition = vec4(gl_FragCoord.xy / resolution * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
|
|
||||||
vec4 eyePosition = projectionMatrixInverse * clipPosition;
|
|
||||||
vec3 NDCPosition = eyePosition.xyz / eyePosition.w;
|
|
||||||
float worldDistance = length(NDCPosition);
|
|
||||||
|
|
||||||
return worldDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
float angleTo(vec3 a, vec3 b) {
|
|
||||||
return acos(dot(a, b) / (length(a) * length(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float penetrationChance = -1.0;
|
|
||||||
float splashChance = -1.0;
|
|
||||||
|
|
||||||
vec2 screenCoordinates = gl_FragCoord.xy / resolution;
|
|
||||||
vec4 externalModuleMaskColor = texture2D(externalModuleMask, screenCoordinates);
|
|
||||||
vec4 spacedArmorMaskColor = texture2D(spacedArmorMask, screenCoordinates);
|
|
||||||
bool isUnderExternalModule = externalModuleMaskColor.r == 1.0;
|
|
||||||
bool isUnderSpacedArmor = spacedArmorMaskColor.a == 1.0;
|
|
||||||
float spacedArmorAngle = spacedArmorMaskColor.r * (PI / 2.0);
|
|
||||||
|
|
||||||
float coreArmorAngle = angleTo(vNormal, -vViewPosition);
|
|
||||||
bool coreArmorThreeCalibersRule = caliber > 3.0 * thickness;
|
|
||||||
float spacedArmorNominalThickness = spacedArmorMaskColor.g * maxThickness;
|
|
||||||
bool spacedArmorThreeCalibersRule = isUnderSpacedArmor && caliber > 3.0 * spacedArmorNominalThickness;
|
|
||||||
bool hasRicochetedSpacedArmor = isUnderSpacedArmor && !spacedArmorThreeCalibersRule && spacedArmorAngle >= ricochetAngle;
|
|
||||||
bool hasRicochetedCoreArmor = !hasRicochetedSpacedArmor && !isUnderExternalModule && canRicochet && !coreArmorThreeCalibersRule && coreArmorAngle >= ricochetAngle;
|
|
||||||
|
|
||||||
if (hasRicochetedCoreArmor || hasRicochetedSpacedArmor) {
|
|
||||||
penetrationChance = 0.0;
|
|
||||||
splashChance = 0.0;
|
|
||||||
} else {
|
|
||||||
// time to chip away at the penetration, one case at a time
|
|
||||||
float remainingPenetration = penetration;
|
|
||||||
|
|
||||||
if (isUnderExternalModule) {
|
|
||||||
// external modules don't care about angle, they'll reduce penetration by their thickness
|
|
||||||
float externalModuleThickness = externalModuleMaskColor.g * maxThickness;
|
|
||||||
remainingPenetration -= externalModuleThickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUnderSpacedArmor) {
|
|
||||||
// spaced armor on the other hand, does care about angle
|
|
||||||
bool spacedArmorTwoCalibersRule = caliber > 2.0 * spacedArmorNominalThickness && spacedArmorNominalThickness > 0.0;
|
|
||||||
float finalSpacedArmorNormalization = spacedArmorTwoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * spacedArmorNominalThickness) : normalization;
|
|
||||||
float finalSpacedArmorThickness = spacedArmorNominalThickness / cos(spacedArmorAngle - finalSpacedArmorNormalization);
|
|
||||||
remainingPenetration -= finalSpacedArmorThickness;
|
|
||||||
}
|
|
||||||
remainingPenetration = max(remainingPenetration, 0.0);
|
|
||||||
|
|
||||||
float spacedArmorDepth = texture2D(spacedArmorDepth, screenCoordinates).r;
|
|
||||||
float spacedArmorDistance = depthToDistance(spacedArmorDepth);
|
|
||||||
float coreArmorDistance = depthToDistance(gl_FragCoord.z);
|
|
||||||
float distanceFromSpacedArmor = abs(coreArmorDistance - spacedArmorDistance);
|
|
||||||
if (isExplosive) {
|
|
||||||
// there is a 50% penetration loss per meter for HE based shells
|
|
||||||
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool coreArmorTwoCalibersRule = caliber > 2.0 * thickness && thickness > 0.0;
|
|
||||||
float finalNormalization = coreArmorTwoCalibersRule ? (normalization * 1.4 * caliber) / (2.0 * thickness) : normalization;
|
|
||||||
float finalCoreArmorThickness = thickness / cos(coreArmorAngle - finalNormalization);
|
|
||||||
float deltaPenetration = finalCoreArmorThickness - remainingPenetration;
|
|
||||||
float randomRadius = remainingPenetration * 0.05;
|
|
||||||
|
|
||||||
if (deltaPenetration > randomRadius) {
|
|
||||||
// even with a 5% buff to the penetration, can't penetrate
|
|
||||||
penetrationChance = 0.0;
|
|
||||||
} else if (deltaPenetration < -randomRadius) {
|
|
||||||
// even with a 5% nerf, can penetrate
|
|
||||||
penetrationChance = 1.0;
|
|
||||||
} else {
|
|
||||||
// transition area
|
|
||||||
penetrationChance = max(1.0 - (deltaPenetration + randomRadius) / (2.0 * randomRadius), 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canSplash) {
|
|
||||||
// only allow splasing if the damage equation deals more than 0 damage
|
|
||||||
float nominalArmorThickness = finalCoreArmorThickness - remainingPenetration;
|
|
||||||
float finalDamage = 0.5 * damage * (1.0 - distanceFromSpacedArmor / explosionRadius) - 1.1 * nominalArmorThickness;
|
|
||||||
splashChance = finalDamage > 0.0 ? 1.0 : 0.0;
|
|
||||||
} else {
|
|
||||||
splashChance = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vec3 color = vec3(1.0, splashChance * 0.392, 0.0);
|
|
||||||
if (greenPenetration || wireframe) {
|
|
||||||
gl_FragColor = vec4(mix(color, vec3(0.0, 1.0, 0.0), penetrationChance), wireframe ? 1.0 : 0.5);
|
|
||||||
} else {
|
|
||||||
gl_FragColor = vec4(color, (1.0 - penetrationChance) * 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Color, MeshBasicMaterial, Object3D, WebGLRenderTarget } from 'three';
|
|
||||||
import { jsxTree } from '../../../core/blitzkrieg/jsxTree';
|
|
||||||
import { ArmorMeshUserData } from './Armor';
|
|
||||||
|
|
||||||
export const externalModuleMaskRenderTarget = new WebGLRenderTarget();
|
|
||||||
|
|
||||||
type ArmorMeshExternalModuleMaskProps = {
|
|
||||||
ornamental?: boolean;
|
|
||||||
node: Object3D;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
exclude?: false;
|
|
||||||
maxThickness: number;
|
|
||||||
thickness: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
exclude: true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const excludeMaterial = new MeshBasicMaterial({
|
|
||||||
colorWrite: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ArmorMeshExternalModuleMask({
|
|
||||||
ornamental = false,
|
|
||||||
node,
|
|
||||||
...props
|
|
||||||
}: ArmorMeshExternalModuleMaskProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{props.exclude &&
|
|
||||||
!ornamental &&
|
|
||||||
jsxTree(node, { renderOrder: 0, material: excludeMaterial })}
|
|
||||||
|
|
||||||
{!props.exclude &&
|
|
||||||
jsxTree(node, {
|
|
||||||
renderOrder: 1,
|
|
||||||
onClick: ornamental ? () => {} : undefined,
|
|
||||||
userData: {
|
|
||||||
type: 'externalModule',
|
|
||||||
thickness: props.thickness,
|
|
||||||
} satisfies ArmorMeshUserData,
|
|
||||||
material: new MeshBasicMaterial({
|
|
||||||
color: new Color(1, props.thickness / props.maxThickness, 0),
|
|
||||||
depthWrite: false,
|
|
||||||
colorWrite: !ornamental,
|
|
||||||
}),
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import {
|
|
||||||
MeshBasicMaterial,
|
|
||||||
Object3D,
|
|
||||||
ShaderMaterial,
|
|
||||||
WebGLRenderTarget,
|
|
||||||
} from 'three';
|
|
||||||
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
|
|
||||||
import { ArmorMeshUserData } from '../Armor';
|
|
||||||
import fragmentShader from './shaders/fragment.glsl';
|
|
||||||
import vertexShader from './shaders/vertex.glsl';
|
|
||||||
|
|
||||||
export const spacedArmorDepthRenderTarget = new WebGLRenderTarget();
|
|
||||||
|
|
||||||
type ArmorMeshSpacedArmorDepthProps = {
|
|
||||||
ornamental?: boolean;
|
|
||||||
node: Object3D;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
include?: false;
|
|
||||||
isExternalModule: boolean;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
include: true;
|
|
||||||
thickness: number;
|
|
||||||
maxThickness: number;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const excludeMaterial = new MeshBasicMaterial({
|
|
||||||
colorWrite: false,
|
|
||||||
});
|
|
||||||
const includeMaterial = new MeshBasicMaterial({
|
|
||||||
colorWrite: false,
|
|
||||||
depthWrite: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ArmorMeshSpacedArmorDepth({
|
|
||||||
ornamental = false,
|
|
||||||
node,
|
|
||||||
...props
|
|
||||||
}: ArmorMeshSpacedArmorDepthProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!props.include &&
|
|
||||||
!ornamental &&
|
|
||||||
jsxTree(node, {
|
|
||||||
renderOrder: props.isExternalModule ? 2 : 0,
|
|
||||||
material: excludeMaterial,
|
|
||||||
})}
|
|
||||||
|
|
||||||
{props.include &&
|
|
||||||
jsxTree(node, {
|
|
||||||
renderOrder: 1,
|
|
||||||
onClick: ornamental ? () => {} : undefined,
|
|
||||||
userData: {
|
|
||||||
type: 'spacedArmor',
|
|
||||||
thickness: props.thickness,
|
|
||||||
} satisfies ArmorMeshUserData,
|
|
||||||
material: ornamental
|
|
||||||
? includeMaterial
|
|
||||||
: new ShaderMaterial({
|
|
||||||
fragmentShader,
|
|
||||||
vertexShader,
|
|
||||||
uniforms: {
|
|
||||||
thickness: { value: props.thickness },
|
|
||||||
maxThickness: { value: props.maxThickness },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#define PI 3.1415926535897932384626433832795
|
|
||||||
|
|
||||||
varying vec3 vViewPosition;
|
|
||||||
varying vec3 vNormal;
|
|
||||||
uniform float thickness;
|
|
||||||
uniform float maxThickness;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float angle = acos(dot(vNormal, -normalize(vViewPosition)));
|
|
||||||
gl_FragColor = vec4(angle / (PI / 2.0), thickness / maxThickness, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './components/Armor';
|
|
||||||
export * from './components/ExternalModuleMask';
|
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
GrayColor,
|
GrayColor,
|
||||||
PALETTES,
|
PALETTES,
|
||||||
} from '../../../constants/radixColors';
|
} from '../../../constants/radixColors';
|
||||||
import { TREE_TYPE_ICONS, TankType, TreeType } from '../../Tanks';
|
import { TREE_TYPE_ICONS, TankClass, TreeType } from '../../Tanks';
|
||||||
import { RowStat } from './RowStat';
|
import { RowStat } from './RowStat';
|
||||||
|
|
||||||
export interface RowStatItem {
|
export interface RowStatItem {
|
||||||
@@ -22,7 +22,7 @@ interface RowProps {
|
|||||||
title: string;
|
title: string;
|
||||||
minimized?: boolean;
|
minimized?: boolean;
|
||||||
treeType?: TreeType;
|
treeType?: TreeType;
|
||||||
tankType?: TankType;
|
tankType?: TankClass;
|
||||||
color?: AccentColor | GrayColor;
|
color?: AccentColor | GrayColor;
|
||||||
stats: (RowStatItem | undefined)[];
|
stats: (RowStatItem | undefined)[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function RowStat({
|
|||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.textHighContrast,
|
color: theme.textHighContrast,
|
||||||
fontWeight: 'bold',
|
fontWeight: 700,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export interface WrapperProps {
|
interface CommandWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
naked?: boolean;
|
|
||||||
fat?: boolean;
|
fat?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Wrapper({ children, naked, fat }: WrapperProps) {
|
export default function CommandWrapper({ children, fat }: CommandWrapperProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: `16px ${fat ? 0 : 16}px`,
|
padding: `16px ${fat ? 0 : 16}px`,
|
||||||
gap: 32,
|
gap: 16,
|
||||||
width: 480,
|
width: 480,
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
|
|
||||||
...(!naked && {
|
|
||||||
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||||
|
import { translator } from '../../core/localization/translator';
|
||||||
import isNumber from '../../core/math/isNumber';
|
import isNumber from '../../core/math/isNumber';
|
||||||
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
||||||
import { Card } from './components/Card';
|
import { Card } from './components/Card';
|
||||||
@@ -8,12 +10,15 @@ import { Row } from './components/Row';
|
|||||||
export interface GenericAllStatsProps {
|
export interface GenericAllStatsProps {
|
||||||
stats: AllStats;
|
stats: AllStats;
|
||||||
supplementaryStats?: SupplementaryStats;
|
supplementaryStats?: SupplementaryStats;
|
||||||
|
locale: Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GenericAllStats({
|
export default function GenericAllStats({
|
||||||
stats,
|
stats,
|
||||||
supplementaryStats,
|
supplementaryStats,
|
||||||
|
locale,
|
||||||
}: GenericAllStatsProps) {
|
}: GenericAllStatsProps) {
|
||||||
|
const { t } = translator(locale);
|
||||||
const damageRatio = stats.damage_dealt / stats.damage_received;
|
const damageRatio = stats.damage_dealt / stats.damage_received;
|
||||||
const killsToDeathRatio =
|
const killsToDeathRatio =
|
||||||
stats.frags / (stats.battles - stats.survived_battles);
|
stats.frags / (stats.battles - stats.survived_battles);
|
||||||
@@ -22,27 +27,27 @@ export default function GenericAllStats({
|
|||||||
<Root>
|
<Root>
|
||||||
<Row>
|
<Row>
|
||||||
<Card
|
<Card
|
||||||
title="Games"
|
title={t`bot.commands.full_stats.body.cards.games`}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: 'Winrate',
|
label: t`bot.commands.full_stats.body.cards.games.winrate`,
|
||||||
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
|
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Battles',
|
label: t`bot.commands.full_stats.body.cards.games.battles`,
|
||||||
value: stats.battles,
|
value: stats.battles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Wins / Losses',
|
label: t`bot.commands.full_stats.body.cards.games.wins_losses`,
|
||||||
value: `${stats.wins} / ${stats.losses}`,
|
value: `${stats.wins} / ${stats.losses}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Efficiency"
|
title={t`bot.commands.full_stats.body.cards.efficiency`}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: 'WN8',
|
label: t`bot.commands.full_stats.body.cards.efficiency.wn8`,
|
||||||
value:
|
value:
|
||||||
supplementaryStats && isNumber(supplementaryStats.WN8)
|
supplementaryStats && isNumber(supplementaryStats.WN8)
|
||||||
? supplementaryStats.WN8!.toFixed(0)
|
? supplementaryStats.WN8!.toFixed(0)
|
||||||
@@ -52,14 +57,14 @@ export default function GenericAllStats({
|
|||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Survival',
|
label: t`bot.commands.full_stats.body.cards.efficiency.survival`,
|
||||||
value: `${(
|
value: `${(
|
||||||
100 *
|
100 *
|
||||||
(stats.survived_battles / stats.battles)
|
(stats.survived_battles / stats.battles)
|
||||||
).toFixed(2)}%`,
|
).toFixed(2)}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Accuracy',
|
label: t`bot.commands.full_stats.body.cards.efficiency.accuracy`,
|
||||||
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
|
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -67,14 +72,14 @@ export default function GenericAllStats({
|
|||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Card
|
<Card
|
||||||
title="Lethality"
|
title={t`bot.commands.full_stats.body.cards.lethality`}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: 'Average damage',
|
label: t`bot.commands.full_stats.body.cards.lethality.average_damage`,
|
||||||
value: (stats.damage_dealt / stats.battles).toFixed(0),
|
value: (stats.damage_dealt / stats.battles).toFixed(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Damage per tier',
|
label: t`bot.commands.full_stats.body.cards.lethality.damage_per_tier`,
|
||||||
value: supplementaryStats?.tier
|
value: supplementaryStats?.tier
|
||||||
? (
|
? (
|
||||||
stats.damage_dealt /
|
stats.damage_dealt /
|
||||||
@@ -84,32 +89,32 @@ export default function GenericAllStats({
|
|||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Damage ratio',
|
label: t`bot.commands.full_stats.body.cards.lethality.damage_ratio`,
|
||||||
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
|
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Average kills',
|
label: t`bot.commands.full_stats.body.cards.lethality.average_kills`,
|
||||||
value: (stats.frags / stats.battles).toFixed(0),
|
value: (stats.frags / stats.battles).toFixed(0),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Miscellaneous"
|
title={t`bot.commands.full_stats.body.cards.miscellaneous`}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: 'Average tier',
|
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_tier`,
|
||||||
value: supplementaryStats?.tier?.toFixed(2),
|
value: supplementaryStats?.tier?.toFixed(2),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Average spots',
|
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_spots`,
|
||||||
value: (stats.spotted / stats.battles).toFixed(2),
|
value: (stats.spotted / stats.battles).toFixed(2),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Average XP',
|
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_xp`,
|
||||||
value: (stats.xp / stats.battles).toFixed(0),
|
value: (stats.xp / stats.battles).toFixed(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kills to death ratio',
|
label: t`bot.commands.full_stats.body.cards.miscellaneous.kills_to_death_ratio`,
|
||||||
value: isFinite(killsToDeathRatio)
|
value: isFinite(killsToDeathRatio)
|
||||||
? killsToDeathRatio.toFixed(2)
|
? killsToDeathRatio.toFixed(2)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
|||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
fontWeight: 'bold',
|
fontWeight: 700,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { createPortal, useFrame, useThree } from '@react-three/fiber';
|
|
||||||
import { isInteger } from 'lodash';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Scene } from 'three';
|
|
||||||
|
|
||||||
interface HeadsUpDisplayProps {
|
|
||||||
priority?: number;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Renders `children` above the default scene, even if the priority is a
|
|
||||||
* negative number.
|
|
||||||
*/
|
|
||||||
export function HeadsUpDisplay({
|
|
||||||
priority = 1,
|
|
||||||
children,
|
|
||||||
}: HeadsUpDisplayProps) {
|
|
||||||
const { gl: renderer, scene: defaultScene, camera } = useThree();
|
|
||||||
const scene = new Scene();
|
|
||||||
const portal = createPortal(children, scene, {
|
|
||||||
events: { priority: priority + 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
useFrame(() => {
|
|
||||||
if (priority === 1) {
|
|
||||||
renderer.clear();
|
|
||||||
renderer.render(defaultScene, camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.clearDepth();
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}, priority);
|
|
||||||
|
|
||||||
renderer.autoClear = false;
|
|
||||||
if (!isInteger(priority)) {
|
|
||||||
throw new TypeError('Priority property must be an integer');
|
|
||||||
}
|
|
||||||
|
|
||||||
return portal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this component if the default scene disappears on usage of the
|
|
||||||
* HeadsUpDisplay; caused because there are not HeadsUpDisplay components
|
|
||||||
* with `1` priority.
|
|
||||||
*/
|
|
||||||
export function RenderDefault() {
|
|
||||||
const { gl: renderer, scene, camera } = useThree();
|
|
||||||
|
|
||||||
useFrame(() => renderer.render(scene, camera));
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { MeshProps } from '@react-three/fiber';
|
|
||||||
import { forwardRef } from 'react';
|
|
||||||
import { DoubleSide, Mesh } from 'three';
|
|
||||||
|
|
||||||
interface InfiniteGridHelperProps extends MeshProps {
|
|
||||||
size1?: number;
|
|
||||||
size2?: number;
|
|
||||||
color?: THREE.Color;
|
|
||||||
distance?: number;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Thanks, Fyrestar, for [THREE.InfiniteGridHelper](https://github.com/Fyrestar/THREE.InfiniteGridHelper/)!
|
|
||||||
*/
|
|
||||||
const InfiniteGridHelper = forwardRef<Mesh, InfiniteGridHelperProps>(
|
|
||||||
({ size1, size2, color, distance, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<mesh {...props} ref={ref} frustumCulled={false}>
|
|
||||||
<shaderMaterial
|
|
||||||
args={[
|
|
||||||
{
|
|
||||||
side: DoubleSide,
|
|
||||||
uniforms: {
|
|
||||||
uSize1: { value: size1 },
|
|
||||||
uSize2: { value: size2 },
|
|
||||||
uColor: { value: color },
|
|
||||||
uDistance: { value: distance },
|
|
||||||
},
|
|
||||||
extensions: { derivatives: true },
|
|
||||||
transparent: true,
|
|
||||||
|
|
||||||
vertexShader: `
|
|
||||||
varying vec3 worldPosition;
|
|
||||||
uniform float uDistance;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec3 pos = position.xzy * uDistance;
|
|
||||||
worldPosition = pos;
|
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
|
|
||||||
fragmentShader: `
|
|
||||||
varying vec3 worldPosition;
|
|
||||||
uniform float uSize1;
|
|
||||||
uniform float uSize2;
|
|
||||||
uniform vec3 uColor;
|
|
||||||
uniform float uDistance;
|
|
||||||
vec2 origin = vec2(0.0, 0.0);
|
|
||||||
|
|
||||||
float getGrid(float size) {
|
|
||||||
vec2 r = worldPosition.xz / size;
|
|
||||||
vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
|
|
||||||
float line = min(grid.x, grid.y);
|
|
||||||
|
|
||||||
return (1.0 - min(line, 1.0)) * max(0.0, min(1.0, cameraPosition.y / 5.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float d = 1.0 - min(distance(origin, worldPosition.xz) / uDistance, 1.0);
|
|
||||||
float g1 = getGrid(uSize1);
|
|
||||||
float g2 = getGrid(uSize2);
|
|
||||||
gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 10.0));
|
|
||||||
gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
|
|
||||||
|
|
||||||
if ( gl_FragColor.a <= 0.0 ) discard;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<planeGeometry />
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
export default InfiniteGridHelper;
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export enum NoDataType {
|
export enum NoDataType {
|
||||||
@@ -7,16 +9,13 @@ export enum NoDataType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NoDataProps {
|
export interface NoDataProps {
|
||||||
type: NoDataType;
|
type: 'battles_in_period' | 'players_in_period' | 'tanks_found';
|
||||||
|
locale: Locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NO_DATA_MESSAGES: Record<NoDataType, string> = {
|
export default function NoData({ type, locale }: NoDataProps) {
|
||||||
[NoDataType.BattlesInPeriod]: 'No battles played in this period',
|
const { translate } = translator(locale);
|
||||||
[NoDataType.PlayersInPeriod]: 'No players in this period',
|
|
||||||
[NoDataType.TanksFound]: 'No tanks found',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function NoData({ type }: NoDataProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -26,7 +25,7 @@ export default function NoData({ type }: NoDataProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||||
{NO_DATA_MESSAGES[type]}
|
{translate(`bot.common.no_data.${type}`)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
import { Card, Flex, Text } from '@radix-ui/themes';
|
|
||||||
import { Html } from '@react-three/drei';
|
|
||||||
import { useFrame } from '@react-three/fiber';
|
|
||||||
import { ReactNode, useEffect, useRef } from 'react';
|
|
||||||
import { Group, Mesh, Vector3 } from 'three';
|
|
||||||
import { radToDeg } from 'three/src/math/MathUtils';
|
|
||||||
import { isExplosive } from '../core/blitz/isExplosive';
|
|
||||||
import { AngledPenetration } from '../icons/AngledPenetration';
|
|
||||||
import { Block } from '../icons/Block';
|
|
||||||
import { NominalPenetration } from '../icons/NominalPenetration';
|
|
||||||
import { Ricochet } from '../icons/Ricochet';
|
|
||||||
import { useDuel } from '../stores/duel';
|
|
||||||
import { Shot, useTankopediaTemporary } from '../stores/tankopedia';
|
|
||||||
|
|
||||||
export const SHOT_NAMES: Record<Shot['type'], string> = {
|
|
||||||
ricochet: 'Ricochet',
|
|
||||||
penetration: 'Penetration',
|
|
||||||
block: 'Blocked',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SHOT_ICONS: Record<Shot['type'], ReactNode> = {
|
|
||||||
ricochet: <Ricochet width={24} height={24} />,
|
|
||||||
block: <Block width={24} height={24} />,
|
|
||||||
penetration: <NominalPenetration width={24} height={24} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
const THICKNESS = 0.05;
|
|
||||||
const LENGTH = 4;
|
|
||||||
|
|
||||||
export function ShotDisplay() {
|
|
||||||
const shot = useTankopediaTemporary((state) => state.shot);
|
|
||||||
const entryWrapper = useRef<Group>(null);
|
|
||||||
const entryLine = useRef<Mesh>(null);
|
|
||||||
const circle = useRef<Mesh>(null);
|
|
||||||
const ricochetCircle = useRef<Mesh>(null);
|
|
||||||
const exitWrapper = useRef<Group>(null);
|
|
||||||
const exitLine = useRef<Mesh>(null);
|
|
||||||
const secondLineLength = shot?.ricochet ? shot?.ricochet.distance : LENGTH;
|
|
||||||
const number = shot && (shot.thicknesses.length > 1 || shot.ricochet);
|
|
||||||
const shell = useDuel((state) => state.antagonist!.shell);
|
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
|
||||||
const t1 = clock.elapsedTime % 1;
|
|
||||||
const t2 = (clock.elapsedTime + 0.5) % 1;
|
|
||||||
|
|
||||||
entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH);
|
|
||||||
entryLine.current?.scale.set(1, -2 * Math.abs(t1 - 0.5) + 1, 1);
|
|
||||||
exitLine.current?.position.set(0, 0, t2 * secondLineLength);
|
|
||||||
exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!shot) return;
|
|
||||||
|
|
||||||
const point = new Vector3(...shot.point);
|
|
||||||
const shellNormal = new Vector3(...shot.shellNormal);
|
|
||||||
const surfaceNormal = new Vector3(...shot.surfaceNormal);
|
|
||||||
const bounceNormal = shellNormal
|
|
||||||
.clone()
|
|
||||||
.multiplyScalar(-1)
|
|
||||||
.sub(
|
|
||||||
surfaceNormal
|
|
||||||
.clone()
|
|
||||||
.multiplyScalar(
|
|
||||||
2 * surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
entryWrapper.current?.position.set(...shot.point);
|
|
||||||
entryWrapper.current?.lookAt(point.clone().add(shellNormal));
|
|
||||||
exitWrapper.current?.position.set(...shot.point);
|
|
||||||
exitWrapper.current?.lookAt(point.clone().add(bounceNormal));
|
|
||||||
circle.current?.position.set(...shot.point);
|
|
||||||
circle.current?.lookAt(point.clone().add(surfaceNormal));
|
|
||||||
}, [shot]);
|
|
||||||
|
|
||||||
if (!shot) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<mesh ref={circle}>
|
|
||||||
<torusGeometry args={[0.05, 0.0125, 4, 16]} />
|
|
||||||
<meshBasicMaterial
|
|
||||||
color={shot.type === 'penetration' ? 'green' : 'red'}
|
|
||||||
depthTest={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<group ref={entryWrapper}>
|
|
||||||
<Html
|
|
||||||
position={[0, 0, 0]}
|
|
||||||
style={{
|
|
||||||
paddingLeft: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
style={{
|
|
||||||
width: number ? 300 : 200,
|
|
||||||
userSelect: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex direction="column" gap="1">
|
|
||||||
<Flex align="center" gap="2">
|
|
||||||
{SHOT_ICONS[shot.type]}
|
|
||||||
<Text>
|
|
||||||
{SHOT_NAMES[shot.type]} at {Math.round(radToDeg(shot.angle))}°
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
style={{
|
|
||||||
paddingLeft: number ? 8 : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{shot.thicknesses.map((layer, index) => (
|
|
||||||
<Flex gap="2" key={index}>
|
|
||||||
{number && (
|
|
||||||
<Text>
|
|
||||||
{index + 1}. {layer.type[0].toUpperCase()}
|
|
||||||
{layer.type.slice(1)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex gap="1">
|
|
||||||
{layer.type !== 'gap' && (
|
|
||||||
<NominalPenetration width={24} height={24} />
|
|
||||||
)}
|
|
||||||
<Text>
|
|
||||||
{Math.round(
|
|
||||||
layer.type === 'gap'
|
|
||||||
? layer.gap * 1000
|
|
||||||
: layer.nominal,
|
|
||||||
)}
|
|
||||||
mm
|
|
||||||
{layer.type === 'gap' && isExplosive(shell.type)
|
|
||||||
? ` (-${Math.round(Math.min(100, 0.5 * layer.gap * 100))}% penetration)`
|
|
||||||
: ''}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
{layer.type !== 'external' && layer.type !== 'gap' && (
|
|
||||||
<Flex gap="1">
|
|
||||||
<AngledPenetration width={24} height={24} />
|
|
||||||
<Text>{Math.round(layer.angled)}mm</Text>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{shot.ricochet && (
|
|
||||||
<>
|
|
||||||
<Flex gap="1">
|
|
||||||
<Text>{shot.thicknesses.length + 1}. Ricochet</Text>
|
|
||||||
<Ricochet width={24} height={24} />
|
|
||||||
<Text>(-25% penetration)</Text>
|
|
||||||
</Flex>
|
|
||||||
{shot.ricochet.penetration ? (
|
|
||||||
<Flex gap="1">
|
|
||||||
<Text>{shot.thicknesses.length + 2}. Penetration</Text>
|
|
||||||
<NominalPenetration width={24} height={24} />
|
|
||||||
</Flex>
|
|
||||||
) : (
|
|
||||||
<Flex gap="1">
|
|
||||||
<Text>{shot.thicknesses.length + 2}. Blocked</Text>
|
|
||||||
<Block width={24} height={24} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Html>
|
|
||||||
|
|
||||||
<mesh
|
|
||||||
renderOrder={1}
|
|
||||||
position={[0, 0, LENGTH / 2]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
ref={entryLine}
|
|
||||||
>
|
|
||||||
<cylinderGeometry args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} />
|
|
||||||
<meshBasicMaterial color="#ffff80" />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}>
|
|
||||||
<cylinderGeometry args={[THICKNESS / 4, THICKNESS / 4, LENGTH, 8]} />
|
|
||||||
<meshBasicMaterial color="#ffffff" depthTest={false} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
{shot.type === 'ricochet' && (
|
|
||||||
<group ref={exitWrapper}>
|
|
||||||
<mesh
|
|
||||||
renderOrder={1}
|
|
||||||
position={[0, 0, secondLineLength / 2]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
ref={exitLine}
|
|
||||||
>
|
|
||||||
<cylinderGeometry
|
|
||||||
args={[THICKNESS / 2, THICKNESS / 2, secondLineLength, 8]}
|
|
||||||
/>
|
|
||||||
<meshBasicMaterial color="#ff8080" />
|
|
||||||
</mesh>
|
|
||||||
<mesh
|
|
||||||
position={[0, 0, secondLineLength / 2]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
>
|
|
||||||
<cylinderGeometry
|
|
||||||
args={[THICKNESS / 4, THICKNESS / 4, secondLineLength, 8]}
|
|
||||||
/>
|
|
||||||
<meshBasicMaterial color="#ffffff" depthTest={false} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{shot.ricochet && (
|
|
||||||
<mesh ref={ricochetCircle} position={[0, 0, secondLineLength]}>
|
|
||||||
<icosahedronGeometry args={[0.05, 1]} />
|
|
||||||
<meshBasicMaterial
|
|
||||||
color={shot.ricochet.penetration ? 'green' : 'red'}
|
|
||||||
depthTest={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
</group>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,18 +5,19 @@ import {
|
|||||||
} from '../../../core/blitzkrieg/tankDefinitions';
|
} from '../../../core/blitzkrieg/tankDefinitions';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export const TANK_TYPES = [
|
export const TANK_CLASSES = [
|
||||||
'light',
|
'lightTank',
|
||||||
'medium',
|
'mediumTank',
|
||||||
'heavy',
|
'heavyTank',
|
||||||
'tankDestroyer',
|
'AT-SPG',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
|
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
|
||||||
|
|
||||||
export type TankType = (typeof TANK_TYPES)[number];
|
export type TankClass = (typeof TANK_CLASSES)[number];
|
||||||
export type TreeType = (typeof TREE_TYPES)[number];
|
export type TreeType = (typeof TREE_TYPES)[number];
|
||||||
|
|
||||||
|
// BIG TODO: REMOVE THIS AFTER LOCALIZATION
|
||||||
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
|
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
|
||||||
researchable: 'Tech tree',
|
researchable: 'Tech tree',
|
||||||
premium: 'Premium',
|
premium: 'Premium',
|
||||||
@@ -31,7 +32,7 @@ export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
|
|||||||
|
|
||||||
export interface ItemProps {
|
export interface ItemProps {
|
||||||
image?: string;
|
image?: string;
|
||||||
tankType?: TankType;
|
tankType?: TankClass;
|
||||||
name: string;
|
name: string;
|
||||||
treeType: TreeType;
|
treeType: TreeType;
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,7 @@ const TREE_TYPE_COLOR = {
|
|||||||
collector: '_blue',
|
collector: '_blue',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankType, string>> = {
|
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankClass, string>> = {
|
||||||
researchable: TANK_ICONS,
|
researchable: TANK_ICONS,
|
||||||
premium: TANK_ICONS_PREMIUM,
|
premium: TANK_ICONS_PREMIUM,
|
||||||
collector: TANK_ICONS_COLLECTOR,
|
collector: TANK_ICONS_COLLECTOR,
|
||||||
|
|||||||
+18
-41
@@ -1,75 +1,52 @@
|
|||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export interface TitleBarProps {
|
export interface TitleBarProps {
|
||||||
name: string;
|
title: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleBar({ name, image, description }: TitleBarProps) {
|
export default function TitleBar({ title, image, description }: TitleBarProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
gap: 4,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gap: 4,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
|
||||||
{image && (
|
{image && (
|
||||||
<img
|
<img
|
||||||
style={{ width: 64, height: 64, objectFit: 'contain' }}
|
style={{ width: 32, height: 32, objectFit: 'contain' }}
|
||||||
src={image}
|
src={image}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 4,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
color: theme.colors.textHighContrast,
|
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
whiteSpace: 'nowrap',
|
background: `linear-gradient(180deg, ${theme.colors.textHighContrast} 0%, ${theme.colors.textLowContrast} 100%)`,
|
||||||
overflow: 'hidden',
|
backgroundClip: 'text',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: image ? 'flex-start' : 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: theme.colors.textLowContrast,
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Vector3 } from 'three';
|
import { Vector3 } from 'three';
|
||||||
|
|
||||||
|
export const ZERO_VECTOR = new Vector3();
|
||||||
export const I_HAT = new Vector3(1, 0, 0);
|
export const I_HAT = new Vector3(1, 0, 0);
|
||||||
export const J_HAT = new Vector3(0, 1, 0);
|
export const J_HAT = new Vector3(0, 1, 0);
|
||||||
export const K_HAT = new Vector3(0, 0, 1);
|
export const K_HAT = new Vector3(0, 0, 1);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export const LEAGUES = [
|
export const LEAGUES = [
|
||||||
{ name: 'Diamond', minScore: 5000 },
|
{ name: 'diamond', minScore: 5000 },
|
||||||
{ name: 'Platinum', minScore: 4000 },
|
{ name: 'platinum', minScore: 4000 },
|
||||||
{ name: 'Gold', minScore: 3000 },
|
{ name: 'gold', minScore: 3000 },
|
||||||
{ name: 'Silver', minScore: 2000 },
|
{ name: 'silver', minScore: 2000 },
|
||||||
{ name: 'Bronze', minScore: 0 },
|
{ name: 'bronze', minScore: 0 },
|
||||||
];
|
];
|
||||||
@@ -3,21 +3,11 @@ export const REGIONS = ['com', 'eu', 'asia'] as const;
|
|||||||
export type Region = (typeof REGIONS)[number];
|
export type Region = (typeof REGIONS)[number];
|
||||||
export type RegionSubdomain = 'na' | 'eu' | 'asia';
|
export type RegionSubdomain = 'na' | 'eu' | 'asia';
|
||||||
|
|
||||||
export const REGION_NAMES: Record<Region, string> = {
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
export const UNLOCALIZED_REGION_NAMES: Record<Region, string> = {
|
||||||
com: 'North America',
|
com: 'North America',
|
||||||
eu: 'Europe',
|
|
||||||
asia: 'Asia',
|
asia: 'Asia',
|
||||||
};
|
|
||||||
|
|
||||||
// https://planetarynames.wr.usgs.gov/Abbreviations
|
|
||||||
export const REGION_NAMES_SHORT: Record<Region, string> = {
|
|
||||||
com: 'NA',
|
|
||||||
eu: 'EU',
|
|
||||||
asia: 'ASIA',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
|
||||||
na: 'North America',
|
|
||||||
eu: 'Europe',
|
eu: 'Europe',
|
||||||
asia: 'Asia',
|
|
||||||
};
|
};
|
||||||
@@ -1,39 +1,38 @@
|
|||||||
import { clamp } from 'lodash';
|
import { clamp } from 'lodash';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
import { PitchLimits, YawLimits } from '../blitzkrieg/modelDefinitions';
|
import { PitchLimits, YawLimits } from '../blitzkrieg/modelDefinitions';
|
||||||
import { normalizeAnglePI } from '../math/normalizeAngle180';
|
import { normalizeAngleRad } from '../math/normalizeAngleRad';
|
||||||
|
|
||||||
export function applyPitchYawLimits(
|
export function applyPitchYawLimits(
|
||||||
pitch: number, // rad
|
pitch: number, // rad
|
||||||
yaw: number, // rad
|
yaw: number, // rad
|
||||||
pitchLimits: PitchLimits, // deg
|
pitchLimits: PitchLimits, // deg
|
||||||
yawLimits?: YawLimits, // deg
|
yawLimits?: YawLimits, // deg
|
||||||
|
verticalStabilizer = false,
|
||||||
) {
|
) {
|
||||||
let newPitch = pitch;
|
let newPitch = pitch;
|
||||||
let newYaw = yaw;
|
let newYaw = yaw;
|
||||||
|
|
||||||
if (yawLimits) {
|
if (yawLimits) {
|
||||||
newYaw = normalizeAnglePI(
|
newYaw = normalizeAngleRad(
|
||||||
clamp(
|
clamp(newYaw, -degToRad(yawLimits.max), -degToRad(yawLimits.min)),
|
||||||
newYaw,
|
|
||||||
-yawLimits.max * (Math.PI / 180),
|
|
||||||
-yawLimits.min * (Math.PI / 180),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lowerPitch = -pitchLimits.max * (Math.PI / 180);
|
const pitchBuff = verticalStabilizer ? degToRad(3) : 0;
|
||||||
let upperPitch = -pitchLimits.min * (Math.PI / 180);
|
let lowerPitch = -degToRad(pitchLimits.max) - pitchBuff;
|
||||||
const transition = (pitchLimits.transition ?? 10) * (Math.PI / 180);
|
let upperPitch = -degToRad(pitchLimits.min) + pitchBuff;
|
||||||
|
const transition = degToRad(pitchLimits.transition ?? 10);
|
||||||
|
|
||||||
if (pitchLimits.back) {
|
if (pitchLimits.back) {
|
||||||
const range = pitchLimits.back.range * (Math.PI / 180);
|
const range = degToRad(pitchLimits.back.range);
|
||||||
const yawRotatedAbs = Math.abs(normalizeAnglePI(newYaw - Math.PI));
|
const yawRotatedAbs = Math.abs(normalizeAngleRad(newYaw - Math.PI));
|
||||||
|
|
||||||
if (yawRotatedAbs <= range + transition) {
|
if (yawRotatedAbs <= range + transition) {
|
||||||
if (yawRotatedAbs <= range) {
|
if (yawRotatedAbs <= range) {
|
||||||
// inside range
|
// inside range
|
||||||
lowerPitch = -pitchLimits.back.max * (Math.PI / 180);
|
lowerPitch = -degToRad(pitchLimits.back.max);
|
||||||
upperPitch = -pitchLimits.back.min * (Math.PI / 180);
|
upperPitch = -degToRad(pitchLimits.back.min);
|
||||||
} else {
|
} else {
|
||||||
// inside transition
|
// inside transition
|
||||||
const transitionProgress = (yawRotatedAbs - range) / transition;
|
const transitionProgress = (yawRotatedAbs - range) / transition;
|
||||||
@@ -44,21 +43,21 @@ export function applyPitchYawLimits(
|
|||||||
(1 - transitionProgress) * pitchLimits.back.min +
|
(1 - transitionProgress) * pitchLimits.back.min +
|
||||||
transitionProgress * pitchLimits.min;
|
transitionProgress * pitchLimits.min;
|
||||||
|
|
||||||
lowerPitch = -maxInterpolatedPitch * (Math.PI / 180);
|
lowerPitch = -degToRad(maxInterpolatedPitch);
|
||||||
upperPitch = -minInterpolatedPitch * (Math.PI / 180);
|
upperPitch = -degToRad(minInterpolatedPitch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pitchLimits.front) {
|
if (pitchLimits.front) {
|
||||||
const range = pitchLimits.front.range * (Math.PI / 180);
|
const range = degToRad(pitchLimits.front.range);
|
||||||
const yawAbs = Math.abs(normalizeAnglePI(newYaw));
|
const yawAbs = Math.abs(normalizeAngleRad(newYaw));
|
||||||
|
|
||||||
if (yawAbs <= range + transition) {
|
if (yawAbs <= range + transition) {
|
||||||
if (yawAbs <= range) {
|
if (yawAbs <= range) {
|
||||||
// inside range
|
// inside range
|
||||||
lowerPitch = -pitchLimits.front.max * (Math.PI / 180);
|
lowerPitch = -degToRad(pitchLimits.front.max);
|
||||||
upperPitch = -pitchLimits.front.min * (Math.PI / 180);
|
upperPitch = -degToRad(pitchLimits.front.min);
|
||||||
} else {
|
} else {
|
||||||
// inside transition
|
// inside transition
|
||||||
const transitionProgress = (yawAbs - range) / transition;
|
const transitionProgress = (yawAbs - range) / transition;
|
||||||
@@ -69,8 +68,8 @@ export function applyPitchYawLimits(
|
|||||||
(1 - transitionProgress) * pitchLimits.front.min +
|
(1 - transitionProgress) * pitchLimits.front.min +
|
||||||
transitionProgress * pitchLimits.min;
|
transitionProgress * pitchLimits.min;
|
||||||
|
|
||||||
lowerPitch = -maxInterpolatedPitch * (Math.PI / 180);
|
lowerPitch = -degToRad(maxInterpolatedPitch);
|
||||||
upperPitch = -minInterpolatedPitch * (Math.PI / 180);
|
upperPitch = -degToRad(minInterpolatedPitch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
import { UserError } from '../../hooks/userError';
|
import { UserError } from '../../hooks/userError';
|
||||||
import { TanksStats } from '../../types/tanksStats';
|
import { TanksStats } from '../../types/tanksStats';
|
||||||
|
import { translator } from '../localization/translator';
|
||||||
import fetchBlitz from './fetchBlitz';
|
import fetchBlitz from './fetchBlitz';
|
||||||
|
|
||||||
export default async function getTankStats(region: Region, id: number) {
|
export default async function getTankStats(
|
||||||
|
region: Region,
|
||||||
|
id: number,
|
||||||
|
locale: Locale,
|
||||||
|
) {
|
||||||
|
const { t } = translator(locale);
|
||||||
const tankStats = await fetchBlitz<TanksStats>(
|
const tankStats = await fetchBlitz<TanksStats>(
|
||||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tankStats[id] === null) {
|
if (tankStats[id] === null) {
|
||||||
throw new UserError(
|
throw new UserError(t`bot.common.errors.no_tank_stats`);
|
||||||
"# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankStats[id];
|
return tankStats[id];
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import { UserError } from '../../hooks/userError';
|
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
|
import { UserError } from '../../hooks/userError';
|
||||||
import {
|
import {
|
||||||
tankDefinitions,
|
tankDefinitions,
|
||||||
tanksDefinitionsArray,
|
tanksDefinitionsArray,
|
||||||
} from '../blitzkrieg/tankDefinitions';
|
} from '../blitzkrieg/tankDefinitions';
|
||||||
|
import { translator } from '../localization/translator';
|
||||||
|
|
||||||
export default async function resolveTankId(tank: string | number) {
|
export default async function resolveTankId(
|
||||||
const number = typeof tank === 'string' ? parseInt(tank) : tank;
|
tank: string | number,
|
||||||
|
locale: Locale,
|
||||||
|
) {
|
||||||
|
const { translate } = translator(locale);
|
||||||
|
const number = typeof tank === 'string' ? Number(tank) : tank;
|
||||||
|
|
||||||
if (Number.isNaN(number)) {
|
if (Number.isNaN(number)) {
|
||||||
const searchResult = go(`${tank}`, await tanksDefinitionsArray, {
|
const searchResult = go(`${tank}`, await tanksDefinitionsArray, {
|
||||||
@@ -17,7 +23,9 @@ export default async function resolveTankId(tank: string | number) {
|
|||||||
|
|
||||||
if (searchResult.length === 0) {
|
if (searchResult.length === 0) {
|
||||||
throw new UserError(
|
throw new UserError(
|
||||||
`# Tank not found\nCould not find tank by the name "${markdownEscape(`${tank}`)}".`,
|
translate('bot.common.errors.tank_name_not_found', [
|
||||||
|
markdownEscape(`${tank}`),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return searchResult[0].obj.id;
|
return searchResult[0].obj.id;
|
||||||
@@ -27,7 +35,7 @@ export default async function resolveTankId(tank: string | number) {
|
|||||||
return number;
|
return number;
|
||||||
} else {
|
} else {
|
||||||
throw new UserError(
|
throw new UserError(
|
||||||
`# Tank not found\nCould not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`,
|
translate('bot.common.errors.tank_id_not_found', [`${number}`]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
import {
|
import {
|
||||||
DuelMember,
|
DuelMember,
|
||||||
genericDefaultEquipmentMatrix,
|
genericDefaultEquipmentMatrix,
|
||||||
mutateDuel,
|
mutateDuel,
|
||||||
} from '../../stores/duel';
|
} from '../../stores/duel';
|
||||||
import { availableProvisions } from './availableProvisions';
|
import { availableProvisions } from './availableProvisions';
|
||||||
|
import { modelDefinitions } from './modelDefinitions';
|
||||||
import { provisionDefinitions } from './provisionDefinitions';
|
import { provisionDefinitions } from './provisionDefinitions';
|
||||||
import { tankDefinitions } from './tankDefinitions';
|
import { tankDefinitions } from './tankDefinitions';
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ export async function assignDuelMember(
|
|||||||
) {
|
) {
|
||||||
const awaitedTankDefinitions = await tankDefinitions;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const awaitedProvisionDefinitions = await provisionDefinitions;
|
const awaitedProvisionDefinitions = await provisionDefinitions;
|
||||||
|
const awaitedModelDefinitions = await modelDefinitions;
|
||||||
|
const tankModelDefinition = awaitedModelDefinitions[id];
|
||||||
const tank = awaitedTankDefinitions[id];
|
const tank = awaitedTankDefinitions[id];
|
||||||
const turret = tank.turrets.at(-1)!;
|
const turret = tank.turrets.at(-1)!;
|
||||||
const gun = turret.guns.at(-1)!;
|
const gun = turret.guns.at(-1)!;
|
||||||
@@ -32,7 +36,7 @@ export async function assignDuelMember(
|
|||||||
shell,
|
shell,
|
||||||
track,
|
track,
|
||||||
equipment: genericDefaultEquipmentMatrix,
|
equipment: genericDefaultEquipmentMatrix,
|
||||||
pitch: 0,
|
pitch: degToRad(tankModelDefinition.turretRotation?.pitch ?? 0),
|
||||||
yaw: 0,
|
yaw: 0,
|
||||||
camouflage: true,
|
camouflage: true,
|
||||||
consumables: [],
|
consumables: [],
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import { TREE_TYPE_NAMES } from '../../components/Tanks';
|
import { Locale } from 'discord.js';
|
||||||
import { encyclopediaInfo } from '../blitz/encyclopediaInfo';
|
import { translator } from '../localization/translator';
|
||||||
import { StatFilters } from '../statistics/filterStats';
|
import { StatFilters } from '../statistics/filterStats';
|
||||||
import { TIER_ROMAN_NUMERALS, Tier, tankDefinitions } from './tankDefinitions';
|
import { TIER_ROMAN_NUMERALS, Tier, tankDefinitions } from './tankDefinitions';
|
||||||
|
|
||||||
export async function filtersToDescription({
|
export async function filtersToDescription(
|
||||||
nation,
|
{ nation, tier, tankType, treeType, tank }: StatFilters,
|
||||||
tier,
|
locale: Locale,
|
||||||
tankType,
|
) {
|
||||||
treeType,
|
const { translate } = translator(locale);
|
||||||
tank,
|
|
||||||
}: StatFilters) {
|
|
||||||
const awaitedEncyclopediaInfo = await encyclopediaInfo;
|
|
||||||
const awaitedTankDefinitions = await tankDefinitions;
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
const info: string[] = [];
|
const info: string[] = [];
|
||||||
|
|
||||||
|
// TODO: somehow localize tank names?
|
||||||
if (tank) return awaitedTankDefinitions[tank].name;
|
if (tank) return awaitedTankDefinitions[tank].name;
|
||||||
if (nation) info.push(awaitedEncyclopediaInfo.vehicle_nations[nation]);
|
if (nation) info.push(translate(`common.nations.${nation}`));
|
||||||
if (tier) info.push(`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]}`);
|
if (tier) {
|
||||||
if (tankType) info.push(awaitedEncyclopediaInfo.vehicle_types[tankType]);
|
info.push(
|
||||||
if (treeType) info.push(TREE_TYPE_NAMES[treeType]);
|
translate('bot.common.filters.tier', [TIER_ROMAN_NUMERALS[tier as Tier]]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tankType) info.push(translate(`common.tank_class_short.${tankType}`));
|
||||||
|
if (treeType) info.push(translate(`common.tree_type.${treeType}`));
|
||||||
|
|
||||||
return info.length === 0 ? 'No filters' : info.join(', ');
|
return info.length === 0
|
||||||
|
? translate('bot.common.filters.none')
|
||||||
|
: info.join(', ');
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import {
|
import {
|
||||||
GetHistoriesOptions,
|
GetHistoriesOptions,
|
||||||
@@ -34,6 +35,7 @@ export const getTankHistoriesDefaultOptions: GetTankHistoriesOptions = {
|
|||||||
export default async function getTankHistories(
|
export default async function getTankHistories(
|
||||||
server: Region,
|
server: Region,
|
||||||
id: number,
|
id: number,
|
||||||
|
locale: Locale,
|
||||||
options?: Partial<GetTankHistoriesOptions>,
|
options?: Partial<GetTankHistoriesOptions>,
|
||||||
) {
|
) {
|
||||||
const mergedOptions = { ...getTankHistoriesDefaultOptions, ...options };
|
const mergedOptions = { ...getTankHistoriesDefaultOptions, ...options };
|
||||||
@@ -76,7 +78,7 @@ export default async function getTankHistories(
|
|||||||
const latestNodes: TankHistories = [];
|
const latestNodes: TankHistories = [];
|
||||||
|
|
||||||
if (mergedOptions.includeLatestHistories) {
|
if (mergedOptions.includeLatestHistories) {
|
||||||
(await getTankStats(server, id)).forEach((tankHistory) => {
|
(await getTankStats(server, id, locale)).forEach((tankHistory) => {
|
||||||
if (
|
if (
|
||||||
tankHistory.all.battles > (lastBattles[tankHistory.tank_id] ?? 0) &&
|
tankHistory.all.battles > (lastBattles[tankHistory.tank_id] ?? 0) &&
|
||||||
(mergedOptions.tankId === undefined ||
|
(mergedOptions.tankId === undefined ||
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useDuel } from '../../stores/duel';
|
import { useDuel } from '../../stores/duel';
|
||||||
import { equipmentDefinitions } from './equipmentDefinitions';
|
import { equipmentDefinitions } from './equipmentDefinitions';
|
||||||
|
|
||||||
export async function hasEquipment(id: number, antagonist = false) {
|
export async function hasEquipment(
|
||||||
|
id: number,
|
||||||
|
antagonist = false,
|
||||||
|
equipmentMatrix = useDuel.getState()[
|
||||||
|
antagonist ? 'antagonist' : 'protagonist'
|
||||||
|
]!.equipment,
|
||||||
|
) {
|
||||||
const awaitedEquipmentDefinitions = await equipmentDefinitions;
|
const awaitedEquipmentDefinitions = await equipmentDefinitions;
|
||||||
const member = useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!;
|
const member = useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!;
|
||||||
const equipmentRows =
|
const presetRows = awaitedEquipmentDefinitions.presets[member.tank.equipment];
|
||||||
awaitedEquipmentDefinitions.presets[member.tank.equipment];
|
return presetRows.some((equipmentRow, rowIndex) => {
|
||||||
const equipmentMatrix =
|
|
||||||
useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!.equipment;
|
|
||||||
|
|
||||||
return equipmentRows.some((equipmentRow, rowIndex) => {
|
|
||||||
return equipmentRow.some((equipment, columnIndex) => {
|
return equipmentRow.some((equipment, columnIndex) => {
|
||||||
const choice = equipmentMatrix[rowIndex][columnIndex];
|
const choice = equipmentMatrix[rowIndex][columnIndex];
|
||||||
if (choice === 0) return false;
|
if (choice === 0) return false;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { readFile } from 'fs/promises';
|
|
||||||
import satori from 'satori';
|
|
||||||
|
|
||||||
const FONT_NAME = 'Roboto';
|
|
||||||
const FONT_FILES = [
|
|
||||||
{ weight: 400 as const, path: 'src/assets/fonts/Roboto.ttf' },
|
|
||||||
{ weight: 700 as const, path: 'src/assets/fonts/Roboto-Bold.ttf' },
|
|
||||||
{ weight: 900 as const, path: 'src/assets/fonts/Roboto-Black.ttf' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const fonts = Promise.all(
|
|
||||||
FONT_FILES.map(({ path, weight }) =>
|
|
||||||
readFile(path).then((data) => ({
|
|
||||||
data: Buffer.from(data.buffer),
|
|
||||||
name: FONT_NAME,
|
|
||||||
weight,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
export default async function jsxToSvg(jsx: JSX.Element) {
|
|
||||||
return await satori(jsx, {
|
|
||||||
width: 480,
|
|
||||||
fonts: await fonts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user