mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-25 11:39:51 +01:00
Compare commits
355
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
006b2fef78 | ||
|
|
26a0c1f176 | ||
|
|
403d1e7849 | ||
|
|
c9d8f6b445 | ||
|
|
73c53d6ef5 | ||
|
|
f8e8fd4016 | ||
|
|
15b29f439a | ||
|
|
fd96cd5975 | ||
|
|
66b9a9076f | ||
|
|
746c7963de | ||
|
|
ae96ae34c0 | ||
|
|
1f690d11c7 | ||
|
|
8cf3da12fa | ||
|
|
a8ae806f8b | ||
|
|
8a6841adea | ||
|
|
5e88f5a73d | ||
|
|
aa1f357538 | ||
|
|
ca270e2010 | ||
|
|
17264c1adc | ||
|
|
2627aba938 | ||
|
|
a2574efb9c | ||
|
|
d3ab4bac51 | ||
|
|
ad78e45f1b | ||
|
|
5ea7bbd6a1 | ||
|
|
2b955a281b | ||
|
|
676601b06e | ||
|
|
b81d9985ee | ||
|
|
d66d25a261 | ||
|
|
402d054b30 | ||
|
|
61d6d42742 | ||
|
|
03e06b050e | ||
|
|
cca46957bc | ||
|
|
3c92ab3a19 | ||
|
|
6c31f663f6 | ||
|
|
bd140f0eda | ||
|
|
a3d8fcb960 | ||
|
|
06304cf075 | ||
|
|
416d061df1 | ||
|
|
47c87970cd | ||
|
|
ff3ed9d656 | ||
|
|
4e9c3df5eb | ||
|
|
c100a1e655 | ||
|
|
26cc0cc723 | ||
|
|
f697f94bd5 | ||
|
|
30fa18f0fd | ||
|
|
c45af8e156 | ||
|
|
f95a6e3495 | ||
|
|
0e637d772e | ||
|
|
a834d48a38 | ||
|
|
f550386fd9 | ||
|
|
0a4b81cde4 | ||
|
|
e1cc60ea5e | ||
|
|
a40860901b | ||
|
|
f3e96718eb | ||
|
|
a00411919d | ||
|
|
b298874f0d | ||
|
|
7011d4d20f | ||
|
|
d9ce16b41a | ||
|
|
f0e0d849d5 | ||
|
|
6d2573ee95 | ||
|
|
4c77cc2282 | ||
|
|
32f6916266 | ||
|
|
3a91e082d5 | ||
|
|
2184c1c703 | ||
|
|
95ad6bade7 | ||
|
|
8bda4a5331 | ||
|
|
16c77f587a | ||
|
|
a218c791a9 | ||
|
|
dccdf55bfc | ||
|
|
1ba6e70c56 | ||
|
|
0e76184813 | ||
|
|
c674da29d5 | ||
|
|
ba4e6dfd68 | ||
|
|
980fd6e50e | ||
|
|
44e7cd7505 | ||
|
|
3a60535564 | ||
|
|
9abfeb749c | ||
|
|
0442c789f1 | ||
|
|
76dc9f0742 | ||
|
|
ff56f1441e | ||
|
|
e3db886923 | ||
|
|
249f62759e | ||
|
|
7767481633 | ||
|
|
a97b1383d8 | ||
|
|
1b224b3949 | ||
|
|
018aa33a7f | ||
|
|
4b7b8b3392 | ||
|
|
272824a72f | ||
|
|
ac02d65db0 | ||
|
|
9bcaff9fd3 | ||
|
|
7b650bf39b | ||
|
|
ee1ecb83b7 | ||
|
|
d6e37119a8 | ||
|
|
385cc3b6d2 | ||
|
|
b4936bf13b | ||
|
|
e1e9403c60 | ||
|
|
0828d50830 | ||
|
|
0817ba18bd | ||
|
|
121a80d079 | ||
|
|
932c414b0c | ||
|
|
3a38441d42 | ||
|
|
3e039d34c2 | ||
|
|
2bae132d18 | ||
|
|
bc0004574a | ||
|
|
2e208507c3 | ||
|
|
33209a6d31 | ||
|
|
13311245a5 | ||
|
|
4067357b54 | ||
|
|
fd6b69edc1 | ||
|
|
2504833a29 | ||
|
|
6d31dcb74b | ||
|
|
a149af50fd | ||
|
|
13927927b3 | ||
|
|
68edf520c5 | ||
|
|
f7933a1295 | ||
|
|
9a1c09490f | ||
|
|
c8b5c216e3 | ||
|
|
344954513a | ||
|
|
f8029bdf67 | ||
|
|
d9c7d31017 | ||
|
|
79dba0fe2c | ||
|
|
c0376182f8 | ||
|
|
8e3b5de249 | ||
|
|
44711a58c7 | ||
|
|
e7337145d2 | ||
|
|
c116b40dfc | ||
|
|
d88f7ad539 | ||
|
|
b5d61cebb3 | ||
|
|
551108e047 | ||
|
|
5feec9bf1e | ||
|
|
313691d3e9 | ||
|
|
950c99d3d7 | ||
|
|
80cc74a945 | ||
|
|
4e586253c0 | ||
|
|
08980158e4 | ||
|
|
1e917c7b89 | ||
|
|
8d3678ddcb | ||
|
|
6a58c5ea1d | ||
|
|
b9bbf22f2c | ||
|
|
c99ed0cb53 | ||
|
|
cef18d4f64 | ||
|
|
824169fc25 | ||
|
|
841b5708ae | ||
|
|
8ad110f73b | ||
|
|
0869ea09c8 | ||
|
|
0e57943d33 | ||
|
|
8b01b7c192 | ||
|
|
a7bd671309 | ||
|
|
406ad52c8d | ||
|
|
b6c4c4e852 | ||
|
|
f193d5c503 | ||
|
|
6a06cb17a3 | ||
|
|
bf218b0975 | ||
|
|
5412c479c8 | ||
|
|
ccb86ae824 | ||
|
|
001709c25c | ||
|
|
e50b8d11bf | ||
|
|
ea782af079 | ||
|
|
0ebf9f6c3d | ||
|
|
6fec467ec8 | ||
|
|
8b0f1cdfed | ||
|
|
f6008bd288 | ||
|
|
1776c8fc15 | ||
|
|
99f41b9805 | ||
|
|
d4f11fc52a | ||
|
|
750aeb39c9 | ||
|
|
0f355bc2fc | ||
|
|
6ebd3063ed | ||
|
|
b070cad92a | ||
|
|
204fbbff59 | ||
|
|
98e7760142 | ||
|
|
6c1c088d44 | ||
|
|
0b7f717057 | ||
|
|
728cb43f05 | ||
|
|
ff02f06cbc | ||
|
|
7a817cbe8c | ||
|
|
58e0876aa0 | ||
|
|
f84f846e76 | ||
|
|
ddb869e767 | ||
|
|
015d457d23 | ||
|
|
ba6a83a9cb | ||
|
|
689985ed93 | ||
|
|
bf9262f1f1 | ||
|
|
821bd363ad | ||
|
|
4a9fc65e71 | ||
|
|
81877f5abd | ||
|
|
01eed3e045 | ||
|
|
7bc423b570 | ||
|
|
dba56f8946 | ||
|
|
c1f765d5f7 | ||
|
|
7f5b5b614f | ||
|
|
5cd149b2b9 | ||
|
|
0bc2d0aaf6 | ||
|
|
1dc9a341d1 | ||
|
|
2ad1e71176 | ||
|
|
ca6973f529 | ||
|
|
323d624ec7 | ||
|
|
49f84db473 | ||
|
|
7399c402af | ||
|
|
cbfcb29691 | ||
|
|
ae616fbaf3 | ||
|
|
35275d4aa8 | ||
|
|
dda63830ec | ||
|
|
79b4f05809 | ||
|
|
b50c47b744 | ||
|
|
f7c6fdd861 | ||
|
|
47b583821e | ||
|
|
369ac113fa | ||
|
|
fc4d8b5ad0 | ||
|
|
72e1cf7d6a | ||
|
|
dd96556e2a | ||
|
|
a7ee51526a | ||
|
|
610b59f4c6 | ||
|
|
63ce327458 | ||
|
|
6ec7a50320 | ||
|
|
60a29ef2e7 | ||
|
|
52c3a0abe0 | ||
|
|
5bbe5c2720 | ||
|
|
c0c6a21e43 | ||
|
|
a7f93698a6 | ||
|
|
1a18058228 | ||
|
|
67024daa25 | ||
|
|
de6137d1a5 | ||
|
|
11e87bd519 | ||
|
|
3b4e089756 | ||
|
|
4a6f5845f9 | ||
|
|
e9684ce033 | ||
|
|
27abf1b0de | ||
|
|
f5f3f70e35 | ||
|
|
2d282a3261 | ||
|
|
a2d0d2d8c0 | ||
|
|
ea412086d4 | ||
|
|
455679a981 | ||
|
|
a21bc6174d | ||
|
|
fae1a89402 | ||
|
|
5a1a0c59f1 | ||
|
|
c98271f783 | ||
|
|
033660c2d9 | ||
|
|
1a639c51e1 | ||
|
|
1366579dd1 | ||
|
|
f5ed2c2c40 | ||
|
|
1598de9f5f | ||
|
|
6f47f5cc99 | ||
|
|
1117dcb935 | ||
|
|
01a58f9aa8 | ||
|
|
9096644d51 | ||
|
|
d42fc0e3f6 | ||
|
|
17d1526b6f | ||
|
|
db8ac195d4 | ||
|
|
c5f5f69fd6 | ||
|
|
44203223fd | ||
|
|
bdabadcd49 | ||
|
|
220e4388d4 | ||
|
|
51b2d1ce86 | ||
|
|
f3c1825c57 | ||
|
|
359dfcd102 | ||
|
|
649cdd893f | ||
|
|
0cb974acf5 | ||
|
|
9bca5dccc7 | ||
|
|
d707f1cc5d | ||
|
|
4ed4395535 | ||
|
|
0209d48475 | ||
|
|
437bee4a85 | ||
|
|
4b8e73fa57 | ||
|
|
b96f925d9a | ||
|
|
1551dc50fd | ||
|
|
daf4cef61c | ||
|
|
496e127f64 | ||
|
|
da1af66c77 | ||
|
|
01409985a2 | ||
|
|
a86ac0619a | ||
|
|
385d165512 | ||
|
|
fcfc97b964 | ||
|
|
53d05e73a6 | ||
|
|
40a1eb861d | ||
|
|
1d3cb95b89 | ||
|
|
010303a818 | ||
|
|
7281b9fed0 | ||
|
|
c5785c7ae6 | ||
|
|
698731cb9a | ||
|
|
99bae8f2c5 | ||
|
|
5034ec1aa0 | ||
|
|
be821c1f8b | ||
|
|
a364b79a9b | ||
|
|
0aa6e1e79a | ||
|
|
d2a6379131 | ||
|
|
07ce61d19f | ||
|
|
ad9f2d8609 | ||
|
|
57702d8a43 | ||
|
|
9a30af3818 | ||
|
|
23751fbdf3 | ||
|
|
db6d181f78 | ||
|
|
56ca4dcc61 | ||
|
|
d08b537721 | ||
|
|
c1a2edb489 | ||
|
|
164b7fa454 | ||
|
|
428483cf4c | ||
|
|
89f02eb399 | ||
|
|
4ce127516c | ||
|
|
6f4f487dfe | ||
|
|
57dd60e804 | ||
|
|
d24fd2250c | ||
|
|
e00bcdfc06 | ||
|
|
67dd204ee6 | ||
|
|
e130583fa4 | ||
|
|
6ad20a0fcf | ||
|
|
9fad644d7a | ||
|
|
8cb523dd06 | ||
|
|
b50ee23a26 | ||
|
|
a973cf699b | ||
|
|
102abf49c4 | ||
|
|
8929e503ea | ||
|
|
a2a794eb74 | ||
|
|
2e95f5dd95 | ||
|
|
23a42230cb | ||
|
|
e66daed7ae | ||
|
|
f2d0f68f07 | ||
|
|
be4af0bf84 | ||
|
|
15172e8b33 | ||
|
|
c6b7918525 | ||
|
|
4f1c4421e5 | ||
|
|
7b830f5a71 | ||
|
|
6ce1135835 | ||
|
|
66eaaef6de | ||
|
|
48fbf59036 | ||
|
|
0e0f4fbe9f | ||
|
|
6ba6e10108 | ||
|
|
cf8fa4ffb4 | ||
|
|
2b0180dce2 | ||
|
|
caee848a47 | ||
|
|
d01b7cf707 | ||
|
|
8921f84c38 | ||
|
|
6830df01fa | ||
|
|
67da59ef41 | ||
|
|
036cc9915d | ||
|
|
57b6ba19f0 | ||
|
|
32b5593d07 | ||
|
|
3b757562d2 | ||
|
|
4c8c2d0868 | ||
|
|
b2f266401f | ||
|
|
5541eca23d | ||
|
|
122d65840e | ||
|
|
ed4b72d85c | ||
|
|
4033e8618f | ||
|
|
42cada1811 | ||
|
|
ff7ed213c8 | ||
|
|
a0367af06b | ||
|
|
58c2965c53 | ||
|
|
edae41409a | ||
|
|
383d3a36ef | ||
|
|
6a958da390 | ||
|
|
ef154f99d5 | ||
|
|
4a20102de3 | ||
|
|
4e170f2917 | ||
|
|
a70d788f6b |
No files matched your search
@@ -1,45 +0,0 @@
|
|||||||
name: Discover IDs
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: production
|
|
||||||
|
|
||||||
schedule:
|
|
||||||
- cron: 0 */6 * * *
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
discover:
|
|
||||||
name: Discover IDs
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
ref: production
|
|
||||||
submodules: recursive
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install
|
|
||||||
|
|
||||||
- name: Setup Git
|
|
||||||
run: |
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Discover IDs
|
|
||||||
run: bun run discover-ids
|
|
||||||
env:
|
|
||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
|
||||||
GH_TOKEN: ${{ secrets.IDS_TOKEN }}
|
|
||||||
+63
-121
@@ -2,98 +2,45 @@ name: Website
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
||||||
schedule:
|
|
||||||
- cron: "0 4 * * *"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
branch:
|
|
||||||
description: Branch
|
|
||||||
|
|
||||||
type: choice
|
# schedule:
|
||||||
required: true
|
# - cron: "0 4 * * *"
|
||||||
default: production
|
|
||||||
|
|
||||||
options:
|
|
||||||
- production
|
|
||||||
- main
|
|
||||||
|
|
||||||
environment:
|
|
||||||
description: Environment
|
|
||||||
|
|
||||||
type: choice
|
|
||||||
required: true
|
|
||||||
default: production
|
|
||||||
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- beta
|
|
||||||
- production
|
|
||||||
- preview
|
|
||||||
- opentest
|
|
||||||
|
|
||||||
client_provider:
|
|
||||||
description: Client provider
|
|
||||||
|
|
||||||
type: choice
|
|
||||||
required: true
|
|
||||||
default: steam
|
|
||||||
|
|
||||||
options:
|
|
||||||
- steam
|
|
||||||
- zip
|
|
||||||
- wgpkg
|
|
||||||
|
|
||||||
client_source:
|
|
||||||
description: Client source
|
|
||||||
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: 444200/windows
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ inputs.environment || github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# client
|
|
||||||
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
|
|
||||||
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
|
|
||||||
CLIENT_PROVIDER: ${{ inputs.client_provider || 'steam' }}
|
|
||||||
CLIENT_SOURCE: ${{ inputs.client_source || '444200/windows' }}
|
|
||||||
CLIENT_DIR: ../../temp/game
|
|
||||||
|
|
||||||
# blitzkit
|
|
||||||
PUBLIC_ENVIRONMENT: ${{ inputs.environment || 'production' }}
|
|
||||||
PUBLIC_PROMOTE_OPENTEST: false
|
|
||||||
PUBLIC_STATIC_ASSET_BASE: https://assets.blitzkit.app
|
|
||||||
PUBLIC_GUIDE_WEBSITE: https://guidesblitz.com
|
|
||||||
|
|
||||||
# blitz
|
# blitz
|
||||||
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
WOTB_SERVER: ${{ secrets.WOTB_SERVER }}
|
||||||
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
|
WOTB_VERSION: ${{ secrets.WOTB_VERSION }}
|
||||||
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
|
PUBLIC_WOTB_CLIENT: ../../temp/game
|
||||||
|
WOTB_USMAP: ../closed/blitz.usmap
|
||||||
|
|
||||||
# google
|
# google
|
||||||
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||||
|
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||||
|
|
||||||
# wargaming
|
# steam
|
||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
|
||||||
|
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
|
||||||
|
STEAM_APP: 3341250
|
||||||
|
STEAM_OS: windows
|
||||||
|
STEAM_FINGERPRINT: Manifest_UFSFiles_Win64.txt
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build website
|
name: Build chunk ${{ matrix.chunk }}
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
|
||||||
matrix:
|
matrix:
|
||||||
chunk: [default, api]
|
chunk: [pages, api, media]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Free disk space
|
- name: Free disk space
|
||||||
@@ -102,68 +49,63 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.branch || 'production' }}
|
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Setup bun
|
- name: Set up Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Setup protoc
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: latest
|
||||||
|
|
||||||
|
- name: Set up Dotnet
|
||||||
|
uses: actions/setup-dotnet@v5
|
||||||
|
with:
|
||||||
|
dotnet-version: "10.0.x"
|
||||||
|
|
||||||
|
- name: Set up Protoc
|
||||||
uses: arduino/setup-protoc@v3
|
uses: arduino/setup-protoc@v3
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Setup CLIs
|
- name: Set up project
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
bun run setup
|
||||||
sudo apt-get install -y aria2 libarchive-tools
|
./scripts/setup_depot_downloader.sh
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Download fingerprint
|
||||||
run: bun install
|
run: |
|
||||||
|
./scripts/download_fingerprint.sh
|
||||||
|
|
||||||
- name: Download Client
|
- name: Download cached game
|
||||||
run: bun run download-client
|
uses: actions/cache@v5
|
||||||
|
id: game-cache
|
||||||
- name: Build
|
|
||||||
run: bun run --bun build
|
|
||||||
env:
|
|
||||||
CHUNK: ${{ matrix.chunk }}
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
id: upload
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: chunk-${{ matrix.chunk }}
|
path: temp/game
|
||||||
path: dist/website
|
key: ${{ runner.os }}-${{ hashFiles(format('temp/game/{0}', env.FINGERPRINT)) }}
|
||||||
|
|
||||||
- name: Trigger deployment
|
|
||||||
|
|
||||||
if: ${{ github.ref == 'refs/heads/production' }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ secrets.GH_NODE_DEPLOY_TOKEN }}
|
|
||||||
CHUNK: ${{ matrix.chunk }}
|
|
||||||
|
|
||||||
|
- name: Download Steam game
|
||||||
|
if: steps.game-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
ENV_FILE="environments/${PUBLIC_ENVIRONMENT}.json"
|
./scripts/download_game.sh
|
||||||
TARGET_REPO=$(jq -r ".nodes.\"$CHUNK\"" "$ENV_FILE")
|
|
||||||
|
|
||||||
if [ "$TARGET_REPO" == "null" ] || [ -z "$TARGET_REPO" ]; then
|
# - name: Build
|
||||||
exit 1
|
# run: bun run build-website
|
||||||
fi
|
# env:
|
||||||
|
# BUILD_CHUNK: ${{ matrix.chunk }}
|
||||||
|
|
||||||
curl \
|
# - name: Upload artifact
|
||||||
-X POST \
|
# id: upload
|
||||||
-H "Accept: application/vnd.github+json" \
|
# uses: actions/upload-pages-artifact@v4
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
# with:
|
||||||
"https://api.github.com/repos/$TARGET_REPO/dispatches" \
|
# path: dist/website
|
||||||
-d "{
|
|
||||||
\"event_type\": \"deploy-pages\",
|
# - name: Trigger target repository
|
||||||
\"client_payload\": {
|
# run: |
|
||||||
\"source_repo\": \"${{ github.repository }}\",
|
# curl -X POST \
|
||||||
\"run_id\": \"${{ github.run_id }}\",
|
# -H "Authorization: token ${{ secrets.GH_TOKEN }}" \
|
||||||
\"artifact_id\": \"${{ steps.upload.outputs.artifact-id }}\",
|
# -H "Accept: application/vnd.github.v3+json" \
|
||||||
\"chunk\": \"$CHUNK\"
|
# https://github.com \
|
||||||
}
|
# -d '{ "event_type": "deploy-pages", "client_payload": { "artifact_url": "${{ steps.upload.outputs.artifact_url }}" } }'
|
||||||
}"
|
|
||||||
+4
-2
@@ -1,7 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
_node_modules
|
_node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.env
|
*.env
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
@@ -22,11 +22,12 @@ depots
|
|||||||
public/sitemap*
|
public/sitemap*
|
||||||
/firebase.json
|
/firebase.json
|
||||||
target
|
target
|
||||||
# packages/core/src/protos/*.ts
|
packages/website-ue/src/protos/*.ts
|
||||||
/.vercel
|
/.vercel
|
||||||
.astro
|
.astro
|
||||||
.vercel
|
.vercel
|
||||||
/src
|
/src
|
||||||
|
/packages/bot/src/prisma
|
||||||
dev-dist
|
dev-dist
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
@@ -52,3 +53,4 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
*.lscache
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
[submodule "packages/closed"]
|
[submodule "packages/closed"]
|
||||||
path = packages/closed
|
path = packages/closed
|
||||||
url = https://github.com/blitzkit/closed.git
|
url = https://github.com/blitzkit/closed.git
|
||||||
|
[submodule "packages/cue4parse"]
|
||||||
|
path = packages/cue4parse
|
||||||
|
url = https://github.com/tresabhi/CUE4Parse.git
|
||||||
Vendored
+15
-3
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
// "js/ts.experimental.useTsgo": true,
|
||||||
|
"protobuf.includes": ["${workspaceFolder}/path/to/protos"],
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"AABBOX",
|
"AABBOX",
|
||||||
"Abhi",
|
"Abhi",
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
"Callouts",
|
"Callouts",
|
||||||
"Camo",
|
"Camo",
|
||||||
"Carro",
|
"Carro",
|
||||||
|
"catalogitemaccessor",
|
||||||
"CCPAOPTOUT",
|
"CCPAOPTOUT",
|
||||||
"CDON",
|
"CDON",
|
||||||
"clanid",
|
"clanid",
|
||||||
@@ -62,15 +65,16 @@
|
|||||||
"Ddpf",
|
"Ddpf",
|
||||||
"Ddsd",
|
"Ddsd",
|
||||||
"deadrail",
|
"deadrail",
|
||||||
|
"DEBUFF",
|
||||||
"debugflags",
|
"debugflags",
|
||||||
"deburr",
|
"deburr",
|
||||||
|
"deburred",
|
||||||
"decalmask",
|
"decalmask",
|
||||||
"decompressor",
|
"decompressor",
|
||||||
"décor",
|
"décor",
|
||||||
"dedup",
|
"dedup",
|
||||||
"defence",
|
"defence",
|
||||||
"degressive",
|
"degressive",
|
||||||
"depotdownloader",
|
|
||||||
"devkrieg",
|
"devkrieg",
|
||||||
"diacriticless",
|
"diacriticless",
|
||||||
"DIDS",
|
"DIDS",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Everytime",
|
"Everytime",
|
||||||
"FASTNAME",
|
"FASTNAME",
|
||||||
"favicons",
|
"favicons",
|
||||||
|
"filelist",
|
||||||
"flexdirection",
|
"flexdirection",
|
||||||
"fontsource",
|
"fontsource",
|
||||||
"FOURCC",
|
"FOURCC",
|
||||||
@@ -147,6 +152,7 @@
|
|||||||
"noout",
|
"noout",
|
||||||
"normalmap",
|
"normalmap",
|
||||||
"oneof",
|
"oneof",
|
||||||
|
"Oodle",
|
||||||
"oopsies",
|
"oopsies",
|
||||||
"Oopsy",
|
"Oopsy",
|
||||||
"opentest",
|
"opentest",
|
||||||
@@ -162,6 +168,7 @@
|
|||||||
"playerachievements",
|
"playerachievements",
|
||||||
"playerinfo",
|
"playerinfo",
|
||||||
"playform",
|
"playform",
|
||||||
|
"Playtest",
|
||||||
"pngjs",
|
"pngjs",
|
||||||
"POSITIVEX",
|
"POSITIVEX",
|
||||||
"POSITIVEY",
|
"POSITIVEY",
|
||||||
@@ -178,6 +185,7 @@
|
|||||||
"ratingleaderboard",
|
"ratingleaderboard",
|
||||||
"raycaster",
|
"raycaster",
|
||||||
"Redirector",
|
"Redirector",
|
||||||
|
"REGEN",
|
||||||
"Relogin",
|
"Relogin",
|
||||||
"researchable",
|
"researchable",
|
||||||
"Resvg",
|
"Resvg",
|
||||||
@@ -191,12 +199,14 @@
|
|||||||
"searchplayers",
|
"searchplayers",
|
||||||
"searchtanks",
|
"searchtanks",
|
||||||
"SHAREDEXP",
|
"SHAREDEXP",
|
||||||
|
"Skia",
|
||||||
"SKLLD",
|
"SKLLD",
|
||||||
"sluggify",
|
"sluggify",
|
||||||
"smoothstep",
|
"smoothstep",
|
||||||
"Smri",
|
"Smri",
|
||||||
"Specularity",
|
"Specularity",
|
||||||
"SSAO",
|
"SSAO",
|
||||||
|
"stackable",
|
||||||
"startcase",
|
"startcase",
|
||||||
"statkit",
|
"statkit",
|
||||||
"statsfull",
|
"statsfull",
|
||||||
@@ -220,6 +230,7 @@
|
|||||||
"tungstentip",
|
"tungstentip",
|
||||||
"Turretless",
|
"Turretless",
|
||||||
"TYPELESS",
|
"TYPELESS",
|
||||||
|
"uasset",
|
||||||
"ubjson",
|
"ubjson",
|
||||||
"Unicum",
|
"Unicum",
|
||||||
"UNLOCALIZED",
|
"UNLOCALIZED",
|
||||||
@@ -229,6 +240,7 @@
|
|||||||
"unrotate",
|
"unrotate",
|
||||||
"unwrapper",
|
"unwrapper",
|
||||||
"userns",
|
"userns",
|
||||||
|
"usmap",
|
||||||
"vercel",
|
"vercel",
|
||||||
"vickers",
|
"vickers",
|
||||||
"vitepress",
|
"vitepress",
|
||||||
@@ -241,6 +253,7 @@
|
|||||||
"winrate",
|
"winrate",
|
||||||
"wireframe",
|
"wireframe",
|
||||||
"Woah",
|
"Woah",
|
||||||
|
"wooorm",
|
||||||
"WOTB",
|
"WOTB",
|
||||||
"wotblitz",
|
"wotblitz",
|
||||||
"wotbreplay",
|
"wotbreplay",
|
||||||
@@ -253,6 +266,5 @@
|
|||||||
"youtuber",
|
"youtuber",
|
||||||
"youtubers",
|
"youtubers",
|
||||||
"zoomable"
|
"zoomable"
|
||||||
],
|
]
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,15 @@ If you intend to contribute to the code, note that you may need to check out sen
|
|||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
| Package | Title | Source | Memo |
|
| Package | Title | Source | Memo |
|
||||||
| ------------------------------------ | -------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
|
| [packages/app](packages/app) | BlitzKit | [Play Store](https://play.google.com/store/apps/details?id=com.tresabhi.blitzkit), [App Store](https://apps.apple.com/app/4321), [Microsoft Store](https://apps.microsoft.com/detail/zyxw) | The native app of BlitzKit for all platforms. Unavailable from stores at this time. |
|
||||||
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
|
| [packages/bot](packages/bot) | BlitzKit Discord Bot | [Discord Application Directory](https://discord.com/discovery/applications/1097673957865443370) | The one-in-all Discord bot for statistics. |
|
||||||
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
|
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
|
||||||
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
|
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
|
||||||
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. |
|
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
|
||||||
|
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
|
||||||
|
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. |
|
||||||
|
|
||||||
## Submodules
|
## Submodules
|
||||||
|
|
||||||
@@ -24,18 +26,22 @@ If you intend to contribute to the code, note that you may need to check out sen
|
|||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Name | Source | BlitzKit Default | Memo |
|
| Name | Source | BlitzKit Default | Memo |
|
||||||
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
|
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
|
||||||
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
|
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
|
||||||
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
|
| `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. |
|
||||||
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
|
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
|
||||||
| `PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
|
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
|
||||||
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
|
| `PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
|
||||||
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
|
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
|
||||||
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
|
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
|
||||||
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
|
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
|
||||||
| `SERVICING` | | `false` | Wether the website is down for servicing. |
|
| `PUBLIC_BRANCH` | | `main` | The branch that the website is operating on. This is completely nominal. |
|
||||||
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
|
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
|
||||||
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
|
| `SERVICING` | | `false` | Wether the website is down for servicing. |
|
||||||
| `WOTB_DLC_CDN` | | | |
|
| `DISCORD_CLIENT_ID` | [Discord Developer Portal](https://discord.com/developers/applications) | `1097673957865443370` | The BlitzKit Discord Bot's user ID. |
|
||||||
|
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
|
||||||
|
| `DISCORD_TOKEN` | [Discord Developer Portal](https://discord.com/developers/applications) | | The Discord API Key for the BlitzKit Discord Bot. |
|
||||||
|
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
|
||||||
|
| `WOTB_DLC_CDN` | | | |
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": ".",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "C:/Program Files/WindowsApps/7458BE2C.WorldofTanksBlitz_11.1.256.0_x64__x4tje2y229k00/Data",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"words": [
|
|
||||||
"gamemodes",
|
|
||||||
"Tankopedia"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# BlitzKit 1.30.3
|
|
||||||
|
|
||||||
Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions this update.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- [GonnaHetzMe] Added tooltips to the module selector in Tankopedia
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Added aggressive re-rendering for the Guess mini-game's score readout to make inspect-element harder
|
|
||||||
- Removed tank visual and collision model caching
|
|
||||||
- Guesser once again breaks streaks if not all tiers are selected
|
|
||||||
- BlitzKit now properly unloads tank models from memory when visually hidden in Guesser
|
|
||||||
- [GonnaHetzMe] Tanks with complex transparency like the Herdbreaker once again handle opacity correctly
|
|
||||||
- [GonnaHetzMe] Pieces of tank models that have transparency now handle culling properly (they're visible from both sides)
|
|
||||||
- [GonnaHetzMe] Tanks with "cardboard" textures have been fixed, thanks to better PVR file parsing
|
|
||||||
- [GonnaHetzMe] Tank icons are now scaled correctly so small tanks are now properly zoomed out in contrast to bigger ones
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# BlitzKit 1.30.4
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- BlitzKit now correctly implements minor WoTB updates
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# BlitzKit 1.30.5
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Reduced the amount of code that executes for every rendered frame, hopefully improving performance
|
|
||||||
- Reduced the number of lights by 20% to decrease the amount of shadow calculations
|
|
||||||
- Decreased the ground size by 80% for an equal reduction in shadow texture sizes
|
|
||||||
- Added the ability for BlitzKit to dismount the components that handled the intro lighting animation when not animating, reducing checks running every frame
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# BlitzKit 1.30.6
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Tanks no longer "randomly" appear dark in Tankopedia and Mixer
|
|
||||||
- The ground is now as dark as other modes when Static Armor is selected in Tankopedia
|
|
||||||
- Fixed some naming issues with the "equal" icon, improving performance a tiny tiny bit
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# BlitzKit 1.30.7
|
|
||||||
|
|
||||||
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Dragging items in the Tier List now automatically scrolls the screen if you reach the top/bottom edge of the screen
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
|
|
||||||
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
|
|
||||||
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
|
|
||||||
- The spot light angle has been increased to more evenly light up larger tanks
|
|
||||||
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
|
|
||||||
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
|
|
||||||
- Increased the floor size and light decay to create the illusion that the floor is infinite
|
|
||||||
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
|
|
||||||
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
|
|
||||||
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# BlitzKit 1.30.8
|
|
||||||
|
|
||||||
Support for tanks with Chinese names.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- The URL generator for BlitzKit now handles names that have only Chinese characters
|
|
||||||
- The Mixed Virtual File System now handles non-existent directories correctly
|
|
||||||
- Updated equipments to match the game again
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# BlitzKit 1.31.0
|
|
||||||
|
|
||||||
This update massively upgrades BlitzKit's graphical fidelity (while making it faster) and updates the way BlitzKit deploys updates, speeding up future versions.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Improved the performance of the Tank Sandbox in Tankopedia
|
|
||||||
- Reduced the number of lights by 25%
|
|
||||||
- Reduced the resolution of shadows by 50%
|
|
||||||
- Improved the graphical fidelity of tanks in the Tankopedia
|
|
||||||
- Added soft contact shadows below tanks
|
|
||||||
- Added sky and ground reflected lighting to the top and bottom of tanks
|
|
||||||
- Removed the fog that used to darken tanks when zoomed far away
|
|
||||||
- Made BlitzKitLand™ fade out circularly to hide the sharp edges
|
|
||||||
- Pointed the lights 1.5m above the ground
|
|
||||||
- This genuinely makes BlitzKit almost photo-realistic in my opinion
|
|
||||||
- Non-PBR tanks are now lit up brighter to accommodate for their dark textures
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Fixed a bug where the ambient occlusion map (a measure of how dark a part of a tank gets due to low light bounces) was applied to the color of the tank directly, making many tanks appear dark even in direct light
|
|
||||||
- Tanks with chinese characters now generate urls through transliteration
|
|
||||||
|
|
||||||
## Known Issues
|
|
||||||
|
|
||||||
- The flags found behind the selected tank in the tech tree section of the Tankopedia is offset incorrectly
|
|
||||||
|
|
||||||
## Technical Features
|
|
||||||
|
|
||||||
- BlitzKit can now calculate and store performance statistics and account details for up 10,000,000,000 players
|
|
||||||
- This will help make the BlitzKit Performance tool more accurate, and play well with an upcoming, unannounced major feature
|
|
||||||
|
|
||||||
## Technical Changes
|
|
||||||
|
|
||||||
- The `production` branch now deploys updates to `blitzkit.app` instead of the `main` branch
|
|
||||||
- `robots.txt` now disables indexing of BlitzKit variants based on the environment
|
|
||||||
- The website is now compiled in "chunks" of which the `api` chunk now takes on the task of mangling what was previously stored in `assets3`
|
|
||||||
- `assets3` will be removed in `1.31.1`, please migrate to the new `api` if you use any BlitzKit assets
|
|
||||||
- Removed the `bot` and `app` packages
|
|
||||||
- Removed Prisma
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# BlitzKit 1.31.1
|
|
||||||
|
|
||||||
This update adds capabilities to release BlitzKit OpenTest and Preview.
|
|
||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
🎉 Services for everything and anything World of Tanks Blitz
|
🎉 Services for everything and anything World of Tanks Blitz
|
||||||
|
|
||||||
|
## Bot
|
||||||
|
|
||||||
|
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
|
||||||
|
|
||||||
|
- Track statistics
|
||||||
|
- Find inactive players in a clan
|
||||||
|
- Lookup owned tanks
|
||||||
|
- Create embeds for streams
|
||||||
|
- Track a player/tanks statistical evolution
|
||||||
|
- ...and a variety of other similar things
|
||||||
|
|
||||||
## Website
|
## Website
|
||||||
|
|
||||||
Provides a bunch of very helpful tools; https://blitzkit.app/
|
Provides a bunch of very helpful tools; https://blitzkit.app/
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Inviting the Bot
|
||||||
|
|
||||||
|
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"robots": false,
|
|
||||||
"canonical": "https://blitzkit.app",
|
|
||||||
|
|
||||||
"paths": {
|
|
||||||
"default": null,
|
|
||||||
"api": null
|
|
||||||
},
|
|
||||||
"nodes": {
|
|
||||||
"default": "blitzkit/deploy-node-beta.blitzkit.app",
|
|
||||||
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"api": "https://api.beta.blitzkit.app",
|
|
||||||
"static": "https://static.beta.blitzkit.app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"robots": false,
|
|
||||||
"canonical": "https://blitzkit.app",
|
|
||||||
|
|
||||||
"paths": {
|
|
||||||
"default": null,
|
|
||||||
"api": "api"
|
|
||||||
},
|
|
||||||
"nodes": {},
|
|
||||||
"aliases": {
|
|
||||||
"api": "/api",
|
|
||||||
"static": "https://static.blitzkit.app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"robots": false,
|
|
||||||
"canonical": "https://blitzkit.app",
|
|
||||||
|
|
||||||
"paths": {
|
|
||||||
"default": null,
|
|
||||||
"api": null
|
|
||||||
},
|
|
||||||
"nodes": {
|
|
||||||
"default": "blitzkit/deploy-node-opentest.blitzkit.app",
|
|
||||||
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"api": "https://api.opentest.blitzkit.app",
|
|
||||||
"static": "https://static.opentest.blitzkit.app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"robots": false,
|
|
||||||
"canonical": "https://blitzkit.app",
|
|
||||||
|
|
||||||
"paths": {
|
|
||||||
"default": null,
|
|
||||||
"api": null
|
|
||||||
},
|
|
||||||
"nodes": {
|
|
||||||
"default": "blitzkit/deploy-node-preview.blitzkit.app",
|
|
||||||
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"api": "https://api.preview.blitzkit.app",
|
|
||||||
"static": "https://static.preview.blitzkit.app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"robots": true,
|
|
||||||
"canonical": "https://blitzkit.app",
|
|
||||||
|
|
||||||
"paths": {
|
|
||||||
"default": null,
|
|
||||||
"api": null
|
|
||||||
},
|
|
||||||
"nodes": {
|
|
||||||
"default": "blitzkit/deploy-node-blitzkit.app",
|
|
||||||
"api": "blitzkit/deploy-node-api.blitzkit.app"
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"api": "https://api.blitzkit.app",
|
|
||||||
"static": "https://static.blitzkit.app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-10
@@ -1,24 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.31.1",
|
"version": "2.0.0-beta.3",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd packages/website && bun run dev",
|
"setup": "chmod a+x scripts/*.sh && ./scripts/setup.sh",
|
||||||
"build": "cd packages/website && bun run build",
|
"dev": "./scripts/dev.sh",
|
||||||
"discover-ids": "cd packages/scripts && bun run discover-ids",
|
"build-game": "./scripts/build_game.sh",
|
||||||
"build-protos": "cd packages/scripts && bun run build-protos",
|
"build-website": "./scripts/build_website.sh"
|
||||||
"record-performance": "cd packages/scripts && bun run record-performance",
|
|
||||||
"download-client": "cd packages/scripts && bun run download-client"
|
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gltf-transform/extensions": "^4.4.2",
|
|
||||||
"lz4js": "^0.2.0",
|
|
||||||
"sharp": "^0.34.5",
|
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@blitzkit/app",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
|
"vite": "^6.0.3",
|
||||||
|
"typescript": "~5.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
Generated
+5286
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "blitzkit"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "blitzkit_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"opener:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
|
#[tauri::command]
|
||||||
|
fn greet(name: &str) -> String {
|
||||||
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
let app_handle = app.handle();
|
||||||
|
|
||||||
|
let entry = app_handle
|
||||||
|
.path_resolver()
|
||||||
|
.resolve_resource("../../dist/website/server/entry.mjs")
|
||||||
|
.expect("failed to resolve server.ts");
|
||||||
|
|
||||||
|
Command::new("bun")
|
||||||
|
.arg("run")
|
||||||
|
.arg(entry)
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.expect("failed to start Bun server");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running Tauri app");
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "blitzkit",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.tresabhi.blitzkit",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "cd ../website && bun run dev",
|
||||||
|
"_beforeBuildCommand": "cd ../website && bun run build",
|
||||||
|
"frontendDist": "../../../dist/website/client",
|
||||||
|
"devUrl": "http://localhost:4321"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"withGlobalTauri": true,
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:4321",
|
||||||
|
"title": "BlitzKit",
|
||||||
|
"fullscreen": false,
|
||||||
|
"resizable": true,
|
||||||
|
"maximized": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/[email protected]",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../../tsconfig.json"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@blitzkit/bot",
|
||||||
|
"dependencies": {
|
||||||
|
"@blitzkit/core": "workspace:*",
|
||||||
|
"@blitzkit/i18n": "workspace:*",
|
||||||
|
"@radix-ui/colors": "^3.0.0",
|
||||||
|
"@radix-ui/themes": "^3.3.0",
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
|
"@types/adm-zip": "^0.5.7",
|
||||||
|
"@types/bun": "^1.3.9",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/markdown-escape": "^1.1.3",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
|
"bepaint": "^0.1.0-beta.3",
|
||||||
|
"discord.js": "^14.25.1",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"fuzzysort": "^3.1.0",
|
||||||
|
"lodash-es": "^4.17.23",
|
||||||
|
"markdown-escape": "^2.0.0",
|
||||||
|
"pickleparser": "^0.2.1",
|
||||||
|
"prisma": "^6.19.2",
|
||||||
|
"protobufjs": "^7.5.4",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"satori": "^0.18.4",
|
||||||
|
"sharp": "^0.34.5"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --watch src/main.ts",
|
||||||
|
"start": "bun src/main.ts"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"types": "src/types.d.ts"
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import {
|
||||||
|
commandsArray,
|
||||||
|
type CommandRegistry,
|
||||||
|
} from "../events/interactionCreate";
|
||||||
|
|
||||||
|
const subcommands = [
|
||||||
|
"embeds",
|
||||||
|
"introduction",
|
||||||
|
"invite",
|
||||||
|
"timezones",
|
||||||
|
"credits",
|
||||||
|
] as const;
|
||||||
|
type Subcommand = (typeof subcommands)[number];
|
||||||
|
|
||||||
|
const DOCS: Record<Subcommand, string> = {
|
||||||
|
embeds: "guide/embeds",
|
||||||
|
introduction: "guide/introduction",
|
||||||
|
invite: "guide/invite",
|
||||||
|
timezones: "guide/timezones",
|
||||||
|
credits: "about/credits",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("about", [
|
||||||
|
{ subcommand: "commands" },
|
||||||
|
...subcommands.map((subcommand) => ({ subcommand })),
|
||||||
|
]),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
|
||||||
|
if (subcommand === "commands") {
|
||||||
|
return `${strings.bot.commands.about.subcommands.commands.body}${(
|
||||||
|
await Promise.all(commandsArray)
|
||||||
|
)
|
||||||
|
.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 = `https://raw.githubusercontent.com/tresabhi/blitzkit/main/docs/${DOCS[subcommand as Subcommand]}.md`;
|
||||||
|
const content = fetch(url).then((response) => response.text());
|
||||||
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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 { createLocalizedCommand } from "../../core/discord/createLocalizedCommand";
|
||||||
|
import { getFiltersFromButton } from "../../core/discord/getFiltersFromButton";
|
||||||
|
import { resolvePeriodFromButton } from "../../core/discord/resolvePeriodFromButton";
|
||||||
|
import { resolvePlayerFromButton } from "../../core/discord/resolvePlayerFromButton";
|
||||||
|
import { translator } from "../../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../../events/interactionCreate";
|
||||||
|
import { renderBreakdown } from "./render";
|
||||||
|
|
||||||
|
export const breakdownCommand = new Promise<CommandRegistry>(
|
||||||
|
async (resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand("breakdown"),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
return strings.bot.commands.dead;
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import {
|
||||||
|
type BlitzStats,
|
||||||
|
calculateWN8,
|
||||||
|
getAccountInfo,
|
||||||
|
getClanAccountInfo,
|
||||||
|
getTankStats,
|
||||||
|
getWN8Percentile,
|
||||||
|
TankDefinition,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import { chunk } from "lodash-es";
|
||||||
|
import * as Breakdown from "../../components/Breakdown";
|
||||||
|
import { CommandWrapper } from "../../components/CommandWrapper";
|
||||||
|
import { NoData } from "../../components/NoData";
|
||||||
|
import { TitleBar } from "../../components/TitleBar";
|
||||||
|
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
|
||||||
|
import {
|
||||||
|
averageDefinitions,
|
||||||
|
tankDefinitions,
|
||||||
|
} from "../../core/blitzkit/nonBlockingPromises";
|
||||||
|
import { UserError } from "../../core/blitzkit/userError";
|
||||||
|
import {
|
||||||
|
filterStats,
|
||||||
|
type StatFilters,
|
||||||
|
} from "../../core/blitzstars/filterStats";
|
||||||
|
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
|
||||||
|
import type { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
|
||||||
|
import type { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../../core/localization/translator";
|
||||||
|
|
||||||
|
const ROWS_PER_PAGE = 8;
|
||||||
|
const MAX_PAGES = 9;
|
||||||
|
|
||||||
|
export async function renderBreakdown(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
locale: Locale,
|
||||||
|
) {
|
||||||
|
const { strings, unwrap } = translator(locale);
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const awaitedTankAverages = await averageDefinitions;
|
||||||
|
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
|
||||||
|
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clanData = await getClanAccountInfo(region, id, ["clan"]);
|
||||||
|
const tankStats = await getTankStats(region, id);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
|
const orderedCurrentStats: BlitzStats[] = [];
|
||||||
|
const orderedCareerStats: BlitzStats[] = [];
|
||||||
|
const orderedCurrentWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8Full: (number | undefined)[] = [];
|
||||||
|
|
||||||
|
if (tankStats === null) {
|
||||||
|
throw new UserError(strings.bot.common.errors.no_tank_stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredOrder.forEach((id) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages.averages[id];
|
||||||
|
const current = statsInPeriod.diff[id];
|
||||||
|
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
|
||||||
|
const currentWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.mu, current)
|
||||||
|
: undefined;
|
||||||
|
const careerWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.mu, career)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
orderedCurrentStats.push(current);
|
||||||
|
orderedCareerStats.push(career);
|
||||||
|
orderedCurrentWN8.push(currentWN8);
|
||||||
|
orderedCareerWN8.push(careerWN8);
|
||||||
|
});
|
||||||
|
tankStats.forEach((tank) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages.averages[tank.tank_id];
|
||||||
|
|
||||||
|
orderedCareerWN8Full.push(
|
||||||
|
tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.mu, tank.all)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentBattles = orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const careerBattles = tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const currentWinrate =
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.wins,
|
||||||
|
0,
|
||||||
|
) / currentBattles;
|
||||||
|
const careerWinrate =
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.wins,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
const currentWN8 =
|
||||||
|
orderedCurrentWN8.reduce<number>(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
|
||||||
|
0,
|
||||||
|
) /
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator +
|
||||||
|
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const careerWN8 =
|
||||||
|
orderedCareerWN8Full.reduce<number>(
|
||||||
|
(accumulator, current) => accumulator + (current ?? 0),
|
||||||
|
0,
|
||||||
|
) /
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator +
|
||||||
|
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const currentDamage =
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / currentBattles;
|
||||||
|
const careerDamage =
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
|
||||||
|
const children: React.JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (filteredOrder.length > 0) {
|
||||||
|
children.push(
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
displayType="summary"
|
||||||
|
title={strings.bot.commands.breakdown.body.total}
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.battles,
|
||||||
|
current: currentBattles,
|
||||||
|
career: careerBattles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.winrate,
|
||||||
|
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
||||||
|
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||||
|
delta: currentWinrate - careerWinrate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.wn8,
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.damage,
|
||||||
|
current: Math.round(currentDamage).toLocaleString(),
|
||||||
|
career: Math.round(careerDamage).toLocaleString(),
|
||||||
|
delta: currentDamage - careerDamage,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
filteredOrder.forEach((id, index) => {
|
||||||
|
const tankDefinition = awaitedTankDefinitions.tanks[id] as
|
||||||
|
| TankDefinition
|
||||||
|
| undefined;
|
||||||
|
const current = orderedCurrentStats[index];
|
||||||
|
const career = orderedCareerStats[index];
|
||||||
|
const currentWN8 = orderedCurrentWN8[index];
|
||||||
|
const careerWN8 = orderedCareerWN8[index];
|
||||||
|
|
||||||
|
children.push(
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
displayType="tank"
|
||||||
|
class={tankDefinition?.class}
|
||||||
|
type={tankDefinition?.type}
|
||||||
|
title={
|
||||||
|
tankDefinition
|
||||||
|
? unwrap(tankDefinition.name)
|
||||||
|
: literals(strings.bot.commands.breakdown.body.unknown_tank, {
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.battles,
|
||||||
|
current: current.battles.toLocaleString(),
|
||||||
|
career: career.battles.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.winrate,
|
||||||
|
current: `${(100 * (current.wins / current.battles)).toFixed(
|
||||||
|
2,
|
||||||
|
)}%`,
|
||||||
|
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
|
||||||
|
delta:
|
||||||
|
current.wins / current.battles - career.wins / career.battles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.wn8,
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: strings.bot.commands.breakdown.body.damage,
|
||||||
|
current: Math.round(
|
||||||
|
current.damage_dealt / current.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
career: Math.round(
|
||||||
|
career.damage_dealt / career.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
delta:
|
||||||
|
current.damage_dealt / current.battles -
|
||||||
|
career.damage_dealt / career.battles,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = chunk(
|
||||||
|
children,
|
||||||
|
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredOrder.length > 0) {
|
||||||
|
return pages
|
||||||
|
.map((page, index) => (
|
||||||
|
<CommandWrapper>
|
||||||
|
{index === 0 && (
|
||||||
|
<TitleBar
|
||||||
|
title={accountInfo.nickname}
|
||||||
|
image={
|
||||||
|
clanData?.clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Breakdown.Root>{page}</Breakdown.Root>
|
||||||
|
</CommandWrapper>
|
||||||
|
))
|
||||||
|
.reverse();
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={accountInfo.nickname}
|
||||||
|
image={
|
||||||
|
clanData?.clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NoData type="battles_in_period" locale={locale} />
|
||||||
|
</CommandWrapper>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import packageJSON from "../../../../package.json";
|
||||||
|
import { client } from "../core/discord/client";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
const executionStart = new Date().getTime();
|
||||||
|
|
||||||
|
export const debugCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("debug"),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const uptime = currentTime - executionStart;
|
||||||
|
const list = [
|
||||||
|
[strings.bot.commands.debug.body.version, packageJSON.version],
|
||||||
|
[
|
||||||
|
strings.bot.commands.debug.body.shard.label,
|
||||||
|
client.shard?.ids[0] ?? strings.bot.commands.debug.body.shard.default,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
strings.bot.commands.debug.body.uptime,
|
||||||
|
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
|
||||||
|
(uptime / 1000 / 60) % 60
|
||||||
|
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
|
||||||
|
uptime % 1000
|
||||||
|
)}ms`,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
return `${strings.bot.commands.debug.body.title}${list
|
||||||
|
.map(([key, value]) => `- ${key}: ${value}`)
|
||||||
|
.join("\n")}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
|
||||||
|
import { Locale, SlashCommandSubcommandGroupBuilder } from "discord.js";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import * as Graph from "../components/Graph";
|
||||||
|
import { LineColor } from "../components/Graph/components/Line/constants";
|
||||||
|
import { NoData } from "../components/NoData";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { getPlayerHistories } from "../core/blitzstars/getPlayerHistories";
|
||||||
|
import { getTankHistories } from "../core/blitzstars/getTankHistories";
|
||||||
|
import { addPeriodSubCommands } from "../core/discord/addPeriodSubCommands";
|
||||||
|
import { addTankChoices } from "../core/discord/addTankChoices";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
|
||||||
|
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
|
||||||
|
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
|
||||||
|
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
|
||||||
|
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
type EvolutionStatType = "player" | "tank";
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
type: "player" | "tank",
|
||||||
|
tankId: number | null,
|
||||||
|
locale: Locale,
|
||||||
|
) {
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
|
||||||
|
const logo = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
const histories = await (type === "player"
|
||||||
|
? getPlayerHistories(region, id, {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
})
|
||||||
|
: getTankHistories(region, id, locale, {
|
||||||
|
tankId: tankId!,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
}));
|
||||||
|
const plot = histories.map(
|
||||||
|
(history) =>
|
||||||
|
[
|
||||||
|
history.all.battles,
|
||||||
|
(history.all.wins / history.all.battles) * 100,
|
||||||
|
] as Graph.PlotItem,
|
||||||
|
);
|
||||||
|
const times = histories.map((history) => history.last_battle_time);
|
||||||
|
const minTime = Math.min(...times);
|
||||||
|
const maxTime = Math.max(...times);
|
||||||
|
const ys = plot.map(([, y]) => y);
|
||||||
|
const xs = plot.map(([x]) => x);
|
||||||
|
const maxY = Math.max(...ys);
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const maxX = Math.max(...xs);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar title={accountInfo.nickname} image={logo} description={name} />
|
||||||
|
|
||||||
|
{plot.length > 0 && (
|
||||||
|
<Graph.Root
|
||||||
|
verticalMargin={{
|
||||||
|
min: minY,
|
||||||
|
max: maxY,
|
||||||
|
suffix: "%",
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
horizontalMargin={{
|
||||||
|
min: minX,
|
||||||
|
max: maxX,
|
||||||
|
precision: 0,
|
||||||
|
}}
|
||||||
|
xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
|
||||||
|
xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
|
||||||
|
>
|
||||||
|
{Graph.Line({
|
||||||
|
color: LineColor.Red,
|
||||||
|
plot: plot,
|
||||||
|
minY,
|
||||||
|
maxY,
|
||||||
|
})}
|
||||||
|
</Graph.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plot.length === 0 && <NoData type="battles_in_period" locale={locale} />}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const evolutionCommand = new Promise<CommandRegistry>(
|
||||||
|
async (resolve) => {
|
||||||
|
resolve({
|
||||||
|
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) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
return strings.bot.commands.dead;
|
||||||
|
|
||||||
|
// const commandGroup =
|
||||||
|
// interaction.options.getSubcommandGroup() as EvolutionStatType;
|
||||||
|
// const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
// const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
|
// const tankIdRaw = interaction.options.getString("tank")!;
|
||||||
|
// const tankId =
|
||||||
|
// commandGroup === "tank"
|
||||||
|
// ? await resolveTankId(tankIdRaw, interaction.locale)
|
||||||
|
// : null;
|
||||||
|
// const path = commandToURL(interaction, {
|
||||||
|
// ...player,
|
||||||
|
// ...getCustomPeriodParams(interaction),
|
||||||
|
// tankId,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return [
|
||||||
|
// await render(
|
||||||
|
// player,
|
||||||
|
// period,
|
||||||
|
// commandGroup,
|
||||||
|
// tankId,
|
||||||
|
// interaction.locale
|
||||||
|
// ),
|
||||||
|
// buttonRefresh(interaction, path),
|
||||||
|
// await getBlitzStarsLinkButton(
|
||||||
|
// player.region,
|
||||||
|
// player.id,
|
||||||
|
// tankId ?? undefined
|
||||||
|
// ),
|
||||||
|
// ];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const url = new URL(`https://example.com/${interaction.customId}`);
|
||||||
|
const path = url.pathname.split("/");
|
||||||
|
const commandGroup = path[2] as EvolutionStatType;
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
|
||||||
|
return await render(
|
||||||
|
player,
|
||||||
|
period,
|
||||||
|
commandGroup,
|
||||||
|
parseInt(url.searchParams.get("tankId") ?? "0") || null,
|
||||||
|
interaction.locale,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import {
|
||||||
|
emblemURL,
|
||||||
|
getAccountInfo,
|
||||||
|
getClanAccountInfo,
|
||||||
|
getTierWeights,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import { GenericAllStats } from "../components/GenericAllStats";
|
||||||
|
import { NoData } from "../components/NoData";
|
||||||
|
import { TierWeights } from "../components/TierWeights";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
|
||||||
|
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
|
||||||
|
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
|
||||||
|
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 { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
|
||||||
|
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
|
||||||
|
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
|
||||||
|
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
|
||||||
|
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
locale: Locale,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
|
||||||
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
const diffedTankStats = await getStatsInPeriod(
|
||||||
|
region,
|
||||||
|
id,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
locale,
|
||||||
|
);
|
||||||
|
const { stats, supplementary, filteredOrder } = await filterStats(
|
||||||
|
diffedTankStats,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
|
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{stats.battles === 0 && (
|
||||||
|
<NoData type="battles_in_period" locale={locale} />
|
||||||
|
)}
|
||||||
|
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<GenericAllStats
|
||||||
|
stats={stats}
|
||||||
|
supplementaryStats={supplementary}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fullStatsCommand = new Promise<CommandRegistry>(
|
||||||
|
async (resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand("full-stats"),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
return strings.bot.commands.dead;
|
||||||
|
|
||||||
|
// 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 Promise.all([
|
||||||
|
// render(player, period, filters, interaction.locale),
|
||||||
|
// buttonRefresh(interaction, path),
|
||||||
|
// 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 render(player, period, filters, interaction.locale);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { getAccountInfo, getClanInfo } from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import { GenericStats } from "../components/GenericStats";
|
||||||
|
import { NoData } from "../components/NoData";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { addClanChoices } from "../core/discord/addClanChoices";
|
||||||
|
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 { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
const DEFAULT_THRESHOLD = 7;
|
||||||
|
|
||||||
|
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("inactive")
|
||||||
|
.addStringOption(addClanChoices)
|
||||||
|
.addNumberOption((option) => {
|
||||||
|
const { strings } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.setName("threshold")
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) => strings.bot.commands.inactive.options.threshold.name,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
strings.bot.commands.inactive.options.threshold.description
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.inactive.options.threshold.description
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setMinValue(0);
|
||||||
|
}),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = 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]
|
||||||
|
)
|
||||||
|
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(
|
||||||
|
([name, days]) =>
|
||||||
|
[
|
||||||
|
name,
|
||||||
|
literals(strings.bot.commands.inactive.body.listing, {
|
||||||
|
days: days.toFixed(0),
|
||||||
|
}),
|
||||||
|
] as [string, string]
|
||||||
|
);
|
||||||
|
const hasInactiveMembers = inactive.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={clanInfo.name}
|
||||||
|
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
||||||
|
description={`${literals(
|
||||||
|
strings.bot.commands.inactive.body.subtitle,
|
||||||
|
{ days: threshold }
|
||||||
|
)} • ${new Date().toLocaleDateString(interaction.locale)}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!hasInactiveMembers && (
|
||||||
|
<NoData type="players_in_period" locale={interaction.locale} />
|
||||||
|
)}
|
||||||
|
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteClan,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { getAccountInfo, getClanAccountInfo } from '@blitzkit/core';
|
||||||
|
import { teal } from '@radix-ui/colors';
|
||||||
|
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
|
||||||
|
import { CommandWrapper } from '../components/CommandWrapper';
|
||||||
|
import { linkBlitzAndDiscord } from '../core/db/linkBlitzAndDiscord';
|
||||||
|
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
|
||||||
|
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
|
||||||
|
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
|
||||||
|
import { embedInfo } from '../core/discord/embedInfo';
|
||||||
|
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { translator } from '../core/localization/translator';
|
||||||
|
import type { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand('link').addStringOption((option) =>
|
||||||
|
addUsernameChoices(option).setRequired(true),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
const discordId = BigInt(interaction.user.id);
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
|
||||||
|
|
||||||
|
await linkBlitzAndDiscord(discordId, id);
|
||||||
|
|
||||||
|
return [
|
||||||
|
<CommandWrapper fat>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glow color={teal.teal9} rotation={90} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="avatar"
|
||||||
|
src={
|
||||||
|
interaction.user.avatarURL({ extension: 'png' }) ??
|
||||||
|
'https://i.stack.imgur.com/l60Hf.png'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
color: theme.colors.textLowContrast_teal,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="accounts linked"
|
||||||
|
src="https://i.imgur.com/jIcRgog.png"
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 16 }}>
|
||||||
|
{strings.bot.commands.link.body.accounts_linked}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan ? (
|
||||||
|
<img
|
||||||
|
alt="clan emblem"
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{accountInfo.nickname}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan
|
||||||
|
? `[${clanAccountInfo?.clan?.tag}] • `
|
||||||
|
: ''}
|
||||||
|
{strings.common.regions.normal[region]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
|
</div>
|
||||||
|
</CommandWrapper>,
|
||||||
|
|
||||||
|
embedInfo(
|
||||||
|
strings.bot.commands.link.embed.title,
|
||||||
|
strings.bot.commands.link.embed.description,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
TANK_CLASSES,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
TankDefinition,
|
||||||
|
flags,
|
||||||
|
getAccountInfo,
|
||||||
|
getTankStats,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import markdownEscape from "markdown-escape";
|
||||||
|
import {
|
||||||
|
gameDefinitions,
|
||||||
|
tankDefinitions,
|
||||||
|
} from "../core/blitzkit/nonBlockingPromises";
|
||||||
|
import { addTierChoices } from "../core/discord/addTierChoices";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
|
||||||
|
import { chunkLines } from "../core/discord/chunkLines";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import { type CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("owned-tanks")
|
||||||
|
.addStringOption(addTierChoices)
|
||||||
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings, unwrap } = translator(interaction.locale);
|
||||||
|
const tier = Number(interaction.options.getString("tier"));
|
||||||
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const tankStats = await getTankStats(region, id);
|
||||||
|
|
||||||
|
if (tankStats === null) {
|
||||||
|
return strings.bot.common.errors.no_tank_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredTanks = (
|
||||||
|
await Promise.all(
|
||||||
|
tankStats.map(async (tankData) => ({
|
||||||
|
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
|
||||||
|
id: tankData.tank_id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
).filter((tank) => tank.tankDefinitions?.tier === tier);
|
||||||
|
const groupedTanks: Record<string, TankDefinition[]> = {};
|
||||||
|
const nations: string[] = [];
|
||||||
|
|
||||||
|
filteredTanks.forEach((tank) => {
|
||||||
|
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
|
||||||
|
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
|
||||||
|
nations.push(tank.tankDefinitions.nation);
|
||||||
|
} else {
|
||||||
|
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const awaitedGameDefinitions = await gameDefinitions;
|
||||||
|
const title = literals(strings.bot.commands.owned_tanks.body.title, {
|
||||||
|
name: markdownEscape(accountInfo.nickname),
|
||||||
|
tier: TIER_ROMAN_NUMERALS[tier],
|
||||||
|
});
|
||||||
|
const tankList = tankStats
|
||||||
|
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
|
||||||
|
.filter((tank) => tank.tier === tier)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
awaitedGameDefinitions.nations.indexOf(a.nation) -
|
||||||
|
awaitedGameDefinitions.nations.indexOf(b.nation),
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(tank) =>
|
||||||
|
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name!))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
|
||||||
|
);
|
||||||
|
const lines = [title, ...tankList];
|
||||||
|
|
||||||
|
return chunkLines(lines);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("permissions"),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
// friday the 13th permissions easter egg
|
||||||
|
const isFridayThe13th =
|
||||||
|
new Date().getDay() === 5 && new Date().getDate() === 13;
|
||||||
|
const permissions = [
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has("ViewChannel"),
|
||||||
|
strings.bot.commands.permissions.body.view_channels,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has("ReadMessageHistory"),
|
||||||
|
strings.bot.commands.permissions.body.read_message_history,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has("AttachFiles"),
|
||||||
|
strings.bot.commands.permissions.body.attach_files,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
interaction.appPermissions?.has("UseExternalEmojis"),
|
||||||
|
strings.bot.commands.permissions.body.external_emojis,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
const body = permissions
|
||||||
|
.map(([hasPermission, description]) => {
|
||||||
|
if (hasPermission) {
|
||||||
|
return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
|
||||||
|
} else {
|
||||||
|
return `${isFridayThe13th ? "💀" : "❌"} ${description}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return literals(strings.bot.commands.permissions.body.title, { body });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
handlesInteraction: true,
|
||||||
|
|
||||||
|
command: createLocalizedCommand("ping", [
|
||||||
|
{ subcommand: "blitzkit" },
|
||||||
|
{ subcommand: "wotb" },
|
||||||
|
]),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
const executionStart = Date.now();
|
||||||
|
|
||||||
|
if (subcommand === "blitzkit") {
|
||||||
|
await interaction.editReply(strings.bot.commands.ping.body.pong);
|
||||||
|
} else {
|
||||||
|
await fetch("https://api.wotblitz.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionTime = Date.now() - executionStart;
|
||||||
|
interaction.editReply(
|
||||||
|
literals(strings.bot.commands.ping.body.pong_with_time, {
|
||||||
|
time: executionTime.toFixed(0),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { getAccountInfo } from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import markdownEscape from "markdown-escape";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { embedInfo } from "../core/discord/embedInfo";
|
||||||
|
import { markdownTable } from "../core/discord/markdownTable";
|
||||||
|
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command:
|
||||||
|
createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
|
const { id, region: region } = account;
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
|
||||||
|
return embedInfo(
|
||||||
|
literals(strings.bot.commands.player_info.body.title, {
|
||||||
|
name: markdownEscape(accountInfo.nickname),
|
||||||
|
}),
|
||||||
|
|
||||||
|
markdownTable([
|
||||||
|
[
|
||||||
|
strings.bot.commands.player_info.body.nickname,
|
||||||
|
`${accountInfo.nickname}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
strings.bot.commands.player_info.body.battles,
|
||||||
|
`${accountInfo.statistics.all.battles}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
strings.bot.commands.player_info.body.winrate,
|
||||||
|
`${(
|
||||||
|
100 *
|
||||||
|
(accountInfo.statistics.all.wins /
|
||||||
|
accountInfo.statistics.all.battles)
|
||||||
|
).toFixed(2)}%`,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
strings.bot.commands.player_info.body.account_id,
|
||||||
|
`${accountInfo.account_id}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
strings.bot.commands.player_info.body.created,
|
||||||
|
new Date(accountInfo.created_at * 1000).toLocaleDateString(
|
||||||
|
interaction.locale
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
strings.bot.commands.player_info.body.last_battle,
|
||||||
|
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
|
||||||
|
interaction.locale
|
||||||
|
),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
import {
|
||||||
|
deltaBkrlBlitzStats,
|
||||||
|
emblemURL,
|
||||||
|
getAccountInfo,
|
||||||
|
getArchivedRatingLeaderboard,
|
||||||
|
getClanAccountInfo,
|
||||||
|
getLeagueFromScore,
|
||||||
|
getRatingInfo,
|
||||||
|
getRatingNeighbors,
|
||||||
|
imgur,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { SlashCommandSubcommandBuilder } from "discord.js";
|
||||||
|
import markdownEscape from "markdown-escape";
|
||||||
|
import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import { DeltaCaret } from "../components/DeltaCaret";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
|
||||||
|
import { iconPng } from "../core/blitzkit/iconPng";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
import { theme } from "../stitches.config";
|
||||||
|
|
||||||
|
const LEAGUE_ACCENT = [
|
||||||
|
"_purple",
|
||||||
|
"_sand",
|
||||||
|
"_amber",
|
||||||
|
"_mauve",
|
||||||
|
"_brown",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("rating", [
|
||||||
|
{
|
||||||
|
subcommand: "today",
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option.addStringOption(addUsernameChoices);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subcommand: "season",
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option.addStringOption(addUsernameChoices);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
|
| "season"
|
||||||
|
| "today";
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
|
||||||
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
|
||||||
|
const ratingInfo = await getRatingInfo(region);
|
||||||
|
|
||||||
|
if (ratingInfo.detail) {
|
||||||
|
throw new Error(`Rating info not available for ${region}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaderboard = await getArchivedRatingLeaderboard(
|
||||||
|
region,
|
||||||
|
ratingInfo.current_season - (subcommand === "today" ? 0 : 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (leaderboard.version!.$case === "v1") {
|
||||||
|
throw new Error(
|
||||||
|
"Encountered bkrl minimal format in latest season. Wait till next cron?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountInfo = await getAccountInfo(region, id, [
|
||||||
|
"statistics.rating",
|
||||||
|
]);
|
||||||
|
const statsIndex = leaderboard.version!.value.entries.findIndex(
|
||||||
|
(entry) => entry.id === id
|
||||||
|
);
|
||||||
|
const statsA = leaderboard
|
||||||
|
? leaderboard.version!.value.entries[statsIndex!]
|
||||||
|
: null;
|
||||||
|
const positionA = leaderboard ? statsIndex! + 1 : undefined;
|
||||||
|
const statsB1 = accountInfo.statistics.rating!;
|
||||||
|
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
|
||||||
|
|
||||||
|
if (statsB2Array === undefined) {
|
||||||
|
return literals(strings.bot.commands.rating.errors.no_participation, {
|
||||||
|
name: markdownEscape(accountInfo.nickname),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [statsB2] = statsB2Array;
|
||||||
|
const league = getLeagueFromScore(statsB2.score);
|
||||||
|
|
||||||
|
if (!statsA) {
|
||||||
|
return strings.bot.commands.rating.errors.unrecorded_stats;
|
||||||
|
}
|
||||||
|
if (!statsB1) {
|
||||||
|
return literals(strings.bot.commands.rating.errors.no_participation, {
|
||||||
|
name: markdownEscape(accountInfo.nickname),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const reward = ratingInfo.rewards.find(
|
||||||
|
(reward) =>
|
||||||
|
statsB2.number >= reward.from_position &&
|
||||||
|
statsB2.number <= reward.to_position
|
||||||
|
);
|
||||||
|
const positionDelta = leaderboard
|
||||||
|
? statsB2.number - positionA!
|
||||||
|
: undefined;
|
||||||
|
const delta = deltaBkrlBlitzStats(
|
||||||
|
statsA,
|
||||||
|
accountInfo.statistics.rating!,
|
||||||
|
statsB2
|
||||||
|
);
|
||||||
|
const leagueTheme = LEAGUE_ACCENT[league.index];
|
||||||
|
let rewardImage = reward
|
||||||
|
? reward.type === "vehicle"
|
||||||
|
? reward.vehicle.preview_image_url
|
||||||
|
: reward.stuff.image_url
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (rewardImage) {
|
||||||
|
const { ok } = await fetch(rewardImage);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
rewardImage = imgur("kP88vqr");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={accountInfo.nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${
|
||||||
|
strings.bot.commands.rating.body.subtitle[subcommand]
|
||||||
|
} • ${new Date().toLocaleDateString(interaction.locale)}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
|
||||||
|
borderRadius: 4,
|
||||||
|
border: `1px solid ${
|
||||||
|
theme.colors[`componentInteractive${leagueTheme}`]
|
||||||
|
}`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{reward === undefined && (
|
||||||
|
<Glow
|
||||||
|
endOpacity={3 / 4}
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
transform: "translateX(-16px) rotate(90deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={`League ${league.name}`}
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
src={normalizeLeagueIcon(
|
||||||
|
ratingInfo.leagues[league.index].big_icon
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textHighContrast${leagueTheme}`],
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 900,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statsB2.score.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
{delta.score !== 0 && (
|
||||||
|
<>
|
||||||
|
<DeltaCaret delta={delta.score} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
delta.score > 0
|
||||||
|
? theme.colors.textLowContrast_green
|
||||||
|
: theme.colors.textLowContrast_red,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.abs(delta.score).toLocaleString(
|
||||||
|
interaction.locale
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textLowContrast${leagueTheme}`],
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{statsB2.number.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
{positionDelta !== undefined && positionDelta !== 0 && (
|
||||||
|
<>
|
||||||
|
<DeltaCaret delta={-positionDelta} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
positionDelta > 0
|
||||||
|
? theme.colors.textLowContrast_red
|
||||||
|
: theme.colors.textLowContrast_green,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.abs(positionDelta).toLocaleString(
|
||||||
|
interaction.locale
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{reward && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textHighContrast${leagueTheme}`],
|
||||||
|
fontSize: 16,
|
||||||
|
maxWidth: 136,
|
||||||
|
textAlign: "right",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{reward.type === "vehicle"
|
||||||
|
? reward.vehicle.user_string
|
||||||
|
: reward.stuff.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textLowContrast${leagueTheme}`],
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reward
|
||||||
|
{reward.type === "stuff" ? ` x${reward.count}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
objectFit: "cover",
|
||||||
|
}}
|
||||||
|
src={await iconPng(rewardImage!)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{reward === undefined && (
|
||||||
|
<Glow
|
||||||
|
endOpacity={3 / 4}
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
transform: "translateX(16px) rotate(-90deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* separated because fragment middle man invalidates relative parent: https://github.com/vercel/satori/issues/605 */}
|
||||||
|
{reward && (
|
||||||
|
<Glow
|
||||||
|
direction="reverse"
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: -40,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reward && (
|
||||||
|
<Glow
|
||||||
|
color={theme.colors[`solidBackground${leagueTheme}`]}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -40,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{delta.battles > 0 && (
|
||||||
|
<div style={{ padding: "8px 0", display: "flex" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{delta.battles.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Battles
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Winrate
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.round(delta.damage / delta.battles).toLocaleString(
|
||||||
|
interaction.locale
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Damage
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Survival
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(delta.kills / delta.battles).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kills
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
import {
|
||||||
|
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
|
||||||
|
getAccountInfo,
|
||||||
|
getArchivedLatestSeasonNumber,
|
||||||
|
getArchivedRatingLeaderboard,
|
||||||
|
getClanAccountInfo,
|
||||||
|
getRatingInfo,
|
||||||
|
getRatingLeague,
|
||||||
|
getRatingNeighbors,
|
||||||
|
imgur,
|
||||||
|
isOnGoingRatingSeason,
|
||||||
|
LEAGUES,
|
||||||
|
type RatingInfo,
|
||||||
|
RatingLeaderboard,
|
||||||
|
type Region,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import {
|
||||||
|
type APIApplicationCommandOptionChoice,
|
||||||
|
Locale,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
|
import { range } from "lodash-es";
|
||||||
|
import markdownEscape from "markdown-escape";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import * as Leaderboard from "../components/Leaderboard";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { UserError } from "../core/blitzkit/userError";
|
||||||
|
import { addRegionChoices } from "../core/discord/addRegionChoices";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { localizationObject } from "../core/discord/localizationObject";
|
||||||
|
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
interface SimplifiedPlayer {
|
||||||
|
id: number;
|
||||||
|
score: number;
|
||||||
|
position: number;
|
||||||
|
clan: string | undefined;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT = 10;
|
||||||
|
|
||||||
|
export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
|
||||||
|
async (resolve) => {
|
||||||
|
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
||||||
|
const onGoingSeason = await isOnGoingRatingSeason();
|
||||||
|
const seasonNumbers = range(
|
||||||
|
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
|
||||||
|
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
|
||||||
|
).slice(-25);
|
||||||
|
|
||||||
|
function addOptions(option: SlashCommandSubcommandBuilder) {
|
||||||
|
const { strings } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(strings.bot.commands.rating_leaderboard.options.limit.name)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.rating_leaderboard.options.limit.name,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
literals(
|
||||||
|
strings.bot.commands.rating_leaderboard.options.limit
|
||||||
|
.description,
|
||||||
|
{ count: DEFAULT_LIMIT }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.rating_leaderboard.options.limit
|
||||||
|
.description,
|
||||||
|
{ count: DEFAULT_LIMIT }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30)
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(
|
||||||
|
strings.bot.commands.rating_leaderboard.options.season.name
|
||||||
|
)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.rating_leaderboard.options.season.name,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
strings.bot.commands.rating_leaderboard.options.season.description
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.rating_leaderboard.options.season
|
||||||
|
.description
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
...seasonNumbers
|
||||||
|
.map((number, index) => {
|
||||||
|
const current =
|
||||||
|
index === seasonNumbers.length - 1 && onGoingSeason;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: current
|
||||||
|
? literals(
|
||||||
|
strings.bot.commands.rating_leaderboard.options.season
|
||||||
|
.choices.current,
|
||||||
|
{ season: number }
|
||||||
|
)
|
||||||
|
: `${number}`,
|
||||||
|
value: `${number}`,
|
||||||
|
name_localizations: current
|
||||||
|
? localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.rating_leaderboard.options
|
||||||
|
.season.choices.current,
|
||||||
|
{ season: number }
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
} satisfies APIApplicationCommandOptionChoice<string>;
|
||||||
|
})
|
||||||
|
.reverse()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("rating-leaderboard", [
|
||||||
|
{
|
||||||
|
subcommand: "neighbors",
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option.addStringOption(addUsernameChoices);
|
||||||
|
addOptions(option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subcommand: "league",
|
||||||
|
modify(option: SlashCommandSubcommandBuilder) {
|
||||||
|
option
|
||||||
|
.addStringOption((option) => {
|
||||||
|
const { strings } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.setName(
|
||||||
|
strings.bot.commands.rating_leaderboard.options.league.name
|
||||||
|
)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.rating_leaderboard.options.league
|
||||||
|
.name,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
strings.bot.commands.rating_leaderboard.options.league
|
||||||
|
.description
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.rating_leaderboard.options.league
|
||||||
|
.description
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addChoices(
|
||||||
|
...LEAGUES.map(
|
||||||
|
({ name }, value) =>
|
||||||
|
({
|
||||||
|
name: (
|
||||||
|
strings.common.leagues as Record<string, string>
|
||||||
|
)[name],
|
||||||
|
value: `${value}`,
|
||||||
|
name_localizations: localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
(
|
||||||
|
strings.common.leagues as Record<string, string>
|
||||||
|
)[name]
|
||||||
|
),
|
||||||
|
} satisfies APIApplicationCommandOptionChoice<string>)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setRequired(true);
|
||||||
|
})
|
||||||
|
.addStringOption(addRegionChoices);
|
||||||
|
addOptions(option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
// BIG TODO: title seems to be in english
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = 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 archivedLeaderboard: RatingLeaderboard | null;
|
||||||
|
let playerId: number | undefined;
|
||||||
|
let regionRatingInfo: RatingInfo | null;
|
||||||
|
|
||||||
|
if (subcommand === "league") {
|
||||||
|
const leagueIndex = parseInt(
|
||||||
|
interaction.options.getString("league")!
|
||||||
|
);
|
||||||
|
const region = interaction.options.getString("region") as Region;
|
||||||
|
regionRatingInfo = await (isCurrentSeason
|
||||||
|
? getRatingInfo(region)
|
||||||
|
: null);
|
||||||
|
|
||||||
|
if (
|
||||||
|
regionRatingInfo !== null &&
|
||||||
|
regionRatingInfo.detail !== undefined
|
||||||
|
) {
|
||||||
|
return strings.bot.commands.rating_leaderboard.body
|
||||||
|
.no_ongoing_season;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = isCurrentSeason
|
||||||
|
? await getRatingLeague(region, leagueIndex).then(async (result) =>
|
||||||
|
result.slice(0, limit).map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
score: player.score,
|
||||||
|
position: player.number,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
} satisfies SimplifiedPlayer)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: await getArchivedRatingLeaderboard(region, season!).then(
|
||||||
|
async (players) => {
|
||||||
|
const firstPlayerIndex =
|
||||||
|
leagueIndex === 0
|
||||||
|
? 0
|
||||||
|
: players.version!.value.entries.findIndex(
|
||||||
|
(player) =>
|
||||||
|
player.score < LEAGUES[leagueIndex - 1].minScore
|
||||||
|
);
|
||||||
|
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
||||||
|
const trimmed = players.version!.value.entries.slice(
|
||||||
|
firstPlayerIndex,
|
||||||
|
lastPlayerIndex + 1
|
||||||
|
);
|
||||||
|
const clanData = await getClanAccountInfo(
|
||||||
|
region,
|
||||||
|
trimmed.map(({ id }) => id),
|
||||||
|
["clan"]
|
||||||
|
);
|
||||||
|
|
||||||
|
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 ??
|
||||||
|
literals(
|
||||||
|
strings.bot.commands.rating_leaderboard.body
|
||||||
|
.deleted_player,
|
||||||
|
{ id: player.id }
|
||||||
|
),
|
||||||
|
} satisfies SimplifiedPlayer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const leagueInfo = LEAGUES[leagueIndex];
|
||||||
|
archivedLeaderboard = await getArchivedRatingLeaderboard(
|
||||||
|
region,
|
||||||
|
season
|
||||||
|
);
|
||||||
|
|
||||||
|
players = result;
|
||||||
|
playersAfter =
|
||||||
|
(regionRatingInfo === null
|
||||||
|
? archivedLeaderboard.version!.value.entries.length
|
||||||
|
: regionRatingInfo.count) - result[result.length - 1].position;
|
||||||
|
titleName = `${
|
||||||
|
leagueInfo
|
||||||
|
? (strings.common.leagues as Record<string, string>)[
|
||||||
|
LEAGUES[leagueInfo.index].name
|
||||||
|
]
|
||||||
|
: ""
|
||||||
|
} - ${strings.common.regions.short[region]}`;
|
||||||
|
titleImage = imgur(leagueInfo.icon);
|
||||||
|
titleDescription = literals(
|
||||||
|
strings.bot.commands.rating_leaderboard.body.top_players,
|
||||||
|
{ count: limit }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
|
regionRatingInfo = isCurrentSeason
|
||||||
|
? await getRatingInfo(region)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
regionRatingInfo !== null &&
|
||||||
|
regionRatingInfo.detail !== undefined
|
||||||
|
) {
|
||||||
|
return strings.bot.commands.rating_leaderboard.body
|
||||||
|
.no_ongoing_season;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
|
||||||
|
const neighbors = isCurrentSeason
|
||||||
|
? await getRatingNeighbors(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 getArchivedRatingLeaderboard(region, season!).then(
|
||||||
|
async (players) => {
|
||||||
|
const playerIndex = players.version!.value.entries.findIndex(
|
||||||
|
(player) => player.id === id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerIndex === -1) {
|
||||||
|
throw new UserError(
|
||||||
|
literals(
|
||||||
|
strings.bot.commands.rating_leaderboard.body
|
||||||
|
.no_participation,
|
||||||
|
{ player: markdownEscape(accountInfo.nickname), season }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const halfRange = Math.round(limit / 2);
|
||||||
|
const trimmed = players.version!.value.entries.slice(
|
||||||
|
playerIndex - halfRange,
|
||||||
|
playerIndex + halfRange + 1
|
||||||
|
);
|
||||||
|
const clanData = await getClanAccountInfo(
|
||||||
|
region,
|
||||||
|
trimmed.map((player) => player.id),
|
||||||
|
["clan"]
|
||||||
|
);
|
||||||
|
|
||||||
|
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}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
archivedLeaderboard = await getArchivedRatingLeaderboard(
|
||||||
|
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 : "";
|
||||||
|
titleDescription =
|
||||||
|
strings.bot.commands.rating_leaderboard.body.neighbors;
|
||||||
|
playersAfter =
|
||||||
|
(regionRatingInfo === null
|
||||||
|
? archivedLeaderboard.version!.value.entries.length
|
||||||
|
: regionRatingInfo.count) -
|
||||||
|
neighbors[neighbors.length - 1].position;
|
||||||
|
playerId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = players.map((player) => {
|
||||||
|
const midnightIndex =
|
||||||
|
archivedLeaderboard?.version!.value.entries.findIndex(
|
||||||
|
(item) => item.id === player.id
|
||||||
|
);
|
||||||
|
const midnightScore =
|
||||||
|
midnightIndex === undefined
|
||||||
|
? player.score
|
||||||
|
: archivedLeaderboard!.version!.value.entries[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}
|
||||||
|
deltaScore={player.score - midnightScore}
|
||||||
|
deltaPosition={midnightPosition - player.position}
|
||||||
|
key={player.id}
|
||||||
|
highlight={player.id === playerId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={titleName}
|
||||||
|
image={titleImage}
|
||||||
|
description={titleDescription}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{items && (
|
||||||
|
<Leaderboard.Root>
|
||||||
|
{items}
|
||||||
|
{playersAfter > 0 && (
|
||||||
|
<Leaderboard.Gap
|
||||||
|
message={literals(
|
||||||
|
strings.bot.commands.rating_leaderboard.body.more,
|
||||||
|
{ count: playersAfter.toLocaleString(interaction.locale) }
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Leaderboard.Root>
|
||||||
|
)}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,592 @@
|
|||||||
|
import {
|
||||||
|
calculateWN8,
|
||||||
|
emblemURL,
|
||||||
|
getClanAccountInfo,
|
||||||
|
idToRegion,
|
||||||
|
tankIcon,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import { clamp } from "lodash-es";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { iconPng } from "../core/blitzkit/iconPng";
|
||||||
|
import {
|
||||||
|
averageDefinitions,
|
||||||
|
mapDefinitions,
|
||||||
|
tankDefinitions,
|
||||||
|
} from "../core/blitzkit/nonBlockingPromises";
|
||||||
|
import { RenderConfiguration } from "../core/blitzkit/renderConfiguration";
|
||||||
|
import { buttonLink } from "../core/discord/buttonLink";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { localizationObject } from "../core/discord/localizationObject";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import { type CommandRegistry } from "../events/interactionCreate";
|
||||||
|
import { theme } from "../stitches.config";
|
||||||
|
|
||||||
|
type WotInspectorReplaySubmission =
|
||||||
|
| { status: "error"; data: unknown; error: { message: string } }
|
||||||
|
| {
|
||||||
|
status: "ok";
|
||||||
|
data: {
|
||||||
|
download_url: string;
|
||||||
|
view_url: string;
|
||||||
|
summary: {
|
||||||
|
battle_start_time: string;
|
||||||
|
map_name: string;
|
||||||
|
battle_start_timestamp: number;
|
||||||
|
battle_duration: number;
|
||||||
|
title: string;
|
||||||
|
arena_unique_id: string;
|
||||||
|
allies: number[];
|
||||||
|
enemies: number[];
|
||||||
|
protagonist: number;
|
||||||
|
player_name: string;
|
||||||
|
vehicle: string;
|
||||||
|
exp_base: number;
|
||||||
|
exp_total: number;
|
||||||
|
credits_base: number;
|
||||||
|
credits_total: number;
|
||||||
|
room_type: number;
|
||||||
|
battle_type: number;
|
||||||
|
mastery_badge: number;
|
||||||
|
battle_result: number;
|
||||||
|
vehicle_tier: number;
|
||||||
|
vehicle_type: number;
|
||||||
|
protagonist_team: number;
|
||||||
|
winner_team: number;
|
||||||
|
details: {
|
||||||
|
achievements: unknown[];
|
||||||
|
base_capture_points: number;
|
||||||
|
base_defend_points: number;
|
||||||
|
chassis_id: unknown | null;
|
||||||
|
clan_tag: string;
|
||||||
|
clanid: number;
|
||||||
|
credits: number;
|
||||||
|
damage_assisted: number;
|
||||||
|
damage_assisted_track: number;
|
||||||
|
damage_blocked: unknown | null;
|
||||||
|
damage_made: number;
|
||||||
|
damage_received: number;
|
||||||
|
dbid: number;
|
||||||
|
death_reason: number;
|
||||||
|
distance_travelled: number;
|
||||||
|
enemies_damaged: number;
|
||||||
|
enemies_destroyed: number;
|
||||||
|
enemies_spotted: number;
|
||||||
|
entity_id: number;
|
||||||
|
exp: number;
|
||||||
|
exp_for_assist: number;
|
||||||
|
exp_for_damage: number;
|
||||||
|
exp_team_bonus: number;
|
||||||
|
gun_id: unknown | null;
|
||||||
|
hero_bonus_credits: number;
|
||||||
|
hero_bonus_exp: number;
|
||||||
|
hitpoints_left: number;
|
||||||
|
hits_bounced: number;
|
||||||
|
hits_pen: number;
|
||||||
|
hits_received: number;
|
||||||
|
hits_splash: number;
|
||||||
|
killed_by: number;
|
||||||
|
name: string;
|
||||||
|
shots_hit: number;
|
||||||
|
shots_made: number;
|
||||||
|
shots_pen: number;
|
||||||
|
shots_splash: number;
|
||||||
|
squad_index: number;
|
||||||
|
team: number;
|
||||||
|
time_alive: number;
|
||||||
|
turret_id: unknown | null;
|
||||||
|
vehicle_descr: number;
|
||||||
|
wp_points_earned: number;
|
||||||
|
wp_points_stolen: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
error: {};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface WotInspectorReplay {
|
||||||
|
id: string;
|
||||||
|
map_id: number;
|
||||||
|
battle_duration: number;
|
||||||
|
title: unknown | null;
|
||||||
|
player_name: string;
|
||||||
|
protagonist: number;
|
||||||
|
vehicle_descr: number;
|
||||||
|
mastery_badge: number;
|
||||||
|
exp_base: number;
|
||||||
|
enemies_spotted: number;
|
||||||
|
enemies_destroyed: number;
|
||||||
|
damage_assisted: number;
|
||||||
|
damage_made: number;
|
||||||
|
details_url: string;
|
||||||
|
download_url: string;
|
||||||
|
game_version: {
|
||||||
|
name: string;
|
||||||
|
package: string;
|
||||||
|
};
|
||||||
|
arena_unique_id: string;
|
||||||
|
download_count: number;
|
||||||
|
data_version: number;
|
||||||
|
private: boolean;
|
||||||
|
private_clan: boolean;
|
||||||
|
battle_start_time: Date;
|
||||||
|
upload_time: Date;
|
||||||
|
allies: number[];
|
||||||
|
enemies: number[];
|
||||||
|
protagonist_clan: number;
|
||||||
|
protagonist_team: number;
|
||||||
|
battle_result: number;
|
||||||
|
credits_base: number;
|
||||||
|
tags: number[];
|
||||||
|
battle_type: number;
|
||||||
|
room_type: number;
|
||||||
|
last_accessed_time: Date;
|
||||||
|
winner_team: number;
|
||||||
|
finish_reason: number;
|
||||||
|
players_data: WotInspectorReplayPlayerData[];
|
||||||
|
exp_total: number;
|
||||||
|
credits_total: number;
|
||||||
|
repair_cost: number;
|
||||||
|
exp_free: number;
|
||||||
|
exp_free_base: number;
|
||||||
|
exp_penalty: number;
|
||||||
|
credits_penalty: number;
|
||||||
|
credits_contribution_in: number;
|
||||||
|
credits_contribution_out: number;
|
||||||
|
camouflage_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WotInspectorReplayPlayerData {
|
||||||
|
team: number;
|
||||||
|
name: string;
|
||||||
|
entity_id: number;
|
||||||
|
dbid: number;
|
||||||
|
clanid: number | null;
|
||||||
|
clan_tag: null | string;
|
||||||
|
hitpoints_left: number;
|
||||||
|
credits: number;
|
||||||
|
exp: number;
|
||||||
|
shots_made: number;
|
||||||
|
shots_hit: number;
|
||||||
|
shots_splash: number;
|
||||||
|
shots_pen: number;
|
||||||
|
damage_made: number;
|
||||||
|
damage_received: number;
|
||||||
|
damage_assisted: number;
|
||||||
|
damage_assisted_track: number;
|
||||||
|
hits_received: number;
|
||||||
|
hits_bounced: number;
|
||||||
|
hits_splash: number;
|
||||||
|
hits_pen: number;
|
||||||
|
enemies_spotted: number;
|
||||||
|
enemies_damaged: number;
|
||||||
|
enemies_destroyed: number;
|
||||||
|
time_alive: number;
|
||||||
|
distance_travelled: number;
|
||||||
|
killed_by: number;
|
||||||
|
base_capture_points: number;
|
||||||
|
base_defend_points: number;
|
||||||
|
exp_for_damage: number;
|
||||||
|
exp_for_assist: number;
|
||||||
|
exp_team_bonus: number;
|
||||||
|
wp_points_earned: number;
|
||||||
|
wp_points_stolen: number;
|
||||||
|
hero_bonus_credits: number;
|
||||||
|
hero_bonus_exp: number;
|
||||||
|
death_reason: number;
|
||||||
|
achievements: any[];
|
||||||
|
vehicle_descr: number;
|
||||||
|
turret_id: number;
|
||||||
|
gun_id: number;
|
||||||
|
chassis_id: number;
|
||||||
|
squad_index: number;
|
||||||
|
damage_blocked: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stat(value: string | number, flex = 2) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16 }}>{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playerListing(
|
||||||
|
locale: Locale,
|
||||||
|
player: WotInspectorReplayPlayerData,
|
||||||
|
protagonistTeam: boolean,
|
||||||
|
winningTeam: number,
|
||||||
|
recordingPlayer = false,
|
||||||
|
) {
|
||||||
|
const { unwrap } = translator(locale);
|
||||||
|
const awaitedTankAverages = await averageDefinitions;
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const tank = awaitedTankDefinitions.tanks[player.vehicle_descr];
|
||||||
|
const blockAccent = recordingPlayer ? "_amber" : "";
|
||||||
|
const healthAccent = protagonistTeam
|
||||||
|
? recordingPlayer
|
||||||
|
? "_amber"
|
||||||
|
: "_green"
|
||||||
|
: "_red";
|
||||||
|
|
||||||
|
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
|
||||||
|
// TODO: 💀
|
||||||
|
const turret = tank.turrets.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Math.abs(a.id - player.turret_id) - Math.abs(b.id - player.turret_id),
|
||||||
|
)[0];
|
||||||
|
const totalHealth = turret.health + tank.health;
|
||||||
|
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
|
||||||
|
const tankAveragesEntry = awaitedTankAverages.averages[player.vehicle_descr];
|
||||||
|
const wn8 =
|
||||||
|
tankAveragesEntry === undefined
|
||||||
|
? -1
|
||||||
|
: calculateWN8(tankAveragesEntry.mu, {
|
||||||
|
battles: 1,
|
||||||
|
damage_dealt: player.damage_made,
|
||||||
|
dropped_capture_points: player.base_defend_points,
|
||||||
|
frags: player.enemies_destroyed,
|
||||||
|
spotted: player.enemies_spotted,
|
||||||
|
wins: winningTeam === player.team ? 1 : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={player.entity_id}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
flex: 1,
|
||||||
|
overflow: "hidden",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={unwrap(tank.name!)}
|
||||||
|
src={await iconPng(tankIcon(tank.id))}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
// align
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textLowContrast${blockAccent}`],
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors[`textHighContrast${blockAccent}`],
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{unwrap(tank.name!)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors[`textLowContrast${blockAccent}`],
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{player.name}
|
||||||
|
{player.clan_tag ? ` [${player.clan_tag}]` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stat(player.damage_made.toLocaleString())}
|
||||||
|
{stat(wn8 === -1 ? "--" : Math.round(wn8).toLocaleString())}
|
||||||
|
{stat(player.exp.toLocaleString())}
|
||||||
|
{stat(player.enemies_destroyed, 1)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
|
||||||
|
display: "flex",
|
||||||
|
width: 4,
|
||||||
|
height: "100%",
|
||||||
|
transform: "scaleY(-1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: `${healthLeft * 100}%`,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 8,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 2,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Damage
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 2,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WN8
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 2,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
XP
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kills
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
const { strings } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("replay").addAttachmentOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(strings.bot.commands.replay.options.file.name)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) => strings.bot.commands.replay.options.file.name,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescription(strings.bot.commands.replay.options.file.description)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) => strings.bot.commands.replay.options.file.description,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const awaitedMapDefinitions = await mapDefinitions;
|
||||||
|
const file = interaction.options.getAttachment("file")!;
|
||||||
|
const submissionData = (await fetch(
|
||||||
|
`https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent(
|
||||||
|
file.url,
|
||||||
|
)}`,
|
||||||
|
).then((response) => response.json())) as WotInspectorReplaySubmission;
|
||||||
|
|
||||||
|
if (submissionData.status === "error") {
|
||||||
|
return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replayId = submissionData.data.download_url.split("/").at(-1)!;
|
||||||
|
const replayData = (await fetch(
|
||||||
|
`https://api.wotinspector.com/v2/blitz/replays/${replayId}/`,
|
||||||
|
).then((response) => response.json())) as WotInspectorReplay;
|
||||||
|
const antagonistTeamId = 3 - replayData.protagonist_team;
|
||||||
|
const players = replayData.players_data.sort(
|
||||||
|
(a, b) => b.damage_made - a.damage_made,
|
||||||
|
);
|
||||||
|
const clan = (
|
||||||
|
await getClanAccountInfo(
|
||||||
|
idToRegion(replayData.protagonist),
|
||||||
|
replayData.protagonist,
|
||||||
|
["clan"],
|
||||||
|
)
|
||||||
|
)?.clan;
|
||||||
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
|
||||||
|
return [
|
||||||
|
new RenderConfiguration({ width: 960 }),
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={replayData.player_name}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${
|
||||||
|
replayData.protagonist_team === replayData.winner_team
|
||||||
|
? "Victory"
|
||||||
|
: "Defeat"
|
||||||
|
} • ${awaitedMapDefinitions.maps[replayData.map_id].name}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
{await Promise.all(
|
||||||
|
players
|
||||||
|
.filter(
|
||||||
|
(player) => player.team === replayData.protagonist_team,
|
||||||
|
)
|
||||||
|
.map((player) =>
|
||||||
|
playerListing(
|
||||||
|
interaction.locale,
|
||||||
|
player,
|
||||||
|
true,
|
||||||
|
replayData.winner_team,
|
||||||
|
player.dbid === replayData.protagonist,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
{await Promise.all(
|
||||||
|
players
|
||||||
|
.filter((player) => player.team === antagonistTeamId)
|
||||||
|
.map((player) =>
|
||||||
|
playerListing(
|
||||||
|
interaction.locale,
|
||||||
|
player,
|
||||||
|
false,
|
||||||
|
replayData.winner_team,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandWrapper>,
|
||||||
|
buttonLink(submissionData.data.view_url, "WoT Inspector"),
|
||||||
|
buttonLink(submissionData.data.download_url, "Download"),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,626 @@
|
|||||||
|
import {
|
||||||
|
asset,
|
||||||
|
emblemURL,
|
||||||
|
equipmentPriceMatrix,
|
||||||
|
getAccountInfo,
|
||||||
|
getClanAccountInfo,
|
||||||
|
getTankStats,
|
||||||
|
TANK_ICONS,
|
||||||
|
TankType,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { escapeMarkdown, Locale } from "discord.js";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { resolveTankId } from "../core/blitz/resolveTankId";
|
||||||
|
import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
|
||||||
|
import { iconPng } from "../core/blitzkit/iconPng";
|
||||||
|
import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
|
||||||
|
import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
|
||||||
|
import { tankIconPng } from "../core/blitzkit/tankIconPng";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { localizationObject } from "../core/discord/localizationObject";
|
||||||
|
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import { type CommandRegistry } from "../events/interactionCreate";
|
||||||
|
import { theme } from "../stitches.config";
|
||||||
|
|
||||||
|
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
const { strings } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("research")
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(strings.bot.commands.research.options.target_tank.name!)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.research.options.target_tank.name,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
strings.bot.commands.research.options.target_tank.description,
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.research.options.target_tank.description,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setAutocomplete(true)
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(strings.bot.commands.research.options.starting_tank.name!)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.research.options.starting_tank.name,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
strings.bot.commands.research.options.starting_tank.description,
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.research.options.starting_tank.description,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setAutocomplete(true)
|
||||||
|
.setRequired(false),
|
||||||
|
)
|
||||||
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings, unwrap } = translator(interaction.locale);
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
const targetTankId = await resolveTankId(
|
||||||
|
interaction.options.getString("target-tank", true),
|
||||||
|
interaction.locale,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const startingTankRaw = interaction.options.getString("starting-tank");
|
||||||
|
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
|
||||||
|
const { id, region } = await resolvePlayerFromCommand(interaction);
|
||||||
|
let startingTankId: number;
|
||||||
|
const targetTankAncestry = await resolveAncestry(targetTankId);
|
||||||
|
|
||||||
|
if (startingTankRaw) {
|
||||||
|
startingTankId = await resolveTankId(
|
||||||
|
startingTankRaw,
|
||||||
|
interaction.locale,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetTankId === startingTankId) {
|
||||||
|
return literals(
|
||||||
|
strings.bot.commands.research.errors.start_end_equal,
|
||||||
|
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
|
||||||
|
|
||||||
|
if (startingTank.tier > targetTank.tier) {
|
||||||
|
return literals(strings.bot.commands.research.errors.unordered, {
|
||||||
|
tank_1: escapeMarkdown(unwrap(startingtank.name!)),
|
||||||
|
tank_2: escapeMarkdown(unwrap(targettank.name!)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetTankAncestry.includes(startingTankId)) {
|
||||||
|
return literals(
|
||||||
|
strings.bot.commands.research.errors.tanks_not_on_line,
|
||||||
|
{
|
||||||
|
tank_1: escapeMarkdown(unwrap(targettank.name!)),
|
||||||
|
tank_2: escapeMarkdown(unwrap(startingtank.name!)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (targetTank.type !== TankType.TANK_TYPE_RESEARCHABLE) {
|
||||||
|
return literals(strings.bot.commands.research.errors.non_tech_tree, {
|
||||||
|
tank: escapeMarkdown(unwrap(targettank.name!)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTank.tier === 1) {
|
||||||
|
return literals(strings.bot.commands.research.errors.tier_1, {
|
||||||
|
tank: escapeMarkdown(unwrap(targettank.name!)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tankStats = await getTankStats(region, id);
|
||||||
|
|
||||||
|
if (tankStats === null) {
|
||||||
|
return strings.bot.common.errors.no_tank_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
|
||||||
|
return literals(
|
||||||
|
strings.bot.commands.research.errors.already_researched,
|
||||||
|
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundAncestor = targetTankAncestry.find(
|
||||||
|
(id) =>
|
||||||
|
awaitedTankDefinitions.tanks[id].tier === 1 ||
|
||||||
|
tankStats.some(({ tank_id }) => tank_id === id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundAncestor === undefined) {
|
||||||
|
throw new Error("No first owned tech tree found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
startingTankId = foundAncestor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const techTreeLine = await buildTechTreeLine(
|
||||||
|
startingTankId,
|
||||||
|
targetTankId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const line = [...techTreeLine, startingTankId].reverse();
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
|
||||||
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
const costs = await Promise.all(
|
||||||
|
line.map(async (id) => {
|
||||||
|
const tank = awaitedTankDefinitions.tanks[id];
|
||||||
|
const turretXps = new Map<number, number>();
|
||||||
|
const gunXps = new Map<number, number>();
|
||||||
|
const engineXps = new Map<number, number>();
|
||||||
|
const trackXps = new Map<number, number>();
|
||||||
|
|
||||||
|
tank.turrets.forEach((turret) => {
|
||||||
|
if (
|
||||||
|
turret.research_cost !== undefined &&
|
||||||
|
typeof turret.research_cost.research_cost_type!.value === "number"
|
||||||
|
) {
|
||||||
|
turretXps.set(
|
||||||
|
turret.id,
|
||||||
|
turret.research_cost.research_cost_type!.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
turret.guns.forEach((gun) => {
|
||||||
|
if (
|
||||||
|
gun.research_cost !== undefined &&
|
||||||
|
typeof gun.research_cost.research_cost_type!.value === "number"
|
||||||
|
) {
|
||||||
|
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tank.engines.forEach((engine) => {
|
||||||
|
if (
|
||||||
|
engine.research_cost !== undefined &&
|
||||||
|
typeof engine.research_cost.research_cost_type!.value === "number"
|
||||||
|
) {
|
||||||
|
engineXps.set(
|
||||||
|
engine.id,
|
||||||
|
engine.research_cost.research_cost_type!.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tank.tracks.forEach((track) => {
|
||||||
|
if (
|
||||||
|
track.research_cost !== undefined &&
|
||||||
|
typeof track.research_cost.research_cost_type!.value === "number"
|
||||||
|
) {
|
||||||
|
trackXps.set(
|
||||||
|
track.id,
|
||||||
|
track.research_cost.research_cost_type!.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let moduleXp = 0;
|
||||||
|
turretXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
gunXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
engineXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
trackXps.forEach((value) => {
|
||||||
|
moduleXp += value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
research: tank.research_cost!.research_cost_type!.value as number,
|
||||||
|
upgrades: moduleXp,
|
||||||
|
purchase: tank.price!.value,
|
||||||
|
equipment: equipmentPriceMatrix[tank.tier].reduce(
|
||||||
|
(a, b) => a + 3 * b,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${strings.bot.commands.research.body.subtitle} • ${
|
||||||
|
targetTank.name
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: theme.colors.appBackground2,
|
||||||
|
border: theme.borderStyles.subtle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 48 }}></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
position: "relative",
|
||||||
|
right: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.bot.commands.research.body.total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 8,
|
||||||
|
padding: "16px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="XP icon"
|
||||||
|
src={await iconPng(asset("icons/currencies/xp.webp"))}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Experience:{" "}
|
||||||
|
{costs
|
||||||
|
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
|
||||||
|
.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Silver icon"
|
||||||
|
src={await iconPng(asset("icons/currencies/silver.webp"))}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Purchase:{" "}
|
||||||
|
{costs
|
||||||
|
.reduce((a, b) => a + b.purchase, 0)
|
||||||
|
.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Silver icon"
|
||||||
|
src={await iconPng(asset("icons/currencies/silver.webp"))}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Equipment:{" "}
|
||||||
|
{costs
|
||||||
|
.reduce((a, b) => a + b.equipment, 0)
|
||||||
|
.toLocaleString(interaction.locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{await Promise.all(
|
||||||
|
line.map(async (id, index) => {
|
||||||
|
const tank = awaitedTankDefinitions.tanks[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: theme.colors.appBackground2,
|
||||||
|
border: theme.borderStyles.subtle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
position: "relative",
|
||||||
|
right: "5%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Tank icon"
|
||||||
|
src={await tankIconPng(id)}
|
||||||
|
style={{
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
objectFit: "contain",
|
||||||
|
position: "absolute",
|
||||||
|
left: "25%",
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 8,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={unwrap(tank.name!)}
|
||||||
|
src={TANK_ICONS[tank.class]}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{unwrap(tank.name!)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 8,
|
||||||
|
padding: "16px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="XP icon"
|
||||||
|
src={await iconPng(asset("icons/currencies/xp.webp"))}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{literals(
|
||||||
|
strings.bot.commands.research.body.research,
|
||||||
|
{
|
||||||
|
cost: costs[index].research?.toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{costs[index].upgrades > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Free XP icon"
|
||||||
|
src={await iconPng(
|
||||||
|
asset("icons/currencies/free-xp.webp"),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{literals(
|
||||||
|
strings.bot.commands.research.body.upgrades,
|
||||||
|
{ cost: costs[index].upgrades.toLocaleString() },
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Silver icon"
|
||||||
|
src={await iconPng(
|
||||||
|
asset("icons/currencies/silver.webp"),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{literals(
|
||||||
|
strings.bot.commands.research.body.purchase,
|
||||||
|
{
|
||||||
|
cost: costs[index].purchase.toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Silver icon"
|
||||||
|
src={await iconPng(
|
||||||
|
asset("icons/currencies/silver.webp"),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{literals(
|
||||||
|
strings.bot.commands.research.body.equipment,
|
||||||
|
{
|
||||||
|
cost: costs[index].equipment.toLocaleString(
|
||||||
|
interaction.locale,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
return startingTankRaw
|
||||||
|
? image
|
||||||
|
: [strings.bot.commands.research.body.estimation, image];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete(interaction) {
|
||||||
|
autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { type ClanList, fetchBlitz, type Region } from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import markdownEscape from "markdown-escape";
|
||||||
|
import { addClanChoices } from "../core/discord/addClanChoices";
|
||||||
|
import { addRegionChoices } from "../core/discord/addRegionChoices";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { embedInfo } from "../core/discord/embedInfo";
|
||||||
|
import { localizationObject } from "../core/discord/localizationObject";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT = 25;
|
||||||
|
|
||||||
|
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("search-clans")
|
||||||
|
.addStringOption(addRegionChoices)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
addClanChoices(option).setAutocomplete(false)
|
||||||
|
)
|
||||||
|
.addIntegerOption((option) => {
|
||||||
|
const { strings } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.setName(strings.bot.commands.search_clans.options.limit.name)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) => strings.bot.commands.search_clans.options.limit.name,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
literals(
|
||||||
|
strings.bot.commands.search_clans.options.limit.description,
|
||||||
|
{ limit: DEFAULT_LIMIT }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.search_clans.options.limit.description,
|
||||||
|
{ limit: DEFAULT_LIMIT }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(100);
|
||||||
|
}),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
const region = 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>(region, "clans/list", {
|
||||||
|
search: clan,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return embedInfo(
|
||||||
|
literals(strings.bot.commands.search_clans.body.title, {
|
||||||
|
search: markdownEscape(clan),
|
||||||
|
region: strings.common.regions.normal[region],
|
||||||
|
}),
|
||||||
|
clanList.length === 0
|
||||||
|
? strings.bot.commands.search_clans.body.no_results
|
||||||
|
: `\`\`\`\n${clanList
|
||||||
|
.map((clan) => `${clan.name} [${clan.tag}]`)
|
||||||
|
.join("\n")}\n\`\`\``
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
type AccountList,
|
||||||
|
fetchBlitz,
|
||||||
|
type Region,
|
||||||
|
usernamePattern,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import markdownEscape from "markdown-escape";
|
||||||
|
import { addRegionChoices } from "../core/discord/addRegionChoices";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { embedInfo } from "../core/discord/embedInfo";
|
||||||
|
import { localizationObject } from "../core/discord/localizationObject";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT = 25;
|
||||||
|
|
||||||
|
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
|
||||||
|
const { strings } = translator(Locale.EnglishUS);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
command: createLocalizedCommand("search-players")
|
||||||
|
.addStringOption(addRegionChoices)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
addUsernameChoices(option).setAutocomplete(false).setRequired(true)
|
||||||
|
)
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName(strings.bot.commands.search_players.options.limit.name)
|
||||||
|
.setNameLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.search_players.options.limit.name,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
literals(
|
||||||
|
strings.bot.commands.search_players.options.limit.description,
|
||||||
|
{ limit: DEFAULT_LIMIT }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setDescriptionLocalizations(
|
||||||
|
localizationObject(
|
||||||
|
(strings) =>
|
||||||
|
strings.bot.commands.search_players.options.limit.description,
|
||||||
|
{ limit: DEFAULT_LIMIT }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(100)
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
const region = 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>(region, "account/list", {
|
||||||
|
search: trimmedSearch,
|
||||||
|
limit,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return embedInfo(
|
||||||
|
literals(strings.bot.commands.search_players.body.title, {
|
||||||
|
search: markdownEscape(trimmedSearch),
|
||||||
|
region: strings.common.regions.normal[region],
|
||||||
|
}),
|
||||||
|
`\`\`\`${
|
||||||
|
players.length === 0
|
||||||
|
? strings.bot.commands.search_players.body.no_results
|
||||||
|
: players.map((player) => player.nickname).join("\n")
|
||||||
|
}\`\`\``
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import { AllStatsOverview } from "../components/AllStatsOverview";
|
||||||
|
import { CommandWrapper } from "../components/CommandWrapper";
|
||||||
|
import { NoData } from "../components/NoData";
|
||||||
|
import { TitleBar } from "../components/TitleBar";
|
||||||
|
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
|
||||||
|
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
|
||||||
|
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
|
||||||
|
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 { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
|
||||||
|
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
|
||||||
|
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
|
||||||
|
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
|
||||||
|
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import type { CommandRegistry } from "../events/interactionCreate";
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
locale: Locale,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
|
||||||
|
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
|
||||||
|
const diffedTankStats = await getStatsInPeriod(
|
||||||
|
region,
|
||||||
|
id,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
locale,
|
||||||
|
);
|
||||||
|
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters, locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandWrapper>
|
||||||
|
<TitleBar
|
||||||
|
title={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!stats.battles && <NoData type="battles_in_period" locale={locale} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<AllStatsOverview
|
||||||
|
locale={locale}
|
||||||
|
stats={stats}
|
||||||
|
supplementaryStats={supplementary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommandWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
|
resolve({
|
||||||
|
command: await addPeriodicFilterOptions(
|
||||||
|
createLocalizedCommand("stats"),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
return strings.bot.commands.dead;
|
||||||
|
|
||||||
|
// 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 Promise.all([
|
||||||
|
// render(player, period, filters, interaction.locale),
|
||||||
|
// buttonRefresh(interaction, path),
|
||||||
|
// 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 render(player, period, filters, interaction.locale);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { gameDefinitions } from "../core/blitzkit/nonBlockingPromises";
|
||||||
|
import { addFilterOptions } from "../core/discord/addFilterOptions";
|
||||||
|
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
|
||||||
|
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
|
||||||
|
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
|
||||||
|
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
|
||||||
|
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
|
||||||
|
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
|
||||||
|
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
|
||||||
|
import { translator } from "../core/localization/translator";
|
||||||
|
import { type CommandRegistry } from "../events/interactionCreate";
|
||||||
|
import { renderBreakdown } from "./breakdown/render";
|
||||||
|
|
||||||
|
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
|
||||||
|
const awaitedGameDefinitions = await gameDefinitions;
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
command: addFilterOptions(
|
||||||
|
createLocalizedCommand("today"),
|
||||||
|
awaitedGameDefinitions.nations,
|
||||||
|
).addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { strings } = translator(interaction.locale);
|
||||||
|
return strings.bot.commands.dead;
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { type ComponentProps } from "react";
|
||||||
|
|
||||||
|
export interface GlowProps extends ComponentProps<"div"> {
|
||||||
|
color: string;
|
||||||
|
direction?: "default" | "reverse";
|
||||||
|
rotation?: number;
|
||||||
|
startOpacity?: number;
|
||||||
|
endOpacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = 16;
|
||||||
|
|
||||||
|
export function Glow({
|
||||||
|
color,
|
||||||
|
direction = "default",
|
||||||
|
rotation = 0,
|
||||||
|
style,
|
||||||
|
startOpacity = 0,
|
||||||
|
endOpacity = 1,
|
||||||
|
...props
|
||||||
|
}: GlowProps) {
|
||||||
|
const color1 = `${color}${Math.round(endOpacity * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
const color2 = `${color}${Math.round(startOpacity * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
const background = `linear-gradient(${
|
||||||
|
direction === "default" ? 0 : 180
|
||||||
|
}deg, ${color1} 0%, ${color2} 100%)`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: direction === "default" ? "flex-end" : "flex-start",
|
||||||
|
justifyContent: "center",
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div style={{ height: 54, width, background }} />
|
||||||
|
<div style={{ height: 72, width, background }} />
|
||||||
|
<div style={{ height: 80, width, background }} />
|
||||||
|
<div style={{ height: 72, width, background }} />
|
||||||
|
<div style={{ height: 54, width, background }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { theme } from '../../../../stitches.config';
|
||||||
|
import { Glow } from './components/Glow';
|
||||||
|
|
||||||
|
export interface WN8DisplayProps {
|
||||||
|
subtitle: string;
|
||||||
|
color: string;
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroStat({ color, subtitle, value }: WN8DisplayProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 128 * 2, // idk why twice the width turns into the correct width
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glow color={color} style={{ marginBottom: -16 }} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 128,
|
||||||
|
height: 80,
|
||||||
|
backgroundColor: color,
|
||||||
|
borderRadius: 40,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
fontWeight: 900,
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
lineHeight: '26px',
|
||||||
|
letterSpacing: '-0.03em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: '16px',
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Glow color={color} direction="reverse" style={{ marginTop: -16 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
|
export enum WN8SurroundingStatAlign {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WN8SurroundingStatProps {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
align?: WN8SurroundingStatAlign;
|
||||||
|
padded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WN8SurroundingStat({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
align = WN8SurroundingStatAlign.Left,
|
||||||
|
padded = false,
|
||||||
|
}: WN8SurroundingStatProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems:
|
||||||
|
align === WN8SurroundingStatAlign.Left ? 'flex-start' : 'flex-end',
|
||||||
|
paddingLeft: padded && align === WN8SurroundingStatAlign.Left ? 32 : 0,
|
||||||
|
paddingRight:
|
||||||
|
padded && align === WN8SurroundingStatAlign.Right ? 32 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
getWN8Percentile,
|
||||||
|
Percentile,
|
||||||
|
type SupplementaryStats,
|
||||||
|
} from '@blitzkit/core';
|
||||||
|
import { Locale } from 'discord.js';
|
||||||
|
import { translator } from '../../core/localization/translator';
|
||||||
|
import { PERCENTILE_COLORS } from '../PercentileIndicator/constants';
|
||||||
|
import { HeroStat } from './components/HeroStat';
|
||||||
|
import {
|
||||||
|
WN8SurroundingStat,
|
||||||
|
WN8SurroundingStatAlign,
|
||||||
|
} from './components/WN8SurroundingStat';
|
||||||
|
|
||||||
|
export interface AllStatsOverviewProps {
|
||||||
|
stats: {
|
||||||
|
battles: number;
|
||||||
|
wins: number;
|
||||||
|
damage_dealt: number;
|
||||||
|
damage_received: number;
|
||||||
|
survived_battles: number;
|
||||||
|
hits: number;
|
||||||
|
shots: number;
|
||||||
|
frags: number;
|
||||||
|
};
|
||||||
|
supplementaryStats: SupplementaryStats;
|
||||||
|
locale: Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AllStatsOverview({
|
||||||
|
stats,
|
||||||
|
locale,
|
||||||
|
supplementaryStats,
|
||||||
|
}: AllStatsOverviewProps) {
|
||||||
|
const { strings } = translator(locale);
|
||||||
|
const percentile =
|
||||||
|
typeof supplementaryStats.WN8 === 'number'
|
||||||
|
? getWN8Percentile(supplementaryStats.WN8)
|
||||||
|
: Percentile.VeryBad;
|
||||||
|
const color = PERCENTILE_COLORS[percentile];
|
||||||
|
|
||||||
|
const heroStat =
|
||||||
|
typeof supplementaryStats.WN8 === 'number'
|
||||||
|
? supplementaryStats.WN8.toFixed(0)
|
||||||
|
: '--';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: -64,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.battles}
|
||||||
|
value={stats.battles.toLocaleString()}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.winrate}
|
||||||
|
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.damage}
|
||||||
|
value={Math.round(
|
||||||
|
stats.damage_dealt / stats.battles,
|
||||||
|
).toLocaleString()}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.damage_ratio}
|
||||||
|
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
|
||||||
|
align={WN8SurroundingStatAlign.Right}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HeroStat
|
||||||
|
value={heroStat}
|
||||||
|
subtitle={strings.common.wn8_percentile[percentile]}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.tier}
|
||||||
|
value={
|
||||||
|
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.survival}
|
||||||
|
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
||||||
|
2,
|
||||||
|
)}%`}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.accuracy}
|
||||||
|
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
|
||||||
|
padded
|
||||||
|
/>
|
||||||
|
<WN8SurroundingStat
|
||||||
|
label={strings.bot.common.hero_stats.kills}
|
||||||
|
value={(stats.frags / stats.battles).toFixed(2)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface RootProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Root({ children }: RootProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="session-tracker-root"
|
||||||
|
style={{ display: "flex", flexDirection: "column", gap: 8 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
Percentile,
|
||||||
|
TankClass,
|
||||||
|
TankType,
|
||||||
|
TREE_TYPE_ICONS,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { createColors } from "bepaint";
|
||||||
|
import {
|
||||||
|
type AccentColor,
|
||||||
|
type GrayColor,
|
||||||
|
PALETTES,
|
||||||
|
} from "../../../core/radix/radixColors";
|
||||||
|
import { RowStat } from "./RowStat";
|
||||||
|
|
||||||
|
export interface RowStatItem {
|
||||||
|
title: string;
|
||||||
|
current?: number | string;
|
||||||
|
career?: number | string;
|
||||||
|
delta?: number;
|
||||||
|
percentile?: Percentile;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RowProps {
|
||||||
|
displayType?: "tank" | "summary";
|
||||||
|
naked?: boolean;
|
||||||
|
title: string;
|
||||||
|
minimized?: boolean;
|
||||||
|
type?: TankType;
|
||||||
|
class?: TankClass;
|
||||||
|
color?: AccentColor | GrayColor;
|
||||||
|
stats: (RowStatItem | undefined)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Row({
|
||||||
|
color = "slate",
|
||||||
|
stats,
|
||||||
|
title,
|
||||||
|
minimized,
|
||||||
|
class: tankClass,
|
||||||
|
type,
|
||||||
|
naked = false,
|
||||||
|
displayType,
|
||||||
|
}: RowProps) {
|
||||||
|
const palette = PALETTES[`${color}Dark`];
|
||||||
|
const theme = createColors(palette);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="session-tracker-row"
|
||||||
|
style={{
|
||||||
|
maxWidth: naked ? 600 : Infinity,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: naked
|
||||||
|
? `${theme.appBackground2}c0`
|
||||||
|
: theme.appBackground2,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: 8,
|
||||||
|
gap: 4,
|
||||||
|
backgroundColor: naked
|
||||||
|
? `${theme.componentInteractive}c0`
|
||||||
|
: theme.componentInteractive,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayType === "tank" && tankClass !== undefined && (
|
||||||
|
<img
|
||||||
|
alt={TankClass[tankClass]}
|
||||||
|
src={
|
||||||
|
TREE_TYPE_ICONS[type ?? TankType.TANK_TYPE_RESEARCHABLE][
|
||||||
|
tankClass
|
||||||
|
]
|
||||||
|
}
|
||||||
|
style={{ width: 16, height: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
displayType === "tank"
|
||||||
|
? type === TankType.TANK_TYPE_COLLECTOR
|
||||||
|
? theme.textLowContrast_blue
|
||||||
|
: type === TankType.TANK_TYPE_PREMIUM
|
||||||
|
? theme.textLowContrast_amber
|
||||||
|
: theme.textHighContrast
|
||||||
|
: theme.textLowContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stats.map(
|
||||||
|
(row, index) =>
|
||||||
|
row && (
|
||||||
|
<RowStat
|
||||||
|
minimized={minimized}
|
||||||
|
key={index}
|
||||||
|
name={`${row.title} • ${row.career ?? "--"}`}
|
||||||
|
value={row.current ?? "--"}
|
||||||
|
delta={row.delta}
|
||||||
|
percentile={row.percentile}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Percentile } from "@blitzkit/core";
|
||||||
|
import { createColors } from "bepaint";
|
||||||
|
import {
|
||||||
|
type AccentColor,
|
||||||
|
type GrayColor,
|
||||||
|
PALETTES,
|
||||||
|
} from "../../../core/radix/radixColors";
|
||||||
|
import { PercentileIndicator } from "../../PercentileIndicator";
|
||||||
|
|
||||||
|
interface RowStatProps {
|
||||||
|
name: string;
|
||||||
|
value?: string | number;
|
||||||
|
delta?: number;
|
||||||
|
percentile?: Percentile;
|
||||||
|
minimized?: boolean;
|
||||||
|
color?: AccentColor | GrayColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RowStat({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
delta,
|
||||||
|
percentile,
|
||||||
|
minimized = false,
|
||||||
|
color = "slate",
|
||||||
|
}: RowStatProps) {
|
||||||
|
const theme = createColors(PALETTES[`${color}Dark`]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="session-tracker-row-stat"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{percentile === undefined && delta !== undefined && delta !== 0 && (
|
||||||
|
<img
|
||||||
|
alt={delta > 0 ? "Increase" : "Decrease"}
|
||||||
|
src={
|
||||||
|
(delta ?? 0) > 0
|
||||||
|
? "https://i.imgur.com/qbjiXa1.png"
|
||||||
|
: "https://i.imgur.com/3uyNhun.png"
|
||||||
|
}
|
||||||
|
style={{ width: 12, height: 12 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{percentile !== undefined && (
|
||||||
|
<PercentileIndicator percentile={percentile} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.textHighContrast,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!minimized && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.textLowContrast,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './components/Root';
|
||||||
|
export * from './components/Row';
|
||||||
|
export * from './components/RowStat';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
import { theme } from "../stitches.config";
|
||||||
|
|
||||||
|
interface CommandWrapperProps {
|
||||||
|
children: ReactNode;
|
||||||
|
fat?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommandWrapper({ children, fat }: CommandWrapperProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: `16px ${fat ? 0 : 16}px`,
|
||||||
|
gap: 16,
|
||||||
|
width: "100%",
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
backgroundColor: theme.colors.appBackground1,
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
interface DeltaCaretProps {
|
||||||
|
delta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeltaCaret({ delta }: DeltaCaretProps) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt={delta > 0 ? 'Increase' : 'Decrease'}
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
src={
|
||||||
|
delta > 0
|
||||||
|
? 'https://i.imgur.com/gjI4a5f.png'
|
||||||
|
: 'https://i.imgur.com/lLGZygN.png'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { asset } from '@blitzkit/core';
|
||||||
|
|
||||||
|
interface FlagProps {
|
||||||
|
nation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Flag({ nation }: FlagProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img alt={nation} src={asset(`flags/circle/${nation}.webp`)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Percentile } from '@blitzkit/core';
|
||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
import { PercentileIndicator } from '../../PercentileIndicator';
|
||||||
|
|
||||||
|
interface CardItem {
|
||||||
|
label: string;
|
||||||
|
value: string | number | undefined;
|
||||||
|
percentile?: Percentile;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
items: CardItem[];
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({ items, title }: CardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: theme.colors.componentInteractive,
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontSize: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
borderRadius: 0.5,
|
||||||
|
backgroundColor: theme.colors.borderNonInteractive,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.percentile !== undefined && (
|
||||||
|
<PercentileIndicator percentile={item.percentile} />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.value === undefined || Number.isNaN(item.value)
|
||||||
|
? '--'
|
||||||
|
: item.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
interface RootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Root({ children }: RootProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
interface RowProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Row({ children }: RowProps) {
|
||||||
|
return <div style={{ display: 'flex', gap: 8 }}>{children}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import {
|
||||||
|
type BlitzStats,
|
||||||
|
getWN8Percentile,
|
||||||
|
isNumber,
|
||||||
|
type SupplementaryStats,
|
||||||
|
} from '@blitzkit/core';
|
||||||
|
import { Locale } from 'discord.js';
|
||||||
|
import { translator } from '../../core/localization/translator';
|
||||||
|
import { Card } from './components/Card';
|
||||||
|
import { Root } from './components/Root';
|
||||||
|
import { Row } from './components/Row';
|
||||||
|
|
||||||
|
interface GenericAllStatsProps {
|
||||||
|
stats: BlitzStats;
|
||||||
|
supplementaryStats?: SupplementaryStats;
|
||||||
|
locale: Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenericAllStats({
|
||||||
|
stats,
|
||||||
|
supplementaryStats,
|
||||||
|
locale,
|
||||||
|
}: GenericAllStatsProps) {
|
||||||
|
const { strings } = translator(locale);
|
||||||
|
const damageRatio = stats.damage_dealt / stats.damage_received;
|
||||||
|
const killsToDeathRatio =
|
||||||
|
stats.frags / (stats.battles - stats.survived_battles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<Row>
|
||||||
|
<Card
|
||||||
|
title={strings.bot.commands.full_stats.body.cards.games.title}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: strings.bot.commands.full_stats.body.cards.games.winrate,
|
||||||
|
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: strings.bot.commands.full_stats.body.cards.games.battles,
|
||||||
|
value: stats.battles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.games.wins_losses,
|
||||||
|
value: `${stats.wins} / ${stats.losses}`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title={strings.bot.commands.full_stats.body.cards.efficiency.title}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: strings.bot.commands.full_stats.body.cards.efficiency.wn8,
|
||||||
|
value:
|
||||||
|
supplementaryStats && isNumber(supplementaryStats.WN8)
|
||||||
|
? supplementaryStats.WN8!.toFixed(0)
|
||||||
|
: undefined,
|
||||||
|
percentile: isNumber(supplementaryStats?.WN8)
|
||||||
|
? getWN8Percentile(supplementaryStats!.WN8!)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.efficiency.survival,
|
||||||
|
value: `${(
|
||||||
|
100 *
|
||||||
|
(stats.survived_battles / stats.battles)
|
||||||
|
).toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.efficiency.accuracy,
|
||||||
|
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Card
|
||||||
|
title={strings.bot.commands.full_stats.body.cards.lethality.title}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.lethality
|
||||||
|
.average_damage,
|
||||||
|
value: (stats.damage_dealt / stats.battles).toFixed(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.lethality
|
||||||
|
.damage_per_tier,
|
||||||
|
value: supplementaryStats?.tier
|
||||||
|
? (
|
||||||
|
stats.damage_dealt /
|
||||||
|
stats.battles /
|
||||||
|
supplementaryStats.tier
|
||||||
|
).toFixed(0)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.lethality
|
||||||
|
.damage_ratio,
|
||||||
|
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.lethality
|
||||||
|
.average_kills,
|
||||||
|
value: (stats.frags / stats.battles).toFixed(0),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title={strings.bot.commands.full_stats.body.cards.miscellaneous.title}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.miscellaneous
|
||||||
|
.average_tier,
|
||||||
|
value: supplementaryStats?.tier?.toFixed(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.miscellaneous
|
||||||
|
.average_spots,
|
||||||
|
value: (stats.spotted / stats.battles).toFixed(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.miscellaneous
|
||||||
|
.average_xp,
|
||||||
|
value: (stats.xp / stats.battles).toFixed(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
strings.bot.commands.full_stats.body.cards.miscellaneous
|
||||||
|
.kills_to_death_ratio,
|
||||||
|
value: isFinite(killsToDeathRatio)
|
||||||
|
? killsToDeathRatio.toFixed(2)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { type GenericStat } from "..";
|
||||||
|
import { GenericStatsRow } from "./GenericStatsRow";
|
||||||
|
|
||||||
|
interface GenericStatsColumnProps {
|
||||||
|
stats: GenericStat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<GenericStatsRow key={stat[0]} stat={stat} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { type GenericStat } from "..";
|
||||||
|
import { theme } from "../../../stitches.config";
|
||||||
|
import { PercentileIndicator } from "../../PercentileIndicator";
|
||||||
|
|
||||||
|
interface GenericStatsRowProps {
|
||||||
|
stat: GenericStat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
maxWidth: 128,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stat[0]}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stat[2] !== undefined && <PercentileIndicator percentile={stat[2]} />}
|
||||||
|
{stat[1]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Percentile } from '@blitzkit/core';
|
||||||
|
import { GenericStatsColumn } from './components/GenericStatsColumn';
|
||||||
|
|
||||||
|
export type GenericStat =
|
||||||
|
| [string, string | number | undefined]
|
||||||
|
| [string, string | number | undefined, Percentile | undefined];
|
||||||
|
|
||||||
|
export interface GenericStatsProps {
|
||||||
|
stats: GenericStat[];
|
||||||
|
columnCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenericStats({ stats, columnCount = 2 }: GenericStatsProps) {
|
||||||
|
const filteredStats = stats.filter(([, value]) => value !== undefined);
|
||||||
|
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
|
||||||
|
const columns: GenericStat[][] = [];
|
||||||
|
|
||||||
|
filteredStats.forEach((stat, index) => {
|
||||||
|
const column = Math.floor(index / itemsPerRow);
|
||||||
|
if (!columns[column]) columns[column] = [];
|
||||||
|
columns[column].push(stat);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: 32 }}>
|
||||||
|
{columns.map((stats) => (
|
||||||
|
<GenericStatsColumn
|
||||||
|
key={stats.map((stat) => stat[0]).join('-')}
|
||||||
|
stats={stats}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export enum LineColor {
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Blue,
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
export enum LineStyle {
|
||||||
|
Solid,
|
||||||
|
Dashes,
|
||||||
|
DashesAndDots,
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { blue, green, red, slateDark } from '@radix-ui/colors';
|
||||||
|
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
|
||||||
|
import { LineColor, LineStyle } from './constants';
|
||||||
|
|
||||||
|
const LINE_COLORS = {
|
||||||
|
[LineColor.Red]: red.red9,
|
||||||
|
[LineColor.Green]: green.green9,
|
||||||
|
[LineColor.Blue]: blue.blue9,
|
||||||
|
[LineColor.White]: slateDark.slate11,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LINE_STYLES = {
|
||||||
|
[LineStyle.Solid]: undefined,
|
||||||
|
[LineStyle.Dashes]: '8, 8',
|
||||||
|
[LineStyle.DashesAndDots]: '1, 4, 8, 4',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlotItem = [number, number];
|
||||||
|
type Plot = PlotItem[];
|
||||||
|
|
||||||
|
interface LineProps {
|
||||||
|
plot: Plot;
|
||||||
|
minY?: number;
|
||||||
|
maxY?: number;
|
||||||
|
color?: LineColor;
|
||||||
|
style?: LineStyle;
|
||||||
|
lineWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Line({
|
||||||
|
plot,
|
||||||
|
minY: providedMinY,
|
||||||
|
maxY: providedMaxY,
|
||||||
|
color = LineColor.Red,
|
||||||
|
style = LineStyle.Solid,
|
||||||
|
lineWidth = 2,
|
||||||
|
}: LineProps) {
|
||||||
|
const xs = plot.map(([x]) => x);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
const maxX = Math.max(...xs);
|
||||||
|
const ys = plot.map(([, y]) => y);
|
||||||
|
const minY = providedMinY ?? Math.min(...ys);
|
||||||
|
const maxY = providedMaxY ?? Math.max(...ys);
|
||||||
|
const rangeX = maxX - minX;
|
||||||
|
const rangeY = maxY - minY;
|
||||||
|
const points = plot
|
||||||
|
.map(
|
||||||
|
([x, y]) =>
|
||||||
|
`${(x - minX) * (GRAPH_WIDTH / rangeX)},${
|
||||||
|
GRAPH_HEIGHT - (y - minY) * (GRAPH_HEIGHT / rangeY)
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<polyline
|
||||||
|
vector-effect="non-scaling-stroke"
|
||||||
|
fill="none"
|
||||||
|
stroke={LINE_COLORS[color]}
|
||||||
|
strokeWidth={lineWidth}
|
||||||
|
stroke-dasharray={LINE_STYLES[style]}
|
||||||
|
points={points}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum MarginOrientation {
|
||||||
|
Vertical,
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { times } from 'lodash-es';
|
||||||
|
import { theme } from '../../../../stitches.config';
|
||||||
|
import { type MarginInputProps } from '../Root';
|
||||||
|
import { MarginOrientation } from './constants';
|
||||||
|
|
||||||
|
interface MarginProps extends MarginInputProps {
|
||||||
|
verticalSeparations: number;
|
||||||
|
orientation?: MarginOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Margin({
|
||||||
|
verticalSeparations,
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
suffix,
|
||||||
|
precision = 0,
|
||||||
|
orientation = MarginOrientation.Vertical,
|
||||||
|
}: MarginProps) {
|
||||||
|
const isVertical = orientation === MarginOrientation.Vertical;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: isVertical ? 0 : 64,
|
||||||
|
width: isVertical ? 48 : '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: isVertical ? 'column' : 'row-reverse',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{times(verticalSeparations, (index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${(
|
||||||
|
(max - min) *
|
||||||
|
((verticalSeparations - (index + 1)) /
|
||||||
|
(verticalSeparations - 1)) +
|
||||||
|
min
|
||||||
|
).toFixed(precision)}${suffix ?? ''}`}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const GRAPH_WIDTH = 384;
|
||||||
|
export const GRAPH_HEIGHT = 320;
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { times } from "lodash-es";
|
||||||
|
import { type ReactNode } from "react";
|
||||||
|
import { theme } from "../../../../stitches.config";
|
||||||
|
import { Margin } from "../Margin";
|
||||||
|
import { MarginOrientation } from "../Margin/constants";
|
||||||
|
import { GRAPH_HEIGHT, GRAPH_WIDTH } from "./constants";
|
||||||
|
|
||||||
|
export interface MarginInputProps {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
suffix?: string;
|
||||||
|
precision?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RootProps {
|
||||||
|
children: ReactNode;
|
||||||
|
verticalMargin?: MarginInputProps;
|
||||||
|
horizontalMargin?: MarginInputProps;
|
||||||
|
separations?: number;
|
||||||
|
xMinLabel: string;
|
||||||
|
xMaxLabel: string;
|
||||||
|
xTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Root({
|
||||||
|
children,
|
||||||
|
verticalMargin,
|
||||||
|
horizontalMargin,
|
||||||
|
separations = 5,
|
||||||
|
xMinLabel,
|
||||||
|
xMaxLabel,
|
||||||
|
xTitle,
|
||||||
|
}: RootProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: 64,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
|
||||||
|
{xMinLabel}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
|
||||||
|
{xMaxLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: 16 }}>
|
||||||
|
{verticalMargin && (
|
||||||
|
<Margin {...verticalMargin} verticalSeparations={separations} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<svg
|
||||||
|
viewBox={`0 0 ${GRAPH_WIDTH} ${GRAPH_HEIGHT}`}
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.colors.appBackground2,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: theme.borderStyles.nonInteractive,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{times(separations - 2, (index) => {
|
||||||
|
const y = GRAPH_HEIGHT * ((index + 1) / (separations - 1));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<polyline
|
||||||
|
key={index}
|
||||||
|
vector-effect="non-scaling-stroke"
|
||||||
|
fill="none"
|
||||||
|
stroke={theme.colors.borderNonInteractive}
|
||||||
|
strokeWidth="2"
|
||||||
|
points={`0,${y}, ${GRAPH_WIDTH},${y}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{horizontalMargin && (
|
||||||
|
<Margin
|
||||||
|
{...horizontalMargin}
|
||||||
|
verticalSeparations={separations}
|
||||||
|
orientation={MarginOrientation.Horizontal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{xTitle !== undefined && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: 64,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||||
|
{xTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './components/Line';
|
||||||
|
export * from './components/Margin';
|
||||||
|
export * from './components/Root';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
|
interface GapProps {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Gap({ message: number }: GapProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
padding: 8,
|
||||||
|
backgroundColor: theme.colors.componentNonInteractive,
|
||||||
|
borderRadius: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{number.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { type ComponentProps } from "react";
|
||||||
|
import { theme } from "../../../stitches.config";
|
||||||
|
|
||||||
|
interface ItemProps extends ComponentProps<"div"> {
|
||||||
|
position: number;
|
||||||
|
deltaPosition?: number;
|
||||||
|
score?: number;
|
||||||
|
deltaScore?: number;
|
||||||
|
nickname: string;
|
||||||
|
clan?: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Item({
|
||||||
|
position,
|
||||||
|
deltaPosition,
|
||||||
|
score,
|
||||||
|
deltaScore,
|
||||||
|
nickname,
|
||||||
|
clan,
|
||||||
|
highlight,
|
||||||
|
...props
|
||||||
|
}: ItemProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
padding: 8,
|
||||||
|
backgroundColor: highlight
|
||||||
|
? theme.colors.componentInteractive_blue
|
||||||
|
: theme.colors.componentInteractive,
|
||||||
|
borderRadius: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{position.toLocaleString()}.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textHighContrast_blue
|
||||||
|
: theme.colors.textHighContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nickname}
|
||||||
|
</span>
|
||||||
|
{clan && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
[{clan}]
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deltaPosition !== undefined && deltaPosition !== 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={deltaPosition > 0 ? "Increase" : "Decrease"}
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
}}
|
||||||
|
src={
|
||||||
|
deltaPosition > 0
|
||||||
|
? "https://i.imgur.com/qbjiXa1.png"
|
||||||
|
: "https://i.imgur.com/3uyNhun.png"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.abs(deltaPosition).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TODO: lol remove this spacer div for a better solution */}
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
width: 96,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.abs(deltaScore).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||||
|
<img
|
||||||
|
alt={deltaScore > 0 ? "Increase" : "Decrease"}
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
}}
|
||||||
|
src={
|
||||||
|
deltaScore > 0
|
||||||
|
? "https://i.imgur.com/qbjiXa1.png"
|
||||||
|
: "https://i.imgur.com/3uyNhun.png"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{score !== undefined && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textHighContrast_blue
|
||||||
|
: theme.colors.textHighContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{score.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface RootProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Root({ children }: RootProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './components/Gap';
|
||||||
|
export * from './components/Item';
|
||||||
|
export * from './components/Root';
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum NoDataType {
|
||||||
|
BattlesInPeriod,
|
||||||
|
PlayersInPeriod,
|
||||||
|
TanksFound,
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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'
|
||||||
|
| 'battles_in_season';
|
||||||
|
locale: Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NoData({ type, locale }: NoDataProps) {
|
||||||
|
const { strings } = translator(locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||||
|
{strings.bot.common.no_data[type]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Percentile } from '@blitzkit/core';
|
||||||
|
import {
|
||||||
|
amber,
|
||||||
|
blue,
|
||||||
|
green,
|
||||||
|
lime,
|
||||||
|
orange,
|
||||||
|
pink,
|
||||||
|
purple,
|
||||||
|
red,
|
||||||
|
slate,
|
||||||
|
teal,
|
||||||
|
} from '@radix-ui/colors';
|
||||||
|
|
||||||
|
export const PERCENTILE_COLORS = {
|
||||||
|
[Percentile.VeryBad]: slate.slate9,
|
||||||
|
[Percentile.Bad]: red.red9,
|
||||||
|
[Percentile.BelowAverage]: orange.orange9,
|
||||||
|
[Percentile.Average]: amber.amber10,
|
||||||
|
[Percentile.AboveAverage]: lime.lime8,
|
||||||
|
[Percentile.Good]: green.green9,
|
||||||
|
[Percentile.VeryGood]: teal.teal9,
|
||||||
|
[Percentile.Great]: blue.blue9,
|
||||||
|
[Percentile.Unicum]: pink.pink9,
|
||||||
|
[Percentile.SuperUnicum]: purple.purple9,
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Percentile } from '@blitzkit/core';
|
||||||
|
import { PERCENTILE_COLORS } from './constants';
|
||||||
|
|
||||||
|
interface PercentileIndicatorProps {
|
||||||
|
percentile: Percentile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PercentileIndicator({ percentile }: PercentileIndicatorProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: PERCENTILE_COLORS[percentile],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ColumnProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Column({ children }: ColumnProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { TankClass, TankType, TREE_TYPE_ICONS } from "@blitzkit/core";
|
||||||
|
import { theme } from "../../../../stitches.config";
|
||||||
|
|
||||||
|
export interface ItemProps {
|
||||||
|
tankClass?: TankClass;
|
||||||
|
name: string;
|
||||||
|
tankType: TankType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TREE_TYPE_COLOR = {
|
||||||
|
[TankType.TANK_TYPE_RESEARCHABLE]: "",
|
||||||
|
[TankType.TANK_TYPE_PREMIUM]: "_amber",
|
||||||
|
[TankType.TANK_TYPE_COLLECTOR]: "_blue",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function Item({
|
||||||
|
tankClass: tankType,
|
||||||
|
name,
|
||||||
|
tankType: treeType,
|
||||||
|
}: ItemProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
height: 32,
|
||||||
|
paddingLeft: 8,
|
||||||
|
paddingRight: 8,
|
||||||
|
overflow: "hidden",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor:
|
||||||
|
theme.colors[`componentInteractive${TREE_TYPE_COLOR[treeType]}`],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 4,
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tankType && (
|
||||||
|
<img
|
||||||
|
alt={TankType[tankType]}
|
||||||
|
src={TREE_TYPE_ICONS[treeType][tankType]}
|
||||||
|
style={{ width: 14, height: 14 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
color: theme.colors[`textLowContrast${TREE_TYPE_COLOR[treeType]}`],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* {image && (
|
||||||
|
// TODO: remove hardcoded dimensions when satori fixes non-width unloadable images
|
||||||
|
<img
|
||||||
|
alt={name}
|
||||||
|
src={image}
|
||||||
|
width={106}
|
||||||
|
height={32}
|
||||||
|
style={{ objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface RootProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Root({ children }: RootProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface RowProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Row({ children }: RowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
|
interface TitleProps {
|
||||||
|
children: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Title({ children }: TitleProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: 900,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontSize: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './components/Column';
|
||||||
|
export * from './components/Item';
|
||||||
|
export * from './components/Root';
|
||||||
|
export * from './components/Row';
|
||||||
|
export * from './components/Title';
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { TierWeightsRecord } from "@blitzkit/core";
|
||||||
|
import { theme } from "../stitches.config";
|
||||||
|
|
||||||
|
interface TierWeightsProps {
|
||||||
|
weights: TierWeightsRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TierWeights({ weights }: TierWeightsProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
|
{Object.entries(weights).map(([tier, weight]) => (
|
||||||
|
<div
|
||||||
|
key={tier}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.colors.appBackground1_light,
|
||||||
|
flex: weight,
|
||||||
|
color: theme.colors.textHighContrast_light,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 16,
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: 8,
|
||||||
|
minWidth: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tier}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
interface TitleBarProps {
|
||||||
|
title: string;
|
||||||
|
image?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TitleBar({ title, image, description }: TitleBarProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gap: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '8px 0px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gap: 4,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{image && (
|
||||||
|
<img
|
||||||
|
alt={title}
|
||||||
|
style={{ width: 32, height: 32, objectFit: 'contain' }}
|
||||||
|
src={image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: 900,
|
||||||
|
background: `linear-gradient(180deg, ${theme.colors.textHighContrast} 0%, ${theme.colors.textLowContrast} 100%)`,
|
||||||
|
backgroundClip: 'text',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function normalizeLeagueIcon(url: string) {
|
||||||
|
return url.startsWith('http') ? url : `https:${url}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { SEARCH_KEYS } from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { Locale } from "discord.js";
|
||||||
|
import { go } from "fuzzysort";
|
||||||
|
import markdownEscape from "markdown-escape";
|
||||||
|
import { tankDefinitions, tankNames } from "../blitzkit/nonBlockingPromises";
|
||||||
|
import { UserError } from "../blitzkit/userError";
|
||||||
|
import { tankNamesTechTreeOnly } from "../discord/autocompleteTanks";
|
||||||
|
import { translator } from "../localization/translator";
|
||||||
|
|
||||||
|
export async function resolveTankId(
|
||||||
|
tank: string | number,
|
||||||
|
locale: Locale,
|
||||||
|
techTreeOnly = false
|
||||||
|
) {
|
||||||
|
const { strings } = translator(locale);
|
||||||
|
const number = typeof tank === "string" ? Number(tank) : tank;
|
||||||
|
|
||||||
|
if (Number.isNaN(number)) {
|
||||||
|
const searchResult = go(
|
||||||
|
`${tank}`,
|
||||||
|
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
|
||||||
|
{ keys: SEARCH_KEYS, limit: 1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchResult.length === 0) {
|
||||||
|
throw new UserError(
|
||||||
|
literals(strings.bot.common.errors.tank_name_not_found, {
|
||||||
|
search: markdownEscape(`${tank}`),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return searchResult[0].obj.id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const awaitedTankDefinitions = await tankDefinitions;
|
||||||
|
|
||||||
|
if (awaitedTankDefinitions.tanks[number]) {
|
||||||
|
return number;
|
||||||
|
} else {
|
||||||
|
throw new UserError(
|
||||||
|
literals(strings.bot.common.errors.tank_id_not_found, {
|
||||||
|
search: number,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { searchClansAcrossRegions } from '@blitzkit/core';
|
||||||
|
import { Locale } from 'discord.js';
|
||||||
|
import { UserError } from '../blitzkit/userError';
|
||||||
|
import { translator } from '../localization/translator';
|
||||||
|
|
||||||
|
export async function searchClansAcrossRegionsBotWrapper(
|
||||||
|
search: string,
|
||||||
|
locale: Locale,
|
||||||
|
limit?: number,
|
||||||
|
) {
|
||||||
|
const { strings } = translator(locale);
|
||||||
|
const trimmed = search.trim();
|
||||||
|
|
||||||
|
if (trimmed.length < 3) {
|
||||||
|
throw new UserError(strings.bot.common.errors.query_too_small);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length > 100) {
|
||||||
|
throw new UserError(strings.bot.common.errors.query_too_long);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await searchClansAcrossRegions(trimmed, limit);
|
||||||
|
}
|
||||||
Loaded 100 of 781 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user