Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
31f6123a55 | ||
|
|
18a3e3092c | ||
|
|
5ded0f8744 | ||
|
|
4e0f0536fa | ||
|
|
3f081a5577 | ||
|
|
2447d663fa | ||
|
|
8a0afee97a | ||
|
|
3cca68a8f0 | ||
|
|
622cd92a13 | ||
|
|
6d9e6973d5 | ||
|
|
098e6c09b9 | ||
|
|
186d45e3d9 | ||
|
|
0fdc87a88c | ||
|
|
1d4ac2f133 | ||
|
|
21158e9081 | ||
|
|
fedad8df3e | ||
|
|
6c8f5ca478 | ||
|
|
463af106be | ||
|
|
feeb850ca9 | ||
|
|
80ffff322c | ||
|
|
0833375562 | ||
|
|
297f2f6580 | ||
|
|
438feefa83 | ||
|
|
8f6388b42b | ||
|
|
405f544774 | ||
|
|
dd0644e09f | ||
|
|
d8ac0b2ef6 | ||
|
|
87955214e3 | ||
|
|
f985cbf70b | ||
|
|
f64e72c312 | ||
|
|
ce6715cf2c | ||
|
|
18e5405c54 | ||
|
|
a0f15fc1c4 | ||
|
|
cf20ae38d4 | ||
|
|
2bf5eb78be | ||
|
|
57271e3c23 | ||
|
|
2258b0f936 | ||
|
|
aa8115d260 | ||
|
|
f8dfa386a0 | ||
|
|
00771e130f | ||
|
|
bd612bf777 | ||
|
|
ca208e44a1 | ||
|
|
72c4cd664b | ||
|
|
f907c79d68 | ||
|
|
ae3d0a08cd | ||
|
|
fb66027de0 |
No files matched your search
@@ -0,0 +1,34 @@
|
|||||||
|
name: CD Asia 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=asia --latest
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: CD Asia Ratings Leaderboards
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 17 * * *"
|
||||||
|
|
||||||
|
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=asia
|
||||||
@@ -16,18 +16,14 @@ jobs:
|
|||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [18.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get code
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install Yarn
|
- name: Install Yarn
|
||||||
@@ -51,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
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: CD Europe 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=eu --latest
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: CD Europe Ratings Leaderboards
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 23 * * *"
|
||||||
|
|
||||||
|
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=eu
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
name: Host
|
name: CD Host
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */5 * * *'
|
- cron: '0 */6 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -12,24 +12,21 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
host:
|
host:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [18.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: 18
|
||||||
|
|
||||||
- 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
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: CD North America Ratings Leaderboards
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 5 * * *'
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: CI Build
|
name: CI Build Bot
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
@@ -11,18 +11,14 @@ jobs:
|
|||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [18.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get code
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Use node
|
- name: Use node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: 18
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install Yarn
|
- name: Install Yarn
|
||||||
@@ -38,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
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: CI Build Docs
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
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: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, pull_request]
|
||||||
|
|
||||||
|
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
|
||||||
@@ -5,9 +5,12 @@ package-lock.json
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
dist/
|
dist/
|
||||||
|
dist.*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
dist.zip
|
|
||||||
commands.txt
|
|
||||||
/test.*
|
/test.*
|
||||||
|
test.*
|
||||||
/docs/.vitepress/dist
|
/docs/.vitepress/dist
|
||||||
/docs/.vitepress/cache
|
/docs/.vitepress/cache
|
||||||
|
servers.csv
|
||||||
|
.next/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
tasks:
|
|
||||||
- init: yarn
|
|
||||||
- command: yarn run pw
|
|
||||||
@@ -5,14 +5,23 @@
|
|||||||
"bepaint",
|
"bepaint",
|
||||||
"blitzstars",
|
"blitzstars",
|
||||||
"cleandist",
|
"cleandist",
|
||||||
|
"cockroackdb",
|
||||||
"customperiod",
|
"customperiod",
|
||||||
|
"deburr",
|
||||||
|
"diacriticless",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
|
"fullstats",
|
||||||
"fuzzysort",
|
"fuzzysort",
|
||||||
"kranvagn",
|
"kranvagn",
|
||||||
|
"Leaderboard",
|
||||||
|
"leaderboards",
|
||||||
|
"Oopsy",
|
||||||
|
"outdir",
|
||||||
"outfile",
|
"outfile",
|
||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
"playerinfo",
|
"playerinfo",
|
||||||
|
"ratingsleaderboard",
|
||||||
"Resvg",
|
"Resvg",
|
||||||
"roboto",
|
"roboto",
|
||||||
"searchclans",
|
"searchclans",
|
||||||
@@ -22,16 +31,22 @@
|
|||||||
"Smri",
|
"Smri",
|
||||||
"startcase",
|
"startcase",
|
||||||
"statsfull",
|
"statsfull",
|
||||||
|
"Stringifying",
|
||||||
"strvk",
|
"strvk",
|
||||||
|
"subnation",
|
||||||
"tankaverages",
|
"tankaverages",
|
||||||
"tankopedia",
|
"tankopedia",
|
||||||
"tankstats",
|
"tankstats",
|
||||||
|
"tresabhi",
|
||||||
"Unicum",
|
"Unicum",
|
||||||
"Unregistering",
|
"Unregistering",
|
||||||
"vickers",
|
"vickers",
|
||||||
"vitepress",
|
"vitepress",
|
||||||
"wargaming",
|
"wargaming",
|
||||||
"winrate"
|
"webp",
|
||||||
|
"winrate",
|
||||||
|
"wotblitz",
|
||||||
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
"deno.unstable": false
|
"deno.unstable": false
|
||||||
|
|||||||
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
|||||||
Examples of behavior that contributes to a positive environment for our
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
overall community
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
advances of any kind
|
advances of any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
- Publishing others' private information, such as a physical or email
|
||||||
address, without their explicit permission
|
address, without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
|||||||
@@ -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/about' },
|
||||||
],
|
],
|
||||||
|
|
||||||
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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
// https://vitepress.dev/guide/custom-theme
|
// https://vitepress.dev/guide/custom-theme
|
||||||
import { h } from 'vue'
|
import { h } from 'vue';
|
||||||
import Theme from 'vitepress/theme'
|
import Theme from 'vitepress/theme';
|
||||||
import './style.css'
|
import './style.css';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: Theme,
|
extends: Theme,
|
||||||
Layout: () => {
|
Layout: () => {
|
||||||
return h(Theme.Layout, null, {
|
return h(Theme.Layout, null, {
|
||||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
enhanceApp({ app, router, siteData }) {
|
enhanceApp({ app, router, siteData }) {
|
||||||
// ...
|
// ...
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* Colors
|
* Colors
|
||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--vp-c-brand: #646cff;
|
--vp-c-brand: #646cff;
|
||||||
--vp-c-brand-light: #747bff;
|
--vp-c-brand-light: #747bff;
|
||||||
--vp-c-brand-lighter: #9499ff;
|
--vp-c-brand-lighter: #9499ff;
|
||||||
@@ -88,4 +88,3 @@
|
|||||||
.DocSearch {
|
.DocSearch {
|
||||||
--docsearch-primary-color: var(--vp-c-brand) !important;
|
--docsearch-primary-color: var(--vp-c-brand) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Blitzkrieg 1.10.0
|
||||||
|
|
||||||
|
This update addresses many readability issues that users have reported over time.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/ownedtanks`
|
||||||
|
- Tanks are now displayed in two columns
|
||||||
|
- Collector and premium tanks now have their own colors
|
||||||
|
- Tank types are also displayed
|
||||||
|
- A stylistic image of the tank is also displayed for each tank to the right
|
||||||
|
- `/fullstats`
|
||||||
|
- Was previously called `/statsfull`; this was changed to avoid the clutter that would show up when searching for `/stats`
|
||||||
|
- Added four catagories in which all the stats are sorted into
|
||||||
|
- Catagories now have backgrounds to compartmentalize them
|
||||||
|
- `/evolution`
|
||||||
|
- Gave the axis markings a high contrast color
|
||||||
|
- Added a label for the x axis affirming that it is plotted over the number of battles
|
||||||
|
- `/today`
|
||||||
|
- Added a border to all the rows to compartmentalize them
|
||||||
|
- This command can now generate embeds too
|
||||||
|
- Removed survivability
|
||||||
|
- Added a limit option to cut off after a certain number of tanks played (this is mainly intended to be used by streamers to limit the content of the embeds)
|
||||||
|
- Tanks are now displayed in order of the most recent battle
|
||||||
|
- `/help`
|
||||||
|
- A significant amount of subcommands were added
|
||||||
|
- These new subcommands now return normal Discord messages instead of embeds in favor of readability
|
||||||
|
- The contents of the replies are directly fetched from the Blitzkrieg documentation
|
||||||
|
- Time zones
|
||||||
|
- What is considered a "day's worth of battles" has been adjusted as the "reset time" was the same for all Blitz servers
|
||||||
|
- The reset time is now dependent on the server of the player
|
||||||
|
- Use `/help timezones` to learn more
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- All themes have been merged to leverage Bepaint's suffixes
|
||||||
|
- Switched to Node module resolution
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.10.1
|
||||||
|
|
||||||
|
This is a bit of an experiment.
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Every midnight in the Central North American Time, a permanent record of the entire ratings leaderboard will be stored for North America
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Blitzkrieg 1.10.2
|
||||||
|
|
||||||
|
The experiment in Blitzkrieg `1.10.1` was a bit of a failure so here are a few changes.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- North American leaderboards are now saved in the `na` directory instead of `asia`
|
||||||
|
- Dependencies are now installed before the script is ran
|
||||||
|
- Added leaderboards for all regions
|
||||||
|
- All regions now have their own separate cron timings
|
||||||
@@ -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,12 @@
|
|||||||
|
# Blitzkrieg 1.9.4
|
||||||
|
|
||||||
|
Haha, I lied. More fixes.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Tank names once again show when searching for tank stats
|
||||||
|
- Tier weights now ignore tanks without entries in the `tankopedia`
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- All `tankopedia` derivatives are now promises
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Blitzkrieg 1.9.5
|
||||||
|
|
||||||
|
Hopefully this is the last round of fixes for Blitzkrieg `1.9`.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- All commands have been tweaked to work even with a lack of a tankopedia entry
|
||||||
|
- This should fix test tanks not showing up
|
||||||
|
- `/today` now properly preserves all WN8 entries
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Embeds
|
||||||
|
|
||||||
|
Embeds are URLs generated by commands or tools that represent the "naked" version of the respective command or tool
|
||||||
|
|
||||||
|
## Creating Embeds
|
||||||
|
|
||||||
|
1. Use a command or tool and click on "Embed" if it's available
|
||||||
|
2. Make sure to tweak the settings of the command or tool because embeds replicate everything
|
||||||
|
|
||||||
|
## Using Embeds in Streaming Softwares
|
||||||
|
|
||||||
|
1. Click on the "Embed" button
|
||||||
|
- If this was generated by a command, copy the URL from the browser
|
||||||
|
- If this was generated by a tool, the URL has been copied to your clipboard already
|
||||||
|
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
|
||||||
|
|
||||||
|
- They load like any normal website
|
||||||
|
- They auto refresh every `1s`
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
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,3 @@
|
|||||||
|
# Inviting the Bot
|
||||||
|
|
||||||
|
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." 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
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Timezones
|
||||||
|
|
||||||
|
What is considered a "day's worth of games" is determined by the reset time. The reset time is generally at midnight everyday after which newer games are counted towards the next day. Reset times vary from region to region:
|
||||||
|
|
||||||
|
- North America: `UTC-5:00` (Central North American Time midnight)
|
||||||
|
- Europe: `UTC+1:00` (Central European Time midnight)
|
||||||
|
- Asia: `UTC+7:00` (Central Asia Standard Time midnight)
|
||||||
|
|
||||||
|
**Reset times are different from Blitz Stars** (Blitz Stars resets at 5:00 am locally relative to the user opening their website). Inconsistencies with Blitz Stars only occur with sleep-deprived players and players who play in the wrong region.
|
||||||
@@ -15,7 +15,7 @@ hero:
|
|||||||
link: guide/install
|
link: guide/install
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: Guide
|
text: Guide
|
||||||
link: /guide
|
link: /guide/about
|
||||||
|
|
||||||
features:
|
features:
|
||||||
- title: Clean and Efficient UI
|
- title: Clean and Efficient UI
|
||||||
|
|||||||
@@ -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,12 @@
|
|||||||
|
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
|
||||||
|
|
||||||
|
const withVanillaExtract = createVanillaExtractPlugin();
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
...withVanillaExtract(),
|
||||||
|
|
||||||
|
distDir: 'dist/website',
|
||||||
|
};
|
||||||
|
|
||||||
|
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,48 +1,69 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.9.3",
|
"version": "1.14.0",
|
||||||
"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": "node scripts/build --build=all",
|
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||||
"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": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||||
"zip": "zip -r dist.zip dist",
|
"zip": "zip -r dist.zip dist",
|
||||||
"lint": "npx tsc"
|
"lint": "npx tsc",
|
||||||
|
"lint:website": "next lint",
|
||||||
|
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
||||||
|
"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",
|
"discord.js": "^14.13.0",
|
||||||
"esbuild": "^0.18.10",
|
"dotenv": "^16.3.1",
|
||||||
"escape-html": "^1.0.3",
|
"esbuild": "^0.19.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
|
"immer": "^10.0.2",
|
||||||
"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",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@total-typescript/ts-reset": "^0.4.2",
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@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/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",
|
||||||
"typescript": "^5.0.4",
|
"@vanilla-extract/next-plugin": "^2.3.1",
|
||||||
"vitepress": "^1.0.0-beta.3"
|
"compare-versions": "^6.1.0",
|
||||||
|
"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: 2.6 MiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 8.2 MiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 2.4 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, // cause 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}`)),
|
||||||
|
);
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
import { rm, writeFile } from 'fs/promises';
|
||||||
|
import { argv, env } from 'process';
|
||||||
|
import {
|
||||||
|
RatingsInfo,
|
||||||
|
RatingsNeighbors,
|
||||||
|
RatingsPlayer,
|
||||||
|
} from '../src/commands/ratings';
|
||||||
|
import commitMultipleFiles from './commitMultipleFiles.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Central North American Time (UTC-5): use 0 5 * * *.
|
||||||
|
* Central European Time (UTC+1): use 0 23 * * *.
|
||||||
|
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
||||||
|
*
|
||||||
|
* na/ratings/49/info.json: season info like rewards, start & end time, etc.
|
||||||
|
* na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||||
|
* na/ratings/49/midnight.json: last midnight's leaderboard relative to the region
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
|
||||||
|
|
||||||
|
const publish = argv.includes('--publish');
|
||||||
|
const latest = argv.includes('--latest');
|
||||||
|
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
||||||
|
|
||||||
|
if (!server) throw new Error('Region parameter not specified');
|
||||||
|
|
||||||
|
const NEIGHBORS = 1000;
|
||||||
|
const PLAYERS = await fetch(
|
||||||
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
|
)
|
||||||
|
.then((response) => response.json() as Promise<RatingsInfo>)
|
||||||
|
.then((data) => (data.detail === undefined ? data.count : undefined));
|
||||||
|
|
||||||
|
if (PLAYERS === undefined) {
|
||||||
|
throw new Error('No current ratings season running');
|
||||||
|
}
|
||||||
|
|
||||||
|
const players: Record<number, RatingsPlayer> = {};
|
||||||
|
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
||||||
|
let registered = 0;
|
||||||
|
|
||||||
|
async function branchFromPlayer(id: number) {
|
||||||
|
const { neighbors } = await fetch(
|
||||||
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||||
|
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||||
|
|
||||||
|
const firstPlayer = neighbors[0];
|
||||||
|
const lastPlayer = neighbors[neighbors.length - 1];
|
||||||
|
|
||||||
|
const branches: Promise<void>[] = [];
|
||||||
|
if (!players[firstPlayer.number])
|
||||||
|
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||||
|
if (!players[lastPlayer.number])
|
||||||
|
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
||||||
|
|
||||||
|
neighbors.forEach((neighbor) => {
|
||||||
|
if (players[neighbor.number]) return;
|
||||||
|
|
||||||
|
players[neighbor.number] = neighbor;
|
||||||
|
registered++;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Registered ${registered} / ${PLAYERS} (${(
|
||||||
|
(registered / PLAYERS!) *
|
||||||
|
100
|
||||||
|
).toFixed(2)}%)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(branches);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function branchFromLeague(id: number) {
|
||||||
|
const { result } = await fetch(
|
||||||
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||||
|
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
|
||||||
|
|
||||||
|
const firstPlayer = result[0];
|
||||||
|
const lastPlayer = result[result.length - 1];
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
branchFromPlayer(firstPlayer.spa_id),
|
||||||
|
branchFromPlayer(lastPlayer.spa_id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Branching from leagues...');
|
||||||
|
await Promise.all([
|
||||||
|
branchFromLeague(0),
|
||||||
|
branchFromLeague(1),
|
||||||
|
branchFromLeague(2),
|
||||||
|
branchFromLeague(3),
|
||||||
|
branchFromLeague(4),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('Converting to an array...');
|
||||||
|
for (let index = 0; index < PLAYERS; index++) {
|
||||||
|
const listing = players[index + 1];
|
||||||
|
if (!listing) console.warn(`Position ${index + 1} had no player`);
|
||||||
|
|
||||||
|
leaderboard[index] = {
|
||||||
|
id: listing.spa_id,
|
||||||
|
score: listing.score,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaderboardJSON = JSON.stringify(leaderboard);
|
||||||
|
|
||||||
|
if (publish) {
|
||||||
|
const octokit = new Octokit({ auth: env.GH_TOKEN });
|
||||||
|
|
||||||
|
const info = await fetch(
|
||||||
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
|
).then(
|
||||||
|
(response) =>
|
||||||
|
response.json() as Promise<RatingsInfo & { detail: undefined }>,
|
||||||
|
);
|
||||||
|
const normalizedServer = server === 'na' ? 'com' : server;
|
||||||
|
|
||||||
|
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||||
|
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
|
||||||
|
latest ? 'latest' : 'midnight'
|
||||||
|
}.json`;
|
||||||
|
const infoJSON = JSON.stringify(info);
|
||||||
|
|
||||||
|
console.log(`Publishing to season ${info.current_season}...`);
|
||||||
|
commitMultipleFiles(
|
||||||
|
octokit,
|
||||||
|
'tresabhi',
|
||||||
|
'blitzkrieg-db',
|
||||||
|
'main',
|
||||||
|
new Date().toString(),
|
||||||
|
[
|
||||||
|
{ path: infoPath, content: infoJSON },
|
||||||
|
{ path: leaderboardPath, content: leaderboardJSON },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('Writing to disk...');
|
||||||
|
await rm('dist.json', { force: true });
|
||||||
|
await writeFile('dist.json', leaderboardJSON);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
|
||||||
|
export default async function commitMultipleFiles(
|
||||||
|
octokit: Octokit,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
message: string,
|
||||||
|
changes: { path: string; content: string }[],
|
||||||
|
) {
|
||||||
|
// Get the latest commit SHA on the specified branch
|
||||||
|
const { data: refData } = await octokit.git.getRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `heads/${branch}`,
|
||||||
|
});
|
||||||
|
const latestCommitSha = refData.object.sha;
|
||||||
|
|
||||||
|
// Get the tree SHA for the latest commit
|
||||||
|
const { data: commitData } = await octokit.git.getCommit({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
commit_sha: latestCommitSha,
|
||||||
|
});
|
||||||
|
const treeSha = commitData.tree.sha;
|
||||||
|
|
||||||
|
// Create blobs for each file
|
||||||
|
const blobs = await Promise.all(
|
||||||
|
changes.map(async (change) => {
|
||||||
|
const { data: blobData } = await octokit.git.createBlob({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
content: change.content,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sha: blobData.sha,
|
||||||
|
path: change.path,
|
||||||
|
mode: '100644',
|
||||||
|
type: 'blob',
|
||||||
|
} as const;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a new tree with the new blobs
|
||||||
|
const { data: treeData } = await octokit.git.createTree({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
base_tree: treeSha,
|
||||||
|
tree: blobs,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a new commit with the new tree
|
||||||
|
const { data: newCommitData } = await octokit.git.createCommit({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
message,
|
||||||
|
tree: treeData.sha,
|
||||||
|
parents: [latestCommitSha],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the branch reference to point to the new commit
|
||||||
|
await octokit.git.updateRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `heads/${branch}`,
|
||||||
|
sha: newCommitData.sha,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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/fetchWargaming';
|
||||||
|
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,57 @@
|
|||||||
|
'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"
|
||||||
|
accentColor="blue"
|
||||||
|
>
|
||||||
|
{!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,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,251 @@
|
|||||||
|
'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>
|
||||||
|
<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.promptBeforeReset) {
|
||||||
|
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({
|
||||||
|
promptBeforeReset: !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.showEmbedPrompt) {
|
||||||
|
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>
|
||||||
|
Keep in mind:
|
||||||
|
<ul>
|
||||||
|
<li>Paste this link into your browser to view the embed</li>
|
||||||
|
<li>Embeds abide your custom options</li>
|
||||||
|
<li>
|
||||||
|
Options do not transfer across websites/streaming softwares
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<Text size="2">
|
||||||
|
<Checkbox
|
||||||
|
mr="1"
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({
|
||||||
|
showEmbedPrompt: !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.js';
|
import { secrets } from './core/blitzkrieg/secrets';
|
||||||
import { registerErrorHandlers } from './events/error.js';
|
import { client } from './core/discord/client';
|
||||||
import guildMemberAdd from './events/guildMemberAdd.js';
|
|
||||||
import interactionCreate from './events/interactionCreate/index.js';
|
|
||||||
import ready from './events/ready.js';
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
SlashCommandSubcommandsOnlyBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
|
||||||
|
const DOCS = {
|
||||||
|
permissions: 'guide/permissions',
|
||||||
|
embeds: 'guide/embeds',
|
||||||
|
introduction: 'guide/introduction',
|
||||||
|
invite: 'guide/invite',
|
||||||
|
timezones: 'guide/timezones',
|
||||||
|
};
|
||||||
|
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||||
|
permissions: 'The permissions needed for the bot to function',
|
||||||
|
embeds: 'How embeds work',
|
||||||
|
introduction: 'About the bot',
|
||||||
|
invite: 'How to invite the bot',
|
||||||
|
timezones: 'How Blitzkrieg infers and uses your timezone',
|
||||||
|
};
|
||||||
|
|
||||||
|
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
||||||
|
Object.entries(DOC_DESCRIPTIONS).forEach(([key, value]) => {
|
||||||
|
option.addSubcommand((sub) => sub.setName(key).setDescription(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aboutCommand: CommandRegistry = {
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: addDocsSubcommands(
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('about')
|
||||||
|
.setDescription('All the info you need about the bot')
|
||||||
|
.addSubcommand((option) =>
|
||||||
|
option
|
||||||
|
.setName('commands')
|
||||||
|
.setDescription('List of all commands and their descriptions'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
if (subcommand === 'commands') {
|
||||||
|
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
||||||
|
await Promise.all(COMMANDS_RAW)
|
||||||
|
)
|
||||||
|
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||||
|
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
|
||||||
|
.map(
|
||||||
|
(registry) =>
|
||||||
|
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||||
|
)
|
||||||
|
.join('\n')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${RAW_PATH}${DOCS[subcommand as keyof typeof DOCS]}.md`;
|
||||||
|
const content = fetch(url).then((response) => response.text());
|
||||||
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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} />
|
||||||
|
)}
|
||||||
|
{filteredOrder.length > 0 && (
|
||||||
|
<Breakdown.Root>
|
||||||
|
<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];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
</Breakdown.Root>
|
||||||
|
)}
|
||||||
|
</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 Promise.all([
|
||||||
|
render(player, period, filters),
|
||||||
|
buttonPrimary(path, 'Refresh'),
|
||||||
|
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,17 @@
|
|||||||
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.js';
|
import getClientId from '../core/blitzkrieg/getClientId';
|
||||||
import { tankopedia } from '../core/blitz/tankopedia.js';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
import embedInfo from '../core/discord/embedInfo.js';
|
import { client } from '../core/discord/client';
|
||||||
import markdownTable from '../core/discord/markdownTable.js';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import getClientId from '../core/node/getClientId.js';
|
import markdownTable from '../core/discord/markdownTable';
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
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: true,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import calculateWN8 from '../core/blitz/calculateWN8.js';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
||||||
import sumStats from '../core/blitz/sumStats.js';
|
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
||||||
import { tankopedia } from '../core/blitz/tankopedia.js';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
import getPeriodStart from '../core/blitzstars/getPeriodStart.js';
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
import getTankStatsDiffed from '../core/blitzstars/getTankStatsDiffed.js';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
import embedPositive from '../core/discord/embedPositive';
|
||||||
import embedNegative from '../core/discord/embedNegative.js';
|
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||||
import embedPositive from '../core/discord/embedPositive.js';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import markdownTable, { TableInput } from '../core/discord/markdownTable.js';
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
import sumStats from '../core/statistics/sumStats';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||||
import { AccountInfo } from '../types/accountInfo.js';
|
|
||||||
import { PossiblyPromise } from '../types/possiblyPromise.js';
|
|
||||||
|
|
||||||
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: true,
|
|
||||||
inPublic: false,
|
inPublic: false,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
@@ -49,34 +46,47 @@ 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 tankStatsOverTime = await getTankStatsDiffed(
|
const { diff: diffed } = await getStatsInPeriod(
|
||||||
server,
|
region,
|
||||||
id,
|
id,
|
||||||
getPeriodStart('30'),
|
getTimeDaysAgo(region, 30),
|
||||||
getPeriodNow(),
|
getPeriodNow(),
|
||||||
);
|
);
|
||||||
const entries = Object.entries(tankStatsOverTime);
|
const entries = Object.entries(diffed);
|
||||||
const stats = sumStats(entries.map(([, stats]) => stats));
|
const stats = sumStats(entries.map(([, stats]) => stats));
|
||||||
const averageTier =
|
const averageTier =
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
(await entries.reduce<PossiblyPromise<number>>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
async (accumulator, [tankIdString, stats]) => {
|
||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
const tankTier = (await tankopedia)[tankId].tier;
|
const tankopediaEntry = (await tankopedia)[tankId];
|
||||||
|
|
||||||
|
if (!tankopediaEntry) return accumulator;
|
||||||
|
|
||||||
|
const tankTier = tankopediaEntry.tier;
|
||||||
|
|
||||||
return (await accumulator) + tankTier * stats.battles;
|
return (await accumulator) + tankTier * stats.battles;
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
)) / stats.battles;
|
)) /
|
||||||
|
(await entries.reduce<PossiblyPromise<number>>(
|
||||||
|
async (accumulator, [tankIdString, stats]) => {
|
||||||
|
const tankId = parseInt(tankIdString);
|
||||||
|
const tankopediaEntry = (await tankopedia)[tankId];
|
||||||
|
|
||||||
|
if (!tankopediaEntry) return accumulator;
|
||||||
|
|
||||||
|
return stats.battles;
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
));
|
||||||
const WN8 =
|
const WN8 =
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
(await entries.reduce<PossiblyPromise<number>>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
async (accumulator, [tankIdString, stats]) => {
|
||||||
@@ -101,7 +111,7 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
if ((await tankopedia)[tankId]?.tier === 10) {
|
if ((await tankopedia)[tankId]?.tier === 10) {
|
||||||
const tankStats = tankStatsOverTime[tankId];
|
const tankStats = diffed[tankId];
|
||||||
return (await accumulator) + tankStats.damage_dealt;
|
return (await accumulator) + tankStats.damage_dealt;
|
||||||
} else return accumulator;
|
} else return accumulator;
|
||||||
},
|
},
|
||||||
@@ -112,7 +122,7 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
if ((await tankopedia)[tankId]?.tier === 10) {
|
if ((await tankopedia)[tankId]?.tier === 10) {
|
||||||
const tankStats = tankStatsOverTime[tankId];
|
const tankStats = diffed[tankId];
|
||||||
return (await accumulator) + tankStats.battles;
|
return (await accumulator) + tankStats.battles;
|
||||||
} else return accumulator;
|
} else return accumulator;
|
||||||
},
|
},
|
||||||
@@ -157,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([
|
||||||
|
|||||||
@@ -1,86 +1,195 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import {
|
||||||
import { CYCLIC_API } from '../constants/cyclic.js';
|
SlashCommandBuilder,
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
SlashCommandSubcommandGroupBuilder,
|
||||||
import resolveTankId from '../core/blitz/resolveTankId.js';
|
} from 'discord.js';
|
||||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
|
import * as Graph from '../components/Graph';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
import TitleBar from '../components/TitleBar';
|
||||||
import interactionToURL from '../core/discord/interactionToURL.js';
|
import Wrapper from '../components/Wrapper';
|
||||||
import linkButton from '../core/discord/linkButton.js';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import primaryButton from '../core/discord/primaryButton.js';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
|
import getTankHistories from '../core/blitzkrieg/getTankHistories';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
import addPeriodSubCommands from '../core/discord/addPeriodSubCommands';
|
||||||
import evolution from '../renderers/evolution.js';
|
import addTankChoices from '../core/discord/addTankChoices';
|
||||||
import { StatType } from '../renderers/statsfull.js';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import { AccountInfo } from '../types/accountInfo.js';
|
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 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 { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const evolutionCommand: CommandRegistry = {
|
type EvolutionStatType = 'player' | 'tank';
|
||||||
inProduction: true,
|
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: addStatTypeSubCommandGroups(
|
async function render(
|
||||||
new SlashCommandBuilder()
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
type: 'player' | 'tank',
|
||||||
|
tankId: number | null,
|
||||||
|
) {
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const logo = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const histories = await (type === 'player'
|
||||||
|
? getPlayerHistories(region, id, {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
})
|
||||||
|
: getTankHistories(region, id, {
|
||||||
|
tankId: tankId!,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
}));
|
||||||
|
const plot = histories.map(
|
||||||
|
(history) =>
|
||||||
|
[
|
||||||
|
history.all.battles,
|
||||||
|
(history.all.wins / history.all.battles) * 100,
|
||||||
|
] as Graph.PlotItem,
|
||||||
|
);
|
||||||
|
const times = histories.map((history) => history.last_battle_time);
|
||||||
|
const minTime = Math.min(...times);
|
||||||
|
const maxTime = Math.max(...times);
|
||||||
|
const ys = plot.map(([, y]) => y);
|
||||||
|
const xs = plot.map(([x]) => x);
|
||||||
|
const maxY = Math.max(...ys);
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const maxX = Math.max(...xs);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar name={accountInfo.nickname} image={logo} description={name} />
|
||||||
|
|
||||||
|
{plot.length > 0 && (
|
||||||
|
<Graph.Root
|
||||||
|
verticalMargin={{
|
||||||
|
min: minY,
|
||||||
|
max: maxY,
|
||||||
|
suffix: '%',
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
horizontalMargin={{
|
||||||
|
min: minX,
|
||||||
|
max: maxX,
|
||||||
|
precision: 0,
|
||||||
|
}}
|
||||||
|
xMinLabel={new Date(minTime * 1000).toDateString()}
|
||||||
|
xMaxLabel={new Date(maxTime * 1000).toDateString()}
|
||||||
|
>
|
||||||
|
{Graph.Line({
|
||||||
|
color: Graph.LineColor.Red,
|
||||||
|
plot: plot,
|
||||||
|
minY,
|
||||||
|
maxY,
|
||||||
|
})}
|
||||||
|
</Graph.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
||||||
|
async (resolve) => {
|
||||||
|
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
|
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
|
const command = new SlashCommandBuilder()
|
||||||
.setName('evolution')
|
.setName('evolution')
|
||||||
.setDescription('Evolution of statistics'),
|
.setDescription('Evolution of statistics')
|
||||||
),
|
.addSubcommandGroup((option) => {
|
||||||
|
playerGroup = option
|
||||||
|
.setName('player')
|
||||||
|
.setDescription('Player statistics');
|
||||||
|
|
||||||
async handler(interaction) {
|
return playerGroup;
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
})
|
||||||
true,
|
.addSubcommandGroup((option) => {
|
||||||
) as StatType;
|
tankGroup = option.setName('tank').setDescription('Tank statistics');
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
|
||||||
const period = resolvePeriodFromCommand(interaction);
|
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
|
||||||
const tankId = commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
|
||||||
const start = interaction.options.getInteger('start');
|
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
tankId,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
return tankGroup;
|
||||||
await evolution(commandGroup, period, player, tankId),
|
});
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
await Promise.all([
|
||||||
linkButton(
|
addPeriodSubCommands(playerGroup!, (option) =>
|
||||||
`https://www.blitzstars.com/player/${player.server}/${nickname}${
|
option.addStringOption(addUsernameChoices),
|
||||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
|
||||||
}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
),
|
||||||
];
|
addPeriodSubCommands(tankGroup!, (option) =>
|
||||||
},
|
option
|
||||||
|
.addStringOption(addTankChoices)
|
||||||
|
.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
resolve({
|
||||||
autocompleteUsername(interaction);
|
inProduction: true,
|
||||||
autocompleteTanks(interaction);
|
inPublic: true,
|
||||||
},
|
|
||||||
|
|
||||||
async button(interaction) {
|
command,
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
|
||||||
const commandGroup = path[1] as StatType;
|
|
||||||
const period = resolvePeriodFromButton(interaction);
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
|
|
||||||
return await evolution(
|
async handler(interaction) {
|
||||||
commandGroup,
|
const commandGroup =
|
||||||
period,
|
interaction.options.getSubcommandGroup() as EvolutionStatType;
|
||||||
player,
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
);
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
|
const tankId =
|
||||||
|
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
|
tankId,
|
||||||
|
});
|
||||||
|
|
||||||
|
getBlitzStarsLinkButton;
|
||||||
|
|
||||||
|
return [
|
||||||
|
await render(player, period, commandGroup, tankId),
|
||||||
|
buttonPrimary(path, 'Refresh'),
|
||||||
|
await getBlitzStarsLinkButton(
|
||||||
|
player.region,
|
||||||
|
player.id,
|
||||||
|
tankId ?? undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const url = new URL(`https://example.com/${interaction.customId}`);
|
||||||
|
const path = url.pathname.split('/');
|
||||||
|
const commandGroup = path[2] as EvolutionStatType;
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
|
||||||
|
return await render(
|
||||||
|
player,
|
||||||
|
period,
|
||||||
|
commandGroup,
|
||||||
|
parseInt(url.searchParams.get('tankId') ?? '0') || null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
);
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import GenericAllStats from '../components/GenericAllStats';
|
||||||
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
|
import TierWeights from '../components/TierWeights';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
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 { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
|
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 { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
|
import { getTierWeights } from '../core/statistics/getTierWeights';
|
||||||
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
|
||||||
|
const { stats, supplementary, filteredOrder } = await filterStats(
|
||||||
|
diffedTankStats,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters);
|
||||||
|
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<GenericAllStats stats={stats} supplementaryStats={supplementary} />
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
||||||
|
async (resolve) => {
|
||||||
|
const command = await addFilterOptions(
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('full-stats')
|
||||||
|
.setDescription('Full in-game statistics'),
|
||||||
|
(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 Promise.all([
|
||||||
|
render(player, period, filters),
|
||||||
|
buttonRefresh(interaction, path),
|
||||||
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
|
return await render(player, period, filters);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
|
||||||
import discord from '../../discord.json' assert { type: 'json' };
|
|
||||||
import { client } from '../bot.js';
|
|
||||||
import embedInfo from '../core/discord/embedInfo.js';
|
|
||||||
import {
|
|
||||||
CommandRegistry,
|
|
||||||
guildCommands,
|
|
||||||
publicCommands,
|
|
||||||
} from '../events/interactionCreate/index.js';
|
|
||||||
|
|
||||||
export const helpCommand: CommandRegistry = {
|
|
||||||
inProduction: true,
|
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
|
||||||
.setName('help')
|
|
||||||
.setDescription('All the help you need about the bot'),
|
|
||||||
|
|
||||||
handler(interaction) {
|
|
||||||
return embedInfo(
|
|
||||||
`${client.user?.username} help`,
|
|
||||||
`**About**\n${
|
|
||||||
client.user?.username
|
|
||||||
} automates many mundane tasks and provide statistics in numerous flexible ways.\n\nMade by TresAbhi from the Skilled [SKLLD] clan.\nGitHub: https://github.com/tresabhi/blitzkrieg\nDiscord server: https://discord.gg/nDt7AjGJQH\nSkilled server: https://discord.gg/ZPvcEG7DS8\n\n**Commands**\n${(interaction.guildId ===
|
|
||||||
discord.sklld_guild_id
|
|
||||||
? [...guildCommands, ...publicCommands]
|
|
||||||
: publicCommands
|
|
||||||
)
|
|
||||||
.map((command) => `**/${command.name}**: ${command.description}`)
|
|
||||||
.join(
|
|
||||||
'\n',
|
|
||||||
)}\n\n*I am a new bot; I may make mistakes and go offline unexpectedly from time to time.*`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import GenericStats from '../components/GenericStats/index.js';
|
import GenericStats from '../components/GenericStats';
|
||||||
import NoData, { NoDataType } from '../components/NoData.js';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
|
import TitleBar from '../components/TitleBar';
|
||||||
import TitleBar from '../components/TitleBar.js';
|
import Wrapper from '../components/Wrapper';
|
||||||
import Wrapper from '../components/Wrapper.js';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { BLITZ_SERVERS } from '../constants/servers.js';
|
import { getClanInfo } from '../core/blitz/getClanInfo';
|
||||||
import getBlitzClan from '../core/blitz/getBlitzClan.js';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||||
import addClanChoices from '../core/discord/addClanChoices.js';
|
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
||||||
import autocompleteClan from '../core/discord/autocompleteClan.js';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
|
||||||
import { AccountInfo } from '../types/accountInfo.js';
|
|
||||||
import { ClanInfo } from '../types/clanInfo.js';
|
|
||||||
|
|
||||||
const DEFAULT_THRESHOLD = 7;
|
const DEFAULT_THRESHOLD = 7;
|
||||||
|
|
||||||
export const inactiveCommand: CommandRegistry = {
|
export const inactiveCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
@@ -35,29 +30,20 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
),
|
),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const clanName = interaction.options.getString('clan')!;
|
const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
const { server, id } = await getBlitzClan(clanName);
|
|
||||||
const threshold =
|
const threshold =
|
||||||
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
||||||
const time = new Date().getTime() / 1000;
|
const time = new Date().getTime() / 1000;
|
||||||
const clanData = (
|
const clanInfo = await getClanInfo(region, id);
|
||||||
await getWargamingResponse<ClanInfo>(
|
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
|
||||||
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
const inactive = accountInfo
|
||||||
|
.map(
|
||||||
|
(account) =>
|
||||||
|
[
|
||||||
|
account.nickname,
|
||||||
|
(time - account.last_battle_time) / 60 / 60 / 24,
|
||||||
|
] as [string, number],
|
||||||
)
|
)
|
||||||
)[id];
|
|
||||||
const memberIds = clanData.members_ids;
|
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
|
|
||||||
',',
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
const inactive = memberIds
|
|
||||||
.map((memberId) => {
|
|
||||||
const member = accounts[memberId];
|
|
||||||
const inactiveDays = (time - member.last_battle_time) / 60 / 60 / 24;
|
|
||||||
|
|
||||||
return [member.nickname, inactiveDays] as [string, number];
|
|
||||||
})
|
|
||||||
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.map(
|
.map(
|
||||||
@@ -68,18 +54,13 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={clanData.name}
|
name={clanInfo.name}
|
||||||
nameDiscriminator="(Inactivity)"
|
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
||||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
|
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
||||||
description={`${threshold}+ Days • ${new Date().toDateString()} • ${
|
|
||||||
BLITZ_SERVERS[server]
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
||||||
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.Wargaming} />
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { teal } from '@radix-ui/colors';
|
||||||
|
import {
|
||||||
|
AttachmentBuilder,
|
||||||
|
GuildMemberRoleManager,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
|
import discord from '../../discord.json' assert { type: 'json' };
|
||||||
|
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
||||||
|
|
||||||
|
export const verifyCommand: CommandRegistry = {
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: new SlashCommandBuilder()
|
||||||
|
.setName('link')
|
||||||
|
.setDescription('Links your Blitz and Discord account')
|
||||||
|
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const username = interaction.options.getString('username', true);
|
||||||
|
|
||||||
|
if (serverAndIdPattern.test(username)) {
|
||||||
|
const [region, idString] = username.split('/');
|
||||||
|
const id = parseInt(idString);
|
||||||
|
const discordId = parseInt(interaction.user.id);
|
||||||
|
const accountInfo = await getAccountInfo(region as Region, id);
|
||||||
|
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [
|
||||||
|
'clan',
|
||||||
|
]);
|
||||||
|
|
||||||
|
await linkBlitzAndDiscord(discordId, region as Region, id);
|
||||||
|
|
||||||
|
if (interaction.guildId === discord.sklld_guild_id) {
|
||||||
|
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
||||||
|
return embedNegative(
|
||||||
|
`${markdownEscape(interaction.user.username)} failed to verify`,
|
||||||
|
"I don't have the permission to change your manage roles.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).remove(
|
||||||
|
discord.sklld_verify_role,
|
||||||
|
);
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).add(
|
||||||
|
discord.sklld_peasant_role,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper fat>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glow color={teal.teal9} rotation={90} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
interaction.user.avatarURL({ extension: 'png' }) ??
|
||||||
|
'https://i.stack.imgur.com/l60Hf.png'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interaction.user.displayName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
@{interaction.user.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
color: theme.colors.textLowContrast_teal,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://i.imgur.com/jIcRgog.png"
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 16 }}>Accounts linked</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan ? (
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{accountInfo.nickname}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan
|
||||||
|
? `[${clanAccountInfo?.clan?.tag}] - `
|
||||||
|
: ''}
|
||||||
|
{REGION_NAMES_SHORT[region as Region]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
embedNegative(
|
||||||
|
'Please select from the options',
|
||||||
|
'Follow the procedure like the video above. The search results may take a while to show up.',
|
||||||
|
),
|
||||||
|
new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
};
|
||||||
@@ -1,123 +1,126 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import NoData, { NoDataType } from '../components/NoData.js';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
|
import * as Tanks from '../components/Tanks';
|
||||||
import * as Tanks from '../components/Tanks/index.js';
|
import TitleBar from '../components/TitleBar';
|
||||||
import TitleBar from '../components/TitleBar.js';
|
import Wrapper from '../components/Wrapper';
|
||||||
import Wrapper from '../components/Wrapper.js';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import { BLITZ_SERVERS } from '../constants/servers.js';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getTankStats from '../core/blitz/getTankStats.js';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
import {
|
import {
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
|
TankopediaEntry,
|
||||||
Tier,
|
Tier,
|
||||||
tankopedia,
|
tankopedia,
|
||||||
} from '../core/blitz/tankopedia.js';
|
tankopediaInfo,
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
} from '../core/blitzstars/tankopedia';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
import addTierChoices from '../core/discord/addTierChoices';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { AccountInfo } from '../types/accountInfo.js';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { PlayerClanData } from '../types/playerClanData.js';
|
|
||||||
|
|
||||||
const COMP_TANKS = [
|
|
||||||
// light tanks
|
|
||||||
24321, // t-100 lt
|
|
||||||
20257, // sheridan
|
|
||||||
3649, // bat chat
|
|
||||||
19537, // vickers
|
|
||||||
|
|
||||||
// heavy tanks
|
|
||||||
6145, // is-4
|
|
||||||
7169, // is-7
|
|
||||||
10785, // t110e5
|
|
||||||
12161, // strvk
|
|
||||||
4481, // kranvagn
|
|
||||||
22817, // m6 yoh
|
|
||||||
6225, // fv215b
|
|
||||||
5425, // wz 113
|
|
||||||
7297, // 60tp
|
|
||||||
58641, // vk 72
|
|
||||||
6753, // type 71
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ownedTanksCommand: CommandRegistry = {
|
export const ownedTanksCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('ownedtanks')
|
.setName('owned-tanks')
|
||||||
.setDescription("Shows a player's owned tanks")
|
.setDescription("Shows a player's owned tanks")
|
||||||
.addStringOption((option) =>
|
.addStringOption(addTierChoices)
|
||||||
option
|
|
||||||
.setName('tier')
|
|
||||||
.setDescription('The tier you want to see')
|
|
||||||
.setChoices(
|
|
||||||
{ name: 'Tier I', value: '1' },
|
|
||||||
{ name: 'Tier II', value: '2' },
|
|
||||||
{ name: 'Tier III', value: '3' },
|
|
||||||
{ name: 'Tier IV', value: '4' },
|
|
||||||
{ name: 'Tier V', value: '5' },
|
|
||||||
{ name: 'Tier VI', value: '6' },
|
|
||||||
{ name: 'Tier VII', value: '7' },
|
|
||||||
{ name: 'Tier VIII', value: '8' },
|
|
||||||
{ name: 'Tier IX', value: '9' },
|
|
||||||
{ name: 'Tier X', value: '10' },
|
|
||||||
)
|
|
||||||
.setRequired(true),
|
|
||||||
)
|
|
||||||
.addStringOption(addUsernameChoices),
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const tier = Number(interaction.options.getString('tier'));
|
const tier = Number(interaction.options.getString('tier'));
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, server } = account;
|
const { id, region: server } = account;
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(server, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const tankStats = await getTankStats(server, id);
|
const tankStats = await getTankStats(server, id);
|
||||||
const tanks = (
|
const filteredTanks = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tankStats.map(async (tankData) => (await tankopedia)[tankData.tank_id]),
|
tankStats.map(async (tankData) => ({
|
||||||
|
tankopedia: (await tankopedia)[tankData.tank_id]!,
|
||||||
|
tank_id: tankData.tank_id,
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
).filter((tank) => tank?.tier === tier);
|
).filter((tank) => tank.tankopedia?.tier === tier);
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
||||||
);
|
const nations: string[] = [];
|
||||||
|
|
||||||
|
filteredTanks.forEach((tank) => {
|
||||||
|
if (groupedTanks[tank.tankopedia.nation] === undefined) {
|
||||||
|
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
|
||||||
|
nations.push(tank.tankopedia.nation);
|
||||||
|
} else {
|
||||||
|
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nations.sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{/* TODO: integrate some of these into title bar */}
|
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo.nickname}
|
||||||
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`}
|
|
||||||
image={
|
image={
|
||||||
clanData[id]?.clan
|
clanAccountInfo?.clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
description={`Owned tanks • ${new Date().toDateString()} • ${
|
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`}
|
||||||
BLITZ_SERVERS[server]
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{tanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||||
|
|
||||||
{tanks.length > 0 && (
|
{filteredTanks.length > 0 &&
|
||||||
<Tanks.Root>
|
(await Promise.all(
|
||||||
{tanks.map((tank) => (
|
nations.map(async (nation) => {
|
||||||
<Tanks.Item
|
const tanks = groupedTanks[nation];
|
||||||
key={tank.tank_id}
|
const leftColumnSize = Math.ceil(tanks.length / 2);
|
||||||
name={tank.name}
|
const leftColumn = tanks.slice(0, leftColumnSize);
|
||||||
type={tank.type}
|
const rightColumn = tanks.slice(leftColumnSize);
|
||||||
icon={tank.images.normal}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tanks.Root>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.Wargaming} />
|
return (
|
||||||
|
<Tanks.Root>
|
||||||
|
<Tanks.Title>
|
||||||
|
{(await tankopediaInfo).vehicle_nations[nation]}
|
||||||
|
</Tanks.Title>
|
||||||
|
|
||||||
|
<Tanks.Row>
|
||||||
|
<Tanks.Column>
|
||||||
|
{await Promise.all(
|
||||||
|
leftColumn.map(async (tank) => (
|
||||||
|
<Tanks.Item
|
||||||
|
key={tank.tank_id}
|
||||||
|
name={await resolveTankName(tank.tank_id)}
|
||||||
|
tankType={tank.type}
|
||||||
|
image={tank.images?.normal}
|
||||||
|
treeType={await getTreeType(tank.tank_id)}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</Tanks.Column>
|
||||||
|
<Tanks.Column>
|
||||||
|
{await Promise.all(
|
||||||
|
rightColumn.map(async (tank) => (
|
||||||
|
<Tanks.Item
|
||||||
|
key={tank.tank_id}
|
||||||
|
name={await resolveTankName(tank.tank_id)}
|
||||||
|
tankType={tank.type}
|
||||||
|
image={tank.images?.normal}
|
||||||
|
treeType={await getTreeType(tank.tank_id)}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</Tanks.Column>
|
||||||
|
</Tanks.Row>
|
||||||
|
</Tanks.Root>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
))}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const pingCommand: CommandRegistry<true> = {
|
export const pingCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
handlesInteraction: true,
|
handlesInteraction: true,
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,23 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { startCase } from 'lodash';
|
import { startCase } from 'lodash';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo.js';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import markdownTable, {
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
TableInputEntry,
|
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
||||||
} from '../core/discord/markdownTable.js';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
|
||||||
import { AccountAchievements } from '../types/accountAchievements.js';
|
|
||||||
import { AccountInfo } from '../types/accountInfo.js';
|
|
||||||
|
|
||||||
type SortBy = 'name' | 'count';
|
type SortBy = 'name' | 'count';
|
||||||
|
|
||||||
export const playerAchievementsCommand: CommandRegistry = {
|
export const playerAchievementsCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('playerachievements')
|
.setName('player-achievements')
|
||||||
.setDescription("All the player's achievements")
|
.setDescription("All the player's achievements")
|
||||||
.addStringOption(addUsernameChoices)
|
.addStringOption(addUsernameChoices)
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
@@ -38,22 +33,17 @@ export const playerAchievementsCommand: CommandRegistry = {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, server } = account;
|
const { id, region: region } = account;
|
||||||
const accounts = 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 accountsAchievements = await getAccountAchievements(region, id);
|
||||||
);
|
const accountAchievements = accountsAchievements;
|
||||||
const accountsAchievements =
|
|
||||||
await getWargamingResponse<AccountAchievements>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const accountAchievements = accountsAchievements[id];
|
|
||||||
const compound = {
|
const compound = {
|
||||||
...accountAchievements.achievements,
|
...accountAchievements.achievements,
|
||||||
...accountAchievements.max_series,
|
...accountAchievements.max_series,
|
||||||
};
|
};
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`${markdownEscape(accounts[id].nickname)}'s information`,
|
`${markdownEscape(accountInfo.nickname)}'s information`,
|
||||||
|
|
||||||
markdownTable(
|
markdownTable(
|
||||||
Object.keys(compound)
|
Object.keys(compound)
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedInfo from '../core/discord/embedInfo.js';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable from '../core/discord/markdownTable.js';
|
import markdownTable from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
|
||||||
import { AccountInfo } from '../types/accountInfo.js';
|
|
||||||
|
|
||||||
export const playerInfoCommand: CommandRegistry = {
|
export const playerInfoCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('playerinfo')
|
.setName('player-info')
|
||||||
.setDescription('Basic information about a player')
|
.setDescription('Basic information about a player')
|
||||||
.addStringOption(addUsernameChoices),
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, server } = account;
|
const { id, region: region } = account;
|
||||||
const accounts = 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 accountInfo = accounts[id];
|
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`${markdownEscape(accountInfo.nickname)}'s information`,
|
`${markdownEscape(accountInfo.nickname)}'s information`,
|
||||||
|
|||||||
@@ -0,0 +1,532 @@
|
|||||||
|
import {
|
||||||
|
APIApplicationCommandOptionChoice,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
SlashCommandSubcommandGroupBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||||
|
import * as Leaderboard from '../components/Leaderboard';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||||
|
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||||
|
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
||||||
|
import getMidnightLeaderboard, {
|
||||||
|
DATABASE_REPO,
|
||||||
|
} from '../core/blitzkrieg/getMidnightLeaderboard';
|
||||||
|
import { octokit } from '../core/blitzkrieg/octokit';
|
||||||
|
import { UserError } from '../core/blitzkrieg/userError';
|
||||||
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
export interface RatingsPlayer {
|
||||||
|
spa_id: number;
|
||||||
|
mmr: number;
|
||||||
|
season_number: number;
|
||||||
|
calibrationBattlesLeft: number;
|
||||||
|
number: number;
|
||||||
|
percentile: number;
|
||||||
|
skip: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
score: number;
|
||||||
|
nickname: string;
|
||||||
|
clan_tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueTop {
|
||||||
|
result: RatingsPlayer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RatingsReward = {
|
||||||
|
from_position: number;
|
||||||
|
to_position: number;
|
||||||
|
count: number;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: 'vehicle';
|
||||||
|
|
||||||
|
vehicle: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
nation: string;
|
||||||
|
subnation: string;
|
||||||
|
use_subnation_flag: boolean;
|
||||||
|
type_slug: string;
|
||||||
|
level: number;
|
||||||
|
roman_level: string;
|
||||||
|
user_string: string;
|
||||||
|
image_url: string;
|
||||||
|
preview_url: string;
|
||||||
|
is_premium: boolean;
|
||||||
|
is_collectible: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'stuff';
|
||||||
|
|
||||||
|
stuff: {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
title: string;
|
||||||
|
image_url: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
sizes: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface RatingsLeague {
|
||||||
|
title: string;
|
||||||
|
small_icon: string;
|
||||||
|
big_icon: string;
|
||||||
|
background: string;
|
||||||
|
index: number;
|
||||||
|
percentile: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RatingsInfo =
|
||||||
|
| {
|
||||||
|
detail: undefined;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
icon: string | null;
|
||||||
|
start_at: string;
|
||||||
|
finish_at: string;
|
||||||
|
current_season: number;
|
||||||
|
updated_at: string;
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
rewards: RatingsReward[];
|
||||||
|
leagues: RatingsLeague[];
|
||||||
|
}
|
||||||
|
| { detail: { error: string } };
|
||||||
|
|
||||||
|
export interface RatingsNeighbors {
|
||||||
|
neighbors: RatingsPlayer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEAGUES = [
|
||||||
|
{ name: 'Diamond', minScore: 5000 },
|
||||||
|
{ name: 'Platinum', minScore: 4000 },
|
||||||
|
{ name: 'Gold', minScore: 3000 },
|
||||||
|
{ name: 'Silver', minScore: 2000 },
|
||||||
|
{ name: 'Bronze', minScore: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function addSubcommands(
|
||||||
|
option: SlashCommandSubcommandGroupBuilder,
|
||||||
|
addOptions = (option: SlashCommandSubcommandBuilder) => option,
|
||||||
|
) {
|
||||||
|
return option
|
||||||
|
.addSubcommand((option) =>
|
||||||
|
addOptions(
|
||||||
|
option
|
||||||
|
.setName('neighbours')
|
||||||
|
.setDescription('Your position and neighbours'),
|
||||||
|
)
|
||||||
|
.addStringOption(addUsernameChoices)
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('limit')
|
||||||
|
.setDescription('How many neighbours to display (default: 10)')
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addSubcommand((option) =>
|
||||||
|
addOptions(option.setName('league').setDescription('Top league players'))
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('league')
|
||||||
|
.setDescription('The league to display')
|
||||||
|
.addChoices(
|
||||||
|
...LEAGUES.map(
|
||||||
|
({ name }, value) =>
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
value: `${value}`,
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption(addRegionChoices)
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('limit')
|
||||||
|
.setDescription('How many rows to display (default: 10)')
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const noOngoingSeason = embedNegative(
|
||||||
|
'No ongoing season',
|
||||||
|
"Wargaming didn't provide any data for this season.",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||||
|
async (resolve) => {
|
||||||
|
const seasonChoices = await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: 'com/ratings',
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('Archived ratings data is malformed');
|
||||||
|
}
|
||||||
|
return data.map(
|
||||||
|
(folder) =>
|
||||||
|
({
|
||||||
|
name: `Season ${folder.name}`,
|
||||||
|
value: folder.name,
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new SlashCommandBuilder()
|
||||||
|
.setName('ratings')
|
||||||
|
.setDescription('Ratings battles statistics')
|
||||||
|
.addSubcommandGroup((option) =>
|
||||||
|
addSubcommands(
|
||||||
|
option.setName('archive').setDescription('Any previous season'),
|
||||||
|
(option) =>
|
||||||
|
option.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('season')
|
||||||
|
.setDescription('The season number')
|
||||||
|
.setChoices(...seasonChoices)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addSubcommandGroup((option) =>
|
||||||
|
addSubcommands(
|
||||||
|
option
|
||||||
|
.setName('current')
|
||||||
|
.setDescription('The current running Ratings season'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
interface SimplifiedPlayer {
|
||||||
|
id: number;
|
||||||
|
score: number;
|
||||||
|
position: number;
|
||||||
|
clan: string | undefined;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
|
||||||
|
| 'archive'
|
||||||
|
| 'current';
|
||||||
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
|
| 'league'
|
||||||
|
| 'neighbours';
|
||||||
|
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||||
|
const seasonOption = interaction.options.getString('season');
|
||||||
|
const season = seasonOption ? parseInt(seasonOption) : undefined;
|
||||||
|
let titleName: string;
|
||||||
|
let titleImage: string | undefined;
|
||||||
|
let titleNameDiscriminator: string | undefined;
|
||||||
|
let titleDescription: string;
|
||||||
|
let playersBefore: number;
|
||||||
|
let playersAfter: number;
|
||||||
|
let players: SimplifiedPlayer[] | undefined;
|
||||||
|
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
||||||
|
let playerId: number | undefined;
|
||||||
|
let regionRatingsInfo: RatingsInfo;
|
||||||
|
|
||||||
|
if (subcommand === 'league') {
|
||||||
|
const leagueIndex = parseInt(
|
||||||
|
interaction.options.getString('league')!,
|
||||||
|
);
|
||||||
|
const region = interaction.options.getString('region') as Region;
|
||||||
|
regionRatingsInfo = await (subcommandGroup === 'current'
|
||||||
|
? getRatingsInfo(region)
|
||||||
|
: getArchivedRatingsInfo(season!, region));
|
||||||
|
|
||||||
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
|
|
||||||
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
|
const result =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? await fetch(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
||||||
|
)
|
||||||
|
.then((response) => response.json() as Promise<LeagueTop>)
|
||||||
|
.then(async ({ result }) =>
|
||||||
|
result.slice(0, limit).map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
score: player.score,
|
||||||
|
position: player.number,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/latest.json`,
|
||||||
|
})
|
||||||
|
.then(async ({ data }) => {
|
||||||
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
|
throw new Error('Archived ratings data is malformed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const players = (await fetch(data.download_url!).then(
|
||||||
|
(response) => response.json(),
|
||||||
|
)) as BlitzkriegRatingsLeaderboard;
|
||||||
|
const firstPlayerIndex =
|
||||||
|
leagueIndex === 0
|
||||||
|
? 0
|
||||||
|
: players.findIndex(
|
||||||
|
(player) =>
|
||||||
|
player.score < LEAGUES[leagueIndex - 1].minScore,
|
||||||
|
);
|
||||||
|
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
||||||
|
const trimmed = players.slice(
|
||||||
|
firstPlayerIndex,
|
||||||
|
lastPlayerIndex + 1,
|
||||||
|
);
|
||||||
|
const clanData = await getClanAccountInfo(
|
||||||
|
region,
|
||||||
|
trimmed.map(({ id }) => id),
|
||||||
|
['clan'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return trimmed.map(
|
||||||
|
(player, index) =>
|
||||||
|
({
|
||||||
|
id: player.id,
|
||||||
|
score: player.score,
|
||||||
|
position: firstPlayerIndex + index + 1,
|
||||||
|
clan: clanData[player.id]?.clan?.tag,
|
||||||
|
nickname:
|
||||||
|
clanData[player.id]?.account_name ??
|
||||||
|
`Unknown Player ${player.id}`,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||||
|
midnightLeaderboard =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? await getMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
regionRatingsInfo.current_season,
|
||||||
|
)
|
||||||
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
|
players = result;
|
||||||
|
playersBefore = result ? result[0].position - 1 : 0;
|
||||||
|
playersAfter =
|
||||||
|
regionRatingsInfo.count - result[result.length - 1].position;
|
||||||
|
titleName = `${
|
||||||
|
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
||||||
|
} - ${REGION_NAMES_SHORT[region]}`;
|
||||||
|
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||||
|
? leagueInfo.big_icon
|
||||||
|
: `https:${leagueInfo.big_icon}`;
|
||||||
|
titleDescription = `Top ${limit} players`;
|
||||||
|
} else {
|
||||||
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
|
regionRatingsInfo =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? await getRatingsInfo(region)
|
||||||
|
: await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/info.json`,
|
||||||
|
})
|
||||||
|
.then(async ({ data }) => {
|
||||||
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
|
throw new Error('Archived ratings info is malformed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = fetch(data.download_url!);
|
||||||
|
return (await response.then((response) =>
|
||||||
|
response.json(),
|
||||||
|
)) as RatingsInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
|
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
|
const neighbors =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? await fetch(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||||
|
limit / 2,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
(response) => response.json() as Promise<RatingsNeighbors>,
|
||||||
|
)
|
||||||
|
.then((players) =>
|
||||||
|
players.neighbors.map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
position: player.number,
|
||||||
|
score: player.score,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/latest.json`,
|
||||||
|
})
|
||||||
|
.then(async ({ data }) => {
|
||||||
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
|
throw new Error(
|
||||||
|
'Archived ratings latest data is malformed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = fetch(data.download_url!);
|
||||||
|
const players = (await response.then((response) =>
|
||||||
|
response.json(),
|
||||||
|
)) as BlitzkriegRatingsLeaderboard;
|
||||||
|
const playerIndex = players.findIndex(
|
||||||
|
(player) => player.id === id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerIndex === -1) {
|
||||||
|
throw new UserError(
|
||||||
|
`${accountInfo.nickname} didn't player ratings in season ${season}`,
|
||||||
|
{
|
||||||
|
cause:
|
||||||
|
'This player did not participate in this season or did not get past calibration.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const halfRange = Math.round(limit / 2);
|
||||||
|
const trimmed = players.slice(
|
||||||
|
playerIndex - halfRange,
|
||||||
|
playerIndex + halfRange + 1,
|
||||||
|
);
|
||||||
|
const clanData = await getClanAccountInfo(
|
||||||
|
region,
|
||||||
|
trimmed.map((player) => player.id),
|
||||||
|
['clan'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return trimmed.map((player, index) => ({
|
||||||
|
id: player.id,
|
||||||
|
score: player.score,
|
||||||
|
position: playerIndex + index + 1,
|
||||||
|
clan: clanData[player.id]!.clan?.tag,
|
||||||
|
nickname: clanData[player.id]!.account_name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
midnightLeaderboard =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? await getMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
regionRatingsInfo.current_season,
|
||||||
|
)
|
||||||
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
|
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
||||||
|
|
||||||
|
players = neighbors;
|
||||||
|
titleImage = clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
|
: undefined;
|
||||||
|
titleName = neighbors
|
||||||
|
? accountInfo.nickname
|
||||||
|
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
||||||
|
titleDescription = 'Ratings neighbours';
|
||||||
|
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
||||||
|
playersAfter =
|
||||||
|
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
||||||
|
playerId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
|
||||||
|
detail: undefined;
|
||||||
|
} = regionRatingsInfo;
|
||||||
|
|
||||||
|
const items = players.map((player) => {
|
||||||
|
const reward = resolvedRegionRatingsInfo.rewards.find(
|
||||||
|
(reward) =>
|
||||||
|
player.position >= reward.from_position &&
|
||||||
|
player.position <= reward.to_position,
|
||||||
|
);
|
||||||
|
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||||
|
(item) => item.id === player.id,
|
||||||
|
);
|
||||||
|
const midnightScore =
|
||||||
|
midnightIndex === undefined
|
||||||
|
? player.score
|
||||||
|
: midnightLeaderboard![midnightIndex].score;
|
||||||
|
const midnightPosition =
|
||||||
|
midnightIndex === undefined ? player.position : midnightIndex + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Leaderboard.Item
|
||||||
|
nickname={player.nickname}
|
||||||
|
score={player.score}
|
||||||
|
position={player.position}
|
||||||
|
clan={player.clan}
|
||||||
|
reward={reward}
|
||||||
|
deltaScore={player.score - midnightScore}
|
||||||
|
deltaPosition={midnightPosition - player.position}
|
||||||
|
key={player.id}
|
||||||
|
highlight={player.id === playerId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={titleName}
|
||||||
|
image={titleImage}
|
||||||
|
description={titleDescription}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{items && (
|
||||||
|
<Leaderboard.Root>
|
||||||
|
{playersBefore > 0 && (
|
||||||
|
<Leaderboard.Gap number={playersBefore} />
|
||||||
|
)}
|
||||||
|
{items}
|
||||||
|
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
||||||
|
</Leaderboard.Root>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
|
import { REGION_NAMES, Region } from '../constants/regions';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||||
import addClanChoices from '../core/discord/addClanChoices.js';
|
import fetchBlitz from '../core/blitz/fetchWargaming';
|
||||||
import addServerChoices from '../core/discord/addServerChoices.js';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo.js';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import { CommandRegistry } from '../events/interactionCreate/index.js';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { ClanList } from '../types/clanList.js';
|
import { ClanList } from '../types/clanList';
|
||||||
|
|
||||||
export const searchClansCommand: CommandRegistry = {
|
export const searchClansCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: true,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('searchclans')
|
.setName('search-clans')
|
||||||
.setDescription('Search clans')
|
.setDescription('Search clans')
|
||||||
.addStringOption(addServerChoices)
|
.addStringOption(addRegionChoices)
|
||||||
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
@@ -28,15 +27,15 @@ export const searchClansCommand: CommandRegistry = {
|
|||||||
),
|
),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const server = interaction.options.getString('server') as BlitzServer;
|
const server = interaction.options.getString('region') as Region;
|
||||||
const clan = interaction.options.getString('clan')!;
|
const clan = interaction.options.getString('clan')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const clanList = await getWargamingResponse<ClanList>(
|
const clanList = await fetchBlitz<ClanList>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`Clan search for "${markdownEscape(clan)}" in ${BLITZ_SERVERS[server]}`,
|
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
|
||||||
clanList.length === 0
|
clanList.length === 0
|
||||||
? 'No clans found.'
|
? 'No clans found.'
|
||||||
: `\`\`\`\n${clanList
|
: `\`\`\`\n${clanList
|
||||||
|
|||||||