Compare commits

..
239 Commits
Author SHA1 Message Date
TrèsAbhi cb9d283f62 Merge pull request #39 from tresabhi/1.13.1 2023-10-01 19:03:54 -05:00
TrèsAbhi 8aba19e05b fix: duplicate name 2023-10-01 19:02:47 -05:00
TrèsAbhi b7b4035492 feat: show career options 2023-10-01 19:00:05 -05:00
TrèsAbhi 3667fd82b7 feat: docs 2023-10-01 18:56:25 -05:00
TrèsAbhi 691d5cde27 feat: updated embed docs 2023-10-01 18:52:11 -05:00
TrèsAbhi 7f431b07e1 feat: added options 2023-10-01 18:43:40 -05:00
TrèsAbhi 8264f19ad4 fix: center aligned text in breakdown 2023-10-01 17:45:53 -05:00
TrèsAbhi e947cfba45 feat: ico 2023-10-01 17:43:48 -05:00
TrèsAbhi e69590c035 feat: added favicon 2023-10-01 17:38:28 -05:00
TrèsAbhi 8b19393d7c feat: updated README to be better lol 2023-10-01 17:36:03 -05:00
TrèsAbhi 9480603572 feat: added title and meta desc 2023-10-01 17:26:08 -05:00
TrèsAbhi 90c6535a4c fix: center aligned blitzkrieg logo 2023-10-01 17:24:02 -05:00
TrèsAbhi 258588921a Update cd-host.yml 2023-10-01 17:16:16 -05:00
TrèsAbhi 525b309037 Update ci-build-bot.yml 2023-10-01 17:15:47 -05:00
TrèsAbhi 2f941b9224 Update ci-build-docs.yml 2023-10-01 17:15:36 -05:00
TrèsAbhi 259c4240b5 Update ci-build-website.yml 2023-10-01 17:15:25 -05:00
TrèsAbhi 9eb7157fce Update cd-host.yml 2023-10-01 17:15:02 -05:00
TrèsAbhi 1138e1a826 Merge pull request #37 from tresabhi/1.13.0 2023-10-01 17:11:31 -05:00
TrèsAbhi e5057b8c1d fix: unknown reward icons 2023-10-01 17:10:22 -05:00
TrèsAbhi 7c04243b3d fix: old help command name reference 2023-10-01 17:02:13 -05:00
TrèsAbhi c6b54454be feat: remove gitpod 2023-10-01 16:57:05 -05:00
TrèsAbhi 876fb0fc5e feat: simplified build script 2023-10-01 16:56:59 -05:00
TrèsAbhi 38f912dc0a feat: removed server 2023-10-01 16:48:53 -05:00
TrèsAbhi eb24f49766 feat: added art credits 2023-10-01 16:43:21 -05:00
TrèsAbhi 168ab1e1a1 feat: tooltip for embed button 2023-10-01 16:38:39 -05:00
TrèsAbhi 49f601d200 feat: added reset prompt 2023-10-01 16:37:51 -05:00
TrèsAbhi 15f32a0044 feat: use if 2023-10-01 16:19:20 -05:00
TrèsAbhi 7bb3c60a61 feat: a little bit of keyboard nav 2023-10-01 16:16:32 -05:00
TrèsAbhi 4d1dfc1ae5 feat: use radix ui for dropdown 2023-10-01 16:05:51 -05:00
TrèsAbhi ec5dfd7727 feat: hide search results on unfocus 2023-10-01 15:18:50 -05:00
TrèsAbhi 8e5a588e87 fix: deltas showing up even on no diff 2023-10-01 15:15:36 -05:00
TrèsAbhi b9c3cfaee4 feat: optimized homepage images 2023-10-01 15:04:17 -05:00
TrèsAbhi 01bd3f8d68 feat: added disabled tools 2023-10-01 11:35:44 -05:00
TrèsAbhi b39f545669 feat: added deltas to totals 2023-10-01 11:05:01 -05:00
TrèsAbhi e7f517e157 feat: renamed help command to about 2023-10-01 10:56:03 -05:00
TrèsAbhi 1d7de715e7 feat: updated permissions doc 2023-10-01 10:55:45 -05:00
TrèsAbhi 76501c1aa0 feat: added dedicated tsconfig for esbuild 2023-09-30 22:36:44 -05:00
TrèsAbhi a535a07810 fix: null clan 2023-09-30 22:35:04 -05:00
TrèsAbhi 6add54e86d feat: hide navbar icons 2023-09-30 22:33:01 -05:00
TrèsAbhi a74eba9e97 feat: started migrating to radix 2023-09-30 22:30:52 -05:00
TrèsAbhi 34f782e747 feat: embeds 2023-09-30 22:12:48 -05:00
TrèsAbhi 1990322d6d fix: use effect deps 2023-09-28 10:39:32 -05:00
TrèsAbhi 0339979f9f feat: added "--" for NaN values 2023-09-28 10:28:22 -05:00
TrèsAbhi 823465fde9 feat: added reset and search click functionality 2023-09-28 10:23:46 -05:00
TrèsAbhi 1feae49490 fix: build command 2023-09-24 18:58:41 -05:00
TrèsAbhi 1a1e2169a8 bfsafjkhsadkjfhgkajsdf 2023-09-24 18:56:32 -05:00
TrèsAbhi f1b1e25d9a sad 2023-09-24 18:52:32 -05:00
TrèsAbhi ce6472be8d a 2023-09-24 18:50:38 -05:00
TrèsAbhi 79232cecb9 breath 2023-09-24 18:49:01 -05:00
TrèsAbhi 1a802da0ce (┬┬﹏┬┬) 2023-09-24 18:45:12 -05:00
TrèsAbhi b33a845c08 feat: it works?? 2023-09-24 18:43:33 -05:00
TrèsAbhi 6643b51a9b feat: undo last commit 2023-09-24 18:32:46 -05:00
TrèsAbhi 1cce00a09a feat: removed distDir 2023-09-24 18:28:44 -05:00
TrèsAbhi 1ef9a0289f fix: use pathname instead 2023-09-24 17:36:20 -05:00
TrèsAbhi 498a359f01 fix: use next/navigation instead 2023-09-24 17:35:50 -05:00
TrèsAbhi ebbcdc2089 fix: use useRouter instead of location 2023-09-24 17:33:34 -05:00
TrèsAbhi cced57b22e fix: wg id imports 2023-09-24 17:29:23 -05:00
TrèsAbhi 96d47a82bc fix: account list export 2023-09-24 17:27:04 -05:00
TrèsAbhi b4cab18ecd feat: moved tools outside page 2023-09-24 17:24:21 -05:00
TrèsAbhi d801447e5d feat: career stats 2023-09-24 17:02:09 -05:00
TrèsAbhi 8c041d4eef feat: session tracker list 2023-09-24 15:58:42 -05:00
TrèsAbhi 6ff2a23ff7 feat: removed non-proxy apis 2023-09-23 19:20:29 -05:00
TrèsAbhi 925ac96264 fix: compat for /today 2023-09-23 18:51:44 -05:00
TresAbhi 9e89198cd5 fix: a few type issues 2023-09-23 23:30:14 +00:00
TrèsAbhi a1e6643dc0 Merge pull request #36 from tresabhi/main 2023-09-23 17:30:51 -05:00
TresAbhi 63257a13f5 feat: generalized breakdown 2023-09-23 22:02:30 +00:00
TresAbhi e37e0ccf9f feat: made WARGAMING_APPLICATION_ID public 2023-09-23 21:51:00 +00:00
TresAbhi 018783bb47 feat: session persistance 2023-09-19 15:52:39 +00:00
TrèsAbhi f883670cbc Merge pull request #34 from tresabhi/1.12.9
1.12.9
2023-09-19 08:07:59 -05:00
TresAbhi 57f6e928b7 feat: search functionality 2023-09-18 23:25:24 +00:00
TresAbhi cbeb83d8d0 feat: added player list api 2023-09-18 23:05:19 +00:00
TresAbhi 33b4ea04b4 feat: session toolbar 2023-09-18 22:32:21 +00:00
TresAbhi 350434cc5a feat: added tool ground work 2023-09-18 21:55:40 +00:00
TresAbhi c0eca0c851 fix: only transition box shadow 2023-09-18 21:35:46 +00:00
TresAbhi cdac43fa5f feat: 1.12.9 2023-09-18 21:33:06 +00:00
TresAbhi 30aeabba88 feat: simplified body 2023-09-12 03:05:11 +00:00
TresAbhi 02a07f3a8a feat: homepage 2023-09-12 02:38:50 +00:00
TrèsAbhi 986a32b3ce Merge pull request #33 from tresabhi/main 2023-09-11 13:17:14 -05:00
TrèsAbhi 8c4afa73f8 Merge pull request #32 from tresabhi/1.12.8 2023-09-11 13:15:00 -05:00
TrèsAbhi 6db3ccdb40 feat: removed embed buttons 2023-09-11 13:14:05 -05:00
TresAbhi cf0fe5cbba feat: use dot env 2023-09-09 03:05:59 +00:00
TresAbhi 621398f95b feat: removed random gitpod setup command 2023-09-09 03:00:41 +00:00
TresAbhi 186a972db7 feat: added compatibility for next js 2023-09-09 02:56:34 +00:00
TresAbhi 7473a17a5e fix: added icons lib 2023-09-09 02:53:31 +00:00
TresAbhi 66e08afb0d fix: moved docs to dist/docs 2023-09-09 02:51:44 +00:00
TresAbhi 851da1d020 feat: split up CI build 2023-09-09 02:41:51 +00:00
TrèsAbhi c44840978c Merge pull request #31 from tresabhi/main 2023-09-08 21:40:22 -05:00
TrèsAbhi 90e012319a Merge pull request #30 from tresabhi/1.12.7 2023-09-08 21:35:46 -05:00
TresAbhi 18de9cbaab feat: upgraded dependencies 2023-09-09 02:35:14 +00:00
TresAbhi 42a76cbd38 feat: changelogs 2023-09-09 02:19:20 +00:00
TresAbhi eb95634801 fix: escaped markdown for clan event title 2023-09-09 02:11:32 +00:00
TresAbhi de8bfff47f feat: clearer statement about timezones 2023-09-09 02:10:54 +00:00
TresAbhi 07c18911df feat: clearer statement about public data 2023-09-09 02:06:09 +00:00
TresAbhi cc769cc330 fix: react is now defined 2023-09-09 02:04:36 +00:00
TrèsAbhi 9794f58936 Update cd-host.yml 2023-09-08 11:50:31 -05:00
TrèsAbhi a44fcff3b1 Update cd-host.yml 2023-09-08 11:48:41 -05:00
TrèsAbhi 7e4a3e88b9 Update cd-host.yml 2023-09-08 11:47:39 -05:00
TrèsAbhi 8fd45e2205 Merge pull request #29 from tresabhi/1.12.5 2023-09-08 11:45:15 -05:00
TresAbhi 7d235057e8 feat: changelogs 2023-09-08 16:41:06 +00:00
TresAbhi 63a8a67771 fix: better approximation message and players with 0 wins are omitted 2023-09-08 16:38:08 +00:00
TresAbhi bac39ca176 fix: only use players who have played within the last day 2023-09-08 16:30:44 +00:00
TresAbhi 1979047bbc feat: navbar 2023-09-07 21:25:37 +00:00
TrèsAbhi 8860c1b0f8 Merge pull request #28 from tresabhi/main 2023-09-07 15:23:26 -05:00
TrèsAbhi 626b1e8601 Merge branch '1.13.0' into main 2023-09-07 15:23:10 -05:00
TresAbhi 04086f31b9 fix: spelling 2023-09-07 03:25:07 +00:00
TresAbhi 7037aa9c3b feat: 1.12.5 2023-09-07 03:24:07 +00:00
TresAbhi f5bab8a698 faet: clan-event 2023-09-07 01:24:12 +00:00
TresAbhi ee44b1b1d8 feat: next js 2023-09-07 01:13:03 +00:00
TresAbhi d0f91df95b feat: split bot and server builds 2023-09-04 16:49:38 +00:00
TrèsAbhi dc1c49da6a Merge pull request #27 from tresabhi/main 2023-09-03 13:24:34 -05:00
TrèsAbhi 331040bbdf Merge pull request #26 from tresabhi/1.12.3 2023-09-03 13:21:00 -05:00
TresAbhi 70e1d4b30d feat: changelogs 2023-09-03 18:20:19 +00:00
TresAbhi 03dbd797fb feat: no ongoing season proof 2023-09-03 18:15:10 +00:00
TresAbhi eb4cb1fbfe feat: removed inDevelopment 2023-08-24 00:08:08 +00:00
TrèsAbhi 28172f7f8f Merge pull request #25 from tresabhi/1.12.2 2023-08-23 09:52:48 -05:00
TresAbhi cb5347cbb8 feat: changelog 2023-08-23 14:51:51 +00:00
TresAbhi 1ce0781d2d feat: use application directory 2023-08-23 00:34:37 +00:00
TresAbhi 1257aa76d7 feat: code split bot 2023-08-23 00:33:26 +00:00
TresAbhi 6ec3835896 feat: updated dependencies 2023-08-23 00:18:25 +00:00
TresAbhi 506fe7d9bd feat: removed unused dependencies 2023-08-23 00:17:25 +00:00
TresAbhi 14ef9f6295 feat: removed unused files 2023-08-23 00:17:03 +00:00
TresAbhi 0d76e1a29e feat: added knip 2023-08-23 00:16:39 +00:00
TresAbhi 9e42d7e8f5 feat: removed powered by footer 2023-08-23 00:15:45 +00:00
TresAbhi 364f97332b feat: better and unified tier selection 2023-08-23 00:14:41 +00:00
TresAbhi caa7bb8a65 fix: removed random dev console log 2023-08-22 23:49:34 +00:00
TresAbhi 3e956b34c1 fix: clan name and icon now show up with multi-tank 2023-08-22 23:49:00 +00:00
TresAbhi 038374e5aa fix: changed examples to guide in navbar 2023-08-22 23:47:55 +00:00
TresAbhi dafd54f830 feat: github link 2023-08-22 23:47:35 +00:00
TresAbhi cb65e6b0af fix: corrected order of changelog versions 2023-08-22 23:46:59 +00:00
TresAbhi 4ca8590c83 fix: guide link 2023-08-22 23:46:15 +00:00
TresAbhi c5ef02d7e6 feat: updated dependencies 2023-08-22 23:45:47 +00:00
TrèsAbhi 8175aa8715 Merge pull request #24 from tresabhi/1.12.1 2023-08-11 13:03:52 -05:00
TresAbhi b426634659 feat: changelog 2023-08-11 18:03:41 +00:00
TresAbhi e92da1b700 fix: revert username pattern infrence 2023-08-11 18:01:31 +00:00
TresAbhi 7e0a2e88ee fix: tank id/index mismatch 2023-08-11 17:57:44 +00:00
TresAbhi 16afa8c2d4 feat: url change 2023-08-10 19:48:27 +00:00
TresAbhi 907288455a changed to js 2023-08-10 19:28:53 +00:00
TrèsAbhi 64689e4e3b Update package.json 2023-08-10 14:24:47 -05:00
TresAbhi 294377846a empty commit 2023-08-10 19:23:09 +00:00
TresAbhi 70e4cbaba2 fix: argv undefined 2023-08-10 19:14:43 +00:00
TrèsAbhi 9e2f9526fb Update package.dist.json 2023-08-10 13:58:15 -05:00
TrèsAbhi 84bec20fa5 Update cd-host.yml 2023-08-10 13:55:56 -05:00
TrèsAbhi e9a492fde6 Merge pull request #23 from tresabhi/1.12.0 2023-08-10 13:52:10 -05:00
TresAbhi 76a69f08b4 feat: docs 2023-08-10 18:51:22 +00:00
TresAbhi 893110d3ef feat: added 1 more decimal of precision in evolution 2023-08-10 01:49:05 +00:00
TresAbhi 70575fa9cf feat: accent-tolerant tank search 2023-08-10 01:46:39 +00:00
TresAbhi bb2450eb3c feat: added the new /today embeds 2023-08-10 01:31:05 +00:00
TresAbhi ab56689326 feat: updated /today with figma 2023-08-10 01:03:53 +00:00
TresAbhi a9cf07480a feat: added arabic numerals to owned tanks options 2023-08-10 00:37:54 +00:00
TresAbhi c81de2d758 feat: removed interaction based search param inference 2023-08-09 18:02:15 +00:00
TresAbhi aa1c7639f2 feat: auto refresh rate changed to every 10 seconds 2023-08-09 18:00:23 +00:00
TresAbhi d448101b32 fix: swapped start and end min and max 2023-08-09 17:56:35 +00:00
TresAbhi e806f0ede2 fix: swapped min and max for start and end 2023-08-09 17:51:19 +00:00
TresAbhi 8253c27333 fix: search parameters are now passed correctly 2023-08-09 17:39:21 +00:00
TresAbhi 254392370f feat: only interpret correctly formatter usernames 2023-08-09 17:27:38 +00:00
TresAbhi d2319688a5 fix: removed multi-tank from evolution 2023-08-09 17:25:05 +00:00
TresAbhi ffebe6c696 fix: use regional season info instead of hard-coded NA 2023-08-09 17:20:03 +00:00
TresAbhi 9e4094f3a9 feat: added ratings neighbours 2023-08-09 17:13:26 +00:00
TresAbhi f7eadb08a9 feat: made /ratings data-driven 2023-08-09 00:47:15 +00:00
TresAbhi d511cfa4cc fix: fixed ratings reward alignment 2023-08-09 00:35:33 +00:00
TresAbhi 270a3051b7 feat: updated /owned-tanks to new width 2023-08-09 00:19:50 +00:00
TresAbhi 81bc9f9ba6 feat: updated /inactive 2023-08-07 22:54:55 +00:00
TresAbhi 338cec4f18 feat: updated full-stats to new subcommands 2023-08-07 22:51:04 +00:00
TresAbhi b455e3380f feat: added minimum character limit to clan autocomplete 2023-08-06 19:13:28 +00:00
TresAbhi 92761f2c55 feat: made footer center aligned when without footer text 2023-08-06 19:08:02 +00:00
TresAbhi 7823b8ad2e feat: updated /evolution to new width 2023-08-06 19:07:44 +00:00
TresAbhi cac82f3124 fix: renamed statsfull to fullstats in power by bar 2023-08-06 19:00:32 +00:00
TresAbhi 4b892d06e4 fix: import types from stats instead of fullstats 2023-08-06 19:00:00 +00:00
TresAbhi 92149b0f00 feat: matched titlebar style to figma 2023-08-06 18:53:57 +00:00
TresAbhi 399aa4c03c feat: removed region text in title bar 2023-08-06 18:41:59 +00:00
TresAbhi d5de715032 fix: title bar spacing is now 4px just like figma 2023-08-06 18:39:23 +00:00
TresAbhi e6fb179184 feat: removed self-advert from title bar 2023-08-06 18:38:49 +00:00
TresAbhi af54f12ec3 feat: set width to 480px 2023-08-06 18:38:17 +00:00
TresAbhi 909cabd517 feat: filter functionality 2023-08-04 19:49:40 +00:00
TresAbhi a734a71d2f fix: duplicate efficienty cards 2023-08-04 18:25:20 +00:00
TresAbhi 91dc45c880 feat: added more mature periods 2023-08-04 18:24:20 +00:00
TresAbhi 91f364b76a feat: used dahses for command readibility 2023-08-04 02:24:14 +00:00
TresAbhi 23dd088466 feat: better subcommand organiztion 2023-08-04 02:13:21 +00:00
TresAbhi 2cabcc57f4 feat: moved ratings out of preview 2023-07-31 01:32:31 +00:00
TrèsAbhi dd6092a5e7 fix: GH_TOKEN 2023-07-17 19:37:12 -05:00
TrèsAbhi 3c2861f956 Merge pull request #22 from tresabhi/1.11.0 2023-07-17 19:30:50 -05:00
TresAbhi 894b841769 feat: changelogs 2023-07-18 00:29:49 +00:00
TresAbhi 62627c96a3 feat: added other regions 2023-07-18 00:23:59 +00:00
TresAbhi c173ece882 feat: completed /ratings league 2023-07-18 00:17:00 +00:00
TresAbhi eca7cbbbeb feat: added more mature secrets handling 2023-07-16 18:26:33 +00:00
TresAbhi c72bfa9f71 feat: added rewards 2023-07-16 18:15:34 +00:00
TresAbhi b4ae39a509 feat: added custom lighter ratings leaderboard 2023-07-16 17:56:37 +00:00
TresAbhi 631c051895 feat: ditch histories 2023-07-16 17:51:01 +00:00
TresAbhi 9a2e11a2a4 feat: added basic rendering 2023-07-16 17:45:29 +00:00
TresAbhi aa6011b18d feat: added basic infrastructure for ratings command 2023-07-15 18:44:20 +00:00
TresAbhi 9419aec116 fix: removed "Europe" from /verify example 2023-07-15 18:07:14 +00:00
TresAbhi 638402589f feat: switched scripts to typescript 2023-07-15 01:21:59 +00:00
TrèsAbhi 6d6a6bf738 fix: workflow region names 2023-07-14 12:41:45 -05:00
TrèsAbhi 31f6123a55 Merge pull request #21 from tresabhi/1.10.2 2023-07-14 12:37:51 -05:00
TresAbhi 18a3e3092c fix: gave all regions their own cron times 2023-07-14 17:35:51 +00:00
TresAbhi 5ded0f8744 feat: changelogs 2023-07-14 17:31:05 +00:00
TresAbhi 4e0f0536fa feat: added separate latest and leaderboards for all regions 2023-07-14 17:27:57 +00:00
TresAbhi 3f081a5577 fix: install dependencies 2023-07-14 17:16:51 +00:00
TresAbhi 2447d663fa fix: use na region 2023-07-14 17:14:53 +00:00
TrèsAbhi 8a0afee97a Merge pull request #20 from tresabhi/1.10.1 2023-07-13 20:54:31 -05:00
TresAbhi 3cca68a8f0 feat: changelogs 2023-07-14 01:53:44 +00:00
TresAbhi 622cd92a13 feat: added cron job 2023-07-14 01:52:05 +00:00
TresAbhi 6d9e6973d5 feat: added ratings leaderboard generation and publication 2023-07-14 01:32:20 +00:00
TrèsAbhi 098e6c09b9 Merge pull request #19 from tresabhi/1.10.0 2023-07-12 14:07:25 -05:00
TresAbhi 186d45e3d9 fix: module type 2023-07-12 19:06:22 +00:00
TresAbhi 0fdc87a88c feat: docs 2023-07-12 18:35:31 +00:00
TresAbhi 1d4ac2f133 feat: switched to Node module resolution 2023-07-11 19:54:57 +00:00
TresAbhi 21158e9081 feat: updated /fullstats with figma 2023-07-11 19:43:22 +00:00
TresAbhi fedad8df3e feat: added timezone help 2023-07-11 19:00:10 +00:00
TresAbhi 6c8f5ca478 feat: simplified commands help 2023-07-11 18:44:32 +00:00
TresAbhi 463af106be feat: added more help commands 2023-07-10 19:14:16 +00:00
TresAbhi feeb850ca9 feat: added collector tanks to /ownedtanks 2023-07-10 18:16:33 +00:00
TresAbhi 80ffff322c feat: merge themes 2023-07-10 17:44:56 +00:00
TresAbhi 0833375562 feat: reset stats at midnight 2023-07-10 17:42:29 +00:00
TresAbhi 297f2f6580 feat: added limits to /today 2023-07-10 17:41:48 +00:00
TresAbhi 438feefa83 feat: today embeds 2023-07-10 01:44:05 +00:00
TresAbhi 8f6388b42b fix: time resets at 5:00 am 2023-07-10 01:27:13 +00:00
TresAbhi 405f544774 fix: time zone compliant commands 2023-07-10 01:23:49 +00:00
TresAbhi dd0644e09f fix: no tanks played now does not display a total 2023-07-10 01:23:35 +00:00
TresAbhi d8ac0b2ef6 feat: added order to getDiffedTankStats 2023-07-09 17:49:11 +00:00
TresAbhi 87955214e3 feat: updated /today with figma 2023-07-09 17:37:44 +00:00
TresAbhi f985cbf70b feat: updated evolution from figma 2023-07-09 01:12:02 +00:00
TresAbhi f64e72c312 feat: renamed statsfull to fullstats 2023-07-09 01:02:30 +00:00
TresAbhi ce6715cf2c fix: normal image instead of preview 2023-07-09 00:54:19 +00:00
TresAbhi 18e5405c54 fix: duplicate named interface export 2023-07-09 00:52:13 +00:00
TresAbhi a0f15fc1c4 fix: revert back to blitz stars tankopedia for missing tanks 2023-07-09 00:51:53 +00:00
TresAbhi cf20ae38d4 feat: updated /ownedtanks to new design 2023-07-09 00:47:03 +00:00
TresAbhi 2bf5eb78be feat: optimized tankopedia information 2023-07-08 19:47:21 +00:00
TresAbhi 57271e3c23 feat: infrastructure for ownedtanks columns 2023-07-08 19:39:35 +00:00
TresAbhi 2258b0f936 fix: extra tilda 2023-07-07 01:29:13 +00:00
TrèsAbhi aa8115d260 Merge pull request #18 from tresabhi/1.9.5 2023-07-06 20:27:59 -05:00
TrèsAbhi f8dfa386a0 Merge branch 'main' into 1.9.5 2023-07-06 20:23:42 -05:00
TresAbhi 00771e130f feat: changelogs 2023-07-07 01:22:54 +00:00
TresAbhi bd612bf777 fix: /today now preserves all WN8 values 2023-07-07 01:18:18 +00:00
TresAbhi ca208e44a1 fix: all commands now work with a lack of tankopedia entries 2023-07-07 01:08:00 +00:00
TrèsAbhi 72c4cd664b Merge pull request #17 from tresabhi/1.9.4 2023-07-05 12:47:06 -05:00
TresAbhi f907c79d68 feat: changelogs 2023-07-05 17:46:28 +00:00
TresAbhi ae3d0a08cd fix: tier weights now ignores tanks without entries 2023-07-05 17:43:15 +00:00
TresAbhi fb66027de0 fix: formatted tankopedia arrays not using promise 2023-07-05 17:42:14 +00:00
229 changed files with 8892 additions and 2591 deletions

