Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a92cbd951 | ||
|
|
54c5faf929 | ||
|
|
d1d9e9e4d1 | ||
|
|
5ca64f839e | ||
|
|
4e7cd6f681 | ||
|
|
ba9f593716 | ||
|
|
9d3d74c7c5 | ||
|
|
aae26523ff | ||
|
|
338a2a14bf | ||
|
|
1a66cab435 | ||
|
|
3363c9ab58 | ||
|
|
10f64902aa | ||
|
|
25e1f4caa4 | ||
|
|
222fa89422 | ||
|
|
750e145f03 | ||
|
|
3bbbbfa402 | ||
|
|
f728751fa9 | ||
|
|
f5425d0e54 | ||
|
|
debaaccf4c | ||
|
|
a3e20501af | ||
|
|
c87a75f3fd | ||
|
|
55729c5eda | ||
|
|
06cbcc2339 | ||
|
|
1afe860e76 | ||
|
|
c0ac935652 | ||
|
|
7c76b0f5a5 | ||
|
|
42091c54b7 | ||
|
|
396b48d017 | ||
|
|
bb1c0c7625 | ||
|
|
1170676141 | ||
|
|
7baaaa9db9 | ||
|
|
c1ecef0179 | ||
|
|
032d98e6bd | ||
|
|
f4a90cd531 | ||
|
|
82f03583fe | ||
|
|
81c1a50052 | ||
|
|
c7207799e2 | ||
|
|
e0a719ee93 | ||
|
|
ee6d2e7d82 | ||
|
|
14710b5218 | ||
|
|
4b5e0d1c1c | ||
|
|
9968e8346c | ||
|
|
bc4da17f10 | ||
|
|
da56c563fc | ||
|
|
95f4dbf6a0 | ||
|
|
6c36f343ea | ||
|
|
9257ebdd1e | ||
|
|
8f1017195f | ||
|
|
20da294b36 | ||
|
|
a63415461a | ||
|
|
2bb4c78551 | ||
|
|
2909e61721 | ||
|
|
171da2a194 | ||
|
|
933c468e2d | ||
|
|
0d1eb9b12e | ||
|
|
02a03b4d68 | ||
|
|
ef9f3bd361 | ||
|
|
f18b6ee754 | ||
|
|
deb9e463e1 | ||
|
|
c799534009 | ||
|
|
aea01072fb | ||
|
|
ca8dd5a702 | ||
|
|
e79c4a1e3f | ||
|
|
e19d0d948a | ||
|
|
a160ccd095 | ||
|
|
d2d0477186 | ||
|
|
c742c0725d | ||
|
|
9e6343a111 | ||
|
|
faf33b5f2d | ||
|
|
8a78ac7d82 | ||
|
|
d7c7814b3a | ||
|
|
15bf02f829 | ||
|
|
4f62a594b3 | ||
|
|
64253d0120 | ||
|
|
286936131d | ||
|
|
95588ef6b6 | ||
|
|
daef04ccd8 | ||
|
|
86e385c548 | ||
|
|
cc193b99d6 | ||
|
|
c026007937 | ||
|
|
dbc117a9d4 | ||
|
|
34665bd97a | ||
|
|
6d05e607b3 | ||
|
|
14bfc3018b | ||
|
|
7e4f195f94 | ||
|
|
4b6de934a5 | ||
|
|
14fbb595fd | ||
|
|
18a5fb80d0 | ||
|
|
551ef79f03 | ||
|
|
81aec6bad2 | ||
|
|
d08949c597 | ||
|
|
212aee0373 | ||
|
|
059b78c9fb | ||
|
|
11ba5fc28e | ||
|
|
fbde9ec64f | ||
|
|
6600bba58c | ||
|
|
9e340d2e16 | ||
|
|
6f784e9e94 | ||
|
|
62f6aabf05 | ||
|
|
46457a8bdc | ||
|
|
1081e836f5 | ||
|
|
dfe1e37592 | ||
|
|
0b9db21c10 | ||
|
|
57527115d4 | ||
|
|
8d2679c458 | ||
|
|
ca00f18a6a | ||
|
|
466f68701f | ||
|
|
1bb83da253 | ||
|
|
bd1e17bac9 | ||
|
|
322f95e42e | ||
|
|
3ea77f5899 | ||
|
|
882e61ff44 | ||
|
|
d991be4934 | ||
|
|
dfba8bd15f | ||
|
|
87e23673fb | ||
|
|
8fbd1acf4a | ||
|
|
d7dfbaa799 | ||
|
|
04ce20857e | ||
|
|
4550246d63 | ||
|
|
dd673f44ae | ||
|
|
3245284ecc | ||
|
|
3d9872a671 | ||
|
|
f02db2a2d8 | ||
|
|
d1ebb79432 | ||
|
|
44f03a1fce | ||
|
|
6b6ee94d9b | ||
|
|
81c2ca80bc | ||
|
|
c83cec8ed7 | ||
|
|
93ffe5c507 | ||
|
|
8cd1a70333 | ||
|
|
06ae024c2c | ||
|
|
ce8fd0f22f | ||
|
|
7dc1aff1dc | ||
|
|
a60003127f | ||
|
|
7d1ba58806 | ||
|
|
963d71e221 | ||
|
|
2a804fda1c | ||
|
|
597a1a04d2 | ||
|
|
aec6f49ea1 | ||
|
|
e1ad710de5 | ||
|
|
edc8447617 | ||
|
|
f726d4f8e8 | ||
|
|
1f333f6b88 | ||
|
|
9b855fca47 | ||
|
|
c5f433fdf4 | ||
|
|
fd61dae1c1 | ||
|
|
2c45f9759f | ||
|
|
0d3261f748 | ||
|
|
d38e873800 | ||
|
|
ca38914095 | ||
|
|
ee5beeea45 | ||
|
|
a9d40d9f49 | ||
|
|
b8be916c18 | ||
|
|
24d0313fd3 | ||
|
|
c3d2a81327 | ||
|
|
dfeb117663 | ||
|
|
495a830506 | ||
|
|
a730f67ca5 | ||
|
|
9a3867770d | ||
|
|
f8a66bb5e7 | ||
|
|
4b00e1342e | ||
|
|
f9e0584f00 | ||
|
|
900c290e75 | ||
|
|
69e1577d18 | ||
|
|
a6d9044c3a | ||
|
|
9e23d60ef1 | ||
|
|
f12449119c | ||
|
|
533bfc4458 | ||
|
|
1317c2879c | ||
|
|
f9e51f894a | ||
|
|
6fdd4ba833 | ||
|
|
9495ae2336 | ||
|
|
43f634e198 | ||
|
|
ad51be7afc | ||
|
|
15b8bfdf8f | ||
|
|
cb8ae47be6 | ||
|
|
db846c0680 | ||
|
|
630a37025e | ||
|
|
d3bbc7f2b0 | ||
|
|
4e0321ca5f | ||
|
|
ec00bdf9de | ||
|
|
01dd7ad9c6 | ||
|
|
d726005c38 | ||
|
|
4f3e868367 | ||
|
|
ed8bab7df8 | ||
|
|
9bdb14e13a | ||
|
|
b827f7f730 | ||
|
|
07e3660388 | ||
|
|
970ab9004e | ||
|
|
862445259e | ||
|
|
e96538bae0 | ||
|
|
7447ccc487 | ||
|
|
bcd3a00f91 | ||
|
|
8c3675546f | ||
|
|
ec64dab720 | ||
|
|
db976c4ae9 | ||
|
|
0db96795b3 | ||
|
|
59bae96c1b | ||
|
|
d062a0da1b | ||
|
|
d2668974f7 | ||
|
|
78e4c82f20 | ||
|
|
f965452c5d | ||
|
|
be644bb3d9 | ||
|
|
97cc9509c0 | ||
|
|
6dc7a10c2e | ||
|
|
19298b9ad6 | ||
|
|
97d427a6a8 | ||
|
|
bff6eb0503 | ||
|
|
dc9ab2067d | ||
|
|
e8e1f7bafb | ||
|
|
e50a456025 | ||
|
|
d442bbca1a | ||
|
|
92e35bd0ac | ||
|
|
cb9d283f62 | ||
|
|
8aba19e05b | ||
|
|
b7b4035492 | ||
|
|
3667fd82b7 | ||
|
|
691d5cde27 | ||
|
|
7f431b07e1 | ||
|
|
8264f19ad4 | ||
|
|
e3864b86a4 | ||
|
|
e947cfba45 | ||
|
|
e69590c035 | ||
|
|
8b19393d7c | ||
|
|
9480603572 | ||
|
|
90c6535a4c | ||
|
|
258588921a | ||
|
|
525b309037 | ||
|
|
2f941b9224 | ||
|
|
259c4240b5 | ||
|
|
9eb7157fce | ||
|
|
1138e1a826 | ||
|
|
e5057b8c1d | ||
|
|
7c04243b3d | ||
|
|
c6b54454be | ||
|
|
876fb0fc5e | ||
|
|
38f912dc0a | ||
|
|
eb24f49766 | ||
|
|
168ab1e1a1 | ||
|
|
49f601d200 | ||
|
|
15f32a0044 | ||
|
|
7bb3c60a61 | ||
|
|
4d1dfc1ae5 | ||
|
|
ec5dfd7727 | ||
|
|
8e5a588e87 | ||
|
|
b9c3cfaee4 | ||
|
|
01bd3f8d68 | ||
|
|
b39f545669 | ||
|
|
e7f517e157 | ||
|
|
1d7de715e7 | ||
|
|
76501c1aa0 | ||
|
|
a535a07810 | ||
|
|
6add54e86d | ||
|
|
a74eba9e97 | ||
|
|
34f782e747 | ||
|
|
1990322d6d | ||
|
|
0339979f9f | ||
|
|
823465fde9 | ||
|
|
1feae49490 | ||
|
|
1a1e2169a8 | ||
|
|
f1b1e25d9a | ||
|
|
ce6472be8d | ||
|
|
79232cecb9 | ||
|
|
1a802da0ce | ||
|
|
b33a845c08 | ||
|
|
6643b51a9b | ||
|
|
1cce00a09a | ||
|
|
1ef9a0289f | ||
|
|
498a359f01 | ||
|
|
ebbcdc2089 | ||
|
|
cced57b22e | ||
|
|
96d47a82bc | ||
|
|
b4cab18ecd | ||
|
|
d801447e5d | ||
|
|
8c041d4eef | ||
|
|
6ff2a23ff7 | ||
|
|
925ac96264 | ||
|
|
9e89198cd5 | ||
|
|
a1e6643dc0 | ||
|
|
63257a13f5 | ||
|
|
e37e0ccf9f | ||
|
|
018783bb47 | ||
|
|
f883670cbc | ||
|
|
57f6e928b7 | ||
|
|
cbeb83d8d0 | ||
|
|
33b4ea04b4 | ||
|
|
350434cc5a | ||
|
|
c0eca0c851 | ||
|
|
cdac43fa5f | ||
|
|
30aeabba88 | ||
|
|
02a07f3a8a | ||
|
|
986a32b3ce | ||
|
|
8c4afa73f8 | ||
|
|
6db3ccdb40 | ||
|
|
cf0fe5cbba | ||
|
|
621398f95b | ||
|
|
186a972db7 | ||
|
|
7473a17a5e | ||
|
|
66e08afb0d | ||
|
|
851da1d020 | ||
|
|
c44840978c | ||
|
|
90e012319a | ||
|
|
18de9cbaab | ||
|
|
42a76cbd38 | ||
|
|
eb95634801 | ||
|
|
de8bfff47f | ||
|
|
07c18911df | ||
|
|
cc769cc330 | ||
|
|
9794f58936 | ||
|
|
a44fcff3b1 | ||
|
|
7e4a3e88b9 | ||
|
|
8fd45e2205 | ||
|
|
7d235057e8 | ||
|
|
63a8a67771 | ||
|
|
bac39ca176 | ||
|
|
1979047bbc | ||
|
|
8860c1b0f8 | ||
|
|
626b1e8601 | ||
|
|
04086f31b9 | ||
|
|
7037aa9c3b | ||
|
|
f5bab8a698 | ||
|
|
ee44b1b1d8 | ||
|
|
d0f91df95b | ||
|
|
dc1c49da6a | ||
|
|
331040bbdf | ||
|
|
70e1d4b30d | ||
|
|
03dbd797fb | ||
|
|
eb4cb1fbfe | ||
|
|
28172f7f8f | ||
|
|
cb5347cbb8 | ||
|
|
1ce0781d2d | ||
|
|
1257aa76d7 | ||
|
|
6ec3835896 | ||
|
|
506fe7d9bd | ||
|
|
14ef9f6295 | ||
|
|
0d76e1a29e | ||
|
|
9e42d7e8f5 | ||
|
|
364f97332b | ||
|
|
caa7bb8a65 | ||
|
|
3e956b34c1 | ||
|
|
038374e5aa | ||
|
|
dafd54f830 | ||
|
|
cb65e6b0af | ||
|
|
4ca8590c83 | ||
|
|
c5ef02d7e6 | ||
|
|
8175aa8715 | ||
|
|
b426634659 | ||
|
|
e92da1b700 | ||
|
|
7e0a2e88ee | ||
|
|
16afa8c2d4 | ||
|
|
907288455a | ||
|
|
64689e4e3b | ||
|
|
294377846a | ||
|
|
70e4cbaba2 | ||
|
|
9e2f9526fb | ||
|
|
84bec20fa5 | ||
|
|
e9a492fde6 | ||
|
|
76a69f08b4 | ||
|
|
893110d3ef | ||
|
|
70575fa9cf | ||
|
|
bb2450eb3c | ||
|
|
ab56689326 | ||
|
|
a9cf07480a | ||
|
|
c81de2d758 | ||
|
|
aa1c7639f2 | ||
|
|
d448101b32 | ||
|
|
e806f0ede2 | ||
|
|
8253c27333 | ||
|
|
254392370f | ||
|
|
d2319688a5 | ||
|
|
ffebe6c696 | ||
|
|
9e4094f3a9 | ||
|
|
f7eadb08a9 | ||
|
|
d511cfa4cc | ||
|
|
270a3051b7 | ||
|
|
81bc9f9ba6 | ||
|
|
338cec4f18 | ||
|
|
b455e3380f | ||
|
|
92761f2c55 | ||
|
|
7823b8ad2e | ||
|
|
cac82f3124 | ||
|
|
4b892d06e4 | ||
|
|
92149b0f00 | ||
|
|
399aa4c03c | ||
|
|
d5de715032 | ||
|
|
e6fb179184 | ||
|
|
af54f12ec3 | ||
|
|
909cabd517 | ||
|
|
a734a71d2f | ||
|
|
91dc45c880 | ||
|
|
91f364b76a | ||
|
|
23dd088466 | ||
|
|
2cabcc57f4 | ||
|
|
dd6092a5e7 | ||
|
|
3c2861f956 | ||
|
|
894b841769 | ||
|
|
62627c96a3 | ||
|
|
c173ece882 | ||
|
|
eca7cbbbeb | ||
|
|
c72bfa9f71 | ||
|
|
b4ae39a509 | ||
|
|
631c051895 | ||
|
|
9a2e11a2a4 | ||
|
|
aa6011b18d | ||
|
|
9419aec116 | ||
|
|
638402589f | ||
|
|
6d6a6bf738 |
No files matched your search
@@ -2,7 +2,7 @@ name: CD Docs
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
- name: Upload pages artifact
|
- name: Upload pages artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v1
|
||||||
with:
|
with:
|
||||||
path: docs/.vitepress/dist
|
path: dist/docs
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: CD Host
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */5 * * *'
|
- cron: '0 */6 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -22,11 +22,11 @@ jobs:
|
|||||||
- name: Download release assets
|
- name: Download release assets
|
||||||
run: |
|
run: |
|
||||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
||||||
| grep "browser_download_url.*dist.zip" \
|
| grep "browser_download_url.*bot.zip" \
|
||||||
| cut -d : -f 2,3 \
|
| cut -d : -f 2,3 \
|
||||||
| tr -d \")
|
| tr -d \")
|
||||||
curl -L -o dist.zip $DOWNLOAD_URL
|
curl -L -o bot.zip $DOWNLOAD_URL
|
||||||
unzip dist.zip
|
unzip bot.zip
|
||||||
|
|
||||||
- name: Run bot
|
- name: Run bot
|
||||||
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
|
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
name: CD North America Ratings Latest
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
host:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Use node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install Yarn
|
|
||||||
run: npm install --location=global yarn
|
|
||||||
|
|
||||||
- name: Use node_modules cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: '**/node_modules'
|
|
||||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn
|
|
||||||
|
|
||||||
- name: Generate leaderboard
|
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --latest
|
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
name: CD North America Ratings Leaderboards
|
name: CD Ratings Asia
|
||||||
|
|
||||||
|
# latest: '0 0 * * *'
|
||||||
|
# asia midnight: '0 17 * * *'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
- cron: '0 17 * * *'
|
- cron: '0 17 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -31,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
name: CD North America Ratings Leaderboards
|
name: CD Ratings Europe
|
||||||
|
|
||||||
|
# latest: '0 0 * * *'
|
||||||
|
# eu midnight: '0 23 * * *'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
- cron: '0 23 * * *'
|
- cron: '0 23 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -31,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
name: CD North America Ratings Latest
|
name: CD Ratings North America
|
||||||
|
|
||||||
|
# latest: '0 0 * * *'
|
||||||
|
# na midnight: '0 5 * * *'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
- cron: '0 5 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
host:
|
host:
|
||||||
@@ -31,4 +35,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --latest
|
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: CI Build
|
name: CI Build Bot
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: push
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -34,10 +34,10 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build
|
run: yarn build:bot
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: bot
|
||||||
path: dist
|
path: dist/bot
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
name: CD North America Ratings Latest
|
name: CI Build Docs
|
||||||
|
|
||||||
on:
|
on: push
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
host:
|
build:
|
||||||
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -30,5 +33,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Build
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --latest
|
run: yarn build:docs
|
||||||
|
|
||||||
|
- name: Archive build
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: docs
|
||||||
|
path: dist/docs
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: CI Build Website
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Yarn
|
||||||
|
run: npm install --location=global yarn
|
||||||
|
|
||||||
|
- name: Use node_modules cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: '**/node_modules'
|
||||||
|
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build:website
|
||||||
|
|
||||||
|
- name: Archive build
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: website
|
||||||
|
path: dist/website
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
name: CD North America Ratings Leaderboards
|
name: CI Version
|
||||||
|
|
||||||
on:
|
on: push
|
||||||
schedule:
|
|
||||||
- cron: '0 5 * * *'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
host:
|
host:
|
||||||
@@ -31,4 +29,4 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Generate leaderboard
|
- name: Generate leaderboard
|
||||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na
|
run: yarn lint:version
|
||||||
@@ -7,8 +7,11 @@ package-lock.json
|
|||||||
dist/
|
dist/
|
||||||
dist.*
|
dist.*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
commands.txt
|
|
||||||
/test.*
|
/test.*
|
||||||
|
test.*
|
||||||
/docs/.vitepress/dist
|
/docs/.vitepress/dist
|
||||||
/docs/.vitepress/cache
|
/docs/.vitepress/cache
|
||||||
servers.csv
|
servers.csv
|
||||||
|
.next/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
temp/
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
tasks:
|
|
||||||
- init: yarn
|
|
||||||
- command: yarn run pw
|
|
||||||
@@ -5,13 +5,22 @@
|
|||||||
"bepaint",
|
"bepaint",
|
||||||
"blitzstars",
|
"blitzstars",
|
||||||
"cleandist",
|
"cleandist",
|
||||||
|
"cockroackdb",
|
||||||
|
"corsproxy",
|
||||||
"customperiod",
|
"customperiod",
|
||||||
|
"deburr",
|
||||||
|
"diacriticless",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
"fullstats",
|
"fullstats",
|
||||||
"fuzzysort",
|
"fuzzysort",
|
||||||
|
"immer",
|
||||||
"kranvagn",
|
"kranvagn",
|
||||||
|
"krieg",
|
||||||
"Leaderboard",
|
"Leaderboard",
|
||||||
"leaderboards",
|
"leaderboards",
|
||||||
|
"MURICA",
|
||||||
|
"Oopsy",
|
||||||
|
"outdir",
|
||||||
"outfile",
|
"outfile",
|
||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
@@ -28,8 +37,10 @@
|
|||||||
"statsfull",
|
"statsfull",
|
||||||
"Stringifying",
|
"Stringifying",
|
||||||
"strvk",
|
"strvk",
|
||||||
|
"subnation",
|
||||||
"tankaverages",
|
"tankaverages",
|
||||||
"tankopedia",
|
"tankopedia",
|
||||||
|
"tankopedias",
|
||||||
"tankstats",
|
"tankstats",
|
||||||
"tresabhi",
|
"tresabhi",
|
||||||
"Unicum",
|
"Unicum",
|
||||||
@@ -37,7 +48,10 @@
|
|||||||
"vickers",
|
"vickers",
|
||||||
"vitepress",
|
"vitepress",
|
||||||
"wargaming",
|
"wargaming",
|
||||||
"winrate"
|
"webp",
|
||||||
|
"winrate",
|
||||||
|
"wotblitz",
|
||||||
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
"deno.unstable": false
|
"deno.unstable": false
|
||||||
|
|||||||
@@ -1,21 +1,44 @@
|
|||||||
# blitzkrieg
|
# Blitzkrieg
|
||||||
|
|
||||||
A Discord bot for anything and everything related to World of Tanks Blitz.
|
[](https://codeium.com)
|
||||||
|
|
||||||
## Invite the Bot
|
Tools for everything World of Tanks Blitz
|
||||||
|
|
||||||
Join the [Discord server](https://discord.gg/nDt7AjGJQH), click on the bot's profile, and click "Add to Server".
|
## Invite the bot
|
||||||
|
|
||||||
## Using
|
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||||
|
|
||||||
### Installing dependencies
|

|
||||||
|
|
||||||
|
## Visit the website
|
||||||
|
|
||||||
|
### https://blitz-krieg.vercel.app/
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Visit the documentation website
|
||||||
|
|
||||||
|
### https://tresabhi.github.io/blitzkrieg/
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Run the bot
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running
|
### Create `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
GH_TOKEN={YOUR_GITHUB_TOKEN_HERE}
|
||||||
|
DISCORD_TOKEN={YOUR_DISCORD_TOKE_HERE}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start discord-token=<token> wargaming-application-id=<id>
|
yarn dev:bot
|
||||||
```
|
```
|
||||||
@@ -8,5 +8,7 @@
|
|||||||
"sklld_peasant_role": "896878529512370228",
|
"sklld_peasant_role": "896878529512370228",
|
||||||
|
|
||||||
"tres_guild_id": "734786591205359697",
|
"tres_guild_id": "734786591205359697",
|
||||||
"tres_log_channel": "1109262265850544148"
|
"tres_log_channel": "1109262265850544148",
|
||||||
|
|
||||||
|
"test_guild_id": "1124729294829142116"
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,39 @@
|
|||||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
import { compareVersions } from 'compare-versions';
|
||||||
|
import { readdirSync } from 'fs';
|
||||||
|
import { parse } from 'path';
|
||||||
|
import { defineConfig } from 'vitepress';
|
||||||
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
title: 'blitzkrieg',
|
title: 'blitzkrieg',
|
||||||
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
||||||
base: '/blitzkrieg/',
|
base: '/blitzkrieg/',
|
||||||
|
outDir: '../dist/docs',
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
nav: [
|
nav: [
|
||||||
{ text: 'Home', link: '/' },
|
{ text: 'Home', link: '/' },
|
||||||
{ text: 'Examples', link: '/markdown-examples' },
|
{ text: 'Guide', link: '/guide/introduction' },
|
||||||
],
|
],
|
||||||
|
|
||||||
sidebar: getSidebar({
|
sidebar: [
|
||||||
collapsed: false,
|
...getSidebar({
|
||||||
contentDirs: ['guide', 'legal', 'changelogs'],
|
collapsed: false,
|
||||||
contentRoot: '/docs/',
|
contentDirs: ['guide', 'legal'],
|
||||||
}) as DefaultTheme.Sidebar,
|
contentRoot: '/docs/',
|
||||||
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'Changelogs',
|
||||||
|
items: readdirSync('./docs/changelogs')
|
||||||
|
.map((file) => parse(file).name)
|
||||||
|
.sort(compareVersions)
|
||||||
|
.map((name) => ({ text: name, link: `/changelogs/${name}.html` })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
socialLinks: [
|
socialLinks: [
|
||||||
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
|
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Blitzkrieg 1.11.0
|
||||||
|
|
||||||
|
I am going on vacation so I thought I'd publish this a bit prematurely without all the planned features.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added `/ratings`
|
||||||
|
- This command currently only has the `leagues` subcommand group
|
||||||
|
- Displays the top players in a league
|
||||||
|
- Also shows the changes in their positions and scores since last region midnight
|
||||||
|
- Use `/help timezones` to know when Blitzkrieg records the leaderboard
|
||||||
|
- Blitzkrieg now internally generates and keeps a permanent record of all leaderboards updated every `UTC+0` midnight
|
||||||
|
- This approach will allow us to capture the absolute latest state of the leaderboard before it is wiped for the next season
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed region "Europe" in `/verify` example
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Add scripts were upgraded to TypeScript
|
||||||
|
- Secrets now only throw errors if they're used when not provided
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Blitzkrieg 1.12.0
|
||||||
|
|
||||||
|
_Houston, permission to return to Earth? A new update dropped._
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `multi-tank` filters to `/stats` and `/full-stats`
|
||||||
|
- This lets you pull up the stats by combining filters of `nation`, `tank-type`, `tier`, and `tree-type`
|
||||||
|
- Added `/ratings neighbours`
|
||||||
|
- Lets you see your rank and the neighbours around you
|
||||||
|
- Added a bunch more `/today` parameters (thank you for the suggestions, HisRoyalFatness!)
|
||||||
|
- `cutoff`: the max number of rows to show (excluding the total row)
|
||||||
|
- `maximized`: the max number of rows that show the full non-collapsed stats (default: `4`)
|
||||||
|
- `show-total`: show the total row or not (default: `true`)
|
||||||
|
- Tank search is now accent-tolerant
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/ratings` has been moved out of preview and is now accessible to all
|
||||||
|
- `/today` now has a more appealing look with more efficient space management
|
||||||
|
- `/stats`, `/full-stats`, and `/evolution` now have less convoluted commands
|
||||||
|
- All periods now fall under `period` and `custom`
|
||||||
|
- `period` has preset periods like `today`, `career`, `30`, `60`, `90`
|
||||||
|
- `custom` lets you set a start and end day
|
||||||
|
- Added dashes to command names, subcommands, options, etc. to make them more readable
|
||||||
|
- Titlebar
|
||||||
|
- Removed the Blitzkrieg logo
|
||||||
|
- Removed region
|
||||||
|
- Center aligned items
|
||||||
|
- Footers are now center alinged when they do no have footer text on the left
|
||||||
|
- Moved several title discriminators into the description
|
||||||
|
- `/ratings` is now data-driven
|
||||||
|
- Embeds now auto refresh every `30` seconds instead of `60`
|
||||||
|
- Added numbers besides roman numerals in tier choice
|
||||||
|
- I forgot to implement this in places other than `/owned-tanks`... oh well!
|
||||||
|
- Increased precision of winrate in `/evolution` to `2` decimals
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed duplicate "Efficiency" cards in `/full-stats`
|
||||||
|
- Titlebar gap is now `4px` just like Figma
|
||||||
|
- Renamed `/statsfull` to `/full-stats` in the `/stats` footer
|
||||||
|
- Rewards in `/ratings` are now properly aligned
|
||||||
|
- `/ratings` now uses the correct source of information per region instead of getting it from a cached North American source
|
||||||
|
- Search parameters are now passed onto the URL parser correctly
|
||||||
|
- Custom date ranges now work correctly once again as the start is now the maximum days ago from today and the end is the minimum
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- All generated images are now `480px` wide instead of `640px` to combat the unnecessary amounts of negative space
|
||||||
|
- Changed all relevant typings to be sourced from `stats` instead of `full-stats`
|
||||||
|
- Added minimum character limit to clan search
|
||||||
|
- Username auto-completing now only takes in regex-accepted parts of the input
|
||||||
|
- Search parameters are now longer automatically generated by inferring command interactions
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Blitzkrieg 1.12.1
|
||||||
|
|
||||||
|
A duo of nasty bugs fixed.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed a tank index and id mismatch
|
||||||
|
- Reverted username pattern infrence in command username inputs
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Blitzkrieg 1.12.2
|
||||||
|
|
||||||
|
A much needed room service is here!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Tier selection is now easier
|
||||||
|
- Tier options across all commands have been unified to a single style
|
||||||
|
- Tiers are now sorted from `10` to `1`
|
||||||
|
- Tiers now show up in the format `Tier {tier_number} - {tier_roman_numeral}` (example: `Tier 8 - VIII`)
|
||||||
|
- Removed powered by footer as links are provided regardless
|
||||||
|
- Changed invite instructions to instead use application directory
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Guide link on the website is now fixed
|
||||||
|
- Versions now show up in the correct order in the navigation bar
|
||||||
|
- Added Blitzkrieg's GitHub link instead of the default Vitepress
|
||||||
|
- Changed examples to guide in the navigation bar
|
||||||
|
- Clan name and icon now show up with multi-tank filters
|
||||||
|
- Removed a random `console.log`
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Added `knip`
|
||||||
|
- Removed a lot of unused files and dependencies
|
||||||
|
- Updated all dependencies to their latest
|
||||||
|
- Code split `bot.ts`
|
||||||
|
- `client` and `octokit` now reside separately
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.12.3
|
||||||
|
|
||||||
|
3-day downtime fixed!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Split `/ratings` into `/ratings current` and `/ratings archive`
|
||||||
|
- `current` now properly handles when there is no ongoing ratings season
|
||||||
|
- `archive` displays older archived seasons
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The bot is now no ratings seasons tolerant
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.12.4
|
||||||
|
|
||||||
|
Added very badly-coded temporary `/clan-event` command (without linting)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added `/clan-event` command
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Blitzkrieg 1.12.5
|
||||||
|
|
||||||
|
Some patch up work
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/clan-event` now is more legible and comes with an approximation warning
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- `/clan-event` now uses a more optimized way of fetching large data
|
||||||
|
- This should fix the issue where too many people using the command broke it
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Blitzkrieg 1.12.6
|
||||||
|
|
||||||
|
Stops making the bot lie!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- The approximation warning for `/clan-event` has been made more concise
|
||||||
|
- Players with `0` wins are now omitted
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Players who haven't player within the current day are now omitted
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.12.7
|
||||||
|
|
||||||
|
A few fixes and clarifications
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/clan-event`
|
||||||
|
- Made statement about the limited publicly available data
|
||||||
|
- Added a statement about reset times
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Commands with images now once again work
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.12.8
|
||||||
|
|
||||||
|
Blitzkrieg is a completely free service and I earn no revenue from it. To keep it sustainable, I have to rely on free services. Unfortunately for embeds, seems like I have exceeded my limits. Before this service starts charging me, I have shut down embeds. They'll hopefully be back in 1.13 alongside the website! Only the future will tell...
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Removed all embed buttons
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.12.9
|
||||||
|
|
||||||
|
Now that the clan event is over, we don't need `/clan-event`. This command may be reintroduced in the future as a part of a different command/feature and will definitely be re-enabled during the next clan event.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Disabled the `/clan-event` command
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Blitzkrieg 1.13.0
|
||||||
|
|
||||||
|
One of the biggest updates ever! This changelog's size does not truly represent the work (and brain cells) spent.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added Blitzkrieg homepage
|
||||||
|
- Added session tracker
|
||||||
|
- Search players up and select one
|
||||||
|
- This starts tracking your games from that point
|
||||||
|
- This is displayed in a similar fashion to the `/today` command
|
||||||
|
- Updated the permissions doc
|
||||||
|
- This now includes all 3 of the needed permissions for full operation
|
||||||
|
- Also renamed the permissions to better match Discord
|
||||||
|
- Renamed `/help` to `/about`
|
||||||
|
- Renamed `/help about` to `/about bot`
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Added `/ratings` error handling for `null` clans, players, and rewards
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Removed all server assets
|
||||||
|
- Simplified build script to only compile the bot
|
||||||
|
- Upgraded the build script to TypeScript
|
||||||
|
- Moved docs build to `dist/docs`
|
||||||
|
- Split `tsconfig.json` for Next.js and esbuild
|
||||||
|
- Removed Gitpod config
|
||||||
|
- Switched to `.env`
|
||||||
|
- Made Wargaming application ID public
|
||||||
|
- Started migration to Radix UI
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Blitzkrieg 1.13.1
|
||||||
|
|
||||||
|
The quickest minor update in town 🤠
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added a title, description, and icon to the website
|
||||||
|
- Added display options to the session tracker
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Center aligned stats in breakdown to work better on low width devices
|
||||||
|
- Center aligned the Blitzkrieg logo in the navbar
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Added up-to-date information to `README.md` and embed docs
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.13.2
|
||||||
|
|
||||||
|
One lonely albeit important fix.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Gradients for `/stats` now use Radix's new hex values
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.13.3
|
||||||
|
|
||||||
|
Streamer's delight!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added config context menu for the session tracker embed
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Blitzkrieg 1.14.0
|
||||||
|
|
||||||
|
Elegance has never been met like this before
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `/link`
|
||||||
|
- Unlike `/verify`, `/link` links you Discord and Blitz accounts across all servers
|
||||||
|
- This command does not change your nickname
|
||||||
|
- `/link` now simply displays a deprecation message
|
||||||
|
- Usernames can still be inputted per-command to override your linked account
|
||||||
|
- `/stats` and `/full-stats` are now way simpler
|
||||||
|
- `/stats <type> <period>` has been replaced with `/stats <30|60|90|...>` (likewise, `/full-stats`)
|
||||||
|
- These commands now have filters without having to opt into `multi-tank`
|
||||||
|
- Added `/breakdown`
|
||||||
|
- Unlike `/today`, `/breakdown` lets you select any period of time
|
||||||
|
- `/breakdown` now simply displays a deprecation message
|
||||||
|
- `/about commands` are now sorted alphabetically
|
||||||
|
- Added a `/reset` button to the options dropdown and embed context-menu
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Simplified no ongoing ratings season edge case
|
||||||
|
- Updated welcome message for Skilled server to use `/link` and other up-to-date information
|
||||||
|
- Tier weights are now reported in Arabic numerals instead of Roman
|
||||||
|
- Removed tank images from `/owned-tanks`
|
||||||
|
- Wargaming messed up their API and almost no tanks have any images anymore
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Banners now use a more economical `.webp` format just like the home page
|
||||||
|
- Removed unused color palettes
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Guild commands are now handled more efficiently
|
||||||
|
- Removed nickname-based Blitz username inference
|
||||||
|
- Code split a lot of common functionalities
|
||||||
|
- Re-arranged `core` folder
|
||||||
|
- Added midnight and latest ratings leaderboard caches
|
||||||
|
- Merged renderers into their primary user files
|
||||||
|
- Added sharding
|
||||||
|
- Disabled for now but good for future proofing
|
||||||
|
- Switched to a way cleaner logging schema
|
||||||
|
- Hosting is now done in 6 hour periods instead of 5
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Blitzkrieg 1.14.1
|
||||||
|
|
||||||
|
Oopsy daisy, a memory leak!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Simplified message for users to use `/link`
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed a memory leak in the render thread manager
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Added `UserError` extending `Error`
|
||||||
|
- This lets the bot avoid spamming the debug console for mistakes users make
|
||||||
|
- Reduced render threads to 2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Blitzkrieg 1.14.2
|
||||||
|
|
||||||
|
A late night hot fix for my homies
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Clan info is now fetched using `clan_id` instead of `account_id`
|
||||||
|
- Generic stats row names are now bounded by a max width
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Blitzkrieg 1.14.3
|
||||||
|
|
||||||
|
Pull your brush out and start polishing.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Rows in `/breakdown` are now chunked into `8` per image
|
||||||
|
- This is true unless the number of pages exceeds `10` in which case the rows are even distributed across all `10` pages
|
||||||
|
- `10` is the maximum number of images that Discord allows
|
||||||
|
- Short region names are used in username autocomplete instead of their full names
|
||||||
|
- Example: "North America" is "NA" now
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed reference to username inference in the username option description
|
||||||
|
- Removed an unnecessary type `asd` that did absolutely nothing used nowhere
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Image orders are now preserved even if processed in parallel and all completed out of order
|
||||||
|
- `fetchBlitz` is now throttled to `10` requests per second
|
||||||
|
- Request overflows are managed by queueing
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.14.4
|
||||||
|
|
||||||
|
Streamers get some knowledge today
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added a video in the embed button prompt
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Blitzkrieg 1.15.0
|
||||||
|
|
||||||
|
Ratings will never be the same.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Blitzkrieg ratings leaderboard
|
||||||
|
- Available at https://blitz-krieg.vercel.app/tools/ratings
|
||||||
|
- **Highly experimental; [please report bugs on Discord server](https://discord.gg/nDt7AjGJQH)**
|
||||||
|
- Current season leaderboards are live meaning that there may be scores and positions that are out of order
|
||||||
|
- Options
|
||||||
|
- Regions can be swapped between NA, EU, and Asia
|
||||||
|
- The current season and all archived season (from 49 and onwards) are available
|
||||||
|
- Jump: used to jump to certain players
|
||||||
|
- Score: finds the first player with at least your desired score
|
||||||
|
- Position: finds the player at that position
|
||||||
|
- League: finds the top-most player in that league
|
||||||
|
- Player: finds the player
|
||||||
|
- **Jumping to scores and positions for the current season is a compounding function (hence the loading screen) and is limited to the speed of Wargaming's APIs which is pretty darn slow**
|
||||||
|
- Page controls
|
||||||
|
- Flip between pages left and right
|
||||||
|
- 33 players are loaded at once
|
||||||
|
- Left control is disabled on the first page and vice versa
|
||||||
|
- Page turning controls are also disabled if the next/previous page hasn't loaded yet
|
||||||
|
- Page numbers can be manually typed in the text field too
|
||||||
|
- This is available both at the top and the bottom of the screen
|
||||||
|
- Players
|
||||||
|
- Shows the position of the player
|
||||||
|
- Shows the player nickname and clan name
|
||||||
|
- "Deleted player ID" us shown when loading a deleted player
|
||||||
|
- A skeleton is shown when the player and clan is loading
|
||||||
|
- Shows the player percentile
|
||||||
|
- Shows reward for the position
|
||||||
|
- This is blank when there is no reward
|
||||||
|
- This column isn't available for archived seasons
|
||||||
|
- Shows the score
|
||||||
|
- A skeleton is shown when loading
|
||||||
|
- Click on a row to highlight it
|
||||||
|
- Using a jump button also highlights a row
|
||||||
|
- `/credits`: thanks who helped made this project
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Error messages that user face are now even more simplified
|
||||||
|
- Increased username and clan autocomplete size limits to `25`
|
||||||
|
- Added capability for per-page accent theming
|
||||||
|
- Example: the ratings page is red while the session tracker is blue
|
||||||
|
- Simplified welcome message for SKLLD clan server
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Isolated tankopedia info
|
||||||
|
- Implemented `corsproxy.io` for non-timeout tolerant endpoints
|
||||||
|
- Latest archived season number is now data driven
|
||||||
|
- Ratings archives are now public
|
||||||
|
- This eliminates the need of `octokit`
|
||||||
|
- Ratings leaderboard archiver is now error tolerant
|
||||||
|
- Fetch attempts are made every `1000ms`
|
||||||
|
- Blitz errors are now managed better freeing up the queue faster
|
||||||
|
- Isolated first archived season number
|
||||||
|
- Added more advanced PSAs
|
||||||
|
- These can now feature images or embeds
|
||||||
|
- These can now modify message contents
|
||||||
|
- These can also now be secret
|
||||||
|
|
||||||
|
## Experimental Changes
|
||||||
|
|
||||||
|
- Added tankopedia extractor
|
||||||
|
- Retrieves tankopedia data from the game files
|
||||||
|
- This is not utilized as of right now
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Blitzkrieg 1.15.1
|
||||||
|
|
||||||
|
'MURICA!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- "Neighbor" is now spelled in the American way
|
||||||
|
- Updated documentation website with the latest information
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed the guide link on the documentation website
|
||||||
|
|
||||||
|
## Technical features
|
||||||
|
|
||||||
|
- Added capability to fetch patiently while also parsing the JSON
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Removed neighbor relative position caching
|
||||||
|
|
||||||
|
## Technical fixes
|
||||||
|
|
||||||
|
- `/ratings` now uses the non-caching APIs
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Blitzkrieg 1.15.2
|
||||||
|
|
||||||
|
Infinity reduced to a finite value.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Added a maximum value to the jump to position button
|
||||||
|
- Removed neighbor checking for the seeding player
|
||||||
|
- This fixes the jump to position button not loading at all
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Blitzkrieg 1.15.3
|
||||||
|
|
||||||
|
The bot now gets more expressive!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The bot can now have presence messages; examples:
|
||||||
|
- "Living in {number} servers"
|
||||||
|
- "Server {number} users"
|
||||||
|
- "Observing {number} channels"
|
||||||
|
- More will be added in the future! Give me some ideas
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Moved `/credits` to `/about creators`
|
||||||
|
- `/debug` command now ditches embeds and has better information to serve
|
||||||
|
|
||||||
|
## Technical Features
|
||||||
|
|
||||||
|
- Added a workflow and lint for package and changelog version miss match
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Removed manual trigger for Asian latest ratings leaderboard archiver
|
||||||
|
- Leaderboard archiver now accepts an explicit target like `midnight` or `latest`
|
||||||
|
- Combined midnight and latest leaderboard archival workflow into one file
|
||||||
|
|
||||||
|
## Technical Fixed
|
||||||
|
|
||||||
|
- Reviewed all workflow triggers and updated them
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.15.4
|
||||||
|
|
||||||
|
Unlocking future potential
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Moved the word "Ratings" to the front of the workflow name
|
||||||
|
- Renamed `blitzkrieg-db` to `blitzkrieg-assets`
|
||||||
|
- Moved the region folders from the root to `regions`
|
||||||
|
|
||||||
|
## Technical Fixes
|
||||||
|
|
||||||
|
- The ratings leaderboard archiver now correctly skips undefined players
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.15.5
|
||||||
|
|
||||||
|
Oops, this should've been in the last patch.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- User and channel cache is now guild cache exhaustive
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# About
|
|
||||||
|
|
||||||
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
|
|
||||||
|
|
||||||
- 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
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Creators
|
||||||
|
|
||||||
|
- TrèsAbhi `@tresabhi`
|
||||||
|
- Creator & main programmer; _I do it all!_
|
||||||
|
- Prince_NA `@prince_na`
|
||||||
|
- Advisor & tester
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
# Embeds
|
# Embeds
|
||||||
|
|
||||||
Embeds are generated by a few commands as a URL that can be loaded in a browser/streaming software.
|
Embeds are URLs generated by commands or tools that represent the "naked" version of the respective command or tool
|
||||||
|
|
||||||
## Creating Embeds
|
## Creating Embeds
|
||||||
|
|
||||||
1. Use a command of your liking (for example, `/stats`).
|
1. Use a command or tool and click on "Embed" if it's available
|
||||||
2. Customize the command however you like and the URL generated for the embed will match your needs.
|
2. Make sure to tweak the settings of the command or tool because embeds replicate everything
|
||||||
3. Send the command and when the bot is "done thinking," an "Embed" button will appear below the bot's reply.
|
|
||||||
|
|
||||||
## Using Embeds in Streaming Softwares
|
## Using Embeds in Streaming Softwares
|
||||||
|
|
||||||
1. Click on the "Embed" button.
|
1. Click on the "Embed" button
|
||||||
2. Copy the URL from your browser.
|
- If this was generated by a command, copy the URL from the browser
|
||||||
3. Paste the URL into a Browser Source.
|
- If this was generated by a tool, the URL has been copied to your clipboard already
|
||||||
4. Set the refresh rate/FPS to something very low like `0.0167` (that's 1 frame every 60 seconds).
|
2. Paste this URL into your streaming service as a "Browser Source"
|
||||||
|
- Tool embeds use local storage so data won't be transferred from you browser to your streaming software
|
||||||
|
- Make sure to visit the tool's page in your streaming software (using the "Interact" feature) and then open the embed
|
||||||
|
- Set your browser source refresh rate to `1Hz` because that's how fast embeds refresh
|
||||||
|
|
||||||
## How Embeds Behave
|
## How Embeds Behave
|
||||||
|
|
||||||
- They load like any normal website albeit with a significant delay.
|
- They load like any normal website
|
||||||
- They auto refresh every `60s`.
|
- They auto refresh every `1s`
|
||||||
- All embeds and images generated by Blitzkrieg have a width of `640px` but the height varies and must be found manually through trial and error.
|
|
||||||
- It's okay to have a Browser Source that has a height larger than needed as Blitzkrieg embeds are transparent.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Installing Blitzkrieg
|
|
||||||
|
|
||||||
It's easy as 1, 2, 3!
|
|
||||||
|
|
||||||
## Step 1
|
|
||||||
|
|
||||||
Join a server that already has Blitzkrieg or [join our official Discord server](https://discord.gg/nDt7AjGJQH).
|
|
||||||
|
|
||||||
## Step 2
|
|
||||||
|
|
||||||
View the bot's profile and click "Add to Server".
|
|
||||||
|
|
||||||
<video src="https://i.imgur.com/Zq0P3m2.mp4" autoplay loop muted width=320 title="how to install"></video>
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
Review the permissions the bot is asking for and add it to your server!
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
🎉 Services for everything and anything World of Tanks Blitz
|
||||||
|
|
||||||
|
## Bot
|
||||||
|
|
||||||
|
Blitzkrieg 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
|
||||||
|
|
||||||
|
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
|
||||||
|
|
||||||
|
- Session tracker
|
||||||
|
- Ratings leaderboard
|
||||||
|
- Way more to come
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
# Inviting the Bot
|
# Inviting the Bot
|
||||||
|
|
||||||
It's quite simple really. Just follow the steps below:
|
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||||
|
|
||||||
1. Join a Discord server that already has Blitzkrieg installed or [the official Discord Server](https://discord.gg/nDt7AjGJQH).
|
|
||||||
2. Click on the bot's profile.
|
|
||||||
3. Click the "Add to Server" button under the bot's name and ID.
|
|
||||||
4. Select what server you want it in and see what permissions you seem fit for the bot.
|
|
||||||
|
|
||||||
Voila! You're ready to go.
|
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
The following Discord permissions are required by the bot to ensure full functionality:
|
The following Discord permissions are required by the bot to ensure full functionality:
|
||||||
|
|
||||||
|
- Manage nicknames
|
||||||
|
- Read messages / view channels
|
||||||
- Attach files
|
- Attach files
|
||||||
- View channels
|
|
||||||
@@ -12,10 +12,10 @@ hero:
|
|||||||
actions:
|
actions:
|
||||||
- theme: brand
|
- theme: brand
|
||||||
text: Add to Discord Server
|
text: Add to Discord Server
|
||||||
link: guide/install
|
link: /guide/invite
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: Guide
|
text: Guide
|
||||||
link: /guide
|
link: /guide/introduction
|
||||||
|
|
||||||
features:
|
features:
|
||||||
- title: Clean and Efficient UI
|
- title: Clean and Efficient UI
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
|
||||||
|
|
||||||
|
const withVanillaExtract = createVanillaExtractPlugin();
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
...withVanillaExtract(),
|
||||||
|
|
||||||
|
distDir: 'dist/website',
|
||||||
|
reactStrictMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"main": "server.cjs",
|
"main": "server.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
"start": "node --enable-source-maps server.cjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,53 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.10.2",
|
"version": "1.15.5",
|
||||||
"description": "",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bot": "node scripts/build --build=bot",
|
"build:bot": "vite-node --script scripts/build",
|
||||||
"build:server": "node scripts/build --build=server",
|
"build:website": "next build",
|
||||||
"build:docs": "vitepress build docs",
|
"build:docs": "vitepress build docs",
|
||||||
"build:ratingsleaderboard": "node scripts/generateRatingsLeaderboard",
|
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||||
"build": "node scripts/build --build=all",
|
"build:tankopedia": "vite-node --script scripts/buildTankopedia",
|
||||||
"dev:bot": "yarn build:bot --dev && yarn start:bot",
|
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||||
"dev:server": "yarn build:server --dev && yarn start:server",
|
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
|
"dev:website": "next dev",
|
||||||
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
|
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
||||||
|
"start:website": "next start",
|
||||||
"host:docs": "vitepress preview docs",
|
"host:docs": "vitepress preview docs",
|
||||||
"publish:ratingsleaderboard": "node scripts/generateRatingsLeaderboard --publish",
|
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||||
"zip": "zip -r dist.zip dist",
|
"zip": "zip -r dist.zip dist",
|
||||||
"lint": "npx tsc",
|
"lint": "npx tsc",
|
||||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\""
|
"lint:website": "next lint",
|
||||||
|
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
||||||
|
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
|
||||||
|
"knip": "knip",
|
||||||
|
"update": "yarn upgrade --latest"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/colors": "^0.1.8",
|
"@octokit/rest": "^20.0.1",
|
||||||
|
"@prisma/client": "^5.4.1",
|
||||||
|
"@radix-ui/colors": "^3.0.0-rc.4",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/themes": "^1.1.2",
|
||||||
"@resvg/resvg-js": "^2.4.1",
|
"@resvg/resvg-js": "^2.4.1",
|
||||||
|
"@vanilla-extract/css": "^1.13.0",
|
||||||
"bepaint": "^0.1.0-beta.3",
|
"bepaint": "^0.1.0-beta.3",
|
||||||
"discord.js": "^14.9.0",
|
"decompress": "^4.2.1",
|
||||||
"esbuild": "^0.18.10",
|
"discord.js": "^14.13.0",
|
||||||
"escape-html": "^1.0.3",
|
"dotenv": "^16.3.1",
|
||||||
|
"esbuild": "^0.19.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
|
"immer": "^10.0.2",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-escape": "^2.0.0",
|
"markdown-escape": "^2.0.0",
|
||||||
|
"next": "^13.5.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"satori": "^0.9.1",
|
"react-dom": "^18.2.0",
|
||||||
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
"react-fuzzysort": "^1.0.1",
|
||||||
|
"satori": "^0.10.8",
|
||||||
|
"vite-node": "^0.34.5",
|
||||||
|
"vitepress": "^1.0.0-rc.20",
|
||||||
|
"vitepress-plugin-auto-sidebar": "^1.1.0",
|
||||||
|
"xml-js": "^1.6.11",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/rest": "^20.0.1",
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@total-typescript/ts-reset": "^0.4.2",
|
"@types/decompress": "^4.2.6",
|
||||||
"@types/escape-html": "^1.0.2",
|
"@types/escape-html": "^1.0.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.18",
|
||||||
"@types/lodash": "^4.14.195",
|
"@types/js-yaml": "^4.0.8",
|
||||||
|
"@types/lodash": "^4.14.199",
|
||||||
"@types/markdown-escape": "^1.1.0",
|
"@types/markdown-escape": "^1.1.0",
|
||||||
"@types/node": "^20.2.1",
|
"@types/node": "^20.6.5",
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.22",
|
||||||
"prettier": "^3.0.0",
|
"@vanilla-extract/next-plugin": "^2.3.1",
|
||||||
"typescript": "^5.0.4",
|
"compare-versions": "^6.1.0",
|
||||||
"vitepress": "^1.0.0-beta.3"
|
"knip": "^2.27.1",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"prisma": "^5.4.1",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "discord_blitz" (
|
||||||
|
"discord" INT8 NOT NULL,
|
||||||
|
"region" STRING NOT NULL,
|
||||||
|
"blitz" INT4 NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "cockroachdb"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "cockroachdb"
|
||||||
|
url = env("DISCORD_BLITZ_DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
model discord_blitz {
|
||||||
|
discord BigInt @id
|
||||||
|
region String
|
||||||
|
blitz Int
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 177 KiB |
@@ -1,64 +0,0 @@
|
|||||||
import { build } from 'esbuild';
|
|
||||||
import { copyFile, mkdir, rm } from 'fs/promises';
|
|
||||||
import { argv } from 'process';
|
|
||||||
|
|
||||||
const isProd = !argv.includes('--dev');
|
|
||||||
const buildAll = argv.includes('--build=all');
|
|
||||||
const buildBot = argv.includes('--build=bot') || buildAll;
|
|
||||||
const buildServer = argv.includes('--build=server') || buildAll;
|
|
||||||
|
|
||||||
if (isProd) {
|
|
||||||
// only remove in production to ensure no time is wasted in dev
|
|
||||||
console.log('Removing dist...');
|
|
||||||
await rm('dist', { recursive: true, force: true });
|
|
||||||
console.log('Dist removed');
|
|
||||||
|
|
||||||
console.log('Creating dist...');
|
|
||||||
await mkdir('dist');
|
|
||||||
console.log('Dist created');
|
|
||||||
|
|
||||||
console.log('Copying package.json...');
|
|
||||||
copyFile('package.dist.json', 'dist/package.json').then(() =>
|
|
||||||
console.log('package.json copied'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonOptions = {
|
|
||||||
platform: 'node',
|
|
||||||
loader: {
|
|
||||||
'.node': 'copy',
|
|
||||||
'.ttf': 'file',
|
|
||||||
},
|
|
||||||
|
|
||||||
bundle: true,
|
|
||||||
sourcemap: true,
|
|
||||||
minifyIdentifiers: false, // causes errors
|
|
||||||
minifySyntax: isProd,
|
|
||||||
minifyWhitespace: isProd,
|
|
||||||
|
|
||||||
logLevel: isProd ? 'info' : 'silent',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (buildBot) {
|
|
||||||
console.log('Building bot...');
|
|
||||||
build({
|
|
||||||
...commonOptions,
|
|
||||||
|
|
||||||
entryPoints: ['src/bot.ts'],
|
|
||||||
outfile: 'dist/bot.cjs',
|
|
||||||
}).then(() => {
|
|
||||||
console.log('Bot built');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildServer) {
|
|
||||||
console.log('Building server...');
|
|
||||||
build({
|
|
||||||
...commonOptions,
|
|
||||||
|
|
||||||
entryPoints: ['src/server.ts'],
|
|
||||||
outfile: 'dist/server.cjs',
|
|
||||||
}).then(() => {
|
|
||||||
console.log('Server built');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { build } from 'esbuild';
|
||||||
|
import { cp, readdir } from 'fs/promises';
|
||||||
|
|
||||||
|
readdir('src/workers').then((workers) => {
|
||||||
|
const files = [
|
||||||
|
'bot',
|
||||||
|
'index',
|
||||||
|
|
||||||
|
...workers.map((file) => `workers/${file.replace('.ts', '')}`),
|
||||||
|
];
|
||||||
|
|
||||||
|
build({
|
||||||
|
entryPoints: files.map((file) => `src/${file}.ts`),
|
||||||
|
outdir: 'dist/bot',
|
||||||
|
|
||||||
|
platform: 'node',
|
||||||
|
tsconfig: 'tsconfig.esbuild.json',
|
||||||
|
loader: {
|
||||||
|
'.node': 'copy',
|
||||||
|
'.ttf': 'file',
|
||||||
|
},
|
||||||
|
format: 'cjs',
|
||||||
|
jsx: 'transform',
|
||||||
|
outExtension: {
|
||||||
|
'.js': '.cjs',
|
||||||
|
},
|
||||||
|
|
||||||
|
bundle: true,
|
||||||
|
sourcemap: true,
|
||||||
|
minifyIdentifiers: false, // causes goofy errors
|
||||||
|
minifySyntax: true,
|
||||||
|
minifyWhitespace: true,
|
||||||
|
|
||||||
|
logLevel: 'info',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// copy schema.prisma
|
||||||
|
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||||
|
readdir('node_modules/prisma').then((files) =>
|
||||||
|
files
|
||||||
|
.filter((file) => file.endsWith('.node'))
|
||||||
|
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
||||||
|
);
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
import { rm, writeFile } from 'fs/promises';
|
import { rm, writeFile } from 'fs/promises';
|
||||||
import { argv, env } from 'process';
|
import { argv, env } from 'process';
|
||||||
|
import {
|
||||||
|
RatingsInfo,
|
||||||
|
RatingsNeighbors,
|
||||||
|
RatingsPlayer,
|
||||||
|
} from '../src/commands/ratings';
|
||||||
|
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
||||||
import commitMultipleFiles from './commitMultipleFiles.js';
|
import commitMultipleFiles from './commitMultipleFiles.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -8,37 +14,45 @@ import commitMultipleFiles from './commitMultipleFiles.js';
|
|||||||
* Central European Time (UTC+1): use 0 23 * * *.
|
* Central European Time (UTC+1): use 0 23 * * *.
|
||||||
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
||||||
*
|
*
|
||||||
* :region/ratings/:season/info.json: season info like rewards, start & end time, etc.
|
* [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
|
||||||
* :region/ratings/:season/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
* [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||||
* :region/ratings/:season/leaderboards/:time.json: collection of all recorded leaderboards at local midnights
|
* [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
|
||||||
|
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
|
||||||
|
|
||||||
const publish = argv.includes('--publish');
|
const publish = argv.includes('--publish');
|
||||||
const latest = argv.includes('--latest');
|
const target = argv
|
||||||
|
.find((arg) => arg.startsWith('--target='))
|
||||||
|
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
||||||
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
||||||
|
|
||||||
|
if (!target) throw new Error('Target parameter not specified');
|
||||||
if (!server) throw new Error('Region parameter not specified');
|
if (!server) throw new Error('Region parameter not specified');
|
||||||
|
|
||||||
const NEIGHBORS = 1000;
|
const NEIGHBORS = 2 ** 8;
|
||||||
const PLAYERS = await fetch(
|
const PLAYERS = await patientFetchJSON<RatingsInfo>(
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
)
|
).then((data) => (data.detail === undefined ? data.count : undefined));
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => data.count);
|
|
||||||
|
|
||||||
const players = {};
|
if (PLAYERS === undefined) {
|
||||||
const leaderboard = [];
|
throw new Error('No current ratings season running');
|
||||||
|
}
|
||||||
|
|
||||||
|
const players: Record<number, RatingsPlayer> = {};
|
||||||
|
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
||||||
let registered = 0;
|
let registered = 0;
|
||||||
|
|
||||||
async function branchFromPlayer(id) {
|
async function branchFromPlayer(id: number) {
|
||||||
const { neighbors } = await fetch(
|
const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||||
).then((response) => response.json());
|
);
|
||||||
|
|
||||||
const firstPlayer = neighbors[0];
|
const firstPlayer = neighbors[0];
|
||||||
const lastPlayer = neighbors[neighbors.length - 1];
|
const lastPlayer = neighbors[neighbors.length - 1];
|
||||||
|
|
||||||
const branches = [];
|
const branches: Promise<void>[] = [];
|
||||||
if (!players[firstPlayer.number])
|
if (!players[firstPlayer.number])
|
||||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||||
if (!players[lastPlayer.number])
|
if (!players[lastPlayer.number])
|
||||||
@@ -53,7 +67,7 @@ async function branchFromPlayer(id) {
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Registered ${registered} / ${PLAYERS} (${(
|
`Registered ${registered} / ${PLAYERS} (${(
|
||||||
(registered / PLAYERS) *
|
(registered / PLAYERS!) *
|
||||||
100
|
100
|
||||||
).toFixed(2)}%)`,
|
).toFixed(2)}%)`,
|
||||||
);
|
);
|
||||||
@@ -61,10 +75,10 @@ async function branchFromPlayer(id) {
|
|||||||
await Promise.all(branches);
|
await Promise.all(branches);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function branchFromLeague(id) {
|
async function branchFromLeague(id: number) {
|
||||||
const { result } = await fetch(
|
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||||
).then((response) => response.json());
|
);
|
||||||
|
|
||||||
const firstPlayer = result[0];
|
const firstPlayer = result[0];
|
||||||
const lastPlayer = result[result.length - 1];
|
const lastPlayer = result[result.length - 1];
|
||||||
@@ -75,7 +89,7 @@ async function branchFromLeague(id) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Branching form leagues...');
|
console.log('Branching from leagues...');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
branchFromLeague(0),
|
branchFromLeague(0),
|
||||||
branchFromLeague(1),
|
branchFromLeague(1),
|
||||||
@@ -86,34 +100,37 @@ await Promise.all([
|
|||||||
|
|
||||||
console.log('Converting to an array...');
|
console.log('Converting to an array...');
|
||||||
for (let index = 0; index < PLAYERS; index++) {
|
for (let index = 0; index < PLAYERS; index++) {
|
||||||
leaderboard[index] = players[index + 1];
|
const listing = players[index + 1];
|
||||||
if (!leaderboard[index]) console.warn(`${index} had no player`);
|
if (!listing) {
|
||||||
|
console.warn(`Position ${index + 1} had no player`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderboard[index] = {
|
||||||
|
id: listing.spa_id,
|
||||||
|
score: listing.score,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const leaderboardJSON = JSON.stringify(leaderboard);
|
const leaderboardJSON = JSON.stringify(leaderboard);
|
||||||
|
|
||||||
if (publish) {
|
if (publish) {
|
||||||
const octokit = new Octokit({ auth: env.GH_TOKEN });
|
const octokit = new Octokit({ auth: env.GH_TOKEN });
|
||||||
|
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
|
||||||
const info = await fetch(
|
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
).then((response) => response.json());
|
);
|
||||||
const time = Math.round(Date.now() / 1000);
|
|
||||||
const normalizedServer = server === 'na' ? 'com' : server;
|
const normalizedServer = server === 'na' ? 'com' : server;
|
||||||
|
const infoPath = `regions/${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||||
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
const leaderboardPath = `regions/${normalizedServer}/ratings/${info.current_season}/${target}.json`;
|
||||||
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
|
|
||||||
latest ? 'latest.json' : `leaderboards/${time}.json`
|
|
||||||
}`;
|
|
||||||
const infoJSON = JSON.stringify(info);
|
const infoJSON = JSON.stringify(info);
|
||||||
|
|
||||||
console.log(`Publishing to season ${info.current_season}...`);
|
console.log(`Publishing to season ${info.current_season}...`);
|
||||||
commitMultipleFiles(
|
commitMultipleFiles(
|
||||||
octokit,
|
octokit,
|
||||||
'tresabhi',
|
'tresabhi',
|
||||||
'blitzkrieg-db',
|
'blitzkrieg-assets',
|
||||||
'main',
|
'main',
|
||||||
`${info.current_season}/${time}`,
|
new Date().toString(),
|
||||||
[
|
[
|
||||||
{ path: infoPath, content: infoJSON },
|
{ path: infoPath, content: infoJSON },
|
||||||
{ path: leaderboardPath, content: leaderboardJSON },
|
{ path: leaderboardPath, content: leaderboardJSON },
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import decompress from 'decompress';
|
||||||
|
import { readFile, readdir, rm, writeFile } from 'fs/promises';
|
||||||
|
import { load } from 'js-yaml';
|
||||||
|
import { ElementCompact, xml2js } from 'xml-js';
|
||||||
|
import { NATION_IDS } from '../src/constants/nations';
|
||||||
|
|
||||||
|
const REWRITE = false;
|
||||||
|
const VEHICLE_DEFS = 'assets/Data/XML/item_defs/vehicles';
|
||||||
|
const APK_FILE = 'temp/blitz.apk';
|
||||||
|
const STRINGS = 'assets/Data/Strings';
|
||||||
|
const LANGUAGE = 'en';
|
||||||
|
const DIRECTORIES_OF_INTEREST = [VEHICLE_DEFS, STRINGS];
|
||||||
|
|
||||||
|
if (REWRITE) {
|
||||||
|
await rm('temp/blitz', { recursive: true, force: true });
|
||||||
|
decompress(APK_FILE, 'temp/blitz', {
|
||||||
|
filter: (file) =>
|
||||||
|
DIRECTORIES_OF_INTEREST.some((dir) => file.path.startsWith(dir)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlitzkriegTankopedia {
|
||||||
|
id: number;
|
||||||
|
nation: string;
|
||||||
|
name: string;
|
||||||
|
name_short?: string;
|
||||||
|
is_premium: boolean;
|
||||||
|
is_collector: boolean;
|
||||||
|
tier: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strings = await readFile(
|
||||||
|
`temp/blitz/${STRINGS}/${LANGUAGE}.yaml`,
|
||||||
|
'utf-8',
|
||||||
|
).then((data) => load(data) as Record<string, string>);
|
||||||
|
const tankopedia: Record<number, BlitzkriegTankopedia> = {};
|
||||||
|
const nations = await readdir(`temp/blitz/${VEHICLE_DEFS}`);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
nations
|
||||||
|
.filter((nation) => nation !== 'common')
|
||||||
|
.map(async (nation) => {
|
||||||
|
const listXML = await readFile(
|
||||||
|
`temp/blitz/${VEHICLE_DEFS}/${nation}/list.xml`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const list = xml2js(listXML, { compact: true }) as ElementCompact;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries<ElementCompact>(list.root).map(
|
||||||
|
async ([vehicle, vehicleData]) => {
|
||||||
|
// const vehicleXML = await readFile(
|
||||||
|
// `temp/blitz/${VEHICLE_DEFS}/${nation}/${vehicle}.xml`,
|
||||||
|
// 'utf-8',
|
||||||
|
// ).then((xml) => xml2js(xml, { compact: true }) as ElementCompact);
|
||||||
|
const nationVehicleId = parseInt(list.root[vehicle].id._text);
|
||||||
|
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||||
|
// TODO: use userString property instead
|
||||||
|
const name = strings[`#${nation}_vehicles:${vehicle}`];
|
||||||
|
const name_short = strings[`#${nation}_vehicles:${vehicle}_short`];
|
||||||
|
const is_premium = 'gold' in vehicleData.price;
|
||||||
|
const is_collector = vehicleData.sellPrice
|
||||||
|
? 'gold' in vehicleData.sellPrice
|
||||||
|
: false;
|
||||||
|
const tier = parseInt(vehicleData.level._text);
|
||||||
|
const tags = vehicleData.tags._text.split(' ');
|
||||||
|
const type = tags[0];
|
||||||
|
|
||||||
|
tankopedia[id] = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
name_short,
|
||||||
|
nation,
|
||||||
|
is_premium,
|
||||||
|
is_collector,
|
||||||
|
tier,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFile('dist/tankopedia.json', JSON.stringify(tankopedia, null, 2));
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { readdir } from 'fs/promises';
|
||||||
|
import packageJSON from '../package.json';
|
||||||
|
|
||||||
|
const { version } = packageJSON;
|
||||||
|
const changelogs = await readdir('docs/changelogs');
|
||||||
|
const hasVersion = changelogs.includes(`${version}.md`);
|
||||||
|
|
||||||
|
if (hasVersion) {
|
||||||
|
console.log(`Version ${version} found in changelogs`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Version ${version} not found in changelogs`);
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
|
||||||
export default async function commitMultipleFiles(
|
export default async function commitMultipleFiles(
|
||||||
octokit,
|
octokit: Octokit,
|
||||||
owner,
|
owner: string,
|
||||||
repo,
|
repo: string,
|
||||||
branch,
|
branch: string,
|
||||||
message,
|
message: string,
|
||||||
changes,
|
changes: { path: string; content: string }[],
|
||||||
) {
|
) {
|
||||||
// Get the latest commit SHA on the specified branch
|
// Get the latest commit SHA on the specified branch
|
||||||
const { data: refData } = await octokit.git.getRef({
|
const { data: refData } = await octokit.git.getRef({
|
||||||
@@ -31,12 +33,13 @@ export default async function commitMultipleFiles(
|
|||||||
content: change.content,
|
content: change.content,
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sha: blobData.sha,
|
sha: blobData.sha,
|
||||||
path: change.path,
|
path: change.path,
|
||||||
mode: '100644',
|
mode: '100644',
|
||||||
type: 'blob',
|
type: 'blob',
|
||||||
};
|
} as const;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { Region } from '../../../../../../constants/regions';
|
||||||
|
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { region: Region } },
|
||||||
|
) {
|
||||||
|
return NextResponse.json(await getRatingsInfo(params.region));
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { Region } from '../../../../../../../constants/regions';
|
||||||
|
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { region: Region; league: number } },
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
await getRatingsLeague(params.region, params.league, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { Region } from '../../../../../../../constants/regions';
|
||||||
|
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { region: Region; username: string } },
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
await searchCurrentRatingsPlayers(params.region, params.username),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json(await tankAverages);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { tankopedia } from '../../../core/blitzstars/tankopedia';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json(await tankopedia);
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ContextMenu } from '@radix-ui/themes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import * as Breakdown from '../../../components/Breakdown';
|
||||||
|
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||||
|
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||||
|
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||||
|
import {
|
||||||
|
TankopediaEntry,
|
||||||
|
tankopedia,
|
||||||
|
} from '../../../core/blitzstars/tankopedia';
|
||||||
|
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
||||||
|
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
||||||
|
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
||||||
|
import { resetSession, useSession } from '../../../stores/session';
|
||||||
|
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
||||||
|
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
||||||
|
import { Menu } from '../../tools/session/components/Menu';
|
||||||
|
|
||||||
|
const REFRESH_RATE = 1 / 5;
|
||||||
|
|
||||||
|
export default function SessionPage() {
|
||||||
|
const session = useSession();
|
||||||
|
const since = session.isTracking ? new Date(session.time) : undefined;
|
||||||
|
const [diff, setDiff] = useState<
|
||||||
|
| {
|
||||||
|
careerWN8: number;
|
||||||
|
|
||||||
|
list: {
|
||||||
|
stats: IndividualTankStats;
|
||||||
|
tankopedia?: TankopediaEntry;
|
||||||
|
career: IndividualTankStats;
|
||||||
|
currentWN8?: number;
|
||||||
|
careerWN8?: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(undefined);
|
||||||
|
const sessionBattles = diff?.list.reduce(
|
||||||
|
(accumulator, { stats }) => accumulator + stats.all.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const tankStatsArray = session.isTracking
|
||||||
|
? (Object.values(session.tankStats) as IndividualTankStats[])
|
||||||
|
: [];
|
||||||
|
const sessionWinrate = diff
|
||||||
|
? diff.list.reduce(
|
||||||
|
(accumulator, { stats }) => accumulator + stats.all.wins,
|
||||||
|
0,
|
||||||
|
) / sessionBattles!
|
||||||
|
: 0;
|
||||||
|
const careerBattles = tankStatsArray.reduce(
|
||||||
|
(accumulator, stats) => accumulator + stats.all.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const careerWinrate =
|
||||||
|
tankStatsArray.reduce(
|
||||||
|
(accumulator, stats) => accumulator + stats.all.wins,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
const currentDamage = diff
|
||||||
|
? diff.list.reduce(
|
||||||
|
(accumulator, { stats }) => accumulator + stats.all.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / sessionBattles!
|
||||||
|
: 0;
|
||||||
|
const careerDamage =
|
||||||
|
tankStatsArray.reduce(
|
||||||
|
(accumulator, stats) => accumulator + stats.all.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function recalculateDiff() {
|
||||||
|
if (session.isTracking) {
|
||||||
|
const { id, region } = session;
|
||||||
|
const careerRaw = await fetchBlitz<TanksStats>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||||
|
);
|
||||||
|
const career = careerRaw[id].reduce<
|
||||||
|
Record<number, IndividualTankStats>
|
||||||
|
>(
|
||||||
|
(accumulator, curr) => ({
|
||||||
|
...accumulator,
|
||||||
|
[curr.tank_id]: curr,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const awaitedTankopedia = await tankopedia;
|
||||||
|
const awaitedTankAverages = await tankAverages;
|
||||||
|
const careerWN8 =
|
||||||
|
careerRaw[id].reduce(
|
||||||
|
(accumulator, { tank_id, all }) =>
|
||||||
|
accumulator +
|
||||||
|
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||||
|
? all.battles *
|
||||||
|
calculateWN8(awaitedTankAverages[tank_id].all, all)
|
||||||
|
: 0),
|
||||||
|
0,
|
||||||
|
) /
|
||||||
|
careerRaw[id].reduce(
|
||||||
|
(accumulator, { tank_id, all }) =>
|
||||||
|
accumulator +
|
||||||
|
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||||
|
? all.battles
|
||||||
|
: 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
setDiff({
|
||||||
|
careerWN8,
|
||||||
|
|
||||||
|
list: (
|
||||||
|
Object.values(
|
||||||
|
deltaTankStats(session.tankStats, career),
|
||||||
|
) as IndividualTankStats[]
|
||||||
|
).map((stats) => {
|
||||||
|
return {
|
||||||
|
stats,
|
||||||
|
tankopedia: awaitedTankopedia[stats.tank_id],
|
||||||
|
career: career[stats.tank_id],
|
||||||
|
currentWN8: awaitedTankAverages[stats.tank_id]
|
||||||
|
? calculateWN8(
|
||||||
|
awaitedTankAverages[stats.tank_id].all,
|
||||||
|
stats.all,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
careerWN8: awaitedTankAverages[stats.tank_id]
|
||||||
|
? calculateWN8(
|
||||||
|
awaitedTankAverages[stats.tank_id].all,
|
||||||
|
career[stats.tank_id].all,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recalculateDiff();
|
||||||
|
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const totalRowStats: Record<
|
||||||
|
CustomColumnDisplay,
|
||||||
|
Breakdown.RowStatItem | undefined
|
||||||
|
> = {
|
||||||
|
battles: {
|
||||||
|
title: 'Battles',
|
||||||
|
current: sessionBattles?.toLocaleString(),
|
||||||
|
career: tankStatsArray
|
||||||
|
.reduce((accumulator, stats) => accumulator + stats.all.battles, 0)
|
||||||
|
.toLocaleString(),
|
||||||
|
},
|
||||||
|
winrate: {
|
||||||
|
title: 'Winrate',
|
||||||
|
current: sessionBattles ? `${(100 * sessionWinrate).toFixed(2)}%` : '--',
|
||||||
|
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||||
|
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
|
||||||
|
},
|
||||||
|
wn8: (() => {
|
||||||
|
if (!diff) return undefined;
|
||||||
|
|
||||||
|
const battlesWithWN8 = diff.list.reduce(
|
||||||
|
(accumulator, { currentWN8, stats }) =>
|
||||||
|
(currentWN8 === undefined ? 0 : stats.all.battles) + accumulator,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const currentWN8 =
|
||||||
|
diff.list.reduce(
|
||||||
|
(accumulator, { currentWN8, stats }) =>
|
||||||
|
stats.all.battles * (currentWN8 ?? 0) + accumulator,
|
||||||
|
0,
|
||||||
|
) / battlesWithWN8!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'WN8',
|
||||||
|
current: battlesWithWN8
|
||||||
|
? Math.round(currentWN8!).toLocaleString()
|
||||||
|
: '--',
|
||||||
|
percentile: battlesWithWN8 ? getWN8Percentile(currentWN8!) : undefined,
|
||||||
|
career: Math.round(diff.careerWN8).toLocaleString(),
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
damage: {
|
||||||
|
title: 'Damage',
|
||||||
|
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
|
||||||
|
current: sessionBattles
|
||||||
|
? Math.round(currentDamage).toLocaleString()
|
||||||
|
: '--',
|
||||||
|
career: Math.round(careerDamage).toLocaleString(),
|
||||||
|
},
|
||||||
|
none: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu.Root>
|
||||||
|
<ContextMenu.Trigger>
|
||||||
|
<div>
|
||||||
|
<Breakdown.Root>
|
||||||
|
{session.showTotal &&
|
||||||
|
session.isTracking &&
|
||||||
|
since !== undefined &&
|
||||||
|
diff !== undefined && (
|
||||||
|
<Breakdown.Row
|
||||||
|
minimized={!session.showCareer}
|
||||||
|
title={`Since ${since.toLocaleString(undefined, {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
hour12: true,
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
})}`}
|
||||||
|
stats={session.customColumns.map((customColumn) => {
|
||||||
|
const rowStat = totalRowStats[customColumn.display];
|
||||||
|
|
||||||
|
if (!rowStat) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rowStat,
|
||||||
|
delta:
|
||||||
|
customColumn.showDelta &&
|
||||||
|
rowStat.percentile === undefined
|
||||||
|
? rowStat.delta
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{diff?.list
|
||||||
|
.sort(
|
||||||
|
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
|
||||||
|
)
|
||||||
|
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
|
||||||
|
const rowStats: Record<
|
||||||
|
CustomColumnDisplay,
|
||||||
|
Breakdown.RowStatItem | undefined
|
||||||
|
> = {
|
||||||
|
battles: {
|
||||||
|
title: 'Battles',
|
||||||
|
current: stats.all.battles.toLocaleString(),
|
||||||
|
career: career.all.battles.toLocaleString(),
|
||||||
|
},
|
||||||
|
winrate: {
|
||||||
|
title: 'Winrate',
|
||||||
|
current: `${(
|
||||||
|
100 *
|
||||||
|
(stats.all.wins / stats.all.battles)
|
||||||
|
).toFixed(2)}%`,
|
||||||
|
career: `${(
|
||||||
|
100 *
|
||||||
|
(career.all.wins / career.all.battles)
|
||||||
|
).toFixed(2)}%`,
|
||||||
|
delta:
|
||||||
|
stats.all.wins / stats.all.battles -
|
||||||
|
career.all.wins / career.all.battles,
|
||||||
|
},
|
||||||
|
wn8: {
|
||||||
|
title: 'WN8',
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
title: 'Damage',
|
||||||
|
current: Math.round(
|
||||||
|
stats.all.damage_dealt / stats.all.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
career: Math.round(
|
||||||
|
career.all.damage_dealt / career.all.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
delta:
|
||||||
|
stats.all.damage_dealt / stats.all.battles -
|
||||||
|
career.all.damage_dealt / career.all.battles,
|
||||||
|
},
|
||||||
|
none: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breakdown.Row
|
||||||
|
key={stats.tank_id}
|
||||||
|
minimized={!session.showCareer}
|
||||||
|
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
|
||||||
|
type="tank"
|
||||||
|
tankType={tankopedia?.type}
|
||||||
|
treeType={(() => {
|
||||||
|
if (tankopedia?.is_collectible)
|
||||||
|
return TreeTypeEnum.Collector;
|
||||||
|
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
|
||||||
|
})()}
|
||||||
|
stats={session.customColumns.map((customColumn) => {
|
||||||
|
const rowStat = rowStats[customColumn.display];
|
||||||
|
|
||||||
|
if (!rowStat) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rowStat,
|
||||||
|
delta:
|
||||||
|
customColumn.showDelta &&
|
||||||
|
rowStat.percentile === undefined
|
||||||
|
? rowStat.delta
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Breakdown.Root>
|
||||||
|
</div>
|
||||||
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
|
<Menu reset={resetSession} Builder={ContextMenu} />
|
||||||
|
</ContextMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Flex, Theme } from '@radix-ui/themes';
|
||||||
|
import '@radix-ui/themes/styles.css';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { Roboto_Flex } from 'next/font/google';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import Navbar from '../components/Navbar';
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
interface RootLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const robotoFlex = Roboto_Flex({
|
||||||
|
subsets: ['latin'],
|
||||||
|
display: 'swap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isEmbed = pathname.split('/')[1] === 'embeds';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="en" className={robotoFlex.className}>
|
||||||
|
<head>
|
||||||
|
<title>Blitzkrieg</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="🎉 Tools for everything World of Tanks Blitz"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
background: isEmbed
|
||||||
|
? 'transparent'
|
||||||
|
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Theme appearance="dark" panelBackground="translucent" radius="full">
|
||||||
|
{!isEmbed && <Navbar />}
|
||||||
|
|
||||||
|
<Flex direction="column">{children}</Flex>
|
||||||
|
</Theme>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { styleVariants } from '@vanilla-extract/css';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
export const tool = styleVariants({
|
||||||
|
enabled: {
|
||||||
|
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
|
||||||
|
':hover': {
|
||||||
|
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Flex, Text, TextField } from '@radix-ui/themes';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { Search, SearchItem } from 'react-fuzzysort';
|
||||||
|
import PageWrapper from '../components/PageWrapper';
|
||||||
|
import { TOOLS } from '../constants/tools';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
import * as styles from './page.css';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const input = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Slot>
|
||||||
|
<MagnifyingGlassIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
<TextField.Input ref={input} placeholder="Search tools..." />
|
||||||
|
</TextField.Root>
|
||||||
|
|
||||||
|
<Flex gap="3" wrap="wrap" align="center" justify="center">
|
||||||
|
<Search
|
||||||
|
input={input}
|
||||||
|
list={TOOLS.map(
|
||||||
|
(tool) =>
|
||||||
|
({
|
||||||
|
node: (
|
||||||
|
<Link
|
||||||
|
href={`/tools/${tool.id}`}
|
||||||
|
className={
|
||||||
|
tool.disabled ? styles.tool.disabled : styles.tool.enabled
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
|
||||||
|
cursor: tool.disabled ? 'default' : 'pointer',
|
||||||
|
opacity: tool.disabled ? 0.25 : 1,
|
||||||
|
height: 128,
|
||||||
|
minWidth: 256,
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 8,
|
||||||
|
textDecoration: 'none',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: 16,
|
||||||
|
gap: 4,
|
||||||
|
transition: `box-shadow ${theme.durations.regular}`,
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (tool.disabled) event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tool.title}
|
||||||
|
<CaretRightIcon style={{ width: '1em', height: '1em' }} />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tool.disabled ? 'Coming soon!' : tool.description}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
query: `${tool.title} ${tool.description}`,
|
||||||
|
}) satisfies SearchItem,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text size="1" color="gray" align="center">
|
||||||
|
Images courtesy of Wargaming Art Department
|
||||||
|
</Text>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { TOOLS } from '../../constants/tools';
|
||||||
|
import { theme } from '../../stitches.config';
|
||||||
|
|
||||||
|
interface ToolsLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const toolId = pathname.split('/').at(-1);
|
||||||
|
const tool = TOOLS.find((tool) => tool.id === toolId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 128,
|
||||||
|
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
backgroundImage: `url(/assets/banners/${toolId}.webp)`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
filter: 'blur(4px)',
|
||||||
|
transform: 'scale(calc((128 + 8) / 128))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontSize: 32,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tool?.title}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tool?.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
const spinKeyframes = keyframes({
|
||||||
|
from: { transform: 'rotate(0deg)' },
|
||||||
|
to: { transform: 'rotate(360deg)' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const infiniteSpin = style({
|
||||||
|
animationName: spinKeyframes,
|
||||||
|
animationIterationCount: 'infinite',
|
||||||
|
animationTimingFunction: 'linear',
|
||||||
|
animationDuration: '1s',
|
||||||
|
});
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
CaretLeftIcon,
|
||||||
|
CaretRightIcon,
|
||||||
|
SymbolIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import { Button, Flex, TextField } from '@radix-ui/themes';
|
||||||
|
import { range } from 'lodash';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { infiniteSpin } from './index.css';
|
||||||
|
|
||||||
|
interface PageTurnerProps {
|
||||||
|
leaderboard: Record<number, number>;
|
||||||
|
page: number;
|
||||||
|
rowsPerPage: number;
|
||||||
|
pages: number;
|
||||||
|
onPageChange: (page: number, isButtonClick: boolean) => void;
|
||||||
|
totalPlayers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageTurner({
|
||||||
|
page,
|
||||||
|
rowsPerPage,
|
||||||
|
onPageChange,
|
||||||
|
pages,
|
||||||
|
leaderboard,
|
||||||
|
totalPlayers,
|
||||||
|
}: PageTurnerProps) {
|
||||||
|
const pageInput = useRef<HTMLInputElement>(null);
|
||||||
|
const rightLoading = range(
|
||||||
|
page * rowsPerPage,
|
||||||
|
Math.min(totalPlayers - 1, page * rowsPerPage + rowsPerPage + 1),
|
||||||
|
).some((index) => !(index in leaderboard));
|
||||||
|
const leftLoading = range(
|
||||||
|
Math.max(0, page * rowsPerPage - 1),
|
||||||
|
Math.max(0, page * rowsPerPage - rowsPerPage - 1) + 1,
|
||||||
|
).some((index) => !(index in leaderboard));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justify="center" wrap="wrap" gap="2">
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => onPageChange(Math.max(page - 1, 0), true)}
|
||||||
|
disabled={page === 0 || leftLoading}
|
||||||
|
>
|
||||||
|
{leftLoading ? (
|
||||||
|
<SymbolIcon className={infiniteSpin} />
|
||||||
|
) : (
|
||||||
|
<CaretLeftIcon />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Slot>Page</TextField.Slot>
|
||||||
|
<TextField.Input
|
||||||
|
type="number"
|
||||||
|
ref={pageInput}
|
||||||
|
style={{ width: 64, textAlign: 'center' }}
|
||||||
|
onBlur={(event) => {
|
||||||
|
onPageChange(
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(pages - 1, event.target.valueAsNumber - 1),
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
(event.target as HTMLInputElement).blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField.Slot>out of {pages}</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => {
|
||||||
|
onPageChange(Math.min(page + 1, pages - 1), true);
|
||||||
|
}}
|
||||||
|
disabled={page === pages - 1 || rightLoading}
|
||||||
|
>
|
||||||
|
{rightLoading ? (
|
||||||
|
<SymbolIcon className={infiniteSpin} />
|
||||||
|
) : (
|
||||||
|
<CaretRightIcon />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
|
||||||
|
import mutateSession, { useSession } from '../../../../stores/session';
|
||||||
|
|
||||||
|
export type CustomColumnDisplay =
|
||||||
|
| 'battles'
|
||||||
|
| 'winrate'
|
||||||
|
| 'wn8'
|
||||||
|
| 'damage'
|
||||||
|
| 'none';
|
||||||
|
|
||||||
|
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
|
||||||
|
|
||||||
|
interface CustomColumn {
|
||||||
|
Builder: typeof DropdownMenu | typeof ContextMenu;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomColumn({ column, Builder }: CustomColumn) {
|
||||||
|
const customColumn = useSession((state) => state.customColumns[column]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Builder.Sub>
|
||||||
|
<Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger>
|
||||||
|
|
||||||
|
<Builder.SubContent>
|
||||||
|
<Builder.Label>Display</Builder.Label>
|
||||||
|
|
||||||
|
<Builder.RadioGroup
|
||||||
|
value={customColumn.display}
|
||||||
|
onValueChange={(event) =>
|
||||||
|
mutateSession((draft) => {
|
||||||
|
draft.customColumns[column].display =
|
||||||
|
event as CustomColumnDisplay;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Builder.RadioItem value="battles">Battles</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="winrate">Winrate</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="wn8">WN8</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="damage">Damage</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="none">None</Builder.RadioItem>
|
||||||
|
</Builder.RadioGroup>
|
||||||
|
|
||||||
|
{!NO_DELTA.includes(customColumn.display) && (
|
||||||
|
<>
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={customColumn.showDelta}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
mutateSession((draft) => {
|
||||||
|
draft.customColumns[column].showDelta = checked;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show delta
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Builder.SubContent>
|
||||||
|
</Builder.Sub>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
|
||||||
|
import { useSession } from '../../../../stores/session';
|
||||||
|
import { CustomColumn } from './CustomColumn';
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
Builder: typeof DropdownMenu | typeof ContextMenu;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Menu({ Builder, reset }: ContextMenuProps) {
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Builder.Content>
|
||||||
|
<CustomColumn Builder={Builder} column={0} />
|
||||||
|
<CustomColumn Builder={Builder} column={1} />
|
||||||
|
<CustomColumn Builder={Builder} column={2} />
|
||||||
|
<CustomColumn Builder={Builder} column={3} />
|
||||||
|
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={session.showTotal}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({ showTotal: checked })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show total
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={session.showCareer}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({ showCareer: checked })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show career
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.Item color="red" onClick={reset}>
|
||||||
|
Reset
|
||||||
|
</Builder.Item>
|
||||||
|
</Builder.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
|
export const toolBar = style({
|
||||||
|
'@media': {
|
||||||
|
'screen and (min-width: 640px)': {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
flexDirection: 'column',
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toolbarButton = style({
|
||||||
|
'@media': {
|
||||||
|
'screen and (min-width: 640px)': {
|
||||||
|
flex: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const searchButton = style({
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
fontSize: 16,
|
||||||
|
display: 'flex',
|
||||||
|
padding: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: theme.colors.componentInteractiveHover,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CaretDownIcon,
|
||||||
|
CopyIcon,
|
||||||
|
PersonIcon,
|
||||||
|
ReloadIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
DropdownMenu,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
} from '@radix-ui/themes';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
|
import { REGION_NAMES } from '../../../constants/regions';
|
||||||
|
import searchPlayersAcrossRegions, {
|
||||||
|
AccountListWithServer,
|
||||||
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||||
|
import { resetSession, setSession, useSession } from '../../../stores/session';
|
||||||
|
import SessionPage from '../../embeds/session/page';
|
||||||
|
import { Menu } from './components/Menu';
|
||||||
|
import * as styles from './page.css';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const input = useRef<HTMLInputElement>(null);
|
||||||
|
const [searchResults, setSearchResults] = useState<
|
||||||
|
AccountListWithServer | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [showSearchResults, setShowSearchResults] = useState(false);
|
||||||
|
const handleChange = debounce(
|
||||||
|
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (event.target.value) {
|
||||||
|
setSearchResults(await searchPlayersAcrossRegions(event.target.value));
|
||||||
|
} else {
|
||||||
|
setSearchResults(undefined);
|
||||||
|
setShowSearchResults(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session.isTracking) input.current!.value = session.nickname;
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper color="blue">
|
||||||
|
<div className={styles.toolBar}>
|
||||||
|
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Slot>
|
||||||
|
<PersonIcon height="16" width="16" />
|
||||||
|
</TextField.Slot>
|
||||||
|
|
||||||
|
<TextField.Input
|
||||||
|
defaultValue={session.isTracking ? session.nickname : undefined}
|
||||||
|
ref={input}
|
||||||
|
onChange={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (event.target.value) {
|
||||||
|
setShowSearchResults(true);
|
||||||
|
setSearchResults(undefined);
|
||||||
|
} else {
|
||||||
|
setShowSearchResults(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event);
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 is fixed
|
||||||
|
if (showSearchResults) event.target.focus();
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setShowSearchResults(false);
|
||||||
|
input.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Search for a player..."
|
||||||
|
/>
|
||||||
|
</TextField.Root>
|
||||||
|
|
||||||
|
<DropdownMenu.Root open={showSearchResults} modal={false}>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<div />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{searchResults === undefined ? (
|
||||||
|
<DropdownMenu.Item disabled>Searching...</DropdownMenu.Item>
|
||||||
|
) : searchResults.length === 0 ? (
|
||||||
|
<DropdownMenu.Item disabled>No results</DropdownMenu.Item>
|
||||||
|
) : (
|
||||||
|
searchResults?.map(
|
||||||
|
({ account_id: id, nickname, region }, index) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={id}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSearchResults(false);
|
||||||
|
setSession(region, id, nickname);
|
||||||
|
}}
|
||||||
|
shortcut={REGION_NAMES[region]}
|
||||||
|
>
|
||||||
|
{nickname}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!session.resetPrompt) {
|
||||||
|
event.preventDefault();
|
||||||
|
resetSession();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={styles.toolbarButton}
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
<ReloadIcon width="16" height="16" /> Reset
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<Flex direction="column" gap="4">
|
||||||
|
<Flex direction="column" gap="0">
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Are you sure you want to reset?
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
This'll discard your current session and start tracking from
|
||||||
|
scratch again.
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Text size="2">
|
||||||
|
<Checkbox
|
||||||
|
mr="1"
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({
|
||||||
|
resetPrompt: !checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Don't ask me again
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4">
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
<Button variant="soft" color="gray">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
|
||||||
|
<AlertDialog.Action>
|
||||||
|
<Button variant="solid" color="red" onClick={resetSession}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
className={styles.toolbarButton}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!session.embedPrompt) {
|
||||||
|
event.preventDefault();
|
||||||
|
resetSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${location.origin}/embeds/session`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon width="16" height="16" /> Embed
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Copied link to your clipboard
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
Instructions for streamers:
|
||||||
|
<iframe
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
|
||||||
|
title="YouTube video player"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
></iframe>
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<Text size="2">
|
||||||
|
<Checkbox
|
||||||
|
mr="1"
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({
|
||||||
|
embedPrompt: !checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Don't show this again
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4">
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
<Button variant="soft">Got it!</Button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="soft" className={styles.toolbarButton}>
|
||||||
|
Options
|
||||||
|
<CaretDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<Menu Builder={DropdownMenu} reset={resetSession} />
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SessionPage />
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
import { Client, GatewayIntentBits } from 'discord.js';
|
import { registerProcesses } from './core/blitzkrieg/processes';
|
||||||
import { DISCORD_TOKEN } from './core/node/arguments';
|
import { secrets } from './core/blitzkrieg/secrets';
|
||||||
import { registerErrorHandlers } from './events/error';
|
import { client } from './core/discord/client';
|
||||||
import guildMemberAdd from './events/guildMemberAdd';
|
|
||||||
import interactionCreate from './events/interactionCreate';
|
|
||||||
import ready from './events/ready';
|
|
||||||
|
|
||||||
export const client = new Client({
|
// console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
|
||||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
|
|
||||||
})
|
|
||||||
.on('ready', ready)
|
|
||||||
.on('guildMemberAdd', guildMemberAdd)
|
|
||||||
.on('interactionCreate', interactionCreate);
|
|
||||||
|
|
||||||
registerErrorHandlers();
|
registerProcesses();
|
||||||
client.login(DISCORD_TOKEN);
|
client.login(secrets.DISCORD_TOKEN);
|
||||||
@@ -8,16 +8,18 @@ const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/doc
|
|||||||
const DOCS = {
|
const DOCS = {
|
||||||
permissions: 'guide/permissions',
|
permissions: 'guide/permissions',
|
||||||
embeds: 'guide/embeds',
|
embeds: 'guide/embeds',
|
||||||
about: 'guide/about',
|
introduction: 'guide/introduction',
|
||||||
invite: 'guide/invite',
|
invite: 'guide/invite',
|
||||||
timezones: 'guide/timezones',
|
timezones: 'guide/timezones',
|
||||||
|
creators: 'guide/creators',
|
||||||
};
|
};
|
||||||
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||||
permissions: 'The permissions needed for the bot to function',
|
permissions: 'The permissions needed for the bot to function',
|
||||||
embeds: 'How embeds work',
|
embeds: 'How embeds work',
|
||||||
about: 'About the bot',
|
introduction: 'About the bot',
|
||||||
invite: 'How to invite the bot',
|
invite: 'How to invite the bot',
|
||||||
timezones: 'How Blitzkrieg infers and uses your timezone',
|
timezones: 'How Blitzkrieg infers and uses your timezone',
|
||||||
|
creators: 'Who created the bot',
|
||||||
};
|
};
|
||||||
|
|
||||||
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
||||||
@@ -28,15 +30,14 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
|||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helpCommand: CommandRegistry = {
|
export const aboutCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: addDocsSubcommands(
|
command: addDocsSubcommands(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('help')
|
.setName('about')
|
||||||
.setDescription('All the help you need about the bot')
|
.setDescription('All the info you need about the bot')
|
||||||
.addSubcommand((option) =>
|
.addSubcommand((option) =>
|
||||||
option
|
option
|
||||||
.setName('commands')
|
.setName('commands')
|
||||||
@@ -44,13 +45,15 @@ export const helpCommand: CommandRegistry = {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
handler(interaction) {
|
async handler(interaction) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
if (subcommand === 'commands') {
|
if (subcommand === 'commands') {
|
||||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${COMMANDS_RAW.filter(
|
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
||||||
(registry) => registry.inPublic && registry.inProduction,
|
await Promise.all(COMMANDS_RAW)
|
||||||
)
|
)
|
||||||
|
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||||
|
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
|
||||||
.map(
|
.map(
|
||||||
(registry) =>
|
(registry) =>
|
||||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { chunk } from 'lodash';
|
||||||
|
import * as Breakdown from '../components/Breakdown';
|
||||||
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
|
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 buttonPrimary from '../core/discord/buttonPrimary';
|
||||||
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
|
import resolvePlayerFromCommand, {
|
||||||
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
|
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
||||||
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
const ROWS_PER_PAGE = 8;
|
||||||
|
const MAX_PAGES = 10;
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const awaitedTankopedia = await tankopedia;
|
||||||
|
const awaitedTankAverages = await tankAverages;
|
||||||
|
const statsInPeriod = await getStatsInPeriod(region, id, start, end);
|
||||||
|
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);
|
||||||
|
const orderedCurrentStats: AllStats[] = [];
|
||||||
|
const orderedCareerStats: AllStats[] = [];
|
||||||
|
const orderedCurrentWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8Full: (number | undefined)[] = [];
|
||||||
|
|
||||||
|
filteredOrder.forEach((id) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages[id];
|
||||||
|
const current = statsInPeriod.diff[id];
|
||||||
|
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
|
||||||
|
const currentWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, current)
|
||||||
|
: undefined;
|
||||||
|
const careerWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, career)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
orderedCurrentStats.push(current);
|
||||||
|
orderedCareerStats.push(career);
|
||||||
|
orderedCurrentWN8.push(currentWN8);
|
||||||
|
orderedCareerWN8.push(careerWN8);
|
||||||
|
});
|
||||||
|
tankStats.forEach((tank) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages[tank.tank_id];
|
||||||
|
|
||||||
|
orderedCareerWN8Full.push(
|
||||||
|
tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, 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: JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (filteredOrder.length > 0) {
|
||||||
|
children.push(
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
type="summary"
|
||||||
|
title="Total"
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: 'Battles',
|
||||||
|
current: currentBattles,
|
||||||
|
career: careerBattles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Winrate',
|
||||||
|
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
||||||
|
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||||
|
delta: currentWinrate - careerWinrate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WN8',
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Damage',
|
||||||
|
current: Math.round(currentDamage).toLocaleString(),
|
||||||
|
career: Math.round(careerDamage).toLocaleString(),
|
||||||
|
delta: currentDamage - careerDamage,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
filteredOrder.map(async (id, index) => {
|
||||||
|
const tankopediaEntry = awaitedTankopedia[id];
|
||||||
|
const current = orderedCurrentStats[index];
|
||||||
|
const career = orderedCareerStats[index];
|
||||||
|
const currentWN8 = orderedCurrentWN8[index];
|
||||||
|
const careerWN8 = orderedCareerWN8[index];
|
||||||
|
|
||||||
|
children.push(
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
type="tank"
|
||||||
|
tankType={tankopediaEntry?.type}
|
||||||
|
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||||
|
title={await resolveTankName(id)}
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: 'Battles',
|
||||||
|
current: current.battles.toLocaleString(),
|
||||||
|
career: career.battles.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '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: 'WN8',
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '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) => (
|
||||||
|
<Wrapper>
|
||||||
|
{index === 0 && (
|
||||||
|
<TitleBar
|
||||||
|
name={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>
|
||||||
|
</Wrapper>
|
||||||
|
))
|
||||||
|
.reverse();
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={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}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{filteredOrder.length === 0 && (
|
||||||
|
<NoData type={NoDataType.BattlesInPeriod} />
|
||||||
|
)}
|
||||||
|
</Wrapper>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
||||||
|
async (resolve) => {
|
||||||
|
const command = await addFilterOptions(
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('breakdown')
|
||||||
|
.setDescription("A period's breakdown by tanks played"),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(await render(player, period, filters)),
|
||||||
|
buttonPrimary(path, 'Refresh'),
|
||||||
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
|
return await render(player, period, filters);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
|
||||||
|
// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
|
||||||
|
// | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
|
||||||
|
// | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
|
||||||
|
// _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
|
||||||
|
// |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
|
||||||
|
|
||||||
|
// import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
|
||||||
|
// import markdownEscape from 'markdown-escape';
|
||||||
|
// import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
||||||
|
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
// import { getClanInfo } from '../core/blitz/getClanInfo';
|
||||||
|
// import { PlayerHistoriesRaw } from '../LEGACY_core/blitzstars/getPlayerHistories';
|
||||||
|
// import getTimeDaysAgo from '../LEGACY_core/blitzstars/getTimeDaysAgo';
|
||||||
|
// import addClanChoices from '../LEGACY_core/discord/addClanChoices';
|
||||||
|
// import autocompleteClan from '../LEGACY_core/discord/autocompleteClan';
|
||||||
|
// import embedWarning from '../LEGACY_core/discord/embedWarning';
|
||||||
|
// import resolveClanFromCommand from '../LEGACY_core/discord/resolveClanFromCommand';
|
||||||
|
// import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
// export const clanEventCommand: CommandRegistry = {
|
||||||
|
// inProduction: false,
|
||||||
|
// inPublic: true,
|
||||||
|
|
||||||
|
// command: new SlashCommandBuilder()
|
||||||
|
// .setName('clan-event')
|
||||||
|
// .setDescription('Lists all inactive players')
|
||||||
|
// .addStringOption(addClanChoices),
|
||||||
|
|
||||||
|
// async handler(interaction) {
|
||||||
|
// const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
|
// const clan = await getClanInfo(region, id);
|
||||||
|
// const [players, achievements, previousJointVictories] = await Promise.all([
|
||||||
|
// getAccountInfo(region, clan.members_ids),
|
||||||
|
// getAccountAchievements(region, clan.members_ids),
|
||||||
|
// Promise.all(
|
||||||
|
// clan.members_ids.map((id) =>
|
||||||
|
// fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
|
||||||
|
// .then((response) => response.json() as Promise<PlayerHistoriesRaw>)
|
||||||
|
// .then((data) => ({
|
||||||
|
// id,
|
||||||
|
// jointVictory: data[0]?.achievements.jointVictoryCount as
|
||||||
|
// | number
|
||||||
|
// | undefined,
|
||||||
|
// })),
|
||||||
|
// ),
|
||||||
|
// ).then((data) =>
|
||||||
|
// data.reduce<Record<number, number | undefined>>(
|
||||||
|
// (accumulator, value) => ({
|
||||||
|
// ...accumulator,
|
||||||
|
// [value.id]: value.jointVictory,
|
||||||
|
// }),
|
||||||
|
// {},
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ]);
|
||||||
|
// const jointVictories = clan.members_ids.map((id) => ({
|
||||||
|
// id,
|
||||||
|
// joinVictory:
|
||||||
|
// players[id].last_battle_time > getTimeDaysAgo(region, 1)
|
||||||
|
// ? previousJointVictories[id] === undefined
|
||||||
|
// ? 0
|
||||||
|
// : achievements[id].max_series.jointVictory -
|
||||||
|
// previousJointVictories[id]!
|
||||||
|
// : 0,
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// return [
|
||||||
|
// `# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
|
||||||
|
// clan.tag,
|
||||||
|
// )}]'s platoon wins today: ${(
|
||||||
|
// jointVictories.reduce(
|
||||||
|
// (accumulator, { joinVictory }) => accumulator + joinVictory,
|
||||||
|
// 0,
|
||||||
|
// ) / 2
|
||||||
|
// ).toFixed(0)}\n${jointVictories
|
||||||
|
// .filter(({ joinVictory }) => joinVictory)
|
||||||
|
// .sort((a, b) => b.joinVictory - a.joinVictory)
|
||||||
|
// .map(
|
||||||
|
// ({ id, joinVictory }) =>
|
||||||
|
// `- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
|
||||||
|
// )
|
||||||
|
// .join('\n')}`,
|
||||||
|
|
||||||
|
// embedWarning(
|
||||||
|
// 'This is an approximation!',
|
||||||
|
// "Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/about timezones` for more info)",
|
||||||
|
// ),
|
||||||
|
// ];
|
||||||
|
// },
|
||||||
|
|
||||||
|
// autocomplete: autocompleteClan,
|
||||||
|
// };
|
||||||
@@ -1,18 +1,12 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||||
import { client } from '../bot';
|
import { client } from '../core/discord/client';
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
|
||||||
import markdownTable from '../core/discord/markdownTable';
|
|
||||||
import getClientId from '../core/node/getClientId';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
const executionStart = new Date().getTime();
|
const executionStart = new Date().getTime();
|
||||||
|
|
||||||
export const debugCommand: CommandRegistry = {
|
export const debugCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
@@ -22,24 +16,21 @@ export const debugCommand: CommandRegistry = {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
const uptime = currentTime - executionStart;
|
const uptime = currentTime - executionStart;
|
||||||
|
const list = [
|
||||||
|
['Version', packageJSON.version],
|
||||||
|
['Shard', client.shard?.ids[0] ?? 'default'],
|
||||||
|
[
|
||||||
|
'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`,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
.map(([key, value]) => `- ${key}: ${value}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
return embedInfo(
|
return `# Debug\n\n${list}`;
|
||||||
`${client.user?.username} debug information`,
|
|
||||||
markdownTable([
|
|
||||||
['Version', packageJSON.version],
|
|
||||||
['Client ID', getClientId()],
|
|
||||||
['Tag', interaction.client.user.tag],
|
|
||||||
[
|
|
||||||
'Uptime',
|
|
||||||
`${Math.floor(uptime / 1000 / 60 / 60 / 24)}d ${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`,
|
|
||||||
],
|
|
||||||
['Tankopedia cached', `${(await tankopedia) !== undefined}`],
|
|
||||||
['Tank averages cached', `${(await tankAverages) !== undefined}`],
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
||||||
import sumStats from '../core/blitz/sumStats';
|
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
|
|
||||||
import getPeriodNow from '../core/blitzstars/getPeriodNow';
|
|
||||||
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
|
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import embedPositive from '../core/discord/embedPositive';
|
import embedPositive from '../core/discord/embedPositive';
|
||||||
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
|
import sumStats from '../core/statistics/sumStats';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||||
|
|
||||||
export type SkilledClan = 'SKLLD' | 'SMRI';
|
export type SkilledClan = 'SKLLD' | 'SMRI';
|
||||||
@@ -26,7 +24,6 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
|
|||||||
|
|
||||||
export const eligibleCommand: CommandRegistry = {
|
export const eligibleCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: false,
|
inPublic: false,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
@@ -49,20 +46,18 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const clan = interaction.options.getString('clan') as SkilledClan;
|
const clan = interaction.options.getString('clan') as SkilledClan;
|
||||||
const { id, server } = await resolvePlayerFromCommand(interaction);
|
const { id, region: region } = await resolvePlayerFromCommand(interaction);
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const problems: TableInput = [];
|
const problems: TableInput = [];
|
||||||
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
||||||
let isEligible = false;
|
let isEligible = false;
|
||||||
let body = '';
|
let body = '';
|
||||||
|
|
||||||
if (clan === 'SKLLD') {
|
if (clan === 'SKLLD') {
|
||||||
const { diffed } = await getDiffedTankStats(
|
const { diff: diffed } = await getStatsInPeriod(
|
||||||
server,
|
region,
|
||||||
id,
|
id,
|
||||||
getTimeDaysAgo(server, 30),
|
getTimeDaysAgo(region, 30),
|
||||||
getPeriodNow(),
|
getPeriodNow(),
|
||||||
);
|
);
|
||||||
const entries = Object.entries(diffed);
|
const entries = Object.entries(diffed);
|
||||||
@@ -172,7 +167,7 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
problems,
|
problems,
|
||||||
)}`;
|
)}`;
|
||||||
} else if (clan === 'SMRI') {
|
} else if (clan === 'SMRI') {
|
||||||
const stats = accountInfo[id].statistics.all;
|
const stats = accountInfo.statistics.all;
|
||||||
|
|
||||||
if (stats.battles < 1500) {
|
if (stats.battles < 1500) {
|
||||||
problems.push([
|
problems.push([
|
||||||
|
|||||||