Compare commits

..
330 Commits
Author SHA1 Message Date
TrèsAbhi c1bd91a7f9 Merge pull request #63 from tresabhi/1.15.11 2024-01-21 18:48:12 -06:00
TrèsAbhi 1d65ff3e0c fix: type 2024-01-21 18:46:38 -06:00
TrèsAbhi f7fac5ad26 feat: chagelogs 2024-01-21 18:44:58 -06:00
TrèsAbhi e45f055493 feat: permissions command 2024-01-21 18:43:07 -06:00
TrèsAbhi 8946f115c0 feat: no embed errors 2024-01-21 18:29:32 -06:00
TrèsAbhi 6094875ffb discord tokens... for some reason? 2024-01-19 21:52:50 -06:00
TrèsAbhi 0644c9a67f Merge pull request #62 from tresabhi/1.15.10 2024-01-19 21:48:33 -06:00
TrèsAbhi c91ab29a23 feat: disabled tankopedia 2024-01-19 21:46:36 -06:00
TrèsAbhi 8f4e33fd5a feat: changelogs 2024-01-19 21:45:23 -06:00
TrèsAbhi 8cf5012602 feat: full region names in /link 2024-01-19 21:35:46 -06:00
TrèsAbhi 23a23e36bb feat: /today alias for /breakdown today 2024-01-19 21:32:46 -06:00
TrèsAbhi 0480445bae feat: easier /link command 2024-01-19 20:53:27 -06:00
TrèsAbhi ae07b8de4c feat: center align text when no image 2024-01-19 20:47:46 -06:00
TrèsAbhi 4c746cbb86 feat: improved error messages 2024-01-19 16:07:15 -06:00
TrèsAbhi 890a4d142b feat: simplified /ratings command 2024-01-19 16:02:03 -06:00
TrèsAbhi ff9bda29cf fix: archive league neighbors 2024-01-19 15:28:02 -06:00
TrèsAbhi 5ca9e29f3d fix: percentile indicator contrast 2024-01-19 15:22:53 -06:00
TrèsAbhi c804a3d564 feat: update version number 2024-01-19 09:53:42 -06:00
TrèsAbhi daa549e650 Merge pull request #61 from tresabhi/1.15.9 2024-01-01 14:26:13 -06:00
TrèsAbhi fe562915cb feat: docs 2024-01-01 14:24:45 -06:00
TrèsAbhi fff42b61e1 fix: deleted ratings players 2024-01-01 14:22:39 -06:00
TrèsAbhi 90ee71c9ca fix: archived season number 2024-01-01 14:19:13 -06:00
TrèsAbhi 3f264d6a49 feat: auto restart every 5 hours 2023-12-23 18:31:45 -06:00
TrèsAbhi 61eab0bca8 feat: update docs image 2023-12-15 22:00:20 -06:00
TrèsAbhi 0a19157dbb Merge branch '1.16.0' into main 2023-12-13 12:08:55 -06:00
TrèsAbhi 382a23b661 Merge pull request #58 from tresabhi/1.15.8 2023-12-13 12:03:16 -06:00
TrèsAbhi 98107291f5 feat: changelogs 2023-12-13 12:03:02 -06:00
TrèsAbhi c6779a714b feat: custom titles 2023-12-13 11:59:37 -06:00
TrèsAbhi ef8f0fd72d feat: color previews 2023-12-13 11:46:36 -06:00
TrèsAbhi 634bd2ea07 feat: shell icons and modes 2023-12-12 23:35:08 -06:00
TrèsAbhi f8f88ff8f2 fix: types 2023-12-12 18:11:55 -06:00
TrèsAbhi 8a2dc24f7e feat: more ui 2023-12-12 16:40:22 -06:00
TrèsAbhi dcc3ab6e4e feat: module ui 2023-12-12 15:12:09 -06:00
TrèsAbhi dabeab8612 feat: gun defs 2023-12-12 14:14:26 -06:00
TrèsAbhi 66dbc5b2ed feat: more defs 2023-12-12 13:40:58 -06:00
TrèsAbhi 2e2ab9b2d9 fix: added binary start padding 2023-12-12 12:01:50 -06:00
TrèsAbhi 662e0bb9ce feat: optimizations 2023-12-11 11:21:40 -06:00
TrèsAbhi 3f40e94235 feat: cleanup x2 2023-12-10 17:22:40 -06:00
TrèsAbhi 483b625f2d feat: cleanup 2023-12-10 17:14:05 -06:00
TrèsAbhi d18cf625d0 feat: final compatibility 2023-12-10 17:03:01 -06:00
TrèsAbhi b42df81b48 feat: remove running bot instructions 2023-12-10 12:03:34 -06:00
TrèsAbhi 536fed5448 feat: newer website image 2023-12-10 12:03:02 -06:00
TrèsAbhi dc01eff812 feat: ka 0xff02 handling 2023-12-10 11:57:21 -06:00
TrèsAbhi 5471c7fa32 feat: unconfirmed types 2023-12-10 11:51:54 -06:00
TrèsAbhi 83e28b4dac feat: move versions x2 2023-12-09 19:05:46 -06:00
TrèsAbhi af95897bde feat: move versions 2023-12-09 18:38:24 -06:00
TrèsAbhi 4c2f6bea25 feat: uhh 2023-12-09 15:56:25 -06:00
TrèsAbhi fdf77a43cb feat: sc2 complete 2023-12-08 19:11:40 -06:00
TrèsAbhi f1855a569a feat: removed sub values 2023-12-08 18:45:57 -06:00
TrèsAbhi a0434b6ed2 feat: more sc2 x2 2023-12-08 18:32:11 -06:00
TrèsAbhi 365c714213 feat: more sc2 2023-12-08 18:14:33 -06:00
TrèsAbhi 875abf4d32 feat: sc2 start 2023-12-08 11:36:54 -06:00
TrèsAbhi 6b9bf76da2 feat: model reconstruction 2023-12-08 11:06:56 -06:00
TrèsAbhi 610c75d274 feat: float 2023-12-07 23:36:46 -06:00
TrèsAbhi 81826d0549 feat: more data driven 2023-12-07 22:41:49 -06:00
TrèsAbhi 8caf4069dc feat: more things 2023-12-07 22:13:54 -06:00
TrèsAbhi 6c496e616d feat: scg tests 2023-12-07 12:58:54 -06:00
TrèsAbhi 3d5dd54ec0 feat: start of tank page 2023-12-06 17:58:26 -06:00
TrèsAbhi e7bfe81940 feat: bounded filters 2023-12-06 16:43:36 -06:00
TrèsAbhi fb9c04c779 feat: even cleaner filters 2023-12-06 12:48:19 -06:00
TrèsAbhi 84a1af8100 feat: cleaner filters 2023-12-06 12:40:43 -06:00
TrèsAbhi b804e60bac fix: jumping navbar 2023-12-06 11:33:23 -06:00
TrèsAbhi 604e751bf4 feat: test tank filters 2023-12-06 10:34:43 -06:00
TrèsAbhi 2ce083222e fix: tighter all targets integration 2023-12-06 10:19:13 -06:00
TrèsAbhi 9a44b1fcfb feat: re-added rest of the asset gens 2023-12-06 10:17:42 -06:00
TrèsAbhi 9df47777d0 feat: build assets 2023-12-05 19:14:53 -06:00
TrèsAbhi aa78bc4045 feat: modernized 2023-12-05 18:05:14 -06:00
TrèsAbhi 4bfe34ba53 feat: read dvpl 2023-12-05 17:59:58 -06:00
TrèsAbhi ad0fc69596 feat: move tankopedia to tankDefinitions 2023-12-05 17:36:18 -06:00
TrèsAbhi cfe71e15a4 feat: frosty navbar 2023-12-04 19:46:53 -06:00
TrèsAbhi bf7a319770 feat: better navbar 2023-12-04 19:27:23 -06:00
TrèsAbhi cadc84b32f feat: nation filters 2023-12-04 19:10:26 -06:00
TrèsAbhi 7bde92c7f1 feat: update to new asset format 2023-12-04 18:48:52 -06:00
TrèsAbhi 965d36cdc5 feat: remove more to... array funcs 2023-12-04 18:45:38 -06:00
TrèsAbhi f8ef0063e4 fix: removed toSorted 2023-12-04 18:40:57 -06:00
TrèsAbhi fb4e00afc4 feat: tier display 2023-12-04 18:40:24 -06:00
TrèsAbhi 6ed9a191b9 feat: generalized page turner 2023-12-04 12:39:14 -06:00
TrèsAbhi b503f4fa8f feat: more filters 2023-12-04 12:35:27 -06:00
TrèsAbhi 7020e5641f feat: tier filters 2023-12-04 11:47:33 -06:00
TrèsAbhi 5b4601b671 feat: persistent sorting 2023-12-04 10:25:54 -06:00
TrèsAbhi 3e952faad3 feat: correct flags 2023-12-04 10:13:14 -06:00
TrèsAbhi c51abc1c64 feat: :) 2023-12-03 22:11:37 -06:00
TrèsAbhi c7e2780d17 feat: i am having too much fun 2023-12-03 22:09:08 -06:00
TrèsAbhi 28e2139683 feat: even coooooooooler animations 2023-12-03 21:44:58 -06:00
TrèsAbhi 20c839394a fix: page readjusting on search 2023-12-03 19:51:55 -06:00
TrèsAbhi c5e1a7d6ab feat: page turner 2023-12-03 19:45:43 -06:00
TrèsAbhi 579d7570af feat: better sizing 2023-12-03 18:26:48 -06:00
TrèsAbhi 1d1d751d43 fix: missing tank types 2023-12-03 18:16:59 -06:00
TrèsAbhi 86664b5491 feat: updated dependencies 2023-12-03 18:16:49 -06:00
TrèsAbhi b7e9898163 Merge pull request #57 from tresabhi/main 2023-12-03 10:53:29 -06:00
TrèsAbhi d4c0b01343 Merge pull request #56 from tresabhi/1.15.7 2023-12-03 10:51:33 -06:00
TrèsAbhi af67126156 feat: changelogs 2023-12-03 10:51:15 -06:00
TrèsAbhi 9930360405 feat: session tracker classnames 2023-12-03 10:44:49 -06:00
TrèsAbhi 4fb61d6dbd feat: session tracker themeing 2023-12-03 10:43:42 -06:00
TrèsAbhi bcb7ca5c4c fix: local string for ratings overflow count 2023-12-03 09:57:06 -06:00
TrèsAbhi d1fb5da002 feat: flags path 2023-12-03 09:56:08 -06:00
TrèsAbhi 17935fbe89 feat: better contrast 2023-12-03 09:55:57 -06:00
TrèsAbhi 48578407db feat: icons 2023-12-01 12:36:31 -06:00
TrèsAbhi bda0c4ef1b feat: animations on hove 2023-12-01 12:23:17 -06:00
TrèsAbhi 7ea5e444b3 feat: better icons 2023-11-30 18:00:48 -06:00
TrèsAbhi bef362935a feat: flags 2023-11-30 12:59:37 -06:00
TrèsAbhi f3144be382 feat: tankopedia website 2023-11-30 12:21:20 -06:00
TrèsAbhi 818ee1d447 feat: use userstring 2023-11-30 12:21:15 -06:00
TrèsAbhi 67e45ada85 feat: use short tank name 2023-11-29 12:20:59 -06:00
TrèsAbhi bdc7132337 feat: use new tankopedia 2023-11-29 12:17:21 -06:00
TrèsAbhi c5214f8d89 Merge pull request #55 from tresabhi/main 2023-11-28 10:19:47 -06:00
TrèsAbhi 2be4dbea7a Merge pull request #54 from tresabhi/1.15.6 2023-11-28 10:18:06 -06:00
TrèsAbhi b951c81bf5 feat: changelog 2023-11-28 10:17:56 -06:00
TrèsAbhi 8c221a1295 feat: updated creators with a better format 2023-11-28 10:16:28 -06:00
TrèsAbhi c41f721a96 fix: regions at root 2023-11-28 10:14:45 -06:00
TrèsAbhi 6910f3c95e feat: better multiple file handling 2023-11-27 17:50:02 -06:00
TrèsAbhi e23f898c5f feat: assets writing 2023-11-27 16:52:38 -06:00
TrèsAbhi 3cf81afc4a feat: tree type 2023-11-27 12:26:51 -06:00
TrèsAbhi ca463a0f0e fix: removed dist package 2023-11-27 12:19:20 -06:00
TrèsAbhi a76572ebe3 Merge pull request #53 from tresabhi/main 2023-11-27 12:17:35 -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 5bf83da726 feat: ratings leaderboard efficiency plot script 2023-11-20 13:16:44 -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 393da234e4 feat: tankopedia images 2023-11-16 12:57:46 -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
TrèsAbhi dc9ab2067d Merge pull request #40 from tresabhi/1.13.3 2023-10-08 17:35:06 -05:00
TrèsAbhi e8e1f7bafb feat: 1.13.3 2023-10-08 17:34:02 -05:00
TrèsAbhi e50a456025 Update cd-host.yml 2023-10-05 11:10:19 -05:00
TrèsAbhi d442bbca1a Update cd-host.yml 2023-10-05 11:06:48 -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
256 changed files with 11511 additions and 4422 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:
+1 -1
View File
@@ -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 }} DISCORD_TOKEN=${{ secrets.DISCORD_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 }} DISCORD_TOKEN=${{ secrets.DISCORD_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 }} DISCORD_TOKEN=${{ secrets.DISCORD_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/
+33
View File
@@ -1,47 +1,80 @@
{
"cSpell.words": [
"AABBOX",
"Abhi",
"accountinfo",
"alphatest",
"apcr",
"atgm",
"bepaint",
"BINORMAL",
"blitzstars",
"cleandist",
"cockroackdb",
"corsproxy",
"CUBETEXCOORD",
"customperiod",
"deburr",
"decalmask",
"diacriticless",
"dvpl",
"DVPLCRC",
"Everytime",
"FASTNAME",
"fullstats",
"fuzzysort",
"gamemode",
"immer",
"JOINTINDEX",
"JOINTWEIGHT",
"kranvagn",
"krieg",
"Leaderboard",
"leaderboards",
"MURICA",
"normalmap",
"Oopsy",
"outdir",
"outfile",
"ownedtanks",
"playerachievements",
"playerinfo",
"Promisable",
"ratingsleaderboard",
"ratingsplot",
"researchable",
"Resvg",
"roboto",
"SCPG",
"searchclans",
"searchplayers",
"searchtanks",
"SKLLD",
"Smri",
"Specularity",
"startcase",
"statsfull",
"steamapps",
"Stringifying",
"strvk",
"subnation",
"tankaverages",
"tankopedia",
"tankopedias",
"tankstats",
"TEXCOORD",
"tresabhi",
"Unicum",
"Unregistering",
"vickers",
"vitepress",
"Vovko",
"wargaming",
"webp",
"winrate",
"wotblitz",
"XMLDVPL",
"YAMLDVPL",
"zustand"
],
"deno.enable": false,
+2 -23
View File
@@ -14,31 +14,10 @@ Go to [application directory for Blitzkrieg](https://discord.com/application-dir
### https://blitz-krieg.vercel.app/
![Blitzkrieg website](https://i.imgur.com/QOXUP63.png)
![Blitzkrieg website](https://i.imgur.com/eRIZe5P.png)
## Visit the documentation website
### https://tresabhi.github.io/blitzkrieg/
![Blitzkrieg documentation website](https://i.imgur.com/mCZCZEI.png)
## Run the bot
### Install dependencies
```bash
yarn
```
### Create `.env`
```env
GH_TOKEN={YOUR_GITHUB_TOKEN_HERE}
DISCORD_TOKEN={YOUR_DISCORD_TOKE_HERE}
```
### Run the command
```bash
yarn dev:bot
```
![Blitzkrieg documentation website](https://i.imgur.com/TOt5fMx.png)
+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: [
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.13.3
Streamer's delight!
## Features
- Added config context menu for the session tracker embed
+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
+23
View File
@@ -0,0 +1,23 @@
# Blitzkrieg 1.15.10
The 10th patch for the `1.15` update is here, breaking the record of the most patches for a single update!
## Features
- Added `/today`
- This is a direct alias for `/breakdown today`
- Drastically simplified ratings command
- `/ratings current` and `/ratings archive` have been now merged
- Where the data is fetched from is now decided directly from the chosen season number
- Simplified many error messages
- Title bar description text is now centered when there is no image
- It always felt awkward when there was no image on the left but the left still aligned to the right
- Removed `/link`'s requirement to select a username explicitly from the search results
- Full names for regions are now used in the `/link` command
- So "NA" turns into "North America"
## Fixes
- Fixed contrast issues with WN8 percentile colors behind white text
- Archived ratings neighbors now functions once again
- Disabled tankopedia; this feature isn't finished yet and the changes were accidentally leaked
+11
View File
@@ -0,0 +1,11 @@
# Blitzkrieg 1.15.11
There's an easter egg somewhere in this update 🤔
## Features
- Added `/permissions` which lists all permissions required to run the bot and checks if the bot has them or not
## Changes
- Error messages now do no use embeds, instead utilize simple headings and paragraphs
+9
View File
@@ -0,0 +1,9 @@
# Blitzkrieg 1.15.2
Infinity reduced to a finite value.
## Fixes
- Added a maximum value to the jump to position button
- Removed neighbor checking for the seeding player
- This fixes the jump to position button not loading at all
+30
View File
@@ -0,0 +1,30 @@
# Blitzkrieg 1.15.3
The bot now gets more expressive!
## Features
- The bot can now have presence messages; examples:
- "Living in {number} servers"
- "Server {number} users"
- "Observing {number} channels"
- More will be added in the future! Give me some ideas
## Changes
- Moved `/credits` to `/about creators`
- `/debug` command now ditches embeds and has better information to serve
## Technical Features
- Added a workflow and lint for package and changelog version miss match
## Technical Changes
- Removed manual trigger for Asian latest ratings leaderboard archiver
- Leaderboard archiver now accepts an explicit target like `midnight` or `latest`
- Combined midnight and latest leaderboard archival workflow into one file
## Technical Fixed
- Reviewed all workflow triggers and updated them
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.15.4
Unlocking future potential
## Technical Changes
- Moved the word "Ratings" to the front of the workflow name
- Renamed `blitzkrieg-db` to `blitzkrieg-assets`
- Moved the region folders from the root to `regions`
## Technical Fixes
- The ratings leaderboard archiver now correctly skips undefined players
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.15.5
Oops, this should've been in the last patch.
## Fixes
- User and channel cache is now guild cache exhaustive
+11
View File
@@ -0,0 +1,11 @@
# Blitzkrieg 1.15.6
This should have also been in the last update lol.
## Changes
- Changed the format of `/about creators` and added one more person
## Fixes
- Archived ratings leaderboards are now fetched from the correct URL path
+20
View File
@@ -0,0 +1,20 @@
# Blitzkrieg 1.15.7
Simple yet powerful theming options for the session tracker
## Features
- The accent color of the session tracker (hence the embed too) can now be tweaked
- Access this setting via the dropdown from Options > Accent color
## Fixes
- Reimplemented the commas for last numbers in the overflow count of the ratings leaderboard
- For example, "+1234567 more" will now read "+1,234,567 more"
## Technical Features
- Added class names for more in-depth customization for those who are comfortable with CSS
- `session-tracker-root`
- `session-tracker-row`
- `session-tracker-row-stat`
+9
View File
@@ -0,0 +1,9 @@
# Blitzkrieg 1.15.8
A request fulfilled \:\)
## Features
- Added custom titles in session tracker
- Change the title through Options > Edit title...
- Colors are shown when selecting a theme color for the session tracker
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.15.9
~~Curiosity killed the cat~~ Ratings killed the Blitzkrieg
## Fixes
- Deleted ratings players are now handled properly
## Technical Fixes
- Latest archived season number
- Is now cached properly
- Is now fetched in a chaining fashion correctly
+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
-7
View File
@@ -1,7 +0,0 @@
# Permissions
The following Discord permissions are required by the bot to ensure full functionality:
- Manage nicknames
- Read messages / view channels
- Attach files
+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

+2 -1
View File
@@ -1,12 +1,13 @@
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
const withVanillaExtract = createVanillaExtractPlugin();
const withVanillaExtract = createVanillaExtractPlugin({ tests: /\.css$/ });
/** @type {import('next').NextConfig} */
const nextConfig = {
...withVanillaExtract(),
distDir: 'dist/website',
reactStrictMode: false,
};
export default nextConfig;
-7
View File
@@ -1,7 +0,0 @@
{
"main": "server.js",
"type": "module",
"scripts": {
"start": "node --enable-source-maps --trace-warnings server.cjs"
}
}
+27 -5
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkrieg",
"version": "1.13.2",
"version": "1.15.11",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
@@ -9,10 +9,13 @@
"build:website": "next build",
"build:docs": "vitepress build docs",
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"build:assets": "vite-node --script scripts/buildAssets",
"build:test": "vite-node --script scripts/buildTest",
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot",
"dev:bot": "yarn build:bot && yarn start:bot",
"dev:docs": "vitepress dev docs",
"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,48 +23,67 @@
"lint": "npx tsc",
"lint:website": "next lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
"knip": "knip",
"update": "yarn upgrade --latest"
},
"license": "MIT",
"dependencies": {
"@octokit/plugin-throttling": "^8.1.3",
"@octokit/rest": "^20.0.1",
"@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",
"@radix-ui/themes": "^2.0.1",
"@react-three/drei": "^9.90.1",
"@react-three/fiber": "^8.15.12",
"@resvg/resvg-js": "^2.4.1",
"@vanilla-extract/css": "^1.13.0",
"bepaint": "^0.1.0-beta.3",
"decompress": "^4.2.1",
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"esbuild": "^0.19.3",
"express": "^4.18.2",
"false": "^0.0.4",
"fuzzysort": "^2.0.4",
"immer": "^10.0.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"markdown-escape": "^2.0.0",
"next": "^13.5.2",
"next": "^14.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-fuzzysort": "^1.0.1",
"satori": "^0.10.8",
"sharp": "^0.33.0",
"three": "^0.159.0",
"vite-node": "^0.34.5",
"vitepress": "^1.0.0-rc.20",
"vitepress-plugin-auto-sidebar": "^1.1.0",
"yaml": "^2.3.4",
"zustand": "^4.4.1"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
"@types/decompress": "^4.2.6",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.18",
"@types/js-yaml": "^4.0.8",
"@types/lodash": "^4.14.199",
"@types/lz4": "^0.6.4",
"@types/markdown-escape": "^1.1.0",
"@types/node": "^20.6.5",
"@types/react": "^18.2.22",
"@types/three": "^0.159.0",
"@vanilla-extract/next-plugin": "^2.3.1",
"compare-versions": "^6.1.0",
"knip": "^2.27.1",
"crc": "^4.3.2",
"fast-xml-parser": "^4.3.2",
"knip": "^3.4.0",
"lz4": "^0.6.5",
"prettier": "^3.0.3",
"prisma": "^5.4.1",
"typescript": "^5.2.2"
}
}
@@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE "discord_blitz" (
"discord" INT8 NOT NULL,
"region" STRING NOT NULL,
"blitz" INT4 NOT NULL,
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
);
+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}`)),
);
+544
View File
@@ -0,0 +1,544 @@
import { config } from 'dotenv';
import { existsSync } from 'fs';
import { mkdir, readdir, rm } from 'fs/promises';
import { argv } from 'process';
import sharp from 'sharp';
import { TankType } from '../src/components/Tanks';
import { NATION_IDS } from '../src/constants/nations';
import { SCPGStream } from '../src/core/blitz/SCPGStream';
import { readBase64DVPL } from '../src/core/blitz/readBase64DVPL';
import { readDVPLFile } from '../src/core/blitz/readDVPLFile';
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
import { readXMLDVPL } from '../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../src/core/blitz/toUniqueId';
import commitMultipleFiles, {
FileChange,
} from '../src/core/blitzkrieg/commitMultipleFiles';
import { BlitzkriegGunDefinitions } from '../src/core/blitzkrieg/definitions/guns';
import {
BlitzkriegShellDefinitions,
ShellType,
} from '../src/core/blitzkrieg/definitions/shells';
import {
BlitzkriegTankDefinitions,
Tier,
} from '../src/core/blitzkrieg/definitions/tanks';
import { BlitzkriegTurretDefinitions } from '../src/core/blitzkrieg/definitions/turrets';
config();
// WARNING! MOST OF THESE TYPES ARE NOT EXHAUSTIVE!
interface VehicleDefinitionList {
[key: string]: {
id: number;
userString: string;
shortUserString?: string;
description: string;
price: number | { gold: ''; '#text': number };
sellPrice?: { gold: ''; '#text': number };
enrichmentPermanentCost: number;
notInShop?: boolean;
tags: string;
level: number;
combatRole?: Record<string, string>;
configurationModes: string;
};
}
interface TurretDefinitionsList {
nextAvailableId: number;
ids: Record<string, number>;
}
interface GunDefinitionsList {
nextAvailableId: number;
ids: Record<string, number>;
shared: {
[key: string]: {
userString: string;
tags: string;
level: number;
pitchLimits: string;
burst?: { count: number; rate: number };
clip?: { count: number; rate: number };
shots: {
[key: string]: {
speed: number;
piercingPower: string;
};
};
};
};
}
type ShellKind =
| 'ARMOR_PIERCING'
| 'ARMOR_PIERCING_CR'
| 'HIGH_EXPLOSIVE'
| 'HOLLOW_CHARGE';
type ShellDefinitionsList = Record<
string,
{
id: number;
userString: string;
icon: string;
kind: ShellKind;
caliber: number;
damage: { armor: number; devices: number };
normalizationAngle: number;
ricochetAngle: number;
}
> & {
icons: Record<string, string>;
};
interface VehicleDefinitions {
turrets0: {
[key: string]: {
userString: number;
level: number;
yawLimits: string | string[];
guns: {
[key: string]: {
maxAmmo: number;
pitchLimits?: string | string[];
};
};
};
};
}
interface Strings {
[key: string]: string;
}
type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
interface TankParameters {
resourcesPath: {
smallIconPath: string;
bigIconPath: string;
blitzModelPath: string;
};
}
const blitzTankTypeToBlitzkrieg: Record<BlitzTankType, TankType> = {
'AT-SPG': 'tank_destroyer',
lightTank: 'light',
mediumTank: 'medium',
heavyTank: 'heavy',
};
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
ARMOR_PIERCING: 'ap',
ARMOR_PIERCING_CR: 'ap_cr',
HIGH_EXPLOSIVE: 'he',
HOLLOW_CHARGE: 'hc',
};
const DATA =
'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
const LANGUAGE = 'en';
const DOI = {
vehicleDefinitions: 'XML/item_defs/vehicles',
strings: 'Strings',
tankParameters: '3d/Tanks/Parameters',
smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons',
bigIcons: 'Gfx/UI/BigTankIcons',
flags: 'Gfx/Lobby/flags',
'3d': '3d',
bigShellIcons: 'Gfx/Shared/tank-supply/ammunition/big',
};
const allTargets = argv.includes('--all-targets');
const targets = argv
.find((argument) => argument.startsWith('--target'))
?.split('=')[1]
.split(',');
if (!targets && !allTargets) throw new Error('No target(s) specified');
if (allTargets || targets?.includes('definitions')) {
console.log('Building tank definitions...');
const tankDefinitions: BlitzkriegTankDefinitions = {};
const turretDefinitions: BlitzkriegTurretDefinitions = {};
const gunDefinitions: BlitzkriegGunDefinitions = {};
const shellDefinitions: BlitzkriegShellDefinitions = {};
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
(nations) => nations.filter((nation) => nation !== 'common'),
);
const strings = await readYAMLDVPL<Strings>(
`${DATA}/${DOI.strings}/${LANGUAGE}.yaml.dvpl`,
);
await Promise.all(
nations.map(async (nation) => {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
);
const turretList = await readXMLDVPL<{
root: TurretDefinitionsList;
}>(
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/turrets.xml.dvpl`,
);
const gunList = await readXMLDVPL<{
root: GunDefinitionsList;
}>(
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/guns.xml.dvpl`,
);
const shellList = await readXMLDVPL<{
root: ShellDefinitionsList;
}>(
`${DATA}/${DOI.vehicleDefinitions}/${nation}/components/shells.xml.dvpl`,
);
for (const tankKey in tankList.root) {
const tank = tankList.root[tankKey];
const tankDefinition = await readXMLDVPL<{ root: VehicleDefinitions }>(
`${DATA}/${DOI.vehicleDefinitions}/${nation}/${tankKey}.xml.dvpl`,
);
const tankId = toUniqueId(nation, tank.id);
const tankTags = tank.tags.split(' ');
const tankTurrets = Object.keys(tankDefinition.root.turrets0).map(
(turretKey) => {
const turret = tankDefinition.root.turrets0[turretKey];
const turretId = toUniqueId(nation, turretList.root.ids[turretKey]);
const turretGuns = Object.keys(turret.guns).map((gunKey) => {
const turretGunEntry = turret.guns[gunKey];
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
const gun = gunList.root.shared[gunKey];
const pitchLimitsRaw =
turretGunEntry.pitchLimits ?? gun.pitchLimits;
const gunShells = Object.keys(gun.shots).map((shellKey) => {
const turretShellEntry = gun.shots[shellKey];
const shell = shellList.root[shellKey];
const shellId = toUniqueId(nation, shell.id);
shellDefinitions[shellId] = {
id: shellId,
name:
strings[shell.userString] ?? shellKey.replaceAll('_', ' '),
speed: turretShellEntry.speed,
damage: {
armor: shell.damage.armor,
devices: shell.damage.devices,
},
caliber: shell.caliber,
normalization: shell.normalizationAngle,
ricochet: shell.ricochetAngle,
type: blitzShellKindToBLitzkrieg[shell.kind],
icon: shell.icon,
};
return shellId;
});
gunDefinitions[gunId] = {
id: gunId,
pitch: (typeof pitchLimitsRaw === 'string'
? pitchLimitsRaw
: pitchLimitsRaw.at(-1)!
)
.split(' ')
.map(Number) as [number, number],
name: strings[gun.userString] ?? gunKey.replaceAll('_', ' '),
tier: gun.level as Tier,
shells: gunShells,
};
return gunId;
});
turretDefinitions[turretId] = {
id: turretId,
name:
strings[turret.userString] ??
turretKey
.replaceAll('_', ' ')
.replace(/^(Turret ([0-9] )?)+/, ''),
tier: turret.level as Tier,
yaw: (typeof turret.yawLimits === 'string'
? turret.yawLimits
: turret.yawLimits.at(-1)!
)
.split(' ')
.map(Number) as [number, number],
guns: turretGuns,
};
return turretId;
},
);
tankDefinitions[tankId] = {
id: tankId,
name:
(tank.shortUserString
? strings[tank.shortUserString]
: undefined) ??
strings[tank.userString] ??
tankKey.replaceAll('_', ' '),
nation,
tree_type: (tank.sellPrice ? 'gold' in tank.sellPrice : false)
? 'collector'
: (typeof tank.price === 'number' ? false : 'gold' in tank.price)
? 'premium'
: 'researchable',
tier: tank.level as Tier,
type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType],
testing: tankTags.includes('testTank') ? true : undefined,
turrets: tankTurrets,
};
}
}),
);
console.log('Committing tank definitions...');
await commitMultipleFiles(
'tresabhi',
'blitzkrieg-assets',
'main',
'definitions',
[
{
content: JSON.stringify(tankDefinitions),
encoding: 'utf-8',
path: 'definitions/tanks.json',
},
{
content: JSON.stringify(turretDefinitions),
encoding: 'utf-8',
path: 'definitions/turrets.json',
},
{
content: JSON.stringify(gunDefinitions),
encoding: 'utf-8',
path: 'definitions/guns.json',
},
{
content: JSON.stringify(shellDefinitions),
encoding: 'utf-8',
path: 'definitions/shells.json',
},
],
true,
);
}
if (
allTargets ||
targets?.includes('bigTankIcons') ||
targets?.includes('smallTankIcons')
) {
console.log('Building tank icons...');
const changes: FileChange[] = [];
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
(nations) => nations.filter((nation) => nation !== 'common'),
);
await Promise.all(
nations.map(async (nation) => {
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
);
for (const tankIndex in tanks.root) {
const tank = tanks.root[tankIndex];
const nationVehicleId = tank.id;
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`,
);
const small = `${DATA}/${parameters.resourcesPath.smallIconPath
.replace(/~res:\//, '')
.replace(/\..+/, '')}.packed.webp.dvpl`;
const big = `${DATA}/${parameters.resourcesPath.bigIconPath
.replace(/~res:\//, '')
.replace(/\..+/, '')}.packed.webp.dvpl`;
if (allTargets || targets?.includes('bigTankIcons')) {
changes.push({
content: await readBase64DVPL(big),
encoding: 'base64',
path: `icons/big/${id}.webp`,
});
}
if (allTargets || targets?.includes('smallTankIcons')) {
changes.push({
content: await readBase64DVPL(small),
encoding: 'base64',
path: `icons/small/${id}.webp`,
});
}
}
}),
);
console.log('Committing tank icons...');
await commitMultipleFiles(
'tresabhi',
'blitzkrieg-assets',
'main',
'tank icons',
changes,
true,
);
}
if (allTargets || targets?.includes('scratchedFlags')) {
console.log('Building scratched flags...');
const flags = await readdir(`${DATA}/${DOI.flags}`);
console.log('Committing scratched flags...');
commitMultipleFiles(
'tresabhi',
'blitzkrieg-assets',
'main',
'scratched flags',
await Promise.all(
flags
.filter(
(flag) =>
flag.startsWith('flag_tutor-tank_') &&
!flag.endsWith('@2x.packed.webp.dvpl'),
)
.map(async (flag) => {
const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`);
const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
return {
content,
encoding: 'base64',
path: `flags/scratched/${name}.webp`,
} satisfies FileChange;
}),
),
true,
);
}
if (allTargets || targets?.includes('circleFlags')) {
const flags = await readdir(`${DATA}/${DOI.flags}`);
commitMultipleFiles(
'tresabhi',
'blitzkrieg-assets',
'main',
'circle flags',
await Promise.all(
flags
.filter(
(flag) =>
flag.startsWith('flag_profile-stat_') &&
!flag.endsWith('@2x.packed.webp.dvpl'),
)
.map(async (flag) => {
const content = await readBase64DVPL(`${DATA}/${DOI.flags}/${flag}`);
const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1];
return {
content,
encoding: 'base64',
path: `flags/circle/${name}.webp`,
} satisfies FileChange;
}),
),
true,
);
}
if (allTargets || targets?.includes('tankModels')) {
console.log('Building tank models...');
if (existsSync('dist/assets/models')) {
await rm('dist/assets/models', { recursive: true });
}
await mkdir('dist/assets/models', { recursive: true });
const changes: FileChange[] = [];
const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then(
(nations) => nations.filter((nation) => nation !== 'common'),
);
await Promise.all(
nations.map(async (nation) => {
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
);
for (const tankIndex in tanks.root) {
const tank = tanks.root[tankIndex];
const nationVehicleId = tank.id;
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
// if (id !== 897) continue;
console.log(`Building model ${id} @ ${nation}/${tankIndex}`);
const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/${DOI.tankParameters}/${nation}/${tankIndex}.yaml.dvpl`,
);
const sc2Path = parameters.resourcesPath.blitzModelPath;
const scgPath = parameters.resourcesPath.blitzModelPath.replace(
/\.sc2$/,
'.scg',
);
const sc2Stream = await SCPGStream.fromDVPLFile(
`${DATA}/${DOI['3d']}/${sc2Path}.dvpl`,
);
const scgStream = await SCPGStream.fromDVPLFile(
`${DATA}/${DOI['3d']}/${scgPath}.dvpl`,
);
const sc2 = sc2Stream.consumeSC2();
const scg = scgStream.consumeSCG();
}
}),
);
}
if (allTargets || targets?.includes('shellIcons')) {
console.log('Building shell icons...');
const image = sharp(
await readDVPLFile(
`${DATA}/${DOI.bigShellIcons}/texture0.packed.webp.dvpl`,
),
);
const changes = await Promise.all(
(await readdir(`${DATA}/${DOI.bigShellIcons}`))
.filter((file) => file.endsWith('_l.txt.dvpl'))
.map(async (file) => {
const name = file.match(/(.+)_l\.txt\.dvpl/)![1];
const sizes = (
await readStringDVPL(`${DATA}/${DOI.bigShellIcons}/${file}`)
)
.split('\n')[4]
.split(' ')
.map(Number);
return {
content: (
await image
.clone()
.extract({
left: sizes[0],
top: sizes[1],
width: sizes[2],
height: sizes[3],
})
.toBuffer()
).toString('base64'),
encoding: 'base64',
path: `shells/${name}.webp`,
} satisfies FileChange;
}),
);
console.log('Committing shell icons...');
commitMultipleFiles(
'tresabhi',
'blitzkrieg-assets',
'main',
'shell icons',
changes,
true,
);
}
+28 -30
View File
@@ -6,32 +6,35 @@ import {
RatingsNeighbors,
RatingsPlayer,
} from '../src/commands/ratings';
import commitMultipleFiles from './commitMultipleFiles.js';
import commitMultipleFiles from '../src/core/blitzkrieg/commitMultipleFiles.js';
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
/*
* Central North American Time (UTC-5): use 0 5 * * *.
* Central European Time (UTC+1): use 0 23 * * *.
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
*
* na/ratings/49/info.json: season info like rewards, start & end time, etc.
* na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
* na/ratings/49/midnight.json: last midnight's leaderboard relative to the region
* [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
* [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
* [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
*/
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
const publish = argv.includes('--publish');
const latest = argv.includes('--latest');
const target = argv
.find((arg) => arg.startsWith('--target='))
?.split('=')[1] as 'midnight' | 'latest' | undefined;
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
if (!target) throw new Error('Target parameter not specified');
if (!server) throw new Error('Region parameter not specified');
const NEIGHBORS = 1000;
const PLAYERS = await fetch(
const NEIGHBORS = 2 ** 8;
const PLAYERS = await patientFetchJSON<RatingsInfo>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
)
.then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => (data.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,31 +116,23 @@ const leaderboardJSON = JSON.stringify(leaderboard);
if (publish) {
const octokit = new Octokit({ auth: env.GH_TOKEN });
const info = await fetch(
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then(
(response) =>
response.json() as Promise<RatingsInfo & { 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(),
[
{ path: infoPath, content: infoJSON },
{ path: leaderboardPath, content: leaderboardJSON },
{ path: infoPath, content: infoJSON, encoding: 'utf-8' },
{ path: leaderboardPath, content: leaderboardJSON, encoding: 'utf-8' },
],
);
} else {
+46
View File
@@ -0,0 +1,46 @@
import { writeFile } from 'fs/promises';
import { Region } from '../src/constants/regions';
import { getRatingsNeighbors } from '../src/core/blitz/getRatingsNeighbors';
const REGION: Region = 'com';
const PLAYER = 1041988373;
const ITERATIONS = 2 ** 4;
const MAX_NEIGHBORS = 2 ** 7;
const data: [number, number][] = [];
let finished = 0;
for (let index = 0; index < ITERATIONS; index++) {
const neighbors = Math.round((Math.random() * MAX_NEIGHBORS) / 2) * 2;
const start = Date.now();
console.log(`🟡 Iteration ${index} with ${neighbors} neighbors`);
try {
await getRatingsNeighbors(REGION, PLAYER, neighbors);
finished++;
console.log(
`🟢 (${Math.round(
100 * (finished / ITERATIONS),
)}%) Iteration ${index} with ${neighbors} neighbors`,
);
const end = Date.now();
const time = end - start;
data.push([neighbors, time]);
} catch (error) {
console.log(
`🔴 Iteration ${index} with ${neighbors} neighbors\n\n${error}`,
);
}
}
console.log('🟢 Done, sorting');
data.sort(([neighborsA], [neighborsB]) => neighborsA - neighborsB);
console.log('🟢 Writing');
writeFile(
'dist/ratingsPlot.csv',
data.map(([neighbors, time]) => `${neighbors},${time}`).join('\n'),
);
+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`);
}
-70
View File
@@ -1,70 +0,0 @@
import { Octokit } from '@octokit/rest';
export default async function commitMultipleFiles(
octokit: Octokit,
owner: string,
repo: string,
branch: string,
message: string,
changes: { path: string; content: string }[],
) {
// Get the latest commit SHA on the specified branch
const { data: refData } = await octokit.git.getRef({
owner,
repo,
ref: `heads/${branch}`,
});
const latestCommitSha = refData.object.sha;
// Get the tree SHA for the latest commit
const { data: commitData } = await octokit.git.getCommit({
owner,
repo,
commit_sha: latestCommitSha,
});
const treeSha = commitData.tree.sha;
// Create blobs for each file
const blobs = await Promise.all(
changes.map(async (change) => {
const { data: blobData } = await octokit.git.createBlob({
owner,
repo,
content: change.content,
encoding: 'utf-8',
});
return {
sha: blobData.sha,
path: change.path,
mode: '100644',
type: 'blob',
} as const;
}),
);
// Create a new tree with the new blobs
const { data: treeData } = await octokit.git.createTree({
owner,
repo,
base_tree: treeSha,
tree: blobs,
});
// Create a new commit with the new tree
const { data: newCommitData } = await octokit.git.createCommit({
owner,
repo,
message,
tree: treeData.sha,
parents: [latestCommitSha],
});
// Update the branch reference to point to the new commit
await octokit.git.updateRef({
owner,
repo,
ref: `heads/${branch}`,
sha: newCommitData.sha,
});
}
@@ -0,0 +1,10 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../constants/regions';
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
export async function GET(
request: Request,
{ params }: { params: { region: Region } },
) {
return NextResponse.json(await getRatingsInfo(params.region));
}
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions';
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
export async function GET(
request: Request,
{ params }: { params: { region: Region; league: number } },
) {
return NextResponse.json(
await getRatingsLeague(params.region, params.league, true),
);
}
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions';
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
export async function GET(
request: Request,
{ params }: { params: { region: Region; username: string } },
) {
return NextResponse.json(
await searchCurrentRatingsPlayers(params.region, params.username),
);
}
-6
View File
@@ -1,6 +0,0 @@
import { NextResponse } from 'next/server';
import { tankopedia } from '../../../core/blitz/tankopedia';
export async function GET() {
return NextResponse.json(await tankopedia);
}
+138 -118
View File
@@ -1,18 +1,23 @@
'use client';
import { ContextMenu } from '@radix-ui/themes';
import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import { TreeTypeEnum } from '../../../components/Tanks';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import 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 {
BlitzkriegTankDefinition,
tankDefinitions,
} from '../../../core/blitzkrieg/definitions/tanks';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import { useSession } from '../../../stores/session';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import { resetSession, useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
const REFRESH_RATE = 1 / 5;
@@ -25,7 +30,7 @@ export default function SessionPage() {
list: {
stats: IndividualTankStats;
tankopedia?: TankopediaEntry;
tankDefinitions?: BlitzkriegTankDefinition;
career: IndividualTankStats;
currentWN8?: number;
careerWN8?: number;
@@ -71,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<
@@ -83,7 +88,7 @@ export default function SessionPage() {
}),
{},
);
const awaitedTankopedia = await tankopedia;
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await tankAverages;
const careerWN8 =
careerRaw[id].reduce(
@@ -109,12 +114,12 @@ export default function SessionPage() {
list: (
Object.values(
diffNormalizedTankStats(session.tankStats, career),
deltaTankStats(session.tankStats, career),
) as IndividualTankStats[]
).map((stats) => {
return {
stats,
tankopedia: awaitedTankopedia[stats.tank_id],
tankDefinitions: awaitedTankDefinitions[stats.tank_id],
career: career[stats.tank_id],
currentWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
@@ -193,118 +198,133 @@ export default function SessionPage() {
};
return (
<Breakdown.Root>
{session.showTotal &&
session.isTracking &&
since !== undefined &&
diff !== undefined && (
<Breakdown.Row
minimized={!session.showCareer}
title={`Since ${since.toLocaleString(undefined, {
hour: 'numeric',
minute: 'numeric',
hour12: true,
month: 'short',
year: 'numeric',
day: 'numeric',
})}`}
stats={session.customColumns.map((customColumn) => {
const rowStat = totalRowStats[customColumn.display];
<ContextMenu.Root>
<ContextMenu.Trigger>
<div>
<Breakdown.Root>
{session.showTotal &&
session.isTracking &&
since !== undefined &&
diff !== undefined && (
<Breakdown.Row
color={session.color}
minimized={!session.showCareer}
title={session.title}
stats={session.customColumns.map((customColumn) => {
const rowStat = totalRowStats[customColumn.display];
if (!rowStat) return undefined;
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta && rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
)}
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
)}
{diff?.list
.sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time)
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
const rowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: stats.all.battles.toLocaleString(),
career: career.all.battles.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: `${(100 * (stats.all.wins / stats.all.battles)).toFixed(
2,
)}%`,
career: `${(100 * (career.all.wins / career.all.battles)).toFixed(
2,
)}%`,
delta:
stats.all.wins / stats.all.battles -
career.all.wins / career.all.battles,
},
wn8: {
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
damage: {
title: 'Damage',
current: Math.round(
stats.all.damage_dealt / stats.all.battles,
).toLocaleString(),
career: Math.round(
career.all.damage_dealt / career.all.battles,
).toLocaleString(),
delta:
stats.all.damage_dealt / stats.all.battles -
career.all.damage_dealt / career.all.battles,
},
none: undefined,
};
{diff?.list
.sort(
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
)
.map(
({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => {
const rowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: stats.all.battles.toLocaleString(),
career: career.all.battles.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: `${(
100 *
(stats.all.wins / stats.all.battles)
).toFixed(2)}%`,
career: `${(
100 *
(career.all.wins / career.all.battles)
).toFixed(2)}%`,
delta:
stats.all.wins / stats.all.battles -
career.all.wins / career.all.battles,
},
wn8: {
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
damage: {
title: 'Damage',
current: Math.round(
stats.all.damage_dealt / stats.all.battles,
).toLocaleString(),
career: Math.round(
career.all.damage_dealt / career.all.battles,
).toLocaleString(),
delta:
stats.all.damage_dealt / stats.all.battles -
career.all.damage_dealt / career.all.battles,
},
none: undefined,
};
return (
<Breakdown.Row
key={stats.tank_id}
minimized={!session.showCareer}
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
type="tank"
tankType={tankopedia?.type}
treeType={(() => {
if (tankopedia?.is_collectible) return TreeTypeEnum.Collector;
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
})()}
stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display];
return (
<Breakdown.Row
color={session.color}
key={stats.tank_id}
minimized={!session.showCareer}
title={
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
}
type="tank"
tankType={tankDefinitions?.type}
treeType={(() => {
if (tankDefinitions?.tree_type === 'collector')
return TreeTypeEnum.Collector;
if (tankDefinitions?.tree_type === 'premium')
return TreeTypeEnum.Premium;
})()}
stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display];
if (!rowStat) return undefined;
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta && rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
);
})}
</Breakdown.Root>
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
);
},
)}
</Breakdown.Root>
</div>
</ContextMenu.Trigger>
<Menu reset={resetSession} Builder={ContextMenu} />
</ContextMenu.Root>
);
}
+3 -1
View File
@@ -36,6 +36,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
<body
style={{
margin: 0,
paddingTop: isEmbed ? 0 : '3.25rem',
background: isEmbed
? 'transparent'
: 'url(https://i.imgur.com/PhS06NJ.png)',
@@ -45,7 +46,8 @@ export default function RootLayout({ children }: RootLayoutProps) {
appearance="dark"
panelBackground="translucent"
radius="full"
accentColor="blue"
suppressHydrationWarning
suppressContentEditableWarning
>
{!isEmbed && <Navbar />}
+19 -4
View File
@@ -11,7 +11,7 @@ interface ToolsLayoutProps {
export default function ToolsLayout({ children }: ToolsLayoutProps) {
const pathname = usePathname();
const toolId = pathname.split('/').at(-1);
const toolId = pathname.split('/').at(2);
const tool = TOOLS.find((tool) => tool.id === toolId);
return (
@@ -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,97 @@
import {
CaretLeftIcon,
CaretRightIcon,
SymbolIcon,
} from '@radix-ui/react-icons';
import { Button, Flex, TextField } from '@radix-ui/themes';
import { range } from 'lodash';
import { useEffect, useRef } from 'react';
import { infiniteSpin } from './index.css';
interface PageTurnerProps {
leaderboard: Record<number, number>;
page: number;
rowsPerPage: number;
pages: number;
onPageChange: (page: number, isButtonClick: boolean) => void;
totalPlayers: number;
}
export function PageTurner({
page,
rowsPerPage,
onPageChange,
pages,
leaderboard,
totalPlayers,
}: PageTurnerProps) {
const pageInput = useRef<HTMLInputElement>(null);
const rightLoading = range(
page * rowsPerPage,
Math.min(totalPlayers - 1, page * rowsPerPage + rowsPerPage + 1),
).some((index) => !(index in leaderboard));
const leftLoading = range(
Math.max(0, page * rowsPerPage - 1),
Math.max(0, page * rowsPerPage - rowsPerPage - 1) + 1,
).some((index) => !(index in leaderboard));
useEffect(() => {
if (pageInput.current) pageInput.current.value = `${page + 1}`;
}, [page]);
return (
<Flex justify="center" wrap="wrap" gap="2">
<Flex gap="2">
<Button
variant="soft"
onClick={() => onPageChange(Math.max(page - 1, 0), true)}
disabled={page === 0 || leftLoading}
>
{leftLoading ? (
<SymbolIcon className={infiniteSpin} />
) : (
<CaretLeftIcon />
)}
</Button>
<TextField.Root>
<TextField.Slot>Page</TextField.Slot>
<TextField.Input
type="number"
ref={pageInput}
style={{ width: 64, textAlign: 'center' }}
min={1}
max={pages}
onBlur={(event) => {
onPageChange(
Math.max(
0,
Math.min(pages - 1, event.target.valueAsNumber - 1),
),
false,
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
(event.target as HTMLInputElement).blur();
}
}}
/>
<TextField.Slot>out of {pages}</TextField.Slot>
</TextField.Root>
<Button
variant="soft"
onClick={() => {
onPageChange(Math.min(page + 1, pages - 1), true);
}}
disabled={page === pages - 1 || rightLoading}
>
{rightLoading ? (
<SymbolIcon className={infiniteSpin} />
) : (
<CaretRightIcon />
)}
</Button>
</Flex>
</Flex>
);
}
File diff suppressed because it is too large. Load diff
@@ -1,4 +1,4 @@
import { DropdownMenu } from '@radix-ui/themes';
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
import mutateSession, { useSession } from '../../../../stores/session';
export type CustomColumnDisplay =
@@ -11,20 +11,19 @@ export type CustomColumnDisplay =
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
interface CustomColumn {
Builder: typeof DropdownMenu | typeof ContextMenu;
column: number;
}
export function CustomColumn({ column }: CustomColumn) {
export function CustomColumn({ column, Builder }: CustomColumn) {
const customColumn = useSession((state) => state.customColumns[column]);
return (
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Column {column + 1}</DropdownMenu.SubTrigger>
<Builder.Sub>
<Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Label>Display</DropdownMenu.Label>
<DropdownMenu.RadioGroup
<Builder.SubContent>
<Builder.RadioGroup
value={customColumn.display}
onValueChange={(event) =>
mutateSession((draft) => {
@@ -33,22 +32,18 @@ export function CustomColumn({ column }: CustomColumn) {
})
}
>
<DropdownMenu.RadioItem value="battles">
Battles
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="winrate">
Winrate
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="wn8">WN8</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="damage">Damage</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="none">None</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<Builder.RadioItem value="battles">Battles</Builder.RadioItem>
<Builder.RadioItem value="winrate">Winrate</Builder.RadioItem>
<Builder.RadioItem value="wn8">WN8</Builder.RadioItem>
<Builder.RadioItem value="damage">Damage</Builder.RadioItem>
<Builder.RadioItem value="none">None</Builder.RadioItem>
</Builder.RadioGroup>
{!NO_DELTA.includes(customColumn.display) && (
<>
<DropdownMenu.Separator />
<Builder.Separator />
<DropdownMenu.CheckboxItem
<Builder.CheckboxItem
checked={customColumn.showDelta}
onCheckedChange={(checked) =>
mutateSession((draft) => {
@@ -57,10 +52,10 @@ export function CustomColumn({ column }: CustomColumn) {
}
>
Show delta
</DropdownMenu.CheckboxItem>
</Builder.CheckboxItem>
</>
)}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</Builder.SubContent>
</Builder.Sub>
);
}
+136
View File
@@ -0,0 +1,136 @@
import {
Button,
ContextMenu,
Dialog,
DropdownMenu,
Flex,
Heading,
TextField,
} from '@radix-ui/themes';
import { createColors } from 'bepaint';
import { useRef, useState } from 'react';
import {
ACCENT_COLORS,
AccentColor,
GRAY_COLORS,
GrayColor,
PALETTES,
} from '../../../../constants/radixColors';
import { useSession } from '../../../../stores/session';
import { CustomColumn } from './CustomColumn';
interface ContextMenuProps {
Builder: typeof DropdownMenu | typeof ContextMenu;
reset: () => void;
}
export function Menu({ Builder, reset }: ContextMenuProps) {
const session = useSession();
const [showTitleEditor, setShowTitleEditor] = useState(false);
const titleInput = useRef<HTMLInputElement>(null);
return (
<>
<Dialog.Root open={showTitleEditor} onOpenChange={setShowTitleEditor}>
<Dialog.Content>
<Flex direction="column" gap="4">
<Heading size="4">Edit title</Heading>
<Flex gap="2" style={{ width: '100%' }}>
<TextField.Root style={{ flex: 1 }}>
<TextField.Input
ref={titleInput}
defaultValue={session.title}
placeholder="Enter title..."
onKeyDown={(event) => {
if (event.key === 'Enter' && titleInput.current) {
setShowTitleEditor(false);
useSession.setState({ title: titleInput.current!.value });
}
}}
/>
</TextField.Root>
<Dialog.Close>
<Button
onClick={() =>
useSession.setState({ title: titleInput.current!.value })
}
>
Set
</Button>
</Dialog.Close>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
<Builder.Content>
<Builder.Sub>
<Builder.SubTrigger>Accent color</Builder.SubTrigger>
<Builder.SubContent>
<Builder.RadioGroup
value={session.color}
onValueChange={(value) =>
useSession.setState({ color: value as AccentColor | GrayColor })
}
>
{[...ACCENT_COLORS, ...GRAY_COLORS].map((color) => (
<Builder.RadioItem value={color} key={color}>
<Flex gap="2" align="center">
<div
style={{
backgroundColor: createColors(PALETTES[color])
.solidBackground,
width: 16,
height: 16,
borderRadius: '100%',
}}
/>
{`${color.charAt(0).toUpperCase()}${color.slice(1)}`}
</Flex>
</Builder.RadioItem>
))}
</Builder.RadioGroup>
</Builder.SubContent>
</Builder.Sub>
<Builder.Separator />
<Builder.Item onClick={() => setShowTitleEditor(true)}>
Edit title...
</Builder.Item>
<CustomColumn Builder={Builder} column={0} />
<CustomColumn Builder={Builder} column={1} />
<CustomColumn Builder={Builder} column={2} />
<CustomColumn Builder={Builder} column={3} />
<Builder.Separator />
<Builder.CheckboxItem
checked={session.showTotal}
onCheckedChange={(checked) =>
useSession.setState({ showTotal: checked })
}
>
Show total
</Builder.CheckboxItem>
<Builder.CheckboxItem
checked={session.showCareer}
onCheckedChange={(checked) =>
useSession.setState({ showCareer: checked })
}
>
Show career
</Builder.CheckboxItem>
<Builder.Separator />
<Builder.Item color="red" onClick={reset}>
Reset
</Builder.Item>
</Builder.Content>
</>
);
}
+73 -85
View File
@@ -14,21 +14,17 @@ 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 { CustomColumn } from './components/CustomColumn';
import { Menu } from './components/Menu';
import * as styles from './page.css';
export default function Page() {
@@ -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,31 +243,7 @@ export default function Page() {
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<CustomColumn column={0} />
<CustomColumn column={1} />
<CustomColumn column={2} />
<CustomColumn column={3} />
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem
checked={session.showTotal}
onCheckedChange={(checked) =>
useSession.setState({ showTotal: checked })
}
>
Show total
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={session.showCareer}
onCheckedChange={(checked) =>
useSession.setState({ showCareer: checked })
}
>
Show career
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
<Menu Builder={DropdownMenu} reset={resetSession} />
</DropdownMenu.Root>
</div>
</div>
+250
View File
@@ -0,0 +1,250 @@
'use client';
import { ArrowLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
import {
Button,
Card,
Checkbox,
Dialog,
Flex,
Heading,
Slider,
Tabs,
Text,
Theme,
Tooltip,
} from '@radix-ui/themes';
import { OrbitControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import Link from 'next/link';
import { use, useEffect, useState } from 'react';
import { Flag } from '../../../../components/Flag';
import PageWrapper from '../../../../components/PageWrapper';
import { asset } from '../../../../core/blitzkrieg/asset';
import { gunDefinitions } from '../../../../core/blitzkrieg/definitions/guns';
import { shellDefinitions } from '../../../../core/blitzkrieg/definitions/shells';
import {
TIER_ROMAN_NUMERALS,
tankDefinitions,
tanksDefinitionsArray,
} from '../../../../core/blitzkrieg/definitions/tanks';
import { turretDefinitions } from '../../../../core/blitzkrieg/definitions/turrets';
import * as styles from '../page.css';
type Mode = 'model' | 'armor';
export default function Page({ params }: { params: { id: string } }) {
const id = parseInt(params.id);
const awaitedTankDefinitions = use(tankDefinitions);
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
const awaitedTurretDefinitions = use(turretDefinitions);
const awaitedGunDefinitions = use(gunDefinitions);
const awaitedShellDefinitions = use(shellDefinitions);
const tank = awaitedTankDefinitions[id];
const [turret, setTurret] = useState(
awaitedTurretDefinitions[tank.turrets[0]],
);
const [gun, setGun] = useState(awaitedGunDefinitions[turret.guns[0]]);
const [crew, setCrew] = useState(100);
const [versusTank, setVersusTank] = useState(
awaitedTanksDefinitionsArray[0].id,
);
const [mode, setMode] = useState<Mode>('model');
console.log(versusTank);
useEffect(() => setGun(awaitedGunDefinitions[turret.guns[0]]), [turret]);
return (
<PageWrapper>
<Flex gap="8" direction="column">
<Flex gap="4" direction="column">
<Flex justify="between" align="center">
<Link
href="/tools/tankopedia"
style={{ color: 'unset', textDecoration: 'none' }}
>
<Flex gap="1" align="center">
<ArrowLeftIcon />
<Text>Back to tankopedia</Text>
</Flex>
</Link>
<Flex gap="2" align="center">
<Flag nation={tank.nation} />
<Heading>{tank.name}</Heading>
</Flex>
</Flex>
<Card style={{ position: 'relative' }}>
<Flex direction="column" gap="2">
<Tabs.Root
value={mode}
onValueChange={(value) => setMode(value as Mode)}
>
<Tabs.List>
<Tabs.Trigger value="model">Model</Tabs.Trigger>
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<div style={{ height: '50vh', maxHeight: 576 }}>
<Canvas onPointerDown={(event) => event.preventDefault()}>
<OrbitControls />
<ambientLight />
<directionalLight />
<mesh>
<torusKnotGeometry args={[undefined, undefined, 128, 16]} />
<meshStandardMaterial color="cyan" />
</mesh>
</Canvas>
</div>
{mode === 'armor' && (
<Flex gap="2" align="center" className={styles.enhancedArmor}>
<Checkbox defaultChecked />
<Text>Enhanced armor</Text>
</Flex>
)}
</Flex>
</Card>
{mode === 'armor' && (
<Card>
<Flex align="center" justify="between" gap="2">
<Flex align="center" gap="4">
<Text>Versus</Text>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="ghost">
{awaitedTankDefinitions[versusTank].name}
<CaretRightIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content>Do something lol</Dialog.Content>
</Dialog.Root>
</Flex>
<Flex gap="2" align="center">
<Checkbox defaultChecked />
<Text>Calibrated shells</Text>
</Flex>
<Flex gap="1">
<Button variant="solid" radius="small">
<img src={asset('shells/ap.webp')} width={24} height={24} />
</Button>
<Button variant="soft" radius="small" color="gray">
<img
src={asset('shells/hc_premium.webp')}
width={24}
height={24}
/>
</Button>
<Button variant="soft" radius="small" color="gray">
<img src={asset('shells/he.webp')} width={24} height={24} />
</Button>
</Flex>
</Flex>
</Card>
)}
</Flex>
<Flex gap="6" direction="column">
<Heading>Build</Heading>
<Flex gap="2" direction="column">
<Heading size="5" weight="regular">
Modules
</Heading>
<Theme radius="small">
<Flex gap="4">
<Flex gap="1">
{tank.turrets.map((turretId) => {
const thisTurret = awaitedTurretDefinitions[turretId];
return (
<Tooltip content={thisTurret.name}>
<Button
onClick={() => setTurret(thisTurret)}
variant={
turret.id === thisTurret.id ? 'solid' : 'soft'
}
>
<img
src="/images/modules/turret.png"
width={32}
height={32}
/>
{TIER_ROMAN_NUMERALS[thisTurret.tier]}
</Button>
</Tooltip>
);
})}
</Flex>
<Flex gap="1">
{turret.guns.map((gunId, index) => {
const thisGun = awaitedGunDefinitions[gunId];
return (
<Tooltip content={thisGun.name}>
<Button
onClick={() => setGun(thisGun)}
variant={gun.id === thisGun.id ? 'solid' : 'soft'}
>
<img
src="/images/modules/gun.png"
width={32}
height={32}
/>
{TIER_ROMAN_NUMERALS[thisGun.tier]}
</Button>
</Tooltip>
);
})}
</Flex>
</Flex>
</Theme>
</Flex>
<Flex gap="2" direction="column">
<Heading size="5" weight="regular">
Crew
</Heading>
<Flex gap="4" align="center">
<Text>{crew}%</Text>
<Slider
style={{ flex: 1 }}
value={[crew]}
min={50}
max={100}
onValueChange={([crew]) => setCrew(crew)}
/>
</Flex>
</Flex>
</Flex>
<Flex gap="2" direction="column">
<Heading size="5">Shells</Heading>
{gun.shells.map((shellId) => {
const shell = awaitedShellDefinitions[shellId];
return (
<Text>
{shell.name} ({shell.type}): {shell.damage.armor}
</Text>
);
})}
</Flex>
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,17 @@
import { style } from '@vanilla-extract/css';
export const splitFilters = style({
'@media': {
'screen and (min-width: 513px)': {
display: 'none',
},
},
});
export const oneLineFilters = style({
'@media': {
'screen and (max-width: 512px)': {
display: 'none',
},
},
});
@@ -0,0 +1,388 @@
'use client';
import { CaretDownIcon } from '@radix-ui/react-icons';
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
import { use } from 'react';
import { Flag } from '../../../../../components/Flag';
import {
TANK_TYPES,
TREE_TYPES,
TREE_TYPE_ICONS,
TREE_TYPE_IMAGES,
TreeTypeEnum,
} from '../../../../../components/Tanks';
import {
NATIONS,
TIERS,
TIER_ROMAN_NUMERALS,
} from '../../../../../core/blitzkrieg/definitions/tanks';
import mutateTankopedia, {
TankopediaTestTankDisplay,
useTankopedia,
} from '../../../../../stores/tankopedia';
import * as styles from './index.css';
export function Options() {
const nations = use(NATIONS);
const tankopediaState = useTankopedia();
return (
<Flex justify="center" direction="column" gap="2">
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
<Button
color="red"
onClick={() =>
useTankopedia.setState({
filters: {
nations: [],
tiers: [],
treeTypes: [],
types: [],
test: 'include',
},
})
}
>
Clear
</Button>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="soft">
Test tanks
<CaretDownIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup
value={tankopediaState.filters.test}
onValueChange={(value) =>
mutateTankopedia((draft) => {
draft.filters.test = value as TankopediaTestTankDisplay;
})
}
>
<DropdownMenu.RadioItem value="include">
Include
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="exclude">
Exclude
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Flex style={{ flex: 1 }}>
{TREE_TYPES.map((type, index) => (
<Button
key={type}
variant={
tankopediaState.filters.treeTypes.includes(type)
? 'solid'
: 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius:
index === TREE_TYPES.length - 1 ? undefined : 0,
borderBottomRightRadius:
index === TREE_TYPES.length - 1 ? undefined : 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.treeTypes.includes(type)) {
draft.filters.treeTypes = draft.filters.treeTypes.filter(
(preexistingType) => preexistingType !== type,
);
} else {
draft.filters.treeTypes.push(type);
}
})
}
>
<img
src={TREE_TYPE_IMAGES[type]}
style={{ width: '1em', height: '1em' }}
/>
</Button>
))}
</Flex>
<Flex style={{ flex: 1 }}>
{[...TANK_TYPES].reverse().map((type, index) => (
<Button
key={type}
variant={
tankopediaState.filters.types.includes(type) ? 'solid' : 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius:
index === TANK_TYPES.length - 1 ? undefined : 0,
borderBottomRightRadius:
index === TANK_TYPES.length - 1 ? undefined : 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.types.includes(type)) {
draft.filters.types = draft.filters.types.filter(
(preexistingType) => preexistingType !== type,
);
} else {
draft.filters.types.push(type);
}
})
}
>
<img
src={TREE_TYPE_ICONS[TreeTypeEnum.TechTree][type]}
style={{ width: '1em', height: '1em' }}
/>
</Button>
))}
</Flex>
<Flex
direction="column"
className={styles.splitFilters}
style={{ width: '100%' }}
>
<Flex>
{[...TIERS]
.reverse()
.slice(0, 5)
.map((tier, index) => (
<Button
key={tier}
variant={
tankopediaState.filters.tiers.includes(tier)
? 'solid'
: 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: 0,
borderTopRightRadius:
index === TIERS.length - Math.round(TIERS.length / 2) - 1
? undefined
: 0,
borderBottomRightRadius: 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.tiers.includes(tier)) {
draft.filters.tiers = draft.filters.tiers.filter(
(preexistingTier) => preexistingTier !== tier,
);
} else {
draft.filters.tiers.push(tier);
}
})
}
>
{TIER_ROMAN_NUMERALS[tier]}
</Button>
))}
</Flex>
<Flex>
{[...TIERS]
.reverse()
.slice(5, 10)
.map((tier, index) => (
<Button
key={tier}
variant={
tankopediaState.filters.tiers.includes(tier)
? 'solid'
: 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius: 0,
borderBottomRightRadius:
index === TIERS.length - Math.round(TIERS.length / 2) - 1
? undefined
: 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.tiers.includes(tier)) {
draft.filters.tiers = draft.filters.tiers.filter(
(preexistingTier) => preexistingTier !== tier,
);
} else {
draft.filters.tiers.push(tier);
}
})
}
>
{TIER_ROMAN_NUMERALS[tier]}
</Button>
))}
</Flex>
</Flex>
<Flex
direction="column"
className={styles.splitFilters}
style={{ width: '100%' }}
>
<Flex>
{nations.slice(0, 5).map((nation, index) => (
<Button
key={nation}
variant={
tankopediaState.filters.nations.includes(nation)
? 'solid'
: 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: 0,
borderTopRightRadius:
index === nations.length - Math.round(nations.length / 2)
? undefined
: 0,
borderBottomRightRadius:
index === nations.length - 1 ? undefined : 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.nations.includes(nation)) {
draft.filters.nations = draft.filters.nations.filter(
(preexistingType) => preexistingType !== nation,
);
} else {
draft.filters.nations.push(nation);
}
})
}
>
<Flag nation={nation} />
</Button>
))}
</Flex>
<Flex>
{nations.slice(5, 10).map((nation, index) => (
<Button
key={nation}
variant={
tankopediaState.filters.nations.includes(nation)
? 'solid'
: 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius:
index === nations.length - 1 ? undefined : 0,
borderBottomRightRadius:
index ===
nations.length - Math.round(nations.length / 2) - 1
? undefined
: 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.nations.includes(nation)) {
draft.filters.nations = draft.filters.nations.filter(
(preexistingType) => preexistingType !== nation,
);
} else {
draft.filters.nations.push(nation);
}
})
}
>
<Flag nation={nation} />
</Button>
))}
</Flex>
</Flex>
</Flex>
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
{[...TIERS].reverse().map((tier, index) => (
<Button
key={tier}
variant={
tankopediaState.filters.tiers.includes(tier) ? 'solid' : 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius:
index === TIERS.length - 1 ? undefined : 0,
borderBottomRightRadius:
index === TIERS.length - 1 ? undefined : 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.tiers.includes(tier)) {
draft.filters.tiers = draft.filters.tiers.filter(
(preexistingTier) => preexistingTier !== tier,
);
} else {
draft.filters.tiers.push(tier);
}
})
}
>
{TIER_ROMAN_NUMERALS[tier]}
</Button>
))}
</Flex>
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
{nations.map((nation, index) => (
<Button
key={nation}
variant={
tankopediaState.filters.nations.includes(nation)
? 'solid'
: 'soft'
}
style={{
flex: 1,
margin: -0.5,
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius:
index === nations.length - 1 ? undefined : 0,
borderBottomRightRadius:
index === nations.length - 1 ? undefined : 0,
}}
onClick={() =>
mutateTankopedia((draft) => {
if (draft.filters.nations.includes(nation)) {
draft.filters.nations = draft.filters.nations.filter(
(preexistingType) => preexistingType !== nation,
);
} else {
draft.filters.nations.push(nation);
}
})
}
>
<Flag nation={nation} />
</Button>
))}
</Flex>
</Flex>
</Flex>
);
}
@@ -0,0 +1,77 @@
import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
import { Button, Flex, TextField } from '@radix-ui/themes';
import { useEffect, useRef } from 'react';
import { BlitzkriegTankDefinition } from '../../../../core/blitzkrieg/definitions/tanks';
interface PageTurnerProps {
page: number;
setPage: (page: number) => void;
searchedList: BlitzkriegTankDefinition[];
tanksPerPage: number;
}
export function PageTurner({
page,
setPage,
searchedList,
tanksPerPage,
}: PageTurnerProps) {
const pageInput = useRef<HTMLInputElement>(null);
useEffect(() => {
if (pageInput.current) pageInput.current.value = `${page + 1}`;
}, [page]);
return (
<Flex align="center" justify="center" gap="2">
<Button
variant="soft"
disabled={page === 0}
onClick={() => setPage(Math.max(0, page - 1))}
>
<CaretLeftIcon />
</Button>
<TextField.Root>
<TextField.Slot>Page</TextField.Slot>
<TextField.Input
defaultValue={1}
type="number"
ref={pageInput}
min={1}
max={Math.floor(searchedList.length / tanksPerPage) + 1}
style={{ width: 64, textAlign: 'center' }}
onBlur={(event) => {
setPage(
Math.max(
0,
Math.min(
Math.floor(searchedList.length / tanksPerPage),
event.target.valueAsNumber - 1,
),
),
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
(event.target as HTMLInputElement).blur();
}
}}
/>
<TextField.Slot>
out of {Math.floor(searchedList.length / tanksPerPage) + 1}
</TextField.Slot>
</TextField.Root>
<Button
variant="soft"
disabled={Math.floor(searchedList.length / tanksPerPage) === page}
onClick={() =>
setPage(
Math.min(Math.floor(searchedList.length / tanksPerPage), page + 1),
)
}
>
<CaretRightIcon />
</Button>
</Flex>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { style } from '@vanilla-extract/css';
import { theme } from '../../../stitches.config';
export const listing = style({});
export const flag = style({
transitionDuration: theme.durations.regular,
left: '0',
top: '0',
[`.${listing}:hover &, .${listing}:active &`]: {
transform: 'translateX(-25%)',
filter: 'blur(3rem)',
},
});
export const listingShadow = style({
boxShadow: `inset 128px -128px 128px -128px ${theme.colors.appBackground1}`,
[`.${listing}:hover &, .${listing}:active &`]: {
boxShadow: `inset 0 0 0 -128px ${theme.colors.appBackground1}`,
},
});
export const listingLabel = style({
transition: `transform ${theme.durations.regular}`,
transform: 'translateY(0)',
[`.${listing}:hover &, .${listing}:active &`]: {
transform: 'translateY(200%)',
},
});
export const listingImage = style({
transition: `transform ${theme.durations.regular}`,
position: 'absolute',
left: '50%',
[`.${listing}:hover &, .${listing}:active &`]: {
transform: 'translate(-25%, 10%)',
},
});
export const enhancedArmor = style({
'@media': {
'screen and (min-width: 400px)': {
position: 'absolute',
right: 16,
top: 16,
},
'screen and (max-width: 399px)': {
// left: 16,
// bottom: 16,
},
},
});
+329
View File
@@ -0,0 +1,329 @@
'use client';
import { slateDark } from '@radix-ui/colors';
import {
CaretDownIcon,
CaretUpIcon,
MagnifyingGlassIcon,
} from '@radix-ui/react-icons';
import {
Button,
Card,
DropdownMenu,
Flex,
Inset,
Text,
TextField,
} from '@radix-ui/themes';
import { go } from 'fuzzysort';
import { Suspense, use, useEffect, useMemo, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { asset } from '../../../core/blitzkrieg/asset';
import {
BlitzkriegTankDefinition,
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
TIER_ROMAN_NUMERALS,
tanksDefinitionsArray,
} from '../../../core/blitzkrieg/definitions/tanks';
import { tankIcon } from '../../../core/blitzkrieg/tankIcon';
import { theme } from '../../../stitches.config';
import mutateTankopedia, {
TankopediaSortBy,
TankopediaSortDirection,
useTankopedia,
} from '../../../stores/tankopedia';
import { Options } from './components/Options';
import { PageTurner } from './components/PageTurner';
import * as styles from './page.css';
const TANKS_PER_PAGE = 3 * 2 ** 3;
export default function Page() {
const tankopediaState = useTankopedia();
const awaitedTanks = use(tanksDefinitionsArray);
const input = useRef<HTMLInputElement>(null);
const searchableTanks = useMemo(
() =>
awaitedTanks
.filter(
(tank) =>
(tankopediaState.filters.tiers.length === 0
? true
: tankopediaState.filters.tiers.includes(tank.tier)) &&
(tankopediaState.filters.types.length === 0
? true
: tankopediaState.filters.types.includes(tank.type)) &&
(tankopediaState.filters.treeTypes.length === 0
? true
: tankopediaState.filters.treeTypes.includes(tank.tree_type)) &&
(tankopediaState.filters.nations.length === 0
? true
: tankopediaState.filters.nations.includes(tank.nation)) &&
(tankopediaState.filters.test === 'include'
? true
: tankopediaState.filters.test === 'exclude'
? !tank.testing
: tank.testing),
)
.sort((a, b) => {
let diff = 0;
if (tankopediaState.sort.by === 'tier') {
diff = a.tier - b.tier;
}
if (tankopediaState.sort.by === 'name') {
diff = a.name.localeCompare(b.name);
}
return tankopediaState.sort.direction === 'ascending' ? diff : -diff;
}),
[tankopediaState.filters, tankopediaState.sort],
);
const [searchedList, setSearchedList] = useState(searchableTanks);
const [page, setPage] = useState(0);
const [showFilters, setShowFilters] = useState(false);
useEffect(() => {
setSearchedList(searchableTanks);
if (input.current) input.current.value = '';
}, [searchableTanks]);
useEffect(() => {
setPage(
Math.min(
Math.max(0, page),
Math.ceil(searchedList.length / TANKS_PER_PAGE) - 1,
),
);
}, [searchedList]);
return (
<PageWrapper size="wide" color="purple">
<Suspense fallback={<Text>Loading...</Text>}>
<Flex direction="column" gap="4">
<Flex gap="2">
<TextField.Root style={{ flex: 1 }}>
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
<TextField.Input
ref={input}
placeholder="Search tanks..."
onChange={(event) => {
if (event.target.value.length === 0) {
setSearchedList(searchableTanks);
} else {
setSearchedList(
go(event.target.value, searchableTanks, {
keys: [
'name',
'id',
] satisfies (keyof BlitzkriegTankDefinition)[],
}).map(({ obj }) => obj),
);
}
}}
/>
</TextField.Root>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="soft">
Sort
<CaretDownIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>By</DropdownMenu.Label>
<DropdownMenu.RadioGroup
value={tankopediaState.sort.by}
onValueChange={(value) =>
mutateTankopedia((draft) => {
draft.sort.by = value as TankopediaSortBy;
})
}
>
<DropdownMenu.RadioItem value="tier">
Tier
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="name">
Name
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator />
<DropdownMenu.Label>Direction</DropdownMenu.Label>
<DropdownMenu.RadioGroup
value={tankopediaState.sort.direction}
onValueChange={(value) =>
mutateTankopedia((draft) => {
draft.sort.direction = value as TankopediaSortDirection;
})
}
>
<DropdownMenu.RadioItem value="ascending">
Ascending
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="descending">
Descending
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Button
variant="soft"
onClick={() => setShowFilters((state) => !state)}
>
Filters {showFilters ? <CaretUpIcon /> : <CaretDownIcon />}
</Button>
</Flex>
{showFilters && (
<Card>
<Suspense fallback={<Text>Loading...</Text>}>
<Options />
</Suspense>
</Card>
)}
</Flex>
<PageTurner
page={page}
setPage={setPage}
tanksPerPage={TANKS_PER_PAGE}
searchedList={searchedList}
/>
<Flex wrap="wrap" gap="3" justify="center">
{searchedList
.map((tank) => (
<a
key={tank.id}
href={`/tools/tankopedia/${tank.id}`}
style={{
flex: 1,
textDecoration: 'none',
color: 'unset',
}}
>
<Card className={styles.listing}>
<Inset
key={tank.id}
style={{
minWidth: 256,
minHeight: 128,
position: 'relative',
display: 'flex',
}}
>
<img
className={styles.flag}
src={asset(`flags/scratched/${tank.nation}.webp`)}
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
position: 'absolute',
}}
/>
<div
style={{
width: 256,
height: 128,
}}
className={styles.listingImage}
>
<img
src={tankIcon(tank.id)}
style={{
objectFit: 'contain',
objectPosition: 'left center',
}}
/>
</div>
<div
className={styles.listingShadow}
style={{
width: '100%',
height: '100%',
position: 'absolute',
transition: `box-shadow ${theme.durations.regular}`,
}}
/>
<Flex
align="center"
justify="between"
className={styles.listingLabel}
style={{
position: 'absolute',
bottom: 8,
padding: '0 12px',
width: '100%',
boxSizing: 'border-box',
}}
>
<Flex align="center" justify="center" gap="1">
<img
src={
(tank.tree_type === 'collector'
? TANK_ICONS_COLLECTOR
: tank.tree_type === 'premium'
? TANK_ICONS_PREMIUM
: TANK_ICONS)[tank.type]
}
style={{ width: '1em', height: '1em' }}
/>
<Text
size="4"
color={
tank.tree_type === 'collector'
? 'blue'
: tank.tree_type === 'premium'
? 'amber'
: undefined
}
weight="medium"
style={{
color:
tank.tree_type === 'researchable'
? slateDark.slate12
: undefined,
whiteSpace: 'nowrap',
maxWidth: 160,
display: 'block',
textOverflow: 'ellipsis',
overflow: 'hidden',
}}
>
{tank.name}
</Text>
</Flex>
<Text size="4" color="gray" highContrast={false}>
{TIER_ROMAN_NUMERALS[tank.tier]}
</Text>
</Flex>
</Inset>
</Card>
</a>
))
.slice(page * TANKS_PER_PAGE, (page + 1) * TANKS_PER_PAGE)}
</Flex>
<PageTurner
page={page}
setPage={setPage}
tanksPerPage={TANKS_PER_PAGE}
searchedList={searchedList}
/>
</Suspense>
</PageWrapper>
);
}
+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 -2
View File
@@ -6,18 +6,18 @@ import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
const DOCS = {
permissions: 'guide/permissions',
embeds: 'guide/embeds',
introduction: 'guide/introduction',
invite: 'guide/invite',
timezones: 'guide/timezones',
creators: 'guide/creators',
};
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
permissions: 'The permissions needed for the bot to function',
embeds: 'How embeds work',
introduction: 'About the bot',
invite: 'How to invite the bot',
timezones: 'How Blitzkrieg infers and uses your timezone',
creators: 'Who created the bot',
};
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
@@ -51,6 +51,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}`,
+339
View File
@@ -0,0 +1,339 @@
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 { tankDefinitions } from '../core/blitzkrieg/definitions/tanks';
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 addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
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 { CommandRegistryPromisable } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 10;
export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
) {
const awaitedTankDefinitions = await tankDefinitions;
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 tankDefinition = awaitedTankDefinitions[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={tankDefinition?.type}
treeType={tankDefinition ? await getTreeType(id) : undefined}
title={awaitedTankDefinitions[id].name}
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<CommandRegistryPromisable>(
async (resolve) => {
const command = await addPeriodicFilterOptions(
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 renderBreakdown(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 renderBreakdown(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,
// };
+16 -24
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();
@@ -18,27 +13,24 @@ export const debugCommand: CommandRegistry = {
.setName('debug')
.setDescription('Debug information about the bot'),
async handler(interaction) {
async handler() {
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}`;
},
};
+21 -25
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 { tankDefinitions } from '../core/blitzkrieg/definitions/tanks';
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 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);
@@ -70,11 +66,11 @@ export const eligibleCommand: CommandRegistry = {
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankopediaEntry = (await tankopedia)[tankId];
const tankDefinition = (await tankDefinitions)[tankId];
if (!tankopediaEntry) return accumulator;
if (!tankDefinition) return accumulator;
const tankTier = tankopediaEntry.tier;
const tankTier = tankDefinition.tier;
return (await accumulator) + tankTier * stats.battles;
},
@@ -83,9 +79,9 @@ export const eligibleCommand: CommandRegistry = {
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankopediaEntry = (await tankopedia)[tankId];
const tankDefinition = (await tankDefinitions)[tankId];
if (!tankopediaEntry) return accumulator;
if (!tankDefinition) return accumulator;
return stats.battles;
},
@@ -114,7 +110,7 @@ export const eligibleCommand: CommandRegistry = {
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
if ((await tankopedia)[tankId]?.tier === 10) {
if ((await tankDefinitions)[tankId]?.tier === 10) {
const tankStats = diffed[tankId];
return (await accumulator) + tankStats.damage_dealt;
} else return accumulator;
@@ -125,7 +121,7 @@ export const eligibleCommand: CommandRegistry = {
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
if ((await tankopedia)[tankId]?.tier === 10) {
if ((await tankDefinitions)[tankId]?.tier === 10) {
const tankStats = diffed[tankId];
return (await accumulator) + tankStats.battles;
} else return accumulator;
@@ -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([
+151 -49
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 { CommandRegistryRaw } from '../events/interactionCreate';
import evolution from '../renderers/evolution';
import { StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate';
export const evolutionCommand = new Promise<CommandRegistryRaw>(
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<CommandRegistryPromisable>(
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,
);
},
});
+75 -76
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 addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
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 { CommandRegistryRaw } from '../events/interactionCreate';
import fullStats from '../renderers/fullStats';
import { MultiTankFilters, StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { getTierWeights } from '../core/statistics/getTierWeights';
import { CommandRegistryPromisable } from '../events/interactionCreate';
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
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<CommandRegistryPromisable>(
async (resolve) => {
const command = await addStatTypeSubCommandGroups(
const command = await addPeriodicFilterOptions(
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()}`}
/>
+209
View File
@@ -0,0 +1,209 @@
import { teal } from '@radix-ui/colors';
import { 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 } 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 embedInfo from '../core/discord/embedInfo';
import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
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 { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = parseInt(interaction.user.id);
const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
await linkBlitzAndDiscord(discordId, 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[region]}
</span>
</div>
</div>
</div>
<Glow color={teal.teal9} rotation={-90} />
</div>
</Wrapper>,
embedInfo(
'Incorrect account?',
'Use the command again and select a username from the search results.',
),
];
},
autocomplete: autocompleteUsername,
};
+31 -76
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 { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
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 {
BlitzkriegTankDefinition,
TIER_ROMAN_NUMERALS,
TankopediaEntry,
Tier,
tankopedia,
tankopediaInfo,
} from '../core/blitz/tankopedia';
tankDefinitions,
} from '../core/blitzkrieg/definitions/tanks';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { tankIcon } from '../core/blitzkrieg/tankIcon';
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,30 +36,26 @@ 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(
tankStats.map(async (tankData) => ({
tankopedia: (await tankopedia)[tankData.tank_id]!,
tank_id: tankData.tank_id,
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
id: tankData.tank_id,
})),
)
).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 groupedTanks: Record<string, TankopediaEntry[]> = {};
).filter((tank) => tank.tankDefinitions?.tier === tier);
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
const groupedTanks: Record<string, BlitzkriegTankDefinition[]> = {};
const nations: string[] = [];
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankopedia.nation] === undefined) {
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
nations.push(tank.tankopedia.nation);
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation);
} else {
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
}
});
@@ -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} />}
@@ -117,7 +86,7 @@ export const ownedTanksCommand: CommandRegistry = {
return (
<Tanks.Root>
<Tanks.Title>
{(await tankopediaInfo).vehicle_nations[nation]}
{(await encyclopediaInfo).vehicle_nations[nation]}
</Tanks.Title>
<Tanks.Row>
@@ -125,11 +94,11 @@ export const ownedTanksCommand: CommandRegistry = {
{await Promise.all(
leftColumn.map(async (tank) => (
<Tanks.Item
key={tank.tank_id}
name={await resolveTankName(tank.tank_id)}
key={tank.id}
name={tank.name}
tankType={tank.type}
image={tank.images?.normal}
treeType={await getTreeType(tank.tank_id)}
image={tankIcon(tank.id)}
treeType={await getTreeType(tank.id)}
/>
)),
)}
@@ -138,11 +107,11 @@ export const ownedTanksCommand: CommandRegistry = {
{await Promise.all(
rightColumn.map(async (tank) => (
<Tanks.Item
key={tank.tank_id}
name={await resolveTankName(tank.tank_id)}
key={tank.id}
name={tank.name}
tankType={tank.type}
image={tank.images?.normal}
treeType={await getTreeType(tank.tank_id)}
image={tankIcon(tank.id)}
treeType={await getTreeType(tank.id)}
/>
)),
)}
@@ -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'));
+42
View File
@@ -0,0 +1,42 @@
import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate';
export const permissionsCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('permissions')
.setDescription('Checks wether the bot has the required permissions'),
async handler(interaction) {
// friday the 13th permissions easter egg
const isFridayThe13th =
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has('ViewChannel'),
'View channels: needed for the refresh button to work',
],
[
interaction.appPermissions?.has('ReadMessageHistory'),
'Read message history: also needed for the refresh button to work',
],
[
interaction.appPermissions?.has('AttachFiles'),
'Attach files: needed for image-based commands to work',
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
} else {
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
}
})
.join('\n');
return `# Permissions\n${body}`;
},
};
+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`,
+220 -313
View File
@@ -2,30 +2,32 @@ import {
APIApplicationCommandOptionChoice,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { range } from 'lodash';
import markdownEscape from 'markdown-escape';
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 regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
import getMidnightLeaderboard, {
DATABASE_REPO,
} from '../core/database/getMidnightLeaderboard';
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason';
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';
import { CommandRegistryPromisable } from '../events/interactionCreate';
export interface RatingsPlayer {
spa_id: number;
@@ -114,105 +116,80 @@ 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 },
];
const noOngoingSeason = embedNegative(
'No ongoing season',
"Wargaming didn't provide any data for this season.",
);
function addSubcommands(
option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option,
) {
return option
.addSubcommand((option) =>
addOptions(
option
.setName('neighbours')
.setDescription('Your position and neighbours'),
)
.addStringOption(addUsernameChoices)
export const ratingsCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingsSeason();
const seasonNumbers = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
function addSubcommands(option: SlashCommandSubcommandBuilder) {
return option
.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),
),
)
.addSubcommand((option) =>
addOptions(option.setName('league').setDescription('Top league players'))
)
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
value: `${value}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setName('season')
.setDescription('Season number')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
);
}
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>,
.addChoices(
...seasonNumbers
.map(
(number, index) =>
({
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
value: `${number}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
)
.reverse(),
),
);
});
}
const command = new SlashCommandBuilder()
.setName('ratings')
.setDescription('Ratings battles statistics')
.addSubcommandGroup((option) =>
addSubcommands(
option.setName('archive').setDescription('Any previous season'),
(option) =>
option.addStringOption((option) =>
option
.setName('season')
.setDescription('The season number')
.setChoices(...seasonChoices)
.setRequired(true),
),
),
)
.addSubcommandGroup((option) =>
.addSubcommand((option) =>
addSubcommands(
option
.setName('current')
.setDescription('The current running Ratings season'),
.setName('neighbors')
.setDescription('Shows neighboring players on the leaderboard'),
),
)
.addSubcommand((option) =>
addSubcommands(
option
.setName('league')
.setDescription('Shows top players from a league')
.addStringOption(addRegionChoices)
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
value: `${value}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
),
),
);
@@ -231,18 +208,19 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
nickname: string;
}
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
| 'archive'
| '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;
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string;
let titleImage: string | undefined;
let titleNameDiscriminator: string | undefined;
let titleDescription: string;
let playersBefore: number;
let playersAfter: number;
@@ -256,243 +234,167 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
interaction.options.getString('league')!,
);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (subcommandGroup === 'current'
regionRatingsInfo = await (isCurrentSeason
? getRatingsInfo(region)
: getArchivedRatingsInfo(season!, region));
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');
}
: getArchivedRatingsInfo(region, season!));
const players = (await fetch(data.download_url!).then(
(response) => response.json(),
)) as BlitzkriegRatingsLeaderboard;
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const 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`,
);
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag,
nickname:
clanData[player.id]?.account_name ??
`Unknown Player ${player.id}`,
}) satisfies SimplifiedPlayer,
);
});
const leagueInfo =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.leagues[leagueIndex]
: undefined;
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
const result = isCurrentSeason
? 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
: players.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const clanData = await getClanAccountInfo(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
trimmed.map(({ id }) => id),
['clan'],
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag,
nickname:
clanData[index]?.account_name ??
`Deleted ${player.id}`,
}) satisfies SimplifiedPlayer,
);
},
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = isCurrentSeason
? 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');
}
regionRatingsInfo = isCurrentSeason
? await getRatingsInfo(region)
: 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);
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',
);
}
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const response = fetch(data.download_url!);
const players = (await response.then((response) =>
response.json(),
)) as BlitzkriegRatingsLeaderboard;
const playerIndex = players.findIndex(
(player) => player.id === id,
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors = isCurrentSeason
? 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 new UserError(
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`,
);
}
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.',
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.slice(
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`,
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}));
});
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
const halfRange = Math.round(limit / 2);
const trimmed = players.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const clanData = await getClanAccountInfo(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
trimmed.map((player) => player.id),
['clan'],
);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[index]?.clan?.tag,
nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
},
);
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
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 +425,22 @@ 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.toLocaleString()} more`}
/>
)}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
{playersAfter > 0 && (
<Leaderboard.Gap
message={`+ ${playersAfter.toLocaleString()} 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}`,
+2 -2
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 { tankNamesDiacritics } from '../core/blitzkrieg/definitions/tanks';
import addTankChoices from '../core/discord/addTankChoices';
import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate';
@@ -25,7 +25,7 @@ export const searchTanksCommand: CommandRegistry = {
async handler(interaction) {
const tank = interaction.options.getString('tank')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const results = go(tank, await TANK_NAMES_DIACRITICS, {
const results = go(tank, await tankNamesDiacritics, {
limit,
keys: ['combined'],
}).map((result) => result.obj.original);
+91 -105
View File
@@ -1,120 +1,106 @@
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 addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
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 { CommandRegistryRaw } from '../events/interactionCreate';
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { CommandRegistryPromisable } from '../events/interactionCreate';
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('stats')
.setDescription('Regular battles statistics'),
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>
);
}
resolve({
inProduction: true,
inPublic: true,
export const statsCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('stats')
.setDescription('Regular battles statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
command,
resolve({
inProduction: true,
inPublic: true,
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, {
...player,
tankId,
start,
end,
period,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
command,
const tierOption = interaction.options.getString('tier');
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 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) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
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);
async button(interaction) {
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);
},
});
},
);
+71
View File
@@ -0,0 +1,71 @@
import { SlashCommandBuilder } from 'discord.js';
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
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 from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown';
export const todayCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
const command = addFilterOptions(
new SlashCommandBuilder()
.setName('today')
.setDescription("Today's played tanks and statistics"),
awaitedEncyclopediaInfo,
).addStringOption(addUsernameChoices);
resolve({
inProduction: true,
inPublic: true,
command,
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(
player.region,
interaction,
'today',
);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction, true),
...filters,
});
return [
...(await renderBreakdown(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 renderBreakdown(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 }} />
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff. Show more