No files matched your search

@@ -0,0 +1,34 @@
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
@@ -0,0 +1,34 @@
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
+3 -7
View File
@@ -16,18 +16,14 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
strategy:
matrix:
node-version: [18.x]
steps:
- name: Get code
- name: Checkout
uses: actions/checkout@v3
- name: Use node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
node-version: 18
cache: 'npm'
- name: Install Yarn
@@ -51,7 +47,7 @@ jobs:
- name: Upload pages artifact
uses: actions/upload-pages-artifact@v1
with:
path: docs/.vitepress/dist
path: dist/docs
- name: Deploy
id: deployment
@@ -0,0 +1,34 @@
name: CD Europe 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=eu --latest
@@ -0,0 +1,34 @@
name: CD Europe Ratings Leaderboards
on:
schedule:
- cron: "0 23 * * *"
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=eu
@@ -1,4 +1,4 @@
name: Host
name: CD Host
on:
schedule:
@@ -12,24 +12,21 @@ concurrency:
jobs:
host:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- name: Use node
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
node-version: 18
- name: Download release assets
run: |
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
| grep "browser_download_url.*dist.zip" \
| grep "browser_download_url.*bot.zip" \
| cut -d : -f 2,3 \
| tr -d \")
curl -L -o dist.zip $DOWNLOAD_URL
unzip dist.zip
curl -L -o bot.zip $DOWNLOAD_URL
unzip bot.zip
- name: Run bot
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps --trace-warnings index.cjs
@@ -0,0 +1,34 @@
name: CD North America 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=na --latest
@@ -0,0 +1,34 @@
name: CD North America Ratings Leaderboards
on:
schedule:
- cron: '0 5 * * *'
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=na
@@ -1,4 +1,4 @@
name: CI Build
name: CI Build Bot
on: [push, pull_request]
@@ -11,18 +11,14 @@ jobs:
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- name: Get code
- name: Checkout
uses: actions/checkout@v3
- name: Use node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
node-version: 18
cache: 'npm'
- name: Install Yarn
@@ -38,10 +34,10 @@ jobs:
run: yarn
- name: Build
run: yarn build
run: yarn build:bot
- name: Archive build
uses: actions/upload-artifact@v3
with:
name: dist
path: dist
name: bot
path: dist/bot
+43
View File
@@ -0,0 +1,43 @@
name: CI Build Docs
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install Yarn
run: npm install --location=global yarn
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Build
run: yarn build:docs
- name: Archive build
uses: actions/upload-artifact@v3
with:
name: docs
path: dist/docs
+43
View File
@@ -0,0 +1,43 @@
name: CI Build Website
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install Yarn
run: npm install --location=global yarn
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Build
run: yarn build:website
- name: Archive build
uses: actions/upload-artifact@v3
with:
name: website
path: dist/website
+6 -3
View File
@@ -5,9 +5,12 @@ package-lock.json
/playwright-report/
/playwright/.cache/
dist/
dist.*
yarn-error.log
dist.zip
commands.txt
/test.*
test.*
/docs/.vitepress/dist
/docs/.vitepress/cache
/docs/.vitepress/cache
servers.csv
.next/
tsconfig.tsbuildinfo
-3
View File
@@ -1,3 +0,0 @@
tasks:
- init: yarn
- command: yarn run pw
+12 -1
View File
@@ -6,13 +6,19 @@
"blitzstars",
"cleandist",
"customperiod",
"deburr",
"diacriticless",
"Everytime",
"fullstats",
"fuzzysort",
"kranvagn",
"Leaderboard",
"leaderboards",
"outfile",
"ownedtanks",
"playerachievements",
"playerinfo",
"ratingsleaderboard",
"Resvg",
"roboto",
"searchclans",
@@ -22,16 +28,21 @@
"Smri",
"startcase",
"statsfull",
"Stringifying",
"strvk",
"subnation",
"tankaverages",
"tankopedia",
"tankstats",
"tresabhi",
"Unicum",
"Unregistering",
"vickers",
"vitepress",
"wargaming",
"winrate"
"webp",
"winrate",
"zustand"
],
"deno.enable": false,
"deno.unstable": false
+11 -11
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+28 -7
View File
@@ -1,21 +1,42 @@
# blitzkrieg
A Discord bot for anything and everything related to World of Tanks Blitz.
Tools for everything World of Tanks Blitz
## Invite the Bot
## Invite the bot
Join the [Discord server](https://discord.gg/nDt7AjGJQH), click on the bot's profile, and click "Add to Server".
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
## Using
![Blitzkrieg application directory](https://i.imgur.com/JlEA0J1.png)
### Installing dependencies
## Visit the website
### https://blitz-krieg.vercel.app/
![Blitzkrieg website](https://i.imgur.com/QOXUP63.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
```
### Running
### Create `.env`
```env
GH_TOKEN={YOUR_GITHUB_TOKEN_HERE}
DISCORD_TOKEN={YOUR_DISCORD_TOKE_HERE}
```
### Run the command
```bash
yarn start discord-token=<token> wargaming-application-id=<id>
yarn dev:bot
```
+22 -8
View File
@@ -1,25 +1,39 @@
import { DefaultTheme, defineConfig } from 'vitepress';
import { compareVersions } from 'compare-versions';
import { readdirSync } from 'fs';
import { parse } from 'path';
import { defineConfig } from 'vitepress';
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
export default defineConfig({
title: 'blitzkrieg',
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
base: '/blitzkrieg/',
outDir: '../dist/docs',
themeConfig: {
nav: [
{ text: 'Home', link: '/' },
{ text: 'Examples', link: '/markdown-examples' },
{ text: 'Guide', link: '/guide/about' },
],
sidebar: getSidebar({
collapsed: false,
contentDirs: ['guide', 'legal', 'changelogs'],
contentRoot: '/docs/',
}) as DefaultTheme.Sidebar,
sidebar: [
...getSidebar({
collapsed: false,
contentDirs: ['guide', 'legal'],
contentRoot: '/docs/',
}),
{
text: 'Changelogs',
items: readdirSync('./docs/changelogs')
.map((file) => parse(file).name)
.sort(compareVersions)
.map((name) => ({ text: name, link: `/changelogs/${name}.html` })),
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
],
},
});
+6 -6
View File
@@ -1,16 +1,16 @@
// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import Theme from 'vitepress/theme'
import './style.css'
import { h } from 'vue';
import Theme from 'vitepress/theme';
import './style.css';
export default {
extends: Theme,
Layout: () => {
return h(Theme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
});
},
enhanceApp({ app, router, siteData }) {
// ...
}
}
},
};
+1 -2
View File
@@ -7,7 +7,7 @@
* Colors
* -------------------------------------------------------------------------- */
:root {
:root {
--vp-c-brand: #646cff;
--vp-c-brand-light: #747bff;
--vp-c-brand-lighter: #9499ff;
@@ -88,4 +88,3 @@
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand) !important;
}
+37
View File
@@ -0,0 +1,37 @@
# Blitzkrieg 1.10.0
This update addresses many readability issues that users have reported over time.
## Changes
- `/ownedtanks`
- Tanks are now displayed in two columns
- Collector and premium tanks now have their own colors
- Tank types are also displayed
- A stylistic image of the tank is also displayed for each tank to the right
- `/fullstats`
- Was previously called `/statsfull`; this was changed to avoid the clutter that would show up when searching for `/stats`
- Added four catagories in which all the stats are sorted into
- Catagories now have backgrounds to compartmentalize them
- `/evolution`
- Gave the axis markings a high contrast color
- Added a label for the x axis affirming that it is plotted over the number of battles
- `/today`
- Added a border to all the rows to compartmentalize them
- This command can now generate embeds too
- Removed survivability
- Added a limit option to cut off after a certain number of tanks played (this is mainly intended to be used by streamers to limit the content of the embeds)
- Tanks are now displayed in order of the most recent battle
- `/help`
- A significant amount of subcommands were added
- These new subcommands now return normal Discord messages instead of embeds in favor of readability
- The contents of the replies are directly fetched from the Blitzkrieg documentation
- Time zones
- What is considered a "day's worth of battles" has been adjusted as the "reset time" was the same for all Blitz servers
- The reset time is now dependent on the server of the player
- Use `/help timezones` to learn more
## Technical Changes
- All themes have been merged to leverage Bepaint's suffixes
- Switched to Node module resolution
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.10.1
This is a bit of an experiment.
## Technical Changes
- Every midnight in the Central North American Time, a permanent record of the entire ratings leaderboard will be stored for North America
+10
View File
@@ -0,0 +1,10 @@
# Blitzkrieg 1.10.2
The experiment in Blitzkrieg `1.10.1` was a bit of a failure so here are a few changes.
## Fixes
- North American leaderboards are now saved in the `na` directory instead of `asia`
- Dependencies are now installed before the script is ran
- Added leaderboards for all regions
- All regions now have their own separate cron timings
+22
View File
@@ -0,0 +1,22 @@
# Blitzkrieg 1.11.0
I am going on vacation so I thought I'd publish this a bit prematurely without all the planned features.
## Changes
- Added `/ratings`
- This command currently only has the `leagues` subcommand group
- Displays the top players in a league
- Also shows the changes in their positions and scores since last region midnight
- Use `/help timezones` to know when Blitzkrieg records the leaderboard
- Blitzkrieg now internally generates and keeps a permanent record of all leaderboards updated every `UTC+0` midnight
- This approach will allow us to capture the absolute latest state of the leaderboard before it is wiped for the next season
## Fixes
- Removed region "Europe" in `/verify` example
## Technical Changes
- Add scripts were upgraded to TypeScript
- Secrets now only throw errors if they're used when not provided
+54
View File
@@ -0,0 +1,54 @@
# Blitzkrieg 1.12.0
_Houston, permission to return to Earth? A new update dropped._
## Features
- Added `multi-tank` filters to `/stats` and `/full-stats`
- This lets you pull up the stats by combining filters of `nation`, `tank-type`, `tier`, and `tree-type`
- Added `/ratings neighbours`
- Lets you see your rank and the neighbours around you
- Added a bunch more `/today` parameters (thank you for the suggestions, HisRoyalFatness!)
- `cutoff`: the max number of rows to show (excluding the total row)
- `maximized`: the max number of rows that show the full non-collapsed stats (default: `4`)
- `show-total`: show the total row or not (default: `true`)
- Tank search is now accent-tolerant
## Changes
- `/ratings` has been moved out of preview and is now accessible to all
- `/today` now has a more appealing look with more efficient space management
- `/stats`, `/full-stats`, and `/evolution` now have less convoluted commands
- All periods now fall under `period` and `custom`
- `period` has preset periods like `today`, `career`, `30`, `60`, `90`
- `custom` lets you set a start and end day
- Added dashes to command names, subcommands, options, etc. to make them more readable
- Titlebar
- Removed the Blitzkrieg logo
- Removed region
- Center aligned items
- Footers are now center alinged when they do no have footer text on the left
- Moved several title discriminators into the description
- `/ratings` is now data-driven
- Embeds now auto refresh every `30` seconds instead of `60`
- Added numbers besides roman numerals in tier choice
- I forgot to implement this in places other than `/owned-tanks`... oh well!
- Increased precision of winrate in `/evolution` to `2` decimals
## Fixes
- Removed duplicate "Efficiency" cards in `/full-stats`
- Titlebar gap is now `4px` just like Figma
- Renamed `/statsfull` to `/full-stats` in the `/stats` footer
- Rewards in `/ratings` are now properly aligned
- `/ratings` now uses the correct source of information per region instead of getting it from a cached North American source
- Search parameters are now passed onto the URL parser correctly
- Custom date ranges now work correctly once again as the start is now the maximum days ago from today and the end is the minimum
## Technical Changes
- All generated images are now `480px` wide instead of `640px` to combat the unnecessary amounts of negative space
- Changed all relevant typings to be sourced from `stats` instead of `full-stats`
- Added minimum character limit to clan search
- Username auto-completing now only takes in regex-accepted parts of the input
- Search parameters are now longer automatically generated by inferring command interactions
+8
View File
@@ -0,0 +1,8 @@
# Blitzkrieg 1.12.1
A duo of nasty bugs fixed.
## Fixes
- Fixed a tank index and id mismatch
- Reverted username pattern infrence in command username inputs
+29
View File
@@ -0,0 +1,29 @@
# Blitzkrieg 1.12.2
A much needed room service is here!
## Changes
- Tier selection is now easier
- Tier options across all commands have been unified to a single style
- Tiers are now sorted from `10` to `1`
- Tiers now show up in the format `Tier {tier_number} - {tier_roman_numeral}` (example: `Tier 8 - VIII`)
- Removed powered by footer as links are provided regardless
- Changed invite instructions to instead use application directory
## Fixes
- Guide link on the website is now fixed
- Versions now show up in the correct order in the navigation bar
- Added Blitzkrieg's GitHub link instead of the default Vitepress
- Changed examples to guide in the navigation bar
- Clan name and icon now show up with multi-tank filters
- Removed a random `console.log`
## Technical Changes
- Added `knip`
- Removed a lot of unused files and dependencies
- Updated all dependencies to their latest
- Code split `bot.ts`
- `client` and `octokit` now reside separately
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.12.3
3-day downtime fixed!
## Changes
- Split `/ratings` into `/ratings current` and `/ratings archive`
- `current` now properly handles when there is no ongoing ratings season
- `archive` displays older archived seasons
## Fixes
- The bot is now no ratings seasons tolerant
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.12.4
Added very badly-coded temporary `/clan-event` command (without linting)
## Changes
- Added `/clan-event` command
+12
View File
@@ -0,0 +1,12 @@
# Blitzkrieg 1.12.5
Some patch up work
## Changes
- `/clan-event` now is more legible and comes with an approximation warning
## Fixes
- `/clan-event` now uses a more optimized way of fetching large data
- This should fix the issue where too many people using the command broke it
+12
View File
@@ -0,0 +1,12 @@
# Blitzkrieg 1.12.6
Stops making the bot lie!
## Changes
- The approximation warning for `/clan-event` has been made more concise
- Players with `0` wins are now omitted
## Fixes
- Players who haven't player within the current day are now omitted
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.12.7
A few fixes and clarifications
## Changes
- `/clan-event`
- Made statement about the limited publicly available data
- Added a statement about reset times
## Fixes
- Commands with images now once again work
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.12.8
Blitzkrieg is a completely free service and I earn no revenue from it. To keep it sustainable, I have to rely on free services. Unfortunately for embeds, seems like I have exceeded my limits. Before this service starts charging me, I have shut down embeds. They'll hopefully be back in 1.13 alongside the website! Only the future will tell...
## Changes
- Removed all embed buttons
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.12.9
Now that the clan event is over, we don't need `/clan-event`. This command may be reintroduced in the future as a part of a different command/feature and will definitely be re-enabled during the next clan event.
## Changes
- Disabled the `/clan-event` command
+32
View File
@@ -0,0 +1,32 @@
# Blitzkrieg 1.13.0
One of the biggest updates ever! This changelog's size does not truly represent the work (and brain cells) spent.
## Changes
- Added Blitzkrieg homepage
- Added session tracker
- Search players up and select one
- This starts tracking your games from that point
- This is displayed in a similar fashion to the `/today` command
- Updated the permissions doc
- This now includes all 3 of the needed permissions for full operation
- Also renamed the permissions to better match Discord
- Renamed `/help` to `/about`
- Renamed `/help about` to `/about bot`
## Fixes
- Added `/ratings` error handling for `null` clans, players, and rewards
## Technical Changes
- Removed all server assets
- Simplified build script to only compile the bot
- Upgraded the build script to TypeScript
- Moved docs build to `dist/docs`
- Split `tsconfig.json` for Next.js and esbuild
- Removed Gitpod config
- Switched to `.env`
- Made Wargaming application ID public
- Started migration to Radix UI
+17
View File
@@ -0,0 +1,17 @@
# Blitzkrieg 1.13.1
The quickest minor update in town 🤠
## Changes
- Added a title, description, and icon to the website
- Added display options to the session tracker
## Fixes
- Center aligned stats in breakdown to work better on low width devices
- Center aligned the Blitzkrieg logo in the navbar
## Technical Changes
- Added up-to-date information to `README.md` and embed docs
+12
View File
@@ -0,0 +1,12 @@
# Blitzkrieg 1.9.4
Haha, I lied. More fixes.
## Fixes
- Tank names once again show when searching for tank stats
- Tier weights now ignore tanks without entries in the `tankopedia`
## Technical Changes
- All `tankopedia` derivatives are now promises
+9
View File
@@ -0,0 +1,9 @@
# Blitzkrieg 1.9.5
Hopefully this is the last round of fixes for Blitzkrieg `1.9`.
## Fixes
- All commands have been tweaked to work even with a lack of a tankopedia entry
- This should fix test tanks not showing up
- `/today` now properly preserves all WN8 entries
+23
View File
@@ -0,0 +1,23 @@
# Embeds
Embeds are URLs generated by commands or tools that represent the "naked" version of the respective command or tool
## Creating Embeds
1. Use a command or tool and click on "Embed" if it's available
2. Make sure to tweak the settings of the command or tool because embeds replicate everything
## Using Embeds in Streaming Softwares
1. Click on the "Embed" button
- If this was generated by a command, copy the URL from the browser
- If this was generated by a tool, the URL has been copied to your clipboard already
2. Paste this URL into your streaming service as a "Browser Source"
- Tool embeds use local storage so data won't be transferred from you browser to your streaming software
- Make sure to visit the tool's page in your streaming software (using the "Interact" feature) and then open the embed
- Set your browser source refresh rate to `1Hz` because that's how fast embeds refresh
## How Embeds Behave
- They load like any normal website
- They auto refresh every `1s`
+10
View File
@@ -0,0 +1,10 @@
# Introduction
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
- Track statistics
- Find inactive players in a clan
- Lookup owned tanks
- Create embeds for streams
- Track a player/tanks statistical evolution
- ...and a variety of other similar things
+3
View File
@@ -0,0 +1,3 @@
# Inviting the Bot
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
+2 -1
View File
@@ -2,5 +2,6 @@
The following Discord permissions are required by the bot to ensure full functionality:
- Manage nicknames
- Read messages / view channels
- Attach files
- View channels
+9
View File
@@ -0,0 +1,9 @@
# Timezones
What is considered a "day's worth of games" is determined by the reset time. The reset time is generally at midnight everyday after which newer games are counted towards the next day. Reset times vary from region to region:
- North America: `UTC-5:00` (Central North American Time midnight)
- Europe: `UTC+1:00` (Central European Time midnight)
- Asia: `UTC+7:00` (Central Asia Standard Time midnight)
**Reset times are different from Blitz Stars** (Blitz Stars resets at 5:00 am locally relative to the user opening their website). Inconsistencies with Blitz Stars only occur with sleep-deprived players and players who play in the wrong region.
+1 -1
View File
@@ -15,7 +15,7 @@ hero:
link: guide/install
- theme: alt
text: Guide
link: /guide
link: /guide/about
features:
- title: Clean and Efficient UI
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+12
View File
@@ -0,0 +1,12 @@
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
const withVanillaExtract = createVanillaExtractPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {
...withVanillaExtract(),
distDir: 'dist/website',
};
export default nextConfig;
+2 -1
View File
@@ -1,5 +1,6 @@
{
"main": "server.cjs",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node --enable-source-maps --trace-warnings server.cjs"
}
+42 -23
View File
@@ -1,48 +1,67 @@
{
"name": "blitzkrieg",
"version": "1.9.3",
"description": "",
"version": "1.13.1",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
"scripts": {
"build:bot": "node scripts/build --build=bot",
"build:server": "node scripts/build --build=server",
"build:bot": "vite-node --script scripts/build",
"build:website": "next build",
"build:docs": "vitepress build docs",
"build": "node scripts/build --build=all",
"dev:bot": "yarn build:bot --dev && yarn start:bot",
"dev:server": "yarn build:server --dev && yarn start:server",
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"dev:bot": "yarn build:bot && yarn start:bot",
"dev:docs": "vitepress dev docs",
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
"dev:website": "next dev",
"start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs",
"start:website": "next start",
"host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
"zip": "zip -r dist.zip dist",
"lint": "npx tsc"
"lint": "npx tsc",
"lint:website": "next lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"knip": "knip",
"update": "yarn upgrade --latest"
},
"license": "MIT",
"dependencies": {
"@radix-ui/colors": "^0.1.8",
"@octokit/rest": "^20.0.1",
"@radix-ui/colors": "^3.0.0-rc.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^1.1.2",
"@resvg/resvg-js": "^2.4.1",
"@vanilla-extract/css": "^1.13.0",
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.9.0",
"esbuild": "^0.18.10",
"escape-html": "^1.0.3",
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"esbuild": "^0.19.3",
"express": "^4.18.2",
"fuzzysort": "^2.0.4",
"immer": "^10.0.2",
"lodash": "^4.17.21",
"markdown-escape": "^2.0.0",
"next": "^13.5.2",
"react": "^18.2.0",
"satori": "^0.9.1",
"vitepress-plugin-auto-sidebar": "^1.1.0"
"react-dom": "^18.2.0",
"react-fuzzysort": "^1.0.1",
"satori": "^0.10.8",
"vite-node": "^0.34.5",
"vitepress": "^1.0.0-rc.20",
"vitepress-plugin-auto-sidebar": "^1.1.0",
"zustand": "^4.4.1"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.4.2",
"@total-typescript/ts-reset": "^0.5.1",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17",
"@types/lodash": "^4.14.195",
"@types/express": "^4.17.18",
"@types/lodash": "^4.14.199",
"@types/markdown-escape": "^1.1.0",
"@types/node": "^20.2.1",
"@types/react": "^18.2.7",
"typescript": "^5.0.4",
"vitepress": "^1.0.0-beta.3"
"@types/node": "^20.6.5",
"@types/react": "^18.2.22",
"@vanilla-extract/next-plugin": "^2.3.1",
"compare-versions": "^6.1.0",
"knip": "^2.27.1",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

-64
View File
@@ -1,64 +0,0 @@
import { build } from 'esbuild';
import { copyFile, mkdir, rm } from 'fs/promises';
import { argv } from 'process';
const isProd = !argv.includes('--dev');
const buildAll = argv.includes('--build=all');
const buildBot = argv.includes('--build=bot') | buildAll;
const buildServer = argv.includes('--build=server') | buildAll;
if (isProd) {
// only remove in production to ensure no time is wasted in dev
console.log('🟡 Removing dist...');
await rm('dist', { recursive: true, force: true });
console.log('✅ Dist removed');
console.log('🟡 Creating dist...');
await mkdir('dist');
console.log('✅ Dist created');
console.log('🟡 Copying package.json...');
copyFile('package.dist.json', 'dist/package.json').then(() =>
console.log('✅ package.json copied'),
);
}
const commonOptions = {
platform: 'node',
loader: {
'.node': 'copy',
'.ttf': 'file',
},
bundle: true,
sourcemap: true,
minifyIdentifiers: false, // causes errors
minifySyntax: isProd,
minifyWhitespace: isProd,
logLevel: isProd ? 'info' : 'silent',
};
if (buildBot) {
console.log('🟡 Building bot...');
build({
...commonOptions,
entryPoints: ['src/bot.ts'],
outfile: 'dist/bot.cjs',
}).then(() => {
console.log('✅ Bot built');
});
}
if (buildServer) {
console.log('🟡 Building server...');
build({
...commonOptions,
entryPoints: ['src/server.ts'],
outfile: 'dist/server.cjs',
}).then(() => {
console.log('✅ Server built');
});
}
+27
View File
@@ -0,0 +1,27 @@
import { build } from 'esbuild';
console.log('Building bot...');
await build({
entryPoints: ['src/bot.ts'],
outfile: 'dist/bot/index.cjs',
platform: 'node',
tsconfig: 'tsconfig.esbuild.json',
loader: {
'.node': 'copy',
'.ttf': 'file',
},
format: 'cjs',
jsx: 'transform',
bundle: true,
sourcemap: true,
minifyIdentifiers: false, // cause goofy errors
minifySyntax: true,
minifyWhitespace: true,
logLevel: 'info',
});
console.log('Built bot');
+144
View File
@@ -0,0 +1,144 @@
import { Octokit } from '@octokit/rest';
import { rm, writeFile } from 'fs/promises';
import { argv, env } from 'process';
import {
RatingsInfo,
RatingsNeighbors,
RatingsPlayer,
} from '../src/commands/ratings';
import commitMultipleFiles from './commitMultipleFiles.js';
/*
* 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
*/
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
const publish = argv.includes('--publish');
const latest = argv.includes('--latest');
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
if (!server) throw new Error('Region parameter not specified');
const NEIGHBORS = 1000;
const PLAYERS = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
)
.then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) {
throw new Error('No current ratings season running');
}
const players: Record<number, RatingsPlayer> = {};
const leaderboard: BlitzkriegRatingsLeaderboard = [];
let registered = 0;
async function branchFromPlayer(id: number) {
const { neighbors } = await fetch(
`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];
const branches: Promise<void>[] = [];
if (!players[firstPlayer.number])
branches.push(branchFromPlayer(firstPlayer.spa_id));
if (!players[lastPlayer.number])
branches.push(branchFromPlayer(lastPlayer.spa_id));
neighbors.forEach((neighbor) => {
if (players[neighbor.number]) return;
players[neighbor.number] = neighbor;
registered++;
});
console.log(
`Registered ${registered} / ${PLAYERS} (${(
(registered / PLAYERS!) *
100
).toFixed(2)}%)`,
);
await Promise.all(branches);
}
async function branchFromLeague(id: number) {
const { result } = await fetch(
`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];
await Promise.all([
branchFromPlayer(firstPlayer.spa_id),
branchFromPlayer(lastPlayer.spa_id),
]);
}
console.log('Branching from leagues...');
await Promise.all([
branchFromLeague(0),
branchFromLeague(1),
branchFromLeague(2),
branchFromLeague(3),
branchFromLeague(4),
]);
console.log('Converting to an array...');
for (let index = 0; index < PLAYERS; index++) {
const listing = players[index + 1];
if (!listing) console.warn(`Position ${index + 1} had no player`);
leaderboard[index] = {
id: listing.spa_id,
score: listing.score,
};
}
const leaderboardJSON = JSON.stringify(leaderboard);
if (publish) {
const octokit = new Octokit({ auth: env.GH_TOKEN });
const info = await fetch(
`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 infoJSON = JSON.stringify(info);
console.log(`Publishing to season ${info.current_season}...`);
commitMultipleFiles(
octokit,
'tresabhi',
'blitzkrieg-db',
'main',
new Date().toString(),
[
{ path: infoPath, content: infoJSON },
{ path: leaderboardPath, content: leaderboardJSON },
],
);
} else {
console.log('Writing to disk...');
await rm('dist.json', { force: true });
await writeFile('dist.json', leaderboardJSON);
}
+70
View File
@@ -0,0 +1,70 @@
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,
});
}
+6
View File
@@ -0,0 +1,6 @@
import { NextResponse } from 'next/server';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
export async function GET() {
return NextResponse.json(await tankAverages);
}
+6
View File
@@ -0,0 +1,6 @@
import { NextResponse } from 'next/server';
import { tankopedia } from '../../../core/blitz/tankopedia';
export async function GET() {
return NextResponse.json(await tankopedia);
}
+310
View File
@@ -0,0 +1,310 @@
'use client';
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 { tankAverages } from '../../../core/blitzstars/tankAverages';
import { useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
const REFRESH_RATE = 1 / 5;
export default function SessionPage() {
const session = useSession();
const since = session.isTracking ? new Date(session.time) : undefined;
const [diff, setDiff] = useState<
| {
careerWN8: number;
list: {
stats: IndividualTankStats;
tankopedia?: TankopediaEntry;
career: IndividualTankStats;
currentWN8?: number;
careerWN8?: number;
}[];
}
| undefined
>(undefined);
const sessionBattles = diff?.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.battles,
0,
);
const tankStatsArray = session.isTracking
? (Object.values(session.tankStats) as IndividualTankStats[])
: [];
const sessionWinrate = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.wins,
0,
) / sessionBattles!
: 0;
const careerBattles = tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.battles,
0,
);
const careerWinrate =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.wins,
0,
) / careerBattles;
const currentDamage = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.damage_dealt,
0,
) / sessionBattles!
: 0;
const careerDamage =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.damage_dealt,
0,
) / careerBattles;
useEffect(() => {
async function recalculateDiff() {
if (session.isTracking) {
const { id, region } = session;
const careerRaw = await getWargamingResponse<TanksStats>(
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const career = careerRaw[id].reduce<
Record<number, IndividualTankStats>
>(
(accumulator, curr) => ({
...accumulator,
[curr.tank_id]: curr,
}),
{},
);
const awaitedTankopedia = await tankopedia;
const awaitedTankAverages = await tankAverages;
const careerWN8 =
careerRaw[id].reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles *
calculateWN8(awaitedTankAverages[tank_id].all, all)
: 0),
0,
) /
careerRaw[id].reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles
: 0),
0,
);
setDiff({
careerWN8,
list: (
Object.values(
diffNormalizedTankStats(session.tankStats, career),
) as IndividualTankStats[]
).map((stats) => {
return {
stats,
tankopedia: awaitedTankopedia[stats.tank_id],
career: career[stats.tank_id],
currentWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
stats.all,
)
: undefined,
careerWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
career[stats.tank_id].all,
)
: undefined,
};
}),
});
}
}
recalculateDiff();
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
return () => clearInterval(interval);
}, [session]);
const totalRowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: sessionBattles?.toLocaleString(),
career: tankStatsArray
.reduce((accumulator, stats) => accumulator + stats.all.battles, 0)
.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: sessionBattles ? `${(100 * sessionWinrate).toFixed(2)}%` : '--',
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
},
wn8: (() => {
if (!diff) return undefined;
const battlesWithWN8 = diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
(currentWN8 === undefined ? 0 : stats.all.battles) + accumulator,
0,
);
const currentWN8 =
diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
stats.all.battles * (currentWN8 ?? 0) + accumulator,
0,
) / battlesWithWN8!;
return {
title: 'WN8',
current: battlesWithWN8
? Math.round(currentWN8!).toLocaleString()
: '--',
percentile: battlesWithWN8 ? getWN8Percentile(currentWN8!) : undefined,
career: Math.round(diff.careerWN8).toLocaleString(),
};
})(),
damage: {
title: 'Damage',
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
current: sessionBattles
? Math.round(currentDamage).toLocaleString()
: '--',
career: Math.round(careerDamage).toLocaleString(),
},
none: undefined,
};
return (
<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];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta && rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
)}
{diff?.list
.sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time)
.map(({ stats, 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,
};
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];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta && rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
);
})}
</Breakdown.Root>
);
}
+57
View File
@@ -0,0 +1,57 @@
'use client';
import { Flex, Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css';
import { config } from 'dotenv';
import { Roboto_Flex } from 'next/font/google';
import { usePathname } from 'next/navigation';
import { ReactNode } from 'react';
import Navbar from '../components/Navbar';
config();
interface RootLayoutProps {
children: ReactNode;
}
const robotoFlex = Roboto_Flex({
subsets: ['latin'],
display: 'swap',
});
export default function RootLayout({ children }: RootLayoutProps) {
const pathname = usePathname();
const isEmbed = pathname.split('/')[1] === 'embeds';
return (
<html lang="en" className={robotoFlex.className}>
<head>
<title>Blitzkrieg</title>
<meta
name="description"
content="🎉 Tools for everything World of Tanks Blitz"
/>
</head>
<body
style={{
margin: 0,
background: isEmbed
? 'transparent'
: 'url(https://i.imgur.com/PhS06NJ.png)',
}}
>
<Theme
appearance="dark"
panelBackground="translucent"
radius="full"
accentColor="blue"
>
{!isEmbed && <Navbar />}
<Flex direction="column">{children}</Flex>
</Theme>
</body>
</html>
);
}
+15
View File
@@ -0,0 +1,15 @@
import { styleVariants } from '@vanilla-extract/css';
import { theme } from '../stitches.config';
export const tool = styleVariants({
enabled: {
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
':hover': {
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
},
},
disabled: {
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
},
});
+94
View File
@@ -0,0 +1,94 @@
'use client';
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { Flex, Text, TextField } from '@radix-ui/themes';
import Link from 'next/link';
import { useRef } from 'react';
import { Search, SearchItem } from 'react-fuzzysort';
import PageWrapper from '../components/PageWrapper';
import { TOOLS } from '../constants/tools';
import { theme } from '../stitches.config';
import * as styles from './page.css';
export default function Page() {
const input = useRef<HTMLInputElement>(null);
return (
<PageWrapper>
<TextField.Root>
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
<TextField.Input ref={input} placeholder="Search tools..." />
</TextField.Root>
<Flex gap="3" wrap="wrap" align="center" justify="center">
<Search
input={input}
list={TOOLS.map(
(tool) =>
({
node: (
<Link
href={`/tools/${tool.id}`}
className={
tool.disabled ? styles.tool.disabled : styles.tool.enabled
}
style={{
backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
cursor: tool.disabled ? 'default' : 'pointer',
opacity: tool.disabled ? 0.25 : 1,
height: 128,
minWidth: 256,
flex: 1,
borderRadius: 8,
textDecoration: 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-end',
padding: 16,
gap: 4,
transition: `box-shadow ${theme.durations.regular}`,
}}
onClick={(event) => {
if (tool.disabled) event.preventDefault();
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{tool.title}
<CaretRightIcon style={{ width: '1em', height: '1em' }} />
</span>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{tool.disabled ? 'Coming soon!' : tool.description}
</span>
</Link>
),
query: `${tool.title} ${tool.description}`,
}) satisfies SearchItem,
)}
/>
</Flex>
<Text size="1" color="gray" align="center">
Images courtesy of Wargaming Art Department
</Text>
</PageWrapper>
);
}
+54
View File
@@ -0,0 +1,54 @@
'use client';
import { usePathname } from 'next/navigation';
import { ReactNode } from 'react';
import { TOOLS } from '../../constants/tools';
import { theme } from '../../stitches.config';
interface ToolsLayoutProps {
children: ReactNode;
}
export default function ToolsLayout({ children }: ToolsLayoutProps) {
const pathname = usePathname();
const toolId = pathname.split('/').at(-1);
const tool = TOOLS.find((tool) => tool.id === toolId);
return (
<>
<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',
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 32,
}}
>
{tool?.title}
</span>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{tool?.description}
</span>
</div>
{children}
</>
);
}
@@ -0,0 +1,66 @@
import { DropdownMenu } from '@radix-ui/themes';
import mutateSession, { useSession } from '../../../../stores/session';
export type CustomColumnDisplay =
| 'battles'
| 'winrate'
| 'wn8'
| 'damage'
| 'none';
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
interface CustomColumn {
column: number;
}
export function CustomColumn({ column }: CustomColumn) {
const customColumn = useSession((state) => state.customColumns[column]);
return (
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Column {column + 1}</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Label>Display</DropdownMenu.Label>
<DropdownMenu.RadioGroup
value={customColumn.display}
onValueChange={(event) =>
mutateSession((draft) => {
draft.customColumns[column].display =
event as CustomColumnDisplay;
})
}
>
<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>
{!NO_DELTA.includes(customColumn.display) && (
<>
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem
checked={customColumn.showDelta}
onCheckedChange={(checked) =>
mutateSession((draft) => {
draft.customColumns[column].showDelta = checked;
})
}
>
Show delta
</DropdownMenu.CheckboxItem>
</>
)}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { style } from '@vanilla-extract/css';
import { theme } from '../../../stitches.config';
export const toolBar = style({
'@media': {
'screen and (min-width: 640px)': {
flexDirection: 'row',
},
},
flexDirection: 'column',
display: 'flex',
gap: 8,
});
export const toolbarButton = style({
'@media': {
'screen and (min-width: 640px)': {
flex: 0,
},
},
flex: 1,
});
export const searchButton = style({
border: 'none',
background: 'none',
fontSize: 16,
display: 'flex',
padding: 8,
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer',
':hover': {
backgroundColor: theme.colors.componentInteractiveHover,
borderRadius: 4,
},
});
+266
View File
@@ -0,0 +1,266 @@
'use client';
import {
CaretDownIcon,
CopyIcon,
PersonIcon,
ReloadIcon,
} from '@radix-ui/react-icons';
import {
AlertDialog,
Button,
Checkbox,
DropdownMenu,
Flex,
Text,
TextField,
Tooltip,
} from '@radix-ui/themes';
import { debounce } from 'lodash';
import { ChangeEvent, 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, {
AccountListWithServer,
} from '../../../core/blitz/listPlayers';
import { useSession } from '../../../stores/session';
import { NormalizedTankStats, TanksStats } from '../../../types/tanksStats';
import SessionPage from '../../embeds/session/page';
import { CustomColumn } from './components/CustomColumn';
import * as styles from './page.css';
export default function Page() {
const input = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState<
AccountListWithServer | undefined
>(undefined);
const [showSearchResults, setShowSearchResults] = useState(false);
const handleChange = debounce(
async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.value) {
setSearchResults(await listPlayers(event.target.value));
} else {
setSearchResults(undefined);
setShowSearchResults(false);
}
},
500,
);
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);
}
return (
<PageWrapper>
<div className={styles.toolBar}>
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
<TextField.Root>
<TextField.Slot>
<PersonIcon height="16" width="16" />
</TextField.Slot>
<TextField.Input
defaultValue={session.isTracking ? session.nickname : undefined}
ref={input}
onChange={(event) => {
event.stopPropagation();
if (event.target.value) {
setShowSearchResults(true);
setSearchResults(undefined);
} else {
setShowSearchResults(false);
}
handleChange(event);
}}
onBlur={(event) => {
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 is fixed
if (showSearchResults) event.target.focus();
}}
onKeyDown={(event) => {
if (event.key === 'Escape') {
setShowSearchResults(false);
input.current?.blur();
}
}}
placeholder="Search for a player..."
/>
</TextField.Root>
<DropdownMenu.Root open={showSearchResults} modal={false}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{searchResults === undefined ? (
<DropdownMenu.Item disabled>Searching...</DropdownMenu.Item>
) : searchResults.length === 0 ? (
<DropdownMenu.Item disabled>No results</DropdownMenu.Item>
) : (
searchResults?.map(
({ account_id: id, nickname, region }, index) => (
<DropdownMenu.Item
key={id}
onClick={() => {
setShowSearchResults(false);
setSession(region, id, nickname);
}}
shortcut={REGION_NAMES[region]}
>
{nickname}
</DropdownMenu.Item>
),
)
)}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button
variant="soft"
onClick={(event) => {
if (!session.promptBeforeReset) {
event.preventDefault();
reset();
}
}}
className={styles.toolbarButton}
color="red"
>
<ReloadIcon width="16" height="16" /> Reset
</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<Flex direction="column" gap="4">
<Flex direction="column" gap="0">
<AlertDialog.Title>
Are you sure you want to reset?
</AlertDialog.Title>
<AlertDialog.Description>
This'll discard your current session and start tracking from
scratch again.
</AlertDialog.Description>
</Flex>
<Text size="2">
<Checkbox
mr="1"
onCheckedChange={(checked) =>
useSession.setState({
promptBeforeReset: !checked,
})
}
/>
Don't ask me again
</Text>
</Flex>
<Flex gap="3" mt="4">
<AlertDialog.Cancel>
<Button variant="soft" color="gray">
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={reset}>
Reset
</Button>
</AlertDialog.Action>
</Flex>
</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>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="soft" className={styles.toolbarButton}>
Options
<CaretDownIcon />
</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>
</DropdownMenu.Root>
</div>
</div>
<SessionPage />
</PageWrapper>
);
}
+4 -14
View File
@@ -1,16 +1,6 @@
import { Client, GatewayIntentBits } from 'discord.js';
import { DISCORD_TOKEN } from './core/node/arguments.js';
import { registerErrorHandlers } from './events/error.js';
import guildMemberAdd from './events/guildMemberAdd.js';
import interactionCreate from './events/interactionCreate/index.js';
import ready from './events/ready.js';
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
import { client } from './core/discord/client';
import { secrets } from './core/node/secrets';
import { registerErrorHandlers } from './events/error';
registerErrorHandlers();
client.login(DISCORD_TOKEN);
client.login(secrets.DISCORD_TOKEN);
+66
View File
@@ -0,0 +1,66 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
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',
};
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',
};
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
Object.entries(DOC_DESCRIPTIONS).forEach(([key, value]) => {
option.addSubcommand((sub) => sub.setName(key).setDescription(value));
});
return option;
}
export const aboutCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: addDocsSubcommands(
new SlashCommandBuilder()
.setName('about')
.setDescription('All the info you need about the bot')
.addSubcommand((option) =>
option
.setName('commands')
.setDescription('List of all commands and their descriptions'),
),
),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'commands') {
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
await Promise.all(COMMANDS_RAW)
)
.filter((registry) => registry.inPublic && registry.inProduction)
.map(
(registry) =>
`- \`/${registry.command.name}\`: ${registry.command.description}`,
)
.join('\n')}`;
}
const url = `${RAW_PATH}${DOCS[subcommand as keyof typeof DOCS]}.md`;
const content = fetch(url).then((response) => response.text());
return content;
},
};
+97
View File
@@ -0,0 +1,97 @@
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,
command: new SlashCommandBuilder()
.setName('clan-event')
.setDescription('Lists all inactive players')
.addStringOption(addClanChoices),
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,
}));
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')}`,
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,
};
+7 -8
View File
@@ -1,18 +1,17 @@
import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' };
import { client } from '../bot.js';
import { tankopedia } from '../core/blitz/tankopedia.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import embedInfo from '../core/discord/embedInfo.js';
import markdownTable from '../core/discord/markdownTable.js';
import getClientId from '../core/node/getClientId.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
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();
export const debugCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
+40 -26
View File
@@ -1,21 +1,21 @@
import { SlashCommandBuilder } from 'discord.js';
import calculateWN8 from '../core/blitz/calculateWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import sumStats from '../core/blitz/sumStats.js';
import { tankopedia } from '../core/blitz/tankopedia.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
import getPeriodStart from '../core/blitzstars/getPeriodStart.js';
import getTankStatsDiffed from '../core/blitzstars/getTankStatsDiffed.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import embedNegative from '../core/discord/embedNegative.js';
import embedPositive from '../core/discord/embedPositive.js';
import markdownTable, { TableInput } from '../core/discord/markdownTable.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
import { PossiblyPromise } from '../types/possiblyPromise.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 { 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 { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PossiblyPromise } from '../types/possiblyPromise';
export type SkilledClan = 'SKLLD' | 'SMRI';
@@ -26,7 +26,6 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
export const eligibleCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: false,
command: new SlashCommandBuilder()
@@ -49,7 +48,7 @@ export const eligibleCommand: CommandRegistry = {
async handler(interaction) {
const clan = interaction.options.getString('clan') as SkilledClan;
const { id, server } = await resolvePlayerFromCommand(interaction);
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}`,
);
@@ -59,24 +58,39 @@ export const eligibleCommand: CommandRegistry = {
let body = '';
if (clan === 'SKLLD') {
const tankStatsOverTime = await getTankStatsDiffed(
const { diffed } = await getDiffedTankStats(
server,
id,
getPeriodStart('30'),
getTimeDaysAgo(server, 30),
getPeriodNow(),
);
const entries = Object.entries(tankStatsOverTime);
const entries = Object.entries(diffed);
const stats = sumStats(entries.map(([, stats]) => stats));
const averageTier =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankTier = (await tankopedia)[tankId].tier;
const tankopediaEntry = (await tankopedia)[tankId];
if (!tankopediaEntry) return accumulator;
const tankTier = tankopediaEntry.tier;
return (await accumulator) + tankTier * stats.battles;
},
0,
)) / stats.battles;
)) /
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankopediaEntry = (await tankopedia)[tankId];
if (!tankopediaEntry) return accumulator;
return stats.battles;
},
0,
));
const WN8 =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
@@ -101,7 +115,7 @@ export const eligibleCommand: CommandRegistry = {
const tankId = parseInt(tankIdString);
if ((await tankopedia)[tankId]?.tier === 10) {
const tankStats = tankStatsOverTime[tankId];
const tankStats = diffed[tankId];
return (await accumulator) + tankStats.damage_dealt;
} else return accumulator;
},
@@ -112,7 +126,7 @@ export const eligibleCommand: CommandRegistry = {
const tankId = parseInt(tankIdString);
if ((await tankopedia)[tankId]?.tier === 10) {
const tankStats = tankStatsOverTime[tankId];
const tankStats = diffed[tankId];
return (await accumulator) + tankStats.battles;
} else return accumulator;
},
+89 -82
View File
@@ -1,86 +1,93 @@
import { SlashCommandBuilder } from 'discord.js';
import { CYCLIC_API } from '../constants/cyclic.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import evolution from '../renderers/evolution.js';
import { StatType } from '../renderers/statsfull.js';
import { AccountInfo } from '../types/accountInfo.js';
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 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 resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand 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';
export const evolutionCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics'),
),
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(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.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
});
return [
await evolution(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}${
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
}`,
'BlitzStars',
),
];
},
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 period = resolvePeriodFromButton(interaction);
const player = await resolvePlayerFromButton(interaction);
return await evolution(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
export const evolutionCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics'),
false,
);
resolve({
inProduction: true,
inPublic: true,
command,
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
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, {
...player,
tankId,
start,
end,
});
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',
),
];
},
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);
return await evolution(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
);
},
});
},
};
);
+114
View File
@@ -0,0 +1,114 @@
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 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 resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand 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';
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('full-stats')
.setDescription('Full in-game statistics'),
);
resolve({
inProduction: true,
inPublic: true,
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, {
...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];
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',
),
];
},
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);
return await fullStats(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
);
},
});
},
);
-36
View File
@@ -1,36 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import discord from '../../discord.json' assert { type: 'json' };
import { client } from '../bot.js';
import embedInfo from '../core/discord/embedInfo.js';
import {
CommandRegistry,
guildCommands,
publicCommands,
} from '../events/interactionCreate/index.js';
export const helpCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('help')
.setDescription('All the help you need about the bot'),
handler(interaction) {
return embedInfo(
`${client.user?.username} help`,
`**About**\n${
client.user?.username
} automates many mundane tasks and provide statistics in numerous flexible ways.\n\nMade by TresAbhi from the Skilled [SKLLD] clan.\nGitHub: https://github.com/tresabhi/blitzkrieg\nDiscord server: https://discord.gg/nDt7AjGJQH\nSkilled server: https://discord.gg/ZPvcEG7DS8\n\n**Commands**\n${(interaction.guildId ===
discord.sklld_guild_id
? [...guildCommands, ...publicCommands]
: publicCommands
)
.map((command) => `**/${command.name}**: ${command.description}`)
.join(
'\n',
)}\n\n*I am a new bot; I may make mistakes and go offline unexpectedly from time to time.*`,
);
},
};
+16 -25
View File
@@ -1,24 +1,21 @@
import { SlashCommandBuilder } from 'discord.js';
import GenericStats from '../components/GenericStats/index.js';
import NoData, { NoDataType } from '../components/NoData.js';
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js';
import getBlitzClan from '../core/blitz/getBlitzClan.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addClanChoices from '../core/discord/addClanChoices.js';
import autocompleteClan from '../core/discord/autocompleteClan.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
import { ClanInfo } from '../types/clanInfo.js';
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 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;
export const inactiveCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
@@ -35,19 +32,18 @@ export const inactiveCommand: CommandRegistry = {
),
async handler(interaction) {
const clanName = interaction.options.getString('clan')!;
const { server, id } = await getBlitzClan(clanName);
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanData = (
await getWargamingResponse<ClanInfo>(
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
)
)[id];
const memberIds = clanData.members_ids;
const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
',',
)}`,
);
@@ -69,17 +65,12 @@ export const inactiveCommand: CommandRegistry = {
<Wrapper>
<TitleBar
name={clanData.name}
nameDiscriminator="(Inactivity)"
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
description={`${threshold}+ Days • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`}
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
/>
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
{hasInactiveMembers && <GenericStats stats={inactive} />}
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper>
);
},
+107 -59
View File
@@ -1,24 +1,27 @@
import { SlashCommandBuilder } from 'discord.js';
import NoData, { NoDataType } from '../components/NoData.js';
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
import * as Tanks from '../components/Tanks/index.js';
import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js';
import getTankStats from '../core/blitz/getTankStats.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
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 getTankStats from '../core/blitz/getTankStats';
import getTreeType from '../core/blitz/getTreeType';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName';
import {
TIER_ROMAN_NUMERALS,
TankopediaEntry,
Tier,
tankopedia,
} from '../core/blitz/tankopedia.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
tankopediaInfo,
} from '../core/blitz/tankopedia';
import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
const COMP_TANKS = [
// light tanks
@@ -43,84 +46,129 @@ const COMP_TANKS = [
export const ownedTanksCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('ownedtanks')
.setName('owned-tanks')
.setDescription("Shows a player's owned tanks")
.addStringOption((option) =>
option
.setName('tier')
.setDescription('The tier you want to see')
.setChoices(
{ name: 'Tier I', value: '1' },
{ name: 'Tier II', value: '2' },
{ name: 'Tier III', value: '3' },
{ name: 'Tier IV', value: '4' },
{ name: 'Tier V', value: '5' },
{ name: 'Tier VI', value: '6' },
{ name: 'Tier VII', value: '7' },
{ name: 'Tier VIII', value: '8' },
{ name: 'Tier IX', value: '9' },
{ name: 'Tier X', value: '10' },
)
.setRequired(true),
)
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const tier = Number(interaction.options.getString('tier'));
const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account;
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 tankStats = await getTankStats(server, id);
const tanks = (
const filteredTanks = (
await Promise.all(
tankStats.map(async (tankData) => (await tankopedia)[tankData.tank_id]),
tankStats.map(async (tankData) => ({
tankopedia: (await tankopedia)[tankData.tank_id]!,
tank_id: tankData.tank_id,
})),
)
).filter((tank) => tank?.tier === tier);
).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[]> = {};
const nations: string[] = [];
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankopedia.nation] === undefined) {
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
nations.push(tank.tankopedia.nation);
} else {
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
}
});
nations.sort();
return (
<Wrapper>
{/* TODO: integrate some of these into title bar */}
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`}
nameDiscriminator={
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
}
image={
clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined
}
description={`Owned tanks • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`}
description={`Tier ${
TIER_ROMAN_NUMERALS[tier as Tier]
} tanks • ${new Date().toDateString()}`}
/>
{tanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
{tanks.length > 0 && (
<Tanks.Root>
{tanks.map((tank) => (
<Tanks.Item
key={tank.tank_id}
name={tank.name}
type={tank.type}
icon={tank.images.normal}
/>
))}
</Tanks.Root>
)}
{filteredTanks.length > 0 &&
(await Promise.all(
nations.map(async (nation) => {
const tanks = groupedTanks[nation];
const leftColumnSize = Math.ceil(tanks.length / 2);
const leftColumn = tanks.slice(0, leftColumnSize);
const rightColumn = tanks.slice(leftColumnSize);
<PoweredBy type={PoweredByType.Wargaming} />
return (
<Tanks.Root>
<Tanks.Title>
{(await tankopediaInfo).vehicle_nations[nation]}
</Tanks.Title>
<Tanks.Row>
<Tanks.Column>
{await Promise.all(
leftColumn.map(async (tank) => (
<Tanks.Item
key={tank.tank_id}
name={await resolveTankName(tank.tank_id)}
tankType={tank.type}
image={tank.images?.normal}
treeType={await getTreeType(tank.tank_id)}
/>
)),
)}
</Tanks.Column>
<Tanks.Column>
{await Promise.all(
rightColumn.map(async (tank) => (
<Tanks.Item
key={tank.tank_id}
name={await resolveTankName(tank.tank_id)}
tankType={tank.type}
image={tank.images?.normal}
treeType={await getTreeType(tank.tank_id)}
/>
)),
)}
</Tanks.Column>
</Tanks.Row>
</Tanks.Root>
);
}),
))}
</Wrapper>
);
},
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'));
+2 -3
View File
@@ -1,9 +1,8 @@
import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { CommandRegistry } from '../events/interactionCreate';
export const pingCommand: CommandRegistry<true> = {
export const pingCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
handlesInteraction: true,
+12 -15
View File
@@ -1,28 +1,25 @@
import { SlashCommandBuilder } from 'discord.js';
import { startCase } from 'lodash';
import markdownEscape from 'markdown-escape';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import embedInfo from '../core/discord/embedInfo.js';
import markdownTable, {
TableInputEntry,
} from '../core/discord/markdownTable.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountAchievements } from '../types/accountAchievements.js';
import { AccountInfo } from '../types/accountInfo.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 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';
export const playerAchievementsCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('playerachievements')
.setName('player-achievements')
.setDescription("All the player's achievements")
.addStringOption(addUsernameChoices)
.addStringOption((option) =>
@@ -38,7 +35,7 @@ export const playerAchievementsCommand: CommandRegistry = {
async handler(interaction) {
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account;
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}`,
);
+11 -12
View File
@@ -1,28 +1,27 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import embedInfo from '../core/discord/embedInfo.js';
import markdownTable from '../core/discord/markdownTable.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.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 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,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('playerinfo')
.setName('player-info')
.setDescription('Basic information about a player')
.addStringOption(addUsernameChoices),
async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account;
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}`,
);
+546
View File
@@ -0,0 +1,546 @@
import {
APIApplicationCommandOptionChoice,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
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 addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { octokit } from '../core/github/octokit';
import throwError from '../core/node/throwError';
import { CommandRegistryRaw } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
export interface RatingsPlayer {
spa_id: number;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
percentile: number;
skip: boolean;
updated_at: string;
score: number;
nickname: string;
clan_tag: string;
}
export interface LeagueTop {
result: RatingsPlayer[];
}
export type RatingsReward = {
from_position: number;
to_position: number;
count: number;
} & (
| {
type: 'vehicle';
vehicle: {
id: number;
name: string;
nation: string;
subnation: string;
use_subnation_flag: boolean;
type_slug: string;
level: number;
roman_level: string;
user_string: string;
image_url: string;
preview_url: string;
is_premium: boolean;
is_collectible: boolean;
};
}
| {
type: 'stuff';
stuff: {
name: string;
count: number;
title: string;
image_url: string;
type: string;
sizes: {};
};
}
);
export interface RatingsLeague {
title: string;
small_icon: string;
big_icon: string;
background: string;
index: number;
percentile: number;
}
export type RatingsInfo =
| {
detail: undefined;
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingsReward[];
leagues: RatingsLeague[];
}
| { detail: { error: string } };
export interface RatingsNeighbors {
neighbors: RatingsPlayer[];
}
const LEAGUES = [
{ name: 'Diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 },
];
function addSubcommands(
option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option,
) {
return option
.addSubcommand((option) =>
addOptions(
option
.setName('neighbours')
.setDescription('Your position and neighbours'),
)
.addStringOption(addUsernameChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many neighbours 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)')
.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>,
);
});
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) =>
addSubcommands(
option
.setName('current')
.setDescription('The current running Ratings season'),
),
);
resolve({
inProduction: true,
inPublic: true,
command,
async handler(interaction) {
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
| 'archive'
| 'current';
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbours';
const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined;
let titleName: string;
let titleImage: string | undefined;
let titleNameDiscriminator: string | undefined;
let titleDescription: string;
let playersBefore: number;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
let playerId: number | undefined;
let regionRatingsInfo: RatingsInfo;
if (subcommand === 'league') {
const leagueIndex = parseInt(
interaction.options.getString('league')!,
);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (subcommandGroup === 'current'
? 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');
}
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`,
);
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(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(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;
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;
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');
}
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',
);
}
const response = fetch(data.download_url!);
const players = (await response.then((response) =>
response.json(),
)) as BlitzkriegRatingsLeaderboard;
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw throwError(
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
'This player did not participate in this season or did not get past calibration.',
);
}
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(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
players = neighbors;
titleImage = neighbors
? clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined
: undefined;
titleName = neighbors
? accountInfo[id].nickname
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbours';
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.detail === undefined
? neighbors
? regionRatingsInfo.count -
neighbors[neighbors.length - 1].position
: 0
: 0;
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 midnightIndex = midnightLeaderboard?.findIndex(
(item) => item.id === player.id,
);
const midnightScore =
midnightIndex === undefined
? player.score
: midnightLeaderboard![midnightIndex].score;
const midnightPosition =
midnightIndex === undefined ? player.position : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.position}
clan={player.clan}
reward={reward}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return (
<Wrapper>
<TitleBar
name={titleName}
image={titleImage}
nameDiscriminator={titleNameDiscriminator}
description={`${titleDescription} • ${new Date().toDateString()}`}
/>
{items && (
<Leaderboard.Root>
{playersBefore > 0 && (
<Leaderboard.Gap number={playersBefore} />
)}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
</Leaderboard.Root>
)}
</Wrapper>
);
},
autocomplete: autocompleteUsername,
});
},
);
+12 -13
View File
@@ -1,23 +1,22 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addClanChoices from '../core/discord/addClanChoices.js';
import addServerChoices from '../core/discord/addServerChoices.js';
import embedInfo from '../core/discord/embedInfo.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { ClanList } from '../types/clanList.js';
import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices';
import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate';
import { ClanList } from '../types/clanList';
export const searchClansCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('searchclans')
.setName('search-clans')
.setDescription('Search clans')
.addStringOption(addServerChoices)
.addStringOption(addRegionChoices)
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
.addIntegerOption((option) =>
option
@@ -28,7 +27,7 @@ export const searchClansCommand: CommandRegistry = {
),
async handler(interaction) {
const server = interaction.options.getString('server') as BlitzServer;
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>(
@@ -36,7 +35,7 @@ export const searchClansCommand: CommandRegistry = {
);
return embedInfo(
`Clan search for "${markdownEscape(clan)}" in ${BLITZ_SERVERS[server]}`,
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
clanList.length === 0
? 'No clans found.'
: `\`\`\`\n${clanList
+12 -14
View File
@@ -1,24 +1,22 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import { usernamePattern } from '../core/blitz/listPlayers.js';
import addServerChoices from '../core/discord/addServerChoices.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import embedInfo from '../core/discord/embedInfo.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountList } from '../types/accountList.js';
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 addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate';
export const searchPlayersCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('searchplayers')
.setName('search-players')
.setDescription('Search players in a Blitz server')
.addStringOption(addServerChoices)
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
)
@@ -31,7 +29,7 @@ export const searchPlayersCommand: CommandRegistry = {
),
async handler(interaction) {
const server = interaction.options.getString('server') as BlitzServer;
const server = interaction.options.getString('region') as Region;
const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim();
@@ -44,7 +42,7 @@ export const searchPlayersCommand: CommandRegistry = {
: [];
return embedInfo(
`Player search for "${markdownEscape(name)}" in ${BLITZ_SERVERS[server]}`,
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
`\`\`\`${
players.length === 0
? 'No players found.'
+9 -9
View File
@@ -1,18 +1,17 @@
import { SlashCommandBuilder } from 'discord.js';
import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape';
import { TANK_NAMES } from '../core/blitz/tankopedia.js';
import addTankChoices from '../core/discord/addTankChoices.js';
import embedInfo from '../core/discord/embedInfo.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
import addTankChoices from '../core/discord/addTankChoices';
import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate';
export const searchTanksCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('searchtanks')
.setName('search-tanks')
.setDescription('Search tanks')
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
.addIntegerOption((option) =>
@@ -26,9 +25,10 @@ export const searchTanksCommand: CommandRegistry = {
async handler(interaction) {
const tank = interaction.options.getString('tank')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const results = go(tank, TANK_NAMES, { limit }).map(
(result) => result.target,
);
const results = go(tank, await TANK_NAMES_DIACRITICS, {
limit,
keys: ['combined'],
}).map((result) => result.obj.original);
return embedInfo(
`Tank search for "${markdownEscape(tank)}"`,
+111 -76
View File
@@ -1,85 +1,120 @@
import { SlashCommandBuilder } from 'discord.js';
import { CYCLIC_API } from '../constants/cyclic.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import stats from '../renderers/stats.js';
import { StatType } from '../renderers/statsfull.js';
import { AccountInfo } from '../types/accountInfo.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 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 resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand 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';
export const statsCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: addStatTypeSubCommandGroups(
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('stats')
.setDescription('In-game statistics'),
),
.setDescription('Regular battles statistics'),
);
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(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 path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
resolve({
inProduction: true,
inPublic: true,
return [
await stats(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
'BlitzStars',
),
];
},
command,
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
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];
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 period = resolvePeriodFromButton(interaction);
const player = await resolvePlayerFromButton(interaction);
const tierOption = interaction.options.getString('tier');
return await stats(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
);
},
};
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',
),
];
},
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);
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,
);
},
});
});
-84
View File
@@ -1,84 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import { CYCLIC_API } from '../constants/cyclic.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import statsfull, { StatType } from '../renderers/statsfull.js';
import { AccountInfo } from '../types/accountInfo.js';
export const statsFullCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('statsfull')
.setDescription('Full in-game statistics'),
),
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(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 path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
return [
await statsfull(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
'BlitzStars',
),
];
},
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 period = resolvePeriodFromButton(interaction);
const player = await resolvePlayerFromButton(interaction);
return await statsfull(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
);
},
};
+49 -18
View File
@@ -1,42 +1,73 @@
import { SlashCommandBuilder } from 'discord.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import today from '../renderers/today.js';
import { AccountInfo } from '../types/accountInfo.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,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('today')
.setDescription('A general daily breakdown of your performance')
.addStringOption(addUsernameChoices),
.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.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const path = interactionToURL(interaction, player);
const path = interactionToURL(interaction, {
...player,
cutoff,
maximized,
'show-total': showTotal,
});
return [
await today(player),
await today(player, cutoff, maximized, showTotal, false),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars',
),
];
+11 -12
View File
@@ -1,20 +1,19 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' };
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import embedNegative from '../core/discord/embedNegative.js';
import embedPositive from '../core/discord/embedPositive.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.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 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,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
@@ -24,7 +23,7 @@ export const verifyCommand: CommandRegistry = {
async handler(interaction) {
const blitzAccount = await resolvePlayerFromCommand(interaction);
const { id, server } = blitzAccount;
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}`,
);
@@ -1,6 +1,6 @@
import { Percentile } from '../../../../../constants/percentiles.js';
import hslToHex from '../../../../../core/node/hslToHex.js';
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator.js';
import { Percentile } from '../../../../../constants/percentiles';
import hslToHex from '../../../../../core/node/hslToHex';
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator';
export enum GlowSide {
Top,
@@ -1,8 +1,8 @@
import { Percentile } from '../../../../constants/percentiles.js';
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile.js';
import { theme } from '../../../../stitches.config.js';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator.js';
import { Glow, GlowSide } from './components/Glow.js';
import { Percentile } from '../../../../constants/percentiles';
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile';
import { theme } from '../../../../stitches.config';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
import { Glow, GlowSide } from './components/Glow';
export interface WN8DisplayProps {
WN8?: number;
@@ -1,4 +1,4 @@
import { theme } from '../../../stitches.config.js';
import { theme } from '../../../stitches.config';
export enum WN8SurroundingStatAlign {
Left,
+6 -4
View File
@@ -1,9 +1,9 @@
import { AllStats, SupplementaryStats } from '../../types/accountInfo.js';
import { WN8Display } from './components/WN8Display/index.js';
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
import { WN8Display } from './components/WN8Display';
import {
WN8SurroundingStat,
WN8SurroundingStatAlign,
} from './components/WN8SurroundingStat.js';
} from './components/WN8SurroundingStat';
export interface AllStatsOverviewProps {
stats: AllStats;
@@ -69,7 +69,9 @@ export default function AllStatsOverview({
>
<WN8SurroundingStat
label="Tier"
value={supplementaryStats.tier.toFixed(2)}
value={
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
}
/>
<WN8SurroundingStat
label="Survival"
+51
View File
@@ -0,0 +1,51 @@
export function BlitzkriegWormWide() {
return (
<svg
width="74"
height="17"
viewBox="0 0 74 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M33.945 12.9858V0H36.875V7.50095L39.4608 4.63967H42.7879L36.875 10.7408V12.9858H33.945ZM39.7873 12.9858L37.0515 9.04164L38.8695 7.04314L43.2292 12.9858H39.7873Z"
fill="currentColor"
/>
<path
d="M43.8823 12.9858V4.63967H46.8123L46.8299 5.74016H46.8476C47.0712 5.3704 47.3624 5.06813 47.7213 4.83335C48.0861 4.59271 48.4979 4.47239 48.9568 4.47239C49.1216 4.47239 49.2775 4.48413 49.4246 4.50761C49.5775 4.53109 49.6835 4.55457 49.7423 4.57804V6.78783C49.6364 6.74674 49.5069 6.7174 49.354 6.69979C49.2069 6.67631 49.0421 6.66457 48.8598 6.66457C48.4773 6.66457 48.1037 6.78783 47.7389 7.03434C47.3742 7.27498 47.0888 7.59192 46.8829 7.98517V12.9858H43.8823Z"
fill="currentColor"
/>
<path
d="M50.7042 12.9858V4.63967H53.6872V12.9858H50.7042ZM50.5366 1.84883C50.5366 1.42624 50.6807 1.07115 50.969 0.78355C51.2573 0.495955 51.6691 0.35216 52.2045 0.35216C52.7399 0.35216 53.1488 0.493019 53.4313 0.774745C53.7137 1.05647 53.8549 1.41156 53.8549 1.84002C53.8549 2.26261 53.7078 2.62357 53.4136 2.9229C53.1253 3.21637 52.7164 3.3631 52.1869 3.3631C51.6574 3.3631 51.2485 3.21637 50.9602 2.9229C50.6778 2.62357 50.5366 2.26555 50.5366 1.84883Z"
fill="currentColor"
/>
<path
d="M54.9492 8.92719V8.64546C54.9492 7.34835 55.3611 6.31535 56.1848 5.54648C57.0143 4.77173 58.1175 4.38436 59.4942 4.38436C60.8475 4.38436 61.8918 4.75412 62.6272 5.49365C63.3685 6.23318 63.7392 7.24857 63.7392 8.53981V9.36739H56.5731V7.86191H60.9063V7.77387C60.9063 7.38063 60.7857 7.05488 60.5445 6.79663C60.3091 6.53838 59.9502 6.40926 59.4678 6.40926C58.9441 6.40926 58.5441 6.59708 58.2675 6.97271C57.9969 7.34835 57.8616 7.81202 57.8616 8.36374V8.97121C57.8616 9.66966 58.0086 10.2096 58.3028 10.5911C58.6029 10.9726 59.0324 11.1634 59.5913 11.1634C59.9973 11.1634 60.3385 11.0783 60.6151 10.9081C60.8975 10.732 61.1152 10.506 61.2681 10.2302L63.6686 10.9873C63.3921 11.674 62.9243 12.2169 62.2654 12.616C61.6064 13.0093 60.7239 13.2059 59.6178 13.2059C58.141 13.2059 56.9937 12.8244 56.1759 12.0614C55.3581 11.2925 54.9492 10.2478 54.9492 8.92719Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M68.399 14.8787C68.9756 14.8787 69.4874 14.7114 69.9346 14.3768C70.3876 14.0423 70.6141 13.47 70.6141 12.6601V11.9029C70.3611 12.261 70.0287 12.5515 69.6169 12.7745C69.205 12.9975 68.6608 13.1091 67.9842 13.1091C66.9369 13.1091 66.0956 12.7246 65.4601 11.9557C64.8306 11.181 64.5158 10.1773 64.5158 8.94479V8.66307C64.5158 7.43052 64.8188 6.42393 65.4248 5.64332C66.0367 4.85683 66.8898 4.46359 67.9842 4.46359C68.549 4.46359 69.0668 4.58098 69.5374 4.81575C70.0081 5.05052 70.3582 5.34692 70.5876 5.70495H70.6141L70.6318 4.63967H73.5V12.2023C73.5 13.7165 73.0852 14.8728 72.2556 15.671C71.4261 16.4751 70.3082 16.8771 68.902 16.8771H0V14.8787H68.399ZM67.8342 10.5207C67.5694 10.1157 67.437 9.59042 67.437 8.94479V8.67187C67.437 8.03212 67.5694 7.50682 67.8342 7.09597C68.0989 6.68512 68.5078 6.47969 69.0609 6.47969C69.4374 6.47969 69.7551 6.58534 70.014 6.79663C70.2788 7.00793 70.47 7.26031 70.5876 7.55377V10.0541C70.4759 10.3299 70.2876 10.5765 70.0228 10.7936C69.7581 11.0108 69.4404 11.1194 69.0697 11.1194C68.5108 11.1194 68.0989 10.9198 67.8342 10.5207Z"
fill="currentColor"
/>
<path
d="M0 12.977V0H2.92118V5.61692C3.20359 5.24715 3.5566 4.95662 3.98021 4.74533C4.40383 4.52816 4.90687 4.41958 5.48934 4.41958C6.54837 4.41958 7.40443 4.80989 8.0575 5.59051C8.71057 6.36525 9.03711 7.36303 9.03711 8.58385V8.86558C9.03711 10.0923 8.70469 11.1135 8.03985 11.9293C7.37501 12.7452 6.51307 13.1531 5.45404 13.1531C4.87157 13.1531 4.35676 13.0533 3.90961 12.8538C3.46246 12.6542 3.12122 12.3813 2.88587 12.035H2.86822L2.8594 12.977H0ZM2.92118 10.0717C3.04473 10.3534 3.23595 10.6 3.49482 10.8112C3.7537 11.0225 4.092 11.1282 4.50973 11.1282C5.02748 11.1282 5.41579 10.9169 5.67467 10.4943C5.93943 10.0658 6.07181 9.52881 6.07181 8.88318V8.61907C6.07181 7.97344 5.93943 7.45694 5.67467 7.06957C5.41579 6.67633 5.00983 6.47971 4.45678 6.47971C4.07435 6.47971 3.75075 6.58535 3.48599 6.79665C3.22712 7.00794 3.03885 7.25445 2.92118 7.53618V10.0717Z"
fill="currentColor"
/>
<path d="M10.2815 12.977V0H13.238V12.977H10.2815Z" fill="currentColor" />
<path
d="M14.9236 12.977V4.63088H17.9065V12.977H14.9236ZM14.7559 1.84002C14.7559 1.41744 14.9001 1.06234 15.1883 0.774748C15.4766 0.487152 15.8885 0.343354 16.4239 0.343354C16.9593 0.343354 17.3682 0.484217 17.6506 0.765944C17.933 1.04767 18.0742 1.40276 18.0742 1.83122C18.0742 2.25381 17.9271 2.61477 17.633 2.91411C17.3447 3.20757 16.9358 3.3543 16.4062 3.3543C15.8767 3.3543 15.4678 3.20757 15.1795 2.91411C14.8971 2.61477 14.7559 2.25675 14.7559 1.84002Z"
fill="currentColor"
/>
<path
d="M18.8332 6.73502V4.63088H24.7726V6.73502H18.8332ZM20.1217 10.3887V5.15031L20.1923 5.00945V2.04252H23.0076V9.78119C23.0076 10.3329 23.0811 10.6909 23.2282 10.8553C23.3812 11.0137 23.6195 11.093 23.943 11.093C24.096 11.093 24.2402 11.0842 24.3755 11.0666C24.5108 11.0431 24.6432 11.0049 24.7726 10.9521V12.9418C24.6138 12.9946 24.3725 13.0445 24.0489 13.0915C23.7254 13.1443 23.3253 13.1707 22.8487 13.1707C21.9779 13.1707 21.3043 12.9535 20.8277 12.5192C20.357 12.079 20.1217 11.3688 20.1217 10.3887Z"
fill="currentColor"
/>
<path
d="M25.3551 12.977V11.3307L29.1323 6.74382H25.461V4.63088H32.8125V6.33004L29.0264 10.9257H33.0684V12.977H25.3551Z"
fill="currentColor"
/>
</svg>
);
}
Loaded 100 of 229 files, more files were not shown because too many files have changed in this diff. Show more