Compare commits

...
180 Commits
Author SHA1 Message Date
TrèsAbhi 382a23b661 Merge pull request #58 from tresabhi/1.15.8 2023-12-13 12:03:16 -06:00
TrèsAbhi 98107291f5 feat: changelogs 2023-12-13 12:03:02 -06:00
TrèsAbhi c6779a714b feat: custom titles 2023-12-13 11:59:37 -06:00
TrèsAbhi ef8f0fd72d feat: color previews 2023-12-13 11:46:36 -06:00
TrèsAbhi b42df81b48 feat: remove running bot instructions 2023-12-10 12:03:34 -06:00
TrèsAbhi 536fed5448 feat: newer website image 2023-12-10 12:03:02 -06:00
TrèsAbhi d4c0b01343 Merge pull request #56 from tresabhi/1.15.7 2023-12-03 10:51:33 -06:00
TrèsAbhi af67126156 feat: changelogs 2023-12-03 10:51:15 -06:00
TrèsAbhi 9930360405 feat: session tracker classnames 2023-12-03 10:44:49 -06:00
TrèsAbhi 4fb61d6dbd feat: session tracker themeing 2023-12-03 10:43:42 -06:00
TrèsAbhi bcb7ca5c4c fix: local string for ratings overflow count 2023-12-03 09:57:06 -06:00
TrèsAbhi 2be4dbea7a Merge pull request #54 from tresabhi/1.15.6 2023-11-28 10:18:06 -06:00
TrèsAbhi b951c81bf5 feat: changelog 2023-11-28 10:17:56 -06:00
TrèsAbhi 8c221a1295 feat: updated creators with a better format 2023-11-28 10:16:28 -06:00
TrèsAbhi c41f721a96 fix: regions at root 2023-11-28 10:14:45 -06:00
TrèsAbhi 1a92cbd951 Merge pull request #52 from tresabhi/1.15.5 2023-11-27 12:15:09 -06:00
TrèsAbhi 54c5faf929 fix: changelog version 2023-11-27 12:15:24 -06:00
TrèsAbhi d1d9e9e4d1 feat: changelogs 2023-11-27 12:14:35 -06:00
TrèsAbhi 5ca64f839e fix: exhaustive user and channel indexing 2023-11-27 12:12:57 -06:00
TrèsAbhi 4e7cd6f681 Merge pull request #51 from tresabhi/1.15.4 2023-11-27 12:06:01 -06:00
TrèsAbhi ba9f593716 feat: changelogs 2023-11-27 12:06:00 -06:00
TrèsAbhi 9d3d74c7c5 feat: change ratings root dir to "ratings" 2023-11-27 12:03:17 -06:00
TrèsAbhi aae26523ff feat: renamed blitzkrieg-db to blitzkrieg-assets 2023-11-27 12:01:32 -06:00
TrèsAbhi 338a2a14bf fix: continue after no player found 2023-11-27 12:00:19 -06:00
TrèsAbhi 1a66cab435 feat: change name to have "Ratings" in the front 2023-11-27 11:59:02 -06:00
TrèsAbhi 3363c9ab58 Merge pull request #50 from tresabhi/1.15.3 2023-11-25 21:12:30 -06:00
TrèsAbhi 10f64902aa fix: xml in md 2023-11-25 21:10:53 -06:00
TrèsAbhi 25e1f4caa4 fix: workflow triggers 2023-11-25 21:09:24 -06:00
TrèsAbhi 222fa89422 fix: remove pull request 2023-11-25 21:04:04 -06:00
TrèsAbhi 750e145f03 feat: version lint 2023-11-25 21:03:29 -06:00
TrèsAbhi 3bbbbfa402 feat: changelogs 2023-11-25 20:57:58 -06:00
TrèsAbhi f728751fa9 feat: better debug 2023-11-25 20:46:20 -06:00
TrèsAbhi f5425d0e54 fix: more accurate user count 2023-11-25 20:45:00 -06:00
TrèsAbhi debaaccf4c feat: presence 2023-11-25 20:40:11 -06:00
TrèsAbhi a3e20501af feat: new credits transfer 2023-11-25 20:08:01 -06:00
TrèsAbhi c87a75f3fd feat: combined ratings archive workflows 2023-11-25 20:01:12 -06:00
TrèsAbhi 55729c5eda feat: ratings leaderboard archive explicit target 2023-11-25 19:46:56 -06:00
TrèsAbhi 06cbcc2339 fix: "Midnight" in name 2023-11-23 14:39:04 -06:00
TrèsAbhi 1afe860e76 feat: change workflow file name to midnight 2023-11-23 14:38:04 -06:00
TrèsAbhi c0ac935652 feat: remove manual asia leaderboard trigger 2023-11-23 14:36:40 -06:00
TrèsAbhi 7c76b0f5a5 feat: Asia leaderboard manual trigger 2023-11-22 23:51:52 -06:00
TrèsAbhi 42091c54b7 fix: illegal json invocation 2023-11-22 23:49:24 -06:00
TrèsAbhi 396b48d017 feat: smaller fetching size 2023-11-20 13:32:53 -06:00
TrèsAbhi bb1c0c7625 Merge pull request #49 from tresabhi/1.15.2 2023-11-19 14:05:46 -06:00
TrèsAbhi 1170676141 feat: changelogs 2023-11-19 14:05:39 -06:00
TrèsAbhi 7baaaa9db9 fix: remove seeding player neighbor checking 2023-11-19 14:02:00 -06:00
TrèsAbhi c1ecef0179 fix: add max target position 2023-11-19 13:55:11 -06:00
TrèsAbhi 032d98e6bd fix: version number 2023-11-16 11:53:13 -06:00
TrèsAbhi f4a90cd531 Merge pull request #48 from tresabhi/1.15.1 2023-11-16 11:51:58 -06:00
TrèsAbhi 82f03583fe feat: updated docs with latest info 2023-11-16 11:51:10 -06:00
TrèsAbhi 81c1a50052 fix: guide link 2023-11-16 11:47:32 -06:00
TrèsAbhi c7207799e2 feat: changelogs 2023-11-16 11:45:44 -06:00
TrèsAbhi e0a719ee93 fix: patient JSON parsing 2023-11-16 11:41:52 -06:00
TrèsAbhi ee6d2e7d82 feat: remove neighbor relative positional caching 2023-11-16 11:14:38 -06:00
TrèsAbhi 14710b5218 fix: use non-caching apis for ratings command 2023-11-16 11:13:51 -06:00
TrèsAbhi 4b5e0d1c1c feat: american neighbor spelling 2023-11-16 10:58:19 -06:00
TrèsAbhi 9968e8346c Merge pull request #47 from tresabhi/1.15.0 2023-11-15 19:54:03 -06:00
TrèsAbhi bc4da17f10 feat: changelogs 2023-11-15 19:53:34 -06:00
TrèsAbhi da56c563fc feat: more advanced PSAs 2023-11-15 19:30:03 -06:00
TrèsAbhi 95f4dbf6a0 fix: missing season 2023-11-15 19:23:18 -06:00
TrèsAbhi 6c36f343ea fix: current season score jumping 2023-11-15 19:21:08 -06:00
TrèsAbhi 9257ebdd1e feat: skeletons 2023-11-15 16:42:56 -06:00
TrèsAbhi 8f1017195f fix: league jumping for current season 2023-11-15 16:42:52 -06:00
TrèsAbhi 20da294b36 feat: empty no-rewards 2023-11-15 12:18:32 -06:00
TrèsAbhi a63415461a feat: non-loaded page blocking 2023-11-15 12:16:49 -06:00
TrèsAbhi 2bb4c78551 fix: smart neighbor count for next page caching 2023-11-15 12:00:57 -06:00
TrèsAbhi 2909e61721 feat: middle-most caching 2023-11-14 21:13:09 -06:00
TrèsAbhi 171da2a194 fix: no search caching 2023-11-14 21:00:54 -06:00
TrèsAbhi 933c468e2d feat: split id and score 2023-11-14 20:56:27 -06:00
TrèsAbhi 0d1eb9b12e feat: better reward handling 2023-11-14 20:12:34 -06:00
TrèsAbhi 02a03b4d68 feat: advanced PSAs 2023-11-14 13:42:15 -06:00
TrèsAbhi ef9f3bd361 feat: rewards 2023-11-14 13:07:17 -06:00
TrèsAbhi f18b6ee754 feat: radix table 2023-11-14 12:40:49 -06:00
TrèsAbhi deb9e463e1 feat: highlighted player stats 2023-11-14 11:24:38 -06:00
TrèsAbhi c799534009 feat: position relative neighbor caching 2023-11-14 10:42:38 -06:00
TrèsAbhi aea01072fb fix: fetch neighbors after redundancy check 2023-11-14 10:37:09 -06:00
TrèsAbhi ca8dd5a702 fix: no existing cache overwrites for neighbors 2023-11-14 10:29:57 -06:00
TrèsAbhi e79c4a1e3f fix: no cache neighbors 2023-11-14 10:24:10 -06:00
TrèsAbhi e19d0d948a fix: removed ratings info caching 2023-11-14 07:34:47 -06:00
TrèsAbhi a160ccd095 feat: reintroduced overwriting 2023-11-13 23:06:43 -06:00
TrèsAbhi d2d0477186 fix: remove number fetcher 2023-11-13 23:03:28 -06:00
TrèsAbhi c742c0725d feat: removed swr caching 2023-11-13 23:00:57 -06:00
TrèsAbhi 9e6343a111 feat: remove overwrite avoidance 2023-11-13 22:47:17 -06:00
TrèsAbhi faf33b5f2d fix: punctuation 2023-11-13 22:27:26 -06:00
TrèsAbhi 8a78ac7d82 feat: removed /today command 2023-11-13 22:20:09 -06:00
TrèsAbhi d7c7814b3a feat: credits command 2023-11-13 22:19:49 -06:00
TrèsAbhi 15bf02f829 feat: CORS proxy for neighbors 2023-11-13 21:52:21 -06:00
TrèsAbhi 4f62a594b3 feat: button page turning exception 2023-11-13 21:41:37 -06:00
TrèsAbhi 64253d0120 feat: page turning in current season 2023-11-13 21:31:36 -06:00
TrèsAbhi 286936131d feat: simpler welcome message 2023-11-13 21:16:45 -06:00
TrèsAbhi 95588ef6b6 feat: patient fetching 2023-11-13 21:14:56 -06:00
TrèsAbhi daef04ccd8 feat: lowered retry time to 1 second 2023-11-13 13:15:55 -06:00
TrèsAbhi 86e385c548 feat: rewrote jump to score for current season 2023-11-13 12:52:39 -06:00
TrèsAbhi cc193b99d6 fix: bug fixes 2023-11-13 12:27:06 -06:00
TrèsAbhi c026007937 fix: first page caching 2023-11-11 23:07:13 -06:00
TrèsAbhi dbc117a9d4 fix: archived jump to score 2023-11-11 22:20:36 -06:00
TrèsAbhi 34665bd97a fix: fixed archived search results 2023-11-11 22:10:32 -06:00
TrèsAbhi 6d05e607b3 feat: fixed rows per page 2023-11-11 21:42:26 -06:00
TrèsAbhi 14bfc3018b feat: proxy APIs 2023-11-11 21:37:59 -06:00
TrèsAbhi 7e4f195f94 feat: optimized highlighted players 2023-11-11 20:59:48 -06:00
TrèsAbhi 4b6de934a5 feat: current season already cached checking 2023-11-11 20:51:48 -06:00
TrèsAbhi 14fbb595fd fix: better loading screen 2023-11-08 19:38:59 -06:00
TrèsAbhi 18a5fb80d0 feat: more code polish 2023-11-08 19:28:22 -06:00
TrèsAbhi 551ef79f03 feat: jump to position for current season 2023-11-08 17:50:00 -06:00
TrèsAbhi 81aec6bad2 feat: search players for current season 2023-11-08 12:17:08 -06:00
TrèsAbhi d08949c597 feat: jump to league for current season 2023-11-08 11:16:51 -06:00
TrèsAbhi 212aee0373 feat: current season traversal 2023-11-07 20:47:20 -06:00
TrèsAbhi 059b78c9fb feat: page turner disabling buttons 2023-11-07 19:22:28 -06:00
TrèsAbhi 11ba5fc28e feat: more robust deleted player handling 2023-11-07 19:15:41 -06:00
TrèsAbhi fbde9ec64f fix: no double fetch and caching 2023-11-07 18:58:19 -06:00
TrèsAbhi 6600bba58c feat: better layout 2023-11-07 17:33:41 -06:00
TrèsAbhi 9e340d2e16 fix: deleted clan handling 2023-11-07 17:24:51 -06:00
TrèsAbhi 6f784e9e94 feat: remove clan apis 2023-11-07 17:24:36 -06:00
TrèsAbhi 62f6aabf05 feat: better jump buttons 2023-11-07 11:55:27 -06:00
TrèsAbhi 46457a8bdc feat: jump to score 2023-11-07 11:50:51 -06:00
TrèsAbhi 1081e836f5 fix: region based player search 2023-11-07 11:40:46 -06:00
TrèsAbhi dfe1e37592 feat: public blitzkrieg-db 2023-11-07 11:36:08 -06:00
TrèsAbhi 0b9db21c10 feat: deleted user handling 2023-11-07 11:13:30 -06:00
TrèsAbhi 57527115d4 feat: jump to league 2023-11-07 11:02:11 -06:00
TrèsAbhi 8d2679c458 feat: better page turner 2023-11-07 10:50:41 -06:00
TrèsAbhi ca00f18a6a feat: jump to position 2023-11-07 10:47:28 -06:00
TrèsAbhi 466f68701f feat: archived player lookup 2023-11-07 10:32:01 -06:00
TrèsAbhi 1bb83da253 feat: per-page theming 2023-11-07 10:06:15 -06:00
TrèsAbhi bd1e17bac9 feat: tankopedia extractor 2023-11-06 23:02:26 -06:00
TrèsAbhi 322f95e42e feat: rows per page 2023-11-02 11:14:13 -05:00
TrèsAbhi 3ea77f5899 feat: autocomplete size limits 2023-11-01 11:45:19 -05:00
TrèsAbhi 882e61ff44 feat: isolated first archived season number 2023-11-01 11:27:41 -05:00
TrèsAbhi d991be4934 fix: user error now results in user friendly message 2023-11-01 11:21:30 -05:00
TrèsAbhi dfba8bd15f feat: better blitz fetch error management 2023-10-31 15:32:35 -05:00
TrèsAbhi 87e23673fb feat: clan cache 2023-10-31 15:31:03 -05:00
TrèsAbhi 8fbd1acf4a fix: set region 2023-10-31 15:24:00 -05:00
TrèsAbhi d7dfbaa799 feat: username display 2023-10-31 15:20:37 -05:00
TrèsAbhi 04ce20857e feat: no cap 2023-10-31 13:32:08 -05:00
TrèsAbhi 4550246d63 feat: players rendering 2023-10-31 13:30:47 -05:00
TrèsAbhi dd673f44ae feat: use swr 2023-10-31 13:15:28 -05:00
TrèsAbhi 3245284ecc feat: website apis 2023-10-31 12:58:24 -05:00
TrèsAbhi 3d9872a671 feat: patient ratings leaderboard building 2023-10-31 12:41:27 -05:00
TrèsAbhi f02db2a2d8 feat: data driven latest archived season number 2023-10-31 12:35:32 -05:00
TrèsAbhi d1ebb79432 feat: simpler non-user error messaging 2023-10-31 11:56:06 -05:00
TrèsAbhi 44f03a1fce fix: cors url 2023-10-31 11:43:28 -05:00
TrèsAbhi 6b6ee94d9b feat: use corsproxy.io 2023-10-31 11:41:04 -05:00
TrèsAbhi 81c2ca80bc feat: better ratings toolbar 2023-10-30 21:57:22 -05:00
TrèsAbhi c83cec8ed7 feat: simpler descs 2023-10-30 21:56:01 -05:00
TrèsAbhi 93ffe5c507 feat: options for ratings tool 2023-10-29 13:40:32 -05:00
TrèsAbhi 8cd1a70333 feat: isolated tankopediaInfo 2023-10-29 11:54:23 -05:00
TrèsAbhi 06ae024c2c Merge pull request #46 from tresabhi/1.14.4 2023-10-25 10:29:24 -05:00
TrèsAbhi ce8fd0f22f feat: package version 2023-10-25 10:28:07 -05:00
TrèsAbhi 7dc1aff1dc feat: changelogs 2023-10-25 10:27:56 -05:00
TrèsAbhi a60003127f feat: instructions for streamers 2023-10-25 10:26:35 -05:00
TrèsAbhi 7d1ba58806 feat: updated hero logo 2023-10-20 11:31:59 -05:00
TrèsAbhi 963d71e221 Merge pull request #38 from tresabhi/imgbot 2023-10-20 11:29:08 -05:00
TrèsAbhi 2a804fda1c Merge pull request #44 from tresabhi/dependabot/npm_and_yarn/babel/traverse-7.23.2 2023-10-20 11:28:46 -05:00
TrèsAbhi 597a1a04d2 Merge pull request #45 from tresabhi/1.14.3 2023-10-20 11:26:24 -05:00
TrèsAbhi aec6f49ea1 Update 1.14.2.md 2023-10-20 11:26:13 -05:00
TrèsAbhi e1ad710de5 fix: corrected version number 2023-10-20 11:26:03 -05:00
TrèsAbhi edc8447617 feat: changelogs 2023-10-20 11:24:19 -05:00
TrèsAbhi f726d4f8e8 feat: don't manage queue after task completion 2023-10-20 11:12:06 -05:00
TrèsAbhi 1f333f6b88 fix: manage queue after space available 2023-10-20 11:11:41 -05:00
TrèsAbhi 9b855fca47 fix: fetchBlitz return type 2023-10-20 11:08:22 -05:00
TrèsAbhi c5f433fdf4 feat: removed unnesesary type 2023-10-20 10:57:24 -05:00
TrèsAbhi fd61dae1c1 feat: added queues to fetchBlitz 2023-10-20 10:50:54 -05:00
TrèsAbhi 2c45f9759f fix: removed lie about username inference 2023-10-20 10:29:01 -05:00
TrèsAbhi 0d3261f748 feat: named asia region short name "ASIA" 2023-10-20 10:28:41 -05:00
TrèsAbhi d38e873800 feat: used short region names for username autocomplete 2023-10-20 10:28:20 -05:00
TrèsAbhi ca38914095 feat: chunked images in breakdown 2023-10-20 10:22:12 -05:00
TrèsAbhi ee5beeea45 fix: preserve image order 2023-10-20 10:12:01 -05:00
dependabot[bot] a9d40d9f49 build(deps): bump @babel/traverse from 7.22.20 to 7.23.2
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.20 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-10-18 13:18:13 +00:00
TrèsAbhi b8be916c18 Merge pull request #43 from tresabhi/1.14.2 2023-10-17 22:08:51 -05:00
TrèsAbhi 24d0313fd3 feat: changelogs 2023-10-17 22:08:39 -05:00
TrèsAbhi c3d2a81327 fix: generic stats name overflow 2023-10-17 22:06:50 -05:00
TrèsAbhi dfeb117663 fix: getClanInfo now uses clan_id 2023-10-17 22:04:27 -05:00
TrèsAbhi 495a830506 Merge pull request #42 from tresabhi/1.14.1 2023-10-17 21:14:13 -05:00
TrèsAbhi a730f67ca5 feat: changelogs 2023-10-17 21:14:30 -05:00
TrèsAbhi 9a3867770d feat: reduced render workers to 2 2023-10-17 21:01:26 -05:00
TrèsAbhi f8a66bb5e7 feat: simpler /link message 2023-10-17 20:59:33 -05:00
TrèsAbhi 4b00e1342e feat: user error discrimination 2023-10-17 20:58:36 -05:00
TrèsAbhi f9e0584f00 fix: memory leak in render threads 2023-10-17 20:45:45 -05:00
TrèsAbhi 900c290e75 Update cd-host.yml 2023-10-16 20:50:40 -05:00
TrèsAbhi 69e1577d18 fix: prisma copy over 2023-10-16 20:44:47 -05:00
ImgBotApp e3864b86a4 [ImgBot] Optimize images
*Total -- 21,554.66kb -> 14,287.64kb (33.71%)

