Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa549e650 | ||
|
|
fe562915cb | ||
|
|
fff42b61e1 | ||
|
|
90ee71c9ca | ||
|
|
3f264d6a49 | ||
|
|
61eab0bca8 | ||
|
|
0a19157dbb | ||
|
|
382a23b661 | ||
|
|
98107291f5 | ||
|
|
c6779a714b | ||
|
|
ef8f0fd72d | ||
|
|
634bd2ea07 | ||
|
|
f8f88ff8f2 | ||
|
|
8a2dc24f7e | ||
|
|
dcc3ab6e4e | ||
|
|
dabeab8612 | ||
|
|
66dbc5b2ed | ||
|
|
2e2ab9b2d9 | ||
|
|
662e0bb9ce | ||
|
|
3f40e94235 | ||
|
|
483b625f2d | ||
|
|
d18cf625d0 | ||
|
|
b42df81b48 | ||
|
|
536fed5448 | ||
|
|
dc01eff812 | ||
|
|
5471c7fa32 | ||
|
|
83e28b4dac | ||
|
|
af95897bde | ||
|
|
4c2f6bea25 | ||
|
|
fdf77a43cb | ||
|
|
f1855a569a | ||
|
|
a0434b6ed2 | ||
|
|
365c714213 | ||
|
|
875abf4d32 | ||
|
|
6b9bf76da2 | ||
|
|
610c75d274 | ||
|
|
81826d0549 | ||
|
|
8caf4069dc | ||
|
|
6c496e616d | ||
|
|
3d5dd54ec0 | ||
|
|
e7bfe81940 | ||
|
|
fb9c04c779 | ||
|
|
84a1af8100 | ||
|
|
b804e60bac | ||
|
|
604e751bf4 | ||
|
|
2ce083222e | ||
|
|
9a44b1fcfb | ||
|
|
9df47777d0 | ||
|
|
aa78bc4045 | ||
|
|
4bfe34ba53 | ||
|
|
ad0fc69596 | ||
|
|
cfe71e15a4 | ||
|
|
bf7a319770 | ||
|
|
cadc84b32f | ||
|
|
7bde92c7f1 | ||
|
|
965d36cdc5 | ||
|
|
f8ef0063e4 | ||
|
|
fb4e00afc4 | ||
|
|
6ed9a191b9 | ||
|
|
b503f4fa8f | ||
|
|
7020e5641f | ||
|
|
5b4601b671 | ||
|
|
3e952faad3 | ||
|
|
c51abc1c64 | ||
|
|
c7e2780d17 | ||
|
|
28e2139683 | ||
|
|
20c839394a | ||
|
|
c5e1a7d6ab | ||
|
|
579d7570af | ||
|
|
1d1d751d43 | ||
|
|
86664b5491 | ||
|
|
b7e9898163 | ||
|
|
d4c0b01343 | ||
|
|
af67126156 | ||
|
|
9930360405 | ||
|
|
4fb61d6dbd | ||
|
|
bcb7ca5c4c | ||
|
|
d1fb5da002 | ||
|
|
17935fbe89 | ||
|
|
48578407db | ||
|
|
bda0c4ef1b | ||
|
|
7ea5e444b3 | ||
|
|
bef362935a | ||
|
|
f3144be382 | ||
|
|
818ee1d447 | ||
|
|
67e45ada85 | ||
|
|
bdc7132337 | ||
|
|
c5214f8d89 | ||
|
|
2be4dbea7a | ||
|
|
b951c81bf5 | ||
|
|
8c221a1295 | ||
|
|
c41f721a96 | ||
|
|
6910f3c95e | ||
|
|
e23f898c5f | ||
|
|
3cf81afc4a | ||
|
|
ca463a0f0e | ||
|
|
a76572ebe3 | ||
|
|
1a92cbd951 | ||
|
|
54c5faf929 | ||
|
|
d1d9e9e4d1 | ||
|
|
5ca64f839e | ||
|
|
4e7cd6f681 | ||
|
|
ba9f593716 | ||
|
|
9d3d74c7c5 | ||
|
|
aae26523ff | ||
|
|
338a2a14bf | ||
|
|
1a66cab435 | ||
|
|
3363c9ab58 | ||
|
|
10f64902aa | ||
|
|
25e1f4caa4 | ||
|
|
222fa89422 | ||
|
|
750e145f03 | ||
|
|
3bbbbfa402 | ||
|
|
f728751fa9 | ||
|
|
f5425d0e54 | ||
|
|
debaaccf4c | ||
|
|
a3e20501af | ||
|
|
c87a75f3fd | ||
|
|
55729c5eda | ||
|
|
06cbcc2339 | ||
|
|
1afe860e76 | ||
|
|
c0ac935652 | ||
|
|
7c76b0f5a5 | ||
|
|
42091c54b7 | ||
|
|
396b48d017 | ||
|
|
5bf83da726 | ||
|
|
bb1c0c7625 | ||
|
|
1170676141 | ||
|
|
7baaaa9db9 | ||
|
|
c1ecef0179 | ||
|
|
393da234e4 | ||
|
|
032d98e6bd | ||
|
|
f4a90cd531 | ||
|
|
82f03583fe | ||
|
|
81c1a50052 | ||
|
|
c7207799e2 | ||
|
|
e0a719ee93 | ||
|
|
ee6d2e7d82 | ||
|
|
14710b5218 | ||
|
|
4b5e0d1c1c | ||
|
|
9968e8346c | ||
|
|
bc4da17f10 | ||
|
|
da56c563fc | ||
|
|
95f4dbf6a0 | ||
|
|
6c36f343ea | ||
|
|
9257ebdd1e | ||
|
|
8f1017195f | ||
|
|
20da294b36 | ||
|
|
a63415461a | ||
|
|
2bb4c78551 | ||
|
|
2909e61721 | ||
|
|
171da2a194 | ||
|
|
933c468e2d | ||
|
|
0d1eb9b12e | ||
|
|
02a03b4d68 | ||
|
|
ef9f3bd361 | ||
|
|
f18b6ee754 | ||
|
|
deb9e463e1 | ||
|
|
c799534009 | ||
|
|
aea01072fb | ||
|
|
ca8dd5a702 | ||
|
|
e79c4a1e3f | ||
|
|
e19d0d948a | ||
|
|
a160ccd095 | ||
|
|
d2d0477186 | ||
|
|
c742c0725d | ||
|
|
9e6343a111 | ||
|
|
faf33b5f2d | ||
|
|
8a78ac7d82 | ||
|
|
d7c7814b3a | ||
|
|
15bf02f829 | ||
|
|
4f62a594b3 | ||
|
|
64253d0120 | ||
|
|
286936131d | ||
|
|
95588ef6b6 | ||
|
|
daef04ccd8 | ||
|
|
86e385c548 | ||
|
|
cc193b99d6 | ||
|
|
c026007937 | ||
|
|
dbc117a9d4 | ||
|
|
34665bd97a | ||
|
|
6d05e607b3 | ||
|
|
14bfc3018b | ||
|
|
7e4f195f94 | ||
|
|
4b6de934a5 | ||
|
|
14fbb595fd | ||
|
|
18a5fb80d0 | ||
|
|
551ef79f03 | ||
|
|
81aec6bad2 | ||
|
|
d08949c597 | ||
|
|
212aee0373 | ||
|
|
059b78c9fb | ||
|
|
11ba5fc28e | ||
|
|
fbde9ec64f | ||
|
|
6600bba58c | ||
|
|
9e340d2e16 | ||
|
|
6f784e9e94 | ||
|
|
62f6aabf05 | ||
|
|
46457a8bdc | ||
|
|
1081e836f5 | ||
|
|
dfe1e37592 | ||
|
|
0b9db21c10 | ||
|
|
57527115d4 | ||
|
|
8d2679c458 | ||
|
|
ca00f18a6a | ||
|
|
466f68701f | ||
|
|
1bb83da253 | ||
|
|
bd1e17bac9 | ||
|
|
322f95e42e | ||
|
|
3ea77f5899 | ||
|
|
882e61ff44 | ||
|
|
d991be4934 | ||
|
|
dfba8bd15f | ||
|
|
87e23673fb | ||
|
|
8fbd1acf4a | ||
|
|
d7dfbaa799 | ||
|
|
04ce20857e | ||
|
|
4550246d63 | ||
|
|
dd673f44ae | ||
|
|
3245284ecc | ||
|
|
3d9872a671 | ||
|
|
f02db2a2d8 | ||
|
|
d1ebb79432 | ||
|
|
44f03a1fce | ||
|
|
6b6ee94d9b | ||
|
|
81c2ca80bc | ||
|
|
c83cec8ed7 | ||
|
|
93ffe5c507 | ||
|
|
8cd1a70333 | ||
|
|
06ae024c2c | ||
|
|
ce8fd0f22f | ||
|
|
7dc1aff1dc | ||
|
|
a60003127f | ||
|
|
7d1ba58806 | ||
|
|
963d71e221 | ||
|
|
2a804fda1c | ||
|
|
597a1a04d2 | ||
|
|
aec6f49ea1 | ||
|
|
e1ad710de5 | ||
|
|
edc8447617 | ||
|
|
f726d4f8e8 | ||
|
|
1f333f6b88 | ||
|
|
9b855fca47 | ||
|
|
c5f433fdf4 | ||
|
|
fd61dae1c1 | ||
|
|
2c45f9759f | ||
|
|
0d3261f748 | ||
|
|
d38e873800 | ||
|
|
ca38914095 | ||
|
|
ee5beeea45 | ||
|
|
a9d40d9f49 | ||
|
|
b8be916c18 | ||
|
|
24d0313fd3 | ||
|
|
c3d2a81327 | ||
|
|
dfeb117663 | ||
|
|
495a830506 | ||
|
|
a730f67ca5 | ||
|
|
9a3867770d | ||
|
|
f8a66bb5e7 | ||
|
|
4b00e1342e | ||
|
|
f9e0584f00 | ||
|
|
900c290e75 | ||
|
|
69e1577d18 | ||
|
|
a6d9044c3a | ||
|
|
9e23d60ef1 | ||
|
|
f12449119c | ||
|
|
533bfc4458 | ||
|
|
1317c2879c | ||
|
|
f9e51f894a | ||
|
|
6fdd4ba833 | ||
|
|
9495ae2336 | ||
|
|
43f634e198 | ||
|
|
ad51be7afc | ||
|
|
15b8bfdf8f | ||
|
|
cb8ae47be6 | ||
|
|
db846c0680 | ||
|
|
630a37025e | ||
|
|
d3bbc7f2b0 | ||
|
|
4e0321ca5f | ||
|
|
ec00bdf9de | ||
|
|
01dd7ad9c6 | ||
|
|
d726005c38 | ||
|
|
4f3e868367 | ||
|
|
ed8bab7df8 | ||
|
|
9bdb14e13a | ||
|
|
b827f7f730 | ||
|
|
07e3660388 | ||
|
|
970ab9004e | ||
|
|
862445259e | ||
|
|
e96538bae0 | ||
|
|
7447ccc487 | ||
|
|
bcd3a00f91 | ||
|
|
8c3675546f | ||
|
|
ec64dab720 | ||
|
|
db976c4ae9 | ||
|
|
0db96795b3 | ||
|
|
59bae96c1b | ||
|
|
d062a0da1b | ||
|
|
d2668974f7 | ||
|
|
78e4c82f20 | ||
|
|
f965452c5d | ||
|
|
be644bb3d9 | ||
|
|
97cc9509c0 | ||
|
|
6dc7a10c2e | ||
|
|
19298b9ad6 | ||
|
|
97d427a6a8 | ||
|
|
bff6eb0503 | ||
|
|
dc9ab2067d | ||
|
|
e8e1f7bafb | ||
|
|
e50a456025 | ||
|
|
d442bbca1a | ||
|
|
92e35bd0ac | ||
|
|
cb9d283f62 | ||
|
|
8aba19e05b | ||
|
|
b7b4035492 | ||
|
|
3667fd82b7 | ||
|
|
691d5cde27 | ||
|
|
7f431b07e1 | ||
|
|
8264f19ad4 | ||
|
|
e3864b86a4 | ||
|
|
e947cfba45 | ||
|
|
e69590c035 | ||
|
|
8b19393d7c | ||
|
|
9480603572 | ||
|
|
90c6535a4c | ||
|
|
258588921a | ||
|
|
525b309037 | ||
|
|
2f941b9224 | ||
|
|
259c4240b5 | ||
|
|
9eb7157fce | ||
|
|
1138e1a826 | ||
|
|
e5057b8c1d | ||
|
|
7c04243b3d | ||
|
|
c6b54454be | ||
|
|
876fb0fc5e | ||
|
|
38f912dc0a | ||
|
|
eb24f49766 | ||
|
|
168ab1e1a1 | ||
|
|
49f601d200 | ||
|
|
15f32a0044 | ||
|
|
7bb3c60a61 | ||
|
|
4d1dfc1ae5 | ||
|
|
ec5dfd7727 | ||
|
|
8e5a588e87 | ||
|
|
b9c3cfaee4 | ||
|
|
01bd3f8d68 | ||
|
|
b39f545669 | ||
|
|
e7f517e157 | ||
|
|
1d7de715e7 | ||
|
|
76501c1aa0 | ||
|
|
a535a07810 | ||
|
|
6add54e86d | ||
|
|
a74eba9e97 | ||
|
|
34f782e747 | ||
|
|
1990322d6d | ||
|
|
0339979f9f | ||
|
|
823465fde9 | ||
|
|
1feae49490 | ||
|
|
1a1e2169a8 | ||
|
|
f1b1e25d9a | ||
|
|
ce6472be8d | ||
|
|
79232cecb9 | ||
|
|
1a802da0ce | ||
|
|
b33a845c08 | ||
|
|
6643b51a9b | ||
|
|
1cce00a09a | ||
|
|
1ef9a0289f | ||
|
|
498a359f01 | ||
|
|
ebbcdc2089 | ||
|
|
cced57b22e | ||
|
|
96d47a82bc | ||
|
|
b4cab18ecd | ||
|
|
d801447e5d | ||
|
|
8c041d4eef | ||
|
|
6ff2a23ff7 | ||
|
|
925ac96264 | ||
|
|
9e89198cd5 | ||
|
|
a1e6643dc0 | ||
|
|
63257a13f5 | ||
|
|
e37e0ccf9f | ||
|
|
018783bb47 | ||
|
|
f883670cbc | ||
|
|
57f6e928b7 | ||
|
|
cbeb83d8d0 | ||
|
|
33b4ea04b4 | ||
|
|
350434cc5a | ||
|
|
c0eca0c851 | ||
|
|
cdac43fa5f | ||
|
|
30aeabba88 | ||
|
|
02a07f3a8a | ||
|
|
986a32b3ce | ||
|
|
8c4afa73f8 | ||
|
|
6db3ccdb40 | ||
|
|
cf0fe5cbba | ||
|
|
621398f95b | ||
|
|
186a972db7 | ||
|
|
7473a17a5e | ||
|
|
66e08afb0d | ||
|
|
851da1d020 | ||
|
|
c44840978c | ||
|
|
90e012319a | ||
|
|
18de9cbaab | ||
|
|
42a76cbd38 | ||
|
|
eb95634801 | ||
|
|
de8bfff47f | ||
|
|
07c18911df | ||
|
|
cc769cc330 | ||
|
|
9794f58936 | ||
|
|
a44fcff3b1 | ||
|
|
7e4a3e88b9 | ||
|
|
8fd45e2205 | ||
|
|
7d235057e8 | ||
|
|
63a8a67771 | ||
|
|
bac39ca176 | ||
|
|
1979047bbc | ||
|
|
8860c1b0f8 | ||
|
|
626b1e8601 | ||
|
|
04086f31b9 | ||
|
|
7037aa9c3b | ||
|
|
f5bab8a698 | ||
|
|
ee44b1b1d8 | ||
|
|
d0f91df95b | ||
|
|
dc1c49da6a | ||
|
|
331040bbdf | ||
|
|
70e1d4b30d | ||
|
|
03dbd797fb | ||
|
|
eb4cb1fbfe |
No files matched your search
@@ -1,34 +0,0 @@
|
||||
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
|
||||
@@ -1,34 +0,0 @@
|
||||
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
|
||||
@@ -2,7 +2,7 @@ name: CD Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: Upload pages artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
path: dist/docs
|
||||
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
|
||||
@@ -22,11 +22,11 @@ jobs:
|
||||
- name: Download release assets
|
||||
run: |
|
||||
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 \
|
||||
| tr -d \")
|
||||
curl -L -o dist.zip $DOWNLOAD_URL
|
||||
unzip dist.zip
|
||||
curl -L -o bot.zip $DOWNLOAD_URL
|
||||
unzip bot.zip
|
||||
|
||||
- name: Run bot
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
|
||||
@@ -1,8 +1,12 @@
|
||||
name: CD Europe Ratings Latest
|
||||
name: CD Ratings Asia
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# asia midnight: '0 17 * * *'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 17 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +20,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,11 +28,11 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
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
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
@@ -1,8 +1,12 @@
|
||||
name: CD North America Ratings Latest
|
||||
name: CD Ratings Europe
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# eu midnight: '0 23 * * *'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 23 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -31,4 +35,4 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --latest
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
@@ -1,8 +1,12 @@
|
||||
name: CD Europe Ratings Leaderboards
|
||||
name: CD Ratings North America
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# na midnight: '0 5 * * *'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 23 * * *"
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +20,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,11 +28,11 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
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
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
@@ -1,6 +1,6 @@
|
||||
name: CI Build
|
||||
name: CI Build Bot
|
||||
|
||||
on: [push, pull_request]
|
||||
on: push
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -34,10 +34,10 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
run: yarn build:bot
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
name: bot
|
||||
path: dist/bot
|
||||
@@ -0,0 +1,43 @@
|
||||
name: CI Build Docs
|
||||
|
||||
on: push
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Build
|
||||
run: yarn build:docs
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: dist/docs
|
||||
@@ -0,0 +1,43 @@
|
||||
name: CI Build Website
|
||||
|
||||
on: push
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Build
|
||||
run: yarn build:website
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: website
|
||||
path: dist/website
|
||||
@@ -1,8 +1,6 @@
|
||||
name: CD North America Ratings Leaderboards
|
||||
name: CI Version
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -31,4 +29,4 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na
|
||||
run: yarn lint:version
|
||||
@@ -7,8 +7,11 @@ package-lock.json
|
||||
dist/
|
||||
dist.*
|
||||
yarn-error.log
|
||||
commands.txt
|
||||
/test.*
|
||||
test.*
|
||||
/docs/.vitepress/dist
|
||||
/docs/.vitepress/cache
|
||||
servers.csv
|
||||
servers.csv
|
||||
.next/
|
||||
tsconfig.tsbuildinfo
|
||||
temp/
|
||||
@@ -1,3 +0,0 @@
|
||||
tasks:
|
||||
- init: yarn
|
||||
- command: yarn run pw
|
||||
@@ -1,45 +1,80 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"AABBOX",
|
||||
"Abhi",
|
||||
"accountinfo",
|
||||
"alphatest",
|
||||
"apcr",
|
||||
"atgm",
|
||||
"bepaint",
|
||||
"BINORMAL",
|
||||
"blitzstars",
|
||||
"cleandist",
|
||||
"cockroackdb",
|
||||
"corsproxy",
|
||||
"CUBETEXCOORD",
|
||||
"customperiod",
|
||||
"deburr",
|
||||
"decalmask",
|
||||
"diacriticless",
|
||||
"dvpl",
|
||||
"DVPLCRC",
|
||||
"Everytime",
|
||||
"FASTNAME",
|
||||
"fullstats",
|
||||
"fuzzysort",
|
||||
"gamemode",
|
||||
"immer",
|
||||
"JOINTINDEX",
|
||||
"JOINTWEIGHT",
|
||||
"kranvagn",
|
||||
"krieg",
|
||||
"Leaderboard",
|
||||
"leaderboards",
|
||||
"MURICA",
|
||||
"normalmap",
|
||||
"Oopsy",
|
||||
"outdir",
|
||||
"outfile",
|
||||
"ownedtanks",
|
||||
"playerachievements",
|
||||
"playerinfo",
|
||||
"ratingsleaderboard",
|
||||
"ratingsplot",
|
||||
"researchable",
|
||||
"Resvg",
|
||||
"roboto",
|
||||
"SCPG",
|
||||
"searchclans",
|
||||
"searchplayers",
|
||||
"searchtanks",
|
||||
"SKLLD",
|
||||
"Smri",
|
||||
"Specularity",
|
||||
"startcase",
|
||||
"statsfull",
|
||||
"steamapps",
|
||||
"Stringifying",
|
||||
"strvk",
|
||||
"subnation",
|
||||
"tankaverages",
|
||||
"tankopedia",
|
||||
"tankopedias",
|
||||
"tankstats",
|
||||
"TEXCOORD",
|
||||
"tresabhi",
|
||||
"Unicum",
|
||||
"Unregistering",
|
||||
"vickers",
|
||||
"vitepress",
|
||||
"Vovko",
|
||||
"wargaming",
|
||||
"webp",
|
||||
"winrate"
|
||||
"winrate",
|
||||
"wotblitz",
|
||||
"XMLDVPL",
|
||||
"YAMLDVPL",
|
||||
"zustand"
|
||||
],
|
||||
"deno.enable": false,
|
||||
"deno.unstable": false
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
# 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
|
||||

