mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7bbb17f66 | ||
|
|
f407ffe4bb | ||
|
|
791a09daae | ||
|
|
9592c0b55f | ||
|
|
d9a34dc60e | ||
|
|
30e4e4e9b3 | ||
|
|
bd3ea9a8ec | ||
|
|
8131b69422 | ||
|
|
340d9c2666 | ||
|
|
2a169fac34 | ||
|
|
4819100ec6 | ||
|
|
f39a94cbe0 | ||
|
|
e61598f768 | ||
|
|
04ee3a0a44 | ||
|
|
8fa613f1e2 | ||
|
|
bdc240df08 | ||
|
|
889ed7e191 | ||
|
|
1fb8dac805 | ||
|
|
9f71ac68b9 | ||
|
|
c1f3517a79 | ||
|
|
adb0af0f18 | ||
|
|
f79f1ffdc3 | ||
|
|
f02fb00e8c | ||
|
|
3d353cc3cf | ||
|
|
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 |
No files matched your search
@@ -2,10 +2,9 @@ name: CD Build Assets
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: dev
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
# schedule:
|
schedule:
|
||||||
# - cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -18,6 +17,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -46,8 +48,17 @@ jobs:
|
|||||||
- name: Run DepotDownloader
|
- name: Run DepotDownloader
|
||||||
run: ./temp/depotDownloader/DepotDownloader.exe -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
|
run: ./temp/depotDownloader/DepotDownloader.exe -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
|
||||||
|
|
||||||
|
- name: Apply patches
|
||||||
|
run: |
|
||||||
|
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||||
|
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||||
|
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
|
||||||
|
$env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}"
|
||||||
|
yarn setup:patches --depot
|
||||||
|
|
||||||
- name: Build assets
|
- name: Build assets
|
||||||
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' || '' }}
|
||||||
@@ -18,10 +18,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
uses: actions/deploy-pages@v1
|
uses: actions/deploy-pages@v1
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: docs/.vitepress/dist
|
path: docs/.vitepress/dist
|
||||||
@@ -6,6 +6,8 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */5 * * *'
|
- cron: '0 */5 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [created, published, released, edited]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -17,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -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' }}
|
||||||
@@ -14,10 +14,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -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' }}
|
||||||
@@ -14,10 +14,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -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' }}
|
||||||
@@ -13,10 +13,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
run: yarn build:bot
|
run: yarn build:bot
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: bot
|
name: bot
|
||||||
path: dist/bot
|
path: dist/bot
|
||||||
@@ -13,10 +13,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
run: yarn build:docs
|
run: yarn build:docs
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: docs
|
name: docs
|
||||||
path: dist/docs
|
path: dist/docs
|
||||||
@@ -13,10 +13,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
run: yarn build:website
|
run: yarn build:website
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: website
|
name: website
|
||||||
path: dist/website
|
path: dist/website
|
||||||
@@ -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
|
|
||||||
+2
-1
@@ -17,4 +17,5 @@ tsconfig.tsbuildinfo
|
|||||||
temp/
|
temp/
|
||||||
/.yarn
|
/.yarn
|
||||||
/.VSCodeCounter
|
/.VSCodeCounter
|
||||||
/.yarn
|
/.yarn
|
||||||
|
depots
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "submodules/closedkrieg"]
|
||||||
|
path = submodules/closedkrieg
|
||||||
|
url = https://github.com/tresabhi/closedkrieg
|
||||||
Vendored
+8
@@ -5,7 +5,9 @@
|
|||||||
"accountinfo",
|
"accountinfo",
|
||||||
"aeson",
|
"aeson",
|
||||||
"alphatest",
|
"alphatest",
|
||||||
|
"APAC",
|
||||||
"apcr",
|
"apcr",
|
||||||
|
"applypatches",
|
||||||
"artefacts",
|
"artefacts",
|
||||||
"atgm",
|
"atgm",
|
||||||
"AYUV",
|
"AYUV",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"CDON",
|
"CDON",
|
||||||
"classname",
|
"classname",
|
||||||
"cleandist",
|
"cleandist",
|
||||||
|
"closedkrieg",
|
||||||
"cockroackdb",
|
"cockroackdb",
|
||||||
"Coef",
|
"Coef",
|
||||||
"cooldown",
|
"cooldown",
|
||||||
@@ -40,8 +43,10 @@
|
|||||||
"donotremove",
|
"donotremove",
|
||||||
"drei",
|
"drei",
|
||||||
"Droodles",
|
"Droodles",
|
||||||
|
"dvpd",
|
||||||
"dvpl",
|
"dvpl",
|
||||||
"DVPLCRC",
|
"DVPLCRC",
|
||||||
|
"dvpm",
|
||||||
"dxgi",
|
"dxgi",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
"FASTNAME",
|
"FASTNAME",
|
||||||
@@ -70,6 +75,7 @@
|
|||||||
"lods",
|
"lods",
|
||||||
"metallicness",
|
"metallicness",
|
||||||
"minitelrose",
|
"minitelrose",
|
||||||
|
"Minotauro",
|
||||||
"MIPMAPCOUNT",
|
"MIPMAPCOUNT",
|
||||||
"modeltransform",
|
"modeltransform",
|
||||||
"MURICA",
|
"MURICA",
|
||||||
@@ -129,6 +135,7 @@
|
|||||||
"TYPELESS",
|
"TYPELESS",
|
||||||
"ubjson",
|
"ubjson",
|
||||||
"Unicum",
|
"Unicum",
|
||||||
|
"UNLOCALIZED",
|
||||||
"UNORM",
|
"UNORM",
|
||||||
"Unregistering",
|
"Unregistering",
|
||||||
"vickers",
|
"vickers",
|
||||||
@@ -138,6 +145,7 @@
|
|||||||
"webp",
|
"webp",
|
||||||
"winrate",
|
"winrate",
|
||||||
"wireframe",
|
"wireframe",
|
||||||
|
"WOTB",
|
||||||
"wotblitz",
|
"wotblitz",
|
||||||
"xffn",
|
"xffn",
|
||||||
"xkdkw",
|
"xkdkw",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
files:
|
||||||
|
- source: /src/lang/en-US.json
|
||||||
|
translation: /src/lang/%locale%.json
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Blitzkrieg 1.16.4
|
||||||
|
|
||||||
|
A few issues here and there addressed for good measure.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Armor layer penetration indices are now much more compact.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Reverted splash mechanics changes from `1.16.1` as it made them less accurate.
|
||||||
|
- Auto-reloaders now report their DPM stats correctly with crew training.
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- All files in the project were reviewed and constant exports were isolated.
|
||||||
|
- The downloaded game in the workflow now also includes the latest patches.
|
||||||
|
- Asset building now uses sharp under the hood which should in theory be faster.
|
||||||
|
- Fetching CDON LZ4 files now uses `no-store` instead of `no-cache`
|
||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.16.0",
|
"version": "1.16.4",
|
||||||
"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",
|
||||||
@@ -19,13 +19,15 @@
|
|||||||
"start:website": "next start",
|
"start:website": "next start",
|
||||||
"host:docs": "vitepress preview docs",
|
"host:docs": "vitepress preview docs",
|
||||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||||
|
"setup:patches": "vite-node --script scripts/applyPatches",
|
||||||
"lint": "npx tsc",
|
"lint": "npx tsc",
|
||||||
"lint:website": "next lint",
|
"lint:website": "next lint",
|
||||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { mkdir, writeFile } from 'fs/promises';
|
||||||
|
import { parse as parsePath } from 'path';
|
||||||
|
import { parse as parseYaml } from 'yaml';
|
||||||
|
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
|
||||||
|
import { writeDVPL } from '../src/core/blitz/writeDVPL';
|
||||||
|
import { secrets } from '../src/core/blitzkrieg/secrets';
|
||||||
|
import { dvp } from '../submodules/closedkrieg/src/dvp';
|
||||||
|
import { DATA } from './buildAssets/constants';
|
||||||
|
|
||||||
|
const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`);
|
||||||
|
const currentVersion = versionTextFile
|
||||||
|
.split(' ')[0]
|
||||||
|
.split('.')
|
||||||
|
.slice(0, 3)
|
||||||
|
.join('.');
|
||||||
|
|
||||||
|
console.log(`Installing patches for ${currentVersion}...`);
|
||||||
|
|
||||||
|
let patchIndex = 1;
|
||||||
|
while (true) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${secrets.WOTB_DLC_CDN}/dlc/s${currentVersion}_${patchIndex}.yaml`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log(`Applying patch ${patchIndex}...`);
|
||||||
|
|
||||||
|
const data = parseYaml(await response.text());
|
||||||
|
const dvpm = await fetch(`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11}`).then(
|
||||||
|
(response) => response.arrayBuffer(),
|
||||||
|
);
|
||||||
|
const dvpd = await fetch(
|
||||||
|
`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11.replace('.dvpm', '.dvpd')}`,
|
||||||
|
).then((response) => response.arrayBuffer());
|
||||||
|
const files = await dvp(dvpm, dvpd);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async ({ path, data }) => {
|
||||||
|
const { dir } = parsePath(path);
|
||||||
|
|
||||||
|
await mkdir(`${DATA}/${dir}`, { recursive: true });
|
||||||
|
await writeFile(`${DATA}/${path}.dvpl`, writeDVPL(Buffer.from(data)));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
patchIndex++;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ export const POI = {
|
|||||||
collisionMeshes: '3d/Tanks/CollisionMeshes',
|
collisionMeshes: '3d/Tanks/CollisionMeshes',
|
||||||
optionalDevices: 'XML/item_defs/vehicles/common/optional_devices.xml',
|
optionalDevices: 'XML/item_defs/vehicles/common/optional_devices.xml',
|
||||||
optionalDeviceSlots:
|
optionalDeviceSlots:
|
||||||
'XML/item_defs/vehicles/common/optional_device_slots.xml',
|
'XML/item_defs/vehicles/common/optional_device_slots.xml',
|
||||||
optionalDeviceImageMappings:
|
optionalDeviceImageMappings:
|
||||||
'UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml',
|
'UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml',
|
||||||
optionalDevicesImage:
|
optionalDevicesImage:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import { NodeIO } from '@gltf-transform/core';
|
import { NodeIO } from '@gltf-transform/core';
|
||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { Vector3Tuple } from 'three';
|
|
||||||
import { extractModel } from '../../src/core/blitz/extractModel';
|
import { extractModel } from '../../src/core/blitz/extractModel';
|
||||||
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';
|
||||||
// import { AssetCommit } from '../../src/core/github/assetCommit';
|
|
||||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||||
import { DATA, POI } from './constants';
|
import { DATA, POI } from './constants';
|
||||||
import { VehicleDefinitionList } from './definitions';
|
import { VehicleDefinitionList } from './definitions';
|
||||||
import { TankParameters } from './tankIcons';
|
import { TankParameters } from './tankIcons';
|
||||||
|
|
||||||
interface VehicleCustomization {
|
|
||||||
armorColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function tankModels(production: boolean) {
|
export async function tankModels(production: boolean) {
|
||||||
console.log('Building tank models...');
|
console.log('Building tank models...');
|
||||||
|
|
||||||
@@ -27,18 +21,9 @@ export async function tankModels(production: boolean) {
|
|||||||
for (const nationIndex in nations) {
|
for (const nationIndex in nations) {
|
||||||
const nation = nations[nationIndex];
|
const nation = nations[nationIndex];
|
||||||
const changes: FileChange[] = [];
|
const changes: FileChange[] = [];
|
||||||
// const assetCommit = new AssetCommit(`tank models ${nation}`, production);
|
|
||||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||||
);
|
);
|
||||||
const customization = await readXMLDVPL<{ root: VehicleCustomization }>(
|
|
||||||
`${DATA}/${POI.vehicleDefinitions}/${nation}/customization.xml.dvpl`,
|
|
||||||
);
|
|
||||||
const baseColor = customization.root.armorColor
|
|
||||||
.split(' ')
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(Number)
|
|
||||||
.map((channel) => channel / 255) as Vector3Tuple;
|
|
||||||
|
|
||||||
console.log(`Building models for ${nation}`);
|
console.log(`Building models for ${nation}`);
|
||||||
|
|
||||||
@@ -56,11 +41,8 @@ export async function tankModels(production: boolean) {
|
|||||||
const model = await extractModel(
|
const model = await extractModel(
|
||||||
DATA,
|
DATA,
|
||||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
||||||
baseColor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// writeFile(`test.${tankKey}.glb`, await nodeIO.writeBinary(model));
|
|
||||||
|
|
||||||
changes.push({
|
changes.push({
|
||||||
path: `3d/tanks/models/${id}.glb`,
|
path: `3d/tanks/models/${id}.glb`,
|
||||||
content: Buffer.from(await nodeIO.writeBinary(model)).toString(
|
content: Buffer.from(await nodeIO.writeBinary(model)).toString(
|
||||||
@@ -71,7 +53,6 @@ export async function tankModels(production: boolean) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// assetCommit.push();
|
|
||||||
await commitAssets(`tank models ${nation}`, changes, production);
|
await commitAssets(`tank models ${nation}`, changes, production);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import * as Breakdown from '../../../components/Breakdown';
|
import * as Breakdown from '../../../components/Breakdown';
|
||||||
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 { resetSession } from '../../../core/blitzkrieg/resetSession';
|
||||||
import {
|
import {
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
tankDefinitions,
|
tankDefinitions,
|
||||||
@@ -13,10 +14,7 @@ import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
|||||||
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
||||||
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
||||||
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
||||||
import mutateSession, {
|
import mutateSession, { useSession } from '../../../stores/session';
|
||||||
resetSession,
|
|
||||||
useSession,
|
|
||||||
} from '../../../stores/session';
|
|
||||||
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
||||||
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
||||||
import { Menu } from '../../tools/session/components/Menu';
|
import { Menu } from '../../tools/session/components/Menu';
|
||||||
@@ -311,7 +309,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];
|
||||||
|
|||||||
@@ -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,11 +18,13 @@ 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';
|
||||||
import { resetSession, setSession, useSession } from '../../../stores/session';
|
import { resetSession } from '../../../core/blitzkrieg/resetSession';
|
||||||
|
import { setSession } from '../../../core/blitzkrieg/setSession';
|
||||||
|
import { useSession } from '../../../stores/session';
|
||||||
import SessionPage from '../../embeds/session/page';
|
import SessionPage from '../../embeds/session/page';
|
||||||
import { Menu } from './components/Menu';
|
import { Menu } from './components/Menu';
|
||||||
import * as styles from './page.css';
|
import * as styles from './page.css';
|
||||||
@@ -107,7 +109,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>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopedi
|
|||||||
import {
|
import {
|
||||||
SHELL_NAMES,
|
SHELL_NAMES,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '../../../../../core/blitzkrieg/tankDefinitions';
|
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
import { mutateDuel, useDuel } from '../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../stores/duel';
|
||||||
import {
|
import {
|
||||||
mutateTankopediaTemporary,
|
mutateTankopediaTemporary,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Flex, Heading } from '@radix-ui/themes';
|
import { Flex, Heading } from '@radix-ui/themes';
|
||||||
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
|
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
|
||||||
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions';
|
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||||
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
|
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
CREW_MEMBER_NAMES,
|
CREW_MEMBER_NAMES,
|
||||||
GUN_TYPE_NAMES,
|
GUN_TYPE_NAMES,
|
||||||
SHELL_NAMES,
|
SHELL_NAMES,
|
||||||
} from '../../../../../../core/blitzkrieg/tankDefinitions';
|
} from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
|
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
|
||||||
import { useConsumable } from '../../../../../../hooks/useConsumable';
|
import { useConsumable } from '../../../../../../hooks/useConsumable';
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
@@ -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,9 +268,13 @@ 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)) *
|
? ((damageCoefficient * shell.damage.armor) /
|
||||||
|
(gun.reload.at(-1)! * reloadCoefficient +
|
||||||
|
gun.intraClip * intraClipCoefficient)) *
|
||||||
60
|
60
|
||||||
: (shell.damage.armor / gun.reload[0]) * 60}
|
: ((damageCoefficient * shell.damage.armor) /
|
||||||
|
(gun.reload[0] * reloadCoefficient)) *
|
||||||
|
60}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
decimals={0}
|
decimals={0}
|
||||||
@@ -277,13 +283,26 @@ 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)) *
|
? ((damageCoefficient * shell.damage.armor) /
|
||||||
|
(gun.reload.at(-1)! * reloadCoefficient +
|
||||||
|
gun.intraClip * intraClipCoefficient)) *
|
||||||
(60 -
|
(60 -
|
||||||
(gun.reload.slice(0, -1).length - 1) * gun.interClip) +
|
(gun.reload.slice(0, -1).length - 1) *
|
||||||
shell.damage.armor * gun.reload.slice(0, -1).length
|
gun.intraClip *
|
||||||
: (shell.damage.armor / (gun.reload[0] + gun.interClip)) *
|
intraClipCoefficient) +
|
||||||
(60 - (gun.reload.slice(1).length - 1) * gun.interClip) +
|
damageCoefficient *
|
||||||
shell.damage.armor * gun.reload.slice(1).length}
|
shell.damage.armor *
|
||||||
|
gun.reload.slice(0, -1).length
|
||||||
|
: ((damageCoefficient * shell.damage.armor) /
|
||||||
|
(gun.reload[0] * reloadCoefficient +
|
||||||
|
gun.intraClip * intraClipCoefficient)) *
|
||||||
|
(60 -
|
||||||
|
(gun.reload.slice(1).length - 1) *
|
||||||
|
gun.intraClip *
|
||||||
|
intraClipCoefficient) +
|
||||||
|
damageCoefficient *
|
||||||
|
shell.damage.armor *
|
||||||
|
gun.reload.slice(1).length}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
<Info
|
<Info
|
||||||
indent
|
indent
|
||||||
@@ -296,14 +315,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 +332,7 @@ export function Characteristics() {
|
|||||||
decimals={2}
|
decimals={2}
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
>
|
>
|
||||||
{reload}
|
{reload * reloadCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -326,17 +346,18 @@ export function Characteristics() {
|
|||||||
{gun.reload * reloadCoefficient}
|
{gun.reload * reloadCoefficient}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
)}
|
)}
|
||||||
{gun.type === 'autoLoader' && (
|
{gun.type === 'autoLoader' ||
|
||||||
<InfoWithDelta
|
(gun.type === 'autoReloader' && (
|
||||||
indent
|
<InfoWithDelta
|
||||||
decimals={2}
|
indent
|
||||||
name="Inter-clip reload"
|
decimals={2}
|
||||||
unit="s"
|
name="Intra-clip"
|
||||||
deltaType="lowerIsBetter"
|
unit="s"
|
||||||
>
|
deltaType="lowerIsBetter"
|
||||||
{gun.interClip * interClipCoefficient}
|
>
|
||||||
</InfoWithDelta>
|
{gun.intraClip * intraClipCoefficient}
|
||||||
)}
|
</InfoWithDelta>
|
||||||
|
))}
|
||||||
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
||||||
{shell.caliber}
|
{shell.caliber}
|
||||||
</InfoWithDelta>
|
</InfoWithDelta>
|
||||||
@@ -468,10 +489,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
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinit
|
|||||||
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, {
|
||||||
@@ -29,6 +30,7 @@ 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);
|
||||||
@@ -70,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 });
|
||||||
@@ -87,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 });
|
||||||
@@ -104,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 });
|
||||||
@@ -131,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]);
|
||||||
|
|||||||
@@ -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')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { UpdateIcon } from '@radix-ui/react-icons';
|
import { UpdateIcon } from '@radix-ui/react-icons';
|
||||||
import { Button, Dialog, Flex, Heading } from '@radix-ui/themes';
|
import { Button, Dialog, Flex, Heading } from '@radix-ui/themes';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks';
|
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks/components/Item/constants';
|
||||||
import { assignDuelMember } from '../../../../../core/blitzkrieg/assignDuelMember';
|
import { assignDuelMember } from '../../../../../core/blitzkrieg/assignDuelMember';
|
||||||
import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath';
|
import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath';
|
||||||
import { useDuel } from '../../../../../stores/duel';
|
import { useDuel } from '../../../../../stores/duel';
|
||||||
@@ -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={{
|
||||||
|
|||||||
+2
-4
@@ -1,9 +1,7 @@
|
|||||||
import { Button, Flex, Text } from '@radix-ui/themes';
|
import { Button, Flex, Text } from '@radix-ui/themes';
|
||||||
import { SmallTankIcon } from '../../../../../../components/SmallTankIcon';
|
import { SmallTankIcon } from '../../../../../../components/SmallTankIcon';
|
||||||
import {
|
import { TankDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
|
||||||
TIER_ROMAN_NUMERALS,
|
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
TankDefinition,
|
|
||||||
} from '../../../../../../core/blitzkrieg/tankDefinitions';
|
|
||||||
|
|
||||||
interface CompactSearchResultRowProps {
|
interface CompactSearchResultRowProps {
|
||||||
tanks: TankDefinition[];
|
tanks: TankDefinition[];
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { CaretDownIcon } from '@radix-ui/react-icons';
|
|||||||
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
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 {} from '../../../../../../../components/Tanks';
|
||||||
import {
|
import {
|
||||||
TANK_TYPES,
|
TANK_CLASSES,
|
||||||
TREE_TYPES,
|
TREE_TYPES,
|
||||||
TREE_TYPE_ICONS,
|
TREE_TYPE_ICONS,
|
||||||
TREE_TYPE_IMAGES,
|
TREE_TYPE_IMAGES,
|
||||||
} from '../../../../../../../components/Tanks';
|
} from '../../../../../../../components/Tanks/components/Item/constants';
|
||||||
|
import { NATIONS } from '../../../../../../../core/blitzkrieg/tankDefinitions';
|
||||||
import {
|
import {
|
||||||
NATIONS,
|
|
||||||
TIERS,
|
TIERS,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '../../../../../../../core/blitzkrieg/tankDefinitions';
|
} from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
import mutateTankopediaPersistent, {
|
import mutateTankopediaPersistent, {
|
||||||
TankopediaTestTankDisplay,
|
TankopediaTestTankDisplay,
|
||||||
useTankopediaPersistent,
|
useTankopediaPersistent,
|
||||||
@@ -121,7 +122,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 +133,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,8 @@ 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 { TreeType } from '../../../../../components/Tanks';
|
||||||
|
import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants';
|
||||||
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';
|
||||||
@@ -24,13 +25,15 @@ import { resolveDpm } from '../../../../../core/blitzkrieg/resolveDpm';
|
|||||||
import { resolveReload } from '../../../../../core/blitzkrieg/resolveReload';
|
import { resolveReload } from '../../../../../core/blitzkrieg/resolveReload';
|
||||||
import {
|
import {
|
||||||
NATIONS,
|
NATIONS,
|
||||||
|
TankDefinition,
|
||||||
|
tanksDefinitionsArray,
|
||||||
|
} from '../../../../../core/blitzkrieg/tankDefinitions';
|
||||||
|
import {
|
||||||
TANK_ICONS,
|
TANK_ICONS,
|
||||||
TANK_ICONS_COLLECTOR,
|
TANK_ICONS_COLLECTOR,
|
||||||
TANK_ICONS_PREMIUM,
|
TANK_ICONS_PREMIUM,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
TankDefinition,
|
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
tanksDefinitionsArray,
|
|
||||||
} from '../../../../../core/blitzkrieg/tankDefinitions';
|
|
||||||
import { tankIcon } from '../../../../../core/blitzkrieg/tankIcon';
|
import { tankIcon } from '../../../../../core/blitzkrieg/tankIcon';
|
||||||
import { unionBoundingBox } from '../../../../../core/blitzkrieg/unionBoundingBox';
|
import { unionBoundingBox } from '../../../../../core/blitzkrieg/unionBoundingBox';
|
||||||
import { theme } from '../../../../../stitches.config';
|
import { theme } from '../../../../../stitches.config';
|
||||||
@@ -49,7 +52,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 +72,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 +91,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 +846,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' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+54
-52
@@ -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));
|
inProduction: true,
|
||||||
});
|
inPublic: true,
|
||||||
|
|
||||||
return option;
|
command: createLocalizedCommand('about', [
|
||||||
}
|
{ subcommand: 'commands' },
|
||||||
|
...subcommands.map((subcommand) => ({ subcommand })),
|
||||||
|
]),
|
||||||
|
|
||||||
export const aboutCommand: CommandRegistry = {
|
async handler(interaction) {
|
||||||
inProduction: true,
|
const subcommand = interaction.options.getSubcommand();
|
||||||
inPublic: true,
|
const { t } = translator(interaction.locale);
|
||||||
|
|
||||||
command: addDocsSubcommands(
|
if (subcommand === 'commands') {
|
||||||
new SlashCommandBuilder()
|
return t`bot.commands.about.subcommands.commands.body${(
|
||||||
.setName('about')
|
await Promise.all(COMMANDS_RAW)
|
||||||
.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) {
|
|
||||||
const subcommand = interaction.options.getSubcommand();
|
|
||||||
|
|
||||||
if (subcommand === 'commands') {
|
|
||||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
|
||||||
await Promise.all(COMMANDS_RAW)
|
|
||||||
)
|
|
||||||
.filter((registry) => registry.inPublic && registry.inProduction)
|
|
||||||
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
|
|
||||||
.map(
|
|
||||||
(registry) =>
|
|
||||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
|
||||||
)
|
)
|
||||||
.join('\n')}`;
|
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||||
}
|
.sort((a, b) =>
|
||||||
|
(a.command.name_localizations?.[interaction.locale] ??
|
||||||
|
a.command.name) <
|
||||||
|
(b.command.name_localizations?.[interaction.locale] ??
|
||||||
|
b.command.name)
|
||||||
|
? -1
|
||||||
|
: 1,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(registry) =>
|
||||||
|
`- \`/${
|
||||||
|
registry.command.name_localizations?.[interaction.locale] ??
|
||||||
|
registry.command.name
|
||||||
|
}\`: ${
|
||||||
|
registry.command.description_localizations?.[
|
||||||
|
interaction.locale
|
||||||
|
] ?? registry.command.description
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
.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;
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { getBlitzStarsLinkButton } from '../../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import addPeriodicFilterOptions from '../../core/discord/addPeriodicFilterOptions';
|
||||||
|
import addUsernameChoices from '../../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteTanks from '../../core/discord/autocompleteTanks';
|
||||||
|
import autocompleteUsername from '../../core/discord/autocompleteUsername';
|
||||||
|
import { buttonRefresh } from '../../core/discord/buttonRefresh';
|
||||||
|
import commandToURL from '../../core/discord/commandToURL';
|
||||||
|
import { createLocalizedCommand } from '../../core/discord/createLocalizedCommand';
|
||||||
|
import { getCustomPeriodParams } from '../../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../../core/discord/getFiltersFromCommand';
|
||||||
|
import resolvePeriodFromButton from '../../core/discord/resolvePeriodFromButton';
|
||||||
|
import resolvePeriodFromCommand from '../../core/discord/resolvePeriodFromCommand';
|
||||||
|
import resolvePlayerFromButton from '../../core/discord/resolvePlayerFromButton';
|
||||||
|
import resolvePlayerFromCommand from '../../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { CommandRegistry } from '../../events/interactionCreate';
|
||||||
|
import { renderBreakdown } from './render';
|
||||||
|
|
||||||
|
export const breakdownCommand = new Promise<CommandRegistry>(
|
||||||
|
async (resolve) => {
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand('breakdown'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(await renderBreakdown(
|
||||||
|
player,
|
||||||
|
period,
|
||||||
|
filters,
|
||||||
|
interaction.locale,
|
||||||
|
)),
|
||||||
|
buttonRefresh(interaction, path),
|
||||||
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
|
return await renderBreakdown(
|
||||||
|
player,
|
||||||
|
period,
|
||||||
|
filters,
|
||||||
|
interaction.locale,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,41 +1,25 @@
|
|||||||
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 TitleBar from '../components/TitleBar';
|
import NoData from '../../components/NoData';
|
||||||
import Wrapper from '../components/Wrapper';
|
import TitleBar from '../../components/TitleBar';
|
||||||
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';
|
||||||
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
import { filtersToDescription } from '../../core/blitzkrieg/filtersToDescription';
|
||||||
import {
|
import {
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
tankDefinitions,
|
tankDefinitions,
|
||||||
} from '../core/blitzkrieg/tankDefinitions';
|
} from '../../core/blitzkrieg/tankDefinitions';
|
||||||
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
import getStatsInPeriod from '../../core/blitzstars/getStatsInPeriod';
|
||||||
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
import { tankAverages } from '../../core/blitzstars/tankAverages';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
|
||||||
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
|
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import { translator } from '../../core/localization/translator';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import calculateWN8 from '../../core/statistics/calculateWN8';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import { StatFilters, filterStats } from '../../core/statistics/filterStats';
|
||||||
import buttonPrimary from '../core/discord/buttonPrimary';
|
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
||||||
import commandToURL from '../core/discord/commandToURL';
|
|
||||||
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
|
||||||
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
|
||||||
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
|
||||||
import resolvePeriodFromCommand, {
|
|
||||||
ResolvedPeriod,
|
|
||||||
} from '../core/discord/resolvePeriodFromCommand';
|
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
|
||||||
import resolvePlayerFromCommand, {
|
|
||||||
ResolvedPlayer,
|
|
||||||
} from '../core/discord/resolvePlayerFromCommand';
|
|
||||||
import calculateWN8 from '../core/statistics/calculateWN8';
|
|
||||||
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
|
||||||
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
|
||||||
import { CommandRegistryPromisable } from '../events/interactionCreate';
|
|
||||||
|
|
||||||
const ROWS_PER_PAGE = 8;
|
const ROWS_PER_PAGE = 8;
|
||||||
const MAX_PAGES = 9;
|
const MAX_PAGES = 9;
|
||||||
@@ -44,15 +28,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 +130,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 +159,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 +181,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 +203,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 +218,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 +243,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 +257,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,56 +273,8 @@ 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>(
|
|
||||||
async (resolve) => {
|
|
||||||
const command = await addPeriodicFilterOptions(
|
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('breakdown')
|
|
||||||
.setDescription("A period's breakdown by tanks played"),
|
|
||||||
(option) => option.addStringOption(addUsernameChoices),
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command,
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
|
||||||
const filters = await getFiltersFromCommand(interaction);
|
|
||||||
const path = commandToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
...getCustomPeriodParams(interaction),
|
|
||||||
...filters,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
...(await renderBreakdown(player, period, filters)),
|
|
||||||
buttonPrimary(path, 'Refresh'),
|
|
||||||
await getBlitzStarsLinkButton(player.region, player.id),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
|
||||||
autocompleteUsername(interaction);
|
|
||||||
autocompleteTanks(interaction);
|
|
||||||
},
|
|
||||||
|
|
||||||
async button(interaction) {
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
|
||||||
const filters = getFiltersFromButton(interaction);
|
|
||||||
|
|
||||||
return await renderBreakdown(player, period, filters);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
+32
-28
@@ -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) => {
|
||||||
inProduction: true,
|
resolve({
|
||||||
inPublic: true,
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('debug'),
|
||||||
.setName('debug')
|
|
||||||
.setDescription('Debug information about the bot'),
|
|
||||||
|
|
||||||
async handler() {
|
async handler(interaction) {
|
||||||
const currentTime = new Date().getTime();
|
const { t } = translator(interaction.locale);
|
||||||
const uptime = currentTime - executionStart;
|
const currentTime = new Date().getTime();
|
||||||
const list = [
|
const uptime = currentTime - executionStart;
|
||||||
['Version', packageJSON.version],
|
const list = [
|
||||||
['Shard', client.shard?.ids[0] ?? 'default'],
|
[t`bot.commands.debug.body.version`, packageJSON.version],
|
||||||
[
|
[
|
||||||
'Uptime',
|
t`bot.commands.debug.body.shard`,
|
||||||
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
|
client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`,
|
||||||
(uptime / 1000 / 60) % 60,
|
],
|
||||||
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
|
[
|
||||||
uptime % 1000,
|
t`bot.commands.debug.body.uptime`,
|
||||||
)}ms`,
|
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
|
||||||
],
|
(uptime / 1000 / 60) % 60,
|
||||||
]
|
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
|
||||||
.map(([key, value]) => `- ${key}: ${value}`)
|
uptime % 1000,
|
||||||
.join('\n');
|
)}ms`,
|
||||||
|
],
|
||||||
return `# Debug\n\n${list}`;
|
];
|
||||||
},
|
return t`bot.commands.debug.body.title${list
|
||||||
};
|
.map(([key, value]) => `- ${key}: ${value}`)
|
||||||
|
.join('\n')}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
+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);
|
||||||
|
// },
|
||||||
|
// };
|
||||||
+50
-53
@@ -1,11 +1,9 @@
|
|||||||
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 { LineColor } from '../components/Graph/components/Line/constants';
|
||||||
|
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 +11,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 +28,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 +37,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 +49,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 +74,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,11 +90,11 @@ 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: LineColor.Red,
|
||||||
plot: plot,
|
plot: plot,
|
||||||
minY,
|
minY,
|
||||||
maxY,
|
maxY,
|
||||||
@@ -102,47 +102,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 +141,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 +184,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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+75
-53
@@ -1,69 +1,91 @@
|
|||||||
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) => {
|
||||||
inProduction: true,
|
resolve({
|
||||||
inPublic: true,
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('inactive')
|
||||||
.setName('inactive')
|
.addStringOption(addClanChoices)
|
||||||
.setDescription('Lists all inactive players')
|
.addNumberOption((option) => {
|
||||||
.addStringOption(addClanChoices)
|
const { translate } = translator(Locale.EnglishUS);
|
||||||
.addNumberOption((option) =>
|
|
||||||
option
|
return option
|
||||||
.setName('threshold')
|
.setName('threshold')
|
||||||
.setDescription(
|
.setNameLocalizations(
|
||||||
`The number of days inactive (default: ${DEFAULT_THRESHOLD})`,
|
localizationObject('bot.commands.inactive.options.threshold'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate('bot.commands.inactive.options.threshold.description'),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.inactive.options.threshold.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setMinValue(0);
|
||||||
|
}),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { translate } = translator(interaction.locale);
|
||||||
|
const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
|
const threshold =
|
||||||
|
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
||||||
|
const time = new Date().getTime() / 1000;
|
||||||
|
const clanInfo = await getClanInfo(region, id);
|
||||||
|
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
|
||||||
|
const inactive = accountInfo
|
||||||
|
.map(
|
||||||
|
(account) =>
|
||||||
|
[
|
||||||
|
account.nickname,
|
||||||
|
(time - account.last_battle_time) / 60 / 60 / 24,
|
||||||
|
] as [string, number],
|
||||||
)
|
)
|
||||||
.setMinValue(0),
|
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
||||||
),
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(
|
||||||
|
([name, days]) =>
|
||||||
|
[
|
||||||
|
name,
|
||||||
|
translate('bot.commands.inactive.body.listing', [
|
||||||
|
days.toFixed(0),
|
||||||
|
]),
|
||||||
|
] as [string, string],
|
||||||
|
);
|
||||||
|
const hasInactiveMembers = inactive.length > 0;
|
||||||
|
|
||||||
async handler(interaction) {
|
return (
|
||||||
const { region, id } = await resolveClanFromCommand(interaction);
|
<CommandWrapper>
|
||||||
const threshold =
|
<TitleBar
|
||||||
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
title={clanInfo.name}
|
||||||
const time = new Date().getTime() / 1000;
|
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
||||||
const clanInfo = await getClanInfo(region, id);
|
description={`${translate('bot.commands.inactive.body.subtitle', [`${threshold}`])} • ${new Date().toLocaleDateString(interaction.locale)}`}
|
||||||
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
|
/>
|
||||||
const inactive = accountInfo
|
|
||||||
.map(
|
{!hasInactiveMembers && (
|
||||||
(account) =>
|
<NoData type="players_in_period" locale={interaction.locale} />
|
||||||
[
|
)}
|
||||||
account.nickname,
|
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
||||||
(time - account.last_battle_time) / 60 / 60 / 24,
|
</CommandWrapper>
|
||||||
] as [string, number],
|
|
||||||
)
|
|
||||||
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.map(
|
|
||||||
([name, days]) => [name, `${days.toFixed(0)} days`] as [string, string],
|
|
||||||
);
|
);
|
||||||
const hasInactiveMembers = inactive.length > 0;
|
},
|
||||||
|
|
||||||
return (
|
autocomplete: autocompleteClan,
|
||||||
<Wrapper>
|
});
|
||||||
<TitleBar
|
});
|
||||||
name={clanInfo.name}
|
|
||||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
|
||||||
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
|
||||||
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteClan,
|
|
||||||
};
|
|
||||||
+158
-147
@@ -1,209 +1,220 @@
|
|||||||
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) => {
|
||||||
inProduction: true,
|
resolve({
|
||||||
inPublic: true,
|
inProduction: 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 { id, region } = await resolvePlayerFromCommand(interaction);
|
const { t, translate } = translator(interaction.locale);
|
||||||
const discordId = parseInt(interaction.user.id);
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const discordId = parseInt(interaction.user.id);
|
||||||
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
|
||||||
|
|
||||||
await linkBlitzAndDiscord(discordId, region, id);
|
await linkBlitzAndDiscord(discordId, region, id);
|
||||||
|
|
||||||
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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).remove(
|
||||||
|
discord.sklld_verify_role,
|
||||||
|
);
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).add(
|
||||||
|
discord.sklld_peasant_role,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await (interaction.member!.roles as GuildMemberRoleManager).remove(
|
return [
|
||||||
discord.sklld_verify_role,
|
<CommandWrapper fat>
|
||||||
);
|
|
||||||
await (interaction.member!.roles as GuildMemberRoleManager).add(
|
|
||||||
discord.sklld_peasant_role,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
<Wrapper fat>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Glow color={teal.teal9} rotation={90} />
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
gap: 16,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Glow color={teal.teal9} rotation={90} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 8,
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
|
||||||
src={
|
|
||||||
interaction.user.avatarURL({ extension: 'png' }) ??
|
|
||||||
'https://i.stack.imgur.com/l60Hf.png'
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
borderRadius: 32,
|
|
||||||
objectFit: 'cover',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
gap: 8,
|
||||||
gap: 4,
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 32,
|
|
||||||
color: theme.colors.textHighContrast,
|
|
||||||
fontWeight: 900,
|
|
||||||
maxWidth: 240,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{interaction.user.displayName}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 16,
|
|
||||||
color: theme.colors.textLowContrast,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
@{interaction.user.username}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 4,
|
|
||||||
color: theme.colors.textLowContrast_teal,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="https://i.imgur.com/jIcRgog.png"
|
|
||||||
style={{
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: 16 }}>Accounts linked</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 8,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{clanAccountInfo?.clan ? (
|
|
||||||
<img
|
<img
|
||||||
|
src={
|
||||||
|
interaction.user.avatarURL({ extension: 'png' }) ??
|
||||||
|
'https://i.stack.imgur.com/l60Hf.png'
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
borderRadius: 32,
|
borderRadius: 32,
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
}}
|
}}
|
||||||
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interaction.user.displayName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
@{interaction.user.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
|
color: theme.colors.textLowContrast_teal,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<img
|
||||||
|
src="https://i.imgur.com/jIcRgog.png"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 32,
|
width: 16,
|
||||||
color: theme.colors.textHighContrast,
|
height: 16,
|
||||||
fontWeight: 900,
|
}}
|
||||||
maxWidth: 240,
|
/>
|
||||||
overflow: 'hidden',
|
<span
|
||||||
textOverflow: 'ellipsis',
|
style={{ fontSize: 16 }}
|
||||||
whiteSpace: 'nowrap',
|
>{t`bot.commands.link.body.accounts_linked`}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan ? (
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{accountInfo.nickname}
|
<span
|
||||||
</span>
|
style={{
|
||||||
<span
|
fontSize: 32,
|
||||||
style={{
|
color: theme.colors.textHighContrast,
|
||||||
fontSize: 16,
|
fontWeight: 900,
|
||||||
color: theme.colors.textLowContrast,
|
maxWidth: 240,
|
||||||
}}
|
overflow: 'hidden',
|
||||||
>
|
textOverflow: 'ellipsis',
|
||||||
{clanAccountInfo?.clan
|
whiteSpace: 'nowrap',
|
||||||
? `[${clanAccountInfo?.clan?.tag}] - `
|
}}
|
||||||
: ''}
|
>
|
||||||
{REGION_NAMES[region]}
|
{accountInfo.nickname}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan
|
||||||
|
? `[${clanAccountInfo?.clan?.tag}] • `
|
||||||
|
: ''}
|
||||||
|
{translate(`common.regions.normal.${region}`)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
</div>
|
</div>
|
||||||
|
</CommandWrapper>,
|
||||||
|
|
||||||
<Glow color={teal.teal9} rotation={-90} />
|
embedInfo(
|
||||||
</div>
|
t`bot.commands.link.embed.title`,
|
||||||
</Wrapper>,
|
t`bot.commands.link.embed.description`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
embedInfo(
|
autocomplete: autocompleteUsername,
|
||||||
'Incorrect account?',
|
});
|
||||||
'Use the command again and select a username from the search results.',
|
});
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
|
||||||
};
|
|
||||||
+103
-101
@@ -1,128 +1,130 @@
|
|||||||
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';
|
||||||
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
import {
|
import {
|
||||||
TIER_ROMAN_NUMERALS,
|
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
Tier,
|
Tier,
|
||||||
tankDefinitions,
|
tankDefinitions,
|
||||||
} from '../core/blitzkrieg/tankDefinitions';
|
} from '../core/blitzkrieg/tankDefinitions';
|
||||||
import { tankIcon } from '../core/blitzkrieg/tankIcon';
|
import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants';
|
||||||
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) => {
|
||||||
inProduction: true,
|
resolve({
|
||||||
inPublic: true,
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: createLocalizedCommand('owned-tanks')
|
||||||
.setName('owned-tanks')
|
.addStringOption(addTierChoices)
|
||||||
.setDescription("Shows a player's owned tanks")
|
.addStringOption(addUsernameChoices),
|
||||||
.addStringOption(addTierChoices)
|
|
||||||
.addStringOption(addUsernameChoices),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const tier = Number(interaction.options.getString('tier'));
|
const { translate } = translator(interaction.locale);
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const tier = Number(interaction.options.getString('tier'));
|
||||||
const { id, region: server } = account;
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const accountInfo = await getAccountInfo(server, id);
|
const { id, region: server } = account;
|
||||||
const tankStats = await getTankStats(server, id);
|
const accountInfo = await getAccountInfo(server, id);
|
||||||
const filteredTanks = (
|
const tankStats = await getTankStats(server, id, interaction.locale);
|
||||||
await Promise.all(
|
const filteredTanks = (
|
||||||
tankStats.map(async (tankData) => ({
|
await Promise.all(
|
||||||
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
|
tankStats.map(async (tankData) => ({
|
||||||
id: tankData.tank_id,
|
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
|
||||||
})),
|
id: tankData.tank_id,
|
||||||
)
|
})),
|
||||||
).filter((tank) => tank.tankDefinitions?.tier === tier);
|
)
|
||||||
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
).filter((tank) => tank.tankDefinitions?.tier === tier);
|
||||||
const groupedTanks: Record<string, TankDefinition[]> = {};
|
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
||||||
const nations: string[] = [];
|
const groupedTanks: Record<string, TankDefinition[]> = {};
|
||||||
|
const nations: string[] = [];
|
||||||
|
|
||||||
filteredTanks.forEach((tank) => {
|
filteredTanks.forEach((tank) => {
|
||||||
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
|
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
|
||||||
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
|
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
|
||||||
nations.push(tank.tankDefinitions.nation);
|
nations.push(tank.tankDefinitions.nation);
|
||||||
} else {
|
} else {
|
||||||
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
|
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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(
|
||||||
nations.map(async (nation) => {
|
nations.map(async (nation) => {
|
||||||
const tanks = groupedTanks[nation];
|
const tanks = groupedTanks[nation];
|
||||||
const leftColumnSize = Math.ceil(tanks.length / 2);
|
const leftColumnSize = Math.ceil(tanks.length / 2);
|
||||||
const leftColumn = tanks.slice(0, leftColumnSize);
|
const leftColumn = tanks.slice(0, leftColumnSize);
|
||||||
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>
|
||||||
<Tanks.Column>
|
<Tanks.Column>
|
||||||
{await Promise.all(
|
{await Promise.all(
|
||||||
leftColumn.map(async (tank) => (
|
leftColumn.map(async (tank) => (
|
||||||
<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)}
|
treeType={tank.treeType}
|
||||||
treeType={tank.treeType}
|
/>
|
||||||
/>
|
)),
|
||||||
)),
|
)}
|
||||||
)}
|
</Tanks.Column>
|
||||||
</Tanks.Column>
|
<Tanks.Column>
|
||||||
<Tanks.Column>
|
{await Promise.all(
|
||||||
{await Promise.all(
|
rightColumn.map(async (tank) => (
|
||||||
rightColumn.map(async (tank) => (
|
<Tanks.Item
|
||||||
<Tanks.Item
|
key={tank.id}
|
||||||
key={tank.id}
|
name={tank.name}
|
||||||
name={tank.name}
|
tankType={tank.class}
|
||||||
tankType={tank.type}
|
treeType={tank.treeType}
|
||||||
image={tankIcon(tank.id)}
|
/>
|
||||||
treeType={tank.treeType}
|
)),
|
||||||
/>
|
)}
|
||||||
)),
|
</Tanks.Column>
|
||||||
)}
|
</Tanks.Row>
|
||||||
</Tanks.Column>
|
</Tanks.Root>
|
||||||
</Tanks.Row>
|
);
|
||||||
</Tanks.Root>
|
}),
|
||||||
);
|
))}
|
||||||
}),
|
</CommandWrapper>
|
||||||
))}
|
);
|
||||||
</Wrapper>
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+39
-37
@@ -1,42 +1,44 @@
|
|||||||
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) => {
|
||||||
inProduction: true,
|
resolve({
|
||||||
inPublic: true,
|
inProduction: 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) {
|
||||||
// friday the 13th permissions easter egg
|
const { t, translate } = translator(interaction.locale);
|
||||||
const isFridayThe13th =
|
// friday the 13th permissions easter egg
|
||||||
new Date().getDay() === 5 && new Date().getDate() === 13;
|
const isFridayThe13th =
|
||||||
const permissions = [
|
new Date().getDay() === 5 && new Date().getDate() === 13;
|
||||||
[
|
const permissions = [
|
||||||
interaction.appPermissions?.has('ViewChannel'),
|
[
|
||||||
'View channels: needed for the refresh button to work',
|
interaction.appPermissions?.has('ViewChannel'),
|
||||||
],
|
t`bot.commands.permissions.body.view_channels`,
|
||||||
[
|
],
|
||||||
interaction.appPermissions?.has('ReadMessageHistory'),
|
[
|
||||||
'Read message history: also needed for the refresh button to work',
|
interaction.appPermissions?.has('ReadMessageHistory'),
|
||||||
],
|
t`bot.commands.permissions.body.read_message_history`,
|
||||||
[
|
],
|
||||||
interaction.appPermissions?.has('AttachFiles'),
|
[
|
||||||
'Attach files: needed for image-based commands to work',
|
interaction.appPermissions?.has('AttachFiles'),
|
||||||
],
|
t`bot.commands.permissions.body.attach_files`,
|
||||||
];
|
],
|
||||||
const body = permissions
|
];
|
||||||
.map(([hasPermission, description]) => {
|
const body = permissions
|
||||||
if (hasPermission) {
|
.map(([hasPermission, description]) => {
|
||||||
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
|
if (hasPermission) {
|
||||||
} else {
|
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
|
||||||
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
|
} else {
|
||||||
}
|
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
|
||||||
})
|
}
|
||||||
.join('\n');
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
return `# Permissions\n${body}`;
|
return translate('bot.commands.permissions.body.title', [body]);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+29
-26
@@ -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) => {
|
||||||
inProduction: true,
|
resolve({
|
||||||
inPublic: true,
|
inProduction: true,
|
||||||
handlesInteraction: true,
|
inPublic: 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 subcommand = interaction.options.getSubcommand();
|
const { t, translate } = translator(interaction.locale);
|
||||||
const executionStart = Date.now();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
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,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// );
|
||||||
+57
-40
@@ -1,54 +1,71 @@
|
|||||||
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) => {
|
||||||
inProduction: true,
|
resolve({
|
||||||
inPublic: true,
|
inProduction: 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 account = await resolvePlayerFromCommand(interaction);
|
const { t, translate } = translator(interaction.locale);
|
||||||
const { id, region: region } = account;
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const { id, region: region } = account;
|
||||||
|
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}`],
|
t`bot.commands.player_info.body.nickname`,
|
||||||
[
|
`${accountInfo.nickname}`,
|
||||||
'Winrate',
|
],
|
||||||
`${(
|
[
|
||||||
100 *
|
t`bot.commands.player_info.body.battles`,
|
||||||
(accountInfo.statistics.all.wins /
|
`${accountInfo.statistics.all.battles}`,
|
||||||
accountInfo.statistics.all.battles)
|
],
|
||||||
).toFixed(2)}%`,
|
[
|
||||||
],
|
t`bot.commands.player_info.body.winrate`,
|
||||||
[],
|
`${(
|
||||||
['Account ID', `${accountInfo.account_id}`],
|
100 *
|
||||||
[
|
(accountInfo.statistics.all.wins /
|
||||||
'Created',
|
accountInfo.statistics.all.battles)
|
||||||
`${new Date(accountInfo.created_at * 1000).toDateString()}`,
|
).toFixed(2)}%`,
|
||||||
],
|
],
|
||||||
[
|
[],
|
||||||
'Last battle',
|
[
|
||||||
`${new Date(accountInfo.last_battle_time * 1000).toDateString()}`,
|
t`bot.commands.player_info.body.account_id`,
|
||||||
],
|
`${accountInfo.account_id}`,
|
||||||
]),
|
],
|
||||||
);
|
[
|
||||||
},
|
t`bot.commands.player_info.body.created`,
|
||||||
|
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,
|
||||||
};
|
});
|
||||||
|
});
|
||||||
+353
-303
@@ -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,339 +119,387 @@ export interface RatingsNeighbors {
|
|||||||
neighbors: RatingsPlayer[];
|
neighbors: RatingsPlayer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const noOngoingSeason = embedNegative(
|
interface SimplifiedPlayer {
|
||||||
'No ongoing season',
|
id: number;
|
||||||
"Wargaming didn't provide any data for this season.",
|
score: number;
|
||||||
);
|
position: number;
|
||||||
|
clan: string | undefined;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const ratingsCommand = new Promise<CommandRegistryPromisable>(
|
const DEFAULT_LIMIT = 10;
|
||||||
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) {
|
export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
return option
|
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
||||||
.addStringOption(addUsernameChoices)
|
const onGoingSeason = await isOnGoingRatingsSeason();
|
||||||
.addIntegerOption((option) =>
|
const seasonNumbers = range(
|
||||||
option
|
FIRST_ARCHIVED_RATINGS_SEASON,
|
||||||
.setName('limit')
|
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
|
||||||
.setDescription('How many neighbors to display (default: 10)')
|
);
|
||||||
.setRequired(false)
|
|
||||||
.setMinValue(5)
|
function addOptions(option: SlashCommandSubcommandBuilder) {
|
||||||
.setMaxValue(30),
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
)
|
|
||||||
.addStringOption((option) =>
|
return option
|
||||||
option
|
.addIntegerOption((option) =>
|
||||||
.setName('season')
|
option
|
||||||
.setDescription('Season number')
|
.setName(t`bot.commands.ratings.options.limit`)
|
||||||
.setRequired(false)
|
.setNameLocalizations(
|
||||||
.addChoices(
|
localizationObject('bot.commands.ratings.options.limit'),
|
||||||
...seasonNumbers
|
)
|
||||||
.map(
|
.setDescription(
|
||||||
(number, index) =>
|
translate('bot.commands.ratings.options.limit.description', [
|
||||||
({
|
`${DEFAULT_LIMIT}`,
|
||||||
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
|
]),
|
||||||
value: `${number}`,
|
)
|
||||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
.setDescriptionLocalizations(
|
||||||
)
|
localizationObject(
|
||||||
.reverse(),
|
'bot.commands.ratings.options.limit.description',
|
||||||
|
[`${DEFAULT_LIMIT}`],
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
const command = new SlashCommandBuilder()
|
.setMaxValue(30),
|
||||||
.setName('ratings')
|
|
||||||
.setDescription('Ratings battles statistics')
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
addSubcommands(
|
|
||||||
option
|
|
||||||
.setName('neighbors')
|
|
||||||
.setDescription('Shows neighboring players on the leaderboard'),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.addSubcommand((option) =>
|
.addStringOption((option) =>
|
||||||
addSubcommands(
|
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
|
option
|
||||||
.setName('league')
|
.addStringOption((option) => {
|
||||||
.setDescription('Shows top players from a league')
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
.addStringOption(addRegionChoices)
|
|
||||||
.addStringOption((option) =>
|
return option
|
||||||
option
|
.setName(t`bot.commands.ratings.options.league`)
|
||||||
.setName('league')
|
.setNameLocalizations(
|
||||||
.setDescription('The league to display')
|
localizationObject('bot.commands.ratings.options.league'),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
translate('bot.commands.ratings.options.league.description'),
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
'bot.commands.ratings.options.league.description',
|
||||||
|
),
|
||||||
|
)
|
||||||
.addChoices(
|
.addChoices(
|
||||||
...LEAGUES.map(
|
...LEAGUES.map(
|
||||||
({ name }, value) =>
|
({ name }, value) =>
|
||||||
({
|
({
|
||||||
name,
|
name: translate(`common.leagues.${name}`),
|
||||||
value: `${value}`,
|
value: `${value}`,
|
||||||
|
name_localizations: localizationObject(
|
||||||
|
`common.leagues.${name}`,
|
||||||
|
),
|
||||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.setRequired(true),
|
.setRequired(true);
|
||||||
),
|
})
|
||||||
),
|
.addStringOption(addRegionChoices);
|
||||||
);
|
addOptions(option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
resolve({
|
// BIG TODO: title seems to be in english
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command,
|
async handler(interaction) {
|
||||||
|
const { t, translate } = translator(interaction.locale);
|
||||||
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
|
| 'league'
|
||||||
|
| 'neighbors';
|
||||||
|
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
|
||||||
|
const seasonOption = interaction.options.getString('season');
|
||||||
|
const season = seasonOption
|
||||||
|
? parseInt(seasonOption)
|
||||||
|
: onGoingSeason
|
||||||
|
? latestArchivedSeasonNumber + 1
|
||||||
|
: latestArchivedSeasonNumber;
|
||||||
|
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
|
||||||
|
let titleName: string;
|
||||||
|
let titleImage: string | undefined;
|
||||||
|
let titleDescription: string;
|
||||||
|
let playersAfter: number;
|
||||||
|
let players: SimplifiedPlayer[] | undefined;
|
||||||
|
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
||||||
|
let playerId: number | undefined;
|
||||||
|
let regionRatingsInfo: RatingsInfo;
|
||||||
|
|
||||||
async handler(interaction) {
|
if (subcommand === 'league') {
|
||||||
interface SimplifiedPlayer {
|
const leagueIndex = parseInt(interaction.options.getString('league')!);
|
||||||
id: number;
|
const region = interaction.options.getString('region') as Region;
|
||||||
score: number;
|
regionRatingsInfo = await (isCurrentSeason
|
||||||
position: number;
|
? getRatingsInfo(region)
|
||||||
clan: string | undefined;
|
: getArchivedRatingsInfo(region, season!));
|
||||||
nickname: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subcommand = interaction.options.getSubcommand(true) as
|
if (regionRatingsInfo.detail !== undefined)
|
||||||
| 'league'
|
return t`bot.commands.ratings.body.no_ongoing_season`;
|
||||||
| 'neighbors';
|
|
||||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
|
||||||
const seasonOption = interaction.options.getString('season');
|
|
||||||
const season = seasonOption
|
|
||||||
? parseInt(seasonOption)
|
|
||||||
: onGoingSeason
|
|
||||||
? latestArchivedSeasonNumber + 1
|
|
||||||
: latestArchivedSeasonNumber;
|
|
||||||
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
|
|
||||||
let titleName: string;
|
|
||||||
let titleImage: string | undefined;
|
|
||||||
let titleDescription: string;
|
|
||||||
let playersBefore: number;
|
|
||||||
let playersAfter: number;
|
|
||||||
let players: SimplifiedPlayer[] | undefined;
|
|
||||||
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
|
||||||
let playerId: number | undefined;
|
|
||||||
let regionRatingsInfo: RatingsInfo;
|
|
||||||
|
|
||||||
if (subcommand === 'league') {
|
const result = isCurrentSeason
|
||||||
const leagueIndex = parseInt(
|
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
|
||||||
interaction.options.getString('league')!,
|
result.slice(0, limit).map(
|
||||||
);
|
(player) =>
|
||||||
const region = interaction.options.getString('region') as Region;
|
({
|
||||||
regionRatingsInfo = await (isCurrentSeason
|
id: player.spa_id,
|
||||||
? getRatingsInfo(region)
|
score: player.score,
|
||||||
: getArchivedRatingsInfo(region, season!));
|
position: player.number,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: await getArchivedRatingsLeaderboard(region, season!).then(
|
||||||
|
async (players) => {
|
||||||
|
const firstPlayerIndex =
|
||||||
|
leagueIndex === 0
|
||||||
|
? 0
|
||||||
|
: players.findIndex(
|
||||||
|
(player) =>
|
||||||
|
player.score < LEAGUES[leagueIndex - 1].minScore,
|
||||||
|
);
|
||||||
|
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
||||||
|
const trimmed = players.slice(
|
||||||
|
firstPlayerIndex,
|
||||||
|
lastPlayerIndex + 1,
|
||||||
|
);
|
||||||
|
const clanData = await getClanAccountInfo(
|
||||||
|
region,
|
||||||
|
trimmed.map(({ id }) => id),
|
||||||
|
['clan'],
|
||||||
|
);
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
return trimmed.map(
|
||||||
|
(player, index) =>
|
||||||
|
({
|
||||||
|
id: player.id,
|
||||||
|
score: player.score,
|
||||||
|
position: firstPlayerIndex + index + 1,
|
||||||
|
clan: clanData[player.id]?.clan?.tag,
|
||||||
|
nickname:
|
||||||
|
clanData[index]?.account_name ??
|
||||||
|
translate('bot.commands.ratings.body.deleted_player', [
|
||||||
|
`${player.id}`,
|
||||||
|
]),
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||||
|
midnightLeaderboard = isCurrentSeason
|
||||||
|
? await getArchivedRatingsMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
regionRatingsInfo.current_season,
|
||||||
|
)
|
||||||
|
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
const result = isCurrentSeason
|
players = result;
|
||||||
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
|
playersAfter =
|
||||||
result.slice(0, limit).map(
|
regionRatingsInfo.count - result[result.length - 1].position;
|
||||||
|
titleName = `${
|
||||||
|
leagueInfo
|
||||||
|
? translate(`common.leagues.${LEAGUES[leagueInfo.index].name}`)
|
||||||
|
: ''
|
||||||
|
} - ${translate(`common.regions.short.${region}`)}`;
|
||||||
|
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||||
|
? leagueInfo.big_icon
|
||||||
|
: `https:${leagueInfo.big_icon}`;
|
||||||
|
titleDescription = translate('bot.commands.ratings.body.top_players', [
|
||||||
|
`${limit}`,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
|
regionRatingsInfo = isCurrentSeason
|
||||||
|
? await getRatingsInfo(region)
|
||||||
|
: await getArchivedRatingsInfo(region, season!);
|
||||||
|
|
||||||
|
if (regionRatingsInfo.detail !== undefined)
|
||||||
|
return t`bot.commands.ratings.body.no_ongoing_season`;
|
||||||
|
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const neighbors = isCurrentSeason
|
||||||
|
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
|
||||||
|
(players) =>
|
||||||
|
players.neighbors.map(
|
||||||
(player) =>
|
(player) =>
|
||||||
({
|
({
|
||||||
id: player.spa_id,
|
id: player.spa_id,
|
||||||
score: player.score,
|
|
||||||
position: player.number,
|
position: player.number,
|
||||||
|
score: player.score,
|
||||||
clan: player.clan_tag,
|
clan: player.clan_tag,
|
||||||
nickname: player.nickname,
|
nickname: player.nickname,
|
||||||
}) satisfies SimplifiedPlayer,
|
}) satisfies SimplifiedPlayer,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
: await getArchivedRatingsLeaderboard(region, season!).then(
|
||||||
async (players) => {
|
async (players) => {
|
||||||
const firstPlayerIndex =
|
const playerIndex = players.findIndex(
|
||||||
leagueIndex === 0
|
(player) => player.id === id,
|
||||||
? 0
|
);
|
||||||
: players.findIndex(
|
|
||||||
(player) =>
|
if (playerIndex === -1) {
|
||||||
player.score < LEAGUES[leagueIndex - 1].minScore,
|
throw new UserError(
|
||||||
);
|
translate('bot.commands.ratings.body.no_participation', [
|
||||||
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
markdownEscape(accountInfo.nickname),
|
||||||
const trimmed = players.slice(
|
`${season}`,
|
||||||
firstPlayerIndex,
|
]),
|
||||||
lastPlayerIndex + 1,
|
|
||||||
);
|
|
||||||
const clanData = await getClanAccountInfo(
|
|
||||||
region,
|
|
||||||
trimmed.map(({ id }) => id),
|
|
||||||
['clan'],
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return trimmed.map(
|
const halfRange = Math.round(limit / 2);
|
||||||
(player, index) =>
|
const trimmed = players.slice(
|
||||||
({
|
playerIndex - halfRange,
|
||||||
id: player.id,
|
playerIndex + halfRange + 1,
|
||||||
score: player.score,
|
);
|
||||||
position: firstPlayerIndex + index + 1,
|
const clanData = await getClanAccountInfo(
|
||||||
clan: clanData[player.id]?.clan?.tag,
|
region,
|
||||||
nickname:
|
trimmed.map((player) => player.id),
|
||||||
clanData[index]?.account_name ??
|
['clan'],
|
||||||
`Deleted ${player.id}`,
|
);
|
||||||
}) satisfies SimplifiedPlayer,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
|
||||||
midnightLeaderboard = isCurrentSeason
|
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
|
||||||
region,
|
|
||||||
regionRatingsInfo.current_season,
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
|
||||||
|
|
||||||
players = result;
|
return trimmed.map((player, index) => ({
|
||||||
playersBefore = result ? result[0].position - 1 : 0;
|
id: player.id,
|
||||||
playersAfter =
|
score: player.score,
|
||||||
regionRatingsInfo.count - result[result.length - 1].position;
|
position: playerIndex + index + 1,
|
||||||
titleName = `${
|
clan: clanData[index]?.clan?.tag,
|
||||||
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
nickname: clanData[index]
|
||||||
} - ${REGION_NAMES_SHORT[region]}`;
|
? clanData[index]!.account_name
|
||||||
titleImage = leagueInfo.big_icon.startsWith('http')
|
: `Deleted ${player.id}`,
|
||||||
? leagueInfo.big_icon
|
}));
|
||||||
: `https:${leagueInfo.big_icon}`;
|
},
|
||||||
titleDescription = `Top ${limit} players`;
|
);
|
||||||
} else {
|
midnightLeaderboard = isCurrentSeason
|
||||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
? await getArchivedRatingsMidnightLeaderboard(
|
||||||
regionRatingsInfo = isCurrentSeason
|
region,
|
||||||
? await getRatingsInfo(region)
|
regionRatingsInfo.current_season,
|
||||||
: await getArchivedRatingsInfo(region, season!);
|
)
|
||||||
|
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
players = neighbors;
|
||||||
|
titleImage = clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
|
: undefined;
|
||||||
|
titleName = neighbors ? accountInfo.nickname : '';
|
||||||
|
titleDescription = t`bot.commands.ratings.body.neighbors`;
|
||||||
|
playersAfter =
|
||||||
|
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
||||||
|
playerId = id;
|
||||||
|
}
|
||||||
|
|
||||||
const accountInfo = await getAccountInfo(region, id);
|
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
|
||||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
detail: undefined;
|
||||||
const neighbors = isCurrentSeason
|
} = regionRatingsInfo;
|
||||||
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
|
|
||||||
(players) =>
|
|
||||||
players.neighbors.map(
|
|
||||||
(player) =>
|
|
||||||
({
|
|
||||||
id: player.spa_id,
|
|
||||||
position: player.number,
|
|
||||||
score: player.score,
|
|
||||||
clan: player.clan_tag,
|
|
||||||
nickname: player.nickname,
|
|
||||||
}) satisfies SimplifiedPlayer,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
|
||||||
async (players) => {
|
|
||||||
const playerIndex = players.findIndex(
|
|
||||||
(player) => player.id === id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (playerIndex === -1) {
|
const items = players.map((player) => {
|
||||||
throw new UserError(
|
const reward = resolvedRegionRatingsInfo.rewards.find(
|
||||||
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`,
|
(reward) =>
|
||||||
);
|
player.position >= reward.from_position &&
|
||||||
}
|
player.position <= reward.to_position,
|
||||||
|
);
|
||||||
const halfRange = Math.round(limit / 2);
|
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||||
const trimmed = players.slice(
|
(item) => item.id === player.id,
|
||||||
playerIndex - halfRange,
|
);
|
||||||
playerIndex + halfRange + 1,
|
const midnightScore =
|
||||||
);
|
midnightIndex === undefined
|
||||||
const clanData = await getClanAccountInfo(
|
? player.score
|
||||||
region,
|
: midnightLeaderboard![midnightIndex].score;
|
||||||
trimmed.map((player) => player.id),
|
const midnightPosition =
|
||||||
['clan'],
|
midnightIndex === undefined ? player.position : midnightIndex + 1;
|
||||||
);
|
|
||||||
|
|
||||||
return trimmed.map((player, index) => ({
|
|
||||||
id: player.id,
|
|
||||||
score: player.score,
|
|
||||||
position: playerIndex + index + 1,
|
|
||||||
clan: clanData[index]?.clan?.tag,
|
|
||||||
nickname: clanData[index]
|
|
||||||
? clanData[index]!.account_name
|
|
||||||
: `Deleted ${player.id}`,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
midnightLeaderboard = isCurrentSeason
|
|
||||||
? await getArchivedRatingsMidnightLeaderboard(
|
|
||||||
region,
|
|
||||||
regionRatingsInfo.current_season,
|
|
||||||
)
|
|
||||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
|
||||||
|
|
||||||
players = neighbors;
|
|
||||||
titleImage = clan
|
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
|
||||||
: undefined;
|
|
||||||
titleName = neighbors
|
|
||||||
? accountInfo.nickname
|
|
||||||
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
|
||||||
titleDescription = 'Ratings neighbors';
|
|
||||||
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
|
||||||
playersAfter =
|
|
||||||
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
|
||||||
playerId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
|
|
||||||
detail: undefined;
|
|
||||||
} = regionRatingsInfo;
|
|
||||||
|
|
||||||
const items = players.map((player) => {
|
|
||||||
const reward = resolvedRegionRatingsInfo.rewards.find(
|
|
||||||
(reward) =>
|
|
||||||
player.position >= reward.from_position &&
|
|
||||||
player.position <= reward.to_position,
|
|
||||||
);
|
|
||||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
|
||||||
(item) => item.id === player.id,
|
|
||||||
);
|
|
||||||
const midnightScore =
|
|
||||||
midnightIndex === undefined
|
|
||||||
? player.score
|
|
||||||
: midnightLeaderboard![midnightIndex].score;
|
|
||||||
const midnightPosition =
|
|
||||||
midnightIndex === undefined ? player.position : midnightIndex + 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Leaderboard.Item
|
|
||||||
nickname={player.nickname}
|
|
||||||
score={player.score}
|
|
||||||
position={player.position}
|
|
||||||
clan={player.clan}
|
|
||||||
reward={reward}
|
|
||||||
deltaScore={player.score - midnightScore}
|
|
||||||
deltaPosition={midnightPosition - player.position}
|
|
||||||
key={player.id}
|
|
||||||
highlight={player.id === playerId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Leaderboard.Item
|
||||||
<TitleBar
|
nickname={player.nickname}
|
||||||
name={titleName}
|
score={player.score}
|
||||||
image={titleImage}
|
position={player.position}
|
||||||
description={titleDescription}
|
clan={player.clan}
|
||||||
/>
|
reward={reward}
|
||||||
|
deltaScore={player.score - midnightScore}
|
||||||
{items && (
|
deltaPosition={midnightPosition - player.position}
|
||||||
<Leaderboard.Root>
|
key={player.id}
|
||||||
{playersBefore > 0 && (
|
highlight={player.id === playerId}
|
||||||
<Leaderboard.Gap
|
/>
|
||||||
message={`+ ${playersBefore.toLocaleString()} more`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{items}
|
|
||||||
{playersAfter > 0 && (
|
|
||||||
<Leaderboard.Gap
|
|
||||||
message={`+ ${playersAfter.toLocaleString()} more`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Leaderboard.Root>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
return (
|
||||||
});
|
<CommandWrapper>
|
||||||
},
|
<TitleBar
|
||||||
);
|
title={titleName}
|
||||||
|
image={titleImage}
|
||||||
|
description={titleDescription}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{items && (
|
||||||
|
<Leaderboard.Root>
|
||||||
|
{items}
|
||||||
|
{playersAfter > 0 && (
|
||||||
|
<Leaderboard.Gap
|
||||||
|
message={translate('bot.commands.ratings.body.more', [
|
||||||
|
`${playersAfter.toLocaleString(interaction.locale)}`,
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Leaderboard.Root>
|
||||||
|
)}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
});
|
||||||
|
});
|
||||||
+60
-34
@@ -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;
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
.setName('search-clans')
|
resolve({
|
||||||
.setDescription('Search clans')
|
inProduction: true,
|
||||||
.addStringOption(addRegionChoices)
|
inPublic: true,
|
||||||
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('limit')
|
|
||||||
.setDescription('The size of the search result (default: 25)')
|
|
||||||
.setMinValue(1)
|
|
||||||
.setMaxValue(100),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
command: createLocalizedCommand('search-clans')
|
||||||
const server = interaction.options.getString('region') as Region;
|
.addStringOption(addRegionChoices)
|
||||||
const clan = interaction.options.getString('clan')!;
|
.addStringOption((option) =>
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
addClanChoices(option).setAutocomplete(false),
|
||||||
const clanList = await fetchBlitz<ClanList>(
|
)
|
||||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
.addIntegerOption((option) => {
|
||||||
);
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
return embedInfo(
|
return option
|
||||||
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
|
.setName(t`bot.commands.search_clans.options.limit`)
|
||||||
clanList.length === 0
|
.setNameLocalizations(
|
||||||
? 'No clans found.'
|
localizationObject('bot.commands.search_clans.options.limit'),
|
||||||
: `\`\`\`\n${clanList
|
)
|
||||||
.map((clan) => `${clan.name} [${clan.tag}]`)
|
.setDescription(
|
||||||
.join('\n')}\n\`\`\``,
|
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) {
|
||||||
|
const { translate } = translator(interaction.locale);
|
||||||
|
const server = interaction.options.getString('region') as Region;
|
||||||
|
const clan = interaction.options.getString('clan')!;
|
||||||
|
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
|
||||||
|
const clanList = await fetchBlitz<ClanList>(
|
||||||
|
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return embedInfo(
|
||||||
|
translate('bot.commands.search_clans.body.title', [
|
||||||
|
markdownEscape(clan),
|
||||||
|
translate(`common.regions.normal.${server}`),
|
||||||
|
]),
|
||||||
|
clanList.length === 0
|
||||||
|
? translate('bot.commands.search_clans.body.no_results')
|
||||||
|
: `\`\`\`\n${clanList
|
||||||
|
.map((clan) => `${clan.name} [${clan.tag}]`)
|
||||||
|
.join('\n')}\n\`\`\``,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,56 +1,78 @@
|
|||||||
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 { AccountList } from '../core/blitz/searchPlayersAcrossRegions';
|
||||||
AccountList,
|
import { usernamePattern } from '../core/blitz/searchPlayersAcrossRegions/constants';
|
||||||
usernamePattern,
|
|
||||||
} 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;
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
.setName('search-players')
|
const { t, translate } = translator(Locale.EnglishUS);
|
||||||
.setDescription('Search players in a Blitz server')
|
|
||||||
.addStringOption(addRegionChoices)
|
|
||||||
.addStringOption((option) =>
|
|
||||||
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
|
|
||||||
)
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('limit')
|
|
||||||
.setDescription('The size of the search result (default: 25)')
|
|
||||||
.setMinValue(1)
|
|
||||||
.setMaxValue(100),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
resolve({
|
||||||
const server = interaction.options.getString('region') as Region;
|
inProduction: true,
|
||||||
const name = interaction.options.getString('username')!;
|
inPublic: true,
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
|
||||||
const trimmedSearch = name.trim();
|
|
||||||
const players = usernamePattern.test(trimmedSearch)
|
|
||||||
? await fetchBlitz<AccountList>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
|
|
||||||
trimmedSearch,
|
|
||||||
)}&limit=${limit}`,
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return embedInfo(
|
command: createLocalizedCommand('search-players')
|
||||||
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
|
.addStringOption(addRegionChoices)
|
||||||
`\`\`\`${
|
.addStringOption((option) =>
|
||||||
players.length === 0
|
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
|
||||||
? 'No players found.'
|
)
|
||||||
: players.map((player) => player.nickname).join('\n')
|
.addIntegerOption((option) =>
|
||||||
}\`\`\``,
|
option
|
||||||
);
|
.setName(t`bot.commands.search_players.options.limit`)
|
||||||
},
|
.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)
|
||||||
|
.setMaxValue(100),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { translate } = translator(interaction.locale);
|
||||||
|
const server = interaction.options.getString('region') as Region;
|
||||||
|
const name = interaction.options.getString('username')!;
|
||||||
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
|
const trimmedSearch = name.trim();
|
||||||
|
const players = usernamePattern.test(trimmedSearch)
|
||||||
|
? await fetchBlitz<AccountList>(
|
||||||
|
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
|
||||||
|
trimmedSearch,
|
||||||
|
)}&limit=${limit}`,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return embedInfo(
|
||||||
|
translate('bot.commands.search_players.body.title', [
|
||||||
|
markdownEscape(trimmedSearch),
|
||||||
|
translate(`common.regions.normal.${server}`),
|
||||||
|
]),
|
||||||
|
`\`\`\`${
|
||||||
|
players.length === 0
|
||||||
|
? translate('bot.commands.search_players.body.no_results')
|
||||||
|
: 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\`\`\``,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// });
|
||||||
+58
-52
@@ -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,80 +28,85 @@ 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) => {
|
resolve({
|
||||||
const command = await addPeriodicFilterOptions(
|
inProduction: true,
|
||||||
new SlashCommandBuilder()
|
inPublic: true,
|
||||||
.setName('stats')
|
|
||||||
.setDescription('Regular battles statistics'),
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand('stats'),
|
||||||
(option) => option.addStringOption(addUsernameChoices),
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
);
|
),
|
||||||
|
|
||||||
resolve({
|
async handler(interaction) {
|
||||||
inProduction: true,
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
inPublic: true,
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
command,
|
return Promise.all([
|
||||||
|
render(player, period, filters, interaction.locale),
|
||||||
|
buttonRefresh(interaction, path),
|
||||||
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
async handler(interaction) {
|
autocomplete: (interaction) => {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
autocompleteUsername(interaction);
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
autocompleteTanks(interaction);
|
||||||
const filters = await getFiltersFromCommand(interaction);
|
},
|
||||||
const path = commandToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
...getCustomPeriodParams(interaction),
|
|
||||||
...filters,
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all([
|
async button(interaction) {
|
||||||
render(player, period, filters),
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
buttonRefresh(interaction, path),
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
getBlitzStarsLinkButton(player.region, player.id),
|
const filters = getFiltersFromButton(interaction);
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
return await render(player, period, filters, interaction.locale);
|
||||||
autocompleteUsername(interaction);
|
},
|
||||||
autocompleteTanks(interaction);
|
});
|
||||||
},
|
});
|
||||||
|
|
||||||
async button(interaction) {
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
|
||||||
const filters = getFiltersFromButton(interaction);
|
|
||||||
|
|
||||||
return await render(player, period, filters);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
+45
-51
@@ -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,58 +14,52 @@ 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/render';
|
||||||
|
|
||||||
export const todayCommand = new Promise<CommandRegistryPromisable>(
|
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
async (resolve) => {
|
const nations = await NATIONS;
|
||||||
const awaitedEncyclopediaInfo = await encyclopediaInfo;
|
resolve({
|
||||||
const command = addFilterOptions(
|
inProduction: true,
|
||||||
new SlashCommandBuilder()
|
inPublic: true,
|
||||||
.setName('today')
|
|
||||||
.setDescription("Today's played tanks and statistics"),
|
|
||||||
awaitedEncyclopediaInfo,
|
|
||||||
).addStringOption(addUsernameChoices);
|
|
||||||
|
|
||||||
resolve({
|
command: addFilterOptions(
|
||||||
inProduction: true,
|
createLocalizedCommand('today'),
|
||||||
inPublic: true,
|
nations,
|
||||||
|
).addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
command,
|
async handler(interaction) {
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(
|
||||||
|
player.region,
|
||||||
|
interaction,
|
||||||
|
'today',
|
||||||
|
);
|
||||||
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction, true),
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
async handler(interaction) {
|
return [
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
...(await renderBreakdown(player, period, filters, interaction.locale)),
|
||||||
const period = resolvePeriodFromCommand(
|
buttonRefresh(interaction, path),
|
||||||
player.region,
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
interaction,
|
];
|
||||||
'today',
|
},
|
||||||
);
|
|
||||||
const filters = await getFiltersFromCommand(interaction);
|
|
||||||
const path = commandToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
...getCustomPeriodParams(interaction, true),
|
|
||||||
...filters,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
autocomplete: (interaction) => {
|
||||||
...(await renderBreakdown(player, period, filters)),
|
autocompleteUsername(interaction);
|
||||||
buttonPrimary(path, 'Refresh'),
|
autocompleteTanks(interaction);
|
||||||
await getBlitzStarsLinkButton(player.region, player.id),
|
},
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
async button(interaction) {
|
||||||
autocompleteUsername(interaction);
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
autocompleteTanks(interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
},
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
async button(interaction) {
|
return await renderBreakdown(player, period, filters, interaction.locale);
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
},
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
});
|
||||||
const filters = getFiltersFromButton(interaction);
|
});
|
||||||
|
|
||||||
return await renderBreakdown(player, period, filters);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -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/constants';
|
||||||
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)
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {
|
import { MeshBasicMaterial, Object3D, ShaderMaterial, Vector2 } from 'three';
|
||||||
MeshBasicMaterial,
|
|
||||||
Object3D,
|
|
||||||
ShaderMaterial,
|
|
||||||
Vector2,
|
|
||||||
WebGLRenderTarget,
|
|
||||||
} from 'three';
|
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
import { canSplash } from '../../../../core/blitz/canSplash';
|
import { canSplash } from '../../../../core/blitz/canSplash';
|
||||||
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
||||||
@@ -21,8 +15,7 @@ import {
|
|||||||
} from '../../../../stores/tankopedia';
|
} from '../../../../stores/tankopedia';
|
||||||
import fragmentShader from './shaders/fragment.glsl';
|
import fragmentShader from './shaders/fragment.glsl';
|
||||||
import vertexShader from './shaders/vertex.glsl';
|
import vertexShader from './shaders/vertex.glsl';
|
||||||
|
import { spacedArmorRenderTarget } from './target';
|
||||||
export const spacedArmorRenderTarget = new WebGLRenderTarget();
|
|
||||||
|
|
||||||
interface CoreArmorSceneComponentProps {
|
interface CoreArmorSceneComponentProps {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ void main() {
|
|||||||
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
|
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
|
||||||
remainingPenetration -= spacedArmorThickness;
|
remainingPenetration -= spacedArmorThickness;
|
||||||
|
|
||||||
if (isExplosive && !canSplash && remainingPenetration > 0.0) {
|
if (isExplosive && remainingPenetration > 0.0) {
|
||||||
// there is a 50% penetration loss per meter for HEAT shells
|
// there is a 50% penetration loss per meter for HEAT shells
|
||||||
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
|
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { WebGLRenderTarget } from 'three';
|
||||||
|
|
||||||
|
export const spacedArmorRenderTarget = new WebGLRenderTarget();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Flex, Inset, Text } from '@radix-ui/themes';
|
import { Card, Flex, Text } from '@radix-ui/themes';
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from '@react-three/drei';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
@@ -182,33 +182,38 @@ export function ShotDisplay() {
|
|||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
trueStatus === 'penetration'
|
trueStatus === 'penetration'
|
||||||
? '#00ff00c0'
|
? '#00ff0080'
|
||||||
: trueStatus === 'blocked'
|
: trueStatus === 'blocked'
|
||||||
? '#ff0000c0'
|
? '#ff000080'
|
||||||
: '#ffff00c0',
|
: '#ffff0080',
|
||||||
|
borderRadius: 8,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
padding: 4,
|
||||||
|
width: hasMultipleLayers ? undefined : 8,
|
||||||
|
height: hasMultipleLayers ? undefined : 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Inset>
|
{hasMultipleLayers && (
|
||||||
{hasMultipleLayers && (
|
<Text
|
||||||
<Text
|
size="2"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
color:
|
color: trueStatus === 'ricochet' ? 'black' : undefined,
|
||||||
trueStatus === 'ricochet' ? 'black' : undefined,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<b>{layer.index + 1}</b>
|
||||||
<b>{layer.index + 1}</b>
|
</Text>
|
||||||
</Text>
|
)}
|
||||||
)}
|
</div>
|
||||||
</Inset>
|
|
||||||
</Card>
|
|
||||||
</Html>
|
</Html>
|
||||||
</group>
|
</group>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const distance = (
|
const distance = (
|
||||||
shot.layers.at(-1) as ShotLayerBase
|
shot.layers.at(-1) as ShotLayerBase
|
||||||
).point.distanceTo(intersections[0].point);
|
).point.distanceTo(intersections[0].point);
|
||||||
if (explosive && !splashing)
|
if (explosive)
|
||||||
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
||||||
const blocked = remainingPenetration < 0;
|
const blocked = remainingPenetration < 0;
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const previousIntersection = intersections[layerIndex - 1];
|
const previousIntersection = intersections[layerIndex - 1];
|
||||||
const distance =
|
const distance =
|
||||||
intersection.distance - previousIntersection.distance;
|
intersection.distance - previousIntersection.distance;
|
||||||
if (explosive && !splashing)
|
if (explosive)
|
||||||
remainingPenetration -=
|
remainingPenetration -=
|
||||||
0.5 * remainingPenetration * distance;
|
0.5 * remainingPenetration * distance;
|
||||||
const wasted = remainingPenetration < 0;
|
const wasted = remainingPenetration < 0;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createPortal, useFrame } from '@react-three/fiber';
|
|||||||
import { memo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { DepthTexture, Scene, Vector2 } from 'three';
|
import { DepthTexture, Scene, Vector2 } from 'three';
|
||||||
import { CoreArmorScene } from './components/CoreArmorScene';
|
import { CoreArmorScene } from './components/CoreArmorScene';
|
||||||
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
|
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent/target';
|
||||||
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||||
|
|
||||||
export const Armor = memo(() => {
|
export const Armor = memo(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface RootProps {
|
interface RootProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
GrayColor,
|
GrayColor,
|
||||||
PALETTES,
|
PALETTES,
|
||||||
} from '../../../constants/radixColors';
|
} from '../../../constants/radixColors';
|
||||||
import { TREE_TYPE_ICONS, TankType, TreeType } from '../../Tanks';
|
import { TankClass, TreeType } from '../../Tanks';
|
||||||
|
import { TREE_TYPE_ICONS } from '../../Tanks/components/Item/constants';
|
||||||
import { RowStat } from './RowStat';
|
import { RowStat } from './RowStat';
|
||||||
|
|
||||||
export interface RowStatItem {
|
export interface RowStatItem {
|
||||||
@@ -22,7 +23,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)[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '../../../constants/radixColors';
|
} from '../../../constants/radixColors';
|
||||||
import PercentileIndicator from '../../PercentileIndicator';
|
import PercentileIndicator from '../../PercentileIndicator';
|
||||||
|
|
||||||
export interface RowStatProps {
|
interface RowStatProps {
|
||||||
name: string;
|
name: string;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
delta?: number;
|
delta?: number;
|
||||||
@@ -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,
|
||||||
|
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||||
...(!naked && {
|
|
||||||
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -2,13 +2,13 @@ import { Percentile } from '../../../constants/percentiles';
|
|||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import PercentileIndicator from '../../PercentileIndicator';
|
import PercentileIndicator from '../../PercentileIndicator';
|
||||||
|
|
||||||
export interface CardItem {
|
interface CardItem {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number | undefined;
|
value: string | number | undefined;
|
||||||
percentile?: Percentile;
|
percentile?: Percentile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardProps {
|
interface CardProps {
|
||||||
items: CardItem[];
|
items: CardItem[];
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface RootProps {
|
interface RootProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface RowProps {
|
interface RowProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
|
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';
|
||||||
import { Root } from './components/Root';
|
import { Root } from './components/Root';
|
||||||
import { Row } from './components/Row';
|
import { Row } from './components/Row';
|
||||||
|
|
||||||
export interface GenericAllStatsProps {
|
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,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Stat } from '..';
|
import { GenericStat } from '..';
|
||||||
import GenericStatsRow from './GenericStatsRow';
|
import GenericStatsRow from './GenericStatsRow';
|
||||||
|
|
||||||
export interface GenericStatsColumnProps {
|
interface GenericStatsColumnProps {
|
||||||
stats: Stat[];
|
stats: GenericStat[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
|
export default function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Stat } from '..';
|
import { GenericStat } from '..';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import PercentileIndicator from '../../PercentileIndicator';
|
import PercentileIndicator from '../../PercentileIndicator';
|
||||||
|
|
||||||
export interface GenericStatsRowProps {
|
interface GenericStatsRowProps {
|
||||||
stat: Stat;
|
stat: GenericStat;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
||||||
@@ -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,12 +1,12 @@
|
|||||||
import { Percentile } from '../../constants/percentiles';
|
import { Percentile } from '../../constants/percentiles';
|
||||||
import GenericStatsColumn from './components/GenericStatsColumn';
|
import GenericStatsColumn from './components/GenericStatsColumn';
|
||||||
|
|
||||||
export type Stat =
|
export type GenericStat =
|
||||||
| [string, string | number | undefined]
|
| [string, string | number | undefined]
|
||||||
| [string, string | number | undefined, Percentile | undefined];
|
| [string, string | number | undefined, Percentile | undefined];
|
||||||
|
|
||||||
export interface GenericStatsProps {
|
export interface GenericStatsProps {
|
||||||
stats: Stat[];
|
stats: GenericStat[];
|
||||||
columnCount?: number;
|
columnCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export default function GenericStats({
|
|||||||
}: GenericStatsProps) {
|
}: GenericStatsProps) {
|
||||||
const filteredStats = stats.filter(([, value]) => value !== undefined);
|
const filteredStats = stats.filter(([, value]) => value !== undefined);
|
||||||
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
|
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
|
||||||
const columns: Stat[][] = [];
|
const columns: GenericStat[][] = [];
|
||||||
|
|
||||||
filteredStats.forEach((stat, index) => {
|
filteredStats.forEach((stat, index) => {
|
||||||
const column = Math.floor(index / itemsPerRow);
|
const column = Math.floor(index / itemsPerRow);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export enum LineColor {
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Blue,
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
export enum LineStyle {
|
||||||
|
Solid,
|
||||||
|
Dashes,
|
||||||
|
DashesAndDots,
|
||||||
|
}
|
||||||
+4
-16
@@ -1,12 +1,6 @@
|
|||||||
import { blue, green, red, slateDark } from '@radix-ui/colors';
|
import { blue, green, red, slateDark } from '@radix-ui/colors';
|
||||||
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './Root';
|
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
|
||||||
|
import { LineColor, LineStyle } from './constants';
|
||||||
export enum LineColor {
|
|
||||||
Red,
|
|
||||||
Green,
|
|
||||||
Blue,
|
|
||||||
White,
|
|
||||||
}
|
|
||||||
|
|
||||||
const LINE_COLORS = {
|
const LINE_COLORS = {
|
||||||
[LineColor.Red]: red.red9,
|
[LineColor.Red]: red.red9,
|
||||||
@@ -15,12 +9,6 @@ const LINE_COLORS = {
|
|||||||
[LineColor.White]: slateDark.slate11,
|
[LineColor.White]: slateDark.slate11,
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum LineStyle {
|
|
||||||
Solid,
|
|
||||||
Dashes,
|
|
||||||
DashesAndDots,
|
|
||||||
}
|
|
||||||
|
|
||||||
const LINE_STYLES = {
|
const LINE_STYLES = {
|
||||||
[LineStyle.Solid]: undefined,
|
[LineStyle.Solid]: undefined,
|
||||||
[LineStyle.Dashes]: '8, 8',
|
[LineStyle.Dashes]: '8, 8',
|
||||||
@@ -28,9 +16,9 @@ const LINE_STYLES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PlotItem = [number, number];
|
export type PlotItem = [number, number];
|
||||||
export type Plot = PlotItem[];
|
type Plot = PlotItem[];
|
||||||
|
|
||||||
export interface LineProps {
|
interface LineProps {
|
||||||
plot: Plot;
|
plot: Plot;
|
||||||
minY?: number;
|
minY?: number;
|
||||||
maxY?: number;
|
maxY?: number;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum MarginOrientation {
|
||||||
|
Vertical,
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
+4
-8
@@ -1,13 +1,9 @@
|
|||||||
import { times } from 'lodash';
|
import { times } from 'lodash';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../../stitches.config';
|
||||||
import { MarginInputProps } from './Root';
|
import { MarginInputProps } from '../Root';
|
||||||
|
import { MarginOrientation } from './constants';
|
||||||
|
|
||||||
export enum MarginOrientation {
|
interface MarginProps extends MarginInputProps {
|
||||||
Vertical,
|
|
||||||
Horizontal,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarginProps extends MarginInputProps {
|
|
||||||
verticalSeparations: number;
|
verticalSeparations: number;
|
||||||
orientation?: MarginOrientation;
|
orientation?: MarginOrientation;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const GRAPH_WIDTH = 384;
|
||||||
|
export const GRAPH_HEIGHT = 320;
|
||||||
+5
-6
@@ -1,10 +1,9 @@
|
|||||||
import { times } from 'lodash';
|
import { times } from 'lodash';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../../stitches.config';
|
||||||
import { Margin, MarginOrientation } from './Margin';
|
import { Margin } from '../Margin';
|
||||||
|
import { MarginOrientation } from '../Margin/constants';
|
||||||
export const GRAPH_WIDTH = 384;
|
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './constants';
|
||||||
export const GRAPH_HEIGHT = 320;
|
|
||||||
|
|
||||||
export interface MarginInputProps {
|
export interface MarginInputProps {
|
||||||
min: number;
|
min: number;
|
||||||
@@ -13,7 +12,7 @@ export interface MarginInputProps {
|
|||||||
precision?: number;
|
precision?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootProps {
|
interface RootProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
verticalMargin?: MarginInputProps;
|
verticalMargin?: MarginInputProps;
|
||||||
horizontalMargin?: MarginInputProps;
|
horizontalMargin?: MarginInputProps;
|
||||||
@@ -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,6 +1,6 @@
|
|||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export interface GapProps {
|
interface GapProps {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ComponentProps } from 'react';
|
|||||||
import { RatingsReward } from '../../../commands/ratings';
|
import { RatingsReward } from '../../../commands/ratings';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export interface ItemProps extends ComponentProps<'div'> {
|
interface ItemProps extends ComponentProps<'div'> {
|
||||||
position: number;
|
position: number;
|
||||||
deltaPosition?: number;
|
deltaPosition?: number;
|
||||||
score?: number;
|
score?: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface RootProps {
|
interface RootProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DiscordLogoIcon } from '@radix-ui/react-icons';
|
import { DiscordLogoIcon } from '@radix-ui/react-icons';
|
||||||
import { Flex, Text, Theme } from '@radix-ui/themes';
|
import { Flex, Text, Theme } from '@radix-ui/themes';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { theme } from '../../stitches.config';
|
|
||||||
import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide';
|
import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide';
|
||||||
|
import { theme } from '../../stitches.config';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { theme } from '../stitches.config';
|
|
||||||
|
|
||||||
export enum NoDataType {
|
|
||||||
BattlesInPeriod,
|
|
||||||
PlayersInPeriod,
|
|
||||||
TanksFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NoDataProps {
|
|
||||||
type: NoDataType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NO_DATA_MESSAGES: Record<NoDataType, string> = {
|
|
||||||
[NoDataType.BattlesInPeriod]: 'No battles played in this period',
|
|
||||||
[NoDataType.PlayersInPeriod]: 'No players in this period',
|
|
||||||
[NoDataType.TanksFound]: 'No tanks found',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function NoData({ type }: NoDataProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
|
||||||
{NO_DATA_MESSAGES[type]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum NoDataType {
|
||||||
|
BattlesInPeriod,
|
||||||
|
PlayersInPeriod,
|
||||||
|
TanksFound,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Locale } from 'discord.js';
|
||||||
|
import { translator } from '../../core/localization/translator';
|
||||||
|
import { theme } from '../../stitches.config';
|
||||||
|
|
||||||
|
interface NoDataProps {
|
||||||
|
type: 'battles_in_period' | 'players_in_period' | 'tanks_found';
|
||||||
|
locale: Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NoData({ type, locale }: NoDataProps) {
|
||||||
|
const { translate } = translator(locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||||
|
{translate(`bot.common.no_data.${type}`)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-20
@@ -10,11 +10,7 @@ import {
|
|||||||
slate,
|
slate,
|
||||||
teal,
|
teal,
|
||||||
} from '@radix-ui/colors';
|
} from '@radix-ui/colors';
|
||||||
import { Percentile } from '../constants/percentiles';
|
import { Percentile } from '../../constants/percentiles';
|
||||||
|
|
||||||
export interface PercentileIndicatorProps {
|
|
||||||
percentile: Percentile;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PERCENTILE_COLORS = {
|
export const PERCENTILE_COLORS = {
|
||||||
[Percentile.VeryBad]: slate.slate9,
|
[Percentile.VeryBad]: slate.slate9,
|
||||||
@@ -28,18 +24,3 @@ export const PERCENTILE_COLORS = {
|
|||||||
[Percentile.Unicum]: pink.pink9,
|
[Percentile.Unicum]: pink.pink9,
|
||||||
[Percentile.SuperUnicum]: purple.purple9,
|
[Percentile.SuperUnicum]: purple.purple9,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PercentileIndicator({
|
|
||||||
percentile,
|
|
||||||
}: PercentileIndicatorProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: PERCENTILE_COLORS[percentile],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Percentile } from '../../constants/percentiles';
|
||||||
|
import { PERCENTILE_COLORS } from './constants';
|
||||||
|
|
||||||
|
interface PercentileIndicatorProps {
|
||||||
|
percentile: Percentile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PercentileIndicator({
|
||||||
|
percentile,
|
||||||
|
}: PercentileIndicatorProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: PERCENTILE_COLORS[percentile],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface ColumnProps {
|
interface ColumnProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { TankClass, TreeType } from '.';
|
||||||
|
import {
|
||||||
|
TANK_ICONS,
|
||||||
|
TANK_ICONS_COLLECTOR,
|
||||||
|
TANK_ICONS_PREMIUM,
|
||||||
|
} from '../../../../core/blitzkrieg/tankDefinitions/constants';
|
||||||
|
|
||||||
|
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const; // BIG TODO: REMOVE THIS AFTER LOCALIZATION
|
||||||
|
|
||||||
|
export const TANK_CLASSES = [
|
||||||
|
'lightTank',
|
||||||
|
'mediumTank',
|
||||||
|
'heavyTank',
|
||||||
|
'AT-SPG',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
|
||||||
|
researchable: 'Tech tree',
|
||||||
|
premium: 'Premium',
|
||||||
|
collector: 'Collector',
|
||||||
|
};
|
||||||
|
export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
|
||||||
|
researchable: 'https://i.imgur.com/pJxO2XY.png',
|
||||||
|
premium: 'https://i.imgur.com/mZzSwOU.png',
|
||||||
|
collector: 'https://i.imgur.com/7A0RsG5.png',
|
||||||
|
};
|
||||||
|
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankClass, string>> = {
|
||||||
|
researchable: TANK_ICONS,
|
||||||
|
premium: TANK_ICONS_PREMIUM,
|
||||||
|
collector: TANK_ICONS_COLLECTOR,
|
||||||
|
};
|
||||||
+5
-37
@@ -1,37 +1,11 @@
|
|||||||
import {
|
import { theme } from '../../../../stitches.config';
|
||||||
TANK_ICONS,
|
import { TANK_CLASSES, TREE_TYPES, TREE_TYPE_ICONS } from './constants';
|
||||||
TANK_ICONS_COLLECTOR,
|
|
||||||
TANK_ICONS_PREMIUM,
|
|
||||||
} from '../../../core/blitzkrieg/tankDefinitions';
|
|
||||||
import { theme } from '../../../stitches.config';
|
|
||||||
|
|
||||||
export const TANK_TYPES = [
|
export type TankClass = (typeof TANK_CLASSES)[number];
|
||||||
'light',
|
|
||||||
'medium',
|
|
||||||
'heavy',
|
|
||||||
'tankDestroyer',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
|
|
||||||
|
|
||||||
export type TankType = (typeof TANK_TYPES)[number];
|
|
||||||
export type TreeType = (typeof TREE_TYPES)[number];
|
export type TreeType = (typeof TREE_TYPES)[number];
|
||||||
|
|
||||||
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
|
|
||||||
researchable: 'Tech tree',
|
|
||||||
premium: 'Premium',
|
|
||||||
collector: 'Collector',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
|
|
||||||
researchable: 'https://i.imgur.com/pJxO2XY.png',
|
|
||||||
premium: 'https://i.imgur.com/mZzSwOU.png',
|
|
||||||
collector: 'https://i.imgur.com/7A0RsG5.png',
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ItemProps {
|
export interface ItemProps {
|
||||||
image?: string;
|
tankType?: TankClass;
|
||||||
tankType?: TankType;
|
|
||||||
name: string;
|
name: string;
|
||||||
treeType: TreeType;
|
treeType: TreeType;
|
||||||
}
|
}
|
||||||
@@ -42,13 +16,7 @@ const TREE_TYPE_COLOR = {
|
|||||||
collector: '_blue',
|
collector: '_blue',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankType, string>> = {
|
export function Item({ tankType, name, treeType }: ItemProps) {
|
||||||
researchable: TANK_ICONS,
|
|
||||||
premium: TANK_ICONS_PREMIUM,
|
|
||||||
collector: TANK_ICONS_COLLECTOR,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Item({ image, tankType, name, treeType }: ItemProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface RootProps {
|
interface RootProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface RowProps {
|
interface RowProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export interface TitleProps {
|
interface TitleProps {
|
||||||
children: string;
|
children: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { theme } from '../stitches.config';
|
|||||||
|
|
||||||
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
||||||
|
|
||||||
export interface TierWeightsProps {
|
interface TierWeightsProps {
|
||||||
weights: TierWeightsRecord;
|
weights: TierWeightsRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 188 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user