/docs/public/assets/images/hero.png -- 8.27kb -> 4.13kb (50.07%)
/public/assets/banners/tankopedia.png -- 2,422.26kb -> 1,285.27kb (46.94%)
/public/assets/banners/profile.png -- 2,004.61kb -> 1,143.78kb (42.94%)
/public/assets/banners/inactive.png -- 2,675.88kb -> 1,700.96kb (36.43%)
/public/assets/banners/ratings.png -- 3,878.81kb -> 2,495.66kb (35.66%)
/public/assets/banners/more.png -- 8,407.57kb -> 5,532.24kb (34.2%)
/public/assets/banners/session.png -- 2,157.27kb -> 2,125.60kb (1.47%)

Signed-off-by: ImgBotApp <[email protected]>
2023-10-01 22:45:47 +00:00
126 changed files with 3354 additions and 856 deletions

No files matched your search

@@ -1,34 +0,0 @@
name: CD Asia Ratings Latest
on:
schedule:
- cron: "0 0 * * *"
jobs:
host:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use node
uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
- name: Install Yarn
run: npm install --location=global yarn
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --latest
@@ -1,34 +0,0 @@
name: CD Asia Ratings Leaderboards
on:
schedule:
- cron: "0 17 * * *"
jobs:
host:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use node
uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
- name: Install Yarn
run: npm install --location=global yarn
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia
+1 -1
View File
@@ -2,7 +2,7 @@ name: CD Docs
on: on:
push: push:
branches: [main] branches: main
jobs: jobs:
build: build:
+1 -1
View File
@@ -29,4 +29,4 @@ jobs:
unzip bot.zip unzip bot.zip
- name: Run bot - name: Run bot
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps index.cjs run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
@@ -1,8 +1,12 @@
name: CD Europe Ratings Latest name: CD Ratings Asia
# latest: '0 0 * * *'
# asia midnight: '0 17 * * *'
on: on:
schedule: schedule:
- cron: "0 0 * * *" - cron: '0 0 * * *'
- cron: '0 17 * * *'
jobs: jobs:
host: host:
@@ -16,7 +20,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: "npm" cache: 'npm'
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,11 +28,11 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: "**/node_modules" path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --latest run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
@@ -1,8 +1,12 @@
name: CD North America Ratings Latest name: CD Ratings Europe
# latest: '0 0 * * *'
# eu midnight: '0 23 * * *'
on: on:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 0 * * *'
- cron: '0 23 * * *'
jobs: jobs:
host: host:
@@ -31,4 +35,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --latest run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
@@ -1,8 +1,12 @@
name: CD Europe Ratings Leaderboards name: CD Ratings North America
# latest: '0 0 * * *'
# na midnight: '0 5 * * *'
on: on:
schedule: schedule:
- cron: "0 23 * * *" - cron: '0 0 * * *'
- cron: '0 5 * * *'
jobs: jobs:
host: host:
@@ -16,7 +20,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: "npm" cache: 'npm'
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,11 +28,11 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: "**/node_modules" path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
+1 -1
View File
@@ -1,6 +1,6 @@
name: CI Build Bot name: CI Build Bot
on: [push, pull_request] on: push
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
+1 -1
View File
@@ -1,6 +1,6 @@
name: CI Build Docs name: CI Build Docs
on: [push, pull_request] on: push
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
+1 -1
View File
@@ -1,6 +1,6 @@
name: CI Build Website name: CI Build Website
on: [push, pull_request] on: push
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -1,8 +1,6 @@
name: CD North America Ratings Leaderboards name: CI Version
on: on: push
schedule:
- cron: '0 5 * * *'
jobs: jobs:
host: host:
@@ -31,4 +29,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na run: yarn lint:version
+2 -1
View File
@@ -13,4 +13,5 @@ test.*
/docs/.vitepress/cache /docs/.vitepress/cache
servers.csv servers.csv
.next/ .next/
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
temp/
+7
View File
@@ -6,15 +6,20 @@
"blitzstars", "blitzstars",
"cleandist", "cleandist",
"cockroackdb", "cockroackdb",
"corsproxy",
"customperiod", "customperiod",
"deburr", "deburr",
"diacriticless", "diacriticless",
"Everytime", "Everytime",
"fullstats", "fullstats",
"fuzzysort", "fuzzysort",
"immer",
"kranvagn", "kranvagn",
"krieg",
"Leaderboard", "Leaderboard",
"leaderboards", "leaderboards",
"MURICA",
"Oopsy",
"outdir", "outdir",
"outfile", "outfile",
"ownedtanks", "ownedtanks",
@@ -35,12 +40,14 @@
"subnation", "subnation",
"tankaverages", "tankaverages",
"tankopedia", "tankopedia",
"tankopedias",
"tankstats", "tankstats",
"tresabhi", "tresabhi",
"Unicum", "Unicum",
"Unregistering", "Unregistering",
"vickers", "vickers",
"vitepress", "vitepress",
"Vovko",
"wargaming", "wargaming",
"webp", "webp",
"winrate", "winrate",
+1 -22
View File
@@ -14,31 +14,10 @@ Go to [application directory for Blitzkrieg](https://discord.com/application-dir
### https://blitz-krieg.vercel.app/ ### https://blitz-krieg.vercel.app/
![Blitzkrieg website](https://i.imgur.com/QOXUP63.png) ![Blitzkrieg website](https://i.imgur.com/eRIZe5P.png)
## Visit the documentation website ## Visit the documentation website
### https://tresabhi.github.io/blitzkrieg/ ### https://tresabhi.github.io/blitzkrieg/
![Blitzkrieg documentation website](https://i.imgur.com/mCZCZEI.png) ![Blitzkrieg documentation website](https://i.imgur.com/mCZCZEI.png)
## Run the bot
### Install dependencies
```bash
yarn
```
### Create `.env`
```env
GH_TOKEN={YOUR_GITHUB_TOKEN_HERE}
DISCORD_TOKEN={YOUR_DISCORD_TOKE_HERE}
```
### Run the command
```bash
yarn dev:bot
```
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
themeConfig: { themeConfig: {
nav: [ nav: [
{ text: 'Home', link: '/' }, { text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/about' }, { text: 'Guide', link: '/guide/introduction' },
], ],
sidebar: [ sidebar: [
+17
View File
@@ -0,0 +1,17 @@
# Blitzkrieg 1.14.1
Oopsy daisy, a memory leak!
## Changes
- Simplified message for users to use `/link`
## Fixes
- Fixed a memory leak in the render thread manager
## Technical changes
- Added `UserError` extending `Error`
- This lets the bot avoid spamming the debug console for mistakes users make
- Reduced render threads to 2
+8
View File
@@ -0,0 +1,8 @@
# Blitzkrieg 1.14.2
A late night hot fix for my homies
## Fixes
- Clan info is now fetched using `clan_id` instead of `account_id`
- Generic stats row names are now bounded by a max width
+24
View File
@@ -0,0 +1,24 @@
# Blitzkrieg 1.14.3
Pull your brush out and start polishing.
## Changes
![HisRoyalFatness' career breakdown](https://i.imgur.com/GhgMTnS.png)
- Rows in `/breakdown` are now chunked into `8` per image
- This is true unless the number of pages exceeds `10` in which case the rows are even distributed across all `10` pages
- `10` is the maximum number of images that Discord allows
- Short region names are used in username autocomplete instead of their full names
- Example: "North America" is "NA" now
## Fixes
- Removed reference to username inference in the username option description
- Removed an unnecessary type `asd` that did absolutely nothing used nowhere
## Technical changes
- Image orders are now preserved even if processed in parallel and all completed out of order
- `fetchBlitz` is now throttled to `10` requests per second
- Request overflows are managed by queueing
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.14.4
Streamers get some knowledge today
## Changes
- Added a video in the embed button prompt
+72
View File
@@ -0,0 +1,72 @@
# Blitzkrieg 1.15.0
Ratings will never be the same.
## Features
![Blitzkrieg Ratings. Elevate your game. Keep it simple.](https://i.imgur.com/7PZVAGQ.png)
- Blitzkrieg ratings leaderboard
- Available at https://blitz-krieg.vercel.app/tools/ratings
- **Highly experimental; [please report bugs on Discord server](https://discord.gg/nDt7AjGJQH)**
- Current season leaderboards are live meaning that there may be scores and positions that are out of order
- Options
- Regions can be swapped between NA, EU, and Asia
- The current season and all archived season (from 49 and onwards) are available
- Jump: used to jump to certain players
- Score: finds the first player with at least your desired score
- Position: finds the player at that position
- League: finds the top-most player in that league
- Player: finds the player
- **Jumping to scores and positions for the current season is a compounding function (hence the loading screen) and is limited to the speed of Wargaming's APIs which is pretty darn slow**
- Page controls
- Flip between pages left and right
- 33 players are loaded at once
- Left control is disabled on the first page and vice versa
- Page turning controls are also disabled if the next/previous page hasn't loaded yet
- Page numbers can be manually typed in the text field too
- This is available both at the top and the bottom of the screen
- Players
- Shows the position of the player
- Shows the player nickname and clan name
- "Deleted player ID" us shown when loading a deleted player
- A skeleton is shown when the player and clan is loading
- Shows the player percentile
- Shows reward for the position
- This is blank when there is no reward
- This column isn't available for archived seasons
- Shows the score
- A skeleton is shown when loading
- Click on a row to highlight it
- Using a jump button also highlights a row
- `/credits`: thanks who helped made this project
## Changes
- Error messages that user face are now even more simplified
- Increased username and clan autocomplete size limits to `25`
- Added capability for per-page accent theming
- Example: the ratings page is red while the session tracker is blue
- Simplified welcome message for SKLLD clan server
## Technical Changes
- Isolated tankopedia info
- Implemented `corsproxy.io` for non-timeout tolerant endpoints
- Latest archived season number is now data driven
- Ratings archives are now public
- This eliminates the need of `octokit`
- Ratings leaderboard archiver is now error tolerant
- Fetch attempts are made every `1000ms`
- Blitz errors are now managed better freeing up the queue faster
- Isolated first archived season number
- Added more advanced PSAs
- These can now feature images or embeds
- These can now modify message contents
- These can also now be secret
## Experimental Changes
- Added tankopedia extractor
- Retrieves tankopedia data from the game files
- This is not utilized as of right now
+24
View File
@@ -0,0 +1,24 @@
# Blitzkrieg 1.15.1
'MURICA!
## Changes
- "Neighbor" is now spelled in the American way
- Updated documentation website with the latest information
## Fixes
- Fixed the guide link on the documentation website
## Technical features
- Added capability to fetch patiently while also parsing the JSON
## Technical changes
- Removed neighbor relative position caching
## Technical fixes
- `/ratings` now uses the non-caching APIs
+9
View File
@@ -0,0 +1,9 @@
# Blitzkrieg 1.15.2
Infinity reduced to a finite value.
## Fixes
- Added a maximum value to the jump to position button
- Removed neighbor checking for the seeding player
- This fixes the jump to position button not loading at all
+30
View File
@@ -0,0 +1,30 @@
# Blitzkrieg 1.15.3
The bot now gets more expressive!
## Features
- The bot can now have presence messages; examples:
- "Living in {number} servers"
- "Server {number} users"
- "Observing {number} channels"
- More will be added in the future! Give me some ideas
## Changes
- Moved `/credits` to `/about creators`
- `/debug` command now ditches embeds and has better information to serve
## Technical Features
- Added a workflow and lint for package and changelog version miss match
## Technical Changes
- Removed manual trigger for Asian latest ratings leaderboard archiver
- Leaderboard archiver now accepts an explicit target like `midnight` or `latest`
- Combined midnight and latest leaderboard archival workflow into one file
## Technical Fixed
- Reviewed all workflow triggers and updated them
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.15.4
Unlocking future potential
## Technical Changes
- Moved the word "Ratings" to the front of the workflow name
- Renamed `blitzkrieg-db` to `blitzkrieg-assets`
- Moved the region folders from the root to `regions`
## Technical Fixes
- The ratings leaderboard archiver now correctly skips undefined players
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.15.5
Oops, this should've been in the last patch.
## Fixes
- User and channel cache is now guild cache exhaustive
+11
View File
@@ -0,0 +1,11 @@
# Blitzkrieg 1.15.6
This should have also been in the last update lol.
## Changes
- Changed the format of `/about creators` and added one more person
## Fixes
- Archived ratings leaderboards are now fetched from the correct URL path
+20
View File
@@ -0,0 +1,20 @@
# Blitzkrieg 1.15.7
Simple yet powerful theming options for the session tracker
## Features
- The accent color of the session tracker (hence the embed too) can now be tweaked
- Access this setting via the dropdown from Options > Accent color
## Fixes
- Reimplemented the commas for last numbers in the overflow count of the ratings leaderboard
- For example, "+1234567 more" will now read "+1,234,567 more"
## Technical Features
- Added class names for more in-depth customization for those who are comfortable with CSS
- `session-tracker-root`
- `session-tracker-row`
- `session-tracker-row-stat`
+9
View File
@@ -0,0 +1,9 @@
# Blitzkrieg 1.15.8
A request fulfilled \:\)
## Features
- Added custom titles in session tracker
- Change the title through Options > Edit title...
- Colors are shown when selecting a theme color for the session tracker
+5
View File
@@ -0,0 +1,5 @@
# Creators
- **TrèsAbhi** `@tresabhi`: Creator and main programmer
- **Prince_NA** `@prince_na`: Game advisor & tester
- **Vovko**: Technical advisor
-17
View File
@@ -1,17 +0,0 @@
# Installing Blitzkrieg
It's easy as 1, 2, 3!
## Step 1
Join a server that already has Blitzkrieg or [join our official Discord server](https://discord.gg/nDt7AjGJQH).
## Step 2
View the bot's profile and click "Add to Server".
<video src="https://i.imgur.com/Zq0P3m2.mp4" autoplay loop muted width=320 title="how to install"></video>
## Step 3
Review the permissions the bot is asking for and add it to your server!
+13 -1
View File
@@ -1,6 +1,10 @@
# Introduction # Introduction
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz. 🎉 Services for everything and anything World of Tanks Blitz
## Bot
Blitzkrieg is available as a Discord bot and can be [installed very easily](/guide/invite).
- Track statistics - Track statistics
- Find inactive players in a clan - Find inactive players in a clan
@@ -8,3 +12,11 @@ Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blit
- Create embeds for streams - Create embeds for streams
- Track a player/tanks statistical evolution - Track a player/tanks statistical evolution
- ...and a variety of other similar things - ...and a variety of other similar things
## Website
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
- Session tracker
- Ratings leaderboard
- Way more to come
+2 -2
View File
@@ -12,10 +12,10 @@ hero:
actions: actions:
- theme: brand - theme: brand
text: Add to Discord Server text: Add to Discord Server
link: guide/install link: /guide/invite
- theme: alt - theme: alt
text: Guide text: Guide
link: /guide/about link: /guide/introduction
features: features:
- title: Clean and Efficient UI - title: Clean and Efficient UI
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 183 KiB

+1
View File
@@ -7,6 +7,7 @@ const nextConfig = {
...withVanillaExtract(), ...withVanillaExtract(),
distDir: 'dist/website', distDir: 'dist/website',
reactStrictMode: false,
}; };
export default nextConfig; export default nextConfig;
+8 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkrieg", "name": "blitzkrieg",
"version": "1.14.0", "version": "1.15.8",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
@@ -9,6 +9,7 @@
"build:website": "next build", "build:website": "next build",
"build:docs": "vitepress build docs", "build:docs": "vitepress build docs",
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard", "build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"build:tankopedia": "vite-node --script scripts/buildTankopedia",
"dev:bot": "yarn build:bot && yarn start:bot", "dev:bot": "yarn build:bot && yarn start:bot",
"dev:docs": "vitepress dev docs", "dev:docs": "vitepress dev docs",
"dev:website": "next dev", "dev:website": "next dev",
@@ -20,6 +21,7 @@
"lint": "npx tsc", "lint": "npx tsc",
"lint:website": "next lint", "lint:website": "next lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"", "format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
"knip": "knip", "knip": "knip",
"update": "yarn upgrade --latest" "update": "yarn upgrade --latest"
}, },
@@ -33,12 +35,14 @@
"@resvg/resvg-js": "^2.4.1", "@resvg/resvg-js": "^2.4.1",
"@vanilla-extract/css": "^1.13.0", "@vanilla-extract/css": "^1.13.0",
"bepaint": "^0.1.0-beta.3", "bepaint": "^0.1.0-beta.3",
"decompress": "^4.2.1",
"discord.js": "^14.13.0", "discord.js": "^14.13.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"esbuild": "^0.19.3", "esbuild": "^0.19.3",
"express": "^4.18.2", "express": "^4.18.2",
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"immer": "^10.0.2", "immer": "^10.0.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-escape": "^2.0.0", "markdown-escape": "^2.0.0",
"next": "^13.5.2", "next": "^13.5.2",
@@ -49,12 +53,15 @@
"vite-node": "^0.34.5", "vite-node": "^0.34.5",
"vitepress": "^1.0.0-rc.20", "vitepress": "^1.0.0-rc.20",
"vitepress-plugin-auto-sidebar": "^1.1.0", "vitepress-plugin-auto-sidebar": "^1.1.0",
"xml-js": "^1.6.11",
"zustand": "^4.4.1" "zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@total-typescript/ts-reset": "^0.5.1", "@total-typescript/ts-reset": "^0.5.1",
"@types/decompress": "^4.2.6",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/express": "^4.17.18", "@types/express": "^4.17.18",
"@types/js-yaml": "^4.0.8",
"@types/lodash": "^4.14.199", "@types/lodash": "^4.14.199",
"@types/markdown-escape": "^1.1.0", "@types/markdown-escape": "^1.1.0",
"@types/node": "^20.6.5", "@types/node": "^20.6.5",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 MiB

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+36 -28
View File
@@ -1,36 +1,44 @@
import { build } from 'esbuild'; import { build } from 'esbuild';
import { readdir } from 'fs/promises'; import { cp, readdir } from 'fs/promises';
const FILES = [ readdir('src/workers').then((workers) => {
'bot', const files = [
'index', 'bot',
'index',
(await readdir('src/workers')).map( ...workers.map((file) => `workers/${file.replace('.ts', '')}`),
(file) => `workers/${file.replace('.ts', '')}`, ];
),
];
await build({ build({
entryPoints: FILES.map((file) => `src/${file}.ts`), entryPoints: files.map((file) => `src/${file}.ts`),
outdir: 'dist/bot', outdir: 'dist/bot',
platform: 'node', platform: 'node',
tsconfig: 'tsconfig.esbuild.json', tsconfig: 'tsconfig.esbuild.json',
loader: { loader: {
'.node': 'copy', '.node': 'copy',
'.ttf': 'file', '.ttf': 'file',
}, },
format: 'cjs', format: 'cjs',
jsx: 'transform', jsx: 'transform',
outExtension: { outExtension: {
'.js': '.cjs', '.js': '.cjs',
}, },
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
minifyIdentifiers: false, // cause goofy errors minifyIdentifiers: false, // causes goofy errors
minifySyntax: true, minifySyntax: true,
minifyWhitespace: true, minifyWhitespace: true,
logLevel: 'info', logLevel: 'info',
});
}); });
// copy schema.prisma
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
readdir('node_modules/prisma').then((files) =>
files
.filter((file) => file.endsWith('.node'))
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
);
+25 -26
View File
@@ -6,6 +6,7 @@ import {
RatingsNeighbors, RatingsNeighbors,
RatingsPlayer, RatingsPlayer,
} from '../src/commands/ratings'; } from '../src/commands/ratings';
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
import commitMultipleFiles from './commitMultipleFiles.js'; import commitMultipleFiles from './commitMultipleFiles.js';
/* /*
@@ -13,25 +14,27 @@ import commitMultipleFiles from './commitMultipleFiles.js';
* Central European Time (UTC+1): use 0 23 * * *. * Central European Time (UTC+1): use 0 23 * * *.
* Central Asia Standard Time (UTC+7): use 0 17 * * *. * Central Asia Standard Time (UTC+7): use 0 17 * * *.
* *
* na/ratings/49/info.json: season info like rewards, start & end time, etc. * [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
* na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight * [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
* na/ratings/49/midnight.json: last midnight's leaderboard relative to the region * [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
*/ */
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[]; export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
const publish = argv.includes('--publish'); const publish = argv.includes('--publish');
const latest = argv.includes('--latest'); const target = argv
.find((arg) => arg.startsWith('--target='))
?.split('=')[1] as 'midnight' | 'latest' | undefined;
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1]; const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
if (!target) throw new Error('Target parameter not specified');
if (!server) throw new Error('Region parameter not specified'); if (!server) throw new Error('Region parameter not specified');
const NEIGHBORS = 1000; const NEIGHBORS = 2 ** 8;
const PLAYERS = await fetch( const PLAYERS = await patientFetchJSON<RatingsInfo>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
) ).then((data) => (data.detail === undefined ? data.count : undefined));
.then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) { if (PLAYERS === undefined) {
throw new Error('No current ratings season running'); throw new Error('No current ratings season running');
@@ -42,9 +45,9 @@ const leaderboard: BlitzkriegRatingsLeaderboard = [];
let registered = 0; let registered = 0;
async function branchFromPlayer(id: number) { async function branchFromPlayer(id: number) {
const { neighbors } = await fetch( const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
).then((response) => response.json() as Promise<RatingsNeighbors>); );
const firstPlayer = neighbors[0]; const firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1]; const lastPlayer = neighbors[neighbors.length - 1];
@@ -73,9 +76,9 @@ async function branchFromPlayer(id: number) {
} }
async function branchFromLeague(id: number) { async function branchFromLeague(id: number) {
const { result } = await fetch( const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>); );
const firstPlayer = result[0]; const firstPlayer = result[0];
const lastPlayer = result[result.length - 1]; const lastPlayer = result[result.length - 1];
@@ -98,7 +101,10 @@ await Promise.all([
console.log('Converting to an array...'); console.log('Converting to an array...');
for (let index = 0; index < PLAYERS; index++) { for (let index = 0; index < PLAYERS; index++) {
const listing = players[index + 1]; const listing = players[index + 1];
if (!listing) console.warn(`Position ${index + 1} had no player`); if (!listing) {
console.warn(`Position ${index + 1} had no player`);
continue;
}
leaderboard[index] = { leaderboard[index] = {
id: listing.spa_id, id: listing.spa_id,
@@ -110,26 +116,19 @@ const leaderboardJSON = JSON.stringify(leaderboard);
if (publish) { if (publish) {
const octokit = new Octokit({ auth: env.GH_TOKEN }); const octokit = new Octokit({ auth: env.GH_TOKEN });
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
const info = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then(
(response) =>
response.json() as Promise<RatingsInfo & { detail: undefined }>,
); );
const normalizedServer = server === 'na' ? 'com' : server; const normalizedServer = server === 'na' ? 'com' : server;
const infoPath = `regions/${normalizedServer}/ratings/${info.current_season}/info.json`;
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`; const leaderboardPath = `regions/${normalizedServer}/ratings/${info.current_season}/${target}.json`;
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
latest ? 'latest' : 'midnight'
}.json`;
const infoJSON = JSON.stringify(info); const infoJSON = JSON.stringify(info);
console.log(`Publishing to season ${info.current_season}...`); console.log(`Publishing to season ${info.current_season}...`);
commitMultipleFiles( commitMultipleFiles(
octokit, octokit,
'tresabhi', 'tresabhi',
'blitzkrieg-db', 'blitzkrieg-assets',
'main', 'main',
new Date().toString(), new Date().toString(),
[ [
+86
View File
@@ -0,0 +1,86 @@
import decompress from 'decompress';
import { readFile, readdir, rm, writeFile } from 'fs/promises';
import { load } from 'js-yaml';
import { ElementCompact, xml2js } from 'xml-js';
import { NATION_IDS } from '../src/constants/nations';
const REWRITE = false;
const VEHICLE_DEFS = 'assets/Data/XML/item_defs/vehicles';
const APK_FILE = 'temp/blitz.apk';
const STRINGS = 'assets/Data/Strings';
const LANGUAGE = 'en';
const DIRECTORIES_OF_INTEREST = [VEHICLE_DEFS, STRINGS];
if (REWRITE) {
await rm('temp/blitz', { recursive: true, force: true });
decompress(APK_FILE, 'temp/blitz', {
filter: (file) =>
DIRECTORIES_OF_INTEREST.some((dir) => file.path.startsWith(dir)),
});
}
interface BlitzkriegTankopedia {
id: number;
nation: string;
name: string;
name_short?: string;
is_premium: boolean;
is_collector: boolean;
tier: number;
type: string;
}
const strings = await readFile(
`temp/blitz/${STRINGS}/${LANGUAGE}.yaml`,
'utf-8',
).then((data) => load(data) as Record<string, string>);
const tankopedia: Record<number, BlitzkriegTankopedia> = {};
const nations = await readdir(`temp/blitz/${VEHICLE_DEFS}`);
await Promise.all(
nations
.filter((nation) => nation !== 'common')
.map(async (nation) => {
const listXML = await readFile(
`temp/blitz/${VEHICLE_DEFS}/${nation}/list.xml`,
'utf-8',
);
const list = xml2js(listXML, { compact: true }) as ElementCompact;
await Promise.all(
Object.entries<ElementCompact>(list.root).map(
async ([vehicle, vehicleData]) => {
// const vehicleXML = await readFile(
// `temp/blitz/${VEHICLE_DEFS}/${nation}/${vehicle}.xml`,
// 'utf-8',
// ).then((xml) => xml2js(xml, { compact: true }) as ElementCompact);
const nationVehicleId = parseInt(list.root[vehicle].id._text);
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
// TODO: use userString property instead
const name = strings[`#${nation}_vehicles:${vehicle}`];
const name_short = strings[`#${nation}_vehicles:${vehicle}_short`];
const is_premium = 'gold' in vehicleData.price;
const is_collector = vehicleData.sellPrice
? 'gold' in vehicleData.sellPrice
: false;
const tier = parseInt(vehicleData.level._text);
const tags = vehicleData.tags._text.split(' ');
const type = tags[0];
tankopedia[id] = {
id,
name,
name_short,
nation,
is_premium,
is_collector,
tier,
type,
};
},
),
);
}),
);
writeFile('dist/tankopedia.json', JSON.stringify(tankopedia, null, 2));
+12
View File
@@ -0,0 +1,12 @@
import { readdir } from 'fs/promises';
import packageJSON from '../package.json';
const { version } = packageJSON;
const changelogs = await readdir('docs/changelogs');
const hasVersion = changelogs.includes(`${version}.md`);
if (hasVersion) {
console.log(`Version ${version} found in changelogs`);
} else {
throw new Error(`Version ${version} not found in changelogs`);
}
@@ -0,0 +1,10 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../constants/regions';
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
export async function GET(
request: Request,
{ params }: { params: { region: Region } },
) {
return NextResponse.json(await getRatingsInfo(params.region));
}
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions';
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
export async function GET(
request: Request,
{ params }: { params: { region: Region; league: number } },
) {
return NextResponse.json(
await getRatingsLeague(params.region, params.league, true),
);
}
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions';
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
export async function GET(
request: Request,
{ params }: { params: { region: Region; username: string } },
) {
return NextResponse.json(
await searchCurrentRatingsPlayers(params.region, params.username),
);
}
+4 -9
View File
@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown'; import * as Breakdown from '../../../components/Breakdown';
import { TreeTypeEnum } from '../../../components/Tanks'; import { TreeTypeEnum } from '../../../components/Tanks';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchWargaming'; import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { tankAverages } from '../../../core/blitzstars/tankAverages'; import { tankAverages } from '../../../core/blitzstars/tankAverages';
import { import {
TankopediaEntry, TankopediaEntry,
@@ -207,15 +207,9 @@ export default function SessionPage() {
since !== undefined && since !== undefined &&
diff !== undefined && ( diff !== undefined && (
<Breakdown.Row <Breakdown.Row
color={session.color}
minimized={!session.showCareer} minimized={!session.showCareer}
title={`Since ${since.toLocaleString(undefined, { title={session.title}
hour: 'numeric',
minute: 'numeric',
hour12: true,
month: 'short',
year: 'numeric',
day: 'numeric',
})}`}
stats={session.customColumns.map((customColumn) => { stats={session.customColumns.map((customColumn) => {
const rowStat = totalRowStats[customColumn.display]; const rowStat = totalRowStats[customColumn.display];
@@ -293,6 +287,7 @@ export default function SessionPage() {
return ( return (
<Breakdown.Row <Breakdown.Row
color={session.color}
key={stats.tank_id} key={stats.tank_id}
minimized={!session.showCareer} minimized={!session.showCareer}
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`} title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
+1 -6
View File
@@ -41,12 +41,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
: 'url(https://i.imgur.com/PhS06NJ.png)', : 'url(https://i.imgur.com/PhS06NJ.png)',
}} }}
> >
<Theme <Theme appearance="dark" panelBackground="translucent" radius="full">
appearance="dark"
panelBackground="translucent"
radius="full"
accentColor="blue"
>
{!isEmbed && <Navbar />} {!isEmbed && <Navbar />}
<Flex direction="column">{children}</Flex> <Flex direction="column">{children}</Flex>
@@ -0,0 +1,13 @@
import { keyframes, style } from '@vanilla-extract/css';
const spinKeyframes = keyframes({
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
});
export const infiniteSpin = style({
animationName: spinKeyframes,
animationIterationCount: 'infinite',
animationTimingFunction: 'linear',
animationDuration: '1s',
});
@@ -0,0 +1,95 @@
import {
CaretLeftIcon,
CaretRightIcon,
SymbolIcon,
} from '@radix-ui/react-icons';
import { Button, Flex, TextField } from '@radix-ui/themes';
import { range } from 'lodash';
import { useEffect, useRef } from 'react';
import { infiniteSpin } from './index.css';
interface PageTurnerProps {
leaderboard: Record<number, number>;
page: number;
rowsPerPage: number;
pages: number;
onPageChange: (page: number, isButtonClick: boolean) => void;
totalPlayers: number;
}
export function PageTurner({
page,
rowsPerPage,
onPageChange,
pages,
leaderboard,
totalPlayers,
}: PageTurnerProps) {
const pageInput = useRef<HTMLInputElement>(null);
const rightLoading = range(
page * rowsPerPage,
Math.min(totalPlayers - 1, page * rowsPerPage + rowsPerPage + 1),
).some((index) => !(index in leaderboard));
const leftLoading = range(
Math.max(0, page * rowsPerPage - 1),
Math.max(0, page * rowsPerPage - rowsPerPage - 1) + 1,
).some((index) => !(index in leaderboard));
useEffect(() => {
if (pageInput.current) pageInput.current.value = `${page + 1}`;
}, [page]);
return (
<Flex justify="center" wrap="wrap" gap="2">
<Flex gap="2">
<Button
variant="soft"
onClick={() => onPageChange(Math.max(page - 1, 0), true)}
disabled={page === 0 || leftLoading}
>
{leftLoading ? (
<SymbolIcon className={infiniteSpin} />
) : (
<CaretLeftIcon />
)}
</Button>
<TextField.Root>
<TextField.Slot>Page</TextField.Slot>
<TextField.Input
type="number"
ref={pageInput}
style={{ width: 64, textAlign: 'center' }}
onBlur={(event) => {
onPageChange(
Math.max(
0,
Math.min(pages - 1, event.target.valueAsNumber - 1),
),
false,
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
(event.target as HTMLInputElement).blur();
}
}}
/>
<TextField.Slot>out of {pages}</TextField.Slot>
</TextField.Root>
<Button
variant="soft"
onClick={() => {
onPageChange(Math.min(page + 1, pages - 1), true);
}}
disabled={page === pages - 1 || rightLoading}
>
{rightLoading ? (
<SymbolIcon className={infiniteSpin} />
) : (
<CaretRightIcon />
)}
</Button>
</Flex>
</Flex>
);
}
File diff suppressed because it is too large. Load diff
@@ -23,8 +23,6 @@ export function CustomColumn({ column, Builder }: CustomColumn) {
<Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger> <Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger>
<Builder.SubContent> <Builder.SubContent>
<Builder.Label>Display</Builder.Label>
<Builder.RadioGroup <Builder.RadioGroup
value={customColumn.display} value={customColumn.display}
onValueChange={(event) => onValueChange={(event) =>
+118 -28
View File
@@ -1,4 +1,21 @@
import { ContextMenu, DropdownMenu } from '@radix-ui/themes'; import {
Button,
ContextMenu,
Dialog,
DropdownMenu,
Flex,
Heading,
TextField,
} from '@radix-ui/themes';
import { createColors } from 'bepaint';
import { useRef, useState } from 'react';
import {
ACCENT_COLORS,
AccentColor,
GRAY_COLORS,
GrayColor,
PALETTES,
} from '../../../../constants/radixColors';
import { useSession } from '../../../../stores/session'; import { useSession } from '../../../../stores/session';
import { CustomColumn } from './CustomColumn'; import { CustomColumn } from './CustomColumn';
@@ -9,38 +26,111 @@ interface ContextMenuProps {
export function Menu({ Builder, reset }: ContextMenuProps) { export function Menu({ Builder, reset }: ContextMenuProps) {
const session = useSession(); const session = useSession();
const [showTitleEditor, setShowTitleEditor] = useState(false);
const titleInput = useRef<HTMLInputElement>(null);
return ( return (
<Builder.Content> <>
<CustomColumn Builder={Builder} column={0} /> <Dialog.Root open={showTitleEditor} onOpenChange={setShowTitleEditor}>
<CustomColumn Builder={Builder} column={1} /> <Dialog.Content>
<CustomColumn Builder={Builder} column={2} /> <Flex direction="column" gap="4">
<CustomColumn Builder={Builder} column={3} /> <Heading size="4">Edit title</Heading>
<Builder.Separator /> <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>
<Builder.CheckboxItem <Dialog.Close>
checked={session.showTotal} <Button
onCheckedChange={(checked) => onClick={() =>
useSession.setState({ showTotal: checked }) useSession.setState({ title: titleInput.current!.value })
} }
> >
Show total Set
</Builder.CheckboxItem> </Button>
<Builder.CheckboxItem </Dialog.Close>
checked={session.showCareer} </Flex>
onCheckedChange={(checked) => </Flex>
useSession.setState({ showCareer: checked }) </Dialog.Content>
} </Dialog.Root>
>
Show career
</Builder.CheckboxItem>
<Builder.Separator /> <Builder.Content>
<Builder.Sub>
<Builder.SubTrigger>Accent color</Builder.SubTrigger>
<Builder.Item color="red" onClick={reset}> <Builder.SubContent>
Reset <Builder.RadioGroup
</Builder.Item> value={session.color}
</Builder.Content> onValueChange={(value) =>
useSession.setState({ color: value as AccentColor | GrayColor })
}
>
{[...ACCENT_COLORS, ...GRAY_COLORS].map((color) => (
<Builder.RadioItem value={color} key={color}>
<Flex gap="2" align="center">
<div
style={{
backgroundColor: createColors(PALETTES[color])
.solidBackground,
width: 16,
height: 16,
borderRadius: '100%',
}}
/>
{`${color.charAt(0).toUpperCase()}${color.slice(1)}`}
</Flex>
</Builder.RadioItem>
))}
</Builder.RadioGroup>
</Builder.SubContent>
</Builder.Sub>
<Builder.Separator />
<Builder.Item onClick={() => setShowTitleEditor(true)}>
Edit title...
</Builder.Item>
<CustomColumn Builder={Builder} column={0} />
<CustomColumn Builder={Builder} column={1} />
<CustomColumn Builder={Builder} column={2} />
<CustomColumn Builder={Builder} column={3} />
<Builder.Separator />
<Builder.CheckboxItem
checked={session.showTotal}
onCheckedChange={(checked) =>
useSession.setState({ showTotal: checked })
}
>
Show total
</Builder.CheckboxItem>
<Builder.CheckboxItem
checked={session.showCareer}
onCheckedChange={(checked) =>
useSession.setState({ showCareer: checked })
}
>
Show career
</Builder.CheckboxItem>
<Builder.Separator />
<Builder.Item color="red" onClick={reset}>
Reset
</Builder.Item>
</Builder.Content>
</>
); );
} }
+16 -13
View File
@@ -51,7 +51,7 @@ export default function Page() {
}, [session]); }, [session]);
return ( return (
<PageWrapper> <PageWrapper color="blue">
<div className={styles.toolBar}> <div className={styles.toolBar}>
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}> <div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
<TextField.Root> <TextField.Root>
@@ -124,7 +124,7 @@ export default function Page() {
<Button <Button
variant="soft" variant="soft"
onClick={(event) => { onClick={(event) => {
if (!session.promptBeforeReset) { if (!session.resetPrompt) {
event.preventDefault(); event.preventDefault();
resetSession(); resetSession();
} }
@@ -153,7 +153,7 @@ export default function Page() {
mr="1" mr="1"
onCheckedChange={(checked) => onCheckedChange={(checked) =>
useSession.setState({ useSession.setState({
promptBeforeReset: !checked, resetPrompt: !checked,
}) })
} }
/> />
@@ -183,7 +183,7 @@ export default function Page() {
variant="soft" variant="soft"
className={styles.toolbarButton} className={styles.toolbarButton}
onClick={(event) => { onClick={(event) => {
if (!session.showEmbedPrompt) { if (!session.embedPrompt) {
event.preventDefault(); event.preventDefault();
resetSession(); resetSession();
} }
@@ -202,14 +202,17 @@ export default function Page() {
Copied link to your clipboard Copied link to your clipboard
</AlertDialog.Title> </AlertDialog.Title>
<AlertDialog.Description> <AlertDialog.Description>
Keep in mind: Instructions for streamers:
<ul> <iframe
<li>Paste this link into your browser to view the embed</li> style={{
<li>Embeds abide your custom options</li> width: '100%',
<li> aspectRatio: '16 / 9',
Options do not transfer across websites/streaming softwares border: 'none',
</li> }}
</ul> src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</AlertDialog.Description> </AlertDialog.Description>
<Text size="2"> <Text size="2">
@@ -217,7 +220,7 @@ export default function Page() {
mr="1" mr="1"
onCheckedChange={(checked) => onCheckedChange={(checked) =>
useSession.setState({ useSession.setState({
showEmbedPrompt: !checked, embedPrompt: !checked,
}) })
} }
/> />
+2
View File
@@ -11,6 +11,7 @@ const DOCS = {
introduction: 'guide/introduction', introduction: 'guide/introduction',
invite: 'guide/invite', invite: 'guide/invite',
timezones: 'guide/timezones', timezones: 'guide/timezones',
creators: 'guide/creators',
}; };
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = { const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
permissions: 'The permissions needed for the bot to function', permissions: 'The permissions needed for the bot to function',
@@ -18,6 +19,7 @@ const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
introduction: 'About the bot', introduction: 'About the bot',
invite: 'How to invite the bot', invite: 'How to invite the bot',
timezones: 'How Blitzkrieg infers and uses your timezone', timezones: 'How Blitzkrieg infers and uses your timezone',
creators: 'Who created the bot',
}; };
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) { function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
+130 -100
View File
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { chunk } from 'lodash';
import * as Breakdown from '../components/Breakdown'; import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
@@ -35,6 +36,9 @@ import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile'; import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryRaw } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 10;
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
@@ -132,38 +136,80 @@ async function render(
0, 0,
) / careerBattles; ) / careerBattles;
return ( const children: JSX.Element[] = [];
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
{filteredOrder.length === 0 && ( if (filteredOrder.length > 0) {
<NoData type={NoDataType.BattlesInPeriod} /> children.push(
)} <Breakdown.Row
{filteredOrder.length > 0 && ( key={id}
<Breakdown.Root> type="summary"
title="Total"
stats={[
{
title: 'Battles',
current: currentBattles,
career: careerBattles,
},
{
title: 'Winrate',
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
},
{
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
},
]}
/>,
);
await Promise.all(
filteredOrder.map(async (id, index) => {
const tankopediaEntry = awaitedTankopedia[id];
const current = orderedCurrentStats[index];
const career = orderedCareerStats[index];
const currentWN8 = orderedCurrentWN8[index];
const careerWN8 = orderedCareerWN8[index];
children.push(
<Breakdown.Row <Breakdown.Row
key={id} key={id}
type="summary" type="tank"
title="Total" tankType={tankopediaEntry?.type}
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
title={await resolveTankName(id)}
stats={[ stats={[
{ {
title: 'Battles', title: 'Battles',
current: currentBattles, current: current.battles.toLocaleString(),
career: careerBattles, career: career.battles.toLocaleString(),
}, },
{ {
title: 'Winrate', title: 'Winrate',
current: `${(100 * currentWinrate).toFixed(2)}%`, current: `${(100 * (current.wins / current.battles)).toFixed(
career: `${(100 * careerWinrate).toFixed(2)}%`, 2,
delta: currentWinrate - careerWinrate, )}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
current.wins / current.battles - career.wins / career.battles,
}, },
{ {
title: 'WN8', title: 'WN8',
@@ -182,83 +228,67 @@ async function render(
}, },
{ {
title: 'Damage', title: 'Damage',
current: Math.round(currentDamage).toLocaleString(), current: Math.round(
career: Math.round(careerDamage).toLocaleString(), current.damage_dealt / current.battles,
delta: currentDamage - careerDamage, ).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
}, },
]} ]}
/> />,
);
}),
);
}
{await Promise.all( const pages = chunk(
filteredOrder.map(async (id, index) => { children,
const tankopediaEntry = awaitedTankopedia[id]; Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
const current = orderedCurrentStats[index];
const career = orderedCareerStats[index];
const currentWN8 = orderedCurrentWN8[index];
const careerWN8 = orderedCareerWN8[index];
return (
<Breakdown.Row
key={id}
type="tank"
tankType={tankopediaEntry?.type}
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
title={await resolveTankName(id)}
stats={[
{
title: 'Battles',
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: 'Winrate',
current: `${(
100 *
(current.wins / current.battles)
).toFixed(2)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(
2,
)}%`,
delta:
current.wins / current.battles -
career.wins / career.battles,
},
{
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>
);
}),
)}
</Breakdown.Root>
)}
</Wrapper>
); );
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<Wrapper>
{index === 0 && (
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
)}
<Breakdown.Root>{page}</Breakdown.Root>
</Wrapper>
))
.reverse();
} else {
return [
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
{filteredOrder.length === 0 && (
<NoData type={NoDataType.BattlesInPeriod} />
)}
</Wrapper>,
];
}
} }
export const breakdownCommand = new Promise<CommandRegistryRaw>( export const breakdownCommand = new Promise<CommandRegistryRaw>(
@@ -286,11 +316,11 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
...filters, ...filters,
}); });
return Promise.all([ return [
render(player, period, filters), ...(await render(player, period, filters)),
buttonPrimary(path, 'Refresh'), buttonPrimary(path, 'Refresh'),
getBlitzStarsLinkButton(player.region, player.id), await getBlitzStarsLinkButton(player.region, player.id),
]); ];
}, },
autocomplete: (interaction) => { autocomplete: (interaction) => {
+15 -23
View File
@@ -1,11 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' }; import packageJSON from '../../package.json' assert { type: 'json' };
import getClientId from '../core/blitzkrieg/getClientId';
import { tankAverages } from '../core/blitzstars/tankAverages';
import { tankopedia } from '../core/blitzstars/tankopedia';
import { client } from '../core/discord/client'; import { client } from '../core/discord/client';
import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
const executionStart = new Date().getTime(); const executionStart = new Date().getTime();
@@ -21,24 +16,21 @@ export const debugCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const uptime = currentTime - executionStart; const uptime = currentTime - executionStart;
const list = [
['Version', packageJSON.version],
['Shard', client.shard?.ids[0] ?? 'default'],
[
'Uptime',
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
(uptime / 1000 / 60) % 60,
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
uptime % 1000,
)}ms`,
],
]
.map(([key, value]) => `- ${key}: ${value}`)
.join('\n');
return embedInfo( return `# Debug\n\n${list}`;
`${client.user?.username} debug information`,
markdownTable([
['Version', packageJSON.version],
['Client ID', getClientId()],
['Tag', interaction.client.user.tag],
[
'Uptime',
`${Math.floor(uptime / 1000 / 60 / 60 / 24)}d ${Math.floor(
(uptime / 1000 / 60 / 60) % 24,
)}h ${Math.floor((uptime / 1000 / 60) % 60)}m ${Math.floor(
(uptime / 1000) % 60,
)}s ${Math.floor(uptime % 1000)}ms`,
],
['Tankopedia cached', `${(await tankopedia) !== undefined}`],
['Tank averages cached', `${(await tankAverages) !== undefined}`],
]),
);
}, },
}; };
+1 -1
View File
@@ -8,13 +8,13 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats'; import getTankStats from '../core/blitz/getTankStats';
import getTreeType from '../core/blitz/getTreeType'; import getTreeType from '../core/blitz/getTreeType';
import resolveTankName from '../core/blitz/resolveTankName'; import resolveTankName from '../core/blitz/resolveTankName';
import { tankopediaInfo } from '../core/blitz/tankopediaInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { import {
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
TankopediaEntry, TankopediaEntry,
Tier, Tier,
tankopedia, tankopedia,
tankopediaInfo,
} from '../core/blitzstars/tankopedia'; } from '../core/blitzstars/tankopedia';
import addTierChoices from '../core/discord/addTierChoices'; import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
+70 -118
View File
@@ -4,21 +4,24 @@ import {
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder, SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { range } from 'lodash';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import * as Leaderboard from '../components/Leaderboard'; import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { LEAGUES } from '../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings';
import { REGION_NAMES_SHORT, Region } from '../constants/regions'; import { REGION_NAMES_SHORT, Region } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo';
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain'; import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo'; import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
import getMidnightLeaderboard, { import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
DATABASE_REPO, import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
} from '../core/blitzkrieg/getMidnightLeaderboard'; import { UserError } from '../core/blitzkrieg/userError';
import { octokit } from '../core/blitzkrieg/octokit';
import throwError from '../core/blitzkrieg/throwError';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -113,14 +116,6 @@ export interface RatingsNeighbors {
neighbors: RatingsPlayer[]; neighbors: RatingsPlayer[];
} }
const LEAGUES = [
{ name: 'Diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 },
];
function addSubcommands( function addSubcommands(
option: SlashCommandSubcommandGroupBuilder, option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option, addOptions = (option: SlashCommandSubcommandBuilder) => option,
@@ -129,14 +124,14 @@ function addSubcommands(
.addSubcommand((option) => .addSubcommand((option) =>
addOptions( addOptions(
option option
.setName('neighbours') .setName('neighbors')
.setDescription('Your position and neighbours'), .setDescription('Your position and neighbors'),
) )
.addStringOption(addUsernameChoices) .addStringOption(addUsernameChoices)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName('limit')
.setDescription('How many neighbours to display (default: 10)') .setDescription('How many neighbors to display (default: 10)')
.setRequired(false) .setRequired(false)
.setMinValue(5) .setMinValue(5)
.setMaxValue(30), .setMaxValue(30),
@@ -178,23 +173,17 @@ const noOngoingSeason = embedNegative(
export const ratingsCommand = new Promise<CommandRegistryRaw>( export const ratingsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => { async (resolve) => {
const seasonChoices = await octokit.repos const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
.getContent({ const seasonChoices = range(
...DATABASE_REPO, FIRST_ARCHIVED_RATINGS_SEASON,
path: 'com/ratings', latestArchivedSeasonNumber + 1,
}) ).map(
.then(({ data }) => { (number) =>
if (!Array.isArray(data)) { ({
throw throwError('Archived ratings data is malformed'); name: `Season ${number}`,
} value: `${number}`,
return data.map( }) satisfies APIApplicationCommandOptionChoice<string>,
(folder) => );
({
name: `Season ${folder.name}`,
value: folder.name,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
});
const command = new SlashCommandBuilder() const command = new SlashCommandBuilder()
.setName('ratings') .setName('ratings')
@@ -240,7 +229,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
| 'current'; | 'current';
const subcommand = interaction.options.getSubcommand(true) as const subcommand = interaction.options.getSubcommand(true) as
| 'league' | 'league'
| 'neighbours'; | 'neighbors';
const limit = interaction.options.getInteger('limit') ?? 10; const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season'); const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined; const season = seasonOption ? parseInt(seasonOption) : undefined;
@@ -262,18 +251,14 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
const region = interaction.options.getString('region') as Region; const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (subcommandGroup === 'current' regionRatingsInfo = await (subcommandGroup === 'current'
? getRatingsInfo(region) ? getRatingsInfo(region)
: getArchivedRatingsInfo(season!, region)); : getArchivedRatingsInfo(region, season!));
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const regionSubdomain = regionToRegionSubdomain(region);
const result = const result =
subcommandGroup === 'current' subcommandGroup === 'current'
? await fetch( ? await getRatingsLeague(region, leagueIndex).then(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`, async (result) =>
)
.then((response) => response.json() as Promise<LeagueTop>)
.then(async ({ result }) =>
result.slice(0, limit).map( result.slice(0, limit).map(
(player) => (player) =>
({ ({
@@ -284,20 +269,9 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
nickname: player.nickname, nickname: player.nickname,
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer,
), ),
) )
: await octokit.repos : await getArchivedRatingsLeaderboard(region, season!).then(
.getContent({ async (players) => {
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings data is malformed');
}
const players = (await fetch(data.download_url!).then(
(response) => response.json(),
)) as BlitzkriegRatingsLeaderboard;
const firstPlayerIndex = const firstPlayerIndex =
leagueIndex === 0 leagueIndex === 0
? 0 ? 0
@@ -328,15 +302,16 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
`Unknown Player ${player.id}`, `Unknown Player ${player.id}`,
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer,
); );
}); },
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = midnightLeaderboard =
subcommandGroup === 'current' subcommandGroup === 'current'
? await getMidnightLeaderboard( ? await getArchivedRatingsMidnightLeaderboard(
region, region,
regionRatingsInfo.current_season, regionRatingsInfo.current_season,
) )
: await getMidnightLeaderboard(region, season!); : await getArchivedRatingsMidnightLeaderboard(region, season!);
players = result; players = result;
playersBefore = result ? result[0].position - 1 : 0; playersBefore = result ? result[0].position - 1 : 0;
@@ -354,73 +329,43 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
regionRatingsInfo = regionRatingsInfo =
subcommandGroup === 'current' subcommandGroup === 'current'
? await getRatingsInfo(region) ? await getRatingsInfo(region)
: await octokit.repos : await getArchivedRatingsInfo(region, season!);
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings info is malformed');
}
const response = fetch(data.download_url!);
return (await response.then((response) =>
response.json(),
)) as RatingsInfo;
});
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const regionSubdomain = regionToRegionSubdomain(region);
const neighbors = const neighbors =
subcommandGroup === 'current' subcommandGroup === 'current'
? await fetch( ? await getRatingsNeighbors(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round( region,
limit / 2, id,
)}`, Math.round(limit / 2),
).then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
) )
.then( : await getArchivedRatingsLeaderboard(region, season!).then(
(response) => response.json() as Promise<RatingsNeighbors>, async (players) => {
)
.then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError(
'Archived ratings latest data is malformed',
);
}
const response = fetch(data.download_url!);
const players = (await response.then((response) =>
response.json(),
)) as BlitzkriegRatingsLeaderboard;
const playerIndex = players.findIndex( const playerIndex = players.findIndex(
(player) => player.id === id, (player) => player.id === id,
); );
if (playerIndex === -1) { if (playerIndex === -1) {
throw throwError( throw new UserError(
`${accountInfo.nickname} didn't player ratings in season ${season}`, `${accountInfo.nickname} didn't player ratings in season ${season}`,
'This player did not participate in this season or did not get past calibration.', {
cause:
'This player did not participate in this season or did not get past calibration.',
},
); );
} }
@@ -442,14 +387,15 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
clan: clanData[player.id]!.clan?.tag, clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name, nickname: clanData[player.id]!.account_name,
})); }));
}); },
);
midnightLeaderboard = midnightLeaderboard =
subcommandGroup === 'current' subcommandGroup === 'current'
? await getMidnightLeaderboard( ? await getArchivedRatingsMidnightLeaderboard(
region, region,
regionRatingsInfo.current_season, regionRatingsInfo.current_season,
) )
: await getMidnightLeaderboard(region, season!); : await getArchivedRatingsMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`; if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
@@ -460,7 +406,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
titleName = neighbors titleName = neighbors
? accountInfo.nickname ? accountInfo.nickname
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`; : `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbours'; titleDescription = 'Ratings neighbors';
playersBefore = neighbors ? neighbors[0].position - 1 : 0; playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter = playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].position; regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
@@ -513,10 +459,16 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
{items && ( {items && (
<Leaderboard.Root> <Leaderboard.Root>
{playersBefore > 0 && ( {playersBefore > 0 && (
<Leaderboard.Gap number={playersBefore} /> <Leaderboard.Gap
message={`+ ${playersBefore.toLocaleString()} more`}
/>
)} )}
{items} {items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />} {playersAfter > 0 && (
<Leaderboard.Gap
message={`+ ${playersAfter.toLocaleString()} more`}
/>
)}
</Leaderboard.Root> </Leaderboard.Root>
)} )}
</Wrapper> </Wrapper>
+1 -1
View File
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions'; import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming'; import fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
+1 -1
View File
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions'; import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming'; import fetchBlitz from '../core/blitz/fetchBlitz';
import { import {
AccountList, AccountList,
usernamePattern, usernamePattern,
-18
View File
@@ -1,18 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate';
/**
* TODO: @deprecated
*/
export const todayCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('today')
.setDescription('This command has been renamed to `/breakdown today`'),
handler() {
return '# This command has been renamed to `/breakdown today`\n`/today` now comes with filters, periods and more! Use `/breakdown today` for same old functionality but also do checkout the other options.';
},
};
+4 -1
View File
@@ -6,7 +6,10 @@ export interface RootProps {
export function Root({ children }: RootProps) { export function Root({ children }: RootProps) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div
className="session-tracker-root"
style={{ display: 'flex', flexDirection: 'column', gap: 8 }}
>
{children} {children}
</div> </div>
); );
+33 -21
View File
@@ -1,5 +1,10 @@
import { createColors } from 'bepaint';
import { Percentile } from '../../../constants/percentiles'; import { Percentile } from '../../../constants/percentiles';
import { theme } from '../../../stitches.config'; import {
AccentColor,
GrayColor,
PALETTES,
} from '../../../constants/radixColors';
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks'; import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
import { RowStat } from './RowStat'; import { RowStat } from './RowStat';
@@ -17,18 +22,29 @@ interface RowProps {
minimized?: boolean; minimized?: boolean;
treeType?: TreeTypeEnum; treeType?: TreeTypeEnum;
tankType?: string; tankType?: string;
color?: AccentColor | GrayColor;
stats: (RowStatItem | undefined)[]; stats: (RowStatItem | undefined)[];
} }
export function Row(props: RowProps) { export function Row({
color = 'slate',
stats,
title,
minimized,
tankType,
treeType,
type,
}: RowProps) {
const theme = createColors(PALETTES[`${color}Dark`]);
return ( return (
<div <div
className="session-tracker-row"
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
borderRadius: 4, borderRadius: 4,
backgroundColor: theme.colors.appBackground2, backgroundColor: theme.appBackground2,
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
@@ -39,16 +55,12 @@ export function Row(props: RowProps) {
justifyContent: 'center', justifyContent: 'center',
padding: 8, padding: 8,
gap: 4, gap: 4,
backgroundColor: theme.colors.componentInteractive, backgroundColor: theme.componentInteractive,
}} }}
> >
{props.type === 'tank' && props.tankType !== undefined && ( {type === 'tank' && tankType !== undefined && (
<img <img
src={ src={TREE_TYPE_ICONS[treeType ?? TreeTypeEnum.TechTree][tankType]}
TREE_TYPE_ICONS[props.treeType ?? TreeTypeEnum.TechTree][
props.tankType
]
}
style={{ width: 16, height: 16 }} style={{ width: 16, height: 16 }}
/> />
)} )}
@@ -56,18 +68,18 @@ export function Row(props: RowProps) {
<span <span
style={{ style={{
color: color:
props.type === 'tank' type === 'tank'
? props.treeType === TreeTypeEnum.Collector ? treeType === TreeTypeEnum.Collector
? theme.colors.textLowContrast_blue ? theme.textLowContrast_blue
: props.treeType === TreeTypeEnum.Premium : treeType === TreeTypeEnum.Premium
? theme.colors.textLowContrast_amber ? theme.textLowContrast_amber
: theme.colors.textHighContrast : theme.textHighContrast
: theme.colors.textLowContrast, : theme.textLowContrast,
fontWeight: 900, fontWeight: 900,
fontSize: 16, fontSize: 16,
}} }}
> >
{props.title} {title}
</span> </span>
</div> </div>
@@ -78,11 +90,11 @@ export function Row(props: RowProps) {
paddingBottom: 8, paddingBottom: 8,
}} }}
> >
{props.stats.map( {stats.map(
(row, index) => (row, index) =>
row && ( row && (
<RowStat <RowStat
minimized={props.minimized} minimized={minimized}
key={index} key={index}
name={`${row.title} • ${row.career ?? '--'}`} name={`${row.title} • ${row.career ?? '--'}`}
value={row.current ?? '--'} value={row.current ?? '--'}
@@ -1,5 +1,10 @@
import { createColors } from 'bepaint';
import { Percentile } from '../../../constants/percentiles'; import { Percentile } from '../../../constants/percentiles';
import { theme } from '../../../stitches.config'; import {
AccentColor,
GrayColor,
PALETTES,
} from '../../../constants/radixColors';
import PercentileIndicator from '../../PercentileIndicator'; import PercentileIndicator from '../../PercentileIndicator';
export interface RowStatProps { export interface RowStatProps {
@@ -8,6 +13,7 @@ export interface RowStatProps {
delta?: number; delta?: number;
percentile?: Percentile; percentile?: Percentile;
minimized?: boolean; minimized?: boolean;
color?: AccentColor | GrayColor;
} }
export function RowStat({ export function RowStat({
@@ -16,9 +22,13 @@ export function RowStat({
delta, delta,
percentile, percentile,
minimized = false, minimized = false,
color = 'slate',
}: RowStatProps) { }: RowStatProps) {
const theme = createColors(PALETTES[`${color}Dark`]);
return ( return (
<div <div
className="session-tracker-row-stat"
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -52,7 +62,7 @@ export function RowStat({
<span <span
style={{ style={{
color: theme.colors.textHighContrast, color: theme.textHighContrast,
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 16, fontSize: 16,
textAlign: 'center', textAlign: 'center',
@@ -65,7 +75,7 @@ export function RowStat({
{!minimized && ( {!minimized && (
<span <span
style={{ style={{
color: theme.colors.textLowContrast, color: theme.textLowContrast,
textAlign: 'center', textAlign: 'center',
fontSize: 12, fontSize: 12,
}} }}
@@ -15,9 +15,18 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
alignItems: 'center', alignItems: 'center',
}} }}
> >
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}> <span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
whiteSpace: 'nowrap',
maxWidth: 128,
overflow: 'hidden',
}}
>
{stat[0]} {stat[0]}
</span> </span>
<span <span
style={{ style={{
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
@@ -1,10 +1,10 @@
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
export interface GapProps { export interface GapProps {
number: number; message: string;
} }
export function Gap({ number }: GapProps) { export function Gap({ message: number }: GapProps) {
return ( return (
<div <div
style={{ style={{
@@ -22,7 +22,7 @@ export function Gap({ number }: GapProps) {
fontSize: 16, fontSize: 16,
}} }}
> >
+ {number.toLocaleString()} more {number.toLocaleString()}
</span> </span>
</div> </div>
); );
+18 -12
View File
@@ -1,10 +1,11 @@
import { ComponentProps } from 'react';
import { RatingsReward } from '../../../commands/ratings'; import { RatingsReward } from '../../../commands/ratings';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
export interface ItemProps { export interface ItemProps extends ComponentProps<'div'> {
position: number; position: number;
deltaPosition?: number; deltaPosition?: number;
score: number; score?: number;
deltaScore?: number; deltaScore?: number;
reward?: RatingsReward; reward?: RatingsReward;
nickname: string; nickname: string;
@@ -25,10 +26,13 @@ export function Item({
nickname, nickname,
clan, clan,
highlight, highlight,
...props
}: ItemProps) { }: ItemProps) {
return ( return (
<div <div
{...props}
style={{ style={{
cursor: 'pointer',
display: 'flex', display: 'flex',
padding: 8, padding: 8,
backgroundColor: highlight backgroundColor: highlight
@@ -185,16 +189,18 @@ export function Item({
} }
/> />
)} )}
<span {score !== undefined && (
style={{ <span
color: highlight style={{
? theme.colors.textHighContrast_blue color: highlight
: theme.colors.textHighContrast, ? theme.colors.textHighContrast_blue
fontSize: 16, : theme.colors.textHighContrast,
}} fontSize: 16,
> }}
{score.toLocaleString()} >
</span> {score.toLocaleString()}
</span>
)}
</div> </div>
</div> </div>
); );
+22 -15
View File
@@ -1,24 +1,31 @@
import { Flex, Theme } from '@radix-ui/themes';
import { ComponentProps } from 'react'; import { ComponentProps } from 'react';
interface PageWrapperProps extends ComponentProps<typeof Flex> {
color?: ComponentProps<typeof Theme>['accentColor'];
}
export default function PageWrapper({ export default function PageWrapper({
style, style,
color,
...props ...props
}: ComponentProps<'div'>) { }: PageWrapperProps) {
return ( return (
<div <Theme accentColor={color}>
style={{ <Flex
width: '100%', {...props}
maxWidth: 800, direction="column"
margin: 'auto', gap="4"
display: 'flex', style={{
padding: 16, width: '100%',
flexDirection: 'column', maxWidth: 800,
gap: 16, margin: 'auto',
boxSizing: 'border-box', padding: 16,
boxSizing: 'border-box',
...style, ...style,
}} }}
{...props} />
/> </Theme>
); );
} }
+13
View File
@@ -0,0 +1,13 @@
import { slateDarkA } from '@radix-ui/colors';
import { keyframes, style } from '@vanilla-extract/css';
const animation = keyframes({
'0%': { backgroundColor: slateDarkA.slateA2 },
'50%': { backgroundColor: slateDarkA.slateA3 },
'100%': { backgroundColor: slateDarkA.slateA2 },
});
export const skeleton = style({
borderRadius: 8,
animation: `${animation} 2s ease-in-out infinite`,
});
+6
View File
@@ -0,0 +1,6 @@
import { ComponentProps } from 'react';
import { skeleton } from './index.css';
export function Skeleton({ className, ...props }: ComponentProps<'div'>) {
return <div {...props} className={`${className} ${skeleton}`} />;
}
+7
View File
@@ -0,0 +1,7 @@
export const LEAGUES = [
{ name: 'Diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 },
];
+11
View File
@@ -0,0 +1,11 @@
export const NATION_IDS: Record<string, number> = {
ussr: 0,
germany: 1,
usa: 2,
china: 3,
france: 4,
uk: 5,
japan: 6,
other: 7,
european: 8,
};
+55
View File
@@ -0,0 +1,55 @@
import * as colors from '@radix-ui/colors';
import { Theme } from '@radix-ui/themes';
import { ComponentProps } from 'react';
export type AccentColor = Exclude<
ComponentProps<typeof Theme>['accentColor'],
'gray' | undefined
>;
export type AccentColorDark = `${AccentColor}Dark`;
export type GrayColor = Exclude<
ComponentProps<typeof Theme>['grayColor'],
'auto' | undefined
>;
export type GrayColorDark = `${GrayColor}Dark`;
export const ACCENT_COLORS: AccentColor[] = [
'tomato',
'red',
'ruby',
'crimson',
'pink',
'plum',
'purple',
'violet',
'iris',
'indigo',
'blue',
'cyan',
'teal',
'jade',
'green',
'grass',
'brown',
'orange',
'sky',
'mint',
'lime',
'yellow',
'amber',
'gold',
'bronze',
];
export const GRAY_COLORS: GrayColor[] = [
'gray',
'mauve',
'slate',
'sage',
'olive',
'sand',
];
export const PALETTES: Record<
AccentColor | AccentColorDark | GrayColor | GrayColorDark,
{ [key: `${string}${number}`]: string }
> = colors;
+1
View File
@@ -0,0 +1 @@
export const FIRST_ARCHIVED_RATINGS_SEASON = 49;
+4 -2
View File
@@ -1,4 +1,6 @@
export type Region = 'com' | 'eu' | 'asia'; export const REGIONS = ['com', 'eu', 'asia'] as const;
export type Region = (typeof REGIONS)[number];
export type RegionSubdomain = 'na' | 'eu' | 'asia'; export type RegionSubdomain = 'na' | 'eu' | 'asia';
export const REGION_NAMES: Record<Region, string> = { export const REGION_NAMES: Record<Region, string> = {
@@ -11,7 +13,7 @@ export const REGION_NAMES: Record<Region, string> = {
export const REGION_NAMES_SHORT: Record<Region, string> = { export const REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA', com: 'NA',
eu: 'EU', eu: 'EU',
asia: 'AS', asia: 'ASIA',
}; };
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = { export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
+2 -3
View File
@@ -9,13 +9,12 @@ export const TOOLS: Tool[] = [
{ {
id: 'session', id: 'session',
title: 'Session tracker', title: 'Session tracker',
description: 'Track your performance in a session', description: 'Live session performance tracker',
}, },
{ {
id: 'ratings', id: 'ratings',
title: 'Ratings', title: 'Ratings',
description: 'Current and old season leader boards', description: 'Current and archived ratings leaderboards',
disabled: true,
}, },
{ {
id: 'tankopedia', id: 'tankopedia',
+56
View File
@@ -0,0 +1,56 @@
export type BlitzResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
}
| {
status: 'ok';
data: Data;
};
const MAX_CALLS_PER_SECOND = 10;
const queue: { url: string; resolve: (data: object) => void }[] = [];
let inProgress = 0;
async function manageQueue() {
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
inProgress++;
const timeout = setTimeout(() => {
inProgress--;
manageQueue();
}, 1000);
const { url, resolve } = queue.shift()!;
const data = (await fetch(url)
.then((response) => response.json())
.catch((error) => ({ status: 'error', error }))) as BlitzResponse<
typeof resolve
>;
if (data.status === 'ok') {
resolve(data.data);
} else {
clearTimeout(timeout);
inProgress--;
manageQueue();
throw new Error(`Wargaming response error status: "${data.status}"`, {
cause: `Message: "${data.error.message}"\nURL: "${url}"`,
});
}
}
}
export default function fetchBlitz<Data extends object>(url: string) {
return new Promise<Data>((resolve) => {
queue.push({
url,
resolve(data) {
resolve(data as Data);
},
});
manageQueue();
});
}
-26
View File
@@ -1,26 +0,0 @@
import throwError from '../blitzkrieg/throwError';
export type BlitzResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
}
| {
status: 'ok';
data: Data;
};
export default async function fetchBlitz<Data extends object>(url: string) {
const data = (await fetch(url).then((response) =>
response.json(),
)) as BlitzResponse<Data>;
if (data.status === 'ok') {
return data.data;
} else {
throw throwError(
`Wargaming response error status:"${data.status}"`,
`Message: "${data.error.message}"\nURL: "${url}"`,
);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
interface IndividualAccountAchievements { interface IndividualAccountAchievements {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
export interface AllStats { export interface AllStats {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeExtra } from './normalizeExtra'; import { normalizeExtra } from './normalizeExtra';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
+2 -2
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
export interface IndividualClanInfo { export interface IndividualClanInfo {
@@ -41,7 +41,7 @@ export async function getClanInfo<
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[], ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) { >(region: Region, ids: Ids) {
const object = await fetchBlitz<ClanInfo>( const object = await fetchBlitz<ClanInfo>(
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds( `https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${normalizeIds(
ids, ids,
)}`, )}`,
); );
+10 -4
View File
@@ -1,12 +1,18 @@
import { RatingsInfo } from '../../commands/ratings'; import { RatingsInfo } from '../../commands/ratings';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { context } from '../blitzkrieg/context';
import { patientFetch } from '../blitzkrieg/patientFetch';
import regionToRegionSubdomain from './regionToRegionSubdomain'; import regionToRegionSubdomain from './regionToRegionSubdomain';
export default async function getRatingsInfo(region: Region) { export default async function getRatingsInfo(region: Region) {
const response = await fetch( const response = await patientFetch(
`https://${regionToRegionSubdomain( context === 'website'
region, ? `/api/${region}/ratings/current/info`
)}.wotblitz.com/en/api/rating-leaderboards/season/`, : `https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
undefined,
{ cache: 'no-store' },
); );
return (await response.json()) as Promise<RatingsInfo>; return (await response.json()) as Promise<RatingsInfo>;
+28
View File
@@ -0,0 +1,28 @@
import { LeagueTop, RatingsPlayer } from '../../commands/ratings';
import { Region } from '../../constants/regions';
import { context } from '../blitzkrieg/context';
import { patientFetch } from '../blitzkrieg/patientFetch';
import regionToRegionSubdomain from './regionToRegionSubdomain';
export async function getRatingsLeague(
region: Region,
league: number,
usePatientFetcher = false,
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
const response = await fetcher(
context === 'website'
? `/api/${region}/ratings/current/league/${league}`
: `https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
);
const json = await response.json();
return context === 'website'
? (json as RatingsPlayer[])
: (json as LeagueTop).result;
}
+27
View File
@@ -0,0 +1,27 @@
import { RatingsNeighbors } from '../../commands/ratings';
import { Region } from '../../constants/regions';
import { patientFetch } from '../blitzkrieg/patientFetch';
import { withCORSProxy } from '../blitzkrieg/withCORSProxy';
import regionToRegionSubdomain from './regionToRegionSubdomain';
export async function getRatingsNeighbors(
region: Region,
id: number,
count: number,
usePatientFetcher = false,
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
const response = await fetcher(
withCORSProxy(
`https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`,
),
);
const json = (await response.json()) as RatingsNeighbors;
return json;
}
+6 -6
View File
@@ -1,8 +1,8 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { TanksStats } from '../../types/tanksStats'; import { TanksStats } from '../../types/tanksStats';
import throwError from '../blitzkrieg/throwError'; import { UserError } from '../blitzkrieg/userError';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
export default async function getTankStats(region: Region, id: number) { export default async function getTankStats(region: Region, id: number) {
const tankStats = await fetchBlitz<TanksStats>( const tankStats = await fetchBlitz<TanksStats>(
@@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) {
); );
if (tankStats[id] === null) { if (tankStats[id] === null) {
throw throwError( throw new UserError('No tank stats available', {
'No tank stats available', cause:
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.", "This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
); });
} }
return tankStats[id]; return tankStats[id];
+7 -9
View File
@@ -1,5 +1,5 @@
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import throwError from '../blitzkrieg/throwError'; import { UserError } from '../blitzkrieg/userError';
import { TANKS, tankopedia } from '../blitzstars/tankopedia'; import { TANKS, tankopedia } from '../blitzstars/tankopedia';
export default async function resolveTankId(tank: string | number) { export default async function resolveTankId(tank: string | number) {
@@ -12,10 +12,9 @@ export default async function resolveTankId(tank: string | number) {
}); });
if (searchResult.length === 0) { if (searchResult.length === 0) {
throw throwError( throw new UserError('Tank not found', {
'Tank not found', cause: `Could not find tank by the name "${tank}".`,
`Could not find tank by the name "${tank}".`, });
);
} else { } else {
return searchResult[0].obj.tank_id; return searchResult[0].obj.tank_id;
} }
@@ -23,10 +22,9 @@ export default async function resolveTankId(tank: string | number) {
if ((await tankopedia)[number]) { if ((await tankopedia)[number]) {
return number; return number;
} else { } else {
throw throwError( throw new UserError('Tank not found', {
'Tank not found', cause: `Could not find tank by the ID "${number}".`,
`Could not find tank by the ID "${number}".`, });
);
} }
} }
} }
+20 -18
View File
@@ -1,7 +1,7 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { Clan, ClanList } from '../../types/clanList'; import { Clan, ClanList } from '../../types/clanList';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
export type ClanListWithRegion = (Clan & { export type ClanListWithRegion = (Clan & {
region: 'com' | 'eu' | 'asia'; region: 'com' | 'eu' | 'asia';
@@ -9,43 +9,45 @@ export type ClanListWithRegion = (Clan & {
export default async function searchClansAcrossRegions( export default async function searchClansAcrossRegions(
search: string, search: string,
limit = 9, limit = 25,
) { ) {
const trimmed = search.trim(); const trimmed = search.trim();
if (trimmed.length < 3 && trimmed.length > 100) return []; if (trimmed.length < 3 && trimmed.length > 100) return [];
const normalizedLimit = Math.round(limit / 3); const normalizedLimit = Math.floor(limit / 3);
// TODO: remove splice once wargaming fixes the functionality of the limit felid
return ( return (
await Promise.all([ await Promise.all([
fetchBlitz<ClanList>( fetchBlitz<ClanList>(
`https://api.wotblitz.com/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`, `https://api.wotblitz.com/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
).then( ).then((value) =>
(value) => value
value && .map((account) => ({
value.map((account) => ({
...account, ...account,
region: 'com' as Region, region: 'com' as Region,
})), }))
.splice(0, normalizedLimit),
), ),
fetchBlitz<ClanList>( fetchBlitz<ClanList>(
`https://api.wotblitz.eu/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`, `https://api.wotblitz.eu/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
).then( ).then((value) =>
(value) => value
value && .map((account) => ({
value.map((account) => ({
...account, ...account,
region: 'eu' as Region, region: 'eu' as Region,
})), }))
.splice(0, normalizedLimit),
), ),
fetchBlitz<ClanList>( fetchBlitz<ClanList>(
`https://api.wotblitz.asia/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`, `https://api.wotblitz.asia/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
).then( ).then((value) =>
(value) => value
value && .map((account) => ({
value.map((account) => ({
...account, ...account,
region: 'asia' as Region, region: 'asia' as Region,
})), }))
.splice(0, normalizedLimit),
), ),
]) ])
) )
@@ -0,0 +1,43 @@
import { Region } from '../../constants/regions';
import { context } from '../blitzkrieg/context';
import regionToRegionSubdomain from './regionToRegionSubdomain';
type CurrentRatingsPlayerSearch = Record<
number,
{
spa_id: number;
nickname: string;
clan_tag: string;
} & (
| {
skip: true;
}
| {
skip: false;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
updated_at: string;
neighbors: [];
}
)
>;
export async function searchCurrentRatingsPlayers(
region: Region,
name: string,
) {
const encodedSearch = encodeURIComponent(name);
const response = await fetch(
context === 'website'
? `/api/${region}/ratings/current/search/${encodedSearch}`
: `https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
{ cache: 'no-store' },
);
const accountList = (await response.json()) as CurrentRatingsPlayerSearch;
return accountList;
}
+6 -6
View File
@@ -1,13 +1,13 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
interface Account { export interface AccountListItem {
nickname: string; nickname: string;
account_id: number; account_id: number;
} }
export type AccountList = Account[]; export type AccountList = AccountListItem[];
export type AccountListWithServer = (Account & { export type AccountListWithServer = (AccountListItem & {
region: Region; region: Region;
})[]; })[];
@@ -16,10 +16,10 @@ export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
export default async function searchPlayersAcrossRegions( export default async function searchPlayersAcrossRegions(
search: string, search: string,
limit = 9, limit = 25,
) { ) {
const trimmedSearch = search.trim(); const trimmedSearch = search.trim();
const normalizedLimit = Math.round(limit / 3); const normalizedLimit = Math.floor(limit / 3);
const encodedSearch = encodeURIComponent(trimmedSearch); const encodedSearch = encodeURIComponent(trimmedSearch);
if (usernamePattern.test(trimmedSearch)) { if (usernamePattern.test(trimmedSearch)) {
+15
View File
@@ -0,0 +1,15 @@
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchBlitz';
export interface TankopediaInfo {
achievement_sections: Record<string, { name: string; order: number }>;
tanks_updated_at: number;
languages: Record<string, string>;
vehicle_types: Record<string, string>;
vehicle_nations: Record<string, string>;
game_version: string;
}
export const tankopediaInfo = fetchBlitz<TankopediaInfo>(
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
);
+2 -2
View File
@@ -1,4 +1,4 @@
type Context = 'website' | 'bot'; type Context = 'website' | 'server';
export const context: Context = export const context: Context =
typeof window === 'undefined' ? 'bot' : 'website'; typeof window === 'undefined' ? 'server' : 'website';
+2 -5
View File
@@ -1,10 +1,7 @@
import { TREE_TYPE_NAMES } from '../../components/Tanks'; import { TREE_TYPE_NAMES } from '../../components/Tanks';
import resolveTankName from '../blitz/resolveTankName'; import resolveTankName from '../blitz/resolveTankName';
import { import { tankopediaInfo } from '../blitz/tankopediaInfo';
TIER_ROMAN_NUMERALS, import { TIER_ROMAN_NUMERALS, Tier } from '../blitzstars/tankopedia';
Tier,
tankopediaInfo,
} from '../blitzstars/tankopedia';
import { StatFilters } from '../statistics/filterStats'; import { StatFilters } from '../statistics/filterStats';
export async function filtersToDescription({ export async function filtersToDescription({
@@ -0,0 +1,33 @@
import getRatingsInfo from '../blitz/getRatingsInfo';
let cachedLatestArchivedSeasonNumber: number | null = null;
export async function getArchivedLatestSeasonNumber() {
const ratingsInfo = await getRatingsInfo('com');
if (ratingsInfo.detail) {
const comURL = (
(await fetch(
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main/regions',
).then((response) => response.json())) as {
tree: { path: string; url: string }[];
}
).tree.find(({ path }) => path === 'com')!.url;
const ratingsURL = (
(await fetch(comURL).then((response) => response.json())) as {
tree: [{ url: string }];
}
).tree[0].url;
const latestSeason = Math.max(
...(
(await fetch(ratingsURL).then((response) => {
response.json();
})) as { tree: { path: string }[] }
).tree.map(({ path }) => parseInt(path)),
);
return latestSeason;
} else {
return ratingsInfo.current_season - 1;
}
}
+9 -18
View File
@@ -1,10 +1,7 @@
import { RatingsInfo } from '../../commands/ratings'; import { RatingsInfo } from '../../commands/ratings';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { DATABASE_REPO } from './getMidnightLeaderboard';
import { octokit } from './octokit';
import throwError from './throwError';
const ARCHIVED_RATINGS_CACHE: Record< const INFO_CACHE: Record<
Region, Region,
Record< Record<
number, number,
@@ -19,27 +16,21 @@ const ARCHIVED_RATINGS_CACHE: Record<
}; };
export default async function getArchivedRatingsInfo( export default async function getArchivedRatingsInfo(
season: number,
region: Region, region: Region,
season: number,
) { ) {
if (ARCHIVED_RATINGS_CACHE[region][season]) { if (INFO_CACHE[region][season]) {
return ARCHIVED_RATINGS_CACHE[region][season]; return INFO_CACHE[region][season];
} }
const { data } = await octokit.repos.getContent({ const response = await fetch(
...DATABASE_REPO, `https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/regions/${region}/ratings/${season}/info.json`,
path: `${region}/ratings/${season}/info.json`, );
}); const jsonContent = (await response.json()) as RatingsInfo & {
if (Array.isArray(data) || data.type !== 'file')
throw throwError('Archived ratings info is malformed');
const content = Buffer.from(data.content, 'base64').toString();
const jsonContent = JSON.parse(content) as RatingsInfo & {
detail: undefined; detail: undefined;
}; };
ARCHIVED_RATINGS_CACHE[region][season] = jsonContent; INFO_CACHE[region][season] = jsonContent;
return jsonContent; return jsonContent;
} }
@@ -0,0 +1,28 @@
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
import { Region } from '../../constants/regions';
const LEADERBOARD_CACHE: Record<
Region,
Record<number, BlitzkriegRatingsLeaderboard>
> = {
asia: {},
eu: {},
com: {},
};
export async function getArchivedRatingsLeaderboard(
region: Region,
season: number,
) {
if (LEADERBOARD_CACHE[region][season]) {
return LEADERBOARD_CACHE[region][season];
}
const response = await fetch(
`https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/regions/${region}/ratings/${season}/latest.json`,
);
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
LEADERBOARD_CACHE[region][season] = json;
return json;
}
@@ -1,8 +1,7 @@
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard'; import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { octokit } from './octokit';
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' }; export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-assets' };
const MIDNIGHT_LEADERBOARD_CACHE: Record< const MIDNIGHT_LEADERBOARD_CACHE: Record<
Region, Region,
@@ -13,7 +12,7 @@ const MIDNIGHT_LEADERBOARD_CACHE: Record<
asia: {}, asia: {},
}; };
export default async function getMidnightLeaderboard( export default async function getArchivedRatingsMidnightLeaderboard(
region: Region, region: Region,
season: number, season: number,
) { ) {
@@ -22,19 +21,13 @@ export default async function getMidnightLeaderboard(
} }
try { try {
const { data } = await octokit.repos.getContent({ const response = await fetch(
...DATABASE_REPO, `https://raw.githubusercontent.com/tresabhi/blitzkrieg-assets/main/regions/${region}/ratings/${season}/midnight.json`,
path: `${region}/ratings/${season}/midnight.json`, );
}); const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
MIDNIGHT_LEADERBOARD_CACHE[region][season] = json;
if (!Array.isArray(data) && data.type === 'file') { return json;
const response = await fetch(data.download_url!);
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
MIDNIGHT_LEADERBOARD_CACHE[region][season] = json;
return json;
}
} catch (error) { } catch (error) {
console.warn( console.warn(
`No midnight leaderboard found for season ${season}, region ${region}`, `No midnight leaderboard found for season ${season}, region ${region}`,
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more