|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
## Visit the website
|
||||
|
||||
### Running
|
||||
### https://blitz-krieg.vercel.app/
|
||||
|
||||
```bash
|
||||
yarn start discord-token=<token> wargaming-application-id=<id>
|
||||
```
|
||||

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

|
||||
@@ -8,5 +8,7 @@
|
||||
"sklld_peasant_role": "896878529512370228",
|
||||
|
||||
"tres_guild_id": "734786591205359697",
|
||||
"tres_log_channel": "1109262265850544148"
|
||||
"tres_log_channel": "1109262265850544148",
|
||||
|
||||
"test_guild_id": "1124729294829142116"
|
||||
}
|
||||
@@ -8,11 +8,12 @@ export default defineConfig({
|
||||
title: 'blitzkrieg',
|
||||
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
||||
base: '/blitzkrieg/',
|
||||
outDir: '../dist/docs',
|
||||
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Guide', link: '/guide/about' },
|
||||
{ text: 'Guide', link: '/guide/introduction' },
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.12.3
|
||||
|
||||
3-day downtime fixed!
|
||||
|
||||
## Changes
|
||||
|
||||
- Split `/ratings` into `/ratings current` and `/ratings archive`
|
||||
- `current` now properly handles when there is no ongoing ratings season
|
||||
- `archive` displays older archived seasons
|
||||
|
||||
## Fixes
|
||||
|
||||
- The bot is now no ratings seasons tolerant
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.12.4
|
||||
|
||||
Added very badly-coded temporary `/clan-event` command (without linting)
|
||||
|
||||
## Changes
|
||||
|
||||
- Added `/clan-event` command
|
||||
@@ -0,0 +1,12 @@
|
||||
# Blitzkrieg 1.12.5
|
||||
|
||||
Some patch up work
|
||||
|
||||
## Changes
|
||||
|
||||
- `/clan-event` now is more legible and comes with an approximation warning
|
||||
|
||||
## Fixes
|
||||
|
||||
- `/clan-event` now uses a more optimized way of fetching large data
|
||||
- This should fix the issue where too many people using the command broke it
|
||||
@@ -0,0 +1,12 @@
|
||||
# Blitzkrieg 1.12.6
|
||||
|
||||
Stops making the bot lie!
|
||||
|
||||
## Changes
|
||||
|
||||
- The approximation warning for `/clan-event` has been made more concise
|
||||
- Players with `0` wins are now omitted
|
||||
|
||||
## Fixes
|
||||
|
||||
- Players who haven't player within the current day are now omitted
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.12.7
|
||||
|
||||
A few fixes and clarifications
|
||||
|
||||
## Changes
|
||||
|
||||
- `/clan-event`
|
||||
- Made statement about the limited publicly available data
|
||||
- Added a statement about reset times
|
||||
|
||||
## Fixes
|
||||
|
||||
- Commands with images now once again work
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.12.8
|
||||
|
||||
Blitzkrieg is a completely free service and I earn no revenue from it. To keep it sustainable, I have to rely on free services. Unfortunately for embeds, seems like I have exceeded my limits. Before this service starts charging me, I have shut down embeds. They'll hopefully be back in 1.13 alongside the website! Only the future will tell...
|
||||
|
||||
## Changes
|
||||
|
||||
- Removed all embed buttons
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.12.9
|
||||
|
||||
Now that the clan event is over, we don't need `/clan-event`. This command may be reintroduced in the future as a part of a different command/feature and will definitely be re-enabled during the next clan event.
|
||||
|
||||
## Changes
|
||||
|
||||
- Disabled the `/clan-event` command
|
||||
@@ -0,0 +1,32 @@
|
||||
# Blitzkrieg 1.13.0
|
||||
|
||||
One of the biggest updates ever! This changelog's size does not truly represent the work (and brain cells) spent.
|
||||
|
||||
## Changes
|
||||
|
||||
- Added Blitzkrieg homepage
|
||||
- Added session tracker
|
||||
- Search players up and select one
|
||||
- This starts tracking your games from that point
|
||||
- This is displayed in a similar fashion to the `/today` command
|
||||
- Updated the permissions doc
|
||||
- This now includes all 3 of the needed permissions for full operation
|
||||
- Also renamed the permissions to better match Discord
|
||||
- Renamed `/help` to `/about`
|
||||
- Renamed `/help about` to `/about bot`
|
||||
|
||||
## Fixes
|
||||
|
||||
- Added `/ratings` error handling for `null` clans, players, and rewards
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Removed all server assets
|
||||
- Simplified build script to only compile the bot
|
||||
- Upgraded the build script to TypeScript
|
||||
- Moved docs build to `dist/docs`
|
||||
- Split `tsconfig.json` for Next.js and esbuild
|
||||
- Removed Gitpod config
|
||||
- Switched to `.env`
|
||||
- Made Wargaming application ID public
|
||||
- Started migration to Radix UI
|
||||
@@ -0,0 +1,17 @@
|
||||
# Blitzkrieg 1.13.1
|
||||
|
||||
The quickest minor update in town 🤠
|
||||
|
||||
## Changes
|
||||
|
||||
- Added a title, description, and icon to the website
|
||||
- Added display options to the session tracker
|
||||
|
||||
## Fixes
|
||||
|
||||
- Center aligned stats in breakdown to work better on low width devices
|
||||
- Center aligned the Blitzkrieg logo in the navbar
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Added up-to-date information to `README.md` and embed docs
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.13.2
|
||||
|
||||
One lonely albeit important fix.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Gradients for `/stats` now use Radix's new hex values
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.13.3
|
||||
|
||||
Streamer's delight!
|
||||
|
||||
## Features
|
||||
|
||||
- Added config context menu for the session tracker embed
|
||||
@@ -0,0 +1,45 @@
|
||||
# Blitzkrieg 1.14.0
|
||||
|
||||
Elegance has never been met like this before
|
||||
|
||||
## Features
|
||||
|
||||
- Added `/link`
|
||||
- Unlike `/verify`, `/link` links you Discord and Blitz accounts across all servers
|
||||
- This command does not change your nickname
|
||||
- `/link` now simply displays a deprecation message
|
||||
- Usernames can still be inputted per-command to override your linked account
|
||||
- `/stats` and `/full-stats` are now way simpler
|
||||
- `/stats <type> <period>` has been replaced with `/stats <30|60|90|...>` (likewise, `/full-stats`)
|
||||
- These commands now have filters without having to opt into `multi-tank`
|
||||
- Added `/breakdown`
|
||||
- Unlike `/today`, `/breakdown` lets you select any period of time
|
||||
- `/breakdown` now simply displays a deprecation message
|
||||
- `/about commands` are now sorted alphabetically
|
||||
- Added a `/reset` button to the options dropdown and embed context-menu
|
||||
|
||||
## Changes
|
||||
|
||||
- Simplified no ongoing ratings season edge case
|
||||
- Updated welcome message for Skilled server to use `/link` and other up-to-date information
|
||||
- Tier weights are now reported in Arabic numerals instead of Roman
|
||||
- Removed tank images from `/owned-tanks`
|
||||
- Wargaming messed up their API and almost no tanks have any images anymore
|
||||
|
||||
## Fixes
|
||||
|
||||
- Banners now use a more economical `.webp` format just like the home page
|
||||
- Removed unused color palettes
|
||||
|
||||
## Technical changes
|
||||
|
||||
- Guild commands are now handled more efficiently
|
||||
- Removed nickname-based Blitz username inference
|
||||
- Code split a lot of common functionalities
|
||||
- Re-arranged `core` folder
|
||||
- Added midnight and latest ratings leaderboard caches
|
||||
- Merged renderers into their primary user files
|
||||
- Added sharding
|
||||
- Disabled for now but good for future proofing
|
||||
- Switched to a way cleaner logging schema
|
||||
- Hosting is now done in 6 hour periods instead of 5
|
||||
@@ -0,0 +1,17 @@
|
||||
# Blitzkrieg 1.14.1
|
||||
|
||||
Oopsy daisy, a memory leak!
|
||||
|
||||
## Changes
|
||||
|
||||
- Simplified message for users to use `/link`
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed a memory leak in the render thread manager
|
||||
|
||||
## Technical changes
|
||||
|
||||
- Added `UserError` extending `Error`
|
||||
- This lets the bot avoid spamming the debug console for mistakes users make
|
||||
- Reduced render threads to 2
|
||||
@@ -0,0 +1,8 @@
|
||||
# Blitzkrieg 1.14.2
|
||||
|
||||
A late night hot fix for my homies
|
||||
|
||||
## Fixes
|
||||
|
||||
- Clan info is now fetched using `clan_id` instead of `account_id`
|
||||
- Generic stats row names are now bounded by a max width
|
||||
@@ -0,0 +1,24 @@
|
||||
# Blitzkrieg 1.14.3
|
||||
|
||||
Pull your brush out and start polishing.
|
||||
|
||||
## Changes
|
||||
|
||||

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

|
||||
|
||||
- Blitzkrieg ratings leaderboard
|
||||
- Available at https://blitz-krieg.vercel.app/tools/ratings
|
||||
- **Highly experimental; [please report bugs on Discord server](https://discord.gg/nDt7AjGJQH)**
|
||||
- Current season leaderboards are live meaning that there may be scores and positions that are out of order
|
||||
- Options
|
||||
- Regions can be swapped between NA, EU, and Asia
|
||||
- The current season and all archived season (from 49 and onwards) are available
|
||||
- Jump: used to jump to certain players
|
||||
- Score: finds the first player with at least your desired score
|
||||
- Position: finds the player at that position
|
||||
- League: finds the top-most player in that league
|
||||
- Player: finds the player
|
||||
- **Jumping to scores and positions for the current season is a compounding function (hence the loading screen) and is limited to the speed of Wargaming's APIs which is pretty darn slow**
|
||||
- Page controls
|
||||
- Flip between pages left and right
|
||||
- 33 players are loaded at once
|
||||
- Left control is disabled on the first page and vice versa
|
||||
- Page turning controls are also disabled if the next/previous page hasn't loaded yet
|
||||
- Page numbers can be manually typed in the text field too
|
||||
- This is available both at the top and the bottom of the screen
|
||||
- Players
|
||||
- Shows the position of the player
|
||||
- Shows the player nickname and clan name
|
||||
- "Deleted player ID" us shown when loading a deleted player
|
||||
- A skeleton is shown when the player and clan is loading
|
||||
- Shows the player percentile
|
||||
- Shows reward for the position
|
||||
- This is blank when there is no reward
|
||||
- This column isn't available for archived seasons
|
||||
- Shows the score
|
||||
- A skeleton is shown when loading
|
||||
- Click on a row to highlight it
|
||||
- Using a jump button also highlights a row
|
||||
- `/credits`: thanks who helped made this project
|
||||
|
||||
## Changes
|
||||
|
||||
- Error messages that user face are now even more simplified
|
||||
- Increased username and clan autocomplete size limits to `25`
|
||||
- Added capability for per-page accent theming
|
||||
- Example: the ratings page is red while the session tracker is blue
|
||||
- Simplified welcome message for SKLLD clan server
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Isolated tankopedia info
|
||||
- Implemented `corsproxy.io` for non-timeout tolerant endpoints
|
||||
- Latest archived season number is now data driven
|
||||
- Ratings archives are now public
|
||||
- This eliminates the need of `octokit`
|
||||
- Ratings leaderboard archiver is now error tolerant
|
||||
- Fetch attempts are made every `1000ms`
|
||||
- Blitz errors are now managed better freeing up the queue faster
|
||||
- Isolated first archived season number
|
||||
- Added more advanced PSAs
|
||||
- These can now feature images or embeds
|
||||
- These can now modify message contents
|
||||
- These can also now be secret
|
||||
|
||||
## Experimental Changes
|
||||
|
||||
- Added tankopedia extractor
|
||||
- Retrieves tankopedia data from the game files
|
||||
- This is not utilized as of right now
|
||||
@@ -0,0 +1,24 @@
|
||||
# Blitzkrieg 1.15.1
|
||||
|
||||
'MURICA!
|
||||
|
||||
## Changes
|
||||
|
||||
- "Neighbor" is now spelled in the American way
|
||||
- Updated documentation website with the latest information
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed the guide link on the documentation website
|
||||
|
||||
## Technical features
|
||||
|
||||
- Added capability to fetch patiently while also parsing the JSON
|
||||
|
||||
## Technical changes
|
||||
|
||||
- Removed neighbor relative position caching
|
||||
|
||||
## Technical fixes
|
||||
|
||||
- `/ratings` now uses the non-caching APIs
|
||||
@@ -0,0 +1,9 @@
|
||||
# Blitzkrieg 1.15.2
|
||||
|
||||
Infinity reduced to a finite value.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Added a maximum value to the jump to position button
|
||||
- Removed neighbor checking for the seeding player
|
||||
- This fixes the jump to position button not loading at all
|
||||
@@ -0,0 +1,30 @@
|
||||
# Blitzkrieg 1.15.3
|
||||
|
||||
The bot now gets more expressive!
|
||||
|
||||
## Features
|
||||
|
||||
- The bot can now have presence messages; examples:
|
||||
- "Living in {number} servers"
|
||||
- "Server {number} users"
|
||||
- "Observing {number} channels"
|
||||
- More will be added in the future! Give me some ideas
|
||||
|
||||
## Changes
|
||||
|
||||
- Moved `/credits` to `/about creators`
|
||||
- `/debug` command now ditches embeds and has better information to serve
|
||||
|
||||
## Technical Features
|
||||
|
||||
- Added a workflow and lint for package and changelog version miss match
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Removed manual trigger for Asian latest ratings leaderboard archiver
|
||||
- Leaderboard archiver now accepts an explicit target like `midnight` or `latest`
|
||||
- Combined midnight and latest leaderboard archival workflow into one file
|
||||
|
||||
## Technical Fixed
|
||||
|
||||
- Reviewed all workflow triggers and updated them
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.15.4
|
||||
|
||||
Unlocking future potential
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Moved the word "Ratings" to the front of the workflow name
|
||||
- Renamed `blitzkrieg-db` to `blitzkrieg-assets`
|
||||
- Moved the region folders from the root to `regions`
|
||||
|
||||
## Technical Fixes
|
||||
|
||||
- The ratings leaderboard archiver now correctly skips undefined players
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.15.5
|
||||
|
||||
Oops, this should've been in the last patch.
|
||||
|
||||
## Fixes
|
||||
|
||||
- User and channel cache is now guild cache exhaustive
|
||||
@@ -0,0 +1,11 @@
|
||||
# Blitzkrieg 1.15.6
|
||||
|
||||
This should have also been in the last update lol.
|
||||
|
||||
## Changes
|
||||
|
||||
- Changed the format of `/about creators` and added one more person
|
||||
|
||||
## Fixes
|
||||
|
||||
- Archived ratings leaderboards are now fetched from the correct URL path
|
||||
@@ -0,0 +1,20 @@
|
||||
# Blitzkrieg 1.15.7
|
||||
|
||||
Simple yet powerful theming options for the session tracker
|
||||
|
||||
## Features
|
||||
|
||||
- The accent color of the session tracker (hence the embed too) can now be tweaked
|
||||
- Access this setting via the dropdown from Options > Accent color
|
||||
|
||||
## Fixes
|
||||
|
||||
- Reimplemented the commas for last numbers in the overflow count of the ratings leaderboard
|
||||
- For example, "+1234567 more" will now read "+1,234,567 more"
|
||||
|
||||
## Technical Features
|
||||
|
||||
- Added class names for more in-depth customization for those who are comfortable with CSS
|
||||
- `session-tracker-root`
|
||||
- `session-tracker-row`
|
||||
- `session-tracker-row-stat`
|
||||
@@ -0,0 +1,9 @@
|
||||
# Blitzkrieg 1.15.8
|
||||
|
||||
A request fulfilled \:\)
|
||||
|
||||
## Features
|
||||
|
||||
- Added custom titles in session tracker
|
||||
- Change the title through Options > Edit title...
|
||||
- Colors are shown when selecting a theme color for the session tracker
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.15.9
|
||||
|
||||
~~Curiosity killed the cat~~ Ratings killed the Blitzkrieg
|
||||
|
||||
## Fixes
|
||||
|
||||
- Deleted ratings players are now handled properly
|
||||
|
||||
## Technical Fixes
|
||||
|
||||
- Latest archived season number
|
||||
- Is now cached properly
|
||||
- Is now fetched in a chaining fashion correctly
|
||||
@@ -1,10 +0,0 @@
|
||||
# About
|
||||
|
||||
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
|
||||
|
||||
- Track statistics
|
||||
- Find inactive players in a clan
|
||||
- Lookup owned tanks
|
||||
- Create embeds for streams
|
||||
- Track a player/tanks statistical evolution
|
||||
- ...and a variety of other similar things
|
||||
@@ -0,0 +1,5 @@
|
||||
# Creators
|
||||
|
||||
- **TrèsAbhi** `@tresabhi`: Creator and main programmer
|
||||
- **Prince_NA** `@prince_na`: Game advisor & tester
|
||||
- **Vovko**: Technical advisor
|
||||
@@ -1,23 +1,23 @@
|
||||
# Embeds
|
||||
|
||||
Embeds are generated by a few commands as a URL that can be loaded in a browser/streaming software.
|
||||
Embeds are URLs generated by commands or tools that represent the "naked" version of the respective command or tool
|
||||
|
||||
## Creating Embeds
|
||||
|
||||
1. Use a command of your liking (for example, `/stats`).
|
||||
2. Customize the command however you like and the URL generated for the embed will match your needs.
|
||||
3. Send the command and when the bot is "done thinking," an "Embed" button will appear below the bot's reply.
|
||||
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.
|
||||
2. Copy the URL from your browser.
|
||||
3. Paste the URL into a Browser Source.
|
||||
4. Set the refresh rate/FPS to something very low like `0.0167` (that's 1 frame every 60 seconds).
|
||||
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 albeit with a significant delay.
|
||||
- They auto refresh every `60s`.
|
||||
- All embeds and images generated by Blitzkrieg have a width of `640px` but the height varies and must be found manually through trial and error.
|
||||
- It's okay to have a Browser Source that has a height larger than needed as Blitzkrieg embeds are transparent.
|
||||
- They load like any normal website
|
||||
- They auto refresh every `1s`
|
||||
@@ -1,17 +0,0 @@
|
||||
# Installing Blitzkrieg
|
||||
|
||||
It's easy as 1, 2, 3!
|
||||
|
||||
## Step 1
|
||||
|
||||
Join a server that already has Blitzkrieg or [join our official Discord server](https://discord.gg/nDt7AjGJQH).
|
||||
|
||||
## Step 2
|
||||
|
||||
View the bot's profile and click "Add to Server".
|
||||
|
||||
<video src="https://i.imgur.com/Zq0P3m2.mp4" autoplay loop muted width=320 title="how to install"></video>
|
||||
|
||||
## Step 3
|
||||
|
||||
Review the permissions the bot is asking for and add it to your server!
|
||||
@@ -0,0 +1,22 @@
|
||||
# Introduction
|
||||
|
||||
🎉 Services for everything and anything World of Tanks Blitz
|
||||
|
||||
## Bot
|
||||
|
||||
Blitzkrieg is available as a Discord bot and can be [installed very easily](/guide/invite).
|
||||
|
||||
- Track statistics
|
||||
- Find inactive players in a clan
|
||||
- Lookup owned tanks
|
||||
- Create embeds for streams
|
||||
- Track a player/tanks statistical evolution
|
||||
- ...and a variety of other similar things
|
||||
|
||||
## Website
|
||||
|
||||
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
|
||||
|
||||
- Session tracker
|
||||
- Ratings leaderboard
|
||||
- Way more to come
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
The following Discord permissions are required by the bot to ensure full functionality:
|
||||
|
||||
- Manage nicknames
|
||||
- Read messages / view channels
|
||||
- Attach files
|
||||
- View channels
|
||||
@@ -12,10 +12,10 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Add to Discord Server
|
||||
link: guide/install
|
||||
link: /guide/invite
|
||||
- theme: alt
|
||||
text: Guide
|
||||
link: /guide/about
|
||||
link: /guide/introduction
|
||||
|
||||
features:
|
||||
- title: Clean and Efficient UI
|
||||
|
||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
|
||||
|
||||
const withVanillaExtract = createVanillaExtractPlugin({ tests: /\.css$/ });
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
...withVanillaExtract(),
|
||||
|
||||
distDir: 'dist/website',
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,89 @@
|
||||
{
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.12.1",
|
||||
"description": "🎉 All-in-one Discord bot for everything World of Tanks Blitz",
|
||||
"version": "1.15.9",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:bot": "node scripts/build --build=bot",
|
||||
"build:server": "node scripts/build --build=server",
|
||||
"build:bot": "vite-node --script scripts/build",
|
||||
"build:website": "next build",
|
||||
"build:docs": "vitepress build docs",
|
||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||
"build": "node scripts/build --build=all",
|
||||
"dev:bot": "yarn build:bot --dev && yarn start:bot",
|
||||
"dev:server": "yarn build:server --dev && yarn start:server",
|
||||
"build:assets": "vite-node --script scripts/buildAssets",
|
||||
"build:test": "vite-node --script scripts/buildTest",
|
||||
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot",
|
||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||
"dev:docs": "vitepress dev docs",
|
||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
|
||||
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
|
||||
"dev:website": "next dev",
|
||||
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
||||
"start:website": "next start",
|
||||
"host:docs": "vitepress preview docs",
|
||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||
"zip": "zip -r dist.zip dist",
|
||||
"lint": "npx tsc",
|
||||
"lint:website": "next lint",
|
||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
||||
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
|
||||
"knip": "knip",
|
||||
"update": "yarn upgrade --latest"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/plugin-throttling": "^8.1.3",
|
||||
"@octokit/rest": "^20.0.1",
|
||||
"@radix-ui/colors": "^2.1.0",
|
||||
"@prisma/client": "^5.4.1",
|
||||
"@radix-ui/colors": "^3.0.0-rc.4",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/themes": "^2.0.1",
|
||||
"@react-three/drei": "^9.90.1",
|
||||
"@react-three/fiber": "^8.15.12",
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"bepaint": "^0.1.0-beta.3",
|
||||
"decompress": "^4.2.1",
|
||||
"discord.js": "^14.13.0",
|
||||
"esbuild": "^0.19.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"esbuild": "^0.19.3",
|
||||
"express": "^4.18.2",
|
||||
"false": "^0.0.4",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"immer": "^10.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-escape": "^2.0.0",
|
||||
"next": "^14.0.3",
|
||||
"react": "^18.2.0",
|
||||
"satori": "^0.10.3",
|
||||
"vite-node": "^0.34.2",
|
||||
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fuzzysort": "^1.0.1",
|
||||
"satori": "^0.10.8",
|
||||
"sharp": "^0.33.0",
|
||||
"three": "^0.159.0",
|
||||
"vite-node": "^0.34.5",
|
||||
"vitepress": "^1.0.0-rc.20",
|
||||
"vitepress-plugin-auto-sidebar": "^1.1.0",
|
||||
"yaml": "^2.3.4",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@total-typescript/ts-reset": "^0.5.1",
|
||||
"@types/decompress": "^4.2.6",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/js-yaml": "^4.0.8",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/lz4": "^0.6.4",
|
||||
"@types/markdown-escape": "^1.1.0",
|
||||
"@types/node": "^20.5.3",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/node": "^20.6.5",
|
||||
"@types/react": "^18.2.22",
|
||||
"@types/three": "^0.159.0",
|
||||
"@vanilla-extract/next-plugin": "^2.3.1",
|
||||
"compare-versions": "^6.1.0",
|
||||
"knip": "^2.19.9",
|
||||
"prettier": "^3.0.2",
|
||||
"typescript": "^5.0.4",
|
||||
"vitepress": "^1.0.0-rc.4"
|
||||
"crc": "^4.3.2",
|
||||
"fast-xml-parser": "^4.3.2",
|
||||
"knip": "^3.4.0",
|
||||
"lz4": "^0.6.5",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.4.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "discord_blitz" (
|
||||
"discord" INT8 NOT NULL,
|
||||
"region" STRING NOT NULL,
|
||||
"blitz" INT4 NOT NULL,
|
||||
|
||||
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "cockroachdb"
|
||||
@@ -0,0 +1,14 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "cockroachdb"
|
||||
url = env("DISCORD_BLITZ_DB")
|
||||
}
|
||||
|
||||
model discord_blitz {
|
||||
discord BigInt @id
|
||||
region String
|
||||
blitz Int
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 177 KiB |
@@ -1,63 +0,0 @@
|
||||
import { build } from 'esbuild';
|
||||
import { copyFile, mkdirSync, rmSync } from 'fs';
|
||||
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...');
|
||||
rmSync('dist', { recursive: true, force: true });
|
||||
console.log('Dist removed');
|
||||
|
||||
console.log('Creating dist...');
|
||||
mkdirSync('dist');
|
||||
console.log('Dist created');
|
||||
|
||||
console.log('Copying package.json...');
|
||||
copyFile('package.dist.json', 'dist/package.json', () =>
|
||||
console.log('package.json copied'),
|
||||
);
|
||||
}
|
||||
|
||||
const commonOptions = {
|
||||
platform: 'node',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
format: 'cjs',
|
||||
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // causes errors
|
||||
minifySyntax: isProd,
|
||||
minifyWhitespace: isProd,
|
||||
|
||||
logLevel: isProd ? 'info' : 'silent',
|
||||
};
|
||||
|
||||
if (buildBot) {
|
||||
console.log('Building bot...');
|
||||
build({
|
||||
...commonOptions,
|
||||
|
||||
entryPoints: ['src/bot.ts'],
|
||||
outfile: 'dist/bot.cjs',
|
||||
}).then(() => console.log('Bot built'));
|
||||
}
|
||||
|
||||
if (buildServer) {
|
||||
console.log('Building server...');
|
||||
build({
|
||||
...commonOptions,
|
||||
|
||||
entryPoints: ['src/server.ts'],
|
||||
outfile: 'dist/server.cjs',
|
||||
}).then(() => {
|
||||
console.log('Server built');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { build } from 'esbuild';
|
||||
import { cp, readdir } from 'fs/promises';
|
||||
|
||||
readdir('src/workers').then((workers) => {
|
||||
const files = [
|
||||
'bot',
|
||||
'index',
|
||||
|
||||
...workers.map((file) => `workers/${file.replace('.ts', '')}`),
|
||||
];
|
||||
|
||||
build({
|
||||
entryPoints: files.map((file) => `src/${file}.ts`),
|
||||
outdir: 'dist/bot',
|
||||
|
||||
platform: 'node',
|
||||
tsconfig: 'tsconfig.esbuild.json',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
format: 'cjs',
|
||||
jsx: 'transform',
|
||||
outExtension: {
|
||||
'.js': '.cjs',
|
||||
},
|
||||
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // causes goofy errors
|
||||
minifySyntax: true,
|
||||
minifyWhitespace: true,
|
||||
|
||||
logLevel: 'info',
|
||||
});
|
||||
});
|
||||
|
||||
// copy schema.prisma
|
||||
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||
readdir('node_modules/prisma').then((files) =>
|
||||
files
|
||||
.filter((file) => file.endsWith('.node'))
|
||||
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
||||
);
|
||||
@@ -0,0 +1,544 @@
|
||||
import { config } from 'dotenv';
|
||||
import { existsSync } from 'fs';
|
||||
import { mkdir, readdir, rm } from 'fs/promises';
|
||||
import { argv } from 'process';
|
||||
import sharp from 'sharp';
|
||||
import { TankType } from '../src/components/Tanks';
|
||||
import { NATION_IDS } from '../src/constants/nations';
|
||||
import { SCPGStream } from '../src/core/blitz/SCPGStream';
|
||||
import { readBase64DVPL } from '../src/core/blitz/readBase64DVPL';
|
||||
import { readDVPLFile } from '../src/core/blitz/readDVPLFile';
|
||||
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
|
||||
import { readXMLDVPL } from '../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../src/core/blitz/readYAMLDVPL';
|
||||
import { toUniqueId } from '../src/core/blitz/toUniqueId';
|
||||
import commitMultipleFiles, {
|
||||
FileChange,
|
||||
} from '../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { BlitzkriegGunDefinitions } from '../src/core/blitzkrieg/definitions/guns';
|
||||
import {
|
||||
BlitzkriegShellDefinitions,
|
||||
ShellType,
|
||||
} from '../src/core/blitzkrieg/definitions/shells';
|
||||
import {
|
||||
BlitzkriegTankDefinitions,
|
||||
Tier,
|
||||
} from '../src/core/blitzkrieg/definitions/tanks';
|
||||
import { BlitzkriegTurretDefinitions } from '../src/core/blitzkrieg/definitions/turrets';
|
||||
|
||||
config();
|
||||
|
||||
// WARNING! MOST OF THESE TYPES ARE NOT EXHAUSTIVE!
|
||||
|
||||
interface VehicleDefinitionList {
|
||||
[key: string]: {
|
||||
id: number;
|
||||
userString: string;
|
||||
shortUserString?: string;
|
||||
description: string;
|
||||
price: number | { gold: ''; '#text': number };
|
||||
sellPrice?: { gold: ''; '#text': number };
|
||||
enrichmentPermanentCost: number;
|
||||
notInShop?: boolean;
|
||||
tags: string;
|
||||
level: number;
|
||||
combatRole?: Record<string, string>;
|
||||
configurationModes: string;
|
||||
};
|
||||
}
|
||||
interface TurretDefinitionsList {
|
||||
nextAvailableId: number;
|
||||
ids: Record<string, number>;
|
||||
}
|
||||
interface GunDefinitionsList {
|
||||
nextAvailableId: number;
|
||||
ids: Record<string, number>;
|
||||
shared: {
|
||||
[key: string]: {
|
||||
userString: string;
|
||||
tags: string;
|
||||
level: number;
|
||||
pitchLimits: string;
|
||||
burst?: { count: number; rate: number };
|
||||
clip?: { count: number; rate: number };
|
||||
shots: {
|
||||
[key: string]: {
|
||||
speed: number;
|
||||
piercingPower: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
type ShellKind =
|
||||
| 'ARMOR_PIERCING'
|
||||
| 'ARMOR_PIERCING_CR'
|
||||
| 'HIGH_EXPLOSIVE'
|
||||
| 'HOLLOW_CHARGE';
|
||||
type ShellDefinitionsList = Record<
|
||||
string,
|
||||
{
|
||||
id: number;
|
||||
userString: string;
|
||||
icon: string;
|
||||
kind: ShellKind;
|
||||
caliber: number;
|
||||
damage: { armor: number; devices: number };
|
||||
normalizationAngle: number;
|
||||
ricochetAngle: number;
|
||||
}
|
||||
> & {
|
||||
icons: Record<string, string>;
|
||||
};
|
||||
interface VehicleDefinitions {
|
||||
turrets0: {
|
||||
[key: string]: {
|
||||
userString: number;
|
||||
level: number;
|
||||
yawLimits: string | string[];
|
||||
guns: {
|
||||
[key: string]: {
|
||||
maxAmmo: number;
|
||||
pitchLimits?: string | string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
interface Strings {
|
||||
[key: string]: string;
|
||||
}
|
||||
type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
|
||||
interface TankParameters {
|
||||
resourcesPath: {
|
||||
smallIconPath: string;
|
||||
bigIconPath: string;
|
||||
blitzModelPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
const blitzTankTypeToBlitzkrieg: Record<BlitzTankType, TankType> = {
|
||||
'AT-SPG': 'tank_destroyer',
|
||||
lightTank: 'light',
|
||||
mediumTank: 'medium',
|
||||
heavyTank: 'heavy',
|
||||
};
|
||||
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
||||
ARMOR_PIERCING: 'ap',
|
||||
ARMOR_PIERCING_CR: 'ap_cr',
|
||||
HIGH_EXPLOSIVE: 'he',
|
||||
HOLLOW_CHARGE: 'hc',
|
||||
};
|
||||
const DATA =
|
||||
'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
|
||||
const LANGUAGE = 'en';
|
||||
const DOI = {
|
||||
vehicleDefinitions: 'XML/item_defs/vehicles',
|
||||
strings: 'Strings',
|
||||
tankParameters: '3d/Tanks/Parameters',
|
||||
smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons',
|
||||
bigIcons: 'Gfx/UI/BigTankIcons',
|
||||
flags: 'Gfx/Lobby/flags',
|
||||
'3d': '3d',
|
||||
bigShellIcons: 'Gfx/Shared/tank-supply/ammunition/big',
|
||||
};
|
||||
|
||||
const allTargets = argv.includes('--all-targets');
|
||||
const targets = argv
|
||||
.find((argument) => argument.startsWith('--target'))
|
||||
?.split('=')[1]
|
||||
.split(',');
|
||||
|
||||
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||
|
||||
if (allTargets || targets?.includes('definitions')) {
|
||||
console.log('Building tank definitions...');
|
||||
|
||||
const tankDefinitions: BlitzkriegTankDefinitions = {};
|
||||
const turretDefinitions: BlitzkriegTurretDefinitions = {};
|
||||
const gunDefinitions: BlitzkriegGunDefinitions = {};
|
||||
const shellDefinitions: BlitzkriegShellDefinitions = {};
|
||||
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
const strings = await readYAMLDVPL<Strings>(
|
||||
`${DATA}/${DOI.strings}/${LANGUAGE}.yaml.dvpl`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
const turretList = await readXMLDVPL<{
|
||||
root: TurretDefinitionsList;
|
||||
}>(
|
||||
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/turrets.xml.dvpl`,
|
||||
);
|
||||
const gunList = await readXMLDVPL<{
|
||||
root: GunDefinitionsList;
|
||||
}>(
|
||||
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/guns.xml.dvpl`,
|
||||
);
|
||||
const shellList = await readXMLDVPL<{
|
||||
root: ShellDefinitionsList;
|
||||
}>(
|
||||
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/shells.xml.dvpl`,
|
||||
);
|
||||
|
||||
for (const tankKey in tankList.root) {
|
||||
const tank = tankList.root[tankKey];
|
||||
const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>(
|
||||
`${DATA}/${DOI.vehicleDefinitions}/${nation}/${tankKey}.xml.dvpl`,
|
||||
);
|
||||
const tankId = toUniqueId(nation, tank.id);
|
||||
const tankTags = tank.tags.split(' ');
|
||||
const tankTurrets = Object.keys(tankDefinition.root.turrets0).map(
|
||||
(turretKey) => {
|
||||
const turret = tankDefinition.root.turrets0[turretKey];
|
||||
const turretId = toUniqueId(nation, turretList.root.ids[turretKey]);
|
||||
const turretGuns = Object.keys(turret.guns).map((gunKey) => {
|
||||
const turretGunEntry = turret.guns[gunKey];
|
||||
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
|
||||
const gun = gunList.root.shared[gunKey];
|
||||
const pitchLimitsRaw =
|
||||
turretGunEntry.pitchLimits ?? gun.pitchLimits;
|
||||
const gunShells = Object.keys(gun.shots).map((shellKey) => {
|
||||
const turretShellEntry = gun.shots[shellKey];
|
||||
const shell = shellList.root[shellKey];
|
||||
const shellId = toUniqueId(nation, shell.id);
|
||||
|
||||
shellDefinitions[shellId] = {
|
||||
id: shellId,
|
||||
name:
|
||||
strings[shell.userString] ?? shellKey.replaceAll('_', ' '),
|
||||
speed: turretShellEntry.speed,
|
||||
damage: {
|
||||
armor: shell.damage.armor,
|
||||
devices: shell.damage.devices,
|
||||
},
|
||||
caliber: shell.caliber,
|
||||
normalization: shell.normalizationAngle,
|
||||
ricochet: shell.ricochetAngle,
|
||||
type: blitzShellKindToBLitzkrieg[shell.kind],
|
||||
icon: shell.icon,
|
||||
};
|
||||
|
||||
return shellId;
|
||||
});
|
||||
|
||||
gunDefinitions[gunId] = {
|
||||
id: gunId,
|
||||
pitch: (typeof pitchLimitsRaw === 'string'
|
||||
? pitchLimitsRaw
|
||||
: pitchLimitsRaw.at(-1)!
|
||||
)
|
||||
.split(' ')
|
||||
.map(Number) as [number, number],
|
||||
name: strings[gun.userString] ?? gunKey.replaceAll('_', ' '),
|
||||
tier: gun.level as Tier,
|
||||
shells: gunShells,
|
||||
};
|
||||
|
||||
return gunId;
|
||||
});
|
||||
|
||||
turretDefinitions[turretId] = {
|
||||
id: turretId,
|
||||
name:
|
||||
strings[turret.userString] ??
|
||||
turretKey
|
||||
.replaceAll('_', ' ')
|
||||
.replace(/^(Turret ([0-9] )?)+/, ''),
|
||||
tier: turret.level as Tier,
|
||||
yaw: (typeof turret.yawLimits === 'string'
|
||||
? turret.yawLimits
|
||||
: turret.yawLimits.at(-1)!
|
||||
)
|
||||
.split(' ')
|
||||
.map(Number) as [number, number],
|
||||
guns: turretGuns,
|
||||
};
|
||||
|
||||
return turretId;
|
||||
},
|
||||
);
|
||||
|
||||
tankDefinitions[tankId] = {
|
||||
id: tankId,
|
||||
name:
|
||||
(tank.shortUserString
|
||||
? strings[tank.shortUserString]
|
||||
: undefined) ??
|
||||
strings[tank.userString] ??
|
||||
tankKey.replaceAll('_', ' '),
|
||||
nation,
|
||||
tree_type: (tank.sellPrice ? 'gold' in tank.sellPrice : false)
|
||||
? 'collector'
|
||||
: (typeof tank.price === 'number' ? false : 'gold' in tank.price)
|
||||
? 'premium'
|
||||
: 'researchable',
|
||||
tier: tank.level as Tier,
|
||||
type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType],
|
||||
testing: tankTags.includes('testTank') ? true : undefined,
|
||||
turrets: tankTurrets,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
console.log('Committing tank definitions...');
|
||||
await commitMultipleFiles(
|
||||
'tresabhi',
|
||||
'blitzkrieg-assets',
|
||||
'main',
|
||||
'definitions',
|
||||
[
|
||||
{
|
||||
content: JSON.stringify(tankDefinitions),
|
||||
encoding: 'utf-8',
|
||||
path: 'definitions/tanks.json',
|
||||
},
|
||||
{
|
||||
content: JSON.stringify(turretDefinitions),
|
||||
encoding: 'utf-8',
|
||||
path: 'definitions/turrets.json',
|
||||
},
|
||||
{
|
||||
content: JSON.stringify(gunDefinitions),
|
||||
encoding: 'utf-8',
|
||||
path: 'definitions/guns.json',
|
||||
},
|
||||
{
|
||||
content: JSON.stringify(shellDefinitions),
|
||||
encoding: 'utf-8',
|
||||
path: 'definitions/shells.json',
|
||||
},
|
||||
],
|
||||
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
allTargets ||
|
||||
targets?.includes('bigTankIcons') ||
|
||||
targets?.includes('smallTankIcons')
|
||||
) {
|
||||
console.log('Building tank icons...');
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
for (const tankIndex in tanks.root) {
|
||||
const tank = tanks.root[tankIndex];
|
||||
const nationVehicleId = tank.id;
|
||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`,
|
||||
);
|
||||
const small = `${DATA}/${parameters.resourcesPath.smallIconPath
|
||||
.replace(/~res:\//, '')
|
||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||
const big = `${DATA}/${parameters.resourcesPath.bigIconPath
|
||||
.replace(/~res:\//, '')
|
||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||
|
||||
if (allTargets || targets?.includes('bigTankIcons')) {
|
||||
changes.push({
|
||||
content: await readBase64DVPL(big),
|
||||
encoding: 'base64',
|
||||
path: `icons/big/${id}.webp`,
|
||||
});
|
||||
}
|
||||
if (allTargets || targets?.includes('smallTankIcons')) {
|
||||
changes.push({
|
||||
content: await readBase64DVPL(small),
|
||||
encoding: 'base64',
|
||||
path: `icons/small/${id}.webp`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
console.log('Committing tank icons...');
|
||||
await commitMultipleFiles(
|
||||
'tresabhi',
|
||||
'blitzkrieg-assets',
|
||||
'main',
|
||||
'tank icons',
|
||||
changes,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (allTargets || targets?.includes('scratchedFlags')) {
|
||||
console.log('Building scratched flags...');
|
||||
|
||||
const flags = await readdir(`${DATA}/${DOI.flags}`);
|
||||
|
||||
console.log('Committing scratched flags...');
|
||||
commitMultipleFiles(
|
||||
'tresabhi',
|
||||
'blitzkrieg-assets',
|
||||
'main',
|
||||
'scratched flags',
|
||||
await Promise.all(
|
||||
flags
|
||||
.filter(
|
||||
(flag) =>
|
||||
flag.startsWith('flag_tutor-tank_') &&
|
||||
!flag.endsWith('@2x.packed.webp.dvpl'),
|
||||
)
|
||||
.map(async (flag) => {
|
||||
const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`);
|
||||
const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `flags/scratched/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (allTargets || targets?.includes('circleFlags')) {
|
||||
const flags = await readdir(`${DATA}/${DOI.flags}`);
|
||||
|
||||
commitMultipleFiles(
|
||||
'tresabhi',
|
||||
'blitzkrieg-assets',
|
||||
'main',
|
||||
'circle flags',
|
||||
await Promise.all(
|
||||
flags
|
||||
.filter(
|
||||
(flag) =>
|
||||
flag.startsWith('flag_profile-stat_') &&
|
||||
!flag.endsWith('@2x.packed.webp.dvpl'),
|
||||
)
|
||||
.map(async (flag) => {
|
||||
const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`);
|
||||
const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1];
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `flags/circle/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (allTargets || targets?.includes('tankModels')) {
|
||||
console.log('Building tank models...');
|
||||
|
||||
if (existsSync('dist/assets/models')) {
|
||||
await rm('dist/assets/models', { recursive: true });
|
||||
}
|
||||
await mkdir('dist/assets/models', { recursive: true });
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
for (const tankIndex in tanks.root) {
|
||||
const tank = tanks.root[tankIndex];
|
||||
const nationVehicleId = tank.id;
|
||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
|
||||
// if (id !== 897) continue;
|
||||
console.log(`Building model ${id} @ ${nation}/${tankIndex}`);
|
||||
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`,
|
||||
);
|
||||
const sc2Path = parameters.resourcesPath.blitzModelPath;
|
||||
const scgPath = parameters.resourcesPath.blitzModelPath.replace(
|
||||
/\.sc2$/,
|
||||
'.scg',
|
||||
);
|
||||
const sc2Stream = await SCPGStream.fromDVPLFile(
|
||||
`${DATA}/${DOI['3d']}/${sc2Path}.dvpl`,
|
||||
);
|
||||
const scgStream = await SCPGStream.fromDVPLFile(
|
||||
`${DATA}/${DOI['3d']}/${scgPath}.dvpl`,
|
||||
);
|
||||
|
||||
const sc2 = sc2Stream.consumeSC2();
|
||||
const scg = scgStream.consumeSCG();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (allTargets || targets?.includes('shellIcons')) {
|
||||
console.log('Building shell icons...');
|
||||
|
||||
const image = sharp(
|
||||
await readDVPLFile(
|
||||
`${DATA}/${DOI.bigShellIcons}/texture0.packed.webp.dvpl`,
|
||||
),
|
||||
);
|
||||
|
||||
const changes = await Promise.all(
|
||||
(await readdir(`${DATA}/${DOI.bigShellIcons}`))
|
||||
.filter((file) => file.endsWith('_l.txt.dvpl'))
|
||||
.map(async (file) => {
|
||||
const name = file.match(/(.+)_l\.txt\.dvpl/)![1];
|
||||
const sizes = (
|
||||
await readStringDVPL(`${DATA}/${DOI.bigShellIcons}/${file}`)
|
||||
)
|
||||
.split('\n')[4]
|
||||
.split(' ')
|
||||
.map(Number);
|
||||
|
||||
return {
|
||||
content: (
|
||||
await image
|
||||
.clone()
|
||||
.extract({
|
||||
left: sizes[0],
|
||||
top: sizes[1],
|
||||
width: sizes[2],
|
||||
height: sizes[3],
|
||||
})
|
||||
.toBuffer()
|
||||
).toString('base64'),
|
||||
encoding: 'base64',
|
||||
path: `shells/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
);
|
||||
|
||||
console.log('Committing shell icons...');
|
||||
commitMultipleFiles(
|
||||
'tresabhi',
|
||||
'blitzkrieg-assets',
|
||||
'main',
|
||||
'shell icons',
|
||||
changes,
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -6,41 +6,48 @@ import {
|
||||
RatingsNeighbors,
|
||||
RatingsPlayer,
|
||||
} from '../src/commands/ratings';
|
||||
import commitMultipleFiles from './commitMultipleFiles.js';
|
||||
import commitMultipleFiles from '../src/core/blitzkrieg/commitMultipleFiles.js';
|
||||
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
||||
|
||||
/*
|
||||
* 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
|
||||
* [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
|
||||
* [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||
* [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
|
||||
*/
|
||||
|
||||
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
|
||||
export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
|
||||
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
|
||||
|
||||
const publish = argv.includes('--publish');
|
||||
const latest = argv.includes('--latest');
|
||||
const target = argv
|
||||
.find((arg) => arg.startsWith('--target='))
|
||||
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
||||
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
||||
|
||||
if (!target) throw new Error('Target parameter not specified');
|
||||
if (!server) throw new Error('Region parameter not specified');
|
||||
|
||||
const NEIGHBORS = 1000;
|
||||
const PLAYERS = await fetch(
|
||||
const NEIGHBORS = 2 ** 8;
|
||||
const PLAYERS = await patientFetchJSON<RatingsInfo>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
)
|
||||
.then((response) => response.json() as Promise<RatingsInfo>)
|
||||
.then((data) => data.count);
|
||||
).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(
|
||||
const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
|
||||
`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];
|
||||
@@ -60,7 +67,7 @@ async function branchFromPlayer(id: number) {
|
||||
|
||||
console.log(
|
||||
`Registered ${registered} / ${PLAYERS} (${(
|
||||
(registered / PLAYERS) *
|
||||
(registered / PLAYERS!) *
|
||||
100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
@@ -69,9 +76,9 @@ async function branchFromPlayer(id: number) {
|
||||
}
|
||||
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await fetch(
|
||||
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
|
||||
);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result[result.length - 1];
|
||||
@@ -94,7 +101,10 @@ await Promise.all([
|
||||
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`);
|
||||
if (!listing) {
|
||||
console.warn(`Position ${index + 1} had no player`);
|
||||
continue;
|
||||
}
|
||||
|
||||
leaderboard[index] = {
|
||||
id: listing.spa_id,
|
||||
@@ -106,28 +116,23 @@ const leaderboardJSON = JSON.stringify(leaderboard);
|
||||
|
||||
if (publish) {
|
||||
const octokit = new Octokit({ auth: env.GH_TOKEN });
|
||||
|
||||
const info = await fetch(
|
||||
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).then((response) => response.json() as Promise<RatingsInfo>);
|
||||
);
|
||||
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 infoPath = `regions/${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||
const leaderboardPath = `regions/${normalizedServer}/ratings/${info.current_season}/${target}.json`;
|
||||
const infoJSON = JSON.stringify(info);
|
||||
|
||||
console.log(`Publishing to season ${info.current_season}...`);
|
||||
commitMultipleFiles(
|
||||
octokit,
|
||||
'tresabhi',
|
||||
'blitzkrieg-db',
|
||||
'blitzkrieg-assets',
|
||||
'main',
|
||||
new Date().toString(),
|
||||
[
|
||||
{ path: infoPath, content: infoJSON },
|
||||
{ path: leaderboardPath, content: leaderboardJSON },
|
||||
{ path: infoPath, content: infoJSON, encoding: 'utf-8' },
|
||||
{ path: leaderboardPath, content: leaderboardJSON, encoding: 'utf-8' },
|
||||
],
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { Region } from '../src/constants/regions';
|
||||
import { getRatingsNeighbors } from '../src/core/blitz/getRatingsNeighbors';
|
||||
|
||||
const REGION: Region = 'com';
|
||||
const PLAYER = 1041988373;
|
||||
const ITERATIONS = 2 ** 4;
|
||||
const MAX_NEIGHBORS = 2 ** 7;
|
||||
const data: [number, number][] = [];
|
||||
let finished = 0;
|
||||
|
||||
for (let index = 0; index < ITERATIONS; index++) {
|
||||
const neighbors = Math.round((Math.random() * MAX_NEIGHBORS) / 2) * 2;
|
||||
const start = Date.now();
|
||||
|
||||
console.log(`🟡 Iteration ${index} with ${neighbors} neighbors`);
|
||||
|
||||
try {
|
||||
await getRatingsNeighbors(REGION, PLAYER, neighbors);
|
||||
|
||||
finished++;
|
||||
console.log(
|
||||
`🟢 (${Math.round(
|
||||
100 * (finished / ITERATIONS),
|
||||
)}%) Iteration ${index} with ${neighbors} neighbors`,
|
||||
);
|
||||
|
||||
const end = Date.now();
|
||||
const time = end - start;
|
||||
|
||||
data.push([neighbors, time]);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`🔴 Iteration ${index} with ${neighbors} neighbors\n\n${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🟢 Done, sorting');
|
||||
data.sort(([neighborsA], [neighborsB]) => neighborsA - neighborsB);
|
||||
|
||||
console.log('🟢 Writing');
|
||||
writeFile(
|
||||
'dist/ratingsPlot.csv',
|
||||
data.map(([neighbors, time]) => `${neighbors},${time}`).join('\n'),
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import packageJSON from '../package.json';
|
||||
|
||||
const { version } = packageJSON;
|
||||
const changelogs = await readdir('docs/changelogs');
|
||||
const hasVersion = changelogs.includes(`${version}.md`);
|
||||
|
||||
if (hasVersion) {
|
||||
console.log(`Version ${version} found in changelogs`);
|
||||
} else {
|
||||
throw new Error(`Version ${version} not found in changelogs`);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../constants/regions';
|
||||
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region } },
|
||||
) {
|
||||
return NextResponse.json(await getRatingsInfo(params.region));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../../constants/regions';
|
||||
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region; league: number } },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
await getRatingsLeague(params.region, params.league, true),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../../constants/regions';
|
||||
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region; username: string } },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
await searchCurrentRatingsPlayers(params.region, params.username),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(await tankAverages);
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
'use client';
|
||||
|
||||
import { ContextMenu } from '@radix-ui/themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as Breakdown from '../../../components/Breakdown';
|
||||
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||
import {
|
||||
BlitzkriegTankDefinition,
|
||||
tankDefinitions,
|
||||
} from '../../../core/blitzkrieg/definitions/tanks';
|
||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||
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;
|
||||
tankDefinitions?: BlitzkriegTankDefinition;
|
||||
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 awaitedTankDefinitions = await tankDefinitions;
|
||||
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,
|
||||
tankDefinitions: awaitedTankDefinitions[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
|
||||
color={session.color}
|
||||
minimized={!session.showCareer}
|
||||
title={session.title}
|
||||
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, tankDefinitions, 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
|
||||
color={session.color}
|
||||
key={stats.tank_id}
|
||||
minimized={!session.showCareer}
|
||||
title={
|
||||
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
|
||||
}
|
||||
type="tank"
|
||||
tankType={tankDefinitions?.type}
|
||||
treeType={(() => {
|
||||
if (tankDefinitions?.tree_type === 'collector')
|
||||
return TreeTypeEnum.Collector;
|
||||
if (tankDefinitions?.tree_type === '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,59 @@
|
||||
'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,
|
||||
paddingTop: isEmbed ? 0 : '3.25rem',
|
||||
background: isEmbed
|
||||
? 'transparent'
|
||||
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||
}}
|
||||
>
|
||||
<Theme
|
||||
appearance="dark"
|
||||
panelBackground="translucent"
|
||||
radius="full"
|
||||
suppressHydrationWarning
|
||||
suppressContentEditableWarning
|
||||
>
|
||||
{!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(2);
|
||||
const tool = TOOLS.find((tool) => tool.id === toolId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
height: 128,
|
||||
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundImage: `url(/assets/banners/${toolId}.webp)`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: -1,
|
||||
filter: 'blur(4px)',
|
||||
transform: 'scale(calc((128 + 8) / 128))',
|
||||
}}
|
||||
/>
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 32,
|
||||
}}
|
||||
>
|
||||
{tool?.title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{tool?.description}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
const spinKeyframes = keyframes({
|
||||
from: { transform: 'rotate(0deg)' },
|
||||
to: { transform: 'rotate(360deg)' },
|
||||
});
|
||||
|
||||
export const infiniteSpin = style({
|
||||
animationName: spinKeyframes,
|
||||
animationIterationCount: 'infinite',
|
||||
animationTimingFunction: 'linear',
|
||||
animationDuration: '1s',
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
CaretLeftIcon,
|
||||
CaretRightIcon,
|
||||
SymbolIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Button, Flex, TextField } from '@radix-ui/themes';
|
||||
import { range } from 'lodash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { infiniteSpin } from './index.css';
|
||||
|
||||
interface PageTurnerProps {
|
||||
leaderboard: Record<number, number>;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
pages: number;
|
||||
onPageChange: (page: number, isButtonClick: boolean) => void;
|
||||
totalPlayers: number;
|
||||
}
|
||||
|
||||
export function PageTurner({
|
||||
page,
|
||||
rowsPerPage,
|
||||
onPageChange,
|
||||
pages,
|
||||
leaderboard,
|
||||
totalPlayers,
|
||||
}: PageTurnerProps) {
|
||||
const pageInput = useRef<HTMLInputElement>(null);
|
||||
const rightLoading = range(
|
||||
page * rowsPerPage,
|
||||
Math.min(totalPlayers - 1, page * rowsPerPage + rowsPerPage + 1),
|
||||
).some((index) => !(index in leaderboard));
|
||||
const leftLoading = range(
|
||||
Math.max(0, page * rowsPerPage - 1),
|
||||
Math.max(0, page * rowsPerPage - rowsPerPage - 1) + 1,
|
||||
).some((index) => !(index in leaderboard));
|
||||
|
||||
useEffect(() => {
|
||||
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<Flex justify="center" wrap="wrap" gap="2">
|
||||
<Flex gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => onPageChange(Math.max(page - 1, 0), true)}
|
||||
disabled={page === 0 || leftLoading}
|
||||
>
|
||||
{leftLoading ? (
|
||||
<SymbolIcon className={infiniteSpin} />
|
||||
) : (
|
||||
<CaretLeftIcon />
|
||||
)}
|
||||
</Button>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>Page</TextField.Slot>
|
||||
<TextField.Input
|
||||
type="number"
|
||||
ref={pageInput}
|
||||
style={{ width: 64, textAlign: 'center' }}
|
||||
min={1}
|
||||
max={pages}
|
||||
onBlur={(event) => {
|
||||
onPageChange(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(pages - 1, event.target.valueAsNumber - 1),
|
||||
),
|
||||
false,
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField.Slot>out of {pages}</TextField.Slot>
|
||||
</TextField.Root>
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => {
|
||||
onPageChange(Math.min(page + 1, pages - 1), true);
|
||||
}}
|
||||
disabled={page === pages - 1 || rightLoading}
|
||||
>
|
||||
{rightLoading ? (
|
||||
<SymbolIcon className={infiniteSpin} />
|
||||
) : (
|
||||
<CaretRightIcon />
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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.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,136 @@
|
||||
import {
|
||||
Button,
|
||||
ContextMenu,
|
||||
Dialog,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
Heading,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { createColors } from 'bepaint';
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
ACCENT_COLORS,
|
||||
AccentColor,
|
||||
GRAY_COLORS,
|
||||
GrayColor,
|
||||
PALETTES,
|
||||
} from '../../../../constants/radixColors';
|
||||
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();
|
||||
const [showTitleEditor, setShowTitleEditor] = useState(false);
|
||||
const titleInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Root open={showTitleEditor} onOpenChange={setShowTitleEditor}>
|
||||
<Dialog.Content>
|
||||
<Flex direction="column" gap="4">
|
||||
<Heading size="4">Edit title</Heading>
|
||||
|
||||
<Flex gap="2" style={{ width: '100%' }}>
|
||||
<TextField.Root style={{ flex: 1 }}>
|
||||
<TextField.Input
|
||||
ref={titleInput}
|
||||
defaultValue={session.title}
|
||||
placeholder="Enter title..."
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && titleInput.current) {
|
||||
setShowTitleEditor(false);
|
||||
useSession.setState({ title: titleInput.current!.value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
onClick={() =>
|
||||
useSession.setState({ title: titleInput.current!.value })
|
||||
}
|
||||
>
|
||||
Set
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Builder.Content>
|
||||
<Builder.Sub>
|
||||
<Builder.SubTrigger>Accent color</Builder.SubTrigger>
|
||||
|
||||
<Builder.SubContent>
|
||||
<Builder.RadioGroup
|
||||
value={session.color}
|
||||
onValueChange={(value) =>
|
||||
useSession.setState({ color: value as AccentColor | GrayColor })
|
||||
}
|
||||
>
|
||||
{[...ACCENT_COLORS, ...GRAY_COLORS].map((color) => (
|
||||
<Builder.RadioItem value={color} key={color}>
|
||||
<Flex gap="2" align="center">
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: createColors(PALETTES[color])
|
||||
.solidBackground,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
/>
|
||||
{`${color.charAt(0).toUpperCase()}${color.slice(1)}`}
|
||||
</Flex>
|
||||
</Builder.RadioItem>
|
||||
))}
|
||||
</Builder.RadioGroup>
|
||||
</Builder.SubContent>
|
||||
</Builder.Sub>
|
||||
|
||||
<Builder.Separator />
|
||||
|
||||
<Builder.Item onClick={() => setShowTitleEditor(true)}>
|
||||
Edit title...
|
||||
</Builder.Item>
|
||||
<CustomColumn Builder={Builder} column={0} />
|
||||
<CustomColumn Builder={Builder} column={1} />
|
||||
<CustomColumn Builder={Builder} column={2} />
|
||||
<CustomColumn Builder={Builder} column={3} />
|
||||
|
||||
<Builder.Separator />
|
||||
|
||||
<Builder.CheckboxItem
|
||||
checked={session.showTotal}
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({ showTotal: checked })
|
||||
}
|
||||
>
|
||||
Show total
|
||||
</Builder.CheckboxItem>
|
||||
<Builder.CheckboxItem
|
||||
checked={session.showCareer}
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({ showCareer: checked })
|
||||
}
|
||||
>
|
||||
Show career
|
||||
</Builder.CheckboxItem>
|
||||
|
||||
<Builder.Separator />
|
||||
|
||||
<Builder.Item color="red" onClick={reset}>
|
||||
Reset
|
||||
</Builder.Item>
|
||||
</Builder.Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export const toolBar = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 640px)': {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
},
|
||||
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
});
|
||||
|
||||
export const toolbarButton = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 640px)': {
|
||||
flex: 0,
|
||||
},
|
||||
},
|
||||
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const searchButton = style({
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
fontSize: 16,
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: theme.colors.componentInteractiveHover,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,254 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CopyIcon,
|
||||
PersonIcon,
|
||||
ReloadIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import {
|
||||
AlertDialog,
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash';
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { REGION_NAMES } from '../../../constants/regions';
|
||||
import searchPlayersAcrossRegions, {
|
||||
AccountListWithServer,
|
||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||
import { resetSession, setSession, useSession } from '../../../stores/session';
|
||||
import SessionPage from '../../embeds/session/page';
|
||||
import { Menu } from './components/Menu';
|
||||
import * as styles from './page.css';
|
||||
|
||||
export default function Page() {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
AccountListWithServer | undefined
|
||||
>(undefined);
|
||||
const [showSearchResults, setShowSearchResults] = useState(false);
|
||||
const handleChange = debounce(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.value) {
|
||||
setSearchResults(await searchPlayersAcrossRegions(event.target.value));
|
||||
} else {
|
||||
setSearchResults(undefined);
|
||||
setShowSearchResults(false);
|
||||
}
|
||||
},
|
||||
500,
|
||||
);
|
||||
const session = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (session.isTracking) input.current!.value = session.nickname;
|
||||
}, [session]);
|
||||
|
||||
return (
|
||||
<PageWrapper color="blue">
|
||||
<div className={styles.toolBar}>
|
||||
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<PersonIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
|
||||
<TextField.Input
|
||||
defaultValue={session.isTracking ? session.nickname : undefined}
|
||||
ref={input}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.target.value) {
|
||||
setShowSearchResults(true);
|
||||
setSearchResults(undefined);
|
||||
} else {
|
||||
setShowSearchResults(false);
|
||||
}
|
||||
|
||||
handleChange(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 is fixed
|
||||
if (showSearchResults) event.target.focus();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setShowSearchResults(false);
|
||||
input.current?.blur();
|
||||
}
|
||||
}}
|
||||
placeholder="Search for a player..."
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<DropdownMenu.Root open={showSearchResults} modal={false}>
|
||||
<DropdownMenu.Trigger>
|
||||
<div />
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{searchResults === undefined ? (
|
||||
<DropdownMenu.Item disabled>Searching...</DropdownMenu.Item>
|
||||
) : searchResults.length === 0 ? (
|
||||
<DropdownMenu.Item disabled>No results</DropdownMenu.Item>
|
||||
) : (
|
||||
searchResults?.map(
|
||||
({ account_id: id, nickname, region }, index) => (
|
||||
<DropdownMenu.Item
|
||||
key={id}
|
||||
onClick={() => {
|
||||
setShowSearchResults(false);
|
||||
setSession(region, id, nickname);
|
||||
}}
|
||||
shortcut={REGION_NAMES[region]}
|
||||
>
|
||||
{nickname}
|
||||
</DropdownMenu.Item>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger>
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={(event) => {
|
||||
if (!session.resetPrompt) {
|
||||
event.preventDefault();
|
||||
resetSession();
|
||||
}
|
||||
}}
|
||||
className={styles.toolbarButton}
|
||||
color="red"
|
||||
>
|
||||
<ReloadIcon width="16" height="16" /> Reset
|
||||
</Button>
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Content>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex direction="column" gap="0">
|
||||
<AlertDialog.Title>
|
||||
Are you sure you want to reset?
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This'll discard your current session and start tracking from
|
||||
scratch again.
|
||||
</AlertDialog.Description>
|
||||
</Flex>
|
||||
|
||||
<Text size="2">
|
||||
<Checkbox
|
||||
mr="1"
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({
|
||||
resetPrompt: !checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
Don't ask me again
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" mt="4">
|
||||
<AlertDialog.Cancel>
|
||||
<Button variant="soft" color="gray">
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialog.Cancel>
|
||||
|
||||
<AlertDialog.Action>
|
||||
<Button variant="solid" color="red" onClick={resetSession}>
|
||||
Reset
|
||||
</Button>
|
||||
</AlertDialog.Action>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger>
|
||||
<Button
|
||||
variant="soft"
|
||||
className={styles.toolbarButton}
|
||||
onClick={(event) => {
|
||||
if (!session.embedPrompt) {
|
||||
event.preventDefault();
|
||||
resetSession();
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(
|
||||
`${location.origin}/embeds/session`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon width="16" height="16" /> Embed
|
||||
</Button>
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Title>
|
||||
Copied link to your clipboard
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Instructions for streamers:
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16 / 9',
|
||||
border: 'none',
|
||||
}}
|
||||
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
</AlertDialog.Description>
|
||||
|
||||
<Text size="2">
|
||||
<Checkbox
|
||||
mr="1"
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({
|
||||
embedPrompt: !checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
Don't show this again
|
||||
</Text>
|
||||
|
||||
<Flex gap="3" mt="4">
|
||||
<AlertDialog.Cancel>
|
||||
<Button variant="soft">Got it!</Button>
|
||||
</AlertDialog.Cancel>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="soft" className={styles.toolbarButton}>
|
||||
Options
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<Menu Builder={DropdownMenu} reset={resetSession} />
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SessionPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
Flex,
|
||||
Heading,
|
||||
Slider,
|
||||
Tabs,
|
||||
Text,
|
||||
Theme,
|
||||
Tooltip,
|
||||
} from '@radix-ui/themes';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import Link from 'next/link';
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { Flag } from '../../../../components/Flag';
|
||||
import PageWrapper from '../../../../components/PageWrapper';
|
||||
import { asset } from '../../../../core/blitzkrieg/asset';
|
||||
import { gunDefinitions } from '../../../../core/blitzkrieg/definitions/guns';
|
||||
import { shellDefinitions } from '../../../../core/blitzkrieg/definitions/shells';
|
||||
import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
tankDefinitions,
|
||||
tanksDefinitionsArray,
|
||||
} from '../../../../core/blitzkrieg/definitions/tanks';
|
||||
import { turretDefinitions } from '../../../../core/blitzkrieg/definitions/turrets';
|
||||
import * as styles from '../page.css';
|
||||
|
||||
type Mode = 'model' | 'armor';
|
||||
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const id = parseInt(params.id);
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
|
||||
const awaitedTurretDefinitions = use(turretDefinitions);
|
||||
const awaitedGunDefinitions = use(gunDefinitions);
|
||||
const awaitedShellDefinitions = use(shellDefinitions);
|
||||
const tank = awaitedTankDefinitions[id];
|
||||
const [turret, setTurret] = useState(
|
||||
awaitedTurretDefinitions[tank.turrets[0]],
|
||||
);
|
||||
const [gun, setGun] = useState(awaitedGunDefinitions[turret.guns[0]]);
|
||||
const [crew, setCrew] = useState(100);
|
||||
const [versusTank, setVersusTank] = useState(
|
||||
awaitedTanksDefinitionsArray[0].id,
|
||||
);
|
||||
const [mode, setMode] = useState<Mode>('model');
|
||||
|
||||
console.log(versusTank);
|
||||
|
||||
useEffect(() => setGun(awaitedGunDefinitions[turret.guns[0]]), [turret]);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Flex gap="8" direction="column">
|
||||
<Flex gap="4" direction="column">
|
||||
<Flex justify="between" align="center">
|
||||
<Link
|
||||
href="/tools/tankopedia"
|
||||
style={{ color: 'unset', textDecoration: 'none' }}
|
||||
>
|
||||
<Flex gap="1" align="center">
|
||||
<ArrowLeftIcon />
|
||||
<Text>Back to tankopedia</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
<Flag nation={tank.nation} />
|
||||
<Heading>{tank.name}</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Card style={{ position: 'relative' }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Tabs.Root
|
||||
value={mode}
|
||||
onValueChange={(value) => setMode(value as Mode)}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="model">Model</Tabs.Trigger>
|
||||
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
|
||||
<div style={{ height: '50vh', maxHeight: 576 }}>
|
||||
<Canvas onPointerDown={(event) => event.preventDefault()}>
|
||||
<OrbitControls />
|
||||
|
||||
<ambientLight />
|
||||
<directionalLight />
|
||||
|
||||
<mesh>
|
||||
<torusKnotGeometry args={[undefined, undefined, 128, 16]} />
|
||||
<meshStandardMaterial color="cyan" />
|
||||
</mesh>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
{mode === 'armor' && (
|
||||
<Flex gap="2" align="center" className={styles.enhancedArmor}>
|
||||
<Checkbox defaultChecked />
|
||||
<Text>Enhanced armor</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
{mode === 'armor' && (
|
||||
<Card>
|
||||
<Flex align="center" justify="between" gap="2">
|
||||
<Flex align="center" gap="4">
|
||||
<Text>Versus</Text>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="ghost">
|
||||
{awaitedTankDefinitions[versusTank].name}
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>Do something lol</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
<Checkbox defaultChecked />
|
||||
<Text>Calibrated shells</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="1">
|
||||
<Button variant="solid" radius="small">
|
||||
<img src={asset('shells/ap.webp')} width={24} height={24} />
|
||||
</Button>
|
||||
<Button variant="soft" radius="small" color="gray">
|
||||
<img
|
||||
src={asset('shells/hc_premium.webp')}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
<Button variant="soft" radius="small" color="gray">
|
||||
<img src={asset('shells/he.webp')} width={24} height={24} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex gap="6" direction="column">
|
||||
<Heading>Build</Heading>
|
||||
|
||||
<Flex gap="2" direction="column">
|
||||
<Heading size="5" weight="regular">
|
||||
Modules
|
||||
</Heading>
|
||||
<Theme radius="small">
|
||||
<Flex gap="4">
|
||||
<Flex gap="1">
|
||||
{tank.turrets.map((turretId) => {
|
||||
const thisTurret = awaitedTurretDefinitions[turretId];
|
||||
|
||||
return (
|
||||
<Tooltip content={thisTurret.name}>
|
||||
<Button
|
||||
onClick={() => setTurret(thisTurret)}
|
||||
variant={
|
||||
turret.id === thisTurret.id ? 'solid' : 'soft'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="/images/modules/turret.png"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{TIER_ROMAN_NUMERALS[thisTurret.tier]}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
<Flex gap="1">
|
||||
{turret.guns.map((gunId, index) => {
|
||||
const thisGun = awaitedGunDefinitions[gunId];
|
||||
|
||||
return (
|
||||
<Tooltip content={thisGun.name}>
|
||||
<Button
|
||||
onClick={() => setGun(thisGun)}
|
||||
variant={gun.id === thisGun.id ? 'solid' : 'soft'}
|
||||
>
|
||||
<img
|
||||
src="/images/modules/gun.png"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{TIER_ROMAN_NUMERALS[thisGun.tier]}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Theme>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" direction="column">
|
||||
<Heading size="5" weight="regular">
|
||||
Crew
|
||||
</Heading>
|
||||
|
||||
<Flex gap="4" align="center">
|
||||
<Text>{crew}%</Text>
|
||||
|
||||
<Slider
|
||||
style={{ flex: 1 }}
|
||||
value={[crew]}
|
||||
min={50}
|
||||
max={100}
|
||||
onValueChange={([crew]) => setCrew(crew)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" direction="column">
|
||||
<Heading size="5">Shells</Heading>
|
||||
|
||||
{gun.shells.map((shellId) => {
|
||||
const shell = awaitedShellDefinitions[shellId];
|
||||
|
||||
return (
|
||||
<Text>
|
||||
{shell.name} ({shell.type}): {shell.damage.armor}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const splitFilters = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 513px)': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const oneLineFilters = style({
|
||||
'@media': {
|
||||
'screen and (max-width: 512px)': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,388 @@
|
||||
'use client';
|
||||
|
||||
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
||||
import { use } from 'react';
|
||||
import { Flag } from '../../../../../components/Flag';
|
||||
import {
|
||||
TANK_TYPES,
|
||||
TREE_TYPES,
|
||||
TREE_TYPE_ICONS,
|
||||
TREE_TYPE_IMAGES,
|
||||
TreeTypeEnum,
|
||||
} from '../../../../../components/Tanks';
|
||||
import {
|
||||
NATIONS,
|
||||
TIERS,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '../../../../../core/blitzkrieg/definitions/tanks';
|
||||
import mutateTankopedia, {
|
||||
TankopediaTestTankDisplay,
|
||||
useTankopedia,
|
||||
} from '../../../../../stores/tankopedia';
|
||||
import * as styles from './index.css';
|
||||
|
||||
export function Options() {
|
||||
const nations = use(NATIONS);
|
||||
const tankopediaState = useTankopedia();
|
||||
|
||||
return (
|
||||
<Flex justify="center" direction="column" gap="2">
|
||||
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() =>
|
||||
useTankopedia.setState({
|
||||
filters: {
|
||||
nations: [],
|
||||
tiers: [],
|
||||
treeTypes: [],
|
||||
types: [],
|
||||
test: 'include',
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="soft">
|
||||
Test tanks
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaState.filters.test}
|
||||
onValueChange={(value) =>
|
||||
mutateTankopedia((draft) => {
|
||||
draft.filters.test = value as TankopediaTestTankDisplay;
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="include">
|
||||
Include
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="exclude">
|
||||
Exclude
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<Flex style={{ flex: 1 }}>
|
||||
{TREE_TYPES.map((type, index) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={
|
||||
tankopediaState.filters.treeTypes.includes(type)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === TREE_TYPES.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === TREE_TYPES.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.treeTypes.includes(type)) {
|
||||
draft.filters.treeTypes = draft.filters.treeTypes.filter(
|
||||
(preexistingType) => preexistingType !== type,
|
||||
);
|
||||
} else {
|
||||
draft.filters.treeTypes.push(type);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={TREE_TYPE_IMAGES[type]}
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex style={{ flex: 1 }}>
|
||||
{[...TANK_TYPES].reverse().map((type, index) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={
|
||||
tankopediaState.filters.types.includes(type) ? 'solid' : 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === TANK_TYPES.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === TANK_TYPES.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.types.includes(type)) {
|
||||
draft.filters.types = draft.filters.types.filter(
|
||||
(preexistingType) => preexistingType !== type,
|
||||
);
|
||||
} else {
|
||||
draft.filters.types.push(type);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={TREE_TYPE_ICONS[TreeTypeEnum.TechTree][type]}
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
className={styles.splitFilters}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Flex>
|
||||
{[...TIERS]
|
||||
.reverse()
|
||||
.slice(0, 5)
|
||||
.map((tier, index) => (
|
||||
<Button
|
||||
key={tier}
|
||||
variant={
|
||||
tankopediaState.filters.tiers.includes(tier)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius:
|
||||
index === TIERS.length - Math.round(TIERS.length / 2) - 1
|
||||
? undefined
|
||||
: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.tiers.includes(tier)) {
|
||||
draft.filters.tiers = draft.filters.tiers.filter(
|
||||
(preexistingTier) => preexistingTier !== tier,
|
||||
);
|
||||
} else {
|
||||
draft.filters.tiers.push(tier);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{TIER_ROMAN_NUMERALS[tier]}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex>
|
||||
{[...TIERS]
|
||||
.reverse()
|
||||
.slice(5, 10)
|
||||
.map((tier, index) => (
|
||||
<Button
|
||||
key={tier}
|
||||
variant={
|
||||
tankopediaState.filters.tiers.includes(tier)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius:
|
||||
index === TIERS.length - Math.round(TIERS.length / 2) - 1
|
||||
? undefined
|
||||
: 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.tiers.includes(tier)) {
|
||||
draft.filters.tiers = draft.filters.tiers.filter(
|
||||
(preexistingTier) => preexistingTier !== tier,
|
||||
);
|
||||
} else {
|
||||
draft.filters.tiers.push(tier);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{TIER_ROMAN_NUMERALS[tier]}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
className={styles.splitFilters}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Flex>
|
||||
{nations.slice(0, 5).map((nation, index) => (
|
||||
<Button
|
||||
key={nation}
|
||||
variant={
|
||||
tankopediaState.filters.nations.includes(nation)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius:
|
||||
index === nations.length - Math.round(nations.length / 2)
|
||||
? undefined
|
||||
: 0,
|
||||
borderBottomRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.nations.includes(nation)) {
|
||||
draft.filters.nations = draft.filters.nations.filter(
|
||||
(preexistingType) => preexistingType !== nation,
|
||||
);
|
||||
} else {
|
||||
draft.filters.nations.push(nation);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flag nation={nation} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
{nations.slice(5, 10).map((nation, index) => (
|
||||
<Button
|
||||
key={nation}
|
||||
variant={
|
||||
tankopediaState.filters.nations.includes(nation)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index ===
|
||||
nations.length - Math.round(nations.length / 2) - 1
|
||||
? undefined
|
||||
: 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.nations.includes(nation)) {
|
||||
draft.filters.nations = draft.filters.nations.filter(
|
||||
(preexistingType) => preexistingType !== nation,
|
||||
);
|
||||
} else {
|
||||
draft.filters.nations.push(nation);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flag nation={nation} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
|
||||
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
|
||||
{[...TIERS].reverse().map((tier, index) => (
|
||||
<Button
|
||||
key={tier}
|
||||
variant={
|
||||
tankopediaState.filters.tiers.includes(tier) ? 'solid' : 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === TIERS.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === TIERS.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.tiers.includes(tier)) {
|
||||
draft.filters.tiers = draft.filters.tiers.filter(
|
||||
(preexistingTier) => preexistingTier !== tier,
|
||||
);
|
||||
} else {
|
||||
draft.filters.tiers.push(tier);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{TIER_ROMAN_NUMERALS[tier]}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
|
||||
{nations.map((nation, index) => (
|
||||
<Button
|
||||
key={nation}
|
||||
variant={
|
||||
tankopediaState.filters.nations.includes(nation)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.nations.includes(nation)) {
|
||||
draft.filters.nations = draft.filters.nations.filter(
|
||||
(preexistingType) => preexistingType !== nation,
|
||||
);
|
||||
} else {
|
||||
draft.filters.nations.push(nation);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flag nation={nation} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, TextField } from '@radix-ui/themes';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { BlitzkriegTankDefinition } from '../../../../core/blitzkrieg/definitions/tanks';
|
||||
|
||||
interface PageTurnerProps {
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
searchedList: BlitzkriegTankDefinition[];
|
||||
tanksPerPage: number;
|
||||
}
|
||||
|
||||
export function PageTurner({
|
||||
page,
|
||||
setPage,
|
||||
searchedList,
|
||||
tanksPerPage,
|
||||
}: PageTurnerProps) {
|
||||
const pageInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<Flex align="center" justify="center" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
disabled={page === 0}
|
||||
onClick={() => setPage(Math.max(0, page - 1))}
|
||||
>
|
||||
<CaretLeftIcon />
|
||||
</Button>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>Page</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={1}
|
||||
type="number"
|
||||
ref={pageInput}
|
||||
min={1}
|
||||
max={Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||
style={{ width: 64, textAlign: 'center' }}
|
||||
onBlur={(event) => {
|
||||
setPage(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
Math.floor(searchedList.length / tanksPerPage),
|
||||
event.target.valueAsNumber - 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField.Slot>
|
||||
out of {Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
<Button
|
||||
variant="soft"
|
||||
disabled={Math.floor(searchedList.length / tanksPerPage) === page}
|
||||
onClick={() =>
|
||||
setPage(
|
||||
Math.min(Math.floor(searchedList.length / tanksPerPage), page + 1),
|
||||
)
|
||||
}
|
||||
>
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export const listing = style({});
|
||||
|
||||
export const flag = style({
|
||||
transitionDuration: theme.durations.regular,
|
||||
left: '0',
|
||||
top: '0',
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
transform: 'translateX(-25%)',
|
||||
filter: 'blur(3rem)',
|
||||
},
|
||||
});
|
||||
|
||||
export const listingShadow = style({
|
||||
boxShadow: `inset 128px -128px 128px -128px ${theme.colors.appBackground1}`,
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
boxShadow: `inset 0 0 0 -128px ${theme.colors.appBackground1}`,
|
||||
},
|
||||
});
|
||||
|
||||
export const listingLabel = style({
|
||||
transition: `transform ${theme.durations.regular}`,
|
||||
transform: 'translateY(0)',
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
transform: 'translateY(200%)',
|
||||
},
|
||||
});
|
||||
|
||||
export const listingImage = style({
|
||||
transition: `transform ${theme.durations.regular}`,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
transform: 'translate(-25%, 10%)',
|
||||
},
|
||||
});
|
||||
|
||||
export const enhancedArmor = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 400px)': {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 16,
|
||||
},
|
||||
|
||||
'screen and (max-width: 399px)': {
|
||||
// left: 16,
|
||||
// bottom: 16,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
'use client';
|
||||
|
||||
import { slateDark } from '@radix-ui/colors';
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CaretUpIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
Inset,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { go } from 'fuzzysort';
|
||||
import { Suspense, use, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { asset } from '../../../core/blitzkrieg/asset';
|
||||
import {
|
||||
BlitzkriegTankDefinition,
|
||||
TANK_ICONS,
|
||||
TANK_ICONS_COLLECTOR,
|
||||
TANK_ICONS_PREMIUM,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
tanksDefinitionsArray,
|
||||
} from '../../../core/blitzkrieg/definitions/tanks';
|
||||
import { tankIcon } from '../../../core/blitzkrieg/tankIcon';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import mutateTankopedia, {
|
||||
TankopediaSortBy,
|
||||
TankopediaSortDirection,
|
||||
useTankopedia,
|
||||
} from '../../../stores/tankopedia';
|
||||
import { Options } from './components/Options';
|
||||
import { PageTurner } from './components/PageTurner';
|
||||
import * as styles from './page.css';
|
||||
|
||||
const TANKS_PER_PAGE = 3 * 2 ** 3;
|
||||
|
||||
export default function Page() {
|
||||
const tankopediaState = useTankopedia();
|
||||
const awaitedTanks = use(tanksDefinitionsArray);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const searchableTanks = useMemo(
|
||||
() =>
|
||||
awaitedTanks
|
||||
.filter(
|
||||
(tank) =>
|
||||
(tankopediaState.filters.tiers.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.tiers.includes(tank.tier)) &&
|
||||
(tankopediaState.filters.types.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.types.includes(tank.type)) &&
|
||||
(tankopediaState.filters.treeTypes.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.treeTypes.includes(tank.tree_type)) &&
|
||||
(tankopediaState.filters.nations.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.nations.includes(tank.nation)) &&
|
||||
(tankopediaState.filters.test === 'include'
|
||||
? true
|
||||
: tankopediaState.filters.test === 'exclude'
|
||||
? !tank.testing
|
||||
: tank.testing),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
let diff = 0;
|
||||
|
||||
if (tankopediaState.sort.by === 'tier') {
|
||||
diff = a.tier - b.tier;
|
||||
}
|
||||
if (tankopediaState.sort.by === 'name') {
|
||||
diff = a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
return tankopediaState.sort.direction === 'ascending' ? diff : -diff;
|
||||
}),
|
||||
[tankopediaState.filters, tankopediaState.sort],
|
||||
);
|
||||
const [searchedList, setSearchedList] = useState(searchableTanks);
|
||||
const [page, setPage] = useState(0);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchedList(searchableTanks);
|
||||
if (input.current) input.current.value = '';
|
||||
}, [searchableTanks]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(
|
||||
Math.min(
|
||||
Math.max(0, page),
|
||||
Math.ceil(searchedList.length / TANKS_PER_PAGE) - 1,
|
||||
),
|
||||
);
|
||||
}, [searchedList]);
|
||||
|
||||
return (
|
||||
<PageWrapper size="wide" color="purple">
|
||||
<Suspense fallback={<Text>Loading...</Text>}>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex gap="2">
|
||||
<TextField.Root style={{ flex: 1 }}>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input
|
||||
ref={input}
|
||||
placeholder="Search tanks..."
|
||||
onChange={(event) => {
|
||||
if (event.target.value.length === 0) {
|
||||
setSearchedList(searchableTanks);
|
||||
} else {
|
||||
setSearchedList(
|
||||
go(event.target.value, searchableTanks, {
|
||||
keys: [
|
||||
'name',
|
||||
'id',
|
||||
] satisfies (keyof BlitzkriegTankDefinition)[],
|
||||
}).map(({ obj }) => obj),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="soft">
|
||||
Sort
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Label>By</DropdownMenu.Label>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaState.sort.by}
|
||||
onValueChange={(value) =>
|
||||
mutateTankopedia((draft) => {
|
||||
draft.sort.by = value as TankopediaSortBy;
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="tier">
|
||||
Tier
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="name">
|
||||
Name
|
||||
</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Label>Direction</DropdownMenu.Label>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaState.sort.direction}
|
||||
onValueChange={(value) =>
|
||||
mutateTankopedia((draft) => {
|
||||
draft.sort.direction = value as TankopediaSortDirection;
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="ascending">
|
||||
Ascending
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="descending">
|
||||
Descending
|
||||
</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => setShowFilters((state) => !state)}
|
||||
>
|
||||
Filters {showFilters ? <CaretUpIcon /> : <CaretDownIcon />}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{showFilters && (
|
||||
<Card>
|
||||
<Suspense fallback={<Text>Loading...</Text>}>
|
||||
<Options />
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<PageTurner
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
tanksPerPage={TANKS_PER_PAGE}
|
||||
searchedList={searchedList}
|
||||
/>
|
||||
|
||||
<Flex wrap="wrap" gap="3" justify="center">
|
||||
{searchedList
|
||||
.map((tank) => (
|
||||
<a
|
||||
key={tank.id}
|
||||
href={`/tools/tankopedia/${tank.id}`}
|
||||
style={{
|
||||
flex: 1,
|
||||
textDecoration: 'none',
|
||||
color: 'unset',
|
||||
}}
|
||||
>
|
||||
<Card className={styles.listing}>
|
||||
<Inset
|
||||
key={tank.id}
|
||||
style={{
|
||||
minWidth: 256,
|
||||
minHeight: 128,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={styles.flag}
|
||||
src={asset(`flags/scratched/${tank.nation}.webp`)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
position: 'absolute',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: 256,
|
||||
height: 128,
|
||||
}}
|
||||
className={styles.listingImage}
|
||||
>
|
||||
<img
|
||||
src={tankIcon(tank.id)}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'left center',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.listingShadow}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
transition: `box-shadow ${theme.durations.regular}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
justify="between"
|
||||
className={styles.listingLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
padding: '0 12px',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="center" gap="1">
|
||||
<img
|
||||
src={
|
||||
(tank.tree_type === 'collector'
|
||||
? TANK_ICONS_COLLECTOR
|
||||
: tank.tree_type === 'premium'
|
||||
? TANK_ICONS_PREMIUM
|
||||
: TANK_ICONS)[tank.type]
|
||||
}
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
<Text
|
||||
size="4"
|
||||
color={
|
||||
tank.tree_type === 'collector'
|
||||
? 'blue'
|
||||
: tank.tree_type === 'premium'
|
||||
? 'amber'
|
||||
: undefined
|
||||
}
|
||||
weight="medium"
|
||||
style={{
|
||||
color:
|
||||
tank.tree_type === 'researchable'
|
||||
? slateDark.slate12
|
||||
: undefined,
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 160,
|
||||
display: 'block',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{tank.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Text size="4" color="gray" highContrast={false}>
|
||||
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
</a>
|
||||
))
|
||||
.slice(page * TANKS_PER_PAGE, (page + 1) * TANKS_PER_PAGE)}
|
||||
</Flex>
|
||||
|
||||
<PageTurner
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
tanksPerPage={TANKS_PER_PAGE}
|
||||
searchedList={searchedList}
|
||||
/>
|
||||
</Suspense>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||