Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d041b353c9 | ||
|
|
77575168a0 | ||
|
|
051997d2d3 | ||
|
|
e53bdd8e19 | ||
|
|
4013192faa | ||
|
|
3da009f859 | ||
|
|
2a5888c033 | ||
|
|
ded98d78d4 | ||
|
|
f4b231de10 | ||
|
|
9fda3c6861 | ||
|
|
d3f58d9cda | ||
|
|
3ef793a776 | ||
|
|
13a901c839 | ||
|
|
5ede2324b3 | ||
|
|
4e8f1a4a71 | ||
|
|
eae9cbf971 | ||
|
|
0eac25f1c8 | ||
|
|
c281e9b66f | ||
|
|
eb7fc62e00 | ||
|
|
d5f83c444e | ||
|
|
c890bfa8bd | ||
|
|
9ead3eb627 | ||
|
|
b8db78c328 | ||
|
|
8b65b94851 | ||
|
|
9da723ed31 | ||
|
|
2f48dbc25c | ||
|
|
730d6f2bb8 | ||
|
|
335ae4493f | ||
|
|
eb83039967 | ||
|
|
6e91810565 | ||
|
|
a55c259205 | ||
|
|
b5188331bd | ||
|
|
2b354770c7 | ||
|
|
11e4009303 | ||
|
|
9bce5e5d75 | ||
|
|
8e8d68a5b5 | ||
|
|
2c6fcdf21f | ||
|
|
abb72fb674 | ||
|
|
b805c2e3b4 | ||
|
|
7e6aa41ebb | ||
|
|
a2cfbda932 | ||
|
|
0b222be6ef | ||
|
|
1548813a21 | ||
|
|
c615284d87 | ||
|
|
6a48638171 | ||
|
|
0b4829abd3 | ||
|
|
57cd3aae50 | ||
|
|
6298d6230f | ||
|
|
df7a5bb8a0 | ||
|
|
dd41cac3ac | ||
|
|
f17b275aa7 | ||
|
|
4a04f8a8f3 | ||
|
|
0f7d682782 | ||
|
|
14cecdebe3 | ||
|
|
c2b936c925 | ||
|
|
0fd17d9c96 | ||
|
|
b0645c39a0 | ||
|
|
1d947399ab | ||
|
|
659dddbe8e | ||
|
|
a5c1223f7d | ||
|
|
dc084f8d34 | ||
|
|
44b5907435 | ||
|
|
052fbec628 | ||
|
|
23d8c54366 | ||
|
|
dd88d42884 | ||
|
|
ed676f73b2 | ||
|
|
adb0a9af64 | ||
|
|
ccded1b2f8 | ||
|
|
89d43b4791 | ||
|
|
2226ac9852 | ||
|
|
29bace875e | ||
|
|
04107c10b7 | ||
|
|
0a1e823ce4 | ||
|
|
c656d31618 | ||
|
|
7eda4825ea | ||
|
|
60f0eb9a3c | ||
|
|
ce5c9f4afa | ||
|
|
e834d31d92 | ||
|
|
b679393b8a | ||
|
|
eb5f79228b | ||
|
|
6082576b60 | ||
|
|
92e9073939 | ||
|
|
d6669a84d6 | ||
|
|
0c4debe327 | ||
|
|
9f41bab28a | ||
|
|
b906b42998 | ||
|
|
728a11efa4 | ||
|
|
c35916c291 | ||
|
|
2fd2e93f55 | ||
|
|
b566d5982c | ||
|
|
bef43e9ae6 | ||
|
|
e6404d5c77 | ||
|
|
f4d58015f5 | ||
|
|
cd77d24dc6 | ||
|
|
e25a3cd868 | ||
|
|
68923fe800 | ||
|
|
ccc6ddd5c9 | ||
|
|
1c28b33da3 | ||
|
|
900337f3ce | ||
|
|
fae9f8bded | ||
|
|
a0d78f89ff | ||
|
|
6853d63bbb | ||
|
|
2b27954a3b | ||
|
|
7aa0b65d1d | ||
|
|
3e7a73c528 | ||
|
|
076785a9ad | ||
|
|
74828e3c45 | ||
|
|
310640c1ec | ||
|
|
c49c56fba3 | ||
|
|
cc744f6154 | ||
|
|
93bf4a4742 | ||
|
|
badeb588dd | ||
|
|
5876bf35b4 | ||
|
|
73903114c9 | ||
|
|
b29f7ca7f5 | ||
|
|
9d715c23e5 | ||
|
|
4ff5f7cdeb | ||
|
|
dccfe3d652 | ||
|
|
d780d32291 | ||
|
|
ebbdbc53a4 | ||
|
|
9bc1635680 | ||
|
|
d7d19a0b50 | ||
|
|
1b6845abf8 | ||
|
|
545ee70cd0 | ||
|
|
c92729d0e0 | ||
|
|
71c3b5d2ef | ||
|
|
7372075ba3 | ||
|
|
f119b06dd8 | ||
|
|
e23a440508 | ||
|
|
6a9fa08500 | ||
|
|
8e86e81e91 | ||
|
|
8b359e220a | ||
|
|
690bc5dbb5 | ||
|
|
d6e96e17ad | ||
|
|
ab26b346e6 | ||
|
|
f02db6ef0b | ||
|
|
55ba2525d1 | ||
|
|
ea07d2646d | ||
|
|
ca111510f6 | ||
|
|
519558dd28 | ||
|
|
c4d1e305f6 | ||
|
|
59772afb04 | ||
|
|
8c9dcfa551 | ||
|
|
8bbf03a128 | ||
|
|
519fba84dc | ||
|
|
e7e74f706e | ||
|
|
12e2793c92 | ||
|
|
69040dc453 | ||
|
|
511151e101 | ||
|
|
7fc993c2eb | ||
|
|
030ca744e9 | ||
|
|
74756ab7bf | ||
|
|
e9a61d579d | ||
|
|
7c3cad2c7d | ||
|
|
5f39ba55f6 | ||
|
|
2bfd8286a4 | ||
|
|
f3421b6f70 | ||
|
|
abbbf11b3d | ||
|
|
797f5a90d2 | ||
|
|
818d13fade | ||
|
|
129d7c9688 | ||
|
|
28407b0f05 | ||
|
|
8a9d72d0e6 | ||
|
|
f98a4c7caa | ||
|
|
92b329a0fb | ||
|
|
f3d32a4e63 | ||
|
|
0e73d79c9c | ||
|
|
526b5785b7 | ||
|
|
02129feb25 | ||
|
|
0a38c164a4 | ||
|
|
2e0d52e30e | ||
|
|
6d48f9fe7f | ||
|
|
aba52c9968 | ||
|
|
b96b9fd574 | ||
|
|
fe4ee8b611 | ||
|
|
94558393d9 | ||
|
|
db1e4af20c | ||
|
|
be20173acd | ||
|
|
d90a70ed2a | ||
|
|
85d699dbfc | ||
|
|
5d0fe8726b | ||
|
|
850308b2ac | ||
|
|
88a85ef152 | ||
|
|
8160935081 | ||
|
|
c057e295d2 | ||
|
|
28d0c26268 | ||
|
|
52cb73a575 | ||
|
|
9f244452da | ||
|
|
d46d948c66 | ||
|
|
11b99ef211 | ||
|
|
85d350a912 | ||
|
|
145bc84fb1 | ||
|
|
b7486022eb | ||
|
|
eadb4bb7b1 | ||
|
|
79723157d2 | ||
|
|
4214ff0de5 | ||
|
|
f516c9275e | ||
|
|
f0b905593c | ||
|
|
11440bc654 | ||
|
|
2dab957a05 | ||
|
|
60f3951e7a | ||
|
|
4f03788f15 | ||
|
|
17f4435a30 | ||
|
|
04e42c929c | ||
|
|
4ba45b8fd8 | ||
|
|
0ece1f4b18 | ||
|
|
49d3e1d1c2 | ||
|
|
184ca522dd | ||
|
|
9f07d910b8 | ||
|
|
ac3f2ea93a | ||
|
|
a77ff1a638 | ||
|
|
96d802a09f | ||
|
|
1babd66649 | ||
|
|
2f2996a40c | ||
|
|
9d1e059ea5 | ||
|
|
48603b918f | ||
|
|
c1253e8121 | ||
|
|
ecc029402e | ||
|
|
1b88773e28 | ||
|
|
cf419a9ebb | ||
|
|
8565d75747 | ||
|
|
186ea8b458 | ||
|
|
4292081df6 | ||
|
|
4b54b61c35 | ||
|
|
5d098c46fa | ||
|
|
7575f4581c | ||
|
|
4669fa6ec5 | ||
|
|
49294d1906 | ||
|
|
3eee946978 | ||
|
|
7fff1c9737 | ||
|
|
5b4c3119dd | ||
|
|
0a41ee15ed | ||
|
|
ad57d284bb | ||
|
|
b929e9449d | ||
|
|
948ea0bb8d | ||
|
|
c1b1b8d304 | ||
|
|
3bc5a6d740 | ||
|
|
d25da724e1 | ||
|
|
753804b8cf | ||
|
|
096122d43c | ||
|
|
3448427de0 | ||
|
|
d8bbd02715 | ||
|
|
f696c5f3c9 | ||
|
|
bd412cdaa2 | ||
|
|
913411275b | ||
|
|
17ff8e0819 | ||
|
|
b5fdc4d5cf | ||
|
|
59c5d95aff | ||
|
|
16babbad90 | ||
|
|
a9dc9e7ebd | ||
|
|
52e8df560e | ||
|
|
87259ad515 | ||
|
|
c96655a6c0 | ||
|
|
43894567b8 | ||
|
|
76a952a8e6 | ||
|
|
e2efe428a2 | ||
|
|
887cec2dd4 | ||
|
|
04ad48bd33 | ||
|
|
3613f5d13b | ||
|
|
3362e3b091 | ||
|
|
3322fa97d4 | ||
|
|
1dc7748a04 | ||
|
|
6176b3b0d8 | ||
|
|
81c2af08c6 | ||
|
|
2d83519f93 | ||
|
|
869c1f0def | ||
|
|
7a64fb8176 | ||
|
|
9256244286 | ||
|
|
85e54e4a07 | ||
|
|
8075dc8d9f | ||
|
|
57dced109a | ||
|
|
dd19d5b979 | ||
|
|
68d3fe6805 | ||
|
|
70f7698466 | ||
|
|
26a5fdea0e | ||
|
|
367bd1386b | ||
|
|
fac773a8f8 | ||
|
|
6bd8b7d42f | ||
|
|
7093662130 | ||
|
|
4062e56b4c | ||
|
|
0885e7e68a | ||
|
|
3cfa837682 | ||
|
|
1982604878 | ||
|
|
9ef6eea4a9 | ||
|
|
78e6b2696c | ||
|
|
40b6f30092 | ||
|
|
de2d9a1a3e | ||
|
|
b7ad12c698 | ||
|
|
886c2aa1d1 | ||
|
|
9818c9868a | ||
|
|
31055f1d66 | ||
|
|
6cef753053 | ||
|
|
5b889f8483 | ||
|
|
d0d905ccfb | ||
|
|
9a91a8bb13 | ||
|
|
c12cc9b8c5 | ||
|
|
fdbfd0bc10 | ||
|
|
286b178218 | ||
|
|
54e012f736 | ||
|
|
cc6de50de6 | ||
|
|
194e5fceca | ||
|
|
879a5d534c | ||
|
|
29aaf60da0 | ||
|
|
3c8f8b49e4 | ||
|
|
0b88f71f26 | ||
|
|
65e239a22b | ||
|
|
7727e64931 | ||
|
|
7b0465322d | ||
|
|
099e5eb228 | ||
|
|
eaff26c8d9 | ||
|
|
d79129df91 | ||
|
|
b363c76639 | ||
|
|
034429cd47 | ||
|
|
57867317cc | ||
|
|
c2e314b193 | ||
|
|
9d2af1de13 | ||
|
|
232823566b | ||
|
|
958f0f2a52 | ||
|
|
b34fef2147 | ||
|
|
fe661124fa | ||
|
|
457dcbcad5 | ||
|
|
c18ee70eba | ||
|
|
095ffc3d6a | ||
|
|
e8b91648b1 | ||
|
|
1b43f6ba99 | ||
|
|
fc5360363c | ||
|
|
47b3c3029e | ||
|
|
dea7f34ca3 | ||
|
|
1e5026ea4a | ||
|
|
98f568ff6a | ||
|
|
10a6f8371d | ||
|
|
0933aa6615 | ||
|
|
746592418d | ||
|
|
e2404b063c | ||
|
|
bae1301556 | ||
|
|
c2de4952ad | ||
|
|
cce61d8075 | ||
|
|
34a0388f45 | ||
|
|
9d62965fc3 | ||
|
|
892434e28f | ||
|
|
2597fe4bcd | ||
|
|
f85b4b36bd | ||
|
|
f777dafefc | ||
|
|
6ef65ccea3 | ||
|
|
974b27a313 | ||
|
|
5124d8abcd | ||
|
|
849bd356c0 | ||
|
|
6406486441 | ||
|
|
2993ada2e5 | ||
|
|
f083c18dd0 | ||
|
|
c7021c5689 | ||
|
|
815369805a | ||
|
|
d8feb5c3a8 | ||
|
|
cb101da8a8 | ||
|
|
71481f5971 | ||
|
|
817796ed3c | ||
|
|
ebb7618f2d | ||
|
|
9717f153cd | ||
|
|
afa0cfaeef | ||
|
|
298b89b227 | ||
|
|
3b94034767 | ||
|
|
444b4c21af | ||
|
|
3e9c828781 | ||
|
|
ca3a1fbe3f | ||
|
|
5f28b541e3 | ||
|
|
18edd349f2 | ||
|
|
f7c6a065c1 | ||
|
|
91d48eeadd | ||
|
|
21bafa1b7b | ||
|
|
38af2f2817 | ||
|
|
8faa1143e9 | ||
|
|
8f34ebbca4 | ||
|
|
1c03701e17 | ||
|
|
c8edd16eb3 | ||
|
|
b96ff2e239 | ||
|
|
a575da3005 | ||
|
|
3ca2dece3e | ||
|
|
191d6133b2 | ||
|
|
b15e73e3a8 | ||
|
|
9a6b8ab979 | ||
|
|
87786d41a7 | ||
|
|
ee7ffe7bc8 | ||
|
|
379a33375d | ||
|
|
bd3f5dea05 | ||
|
|
146d36234e | ||
|
|
f3edcb8077 | ||
|
|
9e492015ff | ||
|
|
d2977182d9 | ||
|
|
7d2d65bab8 | ||
|
|
415c3bd066 | ||
|
|
990ab1520c | ||
|
|
81cb1afeff | ||
|
|
dba5a0a2cd | ||
|
|
04cd205fee | ||
|
|
ec93d8e9ab | ||
|
|
56321d9e33 | ||
|
|
d39ab43e74 | ||
|
|
787d3ae6ea | ||
|
|
035f751bf1 | ||
|
|
43442ac829 | ||
|
|
da02a089b9 | ||
|
|
476d8e804a | ||
|
|
43e1d53d39 | ||
|
|
5372068830 | ||
|
|
025cec3687 | ||
|
|
c47c629a83 | ||
|
|
2b7907f8f7 | ||
|
|
903d4e4d95 | ||
|
|
8857b2baae | ||
|
|
f7bbb17f66 | ||
|
|
f407ffe4bb | ||
|
|
791a09daae | ||
|
|
9592c0b55f | ||
|
|
d9a34dc60e | ||
|
|
30e4e4e9b3 | ||
|
|
bd3ea9a8ec | ||
|
|
8131b69422 | ||
|
|
340d9c2666 | ||
|
|
2a169fac34 | ||
|
|
4819100ec6 | ||
|
|
f39a94cbe0 | ||
|
|
e61598f768 | ||
|
|
04ee3a0a44 | ||
|
|
8fa613f1e2 | ||
|
|
bdc240df08 | ||
|
|
7169e0d91a | ||
|
|
889ed7e191 | ||
|
|
1fb8dac805 | ||
|
|
9f71ac68b9 | ||
|
|
c1f3517a79 | ||
|
|
adb0af0f18 | ||
|
|
f79f1ffdc3 | ||
|
|
1c9b7463fc | ||
|
|
80741d66fc | ||
|
|
f02fb00e8c | ||
|
|
3d353cc3cf |
No files matched your search
@@ -2,10 +2,10 @@ name: CD Build Assets
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, main]
|
||||
branches: main
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -18,6 +18,9 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -46,9 +49,19 @@ jobs:
|
||||
- name: Run DepotDownloader
|
||||
run: ./temp/depotDownloader/DepotDownloader.exe -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
|
||||
|
||||
- name: Apply patches
|
||||
run: |
|
||||
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||
$env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
|
||||
$env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}"
|
||||
yarn setup:patches --depot
|
||||
|
||||
- name: Build assets
|
||||
run: |
|
||||
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
|
||||
$env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
|
||||
$env:VIDEOS_SHEET = "${{ secrets.VIDEOS_SHEET }}"
|
||||
$env:GOOGLE_APPLICATION_CREDENTIALS = "${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}"
|
||||
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
||||
@@ -18,10 +18,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
uses: actions/deploy-pages@v1
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: docs/.vitepress/dist
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
schedule:
|
||||
- cron: '0 */5 * * *'
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created, published, released, edited]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -17,13 +19,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkit/releases/latest \
|
||||
| grep "browser_download_url.*bot.zip" \
|
||||
| cut -d : -f 2,3 \
|
||||
| tr -d \")
|
||||
@@ -31,4 +33,4 @@ jobs:
|
||||
unzip bot.zip
|
||||
|
||||
- name: Run bot
|
||||
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
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} node --enable-source-maps index.cjs
|
||||
@@ -1,11 +1,7 @@
|
||||
name: CD Ratings Asia
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# asia midnight: '0 17 * * *'
|
||||
name: CD Rating Asia
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 17 * * *'
|
||||
|
||||
jobs:
|
||||
@@ -14,10 +10,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
@@ -35,4 +31,4 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=asia --target=midnight --production
|
||||
@@ -1,11 +1,7 @@
|
||||
name: CD Ratings Europe
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# eu midnight: '0 23 * * *'
|
||||
name: CD Rating Europe
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 23 * * *'
|
||||
|
||||
jobs:
|
||||
@@ -14,10 +10,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
@@ -35,4 +31,4 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=eu --target=midnight --production
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD Rating Latest
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v4
|
||||
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 all leaderboards
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=na,asia,eu --target=latest --production
|
||||
@@ -1,11 +1,7 @@
|
||||
name: CD Ratings North America
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# na midnight: '0 5 * * *'
|
||||
name: CD Rating North America
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
@@ -14,10 +10,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
@@ -35,4 +31,4 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=na --target=midnight --production
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
@@ -33,11 +33,14 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Build Prisma
|
||||
run: yarn prisma generate
|
||||
|
||||
- name: Build
|
||||
run: yarn build:bot
|
||||
run: yarn build:bot --production
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bot
|
||||
path: dist/bot
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
run: yarn build:docs
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs
|
||||
path: dist/docs
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
run: yarn build:website
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: website
|
||||
path: dist/website
|
||||
@@ -8,7 +8,6 @@ dist/
|
||||
dist.*
|
||||
yarn-error.log
|
||||
/test.*
|
||||
test.*
|
||||
/docs/.vitepress/dist
|
||||
/docs/.vitepress/cache
|
||||
servers.csv
|
||||
@@ -17,4 +16,7 @@ tsconfig.tsbuildinfo
|
||||
temp/
|
||||
/.yarn
|
||||
/.VSCodeCounter
|
||||
/.yarn
|
||||
/.yarn
|
||||
depots
|
||||
public/sitemap*
|
||||
/firebase.json
|
||||
@@ -1,3 +1,3 @@
|
||||
[submodule "submodules/closedkrieg"]
|
||||
path = submodules/closedkrieg
|
||||
url = https://github.com/tresabhi/closedkrieg
|
||||
[submodule "submodules/blitzkit-closed"]
|
||||
path = submodules/blitzkit-closed
|
||||
url = https://github.com/tresabhi/blitzkit-closed
|
||||
@@ -3,10 +3,12 @@
|
||||
"AABBOX",
|
||||
"Abhi",
|
||||
"accountinfo",
|
||||
"adsense",
|
||||
"aeson",
|
||||
"alphatest",
|
||||
"APAC",
|
||||
"apcr",
|
||||
"applypatches",
|
||||
"artefacts",
|
||||
"atgm",
|
||||
"AYUV",
|
||||
@@ -15,12 +17,18 @@
|
||||
"betakrieg",
|
||||
"BINORMAL",
|
||||
"Binormals",
|
||||
"BKRL",
|
||||
"BKRTS",
|
||||
"BKRTSMANIFEST",
|
||||
"bkwd",
|
||||
"blitzkit",
|
||||
"blitzrinth",
|
||||
"blitzstars",
|
||||
"Camo",
|
||||
"CDON",
|
||||
"classname",
|
||||
"cleandist",
|
||||
"closedkrieg",
|
||||
"cockroackdb",
|
||||
"Coef",
|
||||
"cooldown",
|
||||
@@ -69,12 +77,15 @@
|
||||
"leaderboards",
|
||||
"lerp",
|
||||
"LINEARSIZE",
|
||||
"loadouts",
|
||||
"loddist",
|
||||
"lods",
|
||||
"mantlets",
|
||||
"metallicness",
|
||||
"minitelrose",
|
||||
"Minotauro",
|
||||
"MIPMAPCOUNT",
|
||||
"mišek",
|
||||
"modeltransform",
|
||||
"MURICA",
|
||||
"NEGATIVEX",
|
||||
@@ -97,13 +108,16 @@
|
||||
"POSITIVEZ",
|
||||
"Promisable",
|
||||
"Pyogenics",
|
||||
"ratingsleaderboard",
|
||||
"ratingsplot",
|
||||
"ratingleaderboard",
|
||||
"raycaster",
|
||||
"researchable",
|
||||
"Resvg",
|
||||
"rizz",
|
||||
"roboto",
|
||||
"rtsc",
|
||||
"RTSM",
|
||||
"SCPG",
|
||||
"scroller",
|
||||
"searchclans",
|
||||
"searchplayers",
|
||||
"searchtanks",
|
||||
@@ -123,6 +137,7 @@
|
||||
"Synx",
|
||||
"tankaverages",
|
||||
"Tankerch",
|
||||
"tankmen",
|
||||
"tankopedia",
|
||||
"tankopedias",
|
||||
"tankstats",
|
||||
@@ -141,14 +156,17 @@
|
||||
"Vovko",
|
||||
"wargaming",
|
||||
"webp",
|
||||
"whitespaces",
|
||||
"winrate",
|
||||
"wireframe",
|
||||
"Woah",
|
||||
"WOTB",
|
||||
"wotblitz",
|
||||
"xffn",
|
||||
"xkdkw",
|
||||
"XMLDVPL",
|
||||
"YAMLDVPL",
|
||||
"youtubers",
|
||||
"zustand"
|
||||
],
|
||||
"deno.enable": false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Blitzkrieg
|
||||
# BlitzKit
|
||||
|
||||
[](https://codeium.com)
|
||||
|
||||
@@ -6,18 +6,18 @@ Tools for everything World of Tanks Blitz
|
||||
|
||||
## Invite the bot
|
||||
|
||||
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

|
||||
@@ -5,9 +5,9 @@ import { defineConfig } from 'vitepress';
|
||||
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
||||
|
||||
export default defineConfig({
|
||||
title: 'blitzkrieg',
|
||||
title: 'BlitzKit',
|
||||
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
||||
base: '/blitzkrieg/',
|
||||
base: '/blitzkit/',
|
||||
outDir: '../dist/docs',
|
||||
|
||||
themeConfig: {
|
||||
@@ -33,7 +33,7 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
|
||||
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkit' },
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
# Blitzkrieg 1.16.2
|
||||
|
||||
¡Hola desde el mundo del español!
|
||||
|
||||
## Features
|
||||
|
||||
- Added translations for Spanish to the bot
|
||||
- Your language will be inferred automatically from your Discord settings
|
||||
- Any untranslated languages and words we forgot to translate will be left in English
|
||||
- If you have your language set to Spanish and see English in any places, please let us know on [the official Discord server](https://discord.gg/nDt7AjGJQH)
|
||||
- All `/about` commands except for `commands` remain in English
|
||||
- Tank names are still in English
|
||||
- The website remains unchanged
|
||||
- The title bar (featured as the big text on top of most commands) has now been made more compact
|
||||
|
||||
## Changes
|
||||
|
||||
- "Tank type" is now called "tank class"
|
||||
- Removed `/player-achievements` and `search-tanks`
|
||||
- They will be reintroduced in the future in a far better way
|
||||
- Also removed a `SKLLD` server exclusive `/eligible` command
|
||||
- This will be generalized for all clans in the future
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed the red glowing floor underneath the tank sandbox
|
||||
- This has been replaced with a static image
|
||||
- The refresh button is now properly removed when lacking permissions
|
||||
- Tank names that start with a number are now properly inferred from command inputs
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Command registration is now error tolerant meaning that if a single command fails to initiate, it will be discarded until the next restart
|
||||
- We are currently looking into how we can allow the command to retry registration after a fixed amount of time
|
||||
- Due to the implications of the code added which can be very easily exploited by hackers, a single file has been added to the repository as a closed source submodule and will remain closed unless a third party open-source implementation is made public
|
||||
- Removed preview commands
|
||||
@@ -0,0 +1,19 @@
|
||||
# Blitzkrieg 1.16.4
|
||||
|
||||
A few issues here and there addressed for good measure.
|
||||
|
||||
## Changes
|
||||
|
||||
- Armor layer penetration indices are now much more compact.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Reverted splash mechanics changes from `1.16.1` as it made them less accurate.
|
||||
- Auto-reloaders now report their DPM stats correctly with crew training.
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- All files in the project were reviewed and constant exports were isolated.
|
||||
- The downloaded game in the workflow now also includes the latest patches.
|
||||
- Asset building now uses sharp under the hood which should in theory be faster.
|
||||
- Fetching CDON LZ4 files now uses `no-store` instead of `no-cache`
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.17.0
|
||||
|
||||
A preview command and a touchup.
|
||||
|
||||
## Features
|
||||
|
||||
- Added `/research` in preview only accessible in the official Discord server.
|
||||
- More details will be posted when the command is made public.
|
||||
|
||||
## Changes
|
||||
|
||||
- `/owned-tanks` now returns a simple text message listing all the tanks instead of an image.
|
||||
- This is meant to improve the readability and ease of copying.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Blitzkrieg 1.17.1
|
||||
|
||||
Some stability fixes.
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- `sharp` is now included in the build instead of having being installed before runtime.
|
||||
- Png converter cache is now more memory efficient.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.17.2
|
||||
|
||||
Introducing the new `/research` command!
|
||||
|
||||
## Features
|
||||
|
||||
- Added the `/research` command
|
||||
- Type in the name of a tech tree tank you desire to research and this command will list all the expenses in researching all tanks in that line
|
||||
- The starting tank is inferred automatically by the tanks you own
|
||||
- This may be a bit inaccurate because of the lack of accurate information Wargaming provides about owned tanks
|
||||
- You are also manually provide a starting tank and a user to see the progress on a line for
|
||||
- Replaced Vercel Analytics (which collapsed under the insane amount of traffic on the website) with Google Analytics
|
||||
- Spaced armor can now be toggled in the Tankopedia armor sandbox
|
||||
@@ -0,0 +1,19 @@
|
||||
# Blitzkrieg 1.18.0
|
||||
|
||||
Introducing the new `/rating` command!
|
||||
|
||||
## Features
|
||||
|
||||
- `/rating` command
|
||||
- Features statistics on today's performance in ratings
|
||||
- The included stats are battles, winrate, damage, survival, and kills
|
||||
- This command also features the score, rank, and league you are at plus the reward you'll get if you are high enough on the leaderboard
|
||||
- `/ratings`, the old leaderboard command, has been renamed `/rating-leaderboard`
|
||||
- Reward icons have been removed from the leaderboard
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Rating leaderboards are now built in a more action-efficient manner.
|
||||
- This version also introduces the BKRL (Blitzkrieg rating leaderboard) file format which is an efficient way to store entire leaderboards of regions to disk (1 MB per 40,000 users).
|
||||
- Latest archival actions now build all regions at once in a single action.
|
||||
- The sharp module is now included in the build.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Blitzkrieg 1.18.1
|
||||
|
||||
HE gets more accurate and say goodbye to NaNaNaNaNaN.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Armor highlighting now works on Apple MacBooks
|
||||
- Applied equipments now persist after switching shells
|
||||
- The bottom of the tank sandbox panel can now be interacted with as it was previously blocked by an invisible layer
|
||||
- `/rating` now uses the midnight leaderboard instead of the absolute latest one
|
||||
- 0-battle stats are now hidden
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.18.2
|
||||
|
||||
Redefining how micro a micro-path is.
|
||||
|
||||
## Fixes
|
||||
|
||||
- The pitch and yaw controls, along with the quick equipment buttons, can once again be interacted with
|
||||
@@ -0,0 +1,29 @@
|
||||
# Blitzkrieg 1.18.3
|
||||
|
||||
Making Tankopedia more pleasing to use. This update only affects the website.
|
||||
|
||||
## Features
|
||||
|
||||
- Added crew skills
|
||||
- Added quick actions
|
||||
- For example, crew skills have the "clear" and "maximize" actions and equipment simply has a "clear" action
|
||||
- Added info buttons
|
||||
- Beside the titles of a few sections, a new info buttons have been added for extended descriptions
|
||||
- Tank configurations have been condensed thanks to a new layout that works well on most screen sizes
|
||||
- The Tankopedia now takes advantage of wider screens than before; hence, getting rid of the useless whitespaces on the sides
|
||||
- Tank modules are now neatly wrapped into dropdown menus
|
||||
- Sorting tanks by "size" has been renamed to "volume" and length has been added as a new option
|
||||
- The antagonist (the shooting tank) bar has been shrunk to only fit the its contents
|
||||
- Fullscreen mode now features all the missing dropdowns and options that were previously hidden
|
||||
- Added icons for donations on Patreon and Ko-fi in the navigation bar
|
||||
- Tank search now uses regular links allowing for things like opening in new tabs, copying links, etc.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Armor highlighting once again works properly even after switching around tank modules
|
||||
- Inputting invalid numbers into the pitch and yaw fields is now handled correctly
|
||||
- Removed unnecessary tooltips that didn't show up anyway due to Radix UI 3
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Upgraded to Radix UI 3
|
||||
@@ -0,0 +1,23 @@
|
||||
# Blitzkrieg 1.19.0
|
||||
|
||||
Introducing the Blitzkrieg Compare tool!
|
||||
|
||||
## Features
|
||||
|
||||
- Added Compare
|
||||
- Allows you to compare multiple tanks
|
||||
- Shows deltas in the form of differences between values or percentages
|
||||
- All stats from the Tankopedia are available here
|
||||
- [CLS](https://web.dev/articles/cls) was reduced due to the new loading screens
|
||||
- Clicking enter when searching for tanks now selects the first result
|
||||
- The first tank name is also displayed in the search input
|
||||
|
||||
## Changes
|
||||
|
||||
- Removed the donation banner instead for a little note in the footer
|
||||
- The footer now shows the version of Blitzkrieg and the version of WoTB its running on
|
||||
- Removed frosted glass effect from most components apart from the navigation bar for performance reasons (I am looking at you, Apple!)
|
||||
|
||||
## Fixes
|
||||
|
||||
- Tank search is now not ignored when changing filters
|
||||
@@ -0,0 +1,11 @@
|
||||
# Blitzkrieg 1.19.1
|
||||
|
||||
A minor-major fix to the armor sandbox. This update only affects the website.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Guns are now clipped properly allowing some tanks like the 252U and T34's mantlets to be penetrable (usually with premium shells) as they are in-game
|
||||
|
||||
| Before | After |
|
||||
| ------------------------------------------ | ----------------------------------------- |
|
||||
|  |  |
|
||||
@@ -0,0 +1,6 @@
|
||||
# Blitzkrieg 1.19.2
|
||||
|
||||
## Fixes
|
||||
|
||||
- Maximum DPM for double shot auto-reloaders is now calculate correctly with intra-clip in mind
|
||||
- Blitzkrieg logo is now removed on lower resolutions to allow for other buttons to fit in the title bar of the Tankopedia
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.19.3
|
||||
|
||||
Google rizz. This version only affects the website.
|
||||
|
||||
## Features
|
||||
|
||||
- The titles and descriptions of the Tankopedia tank pages are now more informative
|
||||
- Added a settings page
|
||||
- Added a hamburger menu to the navigation bar
|
||||
|
||||
## Fixes
|
||||
|
||||
- Tank icons in URL previews are now presented properly
|
||||
@@ -0,0 +1,18 @@
|
||||
# Blitzkrieg 1.19.4
|
||||
|
||||
I genuinely have no idea how these bugs sneaked in here. This update only affects the website.
|
||||
|
||||
## Features
|
||||
|
||||
- Added a button in Compare which links to the corresponding Tankopedia page
|
||||
- Added a button in Tankopedia that links to the corresponding Compare page
|
||||
- If the two tanks (the shooter and the one of which armor is displayed) in the Tankopedia are different, they will be both added by default in the Compare page
|
||||
- Simplified the footer and added a link to my website and the credits page
|
||||
- Added an aggressive donate button right at the top of the home page
|
||||
|
||||
## Fixes
|
||||
|
||||
Thanks to mišek for these bug reports!
|
||||
|
||||
- The intra-clip time is now displayed for auto-loaders
|
||||
- High End Consumables now increase the duration of consumables correctly instead of decreasing it
|
||||
@@ -0,0 +1,19 @@
|
||||
# Blitzkrieg 1.19.5
|
||||
|
||||
The tankopedia, now more helpful than ever! This update only affects the website.
|
||||
|
||||
## Features
|
||||
|
||||
- The Tankopedia pages for tanks now include videos from YouTubers
|
||||
- The selection of tanks that have videos is very small currently but you can help expand it by dropping suggestions on [the official Discord server](https://discord.gg/nDt7AjGJQH)
|
||||
- Added shell normalization and ricochet angles to the Tankopedia and Compare
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed undefined clipping planes
|
||||
- Added proper keying to all JSX tree nodes, navbar menu items, antagonist bar shells, skill rows and fragments of tank stats in the Tankopedia
|
||||
- The second half of barrels for tanks like the T-100 LT are now properly included
|
||||
|
||||
## Technical Features
|
||||
|
||||
- The list of videos is fetched transpiled from a Google Sheets spreadsheet into a CSV file in the asset repository
|
||||
@@ -0,0 +1,3 @@
|
||||
# BlitzKit 1.19.6
|
||||
|
||||
Renamed Blitzkrieg to BlitzKit.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Blitzkit Rating Leaderboard
|
||||
|
||||
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
|
||||
|
||||
```cpp
|
||||
primary BlitzkitRatingLeaderboard {
|
||||
utf8"BKRL" magic;
|
||||
Header header;
|
||||
primary Body<header> body;
|
||||
}
|
||||
|
||||
Header {
|
||||
uint16 version; // always 1
|
||||
Format format;
|
||||
uint32 count;
|
||||
}
|
||||
|
||||
Format enum<uint8> {
|
||||
Base,
|
||||
Superset1,
|
||||
}
|
||||
|
||||
Body<Header header> match<header.format> {
|
||||
Base {
|
||||
primary {
|
||||
uint32 id;
|
||||
uint16 score;
|
||||
}[header.count] entries;
|
||||
};
|
||||
|
||||
Superset1 {
|
||||
primary {
|
||||
uint32 id;
|
||||
uint16 score;
|
||||
|
||||
uint32 battles;
|
||||
uint32 wins;
|
||||
uint32 survived;
|
||||
|
||||
uint32 damage;
|
||||
uint32 kills;
|
||||
}[header.count] entries;
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -1,7 +1,5 @@
|
||||
# Cedilla Object Notation
|
||||
|
||||
This documentation uses an experimental binary notation syntax called "Buffer Up".
|
||||
|
||||
1. JSON compressed as CDON, written directly to disk (a direct 50% reduction).
|
||||
2. CDON further compressed with LZ4 (a clean 90% reduction).
|
||||
3. The raw JSON file as fetched from a random data source.
|
||||
@@ -19,7 +17,7 @@ primary CedillaObjectNotation {
|
||||
}
|
||||
|
||||
Header {
|
||||
uint16 version;
|
||||
uint16 version; // always 1
|
||||
FSFormat fsFormat;
|
||||
StringTable<fsFormat> stringTable;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Team
|
||||
|
||||
- TrèsAbhi: creator & programmer
|
||||
- Prince_NA: game advisor & tester
|
||||
|
||||
# Contributors
|
||||
|
||||
- S0AP: bot verification process & initial advertizement platform
|
||||
- Prince_NA: general game advisor and tester
|
||||
- S0AP: bot verification and initial advertizement platform
|
||||
- Vovko: discord bot rendering pipeline
|
||||
- Maddox: game file decompression
|
||||
- Pyogenics: tank model decompression
|
||||
- Pyogenics: tank model parsing
|
||||
- Aeson: game files layout
|
||||
- Minitelrose, ChickenMan7777: 3-caliber rule edge cases
|
||||
- \_34, HisRoyalFatness, Synx, xkdkw: embed UX
|
||||
- [OPTML], Aeson, \_34, Droodles: beta testing tankopedia
|
||||
- [OPTML], Aeson, \_34, Droodles: beta testing Tankopedia
|
||||
- [OPTML], Droodles: beta testing Compare
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Bot
|
||||
|
||||
Blitzkrieg is available as a Discord bot and can be [installed very easily](/guide/invite).
|
||||
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
|
||||
|
||||
- Track statistics
|
||||
- Find inactive players in a clan
|
||||
@@ -15,8 +15,8 @@ Blitzkrieg is available as a Discord bot and can be [installed very easily](/gui
|
||||
|
||||
## Website
|
||||
|
||||
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
|
||||
Provides a bunch of very helpful tools; https://blitzkit.vercel.app/
|
||||
|
||||
- Session tracker
|
||||
- Ratings leaderboard
|
||||
- Rating leaderboard
|
||||
- Way more to come
|
||||
@@ -1,3 +1,3 @@
|
||||
# Inviting the Bot
|
||||
|
||||
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||
@@ -0,0 +1,71 @@
|
||||
# BlitzKit Regular Tank Stats
|
||||
|
||||
A straightforward way of storing the entire World of Tanks Blitz tank statistics for players to disk.
|
||||
|
||||
## Manifest `manifest.rtsm`
|
||||
|
||||
```cpp
|
||||
primary RegularTankStatisticsManifest {
|
||||
utf8"RTSM" magic;
|
||||
Header header;
|
||||
primary Body body;
|
||||
}
|
||||
|
||||
Header {
|
||||
uint16 version; // always 1
|
||||
}
|
||||
|
||||
Body {
|
||||
uint32 time;
|
||||
uint8 base;
|
||||
}
|
||||
```
|
||||
|
||||
## Chunk `*.rtsc`
|
||||
|
||||
```cpp
|
||||
primary RegularTankStatisticsChunk {
|
||||
utf8"RTSC" magic;
|
||||
Header header;
|
||||
primary Body body<header.playerCount>;
|
||||
}
|
||||
|
||||
Header {
|
||||
uint16 version; // always 1
|
||||
uint32 playerCount;
|
||||
}
|
||||
|
||||
Body<uint32 playerCount> {
|
||||
Player[playerCount] players;
|
||||
}
|
||||
|
||||
Player {
|
||||
uint32 id;
|
||||
uint16 tankCount;
|
||||
Tank[tankCount] tanks;
|
||||
}
|
||||
|
||||
Tank {
|
||||
uint32 id;
|
||||
uint32 battleLifeTime;
|
||||
uint32 lastBattleTime;
|
||||
uint8 markOfMastery;
|
||||
uint32 battles;
|
||||
uint32 capturePoints;
|
||||
uint32 damageDealt;
|
||||
uint32 damageReceived;
|
||||
uint32 droppedCapturePoints;
|
||||
uint32 frags;
|
||||
uint32 frags8p;
|
||||
uint32 hits;
|
||||
uint32 losses;
|
||||
uint8 maxFrags;
|
||||
uint16 maxXp;
|
||||
uint32 shots;
|
||||
uint32 spotted;
|
||||
uint32 survivedBattles;
|
||||
uint32 winAndSurvived;
|
||||
uint32 wins;
|
||||
uint32 xp;
|
||||
}
|
||||
```
|
||||
@@ -3,12 +3,12 @@
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: 'blitzkrieg'
|
||||
name: 'BlitzKit'
|
||||
text: Everything Blitz, right in Discord
|
||||
tagline: All-in-one Discord bot for everything World of Tanks Blitz
|
||||
image:
|
||||
src: '/assets/images/hero.png'
|
||||
alt: 'blitzkrieg'
|
||||
alt: 'blitzkit'
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Add to Discord Server
|
||||
@@ -20,11 +20,11 @@ hero:
|
||||
features:
|
||||
- title: Clean and Efficient UI
|
||||
icon: 🚀
|
||||
details: Blitzkrieg is where concise meets efficient. Get the information you need in the way you want.
|
||||
details: BlitzKit is where concise meets efficient. Get the information you need in the way you want.
|
||||
- title: Endless Possibilities
|
||||
icon: 🛠
|
||||
details: With endless ways to customize the output, it's guaranteed that you'll always get the information you need.
|
||||
- title: Easy to Use
|
||||
icon: 💻
|
||||
details: Leveraging Discord's, Blitz Stars', and Wargaming's powerful APIS, Blitzkrieg does all the hard work for you.
|
||||
details: Leveraging Discord's, Blitz Stars', and Wargaming's powerful APIS, Blitzkit does all the hard work for you.
|
||||
---
|
||||
@@ -1,22 +1,18 @@
|
||||
# Blitzkrieg Privacy Policy
|
||||
# BlitzKit Privacy Policy
|
||||
|
||||
Blitzkrieg collects little to no data. Regardless, here is an exhaustive list of Blitzkrieg's data collection practices.
|
||||
BlitzKit collects little to no data. Regardless, here is an exhaustive list of BlitzKit's data collection practices.
|
||||
|
||||
## Logs
|
||||
|
||||
All Blitzkrieg logs are available to the public as an effort for open source transparency. A history of all logs [can be found on GitHub](https://github.com/tresabhi/blitzkrieg/actions/workflows/host.yml). All logs expire automatically and are permanently deleted after 30 days. However, anyone may chose to save a local copy of the logs which is not tracked by GitHub.
|
||||
All BlitzKit logs are available to the public as an effort for open source transparency. A history of all logs [can be found on GitHub](https://github.com/tresabhi/blitzkit/actions/workflows/host.yml). All logs expire automatically and are permanently deleted after 30 days. However, anyone may chose to save a local copy of the logs which is not tracked by GitHub.
|
||||
|
||||
## Commands
|
||||
|
||||
Everytime a command a command is used, the command name and parameters provided is logged. Any identifying information like manually provided in-game usernames are also recorded.
|
||||
|
||||
## Errors
|
||||
|
||||
Everytime an error occurs, a detailed stack trace, scope of the error, and the error message is logged both in the GitHub logs and in the public channel [#🔲︱blitzkrieg-logs](https://discord.com/channels/734786591205359697/880650462540476416) in [the official Discord server](https://discord.gg/nDt7AjGJQH).
|
||||
|
||||
## Databases
|
||||
|
||||
No databases are used by Blitzkrieg meaning that no data is collected outside of what has already been mentioned above. This is no permanent state.
|
||||
No databases are used by BlitzKit meaning that no data is collected outside of what has already been mentioned above. This is no permanent state.
|
||||
|
||||
## 3rd Party
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Blitzkrieg Terms of Service
|
||||
# BlitzKit Terms of Service
|
||||
|
||||
## About
|
||||
|
||||
Blitzkrieg is a multi-function Discord bot that serves users with needs specific to the game World of Tanks Blitz.
|
||||
BlitzKit is a multi-function Discord bot that serves users with needs specific to the game World of Tanks Blitz.
|
||||
|
||||
## License
|
||||
|
||||
Blitzkrieg is licensed under the MIT License.
|
||||
BlitzKit is licensed under the MIT License.
|
||||
|
||||
## 3rd Party
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 35 KiB |
@@ -1,24 +1,26 @@
|
||||
{
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.16.2",
|
||||
"name": "blitzkit",
|
||||
"version": "1.19.6",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:bot": "vite-node --script scripts/build",
|
||||
"build:website": "next build",
|
||||
"build:db": "prisma generate",
|
||||
"build:bot": "vite-node --script scripts/build --production",
|
||||
"build:website": "yarn build:db && yarn build:sitemap && next build",
|
||||
"build:sitemap": "vite-node --script scripts/sitemap",
|
||||
"build:docs": "vitepress build docs",
|
||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
|
||||
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
|
||||
"build:test": "vite-node --script scripts/buildTest",
|
||||
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot",
|
||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||
"test": "vite-node --script scripts/test",
|
||||
"dev:bot": "vite-node --script scripts/build && yarn start:bot",
|
||||
"dev:docs": "vitepress dev docs",
|
||||
"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",
|
||||
"publish:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard --publish",
|
||||
"setup:patches": "vite-node --script scripts/applyPatches",
|
||||
"lint": "npx tsc",
|
||||
"lint:website": "next lint",
|
||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
||||
@@ -34,15 +36,14 @@
|
||||
"@octokit/plugin-throttling": "^8.1.3",
|
||||
"@octokit/rest": "^20.0.1",
|
||||
"@prisma/client": "^5.4.1",
|
||||
"@radix-ui/colors": "^3.0.0-rc.4",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/themes": "^2.0.1",
|
||||
"@radix-ui/themes": "^3.0.1",
|
||||
"@react-three/drei": "^9.93.0",
|
||||
"@react-three/fiber": "^8.15.14",
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"bepaint": "^0.1.0-beta.3",
|
||||
"decompress": "^4.2.1",
|
||||
"discord.js": "^14.13.0",
|
||||
@@ -51,6 +52,7 @@
|
||||
"express": "^4.18.2",
|
||||
"false": "^0.0.4",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"googleapis": "^134.0.0",
|
||||
"immer": "^10.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -64,8 +66,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fuzzysort": "^1.0.1",
|
||||
"react-promise-suspense": "^0.3.4",
|
||||
"react-spinners": "^0.13.8",
|
||||
"satori": "^0.10.8",
|
||||
"satori": "^0.10.13",
|
||||
"sharp": "0.33.1",
|
||||
"three": "0.159.0",
|
||||
"three-stdlib": "^2.29.4",
|
||||
@@ -83,6 +84,7 @@
|
||||
"@types/js-yaml": "^4.0.8",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/lz4": "^0.6.4",
|
||||
"@types/lz4js": "^0.2.1",
|
||||
"@types/markdown-escape": "^1.1.0",
|
||||
"@types/node": "^20.6.5",
|
||||
"@types/parse-dds": "^1.0.3",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "discord_blitz" (
|
||||
"discord" INT8 NOT NULL,
|
||||
"region" STRING NOT NULL,
|
||||
"blitz" INT4 NOT NULL,
|
||||
|
||||
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "cockroachdb"
|
||||
@@ -4,11 +4,15 @@ generator client {
|
||||
|
||||
datasource db {
|
||||
provider = "cockroachdb"
|
||||
url = env("DISCORD_BLITZ_DB")
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model discord_blitz {
|
||||
model Users {
|
||||
blitz Int @id
|
||||
last_used DateTime? @default(now())
|
||||
}
|
||||
|
||||
model DiscordUsers {
|
||||
discord BigInt @id
|
||||
region String
|
||||
blitz Int
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
google.com, pub-1289406790225052, DIRECT, f08c47fec0942fa0
|
||||
appnexus.com, 14163, RESELLER
|
||||
pubcenter.microsoft.com, 253617120, DIRECT
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 921 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,7 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Host: https://blitzkit.vercel.app
|
||||
|
||||
Sitemap: https://blitzkit.vercel.app/sitemap.txt
|
||||
Sitemap: https://blitzkit.vercel.app/sitemap.xml
|
||||
@@ -0,0 +1,48 @@
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import { parse as parsePath } from 'path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
|
||||
import { writeDVPL } from '../src/core/blitz/writeDVPL';
|
||||
import { secrets } from '../src/core/blitzkit/secrets';
|
||||
import { dvp } from '../submodules/closedkrieg/src/dvp';
|
||||
import { DATA } from './buildAssets/constants';
|
||||
|
||||
const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`);
|
||||
const currentVersion = versionTextFile
|
||||
.split(' ')[0]
|
||||
.split('.')
|
||||
.slice(0, 3)
|
||||
.join('.');
|
||||
|
||||
console.log(`Installing patches for ${currentVersion}...`);
|
||||
|
||||
let patchIndex = 1;
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${secrets.WOTB_DLC_CDN}/dlc/s${currentVersion}_${patchIndex}.yaml`,
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log(`Applying patch ${patchIndex}...`);
|
||||
|
||||
const data = parseYaml(await response.text());
|
||||
const dvpm = await fetch(`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11}`).then(
|
||||
(response) => response.arrayBuffer(),
|
||||
);
|
||||
const dvpd = await fetch(
|
||||
`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11.replace('.dvpm', '.dvpd')}`,
|
||||
).then((response) => response.arrayBuffer());
|
||||
const files = await dvp(dvpm, dvpd);
|
||||
|
||||
await Promise.all(
|
||||
files.map(async ({ path, data }) => {
|
||||
const { dir } = parsePath(path);
|
||||
|
||||
await mkdir(`${DATA}/${dir}`, { recursive: true });
|
||||
await writeFile(`${DATA}/${path}.dvpl`, writeDVPL(Buffer.from(data)));
|
||||
}),
|
||||
);
|
||||
|
||||
patchIndex++;
|
||||
} else break;
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { exec } from 'child_process';
|
||||
import { build } from 'esbuild';
|
||||
import { cp, readdir } from 'fs/promises';
|
||||
import { argv } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
readdir('src/workers').then((workers) => {
|
||||
const execPromise = promisify(exec);
|
||||
const isProduction = argv.includes('--production');
|
||||
|
||||
await readdir('src/workers').then((workers) => {
|
||||
const files = [
|
||||
'bot',
|
||||
'index',
|
||||
@@ -25,6 +31,7 @@ readdir('src/workers').then((workers) => {
|
||||
'.js': '.cjs',
|
||||
},
|
||||
|
||||
external: ['sharp'],
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // causes goofy errors
|
||||
@@ -36,9 +43,19 @@ readdir('src/workers').then((workers) => {
|
||||
});
|
||||
|
||||
// copy schema.prisma
|
||||
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||
readdir('node_modules/prisma').then((files) =>
|
||||
await cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||
await readdir('node_modules/prisma').then((files) =>
|
||||
files
|
||||
.filter((file) => file.endsWith('.node'))
|
||||
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
||||
);
|
||||
|
||||
if (isProduction) {
|
||||
// install fixed sharp
|
||||
console.log('Installing fixed sharp...');
|
||||
await execPromise('yarn init -y && yarn add [email protected] --ignore-engines', {
|
||||
cwd: 'dist/bot',
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Generated:', await readdir('dist/bot'));
|
||||
@@ -2,8 +2,8 @@ import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
|
||||
export async function boosterIcons(production: boolean) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
|
||||
export async function camouflageIcons(production: boolean) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
|
||||
export async function circleFlags(production: boolean) {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const POI = {
|
||||
collisionMeshes: '3d/Tanks/CollisionMeshes',
|
||||
optionalDevices: 'XML/item_defs/vehicles/common/optional_devices.xml',
|
||||
optionalDeviceSlots:
|
||||
'XML/item_defs/vehicles/common/optional_device_slots.xml',
|
||||
'XML/item_defs/vehicles/common/optional_device_slots.xml',
|
||||
optionalDeviceImageMappings:
|
||||
'UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml',
|
||||
optionalDevicesImage:
|
||||
|
||||
@@ -3,8 +3,8 @@ 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 { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
import { ConsumablesCommon, ProvisionsCommon } from './definitions';
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
const ICONS = [
|
||||
['currency_silver_m.packed.webp', 'silver'],
|
||||
['currency_premium_xl.packed.webp', 'premium'],
|
||||
['currency_gold_m.packed.webp', 'gold'],
|
||||
['currency_free-xp_xl.packed.webp', 'free-xp'],
|
||||
['currency_elite-xp_xl.packed.webp', 'elite-xp'],
|
||||
['currency_crew-xp_xl.packed.webp', 'crew-xp'],
|
||||
['currency_battle-xp_xl.packed.webp', 'xp'],
|
||||
];
|
||||
|
||||
export async function currencies(production: boolean) {
|
||||
console.log('Building currency icons...');
|
||||
|
||||
const changes = await Promise.all(
|
||||
ICONS.map(async ([file, name]) => {
|
||||
const content = (
|
||||
await sharp(
|
||||
await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}.dvpl`),
|
||||
)
|
||||
.trim()
|
||||
.toBuffer()
|
||||
).toString('base64');
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `icons/currencies/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
);
|
||||
|
||||
await commitAssets('currency icons', changes, production);
|
||||
}
|
||||
@@ -3,25 +3,28 @@ import { parse as parsePath } from 'path';
|
||||
import { Vector3Tuple } from 'three';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { TankClass } from '../../src/components/Tanks';
|
||||
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 { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import {
|
||||
ConsumableDefinitions,
|
||||
TankFilterDefinitionCategory,
|
||||
} from '../../src/core/blitzkrieg/consumableDefinitions';
|
||||
} from '../../src/core/blitzkit/consumableDefinitions';
|
||||
import {
|
||||
EquipmentDefinitions,
|
||||
EquipmentPreset,
|
||||
EquipmentRow,
|
||||
} from '../../src/core/blitzkrieg/equipmentDefinitions';
|
||||
} from '../../src/core/blitzkit/equipmentDefinitions';
|
||||
import { GameDefinitions } from '../../src/core/blitzkit/gameDefinitions';
|
||||
import {
|
||||
ModelArmor,
|
||||
ModelDefinitions,
|
||||
} from '../../src/core/blitzkrieg/modelDefinitions';
|
||||
import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions';
|
||||
import { superCompress } from '../../src/core/blitzkrieg/superCompress';
|
||||
} from '../../src/core/blitzkit/modelDefinitions';
|
||||
import { ProvisionDefinitions } from '../../src/core/blitzkit/provisionDefinitions';
|
||||
import { SkillDefinitions } from '../../src/core/blitzkit/skillDefinitions';
|
||||
import { superCompress } from '../../src/core/blitzkit/superCompress';
|
||||
import {
|
||||
Crew,
|
||||
CrewMember,
|
||||
@@ -30,11 +33,12 @@ import {
|
||||
TankDefinitionPrice,
|
||||
TankDefinitions,
|
||||
Tier,
|
||||
} from '../../src/core/blitzkrieg/tankDefinitions';
|
||||
} from '../../src/core/blitzkit/tankDefinitions';
|
||||
import { DATA, POI } from './constants';
|
||||
import { Avatar } from './skillIcons';
|
||||
import { TankParameters } from './tankIcons';
|
||||
|
||||
interface Strings {
|
||||
export interface BlitzStrings {
|
||||
[key: string]: string;
|
||||
}
|
||||
type ShellKind =
|
||||
@@ -79,6 +83,16 @@ type VehicleDefinitionArmor = Record<
|
||||
string,
|
||||
number | { vehicleDamageFactor: 0; '#text': number }
|
||||
>;
|
||||
interface UnlocksInner {
|
||||
cost: number;
|
||||
'#text': number;
|
||||
}
|
||||
type Unlocks = {
|
||||
[key in 'vehicle' | 'engine' | 'chassis' | 'turret' | 'gun']:
|
||||
| UnlocksInner
|
||||
| UnlocksInner[];
|
||||
};
|
||||
type UnlocksListing = Unlocks | undefined;
|
||||
interface VehicleDefinitions {
|
||||
invisibility: {
|
||||
moving: number;
|
||||
@@ -92,7 +106,7 @@ interface VehicleDefinitions {
|
||||
};
|
||||
consumableSlots: number;
|
||||
provisionSlots: number;
|
||||
optDevicePreset: string;
|
||||
optDevicePreset: string | string[];
|
||||
hull: {
|
||||
armor: VehicleDefinitionArmor;
|
||||
turretPositions: { turret: string };
|
||||
@@ -102,9 +116,11 @@ interface VehicleDefinitions {
|
||||
};
|
||||
chassis: {
|
||||
[key: string]: {
|
||||
unlocks: UnlocksListing;
|
||||
weight: number;
|
||||
terrainResistance: string;
|
||||
rotationSpeed: number;
|
||||
userString: string;
|
||||
shotDispersionFactors: {
|
||||
vehicleMovement: number;
|
||||
vehicleRotation: number;
|
||||
@@ -130,10 +146,13 @@ interface VehicleDefinitions {
|
||||
};
|
||||
};
|
||||
engines: {
|
||||
[key: string]: unknown;
|
||||
[key: string]: {
|
||||
unlocks: UnlocksListing;
|
||||
};
|
||||
};
|
||||
turrets0: {
|
||||
[key: string]: {
|
||||
unlocks: UnlocksListing;
|
||||
rotationSpeed: number;
|
||||
weight: number;
|
||||
circularVisionRadius: number;
|
||||
@@ -149,6 +168,7 @@ interface VehicleDefinitions {
|
||||
};
|
||||
guns: {
|
||||
[key: string]: {
|
||||
unlocks: UnlocksListing;
|
||||
armor: VehicleDefinitionArmor & {
|
||||
gun?:
|
||||
| number
|
||||
@@ -305,7 +325,7 @@ type ConsumablesVehicleFilter =
|
||||
| { name: string }
|
||||
| { extendedTags: string };
|
||||
|
||||
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
||||
const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
|
||||
ARMOR_PIERCING: 'ap',
|
||||
ARMOR_PIERCING_CR: 'ap_cr',
|
||||
HIGH_EXPLOSIVE: 'he',
|
||||
@@ -314,10 +334,18 @@ const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
|
||||
const missingStrings: Record<string, string> = {
|
||||
'#artefacts:tungstentip/name': 'Tungsten Shells',
|
||||
};
|
||||
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
|
||||
|
||||
export async function definitions(production: boolean) {
|
||||
console.log('Building definitions...');
|
||||
|
||||
const gameDefinitions: GameDefinitions = {
|
||||
version: (await readStringDVPL(`${DATA}/version.txt.dvpl`))
|
||||
.split(' ')[0]
|
||||
.split('.')
|
||||
.slice(0, 3)
|
||||
.join('.'),
|
||||
};
|
||||
const tankDefinitions: TankDefinitions = {};
|
||||
const modelDefinitions: ModelDefinitions = {};
|
||||
const equipmentDefinitions: EquipmentDefinitions = {
|
||||
@@ -326,16 +354,19 @@ export async function definitions(production: boolean) {
|
||||
};
|
||||
const consumableDefinitions: ConsumableDefinitions = {};
|
||||
const provisionDefinitions: ProvisionDefinitions = {};
|
||||
const skillDefinitions: SkillDefinitions = {
|
||||
classes: { lightTank: [], mediumTank: [], heavyTank: [], 'AT-SPG': [] },
|
||||
};
|
||||
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
const tankStringIdMap: Record<string, number> = {};
|
||||
const stringsPreInstalled = await readYAMLDVPL<Strings>(
|
||||
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
|
||||
`${DATA}/${POI.strings}/en.yaml.dvpl`,
|
||||
);
|
||||
const stringsCache = await fetch(POI.cachedStrings)
|
||||
.then((response) => response.text())
|
||||
.then((string) => parseYaml(string) as Strings);
|
||||
.then((string) => parseYaml(string) as BlitzStrings);
|
||||
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
|
||||
`${DATA}/${POI.optionalDevices}.dvpl`,
|
||||
);
|
||||
@@ -352,6 +383,11 @@ export async function definitions(production: boolean) {
|
||||
const provisions = await readXMLDVPL<{ root: ProvisionsCommon }>(
|
||||
`${DATA}/${POI.provisionsCommon}.dvpl`,
|
||||
);
|
||||
const avatar = await readXMLDVPL<{ root: Avatar }>(
|
||||
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
|
||||
);
|
||||
|
||||
const tankXps = new Map<number, number>();
|
||||
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
@@ -385,6 +421,12 @@ export async function definitions(production: boolean) {
|
||||
);
|
||||
|
||||
for (const tankKey in tankList.root) {
|
||||
if (botPattern.test(tankKey)) continue;
|
||||
|
||||
const gunXps = new Map<number, number>();
|
||||
const turretXps = new Map<number, number>();
|
||||
const engineXps = new Map<number, number>();
|
||||
const trackXps = new Map<number, number>();
|
||||
const tank = tankList.root[tankKey];
|
||||
const tankPrice: TankDefinitionPrice =
|
||||
typeof tank.price === 'number'
|
||||
@@ -427,6 +469,7 @@ export async function definitions(production: boolean) {
|
||||
});
|
||||
const crew: Crew[] = [];
|
||||
const fixedCamouflage = tankTags.includes('eventCamouflage_user');
|
||||
const totalUnlocks: UnlocksListing[] = [];
|
||||
|
||||
Object.entries(tankDefinition.root.crew).forEach(([key, value]) => {
|
||||
let entry: Crew;
|
||||
@@ -473,7 +516,8 @@ export async function definitions(production: boolean) {
|
||||
forwards: tankDefinition.root.speedLimits.forward,
|
||||
backwards: tankDefinition.root.speedLimits.backward,
|
||||
},
|
||||
equipment,
|
||||
equipment:
|
||||
typeof equipment === 'string' ? equipment : equipment.at(-1)!,
|
||||
consumables: tankDefinition.root.consumableSlots,
|
||||
provisions: tankDefinition.root.provisionSlots,
|
||||
name:
|
||||
@@ -531,9 +575,11 @@ export async function definitions(production: boolean) {
|
||||
.split(' ')
|
||||
.map(Number) as Vector3Tuple;
|
||||
|
||||
totalUnlocks.push(track.unlocks);
|
||||
tankDefinitions[tankId].tracks.push({
|
||||
id: trackId,
|
||||
weight: track.weight,
|
||||
name: strings[track.userString],
|
||||
traverseSpeed: track.rotationSpeed,
|
||||
dispersion: {
|
||||
move: track.shotDispersionFactors.vehicleMovement,
|
||||
@@ -557,16 +603,18 @@ export async function definitions(production: boolean) {
|
||||
});
|
||||
|
||||
Object.keys(tankDefinition.root.engines).forEach((engineKey) => {
|
||||
const engine = enginesList.root.shared[engineKey];
|
||||
const engine = tankDefinition.root.engines[engineKey];
|
||||
const engineListEntry = enginesList.root.shared[engineKey];
|
||||
const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]);
|
||||
|
||||
totalUnlocks.push(engine.unlocks);
|
||||
tankDefinitions[tankId].engines.push({
|
||||
id: engineId,
|
||||
name: strings[engine.userString],
|
||||
fireChance: engine.fireStartingChance,
|
||||
tier: engine.level as Tier,
|
||||
weight: engine.weight,
|
||||
power: engine.power,
|
||||
name: strings[engineListEntry.userString],
|
||||
fireChance: engineListEntry.fireStartingChance,
|
||||
tier: engineListEntry.level as Tier,
|
||||
weight: engineListEntry.weight,
|
||||
power: engineListEntry.power,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -593,6 +641,7 @@ export async function definitions(production: boolean) {
|
||||
.map(Number) as Vector3Tuple;
|
||||
const turretArmor: ModelArmor = { thickness: {} };
|
||||
|
||||
totalUnlocks.push(turret.unlocks);
|
||||
Object.keys(turret.armor)
|
||||
.filter((name) => name.startsWith('armor_'))
|
||||
.forEach((name) => {
|
||||
@@ -645,11 +694,11 @@ export async function definitions(production: boolean) {
|
||||
};
|
||||
|
||||
Object.keys(turret.guns).forEach((gunKey, gunIndex) => {
|
||||
const turretGunEntry = turret.guns[gunKey];
|
||||
const gun = turret.guns[gunKey];
|
||||
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
|
||||
const gunListEntry = gunList.root.shared[gunKey];
|
||||
const pitchLimitsRaw =
|
||||
turretGunEntry.pitchLimits ?? gunListEntry.pitchLimits;
|
||||
gun.pitchLimits ?? gunListEntry.pitchLimits;
|
||||
const gunPitch = (
|
||||
typeof pitchLimitsRaw === 'string'
|
||||
? pitchLimitsRaw
|
||||
@@ -658,43 +707,52 @@ export async function definitions(production: boolean) {
|
||||
.split(' ')
|
||||
.map(Number) as [number, number];
|
||||
const gunModel = Number(
|
||||
parsePath(turretGunEntry.models.undamaged).name.split('_')[1],
|
||||
parsePath(gun.models.undamaged).name.split('_')[1],
|
||||
);
|
||||
const gunName =
|
||||
strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' ');
|
||||
const gunType =
|
||||
'clip' in turretGunEntry
|
||||
? turretGunEntry.pumpGunMode
|
||||
'clip' in gun
|
||||
? gun.pumpGunMode
|
||||
? 'autoReloader'
|
||||
: 'autoLoader'
|
||||
: 'regular';
|
||||
const gunReload =
|
||||
gunType === 'autoReloader'
|
||||
? turretGunEntry.pumpGunReloadTimes!.split(' ').map(Number)
|
||||
: turretGunEntry.reloadTime;
|
||||
? gun.pumpGunReloadTimes!.split(' ').map(Number)
|
||||
: gun.reloadTime;
|
||||
const gunClipCount =
|
||||
gunType === 'regular' ? undefined : turretGunEntry.clip!.count;
|
||||
gunType === 'regular' ? undefined : gun.clip!.count;
|
||||
const gunIntraClip =
|
||||
gunType === 'regular'
|
||||
? undefined
|
||||
: 60 / turretGunEntry.clip!.rate;
|
||||
const front = turretGunEntry.extraPitchLimits?.front
|
||||
? turretGunEntry.extraPitchLimits.front.split(' ').map(Number)
|
||||
gunType === 'regular' ? undefined : 60 / gun.clip!.rate;
|
||||
const front = gun.extraPitchLimits?.front
|
||||
? gun.extraPitchLimits.front.split(' ').map(Number)
|
||||
: undefined;
|
||||
const back = turretGunEntry.extraPitchLimits?.back
|
||||
? turretGunEntry.extraPitchLimits.back.split(' ').map(Number)
|
||||
const back = gun.extraPitchLimits?.back
|
||||
? gun.extraPitchLimits.back.split(' ').map(Number)
|
||||
: undefined;
|
||||
const transition = turretGunEntry.extraPitchLimits?.transition
|
||||
? typeof turretGunEntry.extraPitchLimits.transition === 'number'
|
||||
? turretGunEntry.extraPitchLimits.transition
|
||||
: turretGunEntry.extraPitchLimits.transition.at(-1)!
|
||||
const transition = gun.extraPitchLimits?.transition
|
||||
? typeof gun.extraPitchLimits.transition === 'number'
|
||||
? gun.extraPitchLimits.transition
|
||||
: gun.extraPitchLimits.transition.at(-1)!
|
||||
: undefined;
|
||||
const gunArmor: ModelArmor = { thickness: {} };
|
||||
const shotDispersionFactors =
|
||||
turretGunEntry.shotDispersionFactors ??
|
||||
gunListEntry.shotDispersionFactors;
|
||||
gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors;
|
||||
const maskName = `gun_${gunModel.toString().padStart(2, '0')}`;
|
||||
const maskEnabled =
|
||||
tankParameters.maskSlice?.[maskName]?.enabled ?? false;
|
||||
let mask: number | undefined;
|
||||
|
||||
Object.keys(turretGunEntry.armor)
|
||||
if (maskEnabled) {
|
||||
const maskRaw = tankParameters.maskSlice![maskName]!;
|
||||
mask = maskRaw.planePosition[1];
|
||||
} else {
|
||||
mask = undefined;
|
||||
}
|
||||
|
||||
totalUnlocks.push(gun.unlocks);
|
||||
Object.keys(gun.armor)
|
||||
.filter((name) => name.startsWith('armor_'))
|
||||
.forEach((name) => {
|
||||
const armorIdString = name.match(/armor_(\d+)/)?.[1];
|
||||
@@ -702,7 +760,7 @@ export async function definitions(production: boolean) {
|
||||
throw new SyntaxError(`Invalid armor id: ${name}`);
|
||||
}
|
||||
const armorId = parseInt(armorIdString);
|
||||
const armorRaw = turretGunEntry.armor[name];
|
||||
const armorRaw = gun.armor[name];
|
||||
if (typeof armorRaw === 'number') {
|
||||
gunArmor.thickness[armorId] = armorRaw;
|
||||
} else {
|
||||
@@ -724,13 +782,13 @@ export async function definitions(production: boolean) {
|
||||
count: gunClipCount,
|
||||
intraClip: gunIntraClip,
|
||||
camouflageLoss:
|
||||
typeof turretGunEntry.invisibilityFactorAtShot === 'number'
|
||||
? turretGunEntry.invisibilityFactorAtShot
|
||||
: turretGunEntry.invisibilityFactorAtShot.at(-1)!,
|
||||
aimTime: turretGunEntry.aimingTime ?? gunListEntry.aimingTime,
|
||||
typeof gun.invisibilityFactorAtShot === 'number'
|
||||
? gun.invisibilityFactorAtShot
|
||||
: gun.invisibilityFactorAtShot.at(-1)!,
|
||||
aimTime: gun.aimingTime ?? gunListEntry.aimingTime,
|
||||
dispersion: {
|
||||
base:
|
||||
turretGunEntry.shotDispersionRadius ??
|
||||
gun.shotDispersionRadius ??
|
||||
gunListEntry.shotDispersionRadius,
|
||||
damaged: shotDispersionFactors.whileGunDamaged,
|
||||
shot: shotDispersionFactors.afterShot,
|
||||
@@ -741,12 +799,13 @@ export async function definitions(production: boolean) {
|
||||
modelDefinitions[tankId].turrets[turretId].guns[gunId] = {
|
||||
armor: gunArmor,
|
||||
model: gunModel,
|
||||
mask,
|
||||
thickness:
|
||||
turretGunEntry.armor.gun === undefined
|
||||
gun.armor.gun === undefined
|
||||
? 0
|
||||
: typeof turretGunEntry.armor.gun === 'number'
|
||||
? turretGunEntry.armor.gun
|
||||
: turretGunEntry.armor.gun['#text'],
|
||||
: typeof gun.armor.gun === 'number'
|
||||
? gun.armor.gun
|
||||
: gun.armor.gun['#text'],
|
||||
pitch: {
|
||||
min: gunPitch[0],
|
||||
max: gunPitch[1],
|
||||
@@ -793,7 +852,7 @@ export async function definitions(production: boolean) {
|
||||
caliber: shell.caliber,
|
||||
normalization: shell.normalizationAngle,
|
||||
ricochet: shell.ricochetAngle,
|
||||
type: blitzShellKindToBLitzkrieg[shell.kind],
|
||||
type: blitzShellKindToBlitzkit[shell.kind],
|
||||
explosionRadius:
|
||||
shell.kind === 'HIGH_EXPLOSIVE'
|
||||
? shell.explosionRadius ?? 0
|
||||
@@ -808,10 +867,100 @@ export async function definitions(production: boolean) {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
totalUnlocks.forEach((unlocks) => {
|
||||
if (unlocks === undefined) return;
|
||||
|
||||
Object.entries(unlocks).forEach(([key, value]) => {
|
||||
(Array.isArray(value) ? value : [value]).forEach((vehicle) => {
|
||||
switch (key as keyof Unlocks) {
|
||||
case 'vehicle': {
|
||||
const tankListEntry = tankList.root[vehicle['#text']];
|
||||
const currentTank = tankDefinitions[tankId];
|
||||
const successorId = toUniqueId(nation, tankListEntry.id);
|
||||
|
||||
tankXps.set(successorId, vehicle.cost);
|
||||
|
||||
if (currentTank.successors === undefined) {
|
||||
currentTank.successors = [];
|
||||
}
|
||||
if (!currentTank.successors!.includes(successorId)) {
|
||||
currentTank.successors!.push(successorId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'gun': {
|
||||
gunXps.set(
|
||||
toUniqueId(nation, gunList.root.ids[vehicle['#text']]),
|
||||
vehicle.cost,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'turret': {
|
||||
turretXps.set(
|
||||
toUniqueId(nation, turretList.root.ids[vehicle['#text']]),
|
||||
vehicle.cost,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'engine': {
|
||||
engineXps.set(
|
||||
toUniqueId(nation, enginesList.root.ids[vehicle['#text']]),
|
||||
vehicle.cost,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'chassis': {
|
||||
trackXps.set(
|
||||
toUniqueId(nation, chassisList.root.ids[vehicle['#text']]),
|
||||
vehicle.cost,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(tankDefinitions[tankId].turrets).forEach((turret) => {
|
||||
turret.xp = turretXps.get(turret.id);
|
||||
|
||||
Object.values(turret.guns).forEach((gun) => {
|
||||
gun.xp = gunXps.get(gun.id);
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(tankDefinitions[tankId].engines).forEach((engine) => {
|
||||
engine.xp = engineXps.get(engine.id);
|
||||
});
|
||||
|
||||
Object.values(tankDefinitions[tankId].tracks).forEach((track) => {
|
||||
track.xp = trackXps.get(track.id);
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Object.values(tankDefinitions).forEach((tank) => {
|
||||
tank.xp = tankXps.get(tank.id);
|
||||
});
|
||||
|
||||
Object.values(tankDefinitions).forEach((tank) => {
|
||||
tank.successors?.forEach((predecessorId) => {
|
||||
if (tankDefinitions[predecessorId].ancestors === undefined) {
|
||||
tankDefinitions[predecessorId].ancestors = [];
|
||||
}
|
||||
|
||||
if (!tankDefinitions[predecessorId].ancestors?.includes(tank.id)) {
|
||||
tankDefinitions[predecessorId].ancestors?.push(tank.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(optionalDevices.root).forEach(
|
||||
([optionalDeviceKey, optionalDeviceEntry]) => {
|
||||
if (optionalDeviceKey === 'nextAvailableId') return;
|
||||
@@ -969,9 +1118,18 @@ export async function definitions(production: boolean) {
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(avatar.root.skillsByClasses).forEach(([tankClass, skills]) => {
|
||||
skillDefinitions.classes[tankClass as TankClass] = skills.split(' ');
|
||||
});
|
||||
|
||||
await commitAssets(
|
||||
'definitions',
|
||||
[
|
||||
{
|
||||
content: superCompress(gameDefinitions),
|
||||
encoding: 'base64',
|
||||
path: 'definitions/game.cdon.lz4',
|
||||
},
|
||||
{
|
||||
content: superCompress(tankDefinitions),
|
||||
encoding: 'base64',
|
||||
@@ -997,6 +1155,11 @@ export async function definitions(production: boolean) {
|
||||
encoding: 'base64',
|
||||
path: 'definitions/provisions.cdon.lz4',
|
||||
},
|
||||
{
|
||||
content: superCompress(skillDefinitions),
|
||||
encoding: 'base64',
|
||||
path: 'definitions/skills.cdon.lz4',
|
||||
},
|
||||
],
|
||||
production,
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@ 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 { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
import { OptionalDevices } from './definitions';
|
||||
|
||||
|
||||
@@ -14,21 +14,26 @@ import { boosterIcons } from './boosterIcons';
|
||||
import { camouflageIcons } from './camouflageIcons';
|
||||
import { circleFlags } from './circleFlags';
|
||||
import { consumableProvisionIcons } from './consumableProvisionIcons';
|
||||
import { currencies } from './currencies';
|
||||
import { definitions } from './definitions';
|
||||
import { equipmentIcons } from './equipmentIcons';
|
||||
import { moduleIcons } from './moduleIcons';
|
||||
import { scratchedFlags } from './scratchedFlags';
|
||||
import { shellIcons } from './shellIcons';
|
||||
import { skillIcons } from './skillIcons';
|
||||
import { tankArmors } from './tankArmors';
|
||||
import { tankIcons } from './tankIcons';
|
||||
import { tankModels } from './tankModels';
|
||||
import { videos } from './videos';
|
||||
|
||||
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||
|
||||
const methods = [
|
||||
definitions,
|
||||
tankIcons,
|
||||
videos,
|
||||
scratchedFlags,
|
||||
currencies,
|
||||
skillIcons,
|
||||
circleFlags,
|
||||
shellIcons,
|
||||
moduleIcons,
|
||||
@@ -36,6 +41,7 @@ const methods = [
|
||||
consumableProvisionIcons,
|
||||
camouflageIcons,
|
||||
boosterIcons,
|
||||
tankIcons,
|
||||
tankArmors,
|
||||
tankModels,
|
||||
];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
|
||||
export async function moduleIcons(production: boolean) {
|
||||
@@ -16,9 +17,11 @@ export async function moduleIcons(production: boolean) {
|
||||
.map(async (file) => {
|
||||
const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1];
|
||||
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
|
||||
const content = await readBase64DVPL(
|
||||
`${DATA}/${POI.moduleIcons}/${file}`,
|
||||
);
|
||||
const content = (
|
||||
await sharp(await readDVPLFile(`${DATA}/${POI.moduleIcons}/${file}`))
|
||||
.trim()
|
||||
.toBuffer()
|
||||
).toString('base64');
|
||||
|
||||
return {
|
||||
content,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
|
||||
export async function scratchedFlags(production: boolean) {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
|
||||
export async function shellIcons(production: boolean) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import sharp from 'sharp';
|
||||
import { TankClass } from '../../src/components/Tanks';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA } from './constants';
|
||||
|
||||
interface SkillIcon {
|
||||
name: string;
|
||||
state: number;
|
||||
}
|
||||
|
||||
export interface Avatar {
|
||||
roles: unknown;
|
||||
skillsByClasses: Record<TankClass, string>;
|
||||
skills: {
|
||||
[name: string]: {
|
||||
userString: string;
|
||||
effectDescription: string;
|
||||
tipDescription: string;
|
||||
icon: SkillIcon | SkillIcon[];
|
||||
type: 'continuous' | 'trigger';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function skillIcons(production: boolean) {
|
||||
console.log('Building skill icons...');
|
||||
|
||||
const avatar = await readXMLDVPL<{ root: Avatar }>(
|
||||
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
|
||||
);
|
||||
const changes = await Promise.all(
|
||||
Object.values(avatar.root.skills).map(async (skill) => {
|
||||
const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon;
|
||||
const name = icon.name.split('/').at(-1)!.replace(/_\d$/, '');
|
||||
const path = `${DATA}${icon.name.replace('~res:', '')}.packed.webp.dvpl`;
|
||||
const image = sharp(await readDVPLFile(path)).trim();
|
||||
const content = (await image.toBuffer()).toString('base64');
|
||||
|
||||
return {
|
||||
content,
|
||||
encoding: 'base64',
|
||||
path: `icons/skills/${name}.webp`,
|
||||
} satisfies FileChange;
|
||||
}),
|
||||
);
|
||||
|
||||
commitAssets('skill icons', changes, production);
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import { readdir } from 'fs/promises';
|
||||
import { extractArmor } from '../../src/core/blitz/extractArmor';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
import { VehicleDefinitionList } from './definitions';
|
||||
|
||||
@@ -28,9 +28,6 @@ export async function tankArmors(production: boolean) {
|
||||
if (tankKey.includes('tutorial_bot')) return;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
console.log(`Building armor ${id} @ ${nation}/${tankKey}`);
|
||||
|
||||
const model = await extractArmor(DATA, `${nation}-${tankKey}`);
|
||||
|
||||
changes.push({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { Vector3Tuple } from 'three';
|
||||
import { NATION_IDS } from '../../src/constants/nations';
|
||||
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
|
||||
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
import { VehicleDefinitionList } from './definitions';
|
||||
|
||||
@@ -25,6 +26,20 @@ export interface TankParameters {
|
||||
averageThickness?: Record<string, number>;
|
||||
};
|
||||
};
|
||||
maskSlice?: {
|
||||
[key: string]:
|
||||
| {
|
||||
enabled: boolean;
|
||||
planePosition: Vector3Tuple;
|
||||
planeNormal: Vector3Tuple;
|
||||
planeAxis: Vector3Tuple;
|
||||
frustumConeBaseRadius: number;
|
||||
frustumConeAngleDegrees: number;
|
||||
planeSliceMode: number;
|
||||
frustumConeSliceMode: number;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export async function tankIcons(production: boolean) {
|
||||
@@ -50,23 +65,29 @@ export async function tankIcons(production: boolean) {
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
|
||||
);
|
||||
const small = `${DATA}/${parameters.resourcesPath.smallIconPath
|
||||
const smallPath = `${DATA}/${parameters.resourcesPath.smallIconPath
|
||||
.replace(/~res:\//, '')
|
||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||
const big = `${DATA}/${parameters.resourcesPath.bigIconPath
|
||||
const bigPath = `${DATA}/${parameters.resourcesPath.bigIconPath
|
||||
.replace(/~res:\//, '')
|
||||
.replace(/\..+/, '')}.packed.webp.dvpl`;
|
||||
const big = await sharp(await readDVPLFile(bigPath))
|
||||
.trim()
|
||||
.toBuffer();
|
||||
const small = await sharp(await readDVPLFile(smallPath))
|
||||
.trim()
|
||||
.toBuffer();
|
||||
|
||||
if (big) {
|
||||
changes.push({
|
||||
content: await readBase64DVPL(big),
|
||||
content: big.toString('base64'),
|
||||
encoding: 'base64',
|
||||
path: `icons/tanks/big/${id}.webp`,
|
||||
});
|
||||
}
|
||||
if (small) {
|
||||
changes.push({
|
||||
content: await readBase64DVPL(small),
|
||||
content: small.toString('base64'),
|
||||
encoding: 'base64',
|
||||
path: `icons/tanks/small/${id}.webp`,
|
||||
});
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { NodeIO } from '@gltf-transform/core';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { Vector3Tuple } from 'three';
|
||||
import { extractModel } from '../../src/core/blitz/extractModel';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||
// import { AssetCommit } from '../../src/core/github/assetCommit';
|
||||
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
|
||||
import { DATA, POI } from './constants';
|
||||
import { VehicleDefinitionList } from './definitions';
|
||||
import { TankParameters } from './tankIcons';
|
||||
|
||||
interface VehicleCustomization {
|
||||
armorColor: string;
|
||||
}
|
||||
|
||||
export async function tankModels(production: boolean) {
|
||||
console.log('Building tank models...');
|
||||
|
||||
@@ -27,18 +21,9 @@ export async function tankModels(production: boolean) {
|
||||
for (const nationIndex in nations) {
|
||||
const nation = nations[nationIndex];
|
||||
const changes: FileChange[] = [];
|
||||
// const assetCommit = new AssetCommit(`tank models ${nation}`, production);
|
||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
const customization = await readXMLDVPL<{ root: VehicleCustomization }>(
|
||||
`${DATA}/${POI.vehicleDefinitions}/${nation}/customization.xml.dvpl`,
|
||||
);
|
||||
const baseColor = customization.root.armorColor
|
||||
.split(' ')
|
||||
.slice(0, 3)
|
||||
.map(Number)
|
||||
.map((channel) => channel / 255) as Vector3Tuple;
|
||||
|
||||
console.log(`Building models for ${nation}`);
|
||||
|
||||
@@ -47,20 +32,14 @@ export async function tankModels(production: boolean) {
|
||||
if (tankKey.includes('tutorial_bot')) return;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
console.log(`Building model ${id} @ ${nation}/${tankKey}`);
|
||||
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
|
||||
);
|
||||
const model = await extractModel(
|
||||
DATA,
|
||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
||||
baseColor,
|
||||
);
|
||||
|
||||
// writeFile(`test.${tankKey}.glb`, await nodeIO.writeBinary(model));
|
||||
|
||||
changes.push({
|
||||
path: `3d/tanks/models/${id}.glb`,
|
||||
content: Buffer.from(await nodeIO.writeBinary(model)).toString(
|
||||
@@ -71,7 +50,6 @@ export async function tankModels(production: boolean) {
|
||||
}),
|
||||
);
|
||||
|
||||
// assetCommit.push();
|
||||
await commitAssets(`tank models ${nation}`, changes, production);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import { google } from 'googleapis';
|
||||
import { cloneDeep, uniqBy } from 'lodash';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
|
||||
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
|
||||
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
|
||||
import {
|
||||
VideoDefinitions,
|
||||
videoDefinitions,
|
||||
} from '../../src/core/blitzkit/videos';
|
||||
import { DATA, POI } from './constants';
|
||||
import { BlitzStrings, botPattern, VehicleDefinitionList } from './definitions';
|
||||
|
||||
const youtubers = [
|
||||
'UCKBYXp4Xn2I2tL1UL4fpbhw', // droodles
|
||||
'UCrQ-Dy8lVsm11u8pCJ8W7Tw', // fatness
|
||||
];
|
||||
|
||||
export async function videos(production: boolean) {
|
||||
console.log('Building videos...');
|
||||
|
||||
const currentVideos = await videoDefinitions;
|
||||
const auth = await google.auth.getClient({
|
||||
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
|
||||
});
|
||||
const youtube = google.youtube({ version: 'v3', auth });
|
||||
|
||||
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
|
||||
`${DATA}/${POI.strings}/en.yaml.dvpl`,
|
||||
);
|
||||
const stringsCache = await fetch(POI.cachedStrings)
|
||||
.then((response) => response.text())
|
||||
.then((string) => parseYaml(string) as BlitzStrings);
|
||||
const strings = {
|
||||
...stringsPreInstalled,
|
||||
...stringsCache,
|
||||
};
|
||||
const tanks: { id: number; name: string; tier: number }[] = [];
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(tankList.root).map(async ([tankKey, tank]) => {
|
||||
if (botPattern.test(tankKey)) return null;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
const name =
|
||||
(tank.shortUserString
|
||||
? strings[tank.shortUserString]
|
||||
: undefined) ?? strings[tank.userString];
|
||||
const tier = tank.level;
|
||||
|
||||
tanks.push({ id, name, tier });
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const tanksSanitized = tanks
|
||||
.sort((a, b) => b.tier - a.tier)
|
||||
.sort((a, b) => {
|
||||
const lastUpdatedA = currentVideos[a.id]?.lastUpdated ?? 0;
|
||||
const lastUpdatedB = currentVideos[b.id]?.lastUpdated ?? 0;
|
||||
return lastUpdatedA - lastUpdatedB;
|
||||
});
|
||||
const videos: VideoDefinitions = cloneDeep(currentVideos);
|
||||
|
||||
let done = 0;
|
||||
for (const tank of tanksSanitized) {
|
||||
try {
|
||||
const results = await youtube.search.list({
|
||||
part: ['snippet'],
|
||||
q: `World of Tanks Blitz ${tank.name}`,
|
||||
});
|
||||
|
||||
if (!results.data.items || results.data.items.length === 0) continue;
|
||||
|
||||
const items = uniqBy(
|
||||
results.data.items.filter(
|
||||
(item) =>
|
||||
item.snippet?.channelId &&
|
||||
item.id?.videoId &&
|
||||
youtubers.includes(item.snippet.channelId),
|
||||
),
|
||||
(item) => item.snippet!.channelId,
|
||||
);
|
||||
|
||||
videos[tank.id] = {
|
||||
lastUpdated: Math.round(Date.now() / 1000),
|
||||
videos: items.map((item) => item.id!.videoId!),
|
||||
};
|
||||
|
||||
console.log(
|
||||
`${++done} / ${tanks.length} (${Math.round(100 * (done / tanks.length))}%) done; found ${items.length} for ${tank.name}`,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
|
||||
} catch (error) {
|
||||
console.warn('An error occurred, safely exiting prematurely...', error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const content = Object.entries(videos)
|
||||
.map(
|
||||
([id, { videos, lastUpdated }]) =>
|
||||
`${id},${lastUpdated},${videos.join(',')}`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
await commitAssets(
|
||||
'videos',
|
||||
[{ content, encoding: 'utf-8', path: 'definitions/videos.csv' }],
|
||||
production,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { chunk } from 'lodash';
|
||||
import { argv } from 'process';
|
||||
import {
|
||||
RatingInfo,
|
||||
RatingNeighbors,
|
||||
RatingPlayer,
|
||||
} from '../src/commands/ratingLeaderboard';
|
||||
import { RegionSubdomain } from '../src/constants/regions';
|
||||
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
|
||||
import regionSubdomainToRegion from '../src/core/blitz/regionSubdomainToRegion';
|
||||
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../src/core/blitzkit/commitMultipleFiles.js';
|
||||
import { patientFetchJSON } from '../src/core/blitzkit/patientFetchJSON';
|
||||
import { superCompress } from '../src/core/blitzkit/superCompress';
|
||||
import {
|
||||
BkrlBaseEntry,
|
||||
BkrlFormat,
|
||||
BkrlSuperset1Entry,
|
||||
BkrlWriteStream,
|
||||
} from '../src/core/streams/bkrl';
|
||||
import { CdonValue, CdonWriteStream } from '../src/core/streams/cdon';
|
||||
|
||||
/*
|
||||
* 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 * * *.
|
||||
*/
|
||||
|
||||
const production = argv.includes('--production');
|
||||
const target = argv
|
||||
.find((arg) => arg.startsWith('--target='))
|
||||
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
||||
const regionSubdomains = argv
|
||||
.find((arg) => arg.startsWith('--region='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (!target) throw new Error('Target parameter not specified');
|
||||
if (!regionSubdomains) throw new Error('Region parameter not specified');
|
||||
|
||||
regionSubdomains.split(',').forEach(async (regionSubdomain) => {
|
||||
const region = regionSubdomainToRegion(regionSubdomain as RegionSubdomain);
|
||||
|
||||
console.log(`Building ${target} rating leaderboard for ${region}`);
|
||||
|
||||
const NEIGHBORS = 2 ** 8;
|
||||
const PLAYERS = await patientFetchJSON<RatingInfo>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).then((data) => (data.detail === undefined ? data.count : undefined));
|
||||
|
||||
if (PLAYERS === undefined) {
|
||||
throw new Error('No current rating season running');
|
||||
}
|
||||
|
||||
const players: Record<number, RatingPlayer> = {};
|
||||
const leaderboard: BkrlBaseEntry[] = [];
|
||||
let registered = 0;
|
||||
|
||||
async function branchFromPlayer(id: number) {
|
||||
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||
);
|
||||
|
||||
const firstPlayer = neighbors[0];
|
||||
const lastPlayer = neighbors[neighbors.length - 1];
|
||||
|
||||
const branches: Promise<void>[] = [];
|
||||
if (!players[firstPlayer.number])
|
||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||
if (!players[lastPlayer.number])
|
||||
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
||||
|
||||
neighbors.forEach((neighbor) => {
|
||||
if (players[neighbor.number]) return;
|
||||
|
||||
players[neighbor.number] = neighbor;
|
||||
registered++;
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Registered ${registered} / ${PLAYERS} (${(
|
||||
(registered / PLAYERS!) *
|
||||
100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result.at(-1)!;
|
||||
|
||||
await Promise.all([
|
||||
branchFromPlayer(firstPlayer.spa_id),
|
||||
branchFromPlayer(lastPlayer.spa_id),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Branching from leagues...');
|
||||
await Promise.all([
|
||||
branchFromLeague(0),
|
||||
branchFromLeague(1),
|
||||
branchFromLeague(2),
|
||||
branchFromLeague(3),
|
||||
branchFromLeague(4),
|
||||
]);
|
||||
|
||||
console.log('Converting to an array...');
|
||||
for (let index = 0; index < PLAYERS; index++) {
|
||||
const listing = players[index + 1];
|
||||
if (listing) {
|
||||
leaderboard[index] = { id: listing.spa_id, score: listing.score };
|
||||
} else {
|
||||
console.warn(`Position ${index + 1} had no player`);
|
||||
leaderboard[index] = { id: 0, score: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Requesting further stats...');
|
||||
const comprehensiveLeaderboard: BkrlSuperset1Entry[] = [];
|
||||
let chunkIndex = 0;
|
||||
for (const leaderboardChunk of chunk(leaderboard, 100)) {
|
||||
console.log(
|
||||
`Chunk ${chunkIndex + 1} of ${Math.ceil(leaderboard.length / 100)}`,
|
||||
);
|
||||
|
||||
const stats = await getAccountInfo(
|
||||
region,
|
||||
leaderboardChunk.map(({ id }) => id),
|
||||
['statistics.rating'],
|
||||
);
|
||||
|
||||
stats.forEach((stat, index) => {
|
||||
const trueIndex = chunkIndex * 100 + index;
|
||||
comprehensiveLeaderboard[trueIndex] =
|
||||
leaderboardChunk[index].id === 0 ||
|
||||
stat?.statistics.rating === undefined
|
||||
? {
|
||||
battles: 0,
|
||||
damage: 0,
|
||||
id: 0,
|
||||
kills: 0,
|
||||
score: 0,
|
||||
survived: 0,
|
||||
wins: 0,
|
||||
}
|
||||
: {
|
||||
id: leaderboardChunk[index].id,
|
||||
score: leaderboardChunk[index].score,
|
||||
battles: stat.statistics.rating.battles,
|
||||
damage: stat.statistics.rating.damage_dealt,
|
||||
kills: stat.statistics.rating.frags,
|
||||
survived: stat.statistics.rating.survived_battles,
|
||||
wins: stat.statistics.rating.wins,
|
||||
};
|
||||
});
|
||||
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
);
|
||||
const normalizedServer = regionSubdomain === 'na' ? 'com' : regionSubdomain;
|
||||
const infoContent = new CdonWriteStream().cdon(
|
||||
info as unknown as CdonValue,
|
||||
).uint8Array;
|
||||
const leaderboardWriteStream = new BkrlWriteStream();
|
||||
|
||||
leaderboardWriteStream.bkrl({
|
||||
format: BkrlFormat.Superset1,
|
||||
entries: comprehensiveLeaderboard,
|
||||
});
|
||||
|
||||
const changes: FileChange[] = [
|
||||
{
|
||||
path: `regions/${normalizedServer}/rating/${info.current_season}/info.cdon.lz4`,
|
||||
content: superCompress(infoContent),
|
||||
encoding: 'base64',
|
||||
},
|
||||
{
|
||||
path: `regions/${normalizedServer}/rating/${info.current_season}/${target}.bkrl`,
|
||||
content: Buffer.from(leaderboardWriteStream.uint8Array).toString(
|
||||
'base64',
|
||||
),
|
||||
encoding: 'base64',
|
||||
},
|
||||
];
|
||||
|
||||
commitAssets(
|
||||
`rating leaderboard ${regionSubdomain} ${target}`,
|
||||
changes,
|
||||
production,
|
||||
);
|
||||
});
|
||||
@@ -1,140 +0,0 @@
|
||||
import { rm, writeFile } from 'fs/promises';
|
||||
import { argv } from 'process';
|
||||
import {
|
||||
RatingsInfo,
|
||||
RatingsNeighbors,
|
||||
RatingsPlayer,
|
||||
} from '../src/commands/ratings';
|
||||
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 * * *.
|
||||
*
|
||||
* [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 BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
|
||||
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
|
||||
|
||||
const publish = argv.includes('--publish');
|
||||
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 = 2 ** 8;
|
||||
const PLAYERS = await patientFetchJSON<RatingsInfo>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).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 patientFetchJSON<RatingsNeighbors>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||
);
|
||||
|
||||
const firstPlayer = neighbors[0];
|
||||
const lastPlayer = neighbors[neighbors.length - 1];
|
||||
|
||||
const branches: Promise<void>[] = [];
|
||||
if (!players[firstPlayer.number])
|
||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||
if (!players[lastPlayer.number])
|
||||
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
||||
|
||||
neighbors.forEach((neighbor) => {
|
||||
if (players[neighbor.number]) return;
|
||||
|
||||
players[neighbor.number] = neighbor;
|
||||
registered++;
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Registered ${registered} / ${PLAYERS} (${(
|
||||
(registered / PLAYERS!) *
|
||||
100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result[result.length - 1];
|
||||
|
||||
await Promise.all([
|
||||
branchFromPlayer(firstPlayer.spa_id),
|
||||
branchFromPlayer(lastPlayer.spa_id),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Branching from leagues...');
|
||||
await Promise.all([
|
||||
branchFromLeague(0),
|
||||
branchFromLeague(1),
|
||||
branchFromLeague(2),
|
||||
branchFromLeague(3),
|
||||
branchFromLeague(4),
|
||||
]);
|
||||
|
||||
console.log('Converting to an array...');
|
||||
for (let index = 0; index < PLAYERS; index++) {
|
||||
const listing = players[index + 1];
|
||||
if (!listing) {
|
||||
console.warn(`Position ${index + 1} had no player`);
|
||||
continue;
|
||||
}
|
||||
|
||||
leaderboard[index] = {
|
||||
id: listing.spa_id,
|
||||
score: listing.score,
|
||||
};
|
||||
}
|
||||
|
||||
const leaderboardJSON = JSON.stringify(leaderboard);
|
||||
|
||||
if (publish) {
|
||||
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
);
|
||||
const normalizedServer = server === 'na' ? 'com' : server;
|
||||
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(
|
||||
'tresabhi',
|
||||
'blitzkrieg-assets',
|
||||
'main',
|
||||
new Date().toString(),
|
||||
[
|
||||
{ path: infoPath, content: infoJSON, encoding: 'utf-8' },
|
||||
{ path: leaderboardPath, content: leaderboardJSON, encoding: 'utf-8' },
|
||||
],
|
||||
);
|
||||
} else {
|
||||
console.log('Writing to disk...');
|
||||
await rm('dist.json', { force: true });
|
||||
await writeFile('dist.json', leaderboardJSON);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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,26 @@
|
||||
import { tankDefinitions } from '../src/core/blitzkit/tankDefinitions';
|
||||
|
||||
// random vercel bug forces require import
|
||||
const { writeFileSync } = require('fs');
|
||||
|
||||
console.log('Building sitemap...');
|
||||
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const values = Object.values(awaitedTankDefinitions);
|
||||
const txt = values
|
||||
.map((tank) => `https://blitzkit.vercel.app/tools/tankopedia/${tank.id}/`)
|
||||
.join('\n');
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
${values
|
||||
.map(
|
||||
(tank) =>
|
||||
`<url><loc>https://blitzkit.vercel.app/tools/tankopedia/${tank.id}/</loc></url>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</urlset>`;
|
||||
|
||||
writeFileSync('public/sitemap.txt', txt);
|
||||
writeFileSync('public/sitemap.xml', xml);
|
||||
|
||||
console.log(`Built sitemap for ${values.length} tanks`);
|
||||
@@ -0,0 +1,96 @@
|
||||
import { chunk, times } from 'lodash';
|
||||
import { compress } from 'lz4js';
|
||||
import { argv } from 'process';
|
||||
import { Region, REGIONS } from '../src/constants/regions';
|
||||
import getTankStats from '../src/core/blitz/getTankStats';
|
||||
import { idToRegion } from '../src/core/blitz/idToRegion';
|
||||
import { commitAssets } from '../src/core/blitzkit/commitAssets';
|
||||
import { FileChange } from '../src/core/blitzkit/commitMultipleFiles';
|
||||
import {
|
||||
RtscWritePlayer,
|
||||
RtscWriteStream,
|
||||
RtsmWriteStream,
|
||||
} from '../src/core/streams/rts';
|
||||
import usersRaw from '../test.users.json';
|
||||
|
||||
const PLAYERS_PER_CHUNK = 256;
|
||||
const CHUNKS_PER_BATCH = 128;
|
||||
|
||||
const production = argv.includes('--production');
|
||||
const region = argv
|
||||
.find((argument) => argument.startsWith('--region'))
|
||||
?.split('=')[1] as Region | undefined;
|
||||
|
||||
if (!region || !REGIONS.includes(region)) {
|
||||
throw new Error('No valid region specified');
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const todayPath = `${today.getUTCFullYear()}/${today.getUTCMonth() + 1}/${today.getUTCDate()}`;
|
||||
const users = usersRaw.filter(({ blitz }) => idToRegion(blitz) === region);
|
||||
const base = Math.ceil(users.length / PLAYERS_PER_CHUNK);
|
||||
const chunks = times(base, (offset) => ({
|
||||
offset,
|
||||
users: users.filter((user) => user.blitz % base === offset),
|
||||
}));
|
||||
const batches = chunk(chunks, CHUNKS_PER_BATCH);
|
||||
const manifestWriteStream = new RtsmWriteStream();
|
||||
|
||||
console.log(`Found ${users.length} players in ${region}`);
|
||||
|
||||
await commitAssets(
|
||||
`regular tank stats manifests ${todayPath}`,
|
||||
[
|
||||
{
|
||||
encoding: 'base64',
|
||||
path: `regions/${region}/regular/tanks/${todayPath}/manifest.rtsm`,
|
||||
content: Buffer.from(
|
||||
compress(manifestWriteStream.rtsm(base).uint8Array),
|
||||
).toString('base64'),
|
||||
},
|
||||
],
|
||||
production,
|
||||
);
|
||||
|
||||
let batchesDone = 0;
|
||||
for (const batch of batches) {
|
||||
console.log(`Batch ${++batchesDone} of ${batches.length}`);
|
||||
|
||||
const changes: FileChange[] = [];
|
||||
|
||||
let chunksDone = 0;
|
||||
for (const chunk of batch) {
|
||||
console.log(`Chunk ${++chunksDone} of ${chunks.length}`);
|
||||
|
||||
let playersDone = 0;
|
||||
const players = await Promise.all(
|
||||
chunk.users.map(async (user) => {
|
||||
const tanks = await getTankStats(region, user.blitz);
|
||||
|
||||
console.log(`Player ${++playersDone} of ${chunk.users.length}`);
|
||||
|
||||
if (tanks === null) return null;
|
||||
|
||||
return {
|
||||
id: user.blitz,
|
||||
tanks,
|
||||
} satisfies RtscWritePlayer;
|
||||
}),
|
||||
).then((players) => players.filter((player) => player !== null));
|
||||
const writeStream = new RtscWriteStream();
|
||||
|
||||
writeStream.rtsc(players);
|
||||
changes.push({
|
||||
encoding: 'base64',
|
||||
content: Buffer.from(compress(writeStream.uint8Array)).toString('base64'),
|
||||
path: `regions/${region}/regular/tanks/${todayPath}/${chunk.offset}.rtsc.lz4`,
|
||||
});
|
||||
}
|
||||
|
||||
// do not await; commit in background
|
||||
commitAssets(
|
||||
`regular tank stats ${todayPath} batch ${batches.indexOf(batch) + 1}`,
|
||||
changes,
|
||||
production,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "preserve",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"incremental": true,
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../constants/regions';
|
||||
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
|
||||
import getRatingInfo from '../../../../../../core/blitz/getRatingInfo';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region } },
|
||||
) {
|
||||
return NextResponse.json(await getRatingsInfo(params.region));
|
||||
return NextResponse.json(await getRatingInfo(params.region));
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../../constants/regions';
|
||||
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
|
||||
import { getRatingLeague } from '../../../../../../../core/blitz/getRatingLeague';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region; league: number } },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
await getRatingsLeague(params.region, params.league, true),
|
||||
await getRatingLeague(params.region, params.league, true),
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../../constants/regions';
|
||||
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
|
||||
import { searchCurrentRatingPlayers } from '../../../../../../../core/blitz/searchCurrentRatingPlayers';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region; username: string } },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
await searchCurrentRatingsPlayers(params.region, params.username),
|
||||
await searchCurrentRatingPlayers(params.region, params.username),
|
||||
);
|
||||
}
|
||||
@@ -3,20 +3,17 @@
|
||||
import { ContextMenu } from '@radix-ui/themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as Breakdown from '../../../components/Breakdown';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||
import { resetSession } from '../../../core/blitzkit/resetSession';
|
||||
import {
|
||||
TankDefinition,
|
||||
tankDefinitions,
|
||||
} from '../../../core/blitzkrieg/tankDefinitions';
|
||||
} from '../../../core/blitzkit/tankDefinitions';
|
||||
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 mutateSession, {
|
||||
resetSession,
|
||||
useSession,
|
||||
} from '../../../stores/session';
|
||||
import mutateSession, { useSession } from '../../../stores/session';
|
||||
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
||||
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
||||
import { Menu } from '../../tools/session/components/Menu';
|
||||
@@ -90,10 +87,10 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
|
||||
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<
|
||||
const careerRaw = await fetchBlitz<TanksStats>(region, 'tanks/stats', {
|
||||
account_id: id,
|
||||
});
|
||||
const career = careerRaw[id]!.reduce<
|
||||
Record<number, IndividualTankStats>
|
||||
>(
|
||||
(accumulator, curr) => ({
|
||||
@@ -105,7 +102,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const awaitedTankAverages = await tankAverages;
|
||||
const careerWN8 =
|
||||
careerRaw[id].reduce(
|
||||
careerRaw[id]!.reduce(
|
||||
(accumulator, { tank_id, all }) =>
|
||||
accumulator +
|
||||
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||
@@ -114,7 +111,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
|
||||
: 0),
|
||||
0,
|
||||
) /
|
||||
careerRaw[id].reduce(
|
||||
careerRaw[id]!.reduce(
|
||||
(accumulator, { tank_id, all }) =>
|
||||
accumulator +
|
||||
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { AlertDialog, Button, Flex, Text, Theme } from '@radix-ui/themes';
|
||||
import { AlertDialog, Button, Flex, Theme } from '@radix-ui/themes';
|
||||
import '@radix-ui/themes/styles.css';
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
import { config } from 'dotenv';
|
||||
import { Roboto_Flex } from 'next/font/google';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { SkyLine } from '../assets/art/SkyLine';
|
||||
import { ReactNode, Suspense, useEffect, useState } from 'react';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { Loader } from '../components/Loader';
|
||||
import Navbar from '../components/Navbar';
|
||||
import PageWrapper from '../components/PageWrapper';
|
||||
import isDev from '../core/blitzkrieg/isDev';
|
||||
import { isLocalhost } from '../core/blitzkrieg/isLocalhost';
|
||||
import { Party3 } from '../components/Party3';
|
||||
import isDev from '../core/blitzkit/isDev';
|
||||
import { isLocalhost } from '../core/blitzkit/isLocalhost';
|
||||
import { useFullScreen } from '../hooks/useFullScreen';
|
||||
import { useApp } from '../stores/app';
|
||||
import { mintTheme } from '../themes/mint';
|
||||
|
||||
config();
|
||||
|
||||
@@ -32,7 +31,10 @@ const robotoFlex = Roboto_Flex({
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const isEmbed = pathname.split('/')[1] === 'embeds';
|
||||
const isRoot = pathname === '/';
|
||||
const darkMode = useApp((state) => state.darkMode);
|
||||
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
|
||||
const isFullScreen = useFullScreen();
|
||||
|
||||
useEffect(() => {
|
||||
setShowDevBuildAlert(
|
||||
@@ -46,116 +48,80 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<html lang="en" className={robotoFlex.className}>
|
||||
<head>
|
||||
<title>Blitzkrieg</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="🎉 Tools for everything World of Tanks Blitz"
|
||||
/>
|
||||
{isRoot && (
|
||||
<>
|
||||
<title>BlitzKit</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="🎉 Tools for everything World of Tanks Blitz"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<meta property="og:site_name" content="BlitzKit" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
|
||||
<Party3 />
|
||||
</head>
|
||||
|
||||
<body
|
||||
style={{
|
||||
margin: 0,
|
||||
paddingTop: isEmbed ? 0 : '3.25rem',
|
||||
backgroundColor: isEmbed ? 'transparent' : undefined,
|
||||
}}
|
||||
>
|
||||
<Analytics />
|
||||
|
||||
<Theme
|
||||
appearance="dark"
|
||||
panelBackground="translucent"
|
||||
appearance={darkMode ? 'dark' : 'light'}
|
||||
panelBackground="solid"
|
||||
radius="full"
|
||||
suppressHydrationWarning
|
||||
suppressContentEditableWarning
|
||||
>
|
||||
{!isEmbed && <Navbar />}
|
||||
<AlertDialog.Root open={showDevBuildAlert}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Title>Experimental version!</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This version may have a lot of issues. Report issues to{' '}
|
||||
<a href="https://discord.gg/nDt7AjGJQH" target="_blank">
|
||||
the official Discord server
|
||||
</a>
|
||||
. Also consider using{' '}
|
||||
<a href="https://blitz-krieg.vercel.app/">
|
||||
the more stable release version
|
||||
</a>
|
||||
. You will be asked again in 8 days.
|
||||
</AlertDialog.Description>
|
||||
<Flex
|
||||
direction="column"
|
||||
style={{ minHeight: '100vh', paddingTop: isEmbed ? undefined : 64 }}
|
||||
>
|
||||
{!isEmbed && <Navbar />}
|
||||
<AlertDialog.Root open={showDevBuildAlert}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Title>Experimental version!</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This version may have a lot of issues. Report issues to{' '}
|
||||
<a href="https://discord.gg/nDt7AjGJQH" target="_blank">
|
||||
the official Discord server
|
||||
</a>
|
||||
. Also consider using{' '}
|
||||
<a href="https://blitzkit.vercel.app/">
|
||||
the more stable release version
|
||||
</a>
|
||||
. You will be asked again in 8 days.
|
||||
</AlertDialog.Description>
|
||||
|
||||
<Flex justify="end">
|
||||
<Button
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
setShowDevBuildAlert(false);
|
||||
useApp.setState({ devBuildAgreementTime: Date.now() });
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<Flex direction="column">{children}</Flex>
|
||||
|
||||
{!isEmbed && (
|
||||
<PageWrapper>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://ko-fi.com/tresabhi"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
style={{
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
backgroundColor: mintTheme.colors.componentInteractive,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="1"
|
||||
style={{
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
<Flex justify="end">
|
||||
<Button
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
setShowDevBuildAlert(false);
|
||||
useApp.setState({ devBuildAgreementTime: Date.now() });
|
||||
}}
|
||||
>
|
||||
<Text size="4" color="mint">
|
||||
Blitzkrieg. Free forever. For everyone.
|
||||
</Text>
|
||||
<Text color="mint">
|
||||
<b>Consider supporting today 🍀</b>
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Text
|
||||
color="mint"
|
||||
style={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
<CaretRightIcon width={32} height={32} />
|
||||
</Flex>
|
||||
</Text>
|
||||
|
||||
<SkyLine
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 'min(8vw, 15%)',
|
||||
top: '5%',
|
||||
}}
|
||||
/>
|
||||
Continue
|
||||
</Button>
|
||||
</Flex>
|
||||
</a>
|
||||
</PageWrapper>
|
||||
)}
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div style={{ flex: 1 }}>
|
||||
<Loader />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Suspense>
|
||||
|
||||
{!isEmbed && !isFullScreen && <Footer />}
|
||||
</Flex>
|
||||
</Theme>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { theme } from '../stitches.config';
|
||||
|
||||
export const tool = styleVariants({
|
||||
enabled: {
|
||||
boxShadow: `inset 0 -256px 512px -128px ${theme.colors.appBackground1}`,
|
||||
boxShadow: `inset 0 -200px 128px -128px ${theme.colors.appBackground1}`,
|
||||
|
||||
':hover': {
|
||||
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
|
||||
|
||||
@@ -1,95 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Text, TextField } from '@radix-ui/themes';
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, Link as LinkRadix } 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 { PatreonIcon } from '../icons/Patreon';
|
||||
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
|
||||
direction="column"
|
||||
gap="5"
|
||||
justify="center"
|
||||
align="center"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<LinkRadix href="https://www.patreon.com/tresabhi" target="_blank">
|
||||
<Button
|
||||
color="ruby"
|
||||
style={{
|
||||
boxShadow: `0 0 8px ${theme.colors.solidBackground_ruby}f0`,
|
||||
}}
|
||||
>
|
||||
<PatreonIcon style={{ width: '1em', height: '1em' }} /> Consider
|
||||
donating on Patreon
|
||||
</Button>
|
||||
</LinkRadix>
|
||||
|
||||
<Flex gap="3" wrap="wrap" align="center" justify="center">
|
||||
<Search
|
||||
input={input}
|
||||
list={TOOLS.map(
|
||||
(tool) =>
|
||||
({
|
||||
node: (
|
||||
<Link
|
||||
href={tool.href ?? `/tools/${tool.id}`}
|
||||
target={tool.href ? '_blank' : undefined}
|
||||
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 gap="3" wrap="wrap" align="center" justify="center">
|
||||
{TOOLS.map((tool) => (
|
||||
<Link
|
||||
href={tool.href ?? `/tools/${tool.id}`}
|
||||
target={tool.href ? '_blank' : undefined}
|
||||
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,
|
||||
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>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Text size="1" color="gray" align="center">
|
||||
Images courtesy of Wargaming Art Department
|
||||
</Text>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { Flex, Heading, Switch, Text } from '@radix-ui/themes';
|
||||
import PageWrapper from '../../components/PageWrapper';
|
||||
import { useApp } from '../../stores/app';
|
||||
|
||||
export default function Page() {
|
||||
const developerMode = useApp((state) => state.developerMode);
|
||||
const darkMode = useApp((state) => state.darkMode);
|
||||
|
||||
return (
|
||||
<PageWrapper justify="center" align="center">
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex gap="3" direction="column">
|
||||
<Heading size="5">Appearance</Heading>
|
||||
|
||||
<Flex align="center" gap="2">
|
||||
<Text>Dark mode</Text>
|
||||
<Switch
|
||||
checked={darkMode}
|
||||
onCheckedChange={(checked) =>
|
||||
useApp.setState({ darkMode: checked })
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" direction="column">
|
||||
<Heading size="5">Advanced</Heading>
|
||||
|
||||
<Flex align="center" gap="2">
|
||||
<Text>Developer mode</Text>
|
||||
<Switch
|
||||
checked={developerMode}
|
||||
onCheckedChange={(checked) =>
|
||||
useApp.setState({ developerMode: checked })
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Text color="gray">
|
||||
<i>Not a lot here, eh?</i>
|
||||
</Text>
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||
import Link from 'next/link';
|
||||
import { use, useState } from 'react';
|
||||
import { provisionDefinitions } from '../../../../core/blitzkit/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../../../core/blitzkit/tankToCompareMember';
|
||||
import { mutateCompareTemporary } from '../../../../stores/compare';
|
||||
import { TankSearch } from '../../tankopedia/components/TankSearch';
|
||||
|
||||
interface TankControlProps {
|
||||
index: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export function TankControl({ index, id }: TankControlProps) {
|
||||
const awaitedProvisionDefinitions = use(provisionDefinitions);
|
||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Flex gap="3" justify="center" style={{ width: '100%' }}>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareTemporary((draft) => {
|
||||
draft.members.splice(index, 1);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
|
||||
<Dialog.Root
|
||||
open={switchTankDialogOpen}
|
||||
onOpenChange={setSwitchTankDialogOpen}
|
||||
>
|
||||
<Dialog.Trigger>
|
||||
<IconButton variant="ghost">
|
||||
<LoopIcon />
|
||||
</IconButton>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Flex gap="4" direction="column">
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
style={{ flex: 1 }}
|
||||
justify="center"
|
||||
>
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
mutateCompareTemporary((draft) => {
|
||||
draft.members[index] = tankToCompareMember(
|
||||
tank,
|
||||
awaitedProvisionDefinitions,
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
setSwitchTankDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Link href={`/tools/tankopedia/${id}`}>
|
||||
<IconButton variant="ghost">
|
||||
<ExternalLinkIcon />
|
||||
</IconButton>
|
||||
</Link>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { blackA } from '@radix-ui/colors';
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import { PuffLoader } from 'react-spinners';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
const NAV_HEIGHT = 52.65;
|
||||
|
||||
interface LoaderProps {
|
||||
naked?: boolean;
|
||||
color?: string;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export function Loader({ naked, color, progress }: LoaderProps) {
|
||||
const animation = (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{progress !== undefined && (
|
||||
<Text
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(calc(-50% + 5px), calc(-50% + 5px))',
|
||||
}}
|
||||
>
|
||||
{Math.round(progress * 100)}%
|
||||
</Text>
|
||||
)}
|
||||
<PuffLoader color={color ?? theme.colors.solidBackground} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (naked) return animation;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
style={{
|
||||
backgroundColor: blackA.blackA10,
|
||||
position: 'fixed',
|
||||
top: NAV_HEIGHT,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `calc(100% - ${NAV_HEIGHT}px)`,
|
||||
padding: theme.spaces.paddingMajor,
|
||||
}}
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
{animation}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode, Suspense } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { TOOLS } from '../../constants/tools';
|
||||
import { theme } from '../../stitches.config';
|
||||
import { Loader } from './components/Loader';
|
||||
|
||||
interface ToolsLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -15,11 +14,34 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const pathSplit = pathname.split('/');
|
||||
const toolId = pathSplit.at(2);
|
||||
const isToolRoot = pathname === `/tools/${toolId}`;
|
||||
const tool = TOOLS.find((tool) => tool.id === toolId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tool && <title>{tool.title}</title>}
|
||||
{tool && (
|
||||
<>
|
||||
{isToolRoot && (
|
||||
<>
|
||||
<title>{tool.title}</title>
|
||||
<meta
|
||||
property="og:image"
|
||||
content={`/assets/banners/${toolId}.webp`}
|
||||
/>
|
||||
|
||||
{tool.pageDescription !== undefined && (
|
||||
<>
|
||||
<meta name="description" content={tool.pageDescription} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={tool.pageDescription}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{pathSplit.length === 3 && (
|
||||
<div
|
||||
@@ -73,7 +95,7 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -53,29 +53,26 @@ export function PageTurner({
|
||||
<CaretLeftIcon />
|
||||
)}
|
||||
</Button>
|
||||
<TextField.Root>
|
||||
|
||||
<TextField.Root
|
||||
type="number"
|
||||
ref={pageInput}
|
||||
style={{ 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>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
|
||||
@@ -15,30 +15,29 @@ import { produce } from 'immer';
|
||||
import { debounce, range } from 'lodash';
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { RatingsInfo, RatingsPlayer } from '../../../commands/ratings';
|
||||
import { RatingInfo, RatingPlayer } from '../../../commands/ratingLeaderboard';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { Skeleton } from '../../../components/Skeleton';
|
||||
import { LEAGUES } from '../../../constants/leagues';
|
||||
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings';
|
||||
import { FIRST_MINIMAL_ARCHIVED_RATING_SEASON } from '../../../constants/rating';
|
||||
import {
|
||||
REGIONS,
|
||||
Region,
|
||||
UNLOCALIZED_REGION_NAMES,
|
||||
} from '../../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
|
||||
import getRatingsInfo from '../../../core/blitz/getRatingsInfo';
|
||||
import { getRatingsLeague } from '../../../core/blitz/getRatingsLeague';
|
||||
import { getRatingsNeighbors } from '../../../core/blitz/getRatingsNeighbors';
|
||||
import { searchCurrentRatingsPlayers } from '../../../core/blitz/searchCurrentRatingsPlayers';
|
||||
import getRatingInfo from '../../../core/blitz/getRatingInfo';
|
||||
import { getRatingLeague } from '../../../core/blitz/getRatingLeague';
|
||||
import { getRatingNeighbors } from '../../../core/blitz/getRatingNeighbors';
|
||||
import { searchCurrentRatingPlayers } from '../../../core/blitz/searchCurrentRatingPlayers';
|
||||
import {
|
||||
AccountList,
|
||||
AccountListItem,
|
||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
||||
import getArchivedRatingsInfo from '../../../core/blitzkrieg/getArchivedRatingsInfo';
|
||||
import { getArchivedRatingsLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
||||
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkit/getArchivedLatestSeasonNumber';
|
||||
import getArchivedRatingInfo from '../../../core/blitzkit/getArchivedRatingInfo';
|
||||
import { getArchivedRatingLeaderboard } from '../../../core/blitzkit/getArchivedRatingLeaderboard';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { PageTurner } from './components/PageTurner';
|
||||
|
||||
@@ -51,7 +50,7 @@ interface NameCacheEntry {
|
||||
}
|
||||
type NameCache = Record<Region, Record<number, NameCacheEntry | null>>;
|
||||
type LeaderboardCache = Record<Region, Record<number, Record<number, number>>>;
|
||||
type InfoCache = Record<Region, Record<number, RatingsInfo | undefined>>;
|
||||
type InfoCache = Record<Region, Record<number, RatingInfo | undefined>>;
|
||||
type ScoreCache = Record<Region, Record<number, Record<number, number>>>;
|
||||
|
||||
const useNameCache = create<NameCache>(() => ({
|
||||
@@ -93,7 +92,7 @@ export default function Page() {
|
||||
{ type: 'id'; id: number } | { type: 'position'; position: number } | null
|
||||
>(null);
|
||||
|
||||
const ratingsInfo = useInfoCache()[region][season];
|
||||
const ratingInfo = useInfoCache()[region][season];
|
||||
const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
@@ -111,7 +110,7 @@ export default function Page() {
|
||||
});
|
||||
|
||||
const pages = Math.ceil(
|
||||
(ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) /
|
||||
(ratingInfo && ratingInfo.detail === undefined ? ratingInfo?.count : 1) /
|
||||
ROWS_PER_PAGE,
|
||||
);
|
||||
|
||||
@@ -122,8 +121,8 @@ export default function Page() {
|
||||
(async () => {
|
||||
const newInfo =
|
||||
season === 0
|
||||
? await getRatingsInfo(region)
|
||||
: await getArchivedRatingsInfo(region, season);
|
||||
? await getRatingInfo(region)
|
||||
: await getArchivedRatingInfo(region, season);
|
||||
useInfoCache.setState(
|
||||
produce((draft: InfoCache) => {
|
||||
draft[region][season] = newInfo;
|
||||
@@ -140,7 +139,7 @@ export default function Page() {
|
||||
const results = (
|
||||
await Promise.all(
|
||||
range(0, 5).map(async (league: number) => {
|
||||
const data = await getRatingsLeague(region, league);
|
||||
const data = await getRatingLeague(region, league);
|
||||
leaguePositionCache.current[region][league] = data[0].number - 1;
|
||||
|
||||
return data;
|
||||
@@ -178,12 +177,12 @@ export default function Page() {
|
||||
);
|
||||
})();
|
||||
} else {
|
||||
getArchivedRatingsLeaderboard(region, season).then((results) => {
|
||||
getArchivedRatingLeaderboard(region, season).then((results) => {
|
||||
useLeaderboardCache.setState(
|
||||
produce((draft: LeaderboardCache) => {
|
||||
if (!(season in draft[region])) draft[region][season] = {};
|
||||
|
||||
results.forEach((result, index) => {
|
||||
results.entries.forEach((result, index) => {
|
||||
draft[region][season][index] = result.id;
|
||||
});
|
||||
}),
|
||||
@@ -192,7 +191,7 @@ export default function Page() {
|
||||
produce((draft: ScoreCache) => {
|
||||
if (!(season in draft[region])) draft[region][season] = {};
|
||||
|
||||
results.forEach((result) => {
|
||||
results.entries.forEach((result) => {
|
||||
draft[region][season][result.id] = result.score;
|
||||
});
|
||||
}),
|
||||
@@ -213,7 +212,7 @@ export default function Page() {
|
||||
}, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]);
|
||||
// neighboring page caching for all seasons
|
||||
useEffect(() => {
|
||||
if (!ratingsInfo || ratingsInfo?.detail) return;
|
||||
if (!ratingInfo || ratingInfo?.detail) return;
|
||||
|
||||
if (season === 0) {
|
||||
const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2;
|
||||
@@ -236,7 +235,7 @@ export default function Page() {
|
||||
const ids = range(
|
||||
Math.max(0, (page - 1) * ROWS_PER_PAGE),
|
||||
Math.min(
|
||||
ratingsInfo.count - 1,
|
||||
ratingInfo.count - 1,
|
||||
(page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE,
|
||||
),
|
||||
)
|
||||
@@ -265,7 +264,7 @@ export default function Page() {
|
||||
page,
|
||||
region,
|
||||
season,
|
||||
ratingsInfo?.detail,
|
||||
ratingInfo?.detail,
|
||||
leaderboard[region][season] !== undefined &&
|
||||
0 in leaderboard[region][season],
|
||||
]);
|
||||
@@ -279,7 +278,7 @@ export default function Page() {
|
||||
async function cacheNeighbors(
|
||||
id: number,
|
||||
size: number,
|
||||
): Promise<RatingsPlayer[] | null> {
|
||||
): Promise<RatingPlayer[] | null> {
|
||||
const radius = Math.round(size / 2);
|
||||
const targetPosition = Object.entries(leaderboard[region][season]).find(
|
||||
([, player]) => id === player,
|
||||
@@ -303,7 +302,7 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
const { neighbors } = await getRatingsNeighbors(region, id, size, true);
|
||||
const { neighbors } = await getRatingNeighbors(region, id, size, true);
|
||||
|
||||
useLeaderboardCache.setState(
|
||||
produce((draft: LeaderboardCache) => {
|
||||
@@ -341,11 +340,11 @@ export default function Page() {
|
||||
return neighbors;
|
||||
}
|
||||
async function jumpToPosition(position: number) {
|
||||
if (!ratingsInfo || ratingsInfo?.detail) return;
|
||||
if (!ratingInfo || ratingInfo?.detail) return;
|
||||
|
||||
const targetPosition = Math.min(
|
||||
ratingsInfo.count - 1,
|
||||
Math.max(0, Math.min(position, ratingsInfo.count - 1)),
|
||||
ratingInfo.count - 1,
|
||||
Math.max(0, Math.min(position, ratingInfo.count - 1)),
|
||||
);
|
||||
|
||||
if (season === 0) {
|
||||
@@ -378,7 +377,7 @@ export default function Page() {
|
||||
const loadingDirection = Math.sign(targetPosition - closestPosition);
|
||||
let seedingPlayer = closestPlayerId as number;
|
||||
let targetPositionAcquired = false;
|
||||
let neighbors: RatingsPlayer[] | null;
|
||||
let neighbors: RatingPlayer[] | null;
|
||||
|
||||
setLoadingProgress([0, closestPositionDistance]);
|
||||
|
||||
@@ -423,7 +422,7 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
async function jumpToScore() {
|
||||
if (!ratingsInfo || ratingsInfo?.detail) return;
|
||||
if (!ratingInfo || ratingInfo?.detail) return;
|
||||
|
||||
const targetScore = scoreInput.current!.valueAsNumber;
|
||||
|
||||
@@ -522,7 +521,7 @@ export default function Page() {
|
||||
} else {
|
||||
let playerIndex = -1;
|
||||
|
||||
for (let index = ratingsInfo.count; index >= 0; index--) {
|
||||
for (let index = ratingInfo.count; index >= 0; index--) {
|
||||
if (!(index in leaderboard[region][season])) continue;
|
||||
|
||||
if (scores[leaderboard[region][season][index]] >= targetScore) {
|
||||
@@ -545,7 +544,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const totalPlayers =
|
||||
!ratingsInfo?.detail && ratingsInfo?.count ? ratingsInfo.count : undefined;
|
||||
!ratingInfo?.detail && ratingInfo?.count ? ratingInfo.count : undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper color="orange">
|
||||
@@ -573,7 +572,7 @@ export default function Page() {
|
||||
<Select.Trigger style={{ flex: 1 }} />
|
||||
|
||||
<Select.Content>
|
||||
{ratingsInfo?.detail === undefined && (
|
||||
{ratingInfo?.detail === undefined && (
|
||||
<Select.Item value="0">Current season</Select.Item>
|
||||
)}
|
||||
|
||||
@@ -582,7 +581,7 @@ export default function Page() {
|
||||
|
||||
{latestArchivedSeasonNumber &&
|
||||
range(
|
||||
FIRST_ARCHIVED_RATINGS_SEASON,
|
||||
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
|
||||
latestArchivedSeasonNumber + 1,
|
||||
)
|
||||
.map((season) => (
|
||||
@@ -605,7 +604,7 @@ export default function Page() {
|
||||
|
||||
<Dialog.Content>
|
||||
<Flex gap="4" justify="center">
|
||||
<TextField.Input
|
||||
<TextField.Root
|
||||
ref={scoreInput}
|
||||
type="number"
|
||||
placeholder="Type a score..."
|
||||
@@ -638,7 +637,7 @@ export default function Page() {
|
||||
|
||||
<Dialog.Content>
|
||||
<Flex gap="4" justify="center">
|
||||
<TextField.Input
|
||||
<TextField.Root
|
||||
ref={positionInput}
|
||||
type="number"
|
||||
placeholder="Type a position..."
|
||||
@@ -681,8 +680,8 @@ export default function Page() {
|
||||
<Select.Trigger />
|
||||
|
||||
<Select.Content>
|
||||
{ratingsInfo?.detail === undefined &&
|
||||
ratingsInfo?.leagues.map(({ index, title }) => (
|
||||
{ratingInfo?.detail === undefined &&
|
||||
ratingInfo?.leagues.map(({ index, title }) => (
|
||||
<Select.Item key={index} value={`${index}`}>
|
||||
{title}
|
||||
</Select.Item>
|
||||
@@ -714,11 +713,7 @@ export default function Page() {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!ratingsInfo ||
|
||||
ratingsInfo.detail ||
|
||||
!leaderboard
|
||||
)
|
||||
if (!ratingInfo || ratingInfo.detail || !leaderboard)
|
||||
return;
|
||||
|
||||
const minScore =
|
||||
@@ -727,7 +722,7 @@ export default function Page() {
|
||||
|
||||
for (
|
||||
let index = 0;
|
||||
index < ratingsInfo.count;
|
||||
index < ratingInfo.count;
|
||||
index++
|
||||
) {
|
||||
if (
|
||||
@@ -763,105 +758,102 @@ export default function Page() {
|
||||
|
||||
<Dialog.Content>
|
||||
<Flex gap="4" direction="column">
|
||||
<TextField.Root>
|
||||
<TextField.Root
|
||||
onChange={debounce(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const trimmedSearch = event.target.value.trim();
|
||||
const encodedSearch = encodeURIComponent(trimmedSearch);
|
||||
|
||||
if (trimmedSearch) {
|
||||
if (season === 0) {
|
||||
setSearchLoading(true);
|
||||
|
||||
const accountList = await searchCurrentRatingPlayers(
|
||||
region,
|
||||
trimmedSearch,
|
||||
);
|
||||
const values = Object.values(accountList).filter(
|
||||
(searchedPlayer) => !searchedPlayer.skip,
|
||||
);
|
||||
const valuesWithTypes =
|
||||
values as ((typeof values)[number] & {
|
||||
skip: false;
|
||||
})[];
|
||||
|
||||
useLeaderboardCache.setState(
|
||||
produce((draft: LeaderboardCache) => {
|
||||
if (draft) {
|
||||
values.forEach((player) => {
|
||||
const playerWithTypes =
|
||||
player as typeof player & { skip: false };
|
||||
|
||||
draft[region][season][
|
||||
playerWithTypes.number - 1
|
||||
] = playerWithTypes.spa_id;
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
useNameCache.setState(
|
||||
produce((draft: typeof names) => {
|
||||
valuesWithTypes.forEach((searchedPlayer) => {
|
||||
if (searchedPlayer.nickname) {
|
||||
draft[region][searchedPlayer.spa_id] = {
|
||||
nickname: searchedPlayer.nickname,
|
||||
clan: searchedPlayer.clan_tag,
|
||||
};
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
setSearchResults(
|
||||
valuesWithTypes.map((searchedPlayer) => ({
|
||||
account_id: searchedPlayer.spa_id,
|
||||
nickname: searchedPlayer.nickname,
|
||||
number: searchedPlayer.number,
|
||||
})),
|
||||
);
|
||||
setSearchLoading(false);
|
||||
} else {
|
||||
setSearchLoading(true);
|
||||
const accountList = await fetchBlitz<AccountList>(
|
||||
region,
|
||||
'account/list',
|
||||
{ search: encodedSearch },
|
||||
);
|
||||
const playerKeys = Object.keys(
|
||||
leaderboard[region][season],
|
||||
);
|
||||
|
||||
setSearchResults(
|
||||
accountList
|
||||
.map((searchedPlayer) => ({
|
||||
...searchedPlayer,
|
||||
number: parseInt(
|
||||
playerKeys.find(
|
||||
(indexAsString) =>
|
||||
leaderboard[region][season][
|
||||
parseInt(indexAsString)
|
||||
] === searchedPlayer.account_id,
|
||||
)!,
|
||||
),
|
||||
}))
|
||||
.filter((player) => !Number.isNaN(player.number)),
|
||||
);
|
||||
|
||||
setSearchLoading(false);
|
||||
}
|
||||
} else {
|
||||
setSearchResults(undefined);
|
||||
}
|
||||
},
|
||||
500,
|
||||
)}
|
||||
placeholder="Search for player..."
|
||||
>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
|
||||
<TextField.Input
|
||||
onChange={debounce(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const trimmedSearch = event.target.value.trim();
|
||||
const encodedSearch = encodeURIComponent(trimmedSearch);
|
||||
|
||||
if (trimmedSearch) {
|
||||
if (season === 0) {
|
||||
setSearchLoading(true);
|
||||
|
||||
const accountList =
|
||||
await searchCurrentRatingsPlayers(
|
||||
region,
|
||||
trimmedSearch,
|
||||
);
|
||||
const values = Object.values(accountList).filter(
|
||||
(searchedPlayer) => !searchedPlayer.skip,
|
||||
);
|
||||
const valuesWithTypes =
|
||||
values as ((typeof values)[number] & {
|
||||
skip: false;
|
||||
})[];
|
||||
|
||||
useLeaderboardCache.setState(
|
||||
produce((draft: LeaderboardCache) => {
|
||||
if (draft) {
|
||||
values.forEach((player) => {
|
||||
const playerWithTypes =
|
||||
player as typeof player & { skip: false };
|
||||
|
||||
draft[region][season][
|
||||
playerWithTypes.number - 1
|
||||
] = playerWithTypes.spa_id;
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
useNameCache.setState(
|
||||
produce((draft: typeof names) => {
|
||||
valuesWithTypes.forEach((searchedPlayer) => {
|
||||
if (searchedPlayer.nickname) {
|
||||
draft[region][searchedPlayer.spa_id] = {
|
||||
nickname: searchedPlayer.nickname,
|
||||
clan: searchedPlayer.clan_tag,
|
||||
};
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
setSearchResults(
|
||||
valuesWithTypes.map((searchedPlayer) => ({
|
||||
account_id: searchedPlayer.spa_id,
|
||||
nickname: searchedPlayer.nickname,
|
||||
number: searchedPlayer.number,
|
||||
})),
|
||||
);
|
||||
setSearchLoading(false);
|
||||
} else {
|
||||
setSearchLoading(true);
|
||||
const accountList = await fetchBlitz<AccountList>(
|
||||
`https://api.wotblitz.${region}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}`,
|
||||
);
|
||||
const playerKeys = Object.keys(
|
||||
leaderboard[region][season],
|
||||
);
|
||||
|
||||
setSearchResults(
|
||||
accountList
|
||||
.map((searchedPlayer) => ({
|
||||
...searchedPlayer,
|
||||
number: parseInt(
|
||||
playerKeys.find(
|
||||
(indexAsString) =>
|
||||
leaderboard[region][season][
|
||||
parseInt(indexAsString)
|
||||
] === searchedPlayer.account_id,
|
||||
)!,
|
||||
),
|
||||
}))
|
||||
.filter(
|
||||
(player) => !Number.isNaN(player.number),
|
||||
),
|
||||
);
|
||||
|
||||
setSearchLoading(false);
|
||||
}
|
||||
} else {
|
||||
setSearchResults(undefined);
|
||||
}
|
||||
},
|
||||
500,
|
||||
)}
|
||||
placeholder="Search for player..."
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<Flex direction="column" gap="2">
|
||||
@@ -885,12 +877,12 @@ export default function Page() {
|
||||
id: player.account_id,
|
||||
});
|
||||
} else {
|
||||
if (!ratingsInfo || ratingsInfo.detail) return;
|
||||
if (!ratingInfo || ratingInfo.detail) return;
|
||||
let playerIndex = -1;
|
||||
|
||||
for (
|
||||
let index = 0;
|
||||
index < ratingsInfo.count;
|
||||
index < ratingInfo.count;
|
||||
index++
|
||||
) {
|
||||
if (
|
||||
@@ -951,9 +943,7 @@ export default function Page() {
|
||||
</Flex>
|
||||
|
||||
<PageTurner
|
||||
totalPlayers={
|
||||
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
|
||||
}
|
||||
totalPlayers={(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0}
|
||||
leaderboard={leaderboard[region][season] ?? {}}
|
||||
rowsPerPage={ROWS_PER_PAGE}
|
||||
page={page}
|
||||
@@ -982,7 +972,7 @@ export default function Page() {
|
||||
page * ROWS_PER_PAGE,
|
||||
Math.min(
|
||||
page * ROWS_PER_PAGE + ROWS_PER_PAGE,
|
||||
ratingsInfo?.detail ? 0 : (ratingsInfo?.count ?? 1) - 1,
|
||||
ratingInfo?.detail ? 0 : (ratingInfo?.count ?? 1) - 1,
|
||||
),
|
||||
).map((index) => {
|
||||
const id = leaderboard[region][season]?.[index];
|
||||
@@ -994,8 +984,8 @@ export default function Page() {
|
||||
: highlightedPlayer.position === index
|
||||
: false;
|
||||
const reward =
|
||||
!ratingsInfo?.detail && ratingsInfo?.rewards
|
||||
? ratingsInfo.rewards.find(
|
||||
!ratingInfo?.detail && ratingInfo?.rewards
|
||||
? ratingInfo.rewards.find(
|
||||
(reward) =>
|
||||
reward.from_position <= position &&
|
||||
reward.to_position >= position,
|
||||
@@ -1090,9 +1080,7 @@ export default function Page() {
|
||||
</Table.Root>
|
||||
|
||||
<PageTurner
|
||||
totalPlayers={
|
||||
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
|
||||
}
|
||||
totalPlayers={(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0}
|
||||
rowsPerPage={ROWS_PER_PAGE}
|
||||
leaderboard={leaderboard[region][season] ?? {}}
|
||||
page={page}
|
||||
@@ -37,19 +37,18 @@ export function Menu({ Builder, reset }: ContextMenuProps) {
|
||||
<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>
|
||||
<TextField.Root
|
||||
style={{ flex: 1 }}
|
||||
ref={titleInput}
|
||||
defaultValue={session.title}
|
||||
placeholder="Enter title..."
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && titleInput.current) {
|
||||
setShowTitleEditor(false);
|
||||
useSession.setState({ title: titleInput.current!.value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
@@ -22,7 +23,9 @@ import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
|
||||
import searchPlayersAcrossRegions, {
|
||||
AccountListWithServer,
|
||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||
import { resetSession, setSession, useSession } from '../../../stores/session';
|
||||
import { resetSession } from '../../../core/blitzkit/resetSession';
|
||||
import { setSession } from '../../../core/blitzkit/setSession';
|
||||
import { useSession } from '../../../stores/session';
|
||||
import SessionPage from '../../embeds/session/page';
|
||||
import { Menu } from './components/Menu';
|
||||
import * as styles from './page.css';
|
||||
@@ -54,38 +57,36 @@ export default function Page() {
|
||||
<PageWrapper color="blue">
|
||||
<div className={styles.toolBar}>
|
||||
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
||||
<TextField.Root>
|
||||
<TextField.Root
|
||||
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 (https://github.com/radix-ui/primitives/issues/1969) is fixed
|
||||
if (showSearchResults) event.target.focus();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setShowSearchResults(false);
|
||||
input.current?.blur();
|
||||
}
|
||||
}}
|
||||
placeholder="Search for a player..."
|
||||
>
|
||||
<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}>
|
||||
@@ -249,6 +250,20 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<SessionPage naked={false} />
|
||||
|
||||
{!session.isTracking && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
direction="column"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Heading color="gray">No player selected</Heading>
|
||||
<Text color="gray">
|
||||
Search for a <PersonIcon /> Player to get started
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { CaretDownIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, Table, Text } from '@radix-ui/themes';
|
||||
import { random } from 'lodash';
|
||||
import { use, useMemo, useState } from 'react';
|
||||
import { tankDefinitions } from '../../../../core/blitzkit/tankDefinitions';
|
||||
import { tankIcon } from '../../../../core/blitzkit/tankIcon';
|
||||
|
||||
interface DayProps {
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export function Day({ offset }: DayProps) {
|
||||
const timeTo = new Date(Date.now() - offset * 24 * 60 * 60 * 1000);
|
||||
const timeFrom = new Date(Date.now() - (offset + 1) * 24 * 60 * 60 * 1000);
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const tanksList = useMemo(
|
||||
() => Object.values(awaitedTankDefinitions),
|
||||
[awaitedTankDefinitions],
|
||||
);
|
||||
const [expanded, setExpanded] = useState(offset === 0);
|
||||
const tanks = tanksList
|
||||
.filter((tank) => tank.tier === 10)
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, random(2, 5));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>
|
||||
<Button
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
variant="ghost"
|
||||
onClick={() => setExpanded((state) => !state)}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
gap="1"
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{expanded ? <CaretDownIcon /> : <CaretRightIcon />}
|
||||
{timeTo.toLocaleString('default', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Table.RowHeaderCell>
|
||||
<Table.Cell align="right">{random(1, 10)}</Table.Cell>
|
||||
<Table.Cell align="right">
|
||||
{random(60, 80, true).toFixed(0)}%
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
{expanded &&
|
||||
tanks.map((tank) => (
|
||||
<Table.Row
|
||||
key={tank.id}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Table.RowHeaderCell
|
||||
style={{
|
||||
paddingLeft: 32,
|
||||
position: 'relative',
|
||||
overflowY: 'hidden',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
draggable={false}
|
||||
src={tankIcon(tank.id)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 128 + 32,
|
||||
height: '200%',
|
||||
top: '-50%',
|
||||
left: 0,
|
||||
boxShadow: 'inset 128px 0 64px -64px #00000040',
|
||||
objectFit: 'contain',
|
||||
objectPosition: '50% 50%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
position: 'relative',
|
||||
textWrap: 'nowrap',
|
||||
textShadow: 'black 0 0 4px',
|
||||
}}
|
||||
>
|
||||
{tank.name}
|
||||
</Text>
|
||||
</Table.RowHeaderCell>
|
||||
<Table.Cell align="right">{random(1, 10)}</Table.Cell>
|
||||
<Table.Cell align="right">
|
||||
{random(60, 80, true).toFixed(0)}%
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { Flex, Heading, Table } from '@radix-ui/themes';
|
||||
import { random, times } from 'lodash';
|
||||
import { HeroStat } from '../../../components/AllStatsOverview/components/HeroStat';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { Day } from './components/Day';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<PageWrapper color="iris">
|
||||
<Flex gap="2">
|
||||
<Heading>TresAbhi</Heading>
|
||||
<Heading color="gray">[OPTML]</Heading>
|
||||
</Flex>
|
||||
|
||||
<HeroStat color="#8E4EC6" subtitle="Super Unicum" value={3142} />
|
||||
|
||||
<Table.Root variant="surface">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>Day</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell width="0" align="right">
|
||||
Battles
|
||||
</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell width="0" align="right">
|
||||
Winrate
|
||||
</Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{times(random(5, 10), (index) => (
|
||||
<Day offset={index} key={index} />
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||