Compare commits

...
213 Commits
Author SHA1 Message Date
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
TrèsAbhi a6d9044c3a Merge pull request #41 from tresabhi/1.14.0 2023-10-16 20:34:08 -05:00
TrèsAbhi 9e23d60ef1 fix: isolated session functions 2023-10-16 20:33:12 -05:00
TrèsAbhi f12449119c feat: changelog 2023-10-16 20:31:00 -05:00
TrèsAbhi 533bfc4458 fix: clan images 2023-10-16 20:16:23 -05:00
TrèsAbhi 1317c2879c feat: switch to 6 hour hostings 2023-10-16 20:11:19 -05:00
TrèsAbhi f9e51f894a feat: removed sharding as it's not needed for now 2023-10-16 20:10:35 -05:00
TrèsAbhi 6fdd4ba833 feat: render threading 2023-10-16 20:08:08 -05:00
TrèsAbhi 9495ae2336 feat: simpler logging 2023-10-15 15:52:35 -05:00
TrèsAbhi 43f634e198 feat: added sharding 2023-10-15 15:42:36 -05:00
TrèsAbhi ad51be7afc feat: added /breakdown 2023-10-15 15:24:07 -05:00
TrèsAbhi 15b8bfdf8f fix: use extra clan feilds 2023-10-14 19:16:07 -05:00
TrèsAbhi cb8ae47be6 feat: removed tank icons in owned tanks 2023-10-14 19:06:26 -05:00
TrèsAbhi db846c0680 fix: provide clan_id 2023-10-14 18:50:08 -05:00
TrèsAbhi 630a37025e feat: added reset in options 2023-10-14 12:04:11 -05:00
TrèsAbhi d3bbc7f2b0 feat: merge renderers 2023-10-14 11:40:16 -05:00
TrèsAbhi 4e0321ca5f fix: removed unused pallets 2023-10-14 11:38:07 -05:00
TrèsAbhi ec00bdf9de feat: core chunk 8 2023-10-14 11:36:48 -05:00
TrèsAbhi 01dd7ad9c6 feat: core chunk 7 2023-10-14 11:36:10 -05:00
TrèsAbhi d726005c38 feat: core chunk 6 2023-10-14 11:32:02 -05:00
TrèsAbhi 4f3e868367 feat: added mightnight leaderboard cache 2023-10-14 11:28:41 -05:00
TrèsAbhi ed8bab7df8 feat: core chunk 5 2023-10-14 11:25:42 -05:00
TrèsAbhi 9bdb14e13a feat: core chunk 4 2023-10-14 11:15:42 -05:00
TrèsAbhi b827f7f730 feat: swap core and _core 2023-10-14 11:06:03 -05:00
TrèsAbhi 07e3660388 feat: core chunk 3 2023-10-14 10:58:48 -05:00
TrèsAbhi 970ab9004e feat: cached archived ratings 2023-10-14 10:55:00 -05:00
TrèsAbhi 862445259e feat: core chunk 2 2023-10-14 10:52:05 -05:00
TrèsAbhi e96538bae0 feat: core chunk 1 2023-10-12 13:00:33 -05:00
TrèsAbhi 7447ccc487 fix: evolution refresh button 2023-10-12 12:07:45 -05:00
TrèsAbhi bcd3a00f91 feat: updated /evolution 2023-10-12 11:56:11 -05:00
TrèsAbhi 8c3675546f feat: sorted commands in /about commands 2023-10-12 11:45:43 -05:00
TrèsAbhi ec64dab720 fix: use webp images in layout 2023-10-11 21:26:33 -05:00
TrèsAbhi db976c4ae9 feat: added future features 👀 2023-10-11 20:18:51 -05:00
TrèsAbhi 0db96795b3 fix: a few type erros 2023-10-11 19:04:58 -05:00
TrèsAbhi 59bae96c1b feat: change tier weights to arabic numerals 2023-10-11 19:01:00 -05:00
TrèsAbhi d062a0da1b feat: updated full stats 2023-10-11 18:57:51 -05:00
TrèsAbhi d2668974f7 feat: code split a lot 2023-10-11 18:42:36 -05:00
TrèsAbhi 78e4c82f20 feat: simpler subcommands 2023-10-11 18:26:35 -05:00
TrèsAbhi f965452c5d feat: re-added manual username entry 2023-10-10 21:45:00 -05:00
TrèsAbhi be644bb3d9 feat: refined welcome 2023-10-10 21:37:17 -05:00
TrèsAbhi 97cc9509c0 feat: updated welcome message 2023-10-10 21:33:11 -05:00
TrèsAbhi 6dc7a10c2e feat: completed link 2023-10-10 21:20:01 -05:00
TrèsAbhi 19298b9ad6 feat: link command basic functionaility 2023-10-10 20:33:24 -05:00
TrèsAbhi 97d427a6a8 feat: better guild command handling 2023-10-08 18:53:21 -05:00
TrèsAbhi bff6eb0503 feat: simplified no ongoing ratings season 2023-10-08 17:53:24 -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
211 changed files with 5268 additions and 2653 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:
push:
branches: [main]
branches: main
jobs:
build:
+2 -2
View File
@@ -2,7 +2,7 @@ name: CD Host
on:
schedule:
- cron: '0 */5 * * *'
- cron: '0 */6 * * *'
workflow_dispatch:
concurrency:
@@ -29,4 +29,4 @@ jobs:
unzip bot.zip
- name: Run bot
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps --trace-warnings 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:
schedule:
- cron: "0 0 * * *"
- cron: '0 0 * * *'
- cron: '0 17 * * *'
jobs:
host:
@@ -16,7 +20,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
cache: 'npm'
- name: Install Yarn
run: npm install --location=global yarn
@@ -24,11 +28,11 @@ jobs:
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: "**/node_modules"
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --latest
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
@@ -1,8 +1,12 @@
name: CD North America Ratings Latest
name: CD Ratings Europe
# latest: '0 0 * * *'
# eu midnight: '0 23 * * *'
on:
schedule:
- cron: '0 0 * * *'
- cron: '0 23 * * *'
jobs:
host:
@@ -31,4 +35,4 @@ jobs:
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --latest
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
@@ -1,8 +1,12 @@
name: CD Europe Ratings Leaderboards
name: CD Ratings North America
# latest: '0 0 * * *'
# na midnight: '0 5 * * *'
on:
schedule:
- cron: "0 23 * * *"
- cron: '0 0 * * *'
- cron: '0 5 * * *'
jobs:
host:
@@ -16,7 +20,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
cache: 'npm'
- name: Install Yarn
run: npm install --location=global yarn
@@ -24,11 +28,11 @@ jobs:
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: "**/node_modules"
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
+1 -1
View File
@@ -1,6 +1,6 @@
name: CI Build Bot
on: [push, pull_request]
on: push
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
+1 -1
View File
@@ -1,6 +1,6 @@
name: CI Build Docs
on: [push, pull_request]
on: push
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
+1 -1
View File
@@ -1,6 +1,6 @@
name: CI Build Website
on: [push, pull_request]
on: push
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -1,8 +1,6 @@
name: CD North America Ratings Leaderboards
name: CI Version
on:
schedule:
- cron: '0 5 * * *'
on: push
jobs:
host:
@@ -31,4 +29,4 @@ jobs:
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na
run: yarn lint:version
+2 -1
View File
@@ -13,4 +13,5 @@ test.*
/docs/.vitepress/cache
servers.csv
.next/
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
temp/
+10
View File
@@ -5,15 +5,22 @@
"bepaint",
"blitzstars",
"cleandist",
"cockroackdb",
"corsproxy",
"customperiod",
"deburr",
"diacriticless",
"Everytime",
"fullstats",
"fuzzysort",
"immer",
"kranvagn",
"krieg",
"Leaderboard",
"leaderboards",
"MURICA",
"Oopsy",
"outdir",
"outfile",
"ownedtanks",
"playerachievements",
@@ -33,15 +40,18 @@
"subnation",
"tankaverages",
"tankopedia",
"tankopedias",
"tankstats",
"tresabhi",
"Unicum",
"Unregistering",
"vickers",
"vitepress",
"Vovko",
"wargaming",
"webp",
"winrate",
"wotblitz",
"zustand"
],
"deno.enable": false,
+3 -1
View File
@@ -8,5 +8,7 @@
"sklld_peasant_role": "896878529512370228",
"tres_guild_id": "734786591205359697",
"tres_log_channel": "1109262265850544148"
"tres_log_channel": "1109262265850544148",
"test_guild_id": "1124729294829142116"
}
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
themeConfig: {
nav: [
{ text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/about' },
{ text: 'Guide', link: '/guide/introduction' },
],
sidebar: [
+45
View File
@@ -0,0 +1,45 @@
# Blitzkrieg 1.14.0
Elegance has never been met like this before
## Features
- Added `/link`
- Unlike `/verify`, `/link` links you Discord and Blitz accounts across all servers
- This command does not change your nickname
- `/link` now simply displays a deprecation message
- Usernames can still be inputted per-command to override your linked account
- `/stats` and `/full-stats` are now way simpler
- `/stats <type> <period>` has been replaced with `/stats <30|60|90|...>` (likewise, `/full-stats`)
- These commands now have filters without having to opt into `multi-tank`
- Added `/breakdown`
- Unlike `/today`, `/breakdown` lets you select any period of time
- `/breakdown` now simply displays a deprecation message
- `/about commands` are now sorted alphabetically
- Added a `/reset` button to the options dropdown and embed context-menu
## Changes
- Simplified no ongoing ratings season edge case
- Updated welcome message for Skilled server to use `/link` and other up-to-date information
- Tier weights are now reported in Arabic numerals instead of Roman
- Removed tank images from `/owned-tanks`
- Wargaming messed up their API and almost no tanks have any images anymore
## Fixes
- Banners now use a more economical `.webp` format just like the home page
- Removed unused color palettes
## Technical changes
- Guild commands are now handled more efficiently
- Removed nickname-based Blitz username inference
- Code split a lot of common functionalities
- Re-arranged `core` folder
- Added midnight and latest ratings leaderboard caches
- Merged renderers into their primary user files
- Added sharding
- Disabled for now but good for future proofing
- Switched to a way cleaner logging schema
- Hosting is now done in 6 hour periods instead of 5
+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
+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
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
- 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
- Track a player/tanks statistical evolution
- ...and a variety of other similar things
## Website
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
- Session tracker
- Ratings leaderboard
- Way more to come
+2 -2
View File
@@ -12,10 +12,10 @@ hero:
actions:
- theme: brand
text: Add to Discord Server
link: guide/install
link: /guide/invite
- theme: alt
text: Guide
link: /guide/about
link: /guide/introduction
features:
- title: Clean and Efficient UI
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(),
distDir: 'dist/website',
reactStrictMode: false,
};
export default nextConfig;
+1 -1
View File
@@ -2,6 +2,6 @@
"main": "server.js",
"type": "module",
"scripts": {
"start": "node --enable-source-maps --trace-warnings server.cjs"
"start": "node --enable-source-maps server.cjs"
}
}
+11 -2
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkrieg",
"version": "1.13.3",
"version": "1.15.6",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
@@ -9,10 +9,11 @@
"build:website": "next build",
"build:docs": "vitepress build docs",
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"build:tankopedia": "vite-node --script scripts/buildTankopedia",
"dev:bot": "yarn build:bot && yarn start:bot",
"dev:docs": "vitepress dev docs",
"dev:website": "next dev",
"start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs",
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
"start:website": "next start",
"host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
@@ -20,24 +21,28 @@
"lint": "npx tsc",
"lint:website": "next lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
"knip": "knip",
"update": "yarn upgrade --latest"
},
"license": "MIT",
"dependencies": {
"@octokit/rest": "^20.0.1",
"@prisma/client": "^5.4.1",
"@radix-ui/colors": "^3.0.0-rc.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^1.1.2",
"@resvg/resvg-js": "^2.4.1",
"@vanilla-extract/css": "^1.13.0",
"bepaint": "^0.1.0-beta.3",
"decompress": "^4.2.1",
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"esbuild": "^0.19.3",
"express": "^4.18.2",
"fuzzysort": "^2.0.4",
"immer": "^10.0.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"markdown-escape": "^2.0.0",
"next": "^13.5.2",
@@ -48,12 +53,15 @@
"vite-node": "^0.34.5",
"vitepress": "^1.0.0-rc.20",
"vitepress-plugin-auto-sidebar": "^1.1.0",
"xml-js": "^1.6.11",
"zustand": "^4.4.1"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
"@types/decompress": "^4.2.6",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.18",
"@types/js-yaml": "^4.0.8",
"@types/lodash": "^4.14.199",
"@types/markdown-escape": "^1.1.0",
"@types/node": "^20.6.5",
@@ -62,6 +70,7 @@
"compare-versions": "^6.1.0",
"knip": "^2.27.1",
"prettier": "^3.0.3",
"prisma": "^5.4.1",
"typescript": "^5.2.2"
}
}
@@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE "discord_blitz" (
"discord" INT8 NOT NULL,
"region" STRING NOT NULL,
"blitz" INT4 NOT NULL,
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
);
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "cockroachdb"
+14
View File
@@ -0,0 +1,14 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "cockroachdb"
url = env("DISCORD_BLITZ_DB")
}
model discord_blitz {
discord BigInt @id
region String
blitz Int
}
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 -19
View File
@@ -1,27 +1,44 @@
import { build } from 'esbuild';
import { cp, readdir } from 'fs/promises';
console.log('Building bot...');
readdir('src/workers').then((workers) => {
const files = [
'bot',
'index',
await build({
entryPoints: ['src/bot.ts'],
outfile: 'dist/bot/index.cjs',
...workers.map((file) => `workers/${file.replace('.ts', '')}`),
];
platform: 'node',
tsconfig: 'tsconfig.esbuild.json',
loader: {
'.node': 'copy',
'.ttf': 'file',
},
format: 'cjs',
jsx: 'transform',
build({
entryPoints: files.map((file) => `src/${file}.ts`),
outdir: 'dist/bot',
bundle: true,
sourcemap: true,
minifyIdentifiers: false, // cause goofy errors
minifySyntax: true,
minifyWhitespace: true,
platform: 'node',
tsconfig: 'tsconfig.esbuild.json',
loader: {
'.node': 'copy',
'.ttf': 'file',
},
format: 'cjs',
jsx: 'transform',
outExtension: {
'.js': '.cjs',
},
logLevel: 'info',
bundle: true,
sourcemap: true,
minifyIdentifiers: false, // causes goofy errors
minifySyntax: true,
minifyWhitespace: true,
logLevel: 'info',
});
});
console.log('Built bot');
// 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,
RatingsPlayer,
} from '../src/commands/ratings';
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
import commitMultipleFiles from './commitMultipleFiles.js';
/*
@@ -13,25 +14,27 @@ import commitMultipleFiles from './commitMultipleFiles.js';
* Central European Time (UTC+1): use 0 23 * * *.
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
*
* na/ratings/49/info.json: season info like rewards, start & end time, etc.
* na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
* na/ratings/49/midnight.json: last midnight's leaderboard relative to the region
* [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
* [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
* [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
*/
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
const publish = argv.includes('--publish');
const latest = argv.includes('--latest');
const target = argv
.find((arg) => arg.startsWith('--target='))
?.split('=')[1] as 'midnight' | 'latest' | undefined;
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
if (!target) throw new Error('Target parameter not specified');
if (!server) throw new Error('Region parameter not specified');
const NEIGHBORS = 1000;
const PLAYERS = await fetch(
const NEIGHBORS = 2 ** 8;
const PLAYERS = await patientFetchJSON<RatingsInfo>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
)
.then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => (data.detail === undefined ? data.count : undefined));
).then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) {
throw new Error('No current ratings season running');
@@ -42,9 +45,9 @@ const leaderboard: BlitzkriegRatingsLeaderboard = [];
let registered = 0;
async function branchFromPlayer(id: number) {
const { neighbors } = await fetch(
const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
).then((response) => response.json() as Promise<RatingsNeighbors>);
);
const firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1];
@@ -73,9 +76,9 @@ async function branchFromPlayer(id: number) {
}
async function branchFromLeague(id: number) {
const { result } = await fetch(
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
);
const firstPlayer = result[0];
const lastPlayer = result[result.length - 1];
@@ -98,7 +101,10 @@ await Promise.all([
console.log('Converting to an array...');
for (let index = 0; index < PLAYERS; index++) {
const listing = players[index + 1];
if (!listing) console.warn(`Position ${index + 1} had no player`);
if (!listing) {
console.warn(`Position ${index + 1} had no player`);
continue;
}
leaderboard[index] = {
id: listing.spa_id,
@@ -110,26 +116,19 @@ const leaderboardJSON = JSON.stringify(leaderboard);
if (publish) {
const octokit = new Octokit({ auth: env.GH_TOKEN });
const info = await fetch(
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then(
(response) =>
response.json() as Promise<RatingsInfo & { detail: undefined }>,
);
const normalizedServer = server === 'na' ? 'com' : server;
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
latest ? 'latest' : 'midnight'
}.json`;
const infoPath = `regions/${normalizedServer}/ratings/${info.current_season}/info.json`;
const leaderboardPath = `regions/${normalizedServer}/ratings/${info.current_season}/${target}.json`;
const infoJSON = JSON.stringify(info);
console.log(`Publishing to season ${info.current_season}...`);
commitMultipleFiles(
octokit,
'tresabhi',
'blitzkrieg-db',
'blitzkrieg-assets',
'main',
new Date().toString(),
[
+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),
);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server';
import { tankopedia } from '../../../core/blitz/tankopedia';
import { tankopedia } from '../../../core/blitzstars/tankopedia';
export async function GET() {
return NextResponse.json(await tankopedia);
+12 -9
View File
@@ -5,13 +5,16 @@ import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import { TreeTypeEnum } from '../../../components/Tanks';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import calculateWN8 from '../../../core/blitz/calculateWN8';
import { diffNormalizedTankStats } from '../../../core/blitz/diffNormalizedTankStats';
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
import getWargamingResponse from '../../../core/blitz/getWargamingResponse';
import { TankopediaEntry, tankopedia } from '../../../core/blitz/tankopedia';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import { useSession } from '../../../stores/session';
import {
TankopediaEntry,
tankopedia,
} from '../../../core/blitzstars/tankopedia';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import { resetSession, useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
@@ -73,7 +76,7 @@ export default function SessionPage() {
async function recalculateDiff() {
if (session.isTracking) {
const { id, region } = session;
const careerRaw = await getWargamingResponse<TanksStats>(
const careerRaw = await fetchBlitz<TanksStats>(
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const career = careerRaw[id].reduce<
@@ -111,7 +114,7 @@ export default function SessionPage() {
list: (
Object.values(
diffNormalizedTankStats(session.tankStats, career),
deltaTankStats(session.tankStats, career),
) as IndividualTankStats[]
).map((stats) => {
return {
@@ -321,7 +324,7 @@ export default function SessionPage() {
</div>
</ContextMenu.Trigger>
<Menu Builder={ContextMenu} />
<Menu reset={resetSession} Builder={ContextMenu} />
</ContextMenu.Root>
);
}
+1 -6
View File
@@ -41,12 +41,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
: 'url(https://i.imgur.com/PhS06NJ.png)',
}}
>
<Theme
appearance="dark"
panelBackground="translucent"
radius="full"
accentColor="blue"
>
<Theme appearance="dark" panelBackground="translucent" radius="full">
{!isEmbed && <Navbar />}
<Flex direction="column">{children}</Flex>
+18 -3
View File
@@ -19,17 +19,32 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
<div
style={{
height: 128,
backgroundImage: `url(/assets/banners/${toolId}.png)`,
backgroundSize: 'cover',
backgroundPosition: 'center',
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
flexDirection: 'column',
position: 'relative',
overflow: 'hidden',
}}
>
<div
style={{
position: 'absolute',
backgroundImage: `url(/assets/banners/${toolId}.webp)`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '100%',
height: '100%',
top: 0,
left: 0,
zIndex: -1,
filter: 'blur(4px)',
transform: 'scale(calc((128 + 8) / 128))',
}}
/>
<span
style={{
color: theme.colors.textHighContrast,
@@ -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
+8 -1
View File
@@ -4,9 +4,10 @@ import { CustomColumn } from './CustomColumn';
interface ContextMenuProps {
Builder: typeof DropdownMenu | typeof ContextMenu;
reset: () => void;
}
export function Menu({ Builder }: ContextMenuProps) {
export function Menu({ Builder, reset }: ContextMenuProps) {
const session = useSession();
return (
@@ -34,6 +35,12 @@ export function Menu({ Builder }: ContextMenuProps) {
>
Show career
</Builder.CheckboxItem>
<Builder.Separator />
<Builder.Item color="red" onClick={reset}>
Reset
</Builder.Item>
</Builder.Content>
);
}
+72 -60
View File
@@ -14,19 +14,15 @@ import {
Flex,
Text,
TextField,
Tooltip,
} from '@radix-ui/themes';
import { debounce } from 'lodash';
import { ChangeEvent, useRef, useState } from 'react';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { REGION_NAMES, Region } from '../../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import getWargamingResponse from '../../../core/blitz/getWargamingResponse';
import listPlayers, {
import { REGION_NAMES } from '../../../constants/regions';
import searchPlayersAcrossRegions, {
AccountListWithServer,
} from '../../../core/blitz/listPlayers';
import { useSession } from '../../../stores/session';
import { NormalizedTankStats, TanksStats } from '../../../types/tanksStats';
} from '../../../core/blitz/searchPlayersAcrossRegions';
import { resetSession, setSession, useSession } from '../../../stores/session';
import SessionPage from '../../embeds/session/page';
import { Menu } from './components/Menu';
import * as styles from './page.css';
@@ -40,7 +36,7 @@ export default function Page() {
const handleChange = debounce(
async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.value) {
setSearchResults(await listPlayers(event.target.value));
setSearchResults(await searchPlayersAcrossRegions(event.target.value));
} else {
setSearchResults(undefined);
setShowSearchResults(false);
@@ -50,40 +46,12 @@ export default function Page() {
);
const session = useSession();
async function setSession(region: Region, id: number, nickname: string) {
input.current!.value = nickname;
const rawTankStats = (
await getWargamingResponse<TanksStats>(
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
)
)[id];
const tankStats = rawTankStats.reduce<NormalizedTankStats>(
(accumulator, tank) => ({
...accumulator,
[tank.tank_id]: tank,
}),
{},
);
useSession.setState({
isTracking: true,
id,
region,
nickname,
tankStats,
time: Date.now(),
});
}
function reset() {
const session = useSession.getState();
if (!session.isTracking) return;
setSession(session.region, session.id, session.nickname);
}
useEffect(() => {
if (session.isTracking) input.current!.value = session.nickname;
}, [session]);
return (
<PageWrapper>
<PageWrapper color="blue">
<div className={styles.toolBar}>
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
<TextField.Root>
@@ -156,9 +124,9 @@ export default function Page() {
<Button
variant="soft"
onClick={(event) => {
if (!session.promptBeforeReset) {
if (!session.resetPrompt) {
event.preventDefault();
reset();
resetSession();
}
}}
className={styles.toolbarButton}
@@ -185,7 +153,7 @@ export default function Page() {
mr="1"
onCheckedChange={(checked) =>
useSession.setState({
promptBeforeReset: !checked,
resetPrompt: !checked,
})
}
/>
@@ -201,7 +169,7 @@ export default function Page() {
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={reset}>
<Button variant="solid" color="red" onClick={resetSession}>
Reset
</Button>
</AlertDialog.Action>
@@ -209,19 +177,63 @@ export default function Page() {
</AlertDialog.Content>
</AlertDialog.Root>
<Tooltip content="Copy to clipboard">
<Button
variant="soft"
className={styles.toolbarButton}
onClick={() =>
navigator.clipboard.writeText(
`${location.origin}/embeds/session`,
)
}
>
<CopyIcon width="16" height="16" /> Embed
</Button>
</Tooltip>
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button
variant="soft"
className={styles.toolbarButton}
onClick={(event) => {
if (!session.embedPrompt) {
event.preventDefault();
resetSession();
}
navigator.clipboard.writeText(
`${location.origin}/embeds/session`,
);
}}
>
<CopyIcon width="16" height="16" /> Embed
</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Title>
Copied link to your clipboard
</AlertDialog.Title>
<AlertDialog.Description>
Instructions for streamers:
<iframe
style={{
width: '100%',
aspectRatio: '16 / 9',
border: 'none',
}}
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</AlertDialog.Description>
<Text size="2">
<Checkbox
mr="1"
onCheckedChange={(checked) =>
useSession.setState({
embedPrompt: !checked,
})
}
/>
Don't show this again
</Text>
<Flex gap="3" mt="4">
<AlertDialog.Cancel>
<Button variant="soft">Got it!</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
@@ -231,7 +243,7 @@ export default function Page() {
</Button>
</DropdownMenu.Trigger>
<Menu Builder={DropdownMenu} />
<Menu Builder={DropdownMenu} reset={resetSession} />
</DropdownMenu.Root>
</div>
</div>
+5 -3
View File
@@ -1,6 +1,8 @@
import { registerProcesses } from './core/blitzkrieg/processes';
import { secrets } from './core/blitzkrieg/secrets';
import { client } from './core/discord/client';
import { secrets } from './core/node/secrets';
import { registerErrorHandlers } from './events/error';
registerErrorHandlers();
// console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
registerProcesses();
client.login(secrets.DISCORD_TOKEN);
+3
View File
@@ -11,6 +11,7 @@ const DOCS = {
introduction: 'guide/introduction',
invite: 'guide/invite',
timezones: 'guide/timezones',
creators: 'guide/creators',
};
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
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',
invite: 'How to invite the bot',
timezones: 'How Blitzkrieg infers and uses your timezone',
creators: 'Who created the bot',
};
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
@@ -51,6 +53,7 @@ export const aboutCommand: CommandRegistry = {
await Promise.all(COMMANDS_RAW)
)
.filter((registry) => registry.inPublic && registry.inProduction)
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
.map(
(registry) =>
`- \`/${registry.command.name}\`: ${registry.command.description}`,
+340
View File
@@ -0,0 +1,340 @@
import { SlashCommandBuilder } from 'discord.js';
import { chunk } from 'lodash';
import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats';
import getTreeType from '../core/blitz/getTreeType';
import resolveTankName from '../core/blitz/resolveTankName';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../core/blitzstars/tankAverages';
import { tankopedia } from '../core/blitzstars/tankopedia';
import addFilterOptions from '../core/discord/addFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary';
import commandToURL from '../core/discord/commandToURL';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand, {
ResolvedPeriod,
} from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import calculateWN8 from '../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryRaw } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 10;
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
) {
const awaitedTankopedia = await tankopedia;
const awaitedTankAverages = await tankAverages;
const statsInPeriod = await getStatsInPeriod(region, id, start, end);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ['clan']);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters);
const orderedCurrentStats: AllStats[] = [];
const orderedCareerStats: AllStats[] = [];
const orderedCurrentWN8: (number | undefined)[] = [];
const orderedCareerWN8: (number | undefined)[] = [];
const orderedCareerWN8Full: (number | undefined)[] = [];
filteredOrder.forEach((id) => {
const tankAveragesEntry = awaitedTankAverages[id];
const current = statsInPeriod.diff[id];
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
const currentWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, current)
: undefined;
const careerWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, career)
: undefined;
orderedCurrentStats.push(current);
orderedCareerStats.push(career);
orderedCurrentWN8.push(currentWN8);
orderedCareerWN8.push(careerWN8);
});
tankStats.forEach((tank) => {
const tankAveragesEntry = awaitedTankAverages[tank.tank_id];
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, tank.all)
: undefined,
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0,
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0,
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0,
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0,
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0,
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0,
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0,
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0,
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0,
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0,
) / careerBattles;
const children: JSX.Element[] = [];
if (filteredOrder.length > 0) {
children.push(
<Breakdown.Row
key={id}
type="summary"
title="Total"
stats={[
{
title: 'Battles',
current: currentBattles,
career: careerBattles,
},
{
title: 'Winrate',
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
},
{
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
},
]}
/>,
);
await Promise.all(
filteredOrder.map(async (id, index) => {
const tankopediaEntry = awaitedTankopedia[id];
const current = orderedCurrentStats[index];
const career = orderedCareerStats[index];
const currentWN8 = orderedCurrentWN8[index];
const careerWN8 = orderedCareerWN8[index];
children.push(
<Breakdown.Row
key={id}
type="tank"
tankType={tankopediaEntry?.type}
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
title={await resolveTankName(id)}
stats={[
{
title: 'Battles',
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: 'Winrate',
current: `${(100 * (current.wins / current.battles)).toFixed(
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
current.wins / current.battles - career.wins / career.battles,
},
{
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>,
);
}),
);
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<Wrapper>
{index === 0 && (
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
)}
<Breakdown.Root>{page}</Breakdown.Root>
</Wrapper>
))
.reverse();
} else {
return [
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
{filteredOrder.length === 0 && (
<NoData type={NoDataType.BattlesInPeriod} />
)}
</Wrapper>,
];
}
}
export const breakdownCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const command = await addFilterOptions(
new SlashCommandBuilder()
.setName('breakdown')
.setDescription("A period's breakdown by tanks played"),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
inProduction: true,
inPublic: true,
command,
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
return [
...(await render(player, period, filters)),
buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters);
},
});
},
);
+87 -91
View File
@@ -1,97 +1,93 @@
import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { PlayerHistoriesRaw } from '../core/blitzstars/getPlayerHistories';
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan';
import embedWarning from '../core/discord/embedWarning';
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountAchievements } from '../types/accountAchievements';
import { AccountInfo } from '../types/accountInfo';
import { ClanInfo } from '../types/clanInfo';
// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
// | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
// | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
// _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
// |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
export const clanEventCommand: CommandRegistry = {
inProduction: false,
inPublic: true,
// import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
// import markdownEscape from 'markdown-escape';
// import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
// import { getClanInfo } from '../core/blitz/getClanInfo';
// import { PlayerHistoriesRaw } from '../LEGACY_core/blitzstars/getPlayerHistories';
// import getTimeDaysAgo from '../LEGACY_core/blitzstars/getTimeDaysAgo';
// import addClanChoices from '../LEGACY_core/discord/addClanChoices';
// import autocompleteClan from '../LEGACY_core/discord/autocompleteClan';
// import embedWarning from '../LEGACY_core/discord/embedWarning';
// import resolveClanFromCommand from '../LEGACY_core/discord/resolveClanFromCommand';
// import { CommandRegistry } from '../events/interactionCreate';
command: new SlashCommandBuilder()
.setName('clan-event')
.setDescription('Lists all inactive players')
.addStringOption(addClanChoices),
// export const clanEventCommand: CommandRegistry = {
// inProduction: false,
// inPublic: true,
async handler(interaction) {
const { region, id } = await resolveClanFromCommand(interaction);
const clan = (
await getWargamingResponse<ClanInfo>(
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
)
)[id];
const memberIds = clan.members_ids.join(',');
const [players, achievements, previousJointVictories] = await Promise.all([
getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
),
getWargamingResponse<AccountAchievements>(
`https://api.wotblitz.${region}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
),
Promise.all(
clan.members_ids.map((id) =>
fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
.then((response) => response.json() as Promise<PlayerHistoriesRaw>)
.then((data) => ({
id,
jointVictory: data[0]?.achievements.jointVictoryCount as
| number
| undefined,
})),
),
).then((data) =>
data.reduce<Record<number, number | undefined>>(
(accumulator, value) => ({
...accumulator,
[value.id]: value.jointVictory,
}),
{},
),
),
]);
const jointVictories = clan.members_ids.map((id) => ({
id,
joinVictory:
players[id].last_battle_time > getTimeDaysAgo(region, 1)
? previousJointVictories[id] === undefined
? 0
: achievements[id].max_series.jointVictory -
previousJointVictories[id]!
: 0,
}));
// command: new SlashCommandBuilder()
// .setName('clan-event')
// .setDescription('Lists all inactive players')
// .addStringOption(addClanChoices),
return [
`# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
clan.tag,
)}]'s platoon wins today: ${(
jointVictories.reduce(
(accumulator, { joinVictory }) => accumulator + joinVictory,
0,
) / 2
).toFixed(0)}\n${jointVictories
.filter(({ joinVictory }) => joinVictory)
.sort((a, b) => b.joinVictory - a.joinVictory)
.map(
({ id, joinVictory }) =>
`- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
)
.join('\n')}`,
// async handler(interaction) {
// const { region, id } = await resolveClanFromCommand(interaction);
// const clan = await getClanInfo(region, id);
// const [players, achievements, previousJointVictories] = await Promise.all([
// getAccountInfo(region, clan.members_ids),
// getAccountAchievements(region, clan.members_ids),
// Promise.all(
// clan.members_ids.map((id) =>
// fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
// .then((response) => response.json() as Promise<PlayerHistoriesRaw>)
// .then((data) => ({
// id,
// jointVictory: data[0]?.achievements.jointVictoryCount as
// | number
// | undefined,
// })),
// ),
// ).then((data) =>
// data.reduce<Record<number, number | undefined>>(
// (accumulator, value) => ({
// ...accumulator,
// [value.id]: value.jointVictory,
// }),
// {},
// ),
// ),
// ]);
// const jointVictories = clan.members_ids.map((id) => ({
// id,
// joinVictory:
// players[id].last_battle_time > getTimeDaysAgo(region, 1)
// ? previousJointVictories[id] === undefined
// ? 0
// : achievements[id].max_series.jointVictory -
// previousJointVictories[id]!
// : 0,
// }));
embedWarning(
'This is an approximation!',
"Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/about timezones` for more info)",
),
];
},
// return [
// `# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
// clan.tag,
// )}]'s platoon wins today: ${(
// jointVictories.reduce(
// (accumulator, { joinVictory }) => accumulator + joinVictory,
// 0,
// ) / 2
// ).toFixed(0)}\n${jointVictories
// .filter(({ joinVictory }) => joinVictory)
// .sort((a, b) => b.joinVictory - a.joinVictory)
// .map(
// ({ id, joinVictory }) =>
// `- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
// )
// .join('\n')}`,
autocomplete: autocompleteClan,
};
// embedWarning(
// 'This is an approximation!',
// "Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/about timezones` for more info)",
// ),
// ];
// },
// autocomplete: autocompleteClan,
// };
+15 -23
View File
@@ -1,11 +1,6 @@
import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' };
import { tankopedia } from '../core/blitz/tankopedia';
import { tankAverages } from '../core/blitzstars/tankAverages';
import { client } from '../core/discord/client';
import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable';
import getClientId from '../core/node/getClientId';
import { CommandRegistry } from '../events/interactionCreate';
const executionStart = new Date().getTime();
@@ -21,24 +16,21 @@ export const debugCommand: CommandRegistry = {
async handler(interaction) {
const currentTime = new Date().getTime();
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(
`${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}`],
]),
);
return `# Debug\n\n${list}`;
},
};
+14 -18
View File
@@ -1,20 +1,18 @@
import { SlashCommandBuilder } from 'discord.js';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import calculateWN8 from '../core/blitz/calculateWN8';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import sumStats from '../core/blitz/sumStats';
import { tankopedia } from '../core/blitz/tankopedia';
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
import getPeriodNow from '../core/blitzstars/getPeriodNow';
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../core/blitzstars/tankAverages';
import { tankopedia } from '../core/blitzstars/tankopedia';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import embedNegative from '../core/discord/embedNegative';
import embedPositive from '../core/discord/embedPositive';
import markdownTable, { TableInput } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import calculateWN8 from '../core/statistics/calculateWN8';
import sumStats from '../core/statistics/sumStats';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PossiblyPromise } from '../types/possiblyPromise';
export type SkilledClan = 'SKLLD' | 'SMRI';
@@ -48,20 +46,18 @@ export const eligibleCommand: CommandRegistry = {
async handler(interaction) {
const clan = interaction.options.getString('clan') as SkilledClan;
const { id, region: server } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const { id, region: region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const problems: TableInput = [];
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
let isEligible = false;
let body = '';
if (clan === 'SKLLD') {
const { diffed } = await getDiffedTankStats(
server,
const { diff: diffed } = await getStatsInPeriod(
region,
id,
getTimeDaysAgo(server, 30),
getTimeDaysAgo(region, 30),
getPeriodNow(),
);
const entries = Object.entries(diffed);
@@ -171,7 +167,7 @@ export const eligibleCommand: CommandRegistry = {
problems,
)}`;
} else if (clan === 'SMRI') {
const stats = accountInfo[id].statistics.all;
const stats = accountInfo.statistics.all;
if (stats.battles < 1500) {
problems.push([
+149 -47
View File
@@ -1,31 +1,142 @@
import { SlashCommandBuilder } from 'discord.js';
import { CYCLIC_API } from '../constants/cyclic';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import {
SlashCommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import * as Graph from '../components/Graph';
import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import resolveTankId from '../core/blitz/resolveTankId';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import getTankHistories from '../core/blitzkrieg/getTankHistories';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
import addPeriodSubCommands from '../core/discord/addPeriodSubCommands';
import addTankChoices from '../core/discord/addTankChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import interactionToURL from '../core/discord/interactionToURL';
import linkButton from '../core/discord/linkButton';
import primaryButton from '../core/discord/primaryButton';
import buttonPrimary from '../core/discord/buttonPrimary';
import commandToURL from '../core/discord/commandToURL';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePeriodFromCommand, {
ResolvedPeriod,
} from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryRaw } from '../events/interactionCreate';
import evolution from '../renderers/evolution';
import { StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo';
type EvolutionStatType = 'player' | 'tank';
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
type: 'player' | 'tank',
tankId: number | null,
) {
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const logo = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
const histories = await (type === 'player'
? getPlayerHistories(region, id, {
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
})
: getTankHistories(region, id, {
tankId: tankId!,
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
}));
const plot = histories.map(
(history) =>
[
history.all.battles,
(history.all.wins / history.all.battles) * 100,
] as Graph.PlotItem,
);
const times = histories.map((history) => history.last_battle_time);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const ys = plot.map(([, y]) => y);
const xs = plot.map(([x]) => x);
const maxY = Math.max(...ys);
const minY = Math.min(...ys);
const maxX = Math.max(...xs);
const minX = Math.min(...xs);
return (
<Wrapper>
<TitleBar name={accountInfo.nickname} image={logo} description={name} />
{plot.length > 0 && (
<Graph.Root
verticalMargin={{
min: minY,
max: maxY,
suffix: '%',
precision: 2,
}}
horizontalMargin={{
min: minX,
max: maxX,
precision: 0,
}}
xMinLabel={new Date(minTime * 1000).toDateString()}
xMaxLabel={new Date(maxTime * 1000).toDateString()}
>
{Graph.Line({
color: Graph.LineColor.Red,
plot: plot,
minY,
maxY,
})}
</Graph.Root>
)}
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
</Wrapper>
);
}
export const evolutionCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics'),
false,
);
let playerGroup: SlashCommandSubcommandGroupBuilder;
let tankGroup: SlashCommandSubcommandGroupBuilder;
const command = new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics')
.addSubcommandGroup((option) => {
playerGroup = option
.setName('player')
.setDescription('Player statistics');
return playerGroup;
})
.addSubcommandGroup((option) => {
tankGroup = option.setName('tank').setDescription('Tank statistics');
return tankGroup;
});
await Promise.all([
addPeriodSubCommands(playerGroup!, (option) =>
option.addStringOption(addUsernameChoices),
),
addPeriodSubCommands(tankGroup!, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameChoices),
),
]);
resolve({
inProduction: true,
@@ -34,37 +145,28 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
command,
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const commandGroup =
interaction.options.getSubcommandGroup() as EvolutionStatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const tankIdRaw = interaction.options.getString('tank')!;
const tankId =
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const path = interactionToURL(interaction, {
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
tankId,
start,
end,
});
getBlitzStarsLinkButton;
return [
await evolution(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}${
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
}`,
'BlitzStars',
await render(player, period, commandGroup, tankId),
buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(
player.region,
player.id,
tankId ?? undefined,
),
];
},
@@ -75,17 +177,17 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const url = new URL(`https://example.com/${interaction.customId}`);
const path = url.pathname.split('/');
const commandGroup = path[2] as EvolutionStatType;
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
return await evolution(
commandGroup,
period,
return await render(
player,
parseInt(url.searchParams.get('tankId')!),
period,
commandGroup,
parseInt(url.searchParams.get('tankId') ?? '0') || null,
);
},
});
+73 -74
View File
@@ -1,30 +1,76 @@
import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
import GenericAllStats from '../components/GenericAllStats';
import NoData, { NoDataType } from '../components/NoData';
import TierWeights from '../components/TierWeights';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import addFilterOptions from '../core/discord/addFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import interactionToURL from '../core/discord/interactionToURL';
import linkButton from '../core/discord/linkButton';
import primaryButton from '../core/discord/primaryButton';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePeriodFromCommand, {
ResolvedPeriod,
} from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { getTierWeights } from '../core/statistics/getTierWeights';
import { CommandRegistryRaw } from '../events/interactionCreate';
import fullStats from '../renderers/fullStats';
import { MultiTankFilters, StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo';
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
const { stats, supplementary, filteredOrder } = await filterStats(
diffedTankStats,
filters,
);
const filterDescriptions = await filtersToDescription(filters);
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
return (
<Wrapper>
<TitleBar
name={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
{stats.battles > 0 && (
<GenericAllStats stats={stats} supplementaryStats={supplementary} />
)}
</Wrapper>
);
}
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const command = await addStatTypeSubCommandGroups(
const command = await addFilterOptions(
new SlashCommandBuilder()
.setName('full-stats')
.setDescription('Full in-game statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
@@ -34,60 +80,20 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
command,
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const resolvedPeriod = resolvePeriodFromCommand(
player.region,
interaction,
);
const period = interaction.options.getString('period');
const tankIdRaw = interaction.options.getString('tank')!;
const tankId =
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const path = interactionToURL(interaction, {
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
tankId,
start,
end,
period,
...getCustomPeriodParams(interaction),
...filters,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const tierOption = interaction.options.getString('tier');
return [
await fullStats(
commandGroup,
resolvedPeriod,
player,
commandGroup === 'tank'
? tankId
: commandGroup === 'multi-tank'
? ({
'tank-type':
interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined,
tier: tierOption ? parseInt(tierOption) : undefined,
'tree-type': (interaction.options.getString('tree-type') ??
undefined) as TreeTypeString | undefined,
} satisfies Partial<MultiTankFilters>)
: null,
),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars',
),
];
return Promise.all([
render(player, period, filters),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
},
autocomplete: (interaction) => {
@@ -96,18 +102,11 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await fullStats(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
);
return await render(player, period, filters);
},
});
},
+13 -23
View File
@@ -3,14 +3,12 @@ import GenericStats from '../components/GenericStats';
import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanInfo } from '../core/blitz/getClanInfo';
import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan';
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { ClanInfo } from '../types/clanInfo';
const DEFAULT_THRESHOLD = 7;
@@ -36,24 +34,16 @@ export const inactiveCommand: CommandRegistry = {
const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanData = (
await getWargamingResponse<ClanInfo>(
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
const inactive = accountInfo
.map(
(account) =>
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number],
)
)[id];
const memberIds = clanData.members_ids;
const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
',',
)}`,
);
const inactive = memberIds
.map((memberId) => {
const member = accounts[memberId];
const inactiveDays = (time - member.last_battle_time) / 60 / 60 / 24;
return [member.nickname, inactiveDays] as [string, number];
})
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
.map(
@@ -64,8 +54,8 @@ export const inactiveCommand: CommandRegistry = {
return (
<Wrapper>
<TitleBar
name={clanData.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
name={clanInfo.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
/>
+223
View File
@@ -0,0 +1,223 @@
import { teal } from '@radix-ui/colors';
import {
AttachmentBuilder,
GuildMemberRoleManager,
SlashCommandBuilder,
} from 'discord.js';
import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' };
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
import Wrapper from '../components/Wrapper';
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export const verifyCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('link')
.setDescription('Links your Blitz and Discord account')
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
async handler(interaction) {
const username = interaction.options.getString('username', true);
if (serverAndIdPattern.test(username)) {
const [region, idString] = username.split('/');
const id = parseInt(idString);
const discordId = parseInt(interaction.user.id);
const accountInfo = await getAccountInfo(region as Region, id);
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [
'clan',
]);
await linkBlitzAndDiscord(discordId, region as Region, id);
if (interaction.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`,
"I don't have the permission to change your manage roles.",
);
}
await (interaction.member!.roles as GuildMemberRoleManager).remove(
discord.sklld_verify_role,
);
await (interaction.member!.roles as GuildMemberRoleManager).add(
discord.sklld_peasant_role,
);
}
return (
<Wrapper fat>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
}}
>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<img
src="https://i.imgur.com/jIcRgog.png"
style={{
width: 16,
height: 16,
}}
/>
<span style={{ fontSize: 16 }}>Accounts linked</span>
</div>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
/>
) : null}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{accountInfo.nickname}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] - `
: ''}
{REGION_NAMES_SHORT[region as Region]}
</span>
</div>
</div>
</div>
<Glow color={teal.teal9} rotation={-90} />
</div>
</Wrapper>
);
} else {
return [
embedNegative(
'Please select from the options',
'Follow the procedure like the video above. The search results may take a while to show up.',
),
new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif'),
];
}
},
autocomplete: autocompleteUsername,
};
+11 -56
View File
@@ -3,46 +3,24 @@ import NoData, { NoDataType } from '../components/NoData';
import * as Tanks from '../components/Tanks';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats';
import getTreeType from '../core/blitz/getTreeType';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName';
import { tankopediaInfo } from '../core/blitz/tankopediaInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import {
TIER_ROMAN_NUMERALS,
TankopediaEntry,
Tier,
tankopedia,
tankopediaInfo,
} from '../core/blitz/tankopedia';
} from '../core/blitzstars/tankopedia';
import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
const COMP_TANKS = [
// light tanks
24321, // t-100 lt
20257, // sheridan
3649, // bat chat
19537, // vickers
// heavy tanks
6145, // is-4
7169, // is-7
10785, // t110e5
12161, // strvk
4481, // kranvagn
22817, // m6 yoh
6225, // fv215b
5425, // wz 113
7297, // 60tp
58641, // vk 72
6753, // type 71
];
export const ownedTanksCommand: CommandRegistry = {
inProduction: true,
@@ -58,9 +36,7 @@ export const ownedTanksCommand: CommandRegistry = {
const tier = Number(interaction.options.getString('tier'));
const account = await resolvePlayerFromCommand(interaction);
const { id, region: server } = account;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const accountInfo = await getAccountInfo(server, id);
const tankStats = await getTankStats(server, id);
const filteredTanks = (
await Promise.all(
@@ -70,9 +46,7 @@ export const ownedTanksCommand: CommandRegistry = {
})),
)
).filter((tank) => tank.tankopedia?.tier === tier);
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
);
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
const groupedTanks: Record<string, TankopediaEntry[]> = {};
const nations: string[] = [];
@@ -90,18 +64,13 @@ export const ownedTanksCommand: CommandRegistry = {
return (
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
}
name={accountInfo.nickname}
image={
clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
clanAccountInfo?.clan
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
: undefined
}
description={`Tier ${
TIER_ROMAN_NUMERALS[tier as Tier]
} tanks • ${new Date().toDateString()}`}
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`}
/>
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
@@ -158,17 +127,3 @@ export const ownedTanksCommand: CommandRegistry = {
autocomplete: autocompleteUsername,
};
// TODO: make this work in the future
// console.log('Caching tank images...');
// TANKS.then((tanks) =>
// jsxToSvg(
// <div style={{ display: 'flex' }}>
// {tanks.map(({ images, tank_id }) => {
// return (
// images.normal && <img key={tank_id} src={images.normal} width={0} />
// );
// })}
// </div>,
// ),
// ).then(() => console.log('Cached tank images'));
+7 -14
View File
@@ -1,16 +1,14 @@
import { SlashCommandBuilder } from 'discord.js';
import { startCase } from 'lodash';
import markdownEscape from 'markdown-escape';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo';
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountAchievements } from '../types/accountAchievements';
import { AccountInfo } from '../types/accountInfo';
type SortBy = 'name' | 'count';
@@ -35,22 +33,17 @@ export const playerAchievementsCommand: CommandRegistry = {
async handler(interaction) {
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
const account = await resolvePlayerFromCommand(interaction);
const { id, region: server } = account;
const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const accountsAchievements =
await getWargamingResponse<AccountAchievements>(
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const accountAchievements = accountsAchievements[id];
const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
const accountsAchievements = await getAccountAchievements(region, id);
const accountAchievements = accountsAchievements;
const compound = {
...accountAchievements.achievements,
...accountAchievements.max_series,
};
return embedInfo(
`${markdownEscape(accounts[id].nickname)}'s information`,
`${markdownEscape(accountInfo.nickname)}'s information`,
markdownTable(
Object.keys(compound)
+3 -8
View File
@@ -1,14 +1,12 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
export const playerInfoCommand: CommandRegistry = {
inProduction: true,
@@ -21,11 +19,8 @@ export const playerInfoCommand: CommandRegistry = {
async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction);
const { id, region: server } = account;
const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const accountInfo = accounts[id];
const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
`${markdownEscape(accountInfo.nickname)}'s information`,
+128 -195
View File
@@ -4,28 +4,31 @@ import {
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { range } from 'lodash';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
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 { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
import getMidnightLeaderboard, {
DATABASE_REPO,
} from '../core/database/getMidnightLeaderboard';
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
import { UserError } from '../core/blitzkrieg/userError';
import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { octokit } from '../core/github/octokit';
import throwError from '../core/node/throwError';
import { CommandRegistryRaw } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
export interface RatingsPlayer {
spa_id: number;
@@ -114,14 +117,6 @@ export interface RatingsNeighbors {
neighbors: RatingsPlayer[];
}
const LEAGUES = [
{ name: 'Diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 },
];
function addSubcommands(
option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option,
@@ -130,14 +125,14 @@ function addSubcommands(
.addSubcommand((option) =>
addOptions(
option
.setName('neighbours')
.setDescription('Your position and neighbours'),
.setName('neighbors')
.setDescription('Your position and neighbors'),
)
.addStringOption(addUsernameChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many neighbours to display (default: 10)')
.setDescription('How many neighbors to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
@@ -172,25 +167,24 @@ function addSubcommands(
);
}
const noOngoingSeason = embedNegative(
'No ongoing season',
"Wargaming didn't provide any data for this season.",
);
export const ratingsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const seasonChoices = await octokit.repos
.getContent({
...DATABASE_REPO,
path: 'com/ratings',
})
.then(({ data }) => {
if (!Array.isArray(data)) {
throw throwError('Archived ratings data is malformed');
}
return data.map(
(folder) =>
({
name: `Season ${folder.name}`,
value: folder.name,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
});
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const seasonChoices = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + 1,
).map(
(number) =>
({
name: `Season ${number}`,
value: `${number}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
const command = new SlashCommandBuilder()
.setName('ratings')
@@ -236,7 +230,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
| 'current';
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbours';
| 'neighbors';
const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined;
@@ -258,41 +252,28 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (subcommandGroup === 'current'
? getRatingsInfo(region)
: getArchivedRatingsInfo(season!, region));
: getArchivedRatingsInfo(region, season!));
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const regionSubdomain = regionToRegionSubdomain(region);
const result =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
)
.then((response) => response.json() as Promise<LeagueTop>)
.then(async ({ result }) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: 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 data is malformed');
}
const players = (await fetch(data.download_url!).then(
(response) => response.json(),
)) as BlitzkriegRatingsLeaderboard;
? await getRatingsLeague(region, leagueIndex).then(
async (result) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const firstPlayerIndex =
leagueIndex === 0
? 0
@@ -305,9 +286,10 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
);
return trimmed.map(
@@ -322,114 +304,70 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
`Unknown Player ${player.id}`,
}) satisfies SimplifiedPlayer,
);
});
const leagueInfo =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.leagues[leagueIndex]
: undefined;
},
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
players = result;
playersBefore = result ? result[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.detail === undefined && result !== undefined
? regionRatingsInfo.count - result[result.length - 1].position
: 0;
regionRatingsInfo.count - result[result.length - 1].position;
titleName = `${
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
} - ${REGION_NAMES_SHORT[region]}`;
titleImage = leagueInfo
? leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`
: undefined;
titleImage = leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`;
titleDescription = `Top ${limit} players`;
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo =
subcommandGroup === 'current'
? await getRatingsInfo(region)
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings info is malformed');
}
: await getArchivedRatingsInfo(region, season!);
const response = fetch(data.download_url!);
return (await response.then((response) =>
response.json(),
)) as RatingsInfo;
});
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const clan = (
await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
)
)[id]?.clan;
const regionSubdomain = regionToRegionSubdomain(region);
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
limit / 2,
)}`,
)
.then(
(response) =>
response.json() as Promise<RatingsNeighbors>,
)
.then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: 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;
? await getRatingsNeighbors(
region,
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,
),
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw throwError(
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
'This player did not participate in this season or did not get past calibration.',
throw new UserError(
`${accountInfo.nickname} didn't player ratings in season ${season}`,
{
cause:
'This player did not participate in this season or did not get past calibration.',
},
);
}
@@ -438,9 +376,10 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
['clan'],
);
return trimmed.map((player, index) => ({
@@ -450,49 +389,42 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}));
});
},
);
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
players = neighbors;
titleImage = neighbors
? clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors
? accountInfo[id].nickname
? accountInfo.nickname
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbours';
titleDescription = 'Ratings neighbors';
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.detail === undefined
? neighbors
? regionRatingsInfo.count -
neighbors[neighbors.length - 1].position
: 0
: 0;
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
playerId = id;
}
const items = players?.map((player) => {
const reward =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.rewards.find(
(reward) =>
player.position >= reward.from_position &&
player.position <= reward.to_position,
)
: undefined;
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
detail: undefined;
} = regionRatingsInfo;
const items = players.map((player) => {
const reward = resolvedRegionRatingsInfo.rewards.find(
(reward) =>
player.position >= reward.from_position &&
player.position <= reward.to_position,
);
const midnightIndex = midnightLeaderboard?.findIndex(
(item) => item.id === player.id,
);
@@ -523,17 +455,18 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
<TitleBar
name={titleName}
image={titleImage}
nameDiscriminator={titleNameDiscriminator}
description={`${titleDescription} • ${new Date().toDateString()}`}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{playersBefore > 0 && (
<Leaderboard.Gap number={playersBefore} />
<Leaderboard.Gap message={`+ ${playersBefore} more`} />
)}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
{playersAfter > 0 && (
<Leaderboard.Gap message={`+ ${playersAfter} more`} />
)}
</Leaderboard.Root>
)}
</Wrapper>
+2 -2
View File
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices';
import embedInfo from '../core/discord/embedInfo';
@@ -30,7 +30,7 @@ export const searchClansCommand: CommandRegistry = {
const server = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const clanList = await getWargamingResponse<ClanList>(
const clanList = await fetchBlitz<ClanList>(
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
);
+6 -3
View File
@@ -2,8 +2,11 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { AccountList, usernamePattern } from '../core/blitz/listPlayers';
import fetchBlitz from '../core/blitz/fetchBlitz';
import {
AccountList,
usernamePattern,
} from '../core/blitz/searchPlayersAcrossRegions';
import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import embedInfo from '../core/discord/embedInfo';
@@ -34,7 +37,7 @@ export const searchPlayersCommand: CommandRegistry = {
const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await getWargamingResponse<AccountList>(
? await fetchBlitz<AccountList>(
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
trimmedSearch,
)}&limit=${limit}`,
+1 -1
View File
@@ -1,7 +1,7 @@
import { SlashCommandBuilder } from 'discord.js';
import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape';
import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
import { TANK_NAMES_DIACRITICS } from '../core/blitzstars/tankopedia';
import addTankChoices from '../core/discord/addTankChoices';
import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate';
+66 -82
View File
@@ -1,28 +1,68 @@
import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
import AllStatsOverview from '../components/AllStatsOverview';
import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import addFilterOptions from '../core/discord/addFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import interactionToURL from '../core/discord/interactionToURL';
import linkButton from '../core/discord/linkButton';
import primaryButton from '../core/discord/primaryButton';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePeriodFromCommand, {
ResolvedPeriod,
} from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { CommandRegistryRaw } from '../events/interactionCreate';
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo';
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
const filterDescriptions = await filtersToDescription(filters);
return (
<Wrapper>
<TitleBar
name={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{!stats.battles && <NoData type={NoDataType.BattlesInPeriod} />}
{stats.battles > 0 && (
<AllStatsOverview stats={stats} supplementaryStats={supplementary} />
)}
</Wrapper>
);
}
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
const command = await addStatTypeSubCommandGroups(
const command = await addFilterOptions(
new SlashCommandBuilder()
.setName('stats')
.setDescription('Regular battles statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
@@ -32,60 +72,20 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
command,
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const resolvedPeriod = resolvePeriodFromCommand(
player.region,
interaction,
);
const period = interaction.options.getString('period');
const tankIdRaw = interaction.options.getString('tank')!;
const tankId =
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const path = interactionToURL(interaction, {
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
tankId,
start,
end,
period,
...getCustomPeriodParams(interaction),
...filters,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const tierOption = interaction.options.getString('tier');
return [
await stats(
commandGroup,
resolvedPeriod,
player,
commandGroup === 'tank'
? tankId
: commandGroup === 'multi-tank'
? ({
'tank-type':
interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined,
tier: tierOption ? parseInt(tierOption) : undefined,
'tree-type': (interaction.options.getString('tree-type') ??
undefined) as TreeTypeString | undefined,
} satisfies Partial<MultiTankFilters>)
: null,
),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars',
),
];
return Promise.all([
render(player, period, filters),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
},
autocomplete: (interaction) => {
@@ -94,27 +94,11 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await stats(
commandGroup,
period,
player,
commandGroup === 'tank'
? parseInt(url.searchParams.get('tankId')!)
: commandGroup === 'multi-tank'
? ({
'tank-type': url.searchParams.get('tank-type')!,
nation: url.searchParams.get('nation')!,
tier: parseInt(url.searchParams.get('tier')!),
'tree-type': url.searchParams.get('tree-type') as TreeTypeString,
} satisfies MultiTankFilters)
: null,
);
return await render(player, period, filters);
},
});
});
-83
View File
@@ -1,83 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import interactionToURL from '../core/discord/interactionToURL';
import linkButton from '../core/discord/linkButton';
import primaryButton from '../core/discord/primaryButton';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import today from '../renderers/today';
import { AccountInfo } from '../types/accountInfo';
export const todayCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('today')
.setDescription('A general daily breakdown of your performance')
.addStringOption(addUsernameChoices)
.addIntegerOption((option) =>
option
.setName('cutoff')
.setDescription(
'The maximum number of tanks to display (default: Infinity)',
)
.setRequired(false)
.setMinValue(1),
)
.addIntegerOption((option) =>
option
.setName('maximized')
.setDescription(
'The number of rows with full stats after which all are collapsed (default: 4)',
)
.setMinValue(0)
.setRequired(false),
)
.addBooleanOption((option) =>
option
.setName('show-total')
.setDescription('Show the total stats at the top (default: true)')
.setRequired(false),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
const maximized = interaction.options.getInteger('maximized') ?? undefined;
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const path = interactionToURL(interaction, {
...player,
cutoff,
maximized,
'show-total': showTotal,
});
return [
await today(player, cutoff, maximized, showTotal, false),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars',
),
];
},
autocomplete: autocompleteUsername,
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
return await today(player);
},
};
-82
View File
@@ -1,82 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' };
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative';
import embedPositive from '../core/discord/embedPositive';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
export const verifyCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('verify')
.setDescription("Set's the user's username to their in-game name")
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
async handler(interaction) {
const blitzAccount = await resolvePlayerFromCommand(interaction);
const { id, region: server } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
);
const clan = clanData[id]?.clan;
const clanTag = clan ? ` [${clan!.tag}]` : '';
const member =
interaction.member &&
interaction.guild?.members.cache.get(interaction.member?.user.id);
if (!interaction.guild?.members.me?.permissions.has('ManageNicknames')) {
return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`,
"I don't have the permission to change your nickname.",
);
}
if (!interaction.member || !member) {
return embedNegative(
`${interaction.user.username} is not in this server`,
"I couldn't find this person in this server. Did they leave?",
);
}
try {
await member.setNickname(`${accountInfo[id].nickname}${clanTag}`);
if (interaction.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`,
"I don't have the permission to change your manage roles.",
);
}
await member.roles.remove(discord.sklld_verify_role);
await member.roles.add(discord.sklld_peasant_role);
}
return embedPositive(
`${interaction.user.username} is verified`,
`The user is now verified as ${markdownEscape(
accountInfo[id].nickname,
)}${markdownEscape(clanTag)}`,
);
} catch (error) {
return embedNegative(
`${interaction.user.username} failed to verify`,
"I can't change your nickname because you have higher permissions than me. Try setting your nickname to your Blitz username manually.",
);
}
},
autocomplete: autocompleteUsername,
};
@@ -1,31 +1,28 @@
import { Percentile } from '../../../../../constants/percentiles';
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator';
export enum GlowSide {
Top,
Bottom,
}
export interface GlowProps {
percentile: Percentile;
side: GlowSide;
color: string;
direction?: 'default' | 'reverse';
rotation?: number;
}
export function Glow({ percentile, side }: GlowProps) {
const color = PERCENTILE_COLORS[percentile];
export function Glow({
color,
direction = 'default',
rotation = 0,
}: GlowProps) {
const color1 = color;
const color2 = `${color}00`;
const background = `linear-gradient(${
side === GlowSide.Top ? 0 : 180
direction === 'default' ? 0 : 180
}deg, ${color1} 0%, ${color2} 100%)`;
return (
<div
style={{
display: 'flex',
alignItems: side === GlowSide.Top ? 'flex-end' : 'flex-start',
alignItems: direction === 'default' ? 'flex-end' : 'flex-start',
justifyContent: 'center',
transform: `rotate(${rotation}deg)`,
}}
>
<div style={{ height: 54, width: 16, background }} />
@@ -1,8 +1,8 @@
import { Percentile } from '../../../../constants/percentiles';
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile';
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
import { theme } from '../../../../stitches.config';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
import { Glow, GlowSide } from './components/Glow';
import { Glow } from './components/Glow';
export interface WN8DisplayProps {
WN8?: number;
@@ -37,7 +37,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
gap: -16,
}}
>
<Glow percentile={percentile} side={GlowSide.Top} />
<Glow color={PERCENTILE_COLORS[percentile]} />
<div
style={{
@@ -71,7 +71,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
</span>
</div>
<Glow percentile={percentile} side={GlowSide.Bottom} />
<Glow color={PERCENTILE_COLORS[percentile]} direction="reverse" />
</div>
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
import { WN8Display } from './components/WN8Display';
import {
WN8SurroundingStat,
+1 -2
View File
@@ -1,5 +1,4 @@
import { Percentile } from '../../../constants/percentiles';
import { TankType } from '../../../core/blitz/tankopedia';
import { theme } from '../../../stitches.config';
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
import { RowStat } from './RowStat';
@@ -17,7 +16,7 @@ interface RowProps {
title: string;
minimized?: boolean;
treeType?: TreeTypeEnum;
tankType?: TankType;
tankType?: string;
stats: (RowStatItem | undefined)[];
}
@@ -15,7 +15,7 @@ export function RowStat({
value,
delta,
percentile,
minimized,
minimized = false,
}: RowStatProps) {
return (
<div
@@ -44,6 +44,7 @@ export function Card({ items, title }: CardProps) {
>
{items.map((item) => (
<div
key={item.label}
style={{
display: 'flex',
gap: 8,
+3 -5
View File
@@ -1,12 +1,10 @@
import getWN8Percentile from '../../core/blitz/getWN8Percentile';
import isNumber from '../../core/node/isNumber';
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
import isNumber from '../../core/math/isNumber';
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
import { Card } from './components/Card';
import { Root } from './components/Root';
import { Row } from './components/Row';
// TODO: remove the .js endings
export interface GenericAllStatsProps {
stats: AllStats;
supplementaryStats?: SupplementaryStats;
@@ -15,9 +15,18 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
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]}
</span>
<span
style={{
color: theme.colors.textHighContrast,
@@ -1,10 +1,10 @@
import { theme } from '../../../stitches.config';
export interface GapProps {
number: number;
message: string;
}
export function Gap({ number }: GapProps) {
export function Gap({ message: number }: GapProps) {
return (
<div
style={{
@@ -22,7 +22,7 @@ export function Gap({ number }: GapProps) {
fontSize: 16,
}}
>
+ {number.toLocaleString()} more
{number.toLocaleString()}
</span>
</div>
);
+18 -12
View File
@@ -1,10 +1,11 @@
import { ComponentProps } from 'react';
import { RatingsReward } from '../../../commands/ratings';
import { theme } from '../../../stitches.config';
export interface ItemProps {
export interface ItemProps extends ComponentProps<'div'> {
position: number;
deltaPosition?: number;
score: number;
score?: number;
deltaScore?: number;
reward?: RatingsReward;
nickname: string;
@@ -25,10 +26,13 @@ export function Item({
nickname,
clan,
highlight,
...props
}: ItemProps) {
return (
<div
{...props}
style={{
cursor: 'pointer',
display: 'flex',
padding: 8,
backgroundColor: highlight
@@ -185,16 +189,18 @@ export function Item({
}
/>
)}
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{score.toLocaleString()}
</span>
{score !== undefined && (
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{score.toLocaleString()}
</span>
)}
</div>
</div>
);
+22 -15
View File
@@ -1,24 +1,31 @@
import { Flex, Theme } from '@radix-ui/themes';
import { ComponentProps } from 'react';
interface PageWrapperProps extends ComponentProps<typeof Flex> {
color?: ComponentProps<typeof Theme>['accentColor'];
}
export default function PageWrapper({
style,
color,
...props
}: ComponentProps<'div'>) {
}: PageWrapperProps) {
return (
<div
style={{
width: '100%',
maxWidth: 800,
margin: 'auto',
display: 'flex',
padding: 16,
flexDirection: 'column',
gap: 16,
boxSizing: 'border-box',
<Theme accentColor={color}>
<Flex
{...props}
direction="column"
gap="4"
style={{
width: '100%',
maxWidth: 800,
margin: 'auto',
padding: 16,
boxSizing: 'border-box',
...style,
}}
{...props}
/>
...style,
}}
/>
</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}`} />;
}
+18 -5
View File
@@ -2,8 +2,7 @@ import {
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
TankType,
} from '../../../core/blitz/tankopedia';
} from '../../../core/blitzstars/tankopedia';
import { theme } from '../../../stitches.config';
export enum TreeTypeEnum {
@@ -12,11 +11,17 @@ export enum TreeTypeEnum {
Collector,
}
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
export type TreeTypeString = 'tech-tree' | 'premium' | 'collector';
export const TREE_TYPE_NAMES: Record<TreeTypeString, string> = {
'tech-tree': 'Tech tree',
premium: 'Premium',
collector: 'Collector',
};
export interface ItemProps {
image?: string;
tankType?: TankType;
tankType?: string;
name: string;
treeType: TreeTypeEnum;
}
@@ -75,7 +80,15 @@ export function Item({ image, tankType, name, treeType }: ItemProps) {
</span>
</div>
{image && <img src={image} style={{ flex: 1 }} />}
{/* {image && (
// TODO: remove hardcoded dimensions when satori fixes non-width unloadable images
<img
src={image}
width={106}
height={32}
style={{ objectFit: 'cover' }}
/>
)} */}
</div>
);
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { TIER_ROMAN_NUMERALS, Tier } from '../core/blitz/tankopedia';
import { Tier } from '../core/blitzstars/tankopedia';
import { theme } from '../stitches.config';
export type TierWeightsRecord = Partial<Record<Tier, number>>;
@@ -26,7 +26,7 @@ export default function TierWeights({ weights }: TierWeightsProps) {
minWidth: 16,
}}
>
{TIER_ROMAN_NUMERALS[parseInt(tier) as Tier]}
{tier}
</div>
))}
</div>
+2 -22
View File
@@ -2,17 +2,11 @@ import { theme } from '../stitches.config';
export interface TitleBarProps {
name: string;
nameDiscriminator?: string;
image?: string;
description?: string;
}
export default function TitleBar({
name,
nameDiscriminator,
image,
description,
}: TitleBarProps) {
export default function TitleBar({ name, image, description }: TitleBarProps) {
return (
<div
style={{
@@ -33,9 +27,8 @@ export default function TitleBar({
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
justifyContent: 'center',
gap: 4,
}}
>
@@ -51,24 +44,11 @@ export default function TitleBar({
color: theme.colors.textHighContrast,
fontWeight: 900,
whiteSpace: 'nowrap',
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
overflow: 'hidden',
}}
>
{name}
</span>
{nameDiscriminator && (
<span
style={{
fontSize: 32,
color: theme.colors.textLowContrast,
fontWeight: 900,
whiteSpace: 'nowrap',
}}
>
{nameDiscriminator}
</span>
)}
</div>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
+3 -2
View File
@@ -4,15 +4,16 @@ import { theme } from '../stitches.config';
export interface WrapperProps {
children: ReactNode;
naked?: boolean;
fat?: boolean;
}
export default function Wrapper({ children, naked }: WrapperProps) {
export default function Wrapper({ children, naked, fat }: WrapperProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: 16,
padding: `16px ${fat ? 0 : 16}px`,
gap: 32,
width: 480,
color: theme.colors.textHighContrast,
-1
View File
@@ -1 +0,0 @@
export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
+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,
};
Loaded 100 of 211 files, more files were not shown because too many files have changed in this diff. Show more