Compare commits

...
289 Commits
Author SHA1 Message Date
tresabhi 238674bd96 groups 2026-10-01 20:11:12 -05:00
tresabhi af85fb74cb haha characteristics render! 2026-10-01 15:52:14 -05:00
tresabhi bd42cb44e4 equipment looks 2026-10-01 11:59:19 -05:00
tresabhi 1f767af0b5 angles 2026-09-30 20:59:18 -05:00
tresabhi f73ffc3c93 correct axes 2026-09-30 19:00:25 -05:00
tresabhi 5d4e0541b2 correct filtering 2026-09-30 18:41:22 -05:00
tresabhi cbf320454f various bug fixes 2026-09-30 18:38:15 -05:00
tresabhi 74fbd15c5f TankopediaCharacteristics 2026-09-30 18:22:18 -05:00
tresabhi 5cae5d1583 models shit 2026-09-30 17:56:31 -05:00
tresabhi 5c88bda07a CanvasControl 2026-09-30 17:48:48 -05:00
tresabhi 19dbd8ca2e SpacedArmorSubSpaced 2026-09-30 17:47:23 -05:00
tresabhi f17ac2b575 SpacedArmorSubExternal 2026-09-30 17:45:11 -05:00
tresabhi 5d7482b158 SpacedArmorSceneComponent 2026-09-30 17:41:46 -05:00
tresabhi 22aff6188a SpacedArmorScene 2026-09-30 17:38:27 -05:00
tresabhi 1ee6cdabe5 SpacedArmorScene 2026-09-30 17:36:51 -05:00
tresabhi 480d2226f7 versions and shit 2026-09-30 17:32:39 -05:00
tresabhi c42b109974 Merge branch 'main' into data-driven 2026-09-30 15:52:42 -05:00
tresabhi 9537349664 add dependabot 2026-09-30 15:49:19 -05:00
tresabhi 8793c9a9c9 simplify readme 2026-09-30 15:44:51 -05:00
tresabhi 4d57b43912 logs 2026-09-30 15:41:08 -05:00
tresabhi ee146b8ed9 fix wgpkg dirs 2026-09-30 13:51:13 -05:00
tresabhi 1c382022a7 proto3 2026-09-27 17:24:48 -05:00
tresabhi 593ae760e7 proto imports 2026-09-27 17:19:39 -05:00
tresabhi 127c55ec93 migrate to soapstone 2026-09-27 17:04:15 -05:00
tresabhi e5b44d4eb8 flatten armor dir 2026-09-27 16:46:22 -05:00
tresabhi e18d6f58f9 somewhat through PrimaryArmorSceneComponent 2026-09-27 16:26:28 -05:00
tresabhi 488451d71f TankopediaLoadout 2026-09-27 13:49:02 -05:00
tresabhi 558a02b6ae compile tank search page 2026-09-27 13:22:34 -05:00
tresabhi d8f6a55ee4 Merge branch 'main' into data-driven 2026-09-27 13:11:01 -05:00
TrèsAbhi 763ecf22b0 New Crowdin updates (#266) 2026-09-24 16:25:00 -05:00
TrèsAbhi 2beb084b67 Merge pull request #267 from Hitjack007/fix/session-missing-average-crash 2026-09-24 14:07:50 -07:00
tresabhi f7ca622906 correct assertions 2026-09-24 16:07:12 -05:00
tresabhi 960279bf00 huh i forgor 2026-09-23 17:37:52 -05:00
tresabhi 4247004b13 canvas 2026-09-23 17:25:51 -05:00
tresabhi 960ca4b0f4 Merge branch 'main' into data-driven 2026-09-23 17:14:22 -05:00
tresabhi 424d9944ad smarter trigger 2026-09-23 17:12:53 -05:00
tresabhi a75c47865b checkpoint 2026-09-23 17:09:43 -05:00
tresabhi c2714c4d6b tank search filters 2026-09-23 17:01:34 -05:00
tresabhi 1c505818a4 to core 2026-09-23 16:58:36 -05:00
tresabhi 444705347f imports 2026-09-23 16:51:09 -05:00
tresabhi 19556b5fc9 game strings 2026-09-23 16:45:07 -05:00
tresabhi e52f003c1b build protos indexing on first shot 2026-09-23 16:42:30 -05:00
tresabhi 4fc57d7fc1 Merge branch 'main' into data-driven 2026-09-23 16:37:39 -05:00
tresabhi 8af41b91b5 logs 2026-09-23 16:32:19 -05:00
tresabhi 347efa75fe add compatibility for string game mode ids 2026-09-23 16:31:35 -05:00
tresabhi 1619ee1e00 checkpoint 2026-09-23 15:36:22 -05:00
tresabhi a38239a6f9 tankClassOrder 2026-09-23 13:58:29 -05:00
tresabhi 9e26da3205 checkpoint 2026-09-23 13:48:50 -05:00
tresabhi 750bdd236a useCompatibility 2026-09-23 10:39:12 -05:00
tresabhi 33bb8ead3c reorganize 2026-09-23 10:25:47 -05:00
tresabhi 79e282e70e strings 2026-09-23 10:23:59 -05:00
tresabhi e911f36ea3 createTankStatus 2026-09-23 10:22:57 -05:00
tresabhi 4e470599e8 speed 2026-09-22 16:39:51 -05:00
tresabhi 5f24887535 api strings 2026-09-22 16:38:30 -05:00
tresabhi 99317c2075 hasUpgrades 2026-09-22 16:24:29 -05:00
tresabhi 99965118b3 useProtagonist 2026-09-22 16:20:39 -05:00
tresabhi 7592381b12 ComputedCharacteristics 2026-09-22 16:17:55 -05:00
tresabhi 7d02f6311f tank states 2026-09-22 16:13:27 -05:00
tresabhi 6c9ce58af5 scripts api 2026-09-22 15:36:12 -05:00
tresabhi 402b6d5113 blitz scripts 2026-09-22 15:25:25 -05:00
Mark Greene 88643d366d Fix: skip session delta entries with no tank definition or average
The /session/ page crashed when a tracked player's tank stats included
a tank with no matching tank definition or no average stats (WN8
baseline). The delta memo dereferenced `tank.id` on a possibly missing
definition and forced `average?.mu!` through as non-null, so a missing
average threw inside `compositeStats`/`calculateWN8` uncaught, breaking
the page on every load while the bad state persisted in localStorage.

Skip those entries instead of crashing: entries with no tank
definition or no average `mu` are dropped from the delta via
`flatMap`, so the rest of the session table still renders.
2026-09-23 08:21:16 +12:00
tresabhi 1733180f2c oh my god this is hideous 2026-09-22 14:52:33 -05:00
tresabhi 11c2c9749c more entries 2026-09-22 11:55:03 -05:00
tresabhi 267bb271c9 some notes 2026-09-22 11:52:39 -05:00
tresabhi 3c01056f4e implement rest 2026-09-16 09:56:13 -05:00
tresabhi 66f9c9d106 start of data driven provisions 2026-09-16 09:42:20 -05:00
tresabhi 3be91469aa strict types 2026-09-13 13:20:45 -05:00
tresabhi 491b03ad00 proto compiling 2026-09-13 12:52:25 -05:00
tresabhi 054f33a649 move protos 2026-09-13 12:36:29 -05:00
tresabhi 5b4ad95a14 clean up a bit 2026-09-06 21:06:02 -05:00
tresabhi fe7282aae7 Merge branch 'main' into data-driven 2026-09-06 20:58:34 -05:00
tresabhi 84f950ade4 fix tag0 assumption 2026-09-02 16:32:46 -05:00
tresabhi 8842283f0e some data driven characteristics 2026-08-31 20:13:31 -05:00
tresabhi b6f9ae9187 some more updates to imports 2026-08-30 15:02:01 -05:00
tresabhi b5a10fcc73 plural 2026-08-30 14:57:01 -05:00
tresabhi 4015c103cf load readdir 2026-08-30 14:56:24 -05:00
tresabhi 8c08a00d64 move server funcs to init 2 2026-08-30 14:46:14 -05:00
tresabhi 5c89bcea99 move server funcs to init 2026-08-30 14:36:51 -05:00
tresabhi 5f737d940d rename methods 2026-08-30 14:26:08 -05:00
tresabhi ec161addab some cleanup 2026-08-29 13:13:44 -05:00
tresabhi 8d7575815b no results 2026-08-28 17:05:15 -05:00
tresabhi 4a0fccc526 tank search 2026-08-28 16:59:07 -05:00
tresabhi 5477616917 cleanup 2026-08-28 16:37:06 -05:00
tresabhi ba1f530de7 back button 2026-08-28 13:55:05 -05:00
tresabhi 1f1c9f26f1 tank search ui rebuilt... somewhat 2026-08-28 10:42:57 -05:00
tresabhi b400ff6d07 compiling search 2026-08-27 11:46:55 -05:00
tresabhi edc3aba545 restore banner 2026-08-27 11:23:41 -05:00
tresabhi 1e1040816b migrate tank filters 2026-08-26 14:02:44 -05:00
tresabhi d477a3a7a4 radix composing styles 2026-08-26 13:49:45 -05:00
tresabhi 37efc2b4a4 Merge branch 'main' into data-driven 2026-08-26 10:23:18 -05:00
tresabhi a0111d8b69 intense contact shadows 2026-08-19 13:57:36 -05:00
tresabhi 5285d081e4 migrate 3 2026-08-18 13:57:01 -05:00
tresabhi de8b3959b3 simplification 2026-08-18 13:21:51 -05:00
tresabhi cfc0ebb8e5 migrate 2 2026-08-18 13:02:59 -05:00
tresabhi 9839d4eef5 migrate 1 2026-08-18 12:49:14 -05:00
tresabhi c637343593 ugh, imma remove radix 2026-08-18 12:40:46 -05:00
tresabhi f3c362dfce home page grouping 2026-08-18 12:34:27 -05:00
tresabhi 20cc6d32a4 home page list 2026-08-18 12:23:48 -05:00
tresabhi 35ed716659 simplify config 2026-08-13 13:14:41 -05:00
tresabhi bea55f1b6c clean tools 2026-08-13 13:00:36 -05:00
tresabhi f355cb9f76 proper concurrency 2026-08-13 12:34:53 -05:00
tresabhi e42576b005 proper concurrency 2026-08-13 12:30:48 -05:00
tresabhi f68198475e install clis 2026-08-13 12:15:55 -05:00
tresabhi 896ae20dce remove pages 2026-08-13 12:06:55 -05:00
tresabhi b8fdc5424d logs 2026-08-13 12:02:49 -05:00
tresabhi 204c3eacc1 i give up 2026-08-13 12:01:16 -05:00
tresabhi 4d0f21e10b zip download 2026-08-13 11:05:01 -05:00
tresabhi d3202e8607 steam downloader 2026-08-13 10:56:15 -05:00
tresabhi a438eb704f smarter max rate 2026-08-13 10:29:11 -05:00
tresabhi 1569de4475 Merge branch 'main' of https://github.com/blitzkit/blitzkit 2026-08-12 20:33:40 -05:00
tresabhi 0f022f8295 chunking based on fetch rate 2026-08-12 20:33:38 -05:00
TrèsAbhi 75c21a5129 New Crowdin updates (#261) 2026-08-12 20:25:55 -05:00
tresabhi 5092d43e8e fix has pbr 2026-08-12 13:41:59 -05:00
TrèsAbhi 56d5d5e956 Delete CNAME 2026-08-12 13:10:52 -05:00
TrèsAbhi 70eca3293b Create CNAME 2026-08-12 13:10:42 -05:00
tresabhi 4f7ee54ef7 logs 2026-08-12 12:17:20 -05:00
TrèsAbhi 269df9586d Merge pull request #265 from blitzkit/infrastructure
Infrastructure
2026-08-12 11:47:24 -05:00
tresabhi 3152c18840 simplify robots.txt 2026-08-12 11:46:45 -05:00
tresabhi 52630596b8 finalize workflows 2026-08-12 11:44:26 -05:00
tresabhi 7e184b8be0 eh whatever, this'll make it into the next update 2026-08-12 11:36:55 -05:00
tresabhi 877ba9408d some more recording 2026-08-11 20:29:50 -05:00
tresabhi 8dc9a6b46d start of recording 2026-08-11 20:11:12 -05:00
tresabhi b3e439669b start of performance 2026-08-11 20:03:57 -05:00
tresabhi 08c909fbe2 rename ids to data-ids 2026-08-11 19:46:26 -05:00
tresabhi a44e2e9866 fix tank modes not switching 2026-08-11 19:45:09 -05:00
tresabhi bae2f253bc screenshot friendly contact shadows 2026-08-11 19:37:57 -05:00
tresabhi d485a463dd orange ground 2026-08-11 14:07:57 -05:00
tresabhi 3da1ed4d6c ground and sky color 2026-08-11 11:30:36 -05:00
tresabhi 051e9ec74a ground with shadows 2026-08-11 10:58:35 -05:00
tresabhi 486171f4db shadow focus and res 2026-08-11 10:52:33 -05:00
tresabhi 72de42a9ab performant ground 2026-08-11 10:38:36 -05:00
tresabhi 404e6625c0 remmove lghts 2026-08-11 10:35:41 -05:00
tresabhi 32e4bf3fd2 fading out fragment 2026-08-11 10:16:14 -05:00
tresabhi 0d1088fefb non pbr lihging 2026-08-11 10:01:34 -05:00
tresabhi 1a17d607fb move light target up 2026-08-11 09:56:58 -05:00
tresabhi dcda627518 dvpl free dir 2026-08-11 09:45:17 -05:00
tresabhi c130da7f0e heheheha 2026-08-10 17:50:48 -05:00
tresabhi e2df4d35b8 flip lighitng and add metalness 2026-08-10 16:58:40 -05:00
tresabhi 7739dbd0a3 faster lighting 2026-08-10 16:42:35 -05:00
tresabhi 9b7aa13d61 fix occlusion baking 2026-08-10 10:39:31 -05:00
TrèsAbhi 84ecaec5d3 base 2026-08-09 22:20:53 -05:00
tresabhi 01a5f1bd47 5hr 55min 2026-08-09 22:18:36 -05:00
tresabhi de0433ca02 Merge branch 'infrastructure' of https://github.com/blitzkit/blitzkit into infrastructure 2026-08-09 22:17:43 -05:00
TrèsAbhi cf76ae7136 add dispatch 2026-08-09 22:13:07 -05:00
tresabhi ea21636c4e abstract away some code 2026-08-09 20:26:39 -05:00
tresabhi dafa834d28 some script cleanup 2026-08-09 20:10:04 -05:00
tresabhi f1a1c78588 remove stub 2026-08-09 20:08:08 -05:00
tresabhi 14d16e5daf shell files raw debug 2026-08-09 19:50:53 -05:00
tresabhi b52c1fc2d9 ok cool 2026-08-09 16:12:26 -05:00
tresabhi f45b24f7ab 5hr 30min runtime 2026-08-09 15:05:06 -05:00
tresabhi 6ae175e5e0 reset author 2026-08-09 15:02:04 -05:00
tresabhi 0df27c38a1 ids token 2026-08-09 14:58:25 -05:00
tresabhi e27cc3f6ed github actions bot 2026-08-09 14:51:26 -05:00
tresabhi 968c3957d0 60s, just for testing 2026-08-09 14:46:35 -05:00
tresabhi 577d2323a8 checkout recursive 2026-08-09 14:45:12 -05:00
tresabhi 625c6cc649 workflow for discovering ids 2026-08-09 14:44:16 -05:00
tresabhi 9bc753b512 i was going to add verification but eh, no right now... 2026-08-09 14:39:05 -05:00
tresabhi fb34e6ce2f shell file paths list 2026-08-09 14:12:46 -05:00
tresabhi f882ea29e2 total saved 2026-08-09 14:06:40 -05:00
tresabhi 62f68f1f10 add manifests 2026-08-09 13:55:15 -05:00
tresabhi 8c9a41d72b add getStaticPaths for 404 page 2026-08-09 13:35:21 -05:00
tresabhi 5bda754b6e branch names 2026-08-08 20:56:45 -05:00
tresabhi c5bb537bf2 shell paths debug 2026-08-08 20:49:39 -05:00
tresabhi c59779bedb move 404 to pages chunk 2026-08-08 20:48:06 -05:00
tresabhi 5059e382b3 some missing aliases 2026-08-08 20:45:09 -05:00
tresabhi 953570461c safer exit 2026-08-08 14:09:48 -05:00
tresabhi 6d527a7170 save on exit 2026-08-08 14:08:35 -05:00
tresabhi ffd74de79f quiet all 2026-08-08 14:02:20 -05:00
tresabhi 7c9ef9e5e0 quiet git add 2026-08-08 13:59:47 -05:00
tresabhi cd10d9ab4a some stats 2026-08-08 13:57:35 -05:00
tresabhi 578d4a3ff2 max ids 2026-08-08 13:52:07 -05:00
tresabhi 0cd01fa258 try streak detector 2026-08-08 13:42:16 -05:00
tresabhi 21b37ced8d ids update 2026-08-08T18:33:52.481Z 2026-08-08 13:33:52 -05:00
tresabhi 81856276b0 seed basing 2026-08-08 13:15:04 -05:00
tresabhi 389052c692 1 sec bro 2026-08-08 13:01:29 -05:00
tresabhi 09edd39a06 more! 2026-08-08 12:28:45 -05:00
tresabhi 5c3077033e more modern way of handling id discovery 2026-08-07 14:33:10 -05:00
tresabhi ef84d5b3b5 don't log paths 2026-08-07 13:34:56 -05:00
tresabhi d29f91afbc don't fail fast for now 2026-08-07 13:33:28 -05:00
tresabhi ab87282103 use PUBLIC_ENVIRONMENT 2026-08-07 13:33:05 -05:00
tresabhi 408417ca68 bruh i am ignorant 2026-08-07 13:30:25 -05:00
tresabhi b171ce2011 beta environment as default for now 2026-08-07 12:46:49 -05:00
tresabhi 58d0d92d4f on push 2026-08-07 12:36:10 -05:00
tresabhi 754f39f623 environment based deploys 2026-08-07 12:34:58 -05:00
tresabhi 10c8ac95a4 move api 2026-08-06 20:10:29 -05:00
tresabhi b367866a22 revert debug env 2026-08-06 20:02:09 -05:00
tresabhi b26c5137f3 aliases 2026-08-06 20:01:50 -05:00
tresabhi 13cda7aec0 environment configs 2026-08-06 19:37:07 -05:00
tresabhi d1b8bfe244 fix imports 2026-08-06 14:29:13 -05:00
tresabhi 734db0957e flatten structure 2026-08-06 14:21:14 -05:00
tresabhi 050cb7a834 chunk typo 2026-08-05 20:17:05 -05:00
tresabhi 6dbcbc2afc relative game dir 2026-08-05 20:02:31 -05:00
tresabhi 484cb8fcb5 max downloads 64 2026-08-05 19:27:51 -05:00
tresabhi e09b407e61 force bun 2026-08-05 19:22:25 -05:00
tresabhi b8742bae66 fix linting errors 2026-08-05 14:17:33 -05:00
tresabhi af58b4614d master deploy 2026-08-05 14:09:00 -05:00
tresabhi db6c4679b7 fix app id 2026-08-05 13:50:25 -05:00
tresabhi 8169dfd85d no .env 2026-08-05 13:43:10 -05:00
tresabhi e6d7f78ea4 game dir 2026-08-05 13:38:50 -05:00
tresabhi f1f29b85b7 s'more fixes 2026-08-05 13:32:33 -05:00
tresabhi f587bc0983 fix legacy redirect 2026-08-05 13:30:20 -05:00
tresabhi 7ef4e0b9af envs 2026-08-05 13:29:55 -05:00
tresabhi b52919ebfe update matrix 2026-08-05 13:22:33 -05:00
tresabhi 84290b1c0e remove assets 2026-08-05 13:19:27 -05:00
tresabhi 24297dcae8 remove historic averages 2026-08-05 13:18:27 -05:00
tresabhi c7ee3dbf7e update changelogs 2026-08-05 13:16:32 -05:00
tresabhi dd769a9aa5 update settings and docs 2026-08-05 13:15:47 -05:00
tresabhi 645cfacecb update embed and fix varuna bug 2026-08-05 13:13:48 -05:00
tresabhi d2041d0ee6 update tier list 2026-08-05 13:05:31 -05:00
tresabhi ed7dd76ee5 update session 2026-08-05 13:04:11 -05:00
tresabhi bc2b9f1267 glossary 2026-08-05 13:01:51 -05:00
tresabhi 851a1108d2 some updates to gallery 2026-08-04 20:38:24 -05:00
tresabhi f09663e1c0 update guesser 2026-08-04 20:35:51 -05:00
tresabhi 6a05f11001 update mixer 2026-08-04 20:34:11 -05:00
tresabhi 36e8a20cd5 update playlist 2026-08-04 20:31:49 -05:00
tresabhi 7630cc52d3 update performance 2026-08-04 20:29:54 -05:00
tresabhi 23d2ca9a42 update compare 2026-08-04 20:28:54 -05:00
tresabhi 6e759e52c9 use small tank icons 2026-08-04 20:25:47 -05:00
tresabhi 85883cd2c5 faded flags 2026-08-04 20:23:50 -05:00
tresabhi 9e4d9c7968 tank model and collision 2026-08-04 20:18:05 -05:00
tresabhi 9968b2c83d small tank icons 2026-08-04 19:54:08 -05:00
tresabhi be0804ae59 booster icons 2026-08-04 19:52:06 -05:00
tresabhi 129a2c52f9 camo icon 2026-08-04 19:49:53 -05:00
tresabhi 996c9c05df game mode banners 2026-08-04 19:45:20 -05:00
tresabhi 36dbb24572 skill icons 2026-08-04 19:42:06 -05:00
tresabhi aaae8fb8ff module icons 2026-08-04 19:40:57 -05:00
tresabhi e5e747f7a0 equipment icons 2026-08-04 14:28:34 -05:00
tresabhi 487d149587 shell icons 2026-08-04 14:17:12 -05:00
tresabhi 48c4a4c76d currencies 2026-08-04 14:12:32 -05:00
tresabhi 33ebbbeb65 imports 2026-08-04 14:02:52 -05:00
tresabhi 1a7a1223d5 scratched flags 2026-08-04 13:46:19 -05:00
tresabhi 9813459310 tank icons 2026-08-04 13:44:19 -05:00
tresabhi 94c3608ca7 fix missing tank map 2026-08-04 13:35:51 -05:00
tresabhi 7e9be14a9a label all chunks 2026-08-04 13:22:44 -05:00
tresabhi 5f773931bc split giant class 2026-08-04 13:19:17 -05:00
tresabhi b2fd85a5b0 some api paths 2026-08-04 13:07:27 -05:00
tresabhi b060e65af5 dynamically import client api 2026-08-02 20:28:57 -05:00
tresabhi 8a3e57637d provision icons 2026-08-02 20:24:39 -05:00
tresabhi 90ad923672 consumable icons 2026-08-02 20:22:32 -05:00
tresabhi 59108d7637 update (4) 2026-08-01 15:08:33 -05:00
tresabhi f85d097c16 rename 2026-08-01 14:55:57 -05:00
tresabhi ed92440b94 update (3) 2026-08-01 14:54:40 -05:00
tresabhi d41d45abae update (2) 2026-08-01 14:48:18 -05:00
tresabhi 32a7db238b update (1) 2026-08-01 14:44:23 -05:00
tresabhi 2dc23ba09d some returns 2026-08-01 14:32:25 -05:00
tresabhi 30522bfda0 caching 2026-08-01 14:21:11 -05:00
tresabhi ed31f40876 (5) some implementation 2026-08-01 14:13:18 -05:00
tresabhi 6ad64c9bd9 (4) some implementation 2026-08-01 14:01:34 -05:00
tresabhi bd7c952478 (3) some implementation 2026-08-01 13:59:56 -05:00
tresabhi f7180f0e1a (2) some implementation 2026-08-01 13:53:00 -05:00
tresabhi b0611d7d72 (1) some implementation 2026-08-01 13:44:44 -05:00
tresabhi 244968bbbf (3) some definitions headers 2026-08-01 13:26:45 -05:00
tresabhi fd8448b0a6 (2) some definitions headers 2026-08-01 13:07:04 -05:00
tresabhi 17c54be1c8 some definitions headers 2026-08-01 12:56:12 -05:00
tresabhi 39745dd475 infrastructure for inline vfs and clients 2026-07-31 20:48:33 -05:00
tresabhi d81d885767 reintroduce assets temporarily 2026-07-31 20:30:35 -05:00
tresabhi 901cb0fcc8 remove assets 2026-07-31 20:30:19 -05:00
tresabhi 7ee49c4026 remove prisma 2026-07-31 20:28:18 -05:00
tresabhi 4cff06110a remove app 2026-07-31 20:27:39 -05:00
tresabhi 251d40b0f4 fix typo 2026-07-31 20:23:39 -05:00
tresabhi ad15f83abb move to chunks 2026-07-31 20:22:12 -05:00
tresabhi c2a5b742a7 clean up root 2026-07-31 20:21:27 -05:00
tresabhi 514999e588 remove bot 2026-07-31 20:17:57 -05:00
tresabhi 50d21918b4 update logs 2026-07-31 20:14:03 -05:00
tresabhi 43b59e4f02 update workflows to work with production as the deploy branch 2026-07-31 20:12:54 -05:00
tresabhi d0834ed572 update equipments 2026-07-31 19:48:03 -05:00
tresabhi caf238dd32 add robust slugs 2026-07-31 13:43:53 -05:00
tresabhi 0a7a220af3 logs 2026-07-31 11:23:45 -05:00
tresabhi 434d1ede3c use transliteration 2026-07-31 11:21:50 -05:00
tresabhi 0f47208f5e add support for missing vfs0 dirs 2026-07-31 11:07:54 -05:00
tresabhi cb2d795461 fix url 2026-07-11 18:14:22 -05:00
tresabhi 6e934e0ff1 logs 2026-07-11 18:13:02 -05:00
tresabhi 375adadbe5 Merge branch 'main' into dev 2026-07-11 18:04:36 -05:00
tresabhi 082a756f1b auto scroll on drag in tier list 2026-07-11 18:02:49 -05:00
tresabhi 9fc8a8e2a2 more generous pitch transition zones 2026-07-11 17:41:14 -05:00
tresabhi 1413475679 bigger floor and more aggressive light decay 2026-07-11 17:38:11 -05:00
tresabhi 86504e685e softer shadows 2026-07-11 17:33:37 -05:00
tresabhi 9df81044d1 increasing lighting angle to cover larger tanks 2026-07-11 17:29:44 -05:00
tresabhi c5fd7591ea complete lighting animation even if canvas is unfocused 2026-07-11 17:29:15 -05:00
TrèsAbhi cd8e43f945 Merge pull request #256 from Hitjack007/mixer-crashing 2026-07-11 17:17:11 -05:00
tresabhi 8df112aab4 type-safe disposal 2026-07-11 17:16:52 -05:00
TrèsAbhi 94c04ce102 Merge pull request #255 from Hitjack007/external-ricochets 2026-07-11 17:14:10 -05:00
TrèsAbhi 3a170202ca Merge pull request #254 from Hitjack007/he-crash-fix 2026-07-11 17:10:49 -05:00
tresabhi 46937693a0 implement nullable refs in a type-safe manner 2026-07-11 17:09:38 -05:00
TrèsAbhi b391d0b036 Merge pull request #253 from Hitjack007/static-armor-uncapitalised-fix 2026-07-11 17:06:29 -05:00
Mark Greene 1061e003f7 Fix Safari crash on mixer texture disposal 2026-07-10 21:30:21 +12:00
Mark Greene a2b573abca fix ricochets on external modules 2026-07-10 17:51:32 +12:00
Mark Greene 4571d141e6 Fix crash on repeated HE shell clicks in Tankopedia armor viewer 2026-07-10 16:03:05 +12:00
Mark Greene d70225b03c fix static armor plate title to use localized strings
Was rendering the raw lowercase layerTypeNames key instead of resolving it through strings.website.tools.tankopedia.sandbox.dynamic.shot_card.element, same pattern already used in ShotDisplayCard.
2026-07-10 15:41:46 +12:00
844 changed files with 13223 additions and 41842 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
---
name: proto-workflow
description: Regenerate this repository's TypeScript protobuf outputs after editing .proto schemas.
---
# Proto Workflow
After editing any `.proto` schema in this repository, run `bun run build-protos` from the repository root. Review the generated changes and address any schema or generator errors before finishing the task.
+13
View File
@@ -0,0 +1,13 @@
---
name: repo-boundaries
description: Apply the user's persistent repository boundaries whenever working in their projects.
---
# Repo Boundaries
Apply these boundaries to all work in the user's repositories:
- Read-only Git commands are allowed when useful, including status, diff, log, and show. Do not run Git commands that modify repository state.
- Never edit `bun.lock`, including regenerating or formatting it. When dependency manifests change, leave the lockfile alone; the user will update it by running `bun install`.
These are firm constraints, not preferences to revisit for a task. Continue useful work within them and explain any limitation if a requested result would require crossing either boundary.
@@ -0,0 +1,3 @@
interface:
display_name: "Repo Boundaries"
short_description: "Respect repository editing boundaries"
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "bun"
directory: "/"
schedule:
interval: "daily"
-86
View File
@@ -1,86 +0,0 @@
name: Assets
on:
push:
branches: main
workflow_dispatch:
inputs:
asset_repo:
type: choice
required: false
description: What asset repo to write to
options:
- blitzkit/assets
- blitzkit/assets-preview
- blitzkit/assets-dev
default: blitzkit/assets
asset_source:
type: string
required: false
description: "Archive URL or Steam to download (example: steam:APP_ID/DEPOT_ID or zip:URL_TO_ZIP)"
default: "steam:444200/444202"
schedule:
- cron: "0 2 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.asset_repo }}
cancel-in-progress: true
env:
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_DLC_CDN: ${{ secrets.WOTB_DLC_CDN }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_REPO: ${{ github.event.inputs.asset_repo != '' && github.event.inputs.asset_repo || secrets.PUBLIC_ASSET_REPO }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: true
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
PUBLIC_PATREON_CLIENT_ID: ${{ secrets.PUBLIC_PATREON_CLIENT_ID }}
PUBLIC_PATREON_REDIRECT_URI: ${{ secrets.PUBLIC_PATREON_REDIRECT_URI }}
PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
PUBLIC_NITROPAY_ID: ${{ secrets.PUBLIC_NITROPAY_ID }}
PUBLIC_DEBUG_MISSING_I18N: false
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_FAST_USER: blitzkit
PUBLIC_ASSET_FAST_REPO: assets
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
jobs:
build_assets:
name: Build assets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- run: bun run setup
- name: AppArmor hack
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
- name: Install patches
run: bun run setup:patches --source=${{ github.event.inputs.asset_source || 'steam:444200/444202' }}
- name: Build assets
run: bun run build:assets --all-targets --source=${{ github.event.inputs.asset_source || 'steam:444200/444202' }}
-54
View File
@@ -1,54 +0,0 @@
name: Averages
on:
workflow_dispatch:
schedule:
- cron: "0 8 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Build averages
env:
PUBLIC_ASSET_BRANCH: main
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
run: bun run build:averages --production
-34
View File
@@ -1,34 +0,0 @@
name: Bot
on:
push:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build bot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Build bot
run: bun run build:bot
- name: Archive build
uses: actions/upload-artifact@v7
with:
name: bot
path: dist/bot
-36
View File
@@ -1,36 +0,0 @@
name: IDs
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 1"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Discover IDs
run: bun build:ids --production
-61
View File
@@ -1,61 +0,0 @@
name: Host
on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- name: Download release assets
run: |
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/blitzkit/blitzkit/releases/latest \
| grep "browser_download_url.*bot.zip" \
| cut -d : -f 2,3 \
| tr -d \")
curl -L -o bot.zip $DOWNLOAD_URL
mkdir -p dist/bot
unzip bot.zip -d dist/bot
- name: Install sharp
run: bun init -y && bun add sharp
- name: Move directories
run: mv dist/bot/prisma .
- name: Run bot
run: bun run dist/bot/main.js
+45
View File
@@ -0,0 +1,45 @@
name: Discover IDs
on:
push:
branches: production
schedule:
- cron: 0 */6 * * *
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
discover:
name: Discover IDs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: production
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Setup Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Discover IDs
run: bun run discover-ids
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.IDS_TOKEN }}
-46
View File
@@ -1,46 +0,0 @@
name: Rating Asia
on:
workflow_dispatch:
schedule:
- cron: "0 17 * * *"
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Generate leaderboard
run: bun build:ratingleaderboard --region=asia
-46
View File
@@ -1,46 +0,0 @@
name: Rating Europe
on:
workflow_dispatch:
schedule:
- cron: "0 23 * * *"
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Generate leaderboard
run: bun build:ratingleaderboard --region=eu
-46
View File
@@ -1,46 +0,0 @@
name: Rating North America
on:
workflow_dispatch:
schedule:
- cron: "0 5 * * *"
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Generate leaderboard
run: bun build:ratingleaderboard --region=com
-38
View File
@@ -1,38 +0,0 @@
name: Reviews
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 2
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Find reviews
run: bun run build:reviews
+176
View File
@@ -0,0 +1,176 @@
name: Website
on:
push:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
inputs:
branch:
description: Branch
type: choice
required: true
default: production
options:
- production
- main
environment:
description: Environment
type: choice
required: true
default: production
options:
- development
- beta
- production
- preview
- opentest
client_provider:
description: Client provider
type: choice
required: true
default: steam
options:
- steam
- zip
- wgpkg
client_source:
description: Client source
type: string
required: true
default: 444200/windows
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ inputs.environment || github.ref }}
cancel-in-progress: true
env:
# client
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
CLIENT_PROVIDER: ${{ inputs.client_provider || 'steam' }}
CLIENT_SOURCE: ${{ inputs.client_source || '444200/windows' }}
CLIENT_DIR: ../../temp/game
# blitzkit
PUBLIC_ENVIRONMENT: ${{ inputs.environment || 'production' }}
PUBLIC_PROMOTE_OPENTEST: false
PUBLIC_STATIC_ASSET_BASE: https://assets.blitzkit.app
PUBLIC_GUIDE_WEBSITE: https://guidesblitz.com
# blitz
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
# google
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
# wargaming
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
jobs:
build:
name: Build website
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
chunk: [default, api]
steps:
- name: Free disk space
uses: jlumbroso/free-disk-space@main
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch || 'production' }}
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- name: Setup CLIs
run: |
sudo apt-get update
sudo apt-get install -y aria2 libarchive-tools
- name: Install dependencies
run: bun install
- name: Download Client
run: bun run download-client
- name: Build
run: bun run --bun build
env:
CHUNK: ${{ matrix.chunk }}
- name: Upload artifact
id: upload
uses: actions/upload-artifact@v4
with:
name: chunk-${{ matrix.chunk }}
path: dist/website
- name: Trigger deployment
# Deploy if:
# 1. Running cron from main using content from production to target production
# 2. Running manually and targeting non-production
if: >-
${{
github.ref == 'refs/heads/production' ||
(inputs.environment != '' && inputs.environment != 'production')
}}
env:
TOKEN: ${{ secrets.GH_NODE_DEPLOY_TOKEN }}
CHUNK: ${{ matrix.chunk }}
run: |
ENV_FILE="environments/${PUBLIC_ENVIRONMENT}.json"
TARGET_REPO=$(jq -r ".nodes.\"$CHUNK\"" "$ENV_FILE")
if [ "$TARGET_REPO" == "null" ] || [ -z "$TARGET_REPO" ]; then
exit 1
fi
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/$TARGET_REPO/dispatches" \
-d "{
\"event_type\": \"deploy-pages\",
\"client_payload\": {
\"source_repo\": \"${{ github.repository }}\",
\"run_id\": \"${{ github.run_id }}\",
\"artifact_id\": \"${{ steps.upload.outputs.artifact-id }}\",
\"chunk\": \"$CHUNK\"
}
}"
-72
View File
@@ -1,72 +0,0 @@
name: Website
on:
push:
workflow_dispatch:
schedule:
- cron: "0 4 * * *"
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
build:
name: Build website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: actions/configure-pages@v6
- run: bun run setup
- name: Build website
run: bun run build:website
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
path: dist/website
deploy-github-pages:
if: ${{ github.ref == 'refs/heads/main' }}
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: build
environment:
name: Production
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
+3 -2
View File
@@ -1,7 +1,9 @@
packages/protos/src/*/*.ts
node_modules
_node_modules
package-lock.json
*.env
.env
/test-results/
/playwright-report/
/playwright/.cache/
@@ -27,7 +29,6 @@ target
.astro
.vercel
/src
/packages/bot/src/prisma
dev-dist
# Logs
logs
+5 -1
View File
@@ -192,6 +192,7 @@
"searchtanks",
"SHAREDEXP",
"SKLLD",
"sluggify",
"smoothstep",
"Smri",
"Specularity",
@@ -253,5 +254,8 @@
"youtubers",
"zoomable"
],
"typescript.tsdk": "node_modules\\typescript\\lib"
"typescript.tsdk": "node_modules\\typescript\\lib",
"cSpell.enabledFileTypes": {
"proto": true
}
}
-40
View File
@@ -5,43 +5,3 @@ Tools for everything and anything, World of Tanks Blitz.
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
If you intend to contribute to the code, note that you may need to check out sensitive, closed-source submodules to locally develop certain aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or discuss sensitive content, such as environment variables or submodules.
## Packages
| Package | Title | Source | Memo |
| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| [packages/app](packages/app) | BlitzKit | [Play Store](https://play.google.com/store/apps/details?id=com.tresabhi.blitzkit), [App Store](https://apps.apple.com/app/4321), [Microsoft Store](https://apps.microsoft.com/detail/zyxw) | The native app of BlitzKit for all platforms. Unavailable from stores at this time. |
| [packages/bot](packages/bot) | BlitzKit Discord Bot | [Discord Application Directory](https://discord.com/discovery/applications/1097673957865443370) | The one-in-all Discord bot for statistics. |
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. |
## Submodules
| Submodule | Title | Source | Memo |
| ------------------------------------------------------ | --------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [submodules/blitzkit-closed](packages/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. |
## Environment Variables
| Name | Source | BlitzKit Default | Memo |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
| `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. |
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
| `PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
| `PUBLIC_BRANCH` | | `main` | The branch that the website is operating on. This is completely nominal. |
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
| `SERVICING` | | `false` | Wether the website is down for servicing. |
| `DISCORD_CLIENT_ID` | [Discord Developer Portal](https://discord.com/developers/applications) | `1097673957865443370` | The BlitzKit Discord Bot's user ID. |
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
| `DISCORD_TOKEN` | [Discord Developer Portal](https://discord.com/developers/applications) | | The Discord API Key for the BlitzKit Discord Bot. |
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
| `WOTB_DLC_CDN` | | | |
+164 -362
View File
File diff suppressed because it is too large. Load diff
+7 -1
View File
@@ -1,5 +1,11 @@
{
"words": [
"Debuff",
"gamemodes",
"Passability",
"Recoverkit",
"Repairkit",
"Respawns",
"Tankopedia"
]
}
}
+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.30.7
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Dragging items in the Tier List now automatically scrolls the screen if you reach the top/bottom edge of the screen
## Fixes
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
- The spot light angle has been increased to more evenly light up larger tanks
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
- Increased the floor size and light decay to create the illusion that the floor is infinite
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
+9
View File
@@ -0,0 +1,9 @@
# BlitzKit 1.30.8
Support for tanks with Chinese names.
## Fixes
- The URL generator for BlitzKit now handles names that have only Chinese characters
- The Mixed Virtual File System now handles non-existent directories correctly
- Updated equipments to match the game again
+40
View File
@@ -0,0 +1,40 @@
# BlitzKit 1.31.0
This update massively upgrades BlitzKit's graphical fidelity (while making it faster) and updates the way BlitzKit deploys updates, speeding up future versions.
## Features
- Improved the performance of the Tank Sandbox in Tankopedia
- Reduced the number of lights by 25%
- Reduced the resolution of shadows by 50%
- Improved the graphical fidelity of tanks in the Tankopedia
- Added soft contact shadows below tanks
- Added sky and ground reflected lighting to the top and bottom of tanks
- Removed the fog that used to darken tanks when zoomed far away
- Made BlitzKitLand™ fade out circularly to hide the sharp edges
- Pointed the lights 1.5m above the ground
- This genuinely makes BlitzKit almost photo-realistic in my opinion
- Non-PBR tanks are now lit up brighter to accommodate for their dark textures
## Fixes
- Fixed a bug where the ambient occlusion map (a measure of how dark a part of a tank gets due to low light bounces) was applied to the color of the tank directly, making many tanks appear dark even in direct light
- Tanks with chinese characters now generate urls through transliteration
## Known Issues
- The flags found behind the selected tank in the tech tree section of the Tankopedia is offset incorrectly
## Technical Features
- BlitzKit can now calculate and store performance statistics and account details for up 10,000,000,000 players
- This will help make the BlitzKit Performance tool more accurate, and play well with an upcoming, unannounced major feature
## Technical Changes
- The `production` branch now deploys updates to `blitzkit.app` instead of the `main` branch
- `robots.txt` now disables indexing of BlitzKit variants based on the environment
- The website is now compiled in "chunks" of which the `api` chunk now takes on the task of mangling what was previously stored in `assets3`
- `assets3` will be removed in `1.31.1`, please migrate to the new `api` if you use any BlitzKit assets
- Removed the `bot` and `app` packages
- Removed Prisma
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.1
This update adds capabilities to release BlitzKit OpenTest and Preview.
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.2
This update fixes an issue that occurred with Wargaming chose to declare the class of a tank in the game code with a tag that isn't the first in the list.
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.3
This update updates BlitzKit to work with the game's new ID system for game modes.
+14
View File
@@ -0,0 +1,14 @@
# BlitzKit 1.31.4
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Fixes
- [Mark Greene] BlitzKit Session now properly handles tanks removed from the game
## Technical Fixes
- BlitzKit now handles the new ID system for game-modes in WoTB
- The directory which preview versions of the game are downloaded to are now created properly
- This was the last bug standing in the way of BlitzKit Previews
- Welcome back, BlitzKit Previews!
-11
View File
@@ -2,17 +2,6 @@
🎉 Services for everything and anything World of Tanks Blitz
## Bot
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
- 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
## Website
Provides a bunch of very helpful tools; https://blitzkit.app/
-3
View File
@@ -1,3 +0,0 @@
# Inviting the Bot
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-beta.blitzkit.app",
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
},
"aliases": {
"api": "https://api.beta.blitzkit.app",
"static": "https://static.beta.blitzkit.app"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": "api"
},
"nodes": {},
"aliases": {
"api": "/api",
"static": "https://static.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-opentest.blitzkit.app",
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
},
"aliases": {
"api": "https://api.opentest.blitzkit.app",
"static": "https://static.opentest.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-preview.blitzkit.app",
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
},
"aliases": {
"api": "https://api.preview.blitzkit.app",
"static": "https://static.preview.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": true,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-blitzkit.app",
"api": "blitzkit/deploy-node-api.blitzkit.app"
},
"aliases": {
"api": "https://api.blitzkit.app",
"static": "https://static.blitzkit.app"
}
}
+8 -36
View File
@@ -1,51 +1,23 @@
{
"name": "blitzkit",
"version": "1.30.6",
"version": "1.31.4",
"license": "BlitzKit License",
"private": true,
"scripts": {
"build:assets": "cd packages/scripts && bun run build:assets",
"build:averages": "bun --filter=@blitzkit/scripts run build:averages",
"build:bot": "cd packages/scripts && bun run build:bot",
"build:website": "cd packages/website && bun run build",
"build:prisma": "prisma generate",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"build:catalogitemaccesssor": "cd packages/scripts && bun run build:catalogitemaccesssor",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"build:app": "cd packages/app && bun run build",
"dev:bot": "bun run build:bot --dev && bun start:bot --dev",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website && bun run dev",
"dev:app": "cd packages/app && bun run dev",
"dev": "bun run dev:website",
"dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
"setup:patches": "mkdir -p packages/closed/node_modules && ln -sf ../../../node_modules/lz4js packages/closed/node_modules/lz4js && cd packages/scripts && bun run setup:patches",
"start:website": "bun --filter=@blitzkit/website run start",
"host:docs": "vitepress preview docs",
"lint": "tsc",
"lint:website": "cd packages/website && bun run lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"update": "yarn upgrade --latest",
"tunnel": "cloudflared tunnel --url http://localhost:3000",
"discoverids": "scripts/discoverIds.ts",
"build:protos": "cd packages/scripts && bun run build:protos",
"preview:website": "cd packages/website && bun run preview",
"build:reviews": "cd packages/scripts && bun build:reviews",
"build:ids": "cd packages/scripts && bun build:ids",
"prunestrings": "cd packages/scripts && bun prunestrings",
"setup": "bun install && bun run build:prisma",
"patchnotes": "cd packages/scripts && bun run patchnotes"
"dev": "cd packages/website && bun run dev",
"build": "cd packages/website && bun run build",
"discover-ids": "cd packages/scripts && bun run discover-ids",
"build-protos": "cd packages/scripts && bun run build-protos",
"record-performance": "cd packages/scripts && bun run record-performance",
"download-client": "cd packages/scripts && bun run download-client"
},
"type": "module",
"workspaces": [
"packages/*"
],
"dependencies": {
"@prisma/client": "^6.18.0",
"@gltf-transform/extensions": "^4.4.2",
"lz4js": "^0.2.0",
"prisma": "^6.18.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
}
-18
View File
@@ -1,18 +0,0 @@
{
"name": "@blitzkit/app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/cli": "^2",
"vite": "^6.0.3",
"typescript": "~5.6.2"
}
}
-7
View File
@@ -1,7 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
-5286
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "blitzkit"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "blitzkit_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
@@ -1,10 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
-14
View File
@@ -1,14 +0,0 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
-27
View File
@@ -1,27 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Command, Stdio};
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
let entry = app_handle
.path_resolver()
.resolve_resource("../../dist/website/server/entry.mjs")
.expect("failed to resolve server.ts");
Command::new("bun")
.arg("run")
.arg(entry)
.stderr(Stdio::null())
.spawn()
.expect("failed to start Bun server");
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running Tauri app");
}
-38
View File
@@ -1,38 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "blitzkit",
"version": "0.1.0",
"identifier": "com.tresabhi.blitzkit",
"build": {
"beforeDevCommand": "cd ../website && bun run dev",
"_beforeBuildCommand": "cd ../website && bun run build",
"frontendDist": "../../../dist/website/client",
"devUrl": "http://localhost:4321"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"url": "http://localhost:4321",
"title": "BlitzKit",
"fullscreen": false,
"resizable": true,
"maximized": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"extends": ["../../tsconfig.json"],
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
-35
View File
@@ -1,35 +0,0 @@
{
"name": "@blitzkit/bot",
"dependencies": {
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/themes": "^3.3.0",
"@resvg/resvg-js": "^2.6.2",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.9",
"@types/lodash-es": "^4.17.12",
"@types/markdown-escape": "^1.1.3",
"@types/react": "^19.2.14",
"adm-zip": "^0.5.16",
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"fuzzysort": "^3.1.0",
"lodash-es": "^4.17.23",
"markdown-escape": "^2.0.0",
"pickleparser": "^0.2.1",
"prisma": "^6.19.2",
"protobufjs": "^7.5.4",
"react": "^19.2.4",
"satori": "^0.18.4",
"sharp": "^0.34.5"
},
"private": true,
"scripts": {
"dev": "bun --watch src/main.ts",
"start": "bun src/main.ts"
},
"type": "module",
"types": "src/types.d.ts"
}
-68
View File
@@ -1,68 +0,0 @@
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import {
commandsArray,
type CommandRegistry,
} from "../events/interactionCreate";
const subcommands = [
"embeds",
"introduction",
"invite",
"timezones",
"credits",
] as const;
type Subcommand = (typeof subcommands)[number];
const DOCS: Record<Subcommand, string> = {
embeds: "guide/embeds",
introduction: "guide/introduction",
invite: "guide/invite",
timezones: "guide/timezones",
credits: "about/credits",
};
export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
command: createLocalizedCommand("about", [
{ subcommand: "commands" },
...subcommands.map((subcommand) => ({ subcommand })),
]),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
const { strings } = translator(interaction.locale);
if (subcommand === "commands") {
return `${strings.bot.commands.about.subcommands.commands.body}${(
await Promise.all(commandsArray)
)
.sort((a, b) =>
(a.command.name_localizations?.[interaction.locale] ??
a.command.name) <
(b.command.name_localizations?.[interaction.locale] ??
b.command.name)
? -1
: 1,
)
.map(
(registry) =>
`- \`/${
registry.command.name_localizations?.[interaction.locale] ??
registry.command.name
}\`: ${
registry.command.description_localizations?.[
interaction.locale
] ?? registry.command.description
}`,
)
.join("\n")}`;
}
const url = `https://raw.githubusercontent.com/tresabhi/blitzkit/main/docs/${DOCS[subcommand as Subcommand]}.md`;
const content = fetch(url).then((response) => response.text());
return content;
},
});
});
@@ -1,65 +0,0 @@
import { addPeriodicFilterOptions } from "../../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../../core/discord/resolvePeriodFromButton";
import { resolvePlayerFromButton } from "../../core/discord/resolvePlayerFromButton";
import { translator } from "../../core/localization/translator";
import type { CommandRegistry } from "../../events/interactionCreate";
import { renderBreakdown } from "./render";
export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("breakdown"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return [
// ...(await renderBreakdown(
// player,
// period,
// filters,
// interaction.locale,
// )),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(player.region, player.id),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(
player,
period,
filters,
interaction.locale,
);
},
});
},
);
@@ -1,295 +0,0 @@
import {
type BlitzStats,
calculateWN8,
getAccountInfo,
getClanAccountInfo,
getTankStats,
getWN8Percentile,
TankDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { chunk } from "lodash-es";
import * as Breakdown from "../../components/Breakdown";
import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import {
averageDefinitions,
tankDefinitions,
} from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import {
filterStats,
type StatFilters,
} from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import type { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import type { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await averageDefinitions;
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ["clan"]);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: BlitzStats[] = [];
const orderedCareerStats: BlitzStats[] = [];
const orderedCurrentWN8: (number | undefined)[] = [];
const orderedCareerWN8: (number | undefined)[] = [];
const orderedCareerWN8Full: (number | undefined)[] = [];
if (tankStats === null) {
throw new UserError(strings.bot.common.errors.no_tank_stats);
}
filteredOrder.forEach((id) => {
const tankAveragesEntry = awaitedTankAverages.averages[id];
const current = statsInPeriod.diff[id];
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
const currentWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, current)
: undefined;
const careerWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, career)
: undefined;
orderedCurrentStats.push(current);
orderedCareerStats.push(career);
orderedCurrentWN8.push(currentWN8);
orderedCareerWN8.push(careerWN8);
});
tankStats.forEach((tank) => {
const tankAveragesEntry = awaitedTankAverages.averages[tank.tank_id];
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, tank.all)
: undefined,
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0,
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0,
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0,
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0,
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0,
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0,
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0,
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0,
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0,
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0,
) / careerBattles;
const children: React.JSX.Element[] = [];
if (filteredOrder.length > 0) {
children.push(
<Breakdown.Row
key={id}
displayType="summary"
title={strings.bot.commands.breakdown.body.total}
stats={[
{
title: strings.bot.commands.breakdown.body.battles,
current: currentBattles,
career: careerBattles,
},
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
},
{
title: strings.bot.commands.breakdown.body.wn8,
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
},
]}
/>,
);
filteredOrder.forEach((id, index) => {
const tankDefinition = awaitedTankDefinitions.tanks[id] as
| TankDefinition
| undefined;
const current = orderedCurrentStats[index];
const career = orderedCareerStats[index];
const currentWN8 = orderedCurrentWN8[index];
const careerWN8 = orderedCareerWN8[index];
children.push(
<Breakdown.Row
key={id}
displayType="tank"
class={tankDefinition?.class}
type={tankDefinition?.type}
title={
tankDefinition
? unwrap(tankDefinition.name)
: literals(strings.bot.commands.breakdown.body.unknown_tank, {
id,
})
}
stats={[
{
title: strings.bot.commands.breakdown.body.battles,
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
current.wins / current.battles - career.wins / career.battles,
},
{
title: strings.bot.commands.breakdown.body.wn8,
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>,
);
});
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<CommandWrapper>
{index === 0 && (
<TitleBar
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
)}
<Breakdown.Root>{page}</Breakdown.Root>
</CommandWrapper>
))
.reverse();
} else {
return [
<CommandWrapper>
<TitleBar
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
<NoData type="battles_in_period" locale={locale} />
</CommandWrapper>,
];
}
}
-37
View File
@@ -1,37 +0,0 @@
import packageJSON from "../../../../package.json";
import { client } from "../core/discord/client";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const executionStart = new Date().getTime();
export const debugCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("debug"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const currentTime = new Date().getTime();
const uptime = currentTime - executionStart;
const list = [
[strings.bot.commands.debug.body.version, packageJSON.version],
[
strings.bot.commands.debug.body.shard.label,
client.shard?.ids[0] ?? strings.bot.commands.debug.body.shard.default,
],
[
strings.bot.commands.debug.body.uptime,
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
(uptime / 1000 / 60) % 60
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
uptime % 1000
)}ms`,
],
];
return `${strings.bot.commands.debug.body.title}${list
.map(([key, value]) => `- ${key}: ${value}`)
.join("\n")}`;
},
});
});
-182
View File
@@ -1,182 +0,0 @@
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
import { Locale, SlashCommandSubcommandGroupBuilder } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Graph from "../components/Graph";
import { LineColor } from "../components/Graph/components/Line/constants";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { getPlayerHistories } from "../core/blitzstars/getPlayerHistories";
import { getTankHistories } from "../core/blitzstars/getTankHistories";
import { addPeriodSubCommands } from "../core/discord/addPeriodSubCommands";
import { addTankChoices } from "../core/discord/addTankChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
type EvolutionStatType = "player" | "tank";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
type: "player" | "tank",
tankId: number | null,
locale: Locale,
) {
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const logo = clan ? emblemURL(clan.emblem_set_id) : undefined;
const histories = await (type === "player"
? getPlayerHistories(region, id, {
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
})
: getTankHistories(region, id, locale, {
tankId: tankId!,
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
}));
const plot = histories.map(
(history) =>
[
history.all.battles,
(history.all.wins / history.all.battles) * 100,
] as Graph.PlotItem,
);
const times = histories.map((history) => history.last_battle_time);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const ys = plot.map(([, y]) => y);
const xs = plot.map(([x]) => x);
const maxY = Math.max(...ys);
const minY = Math.min(...ys);
const maxX = Math.max(...xs);
const minX = Math.min(...xs);
return (
<CommandWrapper>
<TitleBar title={accountInfo.nickname} image={logo} description={name} />
{plot.length > 0 && (
<Graph.Root
verticalMargin={{
min: minY,
max: maxY,
suffix: "%",
precision: 2,
}}
horizontalMargin={{
min: minX,
max: maxX,
precision: 0,
}}
xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
>
{Graph.Line({
color: LineColor.Red,
plot: plot,
minY,
maxY,
})}
</Graph.Root>
)}
{plot.length === 0 && <NoData type="battles_in_period" locale={locale} />}
</CommandWrapper>
);
}
export const evolutionCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: createLocalizedCommand("evolution", [
{
group: "player",
modify(option: SlashCommandSubcommandGroupBuilder) {
addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices),
);
},
},
{
group: "tank",
modify(option: SlashCommandSubcommandGroupBuilder) {
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameChoices),
);
},
},
]),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const commandGroup =
// interaction.options.getSubcommandGroup() as EvolutionStatType;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const tankIdRaw = interaction.options.getString("tank")!;
// const tankId =
// commandGroup === "tank"
// ? await resolveTankId(tankIdRaw, interaction.locale)
// : null;
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// tankId,
// });
// return [
// await render(
// player,
// period,
// commandGroup,
// tankId,
// interaction.locale
// ),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(
// player.region,
// player.id,
// tankId ?? undefined
// ),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const url = new URL(`https://example.com/${interaction.customId}`);
const path = url.pathname.split("/");
const commandGroup = path[2] as EvolutionStatType;
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
return await render(
player,
period,
commandGroup,
parseInt(url.searchParams.get("tankId") ?? "0") || null,
interaction.locale,
);
},
});
},
);
-117
View File
@@ -1,117 +0,0 @@
import {
emblemURL,
getAccountInfo,
getClanAccountInfo,
getTierWeights,
} from "@blitzkit/core";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericAllStats } from "../components/GenericAllStats";
import { NoData } from "../components/NoData";
import { TierWeights } from "../components/TierWeights";
import { TitleBar } from "../components/TitleBar";
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary, filteredOrder } = await filterStats(
diffedTankStats,
filters,
);
const filterDescriptions = await filtersToDescription(filters, locale);
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
return (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{stats.battles === 0 && (
<NoData type="battles_in_period" locale={locale} />
)}
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
{stats.battles > 0 && (
<GenericAllStats
stats={stats}
supplementaryStats={supplementary}
locale={locale}
/>
)}
</CommandWrapper>
);
}
export const fullStatsCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("full-stats"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return Promise.all([
// render(player, period, filters, interaction.locale),
// buttonRefresh(interaction, path),
// getBlitzStarsLinkButton(player.region, player.id),
// ]);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters, interaction.locale);
},
});
},
);
-96
View File
@@ -1,96 +0,0 @@
import { getAccountInfo, getClanInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericStats } from "../components/GenericStats";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { addClanChoices } from "../core/discord/addClanChoices";
import { autocompleteClan } from "../core/discord/autocompleteClan";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_THRESHOLD = 7;
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("inactive")
.addStringOption(addClanChoices)
.addNumberOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName("threshold")
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.inactive.options.threshold.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.inactive.options.threshold.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.inactive.options.threshold.description
)
)
.setMinValue(0);
}),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
const inactive = accountInfo
.map(
(account) =>
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number]
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
.map(
([name, days]) =>
[
name,
literals(strings.bot.commands.inactive.body.listing, {
days: days.toFixed(0),
}),
] as [string, string]
);
const hasInactiveMembers = inactive.length > 0;
return (
<CommandWrapper>
<TitleBar
title={clanInfo.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${literals(
strings.bot.commands.inactive.body.subtitle,
{ days: threshold }
)} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
{!hasInactiveMembers && (
<NoData type="players_in_period" locale={interaction.locale} />
)}
{hasInactiveMembers && <GenericStats stats={inactive} />}
</CommandWrapper>
);
},
autocomplete: autocompleteClan,
});
});
-193
View File
@@ -1,193 +0,0 @@
import { getAccountInfo, getClanAccountInfo } from '@blitzkit/core';
import { teal } from '@radix-ui/colors';
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
import { CommandWrapper } from '../components/CommandWrapper';
import { linkBlitzAndDiscord } from '../core/db/linkBlitzAndDiscord';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import type { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('link').addStringOption((option) =>
addUsernameChoices(option).setRequired(true),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = BigInt(interaction.user.id);
const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
await linkBlitzAndDiscord(discordId, id);
return [
<CommandWrapper fat>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
}}
>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
alt="avatar"
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<img
alt="accounts linked"
src="https://i.imgur.com/jIcRgog.png"
style={{
width: 16,
height: 16,
}}
/>
<span style={{ fontSize: 16 }}>
{strings.bot.commands.link.body.accounts_linked}
</span>
</div>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img
alt="clan emblem"
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
/>
) : null}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{accountInfo.nickname}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] • `
: ''}
{strings.common.regions.normal[region]}
</span>
</div>
</div>
</div>
<Glow color={teal.teal9} rotation={-90} />
</div>
</CommandWrapper>,
embedInfo(
strings.bot.commands.link.embed.title,
strings.bot.commands.link.embed.description,
),
];
},
autocomplete: autocompleteUsername,
});
});
-90
View File
@@ -1,90 +0,0 @@
import {
TANK_CLASSES,
TIER_ROMAN_NUMERALS,
TankDefinition,
flags,
getAccountInfo,
getTankStats,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import {
gameDefinitions,
tankDefinitions,
} from "../core/blitzkit/nonBlockingPromises";
import { addTierChoices } from "../core/discord/addTierChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { chunkLines } from "../core/discord/chunkLines";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("owned-tanks")
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const tier = Number(interaction.options.getString("tier"));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id);
if (tankStats === null) {
return strings.bot.common.errors.no_tank_stats;
}
const filteredTanks = (
await Promise.all(
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id,
})),
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = [];
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation);
} else {
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
}
});
const awaitedTankDefinitions = await tankDefinitions;
const awaitedGameDefinitions = await gameDefinitions;
const title = literals(strings.bot.commands.owned_tanks.body.title, {
name: markdownEscape(accountInfo.nickname),
tier: TIER_ROMAN_NUMERALS[tier],
});
const tankList = tankStats
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
)
.sort(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation),
)
.map(
(tank) =>
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name!))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
);
const lines = [title, ...tankList];
return chunkLines(lines);
},
autocomplete: autocompleteUsername,
});
});
-46
View File
@@ -1,46 +0,0 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("permissions"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
// friday the 13th permissions easter egg
const isFridayThe13th =
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has("ViewChannel"),
strings.bot.commands.permissions.body.view_channels,
],
[
interaction.appPermissions?.has("ReadMessageHistory"),
strings.bot.commands.permissions.body.read_message_history,
],
[
interaction.appPermissions?.has("AttachFiles"),
strings.bot.commands.permissions.body.attach_files,
],
[
interaction.appPermissions?.has("UseExternalEmojis"),
strings.bot.commands.permissions.body.external_emojis,
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
} else {
return `${isFridayThe13th ? "💀" : "❌"} ${description}`;
}
})
.join("\n");
return literals(strings.bot.commands.permissions.body.title, { body });
},
});
});
-34
View File
@@ -1,34 +0,0 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
handlesInteraction: true,
command: createLocalizedCommand("ping", [
{ subcommand: "blitzkit" },
{ subcommand: "wotb" },
]),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === "blitzkit") {
await interaction.editReply(strings.bot.commands.ping.body.pong);
} else {
await fetch("https://api.wotblitz.com/");
}
const executionTime = Date.now() - executionStart;
interaction.editReply(
literals(strings.bot.commands.ping.body.pong_with_time, {
time: executionTime.toFixed(0),
})
);
},
});
});
-69
View File
@@ -1,69 +0,0 @@
import { getAccountInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { markdownTable } from "../core/discord/markdownTable";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command:
createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const account = await resolvePlayerFromCommand(interaction);
const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
literals(strings.bot.commands.player_info.body.title, {
name: markdownEscape(accountInfo.nickname),
}),
markdownTable([
[
strings.bot.commands.player_info.body.nickname,
`${accountInfo.nickname}`,
],
[
strings.bot.commands.player_info.body.battles,
`${accountInfo.statistics.all.battles}`,
],
[
strings.bot.commands.player_info.body.winrate,
`${(
100 *
(accountInfo.statistics.all.wins /
accountInfo.statistics.all.battles)
).toFixed(2)}%`,
],
[],
[
strings.bot.commands.player_info.body.account_id,
`${accountInfo.account_id}`,
],
[
strings.bot.commands.player_info.body.created,
new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale
),
],
[
strings.bot.commands.player_info.body.last_battle,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale
),
],
])
);
},
autocomplete: autocompleteUsername,
});
});
-535
View File
@@ -1,535 +0,0 @@
import {
deltaBkrlBlitzStats,
emblemURL,
getAccountInfo,
getArchivedRatingLeaderboard,
getClanAccountInfo,
getLeagueFromScore,
getRatingInfo,
getRatingNeighbors,
imgur,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { SlashCommandSubcommandBuilder } from "discord.js";
import markdownEscape from "markdown-escape";
import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
import { CommandWrapper } from "../components/CommandWrapper";
import { DeltaCaret } from "../components/DeltaCaret";
import { TitleBar } from "../components/TitleBar";
import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
import { iconPng } from "../core/blitzkit/iconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
const LEAGUE_ACCENT = [
"_purple",
"_sand",
"_amber",
"_mauve",
"_brown",
] as const;
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("rating", [
{
subcommand: "today",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
{
subcommand: "season",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
]),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true) as
| "season"
| "today";
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const ratingInfo = await getRatingInfo(region);
if (ratingInfo.detail) {
throw new Error(`Rating info not available for ${region}`);
}
const leaderboard = await getArchivedRatingLeaderboard(
region,
ratingInfo.current_season - (subcommand === "today" ? 0 : 1)
);
if (leaderboard.version!.$case === "v1") {
throw new Error(
"Encountered bkrl minimal format in latest season. Wait till next cron?"
);
}
const accountInfo = await getAccountInfo(region, id, [
"statistics.rating",
]);
const statsIndex = leaderboard.version!.value.entries.findIndex(
(entry) => entry.id === id
);
const statsA = leaderboard
? leaderboard.version!.value.entries[statsIndex!]
: null;
const positionA = leaderboard ? statsIndex! + 1 : undefined;
const statsB1 = accountInfo.statistics.rating!;
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
if (statsB2Array === undefined) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const [statsB2] = statsB2Array;
const league = getLeagueFromScore(statsB2.score);
if (!statsA) {
return strings.bot.commands.rating.errors.unrecorded_stats;
}
if (!statsB1) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const reward = ratingInfo.rewards.find(
(reward) =>
statsB2.number >= reward.from_position &&
statsB2.number <= reward.to_position
);
const positionDelta = leaderboard
? statsB2.number - positionA!
: undefined;
const delta = deltaBkrlBlitzStats(
statsA,
accountInfo.statistics.rating!,
statsB2
);
const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward
? reward.type === "vehicle"
? reward.vehicle.preview_image_url
: reward.stuff.image_url
: undefined;
if (rewardImage) {
const { ok } = await fetch(rewardImage);
if (!ok) {
rewardImage = imgur("kP88vqr");
}
}
return (
<CommandWrapper>
<TitleBar
title={accountInfo.nickname}
image={clanImage}
description={`${
strings.bot.commands.rating.body.subtitle[subcommand]
} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
<div
style={{
position: "relative",
display: "flex",
overflow: "hidden",
}}
>
<div
style={{
position: "relative",
padding: 16,
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
borderRadius: 4,
border: `1px solid ${
theme.colors[`componentInteractive${leagueTheme}`]
}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
{reward === undefined && (
<Glow
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(-16px) rotate(90deg)",
}}
/>
)}
<div
style={{
display: "flex",
gap: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<img
alt={`League ${league.name}`}
style={{
width: 64,
height: 64,
objectFit: "contain",
}}
src={normalizeLeagueIcon(
ratingInfo.leagues[league.index].big_icon
)}
/>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
style={{
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 24,
fontWeight: 900,
}}
>
{statsB2.score.toLocaleString(interaction.locale)}
</span>
{delta.score !== 0 && (
<>
<DeltaCaret delta={delta.score} />
<span
style={{
color:
delta.score > 0
? theme.colors.textLowContrast_green
: theme.colors.textLowContrast_red,
fontSize: 16,
}}
>
{Math.abs(delta.score).toLocaleString(
interaction.locale
)}
</span>
</>
)}
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${leagueTheme}`],
fontSize: 16,
}}
>
#{statsB2.number.toLocaleString(interaction.locale)}
</span>
{positionDelta !== undefined && positionDelta !== 0 && (
<>
<DeltaCaret delta={-positionDelta} />
<span
style={{
color:
positionDelta > 0
? theme.colors.textLowContrast_red
: theme.colors.textLowContrast_green,
fontSize: 16,
}}
>
{Math.abs(positionDelta).toLocaleString(
interaction.locale
)}
</span>
</>
)}
</div>
</div>
</div>
{reward && (
<div
style={{
display: "flex",
gap: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
style={{
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 16,
maxWidth: 136,
textAlign: "right",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{reward.type === "vehicle"
? reward.vehicle.user_string
: reward.stuff.title}
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${leagueTheme}`],
fontSize: 16,
}}
>
Reward
{reward.type === "stuff" ? ` x${reward.count}` : ""}
</span>
</div>
</div>
<img
style={{
width: 64,
height: 64,
objectFit: "cover",
}}
src={await iconPng(rewardImage!)}
/>
</div>
)}
{reward === undefined && (
<Glow
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(16px) rotate(-90deg)",
}}
/>
)}
</div>
{/* separated because fragment middle man invalidates relative parent: https://github.com/vercel/satori/issues/605 */}
{reward && (
<Glow
direction="reverse"
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
top: -40,
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
{reward && (
<Glow
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
bottom: -40,
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
</div>
{delta.battles > 0 && (
<div style={{ padding: "8px 0", display: "flex" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{delta.battles.toLocaleString(interaction.locale)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Battles
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Winrate
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{Math.round(delta.damage / delta.battles).toLocaleString(
interaction.locale
)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Survival
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(delta.kills / delta.battles).toFixed(2)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
)}
</CommandWrapper>
);
},
});
});
@@ -1,468 +0,0 @@
import {
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
getAccountInfo,
getArchivedLatestSeasonNumber,
getArchivedRatingLeaderboard,
getClanAccountInfo,
getRatingInfo,
getRatingLeague,
getRatingNeighbors,
imgur,
isOnGoingRatingSeason,
LEAGUES,
type RatingInfo,
RatingLeaderboard,
type Region,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
type APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from "discord.js";
import { range } from "lodash-es";
import markdownEscape from "markdown-escape";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Leaderboard from "../components/Leaderboard";
import { TitleBar } from "../components/TitleBar";
import { UserError } from "../core/blitzkit/userError";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const DEFAULT_LIMIT = 10;
export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range(
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
).slice(-25);
function addOptions(option: SlashCommandSubcommandBuilder) {
const { strings } = translator(Locale.EnglishUS);
return option
.addIntegerOption((option) =>
option
.setName(strings.bot.commands.rating_leaderboard.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30)
)
.addStringOption((option) =>
option
.setName(
strings.bot.commands.rating_leaderboard.options.season.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.season.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season
.description
)
)
.setRequired(false)
.addChoices(
...seasonNumbers
.map((number, index) => {
const current =
index === seasonNumbers.length - 1 && onGoingSeason;
return {
name: current
? literals(
strings.bot.commands.rating_leaderboard.options.season
.choices.current,
{ season: number }
)
: `${number}`,
value: `${number}`,
name_localizations: current
? localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options
.season.choices.current,
{ season: number }
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse()
)
);
}
resolve({
command: createLocalizedCommand("rating-leaderboard", [
{
subcommand: "neighbors",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: "league",
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName(
strings.bot.commands.rating_leaderboard.options.league.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.league
.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.description
)
)
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name: (
strings.common.leagues as Record<string, string>
)[name],
value: `${value}`,
name_localizations: localizationObject(
(strings) =>
(
strings.common.leagues as Record<string, string>
)[name]
),
} satisfies APIApplicationCommandOptionChoice<string>)
)
)
.setRequired(true);
})
.addStringOption(addRegionChoices);
addOptions(option);
},
},
]),
// BIG TODO: title seems to be in english
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| "league"
| "neighbors";
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString("season");
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string;
let titleImage: string | undefined;
let titleDescription: string;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let archivedLeaderboard: RatingLeaderboard | null;
let playerId: number | undefined;
let regionRatingInfo: RatingInfo | null;
if (subcommand === "league") {
const leagueIndex = parseInt(
interaction.options.getString("league")!
);
const region = interaction.options.getString("region") as Region;
regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region)
: null);
if (
regionRatingInfo !== null &&
regionRatingInfo.detail !== undefined
) {
return strings.bot.commands.rating_leaderboard.body
.no_ongoing_season;
}
const result = isCurrentSeason
? await getRatingLeague(region, leagueIndex).then(async (result) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
} satisfies SimplifiedPlayer)
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.version!.value.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.version!.value.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
["clan"]
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag,
nickname:
clanData[index]?.account_name ??
literals(
strings.bot.commands.rating_leaderboard.body
.deleted_player,
{ id: player.id }
),
} satisfies SimplifiedPlayer)
);
}
);
const leagueInfo = LEAGUES[leagueIndex];
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
);
players = result;
playersAfter =
(regionRatingInfo === null
? archivedLeaderboard.version!.value.entries.length
: regionRatingInfo.count) - result[result.length - 1].position;
titleName = `${
leagueInfo
? (strings.common.leagues as Record<string, string>)[
LEAGUES[leagueInfo.index].name
]
: ""
} - ${strings.common.regions.short[region]}`;
titleImage = imgur(leagueInfo.icon);
titleDescription = literals(
strings.bot.commands.rating_leaderboard.body.top_players,
{ count: limit }
);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingInfo = isCurrentSeason
? await getRatingInfo(region)
: null;
if (
regionRatingInfo !== null &&
regionRatingInfo.detail !== undefined
) {
return strings.bot.commands.rating_leaderboard.body
.no_ongoing_season;
}
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
} satisfies SimplifiedPlayer)
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.version!.value.entries.findIndex(
(player) => player.id === id
);
if (playerIndex === -1) {
throw new UserError(
literals(
strings.bot.commands.rating_leaderboard.body
.no_participation,
{ player: markdownEscape(accountInfo.nickname), season }
)
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.version!.value.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
["clan"]
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[index]?.clan?.tag,
nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
}
);
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : "";
titleDescription =
strings.bot.commands.rating_leaderboard.body.neighbors;
playersAfter =
(regionRatingInfo === null
? archivedLeaderboard.version!.value.entries.length
: regionRatingInfo.count) -
neighbors[neighbors.length - 1].position;
playerId = id;
}
const items = players.map((player) => {
const midnightIndex =
archivedLeaderboard?.version!.value.entries.findIndex(
(item) => item.id === player.id
);
const midnightScore =
midnightIndex === undefined
? player.score
: archivedLeaderboard!.version!.value.entries[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}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return (
<CommandWrapper>
<TitleBar
title={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={literals(
strings.bot.commands.rating_leaderboard.body.more,
{ count: playersAfter.toLocaleString(interaction.locale) }
)}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
);
},
autocomplete: autocompleteUsername,
});
}
);
-592
View File
@@ -1,592 +0,0 @@
import {
calculateWN8,
emblemURL,
getClanAccountInfo,
idToRegion,
tankIcon,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { Locale } from "discord.js";
import { clamp } from "lodash-es";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { iconPng } from "../core/blitzkit/iconPng";
import {
averageDefinitions,
mapDefinitions,
tankDefinitions,
} from "../core/blitzkit/nonBlockingPromises";
import { RenderConfiguration } from "../core/blitzkit/renderConfiguration";
import { buttonLink } from "../core/discord/buttonLink";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
type WotInspectorReplaySubmission =
| { status: "error"; data: unknown; error: { message: string } }
| {
status: "ok";
data: {
download_url: string;
view_url: string;
summary: {
battle_start_time: string;
map_name: string;
battle_start_timestamp: number;
battle_duration: number;
title: string;
arena_unique_id: string;
allies: number[];
enemies: number[];
protagonist: number;
player_name: string;
vehicle: string;
exp_base: number;
exp_total: number;
credits_base: number;
credits_total: number;
room_type: number;
battle_type: number;
mastery_badge: number;
battle_result: number;
vehicle_tier: number;
vehicle_type: number;
protagonist_team: number;
winner_team: number;
details: {
achievements: unknown[];
base_capture_points: number;
base_defend_points: number;
chassis_id: unknown | null;
clan_tag: string;
clanid: number;
credits: number;
damage_assisted: number;
damage_assisted_track: number;
damage_blocked: unknown | null;
damage_made: number;
damage_received: number;
dbid: number;
death_reason: number;
distance_travelled: number;
enemies_damaged: number;
enemies_destroyed: number;
enemies_spotted: number;
entity_id: number;
exp: number;
exp_for_assist: number;
exp_for_damage: number;
exp_team_bonus: number;
gun_id: unknown | null;
hero_bonus_credits: number;
hero_bonus_exp: number;
hitpoints_left: number;
hits_bounced: number;
hits_pen: number;
hits_received: number;
hits_splash: number;
killed_by: number;
name: string;
shots_hit: number;
shots_made: number;
shots_pen: number;
shots_splash: number;
squad_index: number;
team: number;
time_alive: number;
turret_id: unknown | null;
vehicle_descr: number;
wp_points_earned: number;
wp_points_stolen: number;
};
};
};
error: {};
};
interface WotInspectorReplay {
id: string;
map_id: number;
battle_duration: number;
title: unknown | null;
player_name: string;
protagonist: number;
vehicle_descr: number;
mastery_badge: number;
exp_base: number;
enemies_spotted: number;
enemies_destroyed: number;
damage_assisted: number;
damage_made: number;
details_url: string;
download_url: string;
game_version: {
name: string;
package: string;
};
arena_unique_id: string;
download_count: number;
data_version: number;
private: boolean;
private_clan: boolean;
battle_start_time: Date;
upload_time: Date;
allies: number[];
enemies: number[];
protagonist_clan: number;
protagonist_team: number;
battle_result: number;
credits_base: number;
tags: number[];
battle_type: number;
room_type: number;
last_accessed_time: Date;
winner_team: number;
finish_reason: number;
players_data: WotInspectorReplayPlayerData[];
exp_total: number;
credits_total: number;
repair_cost: number;
exp_free: number;
exp_free_base: number;
exp_penalty: number;
credits_penalty: number;
credits_contribution_in: number;
credits_contribution_out: number;
camouflage_id: number;
}
export interface WotInspectorReplayPlayerData {
team: number;
name: string;
entity_id: number;
dbid: number;
clanid: number | null;
clan_tag: null | string;
hitpoints_left: number;
credits: number;
exp: number;
shots_made: number;
shots_hit: number;
shots_splash: number;
shots_pen: number;
damage_made: number;
damage_received: number;
damage_assisted: number;
damage_assisted_track: number;
hits_received: number;
hits_bounced: number;
hits_splash: number;
hits_pen: number;
enemies_spotted: number;
enemies_damaged: number;
enemies_destroyed: number;
time_alive: number;
distance_travelled: number;
killed_by: number;
base_capture_points: number;
base_defend_points: number;
exp_for_damage: number;
exp_for_assist: number;
exp_team_bonus: number;
wp_points_earned: number;
wp_points_stolen: number;
hero_bonus_credits: number;
hero_bonus_exp: number;
death_reason: number;
achievements: any[];
vehicle_descr: number;
turret_id: number;
gun_id: number;
chassis_id: number;
squad_index: number;
damage_blocked: number;
}
function stat(value: string | number, flex = 2) {
return (
<div
style={{
flex,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
</div>
);
}
async function playerListing(
locale: Locale,
player: WotInspectorReplayPlayerData,
protagonistTeam: boolean,
winningTeam: number,
recordingPlayer = false,
) {
const { unwrap } = translator(locale);
const awaitedTankAverages = await averageDefinitions;
const awaitedTankDefinitions = await tankDefinitions;
const tank = awaitedTankDefinitions.tanks[player.vehicle_descr];
const blockAccent = recordingPlayer ? "_amber" : "";
const healthAccent = protagonistTeam
? recordingPlayer
? "_amber"
: "_green"
: "_red";
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
// TODO: 💀
const turret = tank.turrets.sort(
(a, b) =>
Math.abs(a.id - player.turret_id) - Math.abs(b.id - player.turret_id),
)[0];
const totalHealth = turret.health + tank.health;
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
const tankAveragesEntry = awaitedTankAverages.averages[player.vehicle_descr];
const wn8 =
tankAveragesEntry === undefined
? -1
: calculateWN8(tankAveragesEntry.mu, {
battles: 1,
damage_dealt: player.damage_made,
dropped_capture_points: player.base_defend_points,
frags: player.enemies_destroyed,
spotted: player.enemies_spotted,
wins: winningTeam === player.team ? 1 : 0,
});
return (
<div
key={player.entity_id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
}}
>
<div
style={{
display: "flex",
gap: 8,
flex: 1,
overflow: "hidden",
alignItems: "center",
}}
>
<img
alt={unwrap(tank.name!)}
src={await iconPng(tankIcon(tank.id))}
style={{
width: 32,
height: 32,
objectFit: "contain",
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
// align
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${blockAccent}`],
fontSize: 12,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
<span
style={{
color: theme.colors[`textHighContrast${blockAccent}`],
fontSize: 16,
}}
>
{unwrap(tank.name!)}
</span>
</div>
<span
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
whiteSpace: "nowrap",
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ""}
</span>
</div>
</div>
<div
style={{
display: "flex",
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat(wn8 === -1 ? "--" : Math.round(wn8).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
<div
style={{
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: "flex",
width: 4,
height: "100%",
transform: "scaleY(-1)",
}}
>
<div
style={{
width: "100%",
height: `${healthLeft * 100}%`,
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
}}
/>
</div>
</div>
);
}
function Header() {
return (
<div
style={{
padding: 8,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
flex: 1,
}}
>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
WN8
</span>
</div>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
XP
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
</div>
);
}
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("replay").addAttachmentOption((option) =>
option
.setName(strings.bot.commands.replay.options.file.name)
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.name,
undefined,
true,
),
)
.setDescription(strings.bot.commands.replay.options.file.description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.description,
),
)
.setRequired(true),
),
async handler(interaction) {
const awaitedMapDefinitions = await mapDefinitions;
const file = interaction.options.getAttachment("file")!;
const submissionData = (await fetch(
`https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent(
file.url,
)}`,
).then((response) => response.json())) as WotInspectorReplaySubmission;
if (submissionData.status === "error") {
return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`;
}
const replayId = submissionData.data.download_url.split("/").at(-1)!;
const replayData = (await fetch(
`https://api.wotinspector.com/v2/blitz/replays/${replayId}/`,
).then((response) => response.json())) as WotInspectorReplay;
const antagonistTeamId = 3 - replayData.protagonist_team;
const players = replayData.players_data.sort(
(a, b) => b.damage_made - a.damage_made,
);
const clan = (
await getClanAccountInfo(
idToRegion(replayData.protagonist),
replayData.protagonist,
["clan"],
)
)?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
return [
new RenderConfiguration({ width: 960 }),
<CommandWrapper>
<TitleBar
title={replayData.player_name}
image={clanImage}
description={`${
replayData.protagonist_team === replayData.winner_team
? "Victory"
: "Defeat"
} • ${awaitedMapDefinitions.maps[replayData.map_id].name}`}
/>
<div
style={{
display: "flex",
gap: 8,
}}
>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter(
(player) => player.team === replayData.protagonist_team,
)
.map((player) =>
playerListing(
interaction.locale,
player,
true,
replayData.winner_team,
player.dbid === replayData.protagonist,
),
),
)}
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter((player) => player.team === antagonistTeamId)
.map((player) =>
playerListing(
interaction.locale,
player,
false,
replayData.winner_team,
),
),
)}
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, "WoT Inspector"),
buttonLink(submissionData.data.download_url, "Download"),
];
},
});
});
-626
View File
@@ -1,626 +0,0 @@
import {
asset,
emblemURL,
equipmentPriceMatrix,
getAccountInfo,
getClanAccountInfo,
getTankStats,
TANK_ICONS,
TankType,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { escapeMarkdown, Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { resolveTankId } from "../core/blitz/resolveTankId";
import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
import { iconPng } from "../core/blitzkit/iconPng";
import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
import { tankIconPng } from "../core/blitzkit/tankIconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("research")
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.target_tank.name!)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.name,
undefined,
true,
),
)
.setDescription(
strings.bot.commands.research.options.target_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.description,
),
)
.setAutocomplete(true)
.setRequired(true),
)
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.starting_tank.name!)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.name,
undefined,
true,
),
)
.setDescription(
strings.bot.commands.research.options.starting_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.description,
),
)
.setAutocomplete(true)
.setRequired(false),
)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId(
interaction.options.getString("target-tank", true),
interaction.locale,
true,
);
const startingTankRaw = interaction.options.getString("starting-tank");
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
const targetTankAncestry = await resolveAncestry(targetTankId);
if (startingTankRaw) {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
);
if (targetTankId === startingTankId) {
return literals(
strings.bot.commands.research.errors.start_end_equal,
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
);
}
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
if (startingTank.tier > targetTank.tier) {
return literals(strings.bot.commands.research.errors.unordered, {
tank_1: escapeMarkdown(unwrap(startingtank.name!)),
tank_2: escapeMarkdown(unwrap(targettank.name!)),
});
}
if (!targetTankAncestry.includes(startingTankId)) {
return literals(
strings.bot.commands.research.errors.tanks_not_on_line,
{
tank_1: escapeMarkdown(unwrap(targettank.name!)),
tank_2: escapeMarkdown(unwrap(startingtank.name!)),
},
);
}
} else {
if (targetTank.type !== TankType.TANK_TYPE_RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, {
tank: escapeMarkdown(unwrap(targettank.name!)),
});
}
if (targetTank.tier === 1) {
return literals(strings.bot.commands.research.errors.tier_1, {
tank: escapeMarkdown(unwrap(targettank.name!)),
});
}
const tankStats = await getTankStats(region, id);
if (tankStats === null) {
return strings.bot.common.errors.no_tank_stats;
}
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return literals(
strings.bot.commands.research.errors.already_researched,
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
awaitedTankDefinitions.tanks[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id),
);
if (foundAncestor === undefined) {
throw new Error("No first owned tech tree found.");
}
startingTankId = foundAncestor;
}
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
);
const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const costs = await Promise.all(
line.map(async (id) => {
const tank = awaitedTankDefinitions.tanks[id];
const turretXps = new Map<number, number>();
const gunXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
tank.turrets.forEach((turret) => {
if (
turret.research_cost !== undefined &&
typeof turret.research_cost.research_cost_type!.value === "number"
) {
turretXps.set(
turret.id,
turret.research_cost.research_cost_type!.value,
);
}
turret.guns.forEach((gun) => {
if (
gun.research_cost !== undefined &&
typeof gun.research_cost.research_cost_type!.value === "number"
) {
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
}
});
});
tank.engines.forEach((engine) => {
if (
engine.research_cost !== undefined &&
typeof engine.research_cost.research_cost_type!.value === "number"
) {
engineXps.set(
engine.id,
engine.research_cost.research_cost_type!.value,
);
}
});
tank.tracks.forEach((track) => {
if (
track.research_cost !== undefined &&
typeof track.research_cost.research_cost_type!.value === "number"
) {
trackXps.set(
track.id,
track.research_cost.research_cost_type!.value,
);
}
});
let moduleXp = 0;
turretXps.forEach((value) => {
moduleXp += value;
});
gunXps.forEach((value) => {
moduleXp += value;
});
engineXps.forEach((value) => {
moduleXp += value;
});
trackXps.forEach((value) => {
moduleXp += value;
});
return {
research: tank.research_cost!.research_cost_type!.value as number,
upgrades: moduleXp,
purchase: tank.price!.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
),
};
}),
);
const image = (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${strings.bot.commands.research.body.subtitle} • ${
targetTank.name
}`}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<div
style={{
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div style={{ width: 48 }}></div>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
right: 24,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{strings.bot.commands.research.body.total}
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: "16px 0",
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Experience:{" "}
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Purchase:{" "}
{costs
.reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Equipment:{" "}
{costs
.reduce((a, b) => a + b.equipment, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
</div>
</div>
{await Promise.all(
line.map(async (id, index) => {
const tank = awaitedTankDefinitions.tanks[id];
return (
<div
key={id}
style={{
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div
style={{
width: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
position: "relative",
right: "5%",
}}
>
<img
alt="Tank icon"
src={await tankIconPng(id)}
style={{
height: 100,
width: 100,
objectFit: "contain",
position: "absolute",
left: "25%",
top: 0,
}}
/>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
justifyContent: "center",
position: "absolute",
bottom: 8,
left: "50%",
transform: "translateX(-50%)",
}}
>
<img
alt={unwrap(tank.name!)}
src={TANK_ICONS[tank.class]}
width={16}
height={16}
/>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{unwrap(tank.name!)}
</span>
</div>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: "16px 0",
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.research,
{
cost: costs[index].research?.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
{costs[index].upgrades > 0 && (
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Free XP icon"
src={await iconPng(
asset("icons/currencies/free-xp.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.upgrades,
{ cost: costs[index].upgrades.toLocaleString() },
)}
</span>
</div>
)}
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.purchase,
{
cost: costs[index].purchase.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.equipment,
{
cost: costs[index].equipment.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
</div>
</div>
);
}),
)}
</div>
</CommandWrapper>
);
return startingTankRaw
? image
: [strings.bot.commands.research.body.estimation, image];
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]);
},
});
});
-74
View File
@@ -1,74 +0,0 @@
import { type ClanList, fetchBlitz, type Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addClanChoices } from "../core/discord/addClanChoices";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("search-clans")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addClanChoices(option).setAutocomplete(false)
)
.addIntegerOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName(strings.bot.commands.search_clans.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.search_clans.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100);
}),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const clan = interaction.options.getString("clan")!;
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, "clans/list", {
search: clan,
limit,
});
return embedInfo(
literals(strings.bot.commands.search_clans.body.title, {
search: markdownEscape(clan),
region: strings.common.regions.normal[region],
}),
clanList.length === 0
? strings.bot.commands.search_clans.body.no_results
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join("\n")}\n\`\`\``
);
},
});
});
@@ -1,83 +0,0 @@
import {
type AccountList,
fetchBlitz,
type Region,
usernamePattern,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("search-players")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true)
)
.addIntegerOption((option) =>
option
.setName(strings.bot.commands.search_players.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100)
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const name = interaction.options.getString("username")!;
const limit = interaction.options.getInteger("limit") ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(region, "account/list", {
search: trimmedSearch,
limit,
})
: [];
return embedInfo(
literals(strings.bot.commands.search_players.body.title, {
search: markdownEscape(trimmedSearch),
region: strings.common.regions.normal[region],
}),
`\`\`\`${
players.length === 0
? strings.bot.commands.search_players.body.no_results
: players.map((player) => player.nickname).join("\n")
}\`\`\``
);
},
});
});
-102
View File
@@ -1,102 +0,0 @@
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
import { Locale } from "discord.js";
import { AllStatsOverview } from "../components/AllStatsOverview";
import { CommandWrapper } from "../components/CommandWrapper";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
const filterDescriptions = await filtersToDescription(filters, locale);
return (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{!stats.battles && <NoData type="battles_in_period" locale={locale} />}
{stats.battles > 0 && (
<AllStatsOverview
locale={locale}
stats={stats}
supplementaryStats={supplementary}
/>
)}
</CommandWrapper>
);
}
export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("stats"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return Promise.all([
// render(player, period, filters, interaction.locale),
// buttonRefresh(interaction, path),
// getBlitzStarsLinkButton(player.region, player.id),
// ]);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters, interaction.locale);
},
});
});
-60
View File
@@ -1,60 +0,0 @@
import { gameDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { addFilterOptions } from "../core/discord/addFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { renderBreakdown } from "./breakdown/render";
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
const awaitedGameDefinitions = await gameDefinitions;
resolve({
command: addFilterOptions(
createLocalizedCommand("today"),
awaitedGameDefinitions.nations,
).addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(
// player.region,
// interaction,
// 'today',
// );
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction, true),
// ...filters,
// });
// return [
// ...(await renderBreakdown(player, period, filters, interaction.locale)),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(player.region, player.id),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters, interaction.locale);
},
});
});
@@ -1,50 +0,0 @@
import { type ComponentProps } from "react";
export interface GlowProps extends ComponentProps<"div"> {
color: string;
direction?: "default" | "reverse";
rotation?: number;
startOpacity?: number;
endOpacity?: number;
}
const width = 16;
export function Glow({
color,
direction = "default",
rotation = 0,
style,
startOpacity = 0,
endOpacity = 1,
...props
}: GlowProps) {
const color1 = `${color}${Math.round(endOpacity * 255)
.toString(16)
.padStart(2, "0")}`;
const color2 = `${color}${Math.round(startOpacity * 255)
.toString(16)
.padStart(2, "0")}`;
const background = `linear-gradient(${
direction === "default" ? 0 : 180
}deg, ${color1} 0%, ${color2} 100%)`;
return (
<div
style={{
display: "flex",
alignItems: direction === "default" ? "flex-end" : "flex-start",
justifyContent: "center",
transform: `rotate(${rotation}deg)`,
...style,
}}
{...props}
>
<div style={{ height: 54, width, background }} />
<div style={{ height: 72, width, background }} />
<div style={{ height: 80, width, background }} />
<div style={{ height: 72, width, background }} />
<div style={{ height: 54, width, background }} />
</div>
);
}
@@ -1,72 +0,0 @@
import { theme } from '../../../../stitches.config';
import { Glow } from './components/Glow';
export interface WN8DisplayProps {
subtitle: string;
color: string;
value: string | number;
}
export function HeroStat({ color, subtitle, value }: WN8DisplayProps) {
return (
<div
style={{
width: 128 * 2, // idk why twice the width turns into the correct width
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Glow color={color} style={{ marginBottom: -16 }} />
<div
style={{
width: 128,
height: 80,
backgroundColor: color,
borderRadius: 40,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
}}
>
<span
style={{
display: 'block',
fontWeight: 900,
fontSize: 32,
color: theme.colors.textHighContrast,
lineHeight: '26px',
letterSpacing: '-0.03em',
}}
>
{value}
</span>
<span
style={{
display: 'block',
fontSize: 16,
lineHeight: '16px',
color: theme.colors.textHighContrast,
}}
>
{subtitle}
</span>
</div>
</div>
<Glow color={color} direction="reverse" style={{ marginTop: -16 }} />
</div>
);
}
@@ -1,47 +0,0 @@
import { theme } from '../../../stitches.config';
export enum WN8SurroundingStatAlign {
Left,
Right,
}
export interface WN8SurroundingStatProps {
label: string;
value: string | number;
align?: WN8SurroundingStatAlign;
padded?: boolean;
}
export function WN8SurroundingStat({
label,
value,
align = WN8SurroundingStatAlign.Left,
padded = false,
}: WN8SurroundingStatProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems:
align === WN8SurroundingStatAlign.Left ? 'flex-start' : 'flex-end',
paddingLeft: padded && align === WN8SurroundingStatAlign.Left ? 32 : 0,
paddingRight:
padded && align === WN8SurroundingStatAlign.Right ? 32 : 0,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
fontWeight: 700,
}}
>
{value}
</span>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{label}
</span>
</div>
);
}
@@ -1,130 +0,0 @@
import {
getWN8Percentile,
Percentile,
type SupplementaryStats,
} from '@blitzkit/core';
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { PERCENTILE_COLORS } from '../PercentileIndicator/constants';
import { HeroStat } from './components/HeroStat';
import {
WN8SurroundingStat,
WN8SurroundingStatAlign,
} from './components/WN8SurroundingStat';
export interface AllStatsOverviewProps {
stats: {
battles: number;
wins: number;
damage_dealt: number;
damage_received: number;
survived_battles: number;
hits: number;
shots: number;
frags: number;
};
supplementaryStats: SupplementaryStats;
locale: Locale;
}
export function AllStatsOverview({
stats,
locale,
supplementaryStats,
}: AllStatsOverviewProps) {
const { strings } = translator(locale);
const percentile =
typeof supplementaryStats.WN8 === 'number'
? getWN8Percentile(supplementaryStats.WN8)
: Percentile.VeryBad;
const color = PERCENTILE_COLORS[percentile];
const heroStat =
typeof supplementaryStats.WN8 === 'number'
? supplementaryStats.WN8.toFixed(0)
: '--';
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: -64,
width: '100%',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
alignItems: 'flex-end',
flex: 1,
}}
>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.battles}
value={stats.battles.toLocaleString()}
align={WN8SurroundingStatAlign.Right}
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.winrate}
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.damage}
value={Math.round(
stats.damage_dealt / stats.battles,
).toLocaleString()}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.damage_ratio}
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
align={WN8SurroundingStatAlign.Right}
/>
</div>
<HeroStat
value={heroStat}
subtitle={strings.common.wn8_percentile[percentile]}
color={color}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
flex: 1,
}}
>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.tier}
value={
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
}
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.survival}
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
2,
)}%`}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.accuracy}
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.kills}
value={(stats.frags / stats.battles).toFixed(2)}
/>
</div>
</div>
);
}
@@ -1,16 +0,0 @@
import { type ReactNode } from "react";
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
className="session-tracker-root"
style={{ display: "flex", flexDirection: "column", gap: 8 }}
>
{children}
</div>
);
}
@@ -1,126 +0,0 @@
import {
Percentile,
TankClass,
TankType,
TREE_TYPE_ICONS,
} from "@blitzkit/core";
import { createColors } from "bepaint";
import {
type AccentColor,
type GrayColor,
PALETTES,
} from "../../../core/radix/radixColors";
import { RowStat } from "./RowStat";
export interface RowStatItem {
title: string;
current?: number | string;
career?: number | string;
delta?: number;
percentile?: Percentile;
}
interface RowProps {
displayType?: "tank" | "summary";
naked?: boolean;
title: string;
minimized?: boolean;
type?: TankType;
class?: TankClass;
color?: AccentColor | GrayColor;
stats: (RowStatItem | undefined)[];
}
export function Row({
color = "slate",
stats,
title,
minimized,
class: tankClass,
type,
naked = false,
displayType,
}: RowProps) {
const palette = PALETTES[`${color}Dark`];
const theme = createColors(palette);
return (
<div
className="session-tracker-row"
style={{
maxWidth: naked ? 600 : Infinity,
display: "flex",
flexDirection: "column",
borderRadius: 4,
backgroundColor: naked
? `${theme.appBackground2}c0`
: theme.appBackground2,
overflow: "hidden",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 8,
gap: 4,
backgroundColor: naked
? `${theme.componentInteractive}c0`
: theme.componentInteractive,
}}
>
{displayType === "tank" && tankClass !== undefined && (
<img
alt={TankClass[tankClass]}
src={
TREE_TYPE_ICONS[type ?? TankType.TANK_TYPE_RESEARCHABLE][
tankClass
]
}
style={{ width: 16, height: 16 }}
/>
)}
<span
style={{
color:
displayType === "tank"
? type === TankType.TANK_TYPE_COLLECTOR
? theme.textLowContrast_blue
: type === TankType.TANK_TYPE_PREMIUM
? theme.textLowContrast_amber
: theme.textHighContrast
: theme.textLowContrast,
fontWeight: 900,
fontSize: 16,
}}
>
{title}
</span>
</div>
<div
style={{
display: "flex",
paddingTop: 8,
paddingBottom: 8,
}}
>
{stats.map(
(row, index) =>
row && (
<RowStat
minimized={minimized}
key={index}
name={`${row.title} • ${row.career ?? "--"}`}
value={row.current ?? "--"}
delta={row.delta}
percentile={row.percentile}
/>
),
)}
</div>
</div>
);
}
@@ -1,89 +0,0 @@
import { Percentile } from "@blitzkit/core";
import { createColors } from "bepaint";
import {
type AccentColor,
type GrayColor,
PALETTES,
} from "../../../core/radix/radixColors";
import { PercentileIndicator } from "../../PercentileIndicator";
interface RowStatProps {
name: string;
value?: string | number;
delta?: number;
percentile?: Percentile;
minimized?: boolean;
color?: AccentColor | GrayColor;
}
export function RowStat({
name,
value,
delta,
percentile,
minimized = false,
color = "slate",
}: RowStatProps) {
const theme = createColors(PALETTES[`${color}Dark`]);
return (
<div
className="session-tracker-row-stat"
style={{
display: "flex",
flexDirection: "column",
gap: 4,
alignItems: "center",
justifyContent: "center",
flex: 1,
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
justifyContent: "center",
}}
>
{percentile === undefined && delta !== undefined && delta !== 0 && (
<img
alt={delta > 0 ? "Increase" : "Decrease"}
src={
(delta ?? 0) > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
style={{ width: 12, height: 12 }}
/>
)}
{percentile !== undefined && (
<PercentileIndicator percentile={percentile} />
)}
<span
style={{
color: theme.textHighContrast,
fontWeight: 700,
fontSize: 16,
textAlign: "center",
}}
>
{value}
</span>
</div>
{!minimized && (
<span
style={{
color: theme.textLowContrast,
textAlign: "center",
fontSize: 12,
}}
>
{name}
</span>
)}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Root';
export * from './components/Row';
export * from './components/RowStat';
@@ -1,26 +0,0 @@
import { type ReactNode } from "react";
import { theme } from "../stitches.config";
interface CommandWrapperProps {
children: ReactNode;
fat?: boolean;
}
export function CommandWrapper({ children, fat }: CommandWrapperProps) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
padding: `16px ${fat ? 0 : 16}px`,
gap: 16,
width: "100%",
color: theme.colors.textHighContrast,
backgroundColor: theme.colors.appBackground1,
position: "relative",
}}
>
{children}
</div>
);
}
@@ -1,21 +0,0 @@
interface DeltaCaretProps {
delta: number;
}
export function DeltaCaret({ delta }: DeltaCaretProps) {
return (
<img
alt={delta > 0 ? 'Increase' : 'Decrease'}
style={{
width: 16,
height: 16,
objectFit: 'contain',
}}
src={
delta > 0
? 'https://i.imgur.com/gjI4a5f.png'
: 'https://i.imgur.com/lLGZygN.png'
}
/>
);
}
-19
View File
@@ -1,19 +0,0 @@
import { asset } from '@blitzkit/core';
interface FlagProps {
nation: string;
}
export function Flag({ nation }: FlagProps) {
return (
<div
style={{
width: 22,
height: 22,
overflow: 'hidden',
}}
>
<img alt={nation} src={asset(`flags/circle/${nation}.webp`)} />
</div>
);
}
@@ -1,95 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { theme } from '../../../stitches.config';
import { PercentileIndicator } from '../../PercentileIndicator';
interface CardItem {
label: string;
value: string | number | undefined;
percentile?: Percentile;
}
interface CardProps {
items: CardItem[];
title: string;
}
export function Card({ items, title }: CardProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
flex: 1,
borderRadius: 4,
backgroundColor: theme.colors.componentInteractive,
padding: 8,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
}}
>
{title}
</span>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
{items.map((item) => (
<div
key={item.label}
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{item.label}
</span>
<div
style={{
flex: 1,
height: 1,
borderRadius: 0.5,
backgroundColor: theme.colors.borderNonInteractive,
}}
/>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
{item.percentile !== undefined && (
<PercentileIndicator percentile={item.percentile} />
)}
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{item.value === undefined || Number.isNaN(item.value)
? '--'
: item.value}
</span>
</div>
</div>
))}
</div>
</div>
);
}
@@ -1,17 +0,0 @@
interface RootProps {
children: React.ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
{children}
</div>
);
}
@@ -1,7 +0,0 @@
interface RowProps {
children: React.ReactNode;
}
export function Row({ children }: RowProps) {
return <div style={{ display: 'flex', gap: 8 }}>{children}</div>;
}
@@ -1,149 +0,0 @@
import {
type BlitzStats,
getWN8Percentile,
isNumber,
type SupplementaryStats,
} from '@blitzkit/core';
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { Card } from './components/Card';
import { Root } from './components/Root';
import { Row } from './components/Row';
interface GenericAllStatsProps {
stats: BlitzStats;
supplementaryStats?: SupplementaryStats;
locale: Locale;
}
export function GenericAllStats({
stats,
supplementaryStats,
locale,
}: GenericAllStatsProps) {
const { strings } = translator(locale);
const damageRatio = stats.damage_dealt / stats.damage_received;
const killsToDeathRatio =
stats.frags / (stats.battles - stats.survived_battles);
return (
<Root>
<Row>
<Card
title={strings.bot.commands.full_stats.body.cards.games.title}
items={[
{
label: strings.bot.commands.full_stats.body.cards.games.winrate,
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
},
{
label: strings.bot.commands.full_stats.body.cards.games.battles,
value: stats.battles,
},
{
label:
strings.bot.commands.full_stats.body.cards.games.wins_losses,
value: `${stats.wins} / ${stats.losses}`,
},
]}
/>
<Card
title={strings.bot.commands.full_stats.body.cards.efficiency.title}
items={[
{
label: strings.bot.commands.full_stats.body.cards.efficiency.wn8,
value:
supplementaryStats && isNumber(supplementaryStats.WN8)
? supplementaryStats.WN8!.toFixed(0)
: undefined,
percentile: isNumber(supplementaryStats?.WN8)
? getWN8Percentile(supplementaryStats!.WN8!)
: undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.efficiency.survival,
value: `${(
100 *
(stats.survived_battles / stats.battles)
).toFixed(2)}%`,
},
{
label:
strings.bot.commands.full_stats.body.cards.efficiency.accuracy,
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
},
]}
/>
</Row>
<Row>
<Card
title={strings.bot.commands.full_stats.body.cards.lethality.title}
items={[
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.average_damage,
value: (stats.damage_dealt / stats.battles).toFixed(0),
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.damage_per_tier,
value: supplementaryStats?.tier
? (
stats.damage_dealt /
stats.battles /
supplementaryStats.tier
).toFixed(0)
: undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.damage_ratio,
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.average_kills,
value: (stats.frags / stats.battles).toFixed(0),
},
]}
/>
<Card
title={strings.bot.commands.full_stats.body.cards.miscellaneous.title}
items={[
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_tier,
value: supplementaryStats?.tier?.toFixed(2),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_spots,
value: (stats.spotted / stats.battles).toFixed(2),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_xp,
value: (stats.xp / stats.battles).toFixed(0),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.kills_to_death_ratio,
value: isFinite(killsToDeathRatio)
? killsToDeathRatio.toFixed(2)
: undefined,
},
]}
/>
</Row>
</Root>
);
}
@@ -1,16 +0,0 @@
import { type GenericStat } from "..";
import { GenericStatsRow } from "./GenericStatsRow";
interface GenericStatsColumnProps {
stats: GenericStat[];
}
export function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
return (
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 8 }}>
{stats.map((stat) => (
<GenericStatsRow key={stat[0]} stat={stat} />
))}
</div>
);
}
@@ -1,46 +0,0 @@
import { type GenericStat } from "..";
import { theme } from "../../../stitches.config";
import { PercentileIndicator } from "../../PercentileIndicator";
interface GenericStatsRowProps {
stat: GenericStat;
}
export function GenericStatsRow({ stat }: GenericStatsRowProps) {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
whiteSpace: "nowrap",
maxWidth: 128,
overflow: "hidden",
}}
>
{stat[0]}
</span>
<span
style={{
color: theme.colors.textHighContrast,
fontWeight: 700,
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
{stat[2] !== undefined && <PercentileIndicator percentile={stat[2]} />}
{stat[1]}
</span>
</div>
);
}
@@ -1,34 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { GenericStatsColumn } from './components/GenericStatsColumn';
export type GenericStat =
| [string, string | number | undefined]
| [string, string | number | undefined, Percentile | undefined];
export interface GenericStatsProps {
stats: GenericStat[];
columnCount?: number;
}
export function GenericStats({ stats, columnCount = 2 }: GenericStatsProps) {
const filteredStats = stats.filter(([, value]) => value !== undefined);
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
const columns: GenericStat[][] = [];
filteredStats.forEach((stat, index) => {
const column = Math.floor(index / itemsPerRow);
if (!columns[column]) columns[column] = [];
columns[column].push(stat);
});
return (
<div style={{ display: 'flex', gap: 32 }}>
{columns.map((stats) => (
<GenericStatsColumn
key={stats.map((stat) => stat[0]).join('-')}
stats={stats}
/>
))}
</div>
);
}
@@ -1,11 +0,0 @@
export enum LineColor {
Red,
Green,
Blue,
White,
}
export enum LineStyle {
Solid,
Dashes,
DashesAndDots,
}
@@ -1,65 +0,0 @@
import { blue, green, red, slateDark } from '@radix-ui/colors';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
import { LineColor, LineStyle } from './constants';
const LINE_COLORS = {
[LineColor.Red]: red.red9,
[LineColor.Green]: green.green9,
[LineColor.Blue]: blue.blue9,
[LineColor.White]: slateDark.slate11,
};
const LINE_STYLES = {
[LineStyle.Solid]: undefined,
[LineStyle.Dashes]: '8, 8',
[LineStyle.DashesAndDots]: '1, 4, 8, 4',
};
export type PlotItem = [number, number];
type Plot = PlotItem[];
interface LineProps {
plot: Plot;
minY?: number;
maxY?: number;
color?: LineColor;
style?: LineStyle;
lineWidth?: number;
}
export function Line({
plot,
minY: providedMinY,
maxY: providedMaxY,
color = LineColor.Red,
style = LineStyle.Solid,
lineWidth = 2,
}: LineProps) {
const xs = plot.map(([x]) => x);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const ys = plot.map(([, y]) => y);
const minY = providedMinY ?? Math.min(...ys);
const maxY = providedMaxY ?? Math.max(...ys);
const rangeX = maxX - minX;
const rangeY = maxY - minY;
const points = plot
.map(
([x, y]) =>
`${(x - minX) * (GRAPH_WIDTH / rangeX)},${
GRAPH_HEIGHT - (y - minY) * (GRAPH_HEIGHT / rangeY)
}`,
)
.join(' ');
return (
<polyline
vector-effect="non-scaling-stroke"
fill="none"
stroke={LINE_COLORS[color]}
strokeWidth={lineWidth}
stroke-dasharray={LINE_STYLES[style]}
points={points}
/>
);
}
@@ -1,4 +0,0 @@
export enum MarginOrientation {
Vertical,
Horizontal,
}
@@ -1,49 +0,0 @@
import { times } from 'lodash-es';
import { theme } from '../../../../stitches.config';
import { type MarginInputProps } from '../Root';
import { MarginOrientation } from './constants';
interface MarginProps extends MarginInputProps {
verticalSeparations: number;
orientation?: MarginOrientation;
}
export function Margin({
verticalSeparations,
max,
min,
suffix,
precision = 0,
orientation = MarginOrientation.Vertical,
}: MarginProps) {
const isVertical = orientation === MarginOrientation.Vertical;
return (
<div
style={{
paddingLeft: isVertical ? 0 : 64,
width: isVertical ? 48 : '100%',
display: 'flex',
flexDirection: isVertical ? 'column' : 'row-reverse',
justifyContent: 'space-between',
}}
>
{times(verticalSeparations, (index) => (
<span
key={index}
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{`${(
(max - min) *
((verticalSeparations - (index + 1)) /
(verticalSeparations - 1)) +
min
).toFixed(precision)}${suffix ?? ''}`}
</span>
))}
</div>
);
}
@@ -1,2 +0,0 @@
export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320;
@@ -1,109 +0,0 @@
import { times } from "lodash-es";
import { type ReactNode } from "react";
import { theme } from "../../../../stitches.config";
import { Margin } from "../Margin";
import { MarginOrientation } from "../Margin/constants";
import { GRAPH_HEIGHT, GRAPH_WIDTH } from "./constants";
export interface MarginInputProps {
min: number;
max: number;
suffix?: string;
precision?: number;
}
interface RootProps {
children: ReactNode;
verticalMargin?: MarginInputProps;
horizontalMargin?: MarginInputProps;
separations?: number;
xMinLabel: string;
xMaxLabel: string;
xTitle?: string;
}
export function Root({
children,
verticalMargin,
horizontalMargin,
separations = 5,
xMinLabel,
xMaxLabel,
xTitle,
}: RootProps) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div
style={{
paddingLeft: 64,
display: "flex",
justifyContent: "space-between",
}}
>
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
{xMinLabel}
</span>
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
{xMaxLabel}
</span>
</div>
<div style={{ display: "flex", gap: 16 }}>
{verticalMargin && (
<Margin {...verticalMargin} verticalSeparations={separations} />
)}
<svg
viewBox={`0 0 ${GRAPH_WIDTH} ${GRAPH_HEIGHT}`}
preserveAspectRatio="none"
style={{
flex: 1,
backgroundColor: theme.colors.appBackground2,
borderRadius: 4,
border: theme.borderStyles.nonInteractive,
}}
>
{times(separations - 2, (index) => {
const y = GRAPH_HEIGHT * ((index + 1) / (separations - 1));
return (
<polyline
key={index}
vector-effect="non-scaling-stroke"
fill="none"
stroke={theme.colors.borderNonInteractive}
strokeWidth="2"
points={`0,${y}, ${GRAPH_WIDTH},${y}`}
/>
);
})}
{children}
</svg>
</div>
{horizontalMargin && (
<Margin
{...horizontalMargin}
verticalSeparations={separations}
orientation={MarginOrientation.Horizontal}
/>
)}
{xTitle !== undefined && (
<div
style={{
paddingLeft: 64,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{xTitle}
</span>
</div>
)}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Line';
export * from './components/Margin';
export * from './components/Root';
@@ -1,29 +0,0 @@
import { theme } from '../../../stitches.config';
interface GapProps {
message: string;
}
export function Gap({ message: number }: GapProps) {
return (
<div
style={{
display: 'flex',
padding: 8,
backgroundColor: theme.colors.componentNonInteractive,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{number.toLocaleString()}
</span>
</div>
);
}
@@ -1,171 +0,0 @@
import { type ComponentProps } from "react";
import { theme } from "../../../stitches.config";
interface ItemProps extends ComponentProps<"div"> {
position: number;
deltaPosition?: number;
score?: number;
deltaScore?: number;
nickname: string;
clan?: string;
highlight?: boolean;
}
export function Item({
position,
deltaPosition,
score,
deltaScore,
nickname,
clan,
highlight,
...props
}: ItemProps) {
return (
<div
{...props}
style={{
cursor: "pointer",
display: "flex",
padding: 8,
backgroundColor: highlight
? theme.colors.componentInteractive_blue
: theme.colors.componentInteractive,
borderRadius: 4,
alignItems: "center",
justifyContent: "center",
gap: 8,
}}
>
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{position.toLocaleString()}.
</span>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{nickname}
</span>
{clan && (
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
[{clan}]
</span>
)}
</div>
{deltaPosition !== undefined && deltaPosition !== 0 && (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
<img
alt={deltaPosition > 0 ? "Increase" : "Decrease"}
style={{
width: 8,
height: 8,
}}
src={
deltaPosition > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
/>
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{Math.abs(deltaPosition).toLocaleString()}
</span>
</div>
)}
{/* TODO: lol remove this spacer div for a better solution */}
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
width: 96,
justifyContent: "flex-end",
}}
>
{deltaScore !== 0 && deltaScore !== undefined && (
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{Math.abs(deltaScore).toLocaleString()}
</span>
)}
{deltaScore !== 0 && deltaScore !== undefined && (
<img
alt={deltaScore > 0 ? "Increase" : "Decrease"}
style={{
width: 8,
height: 8,
}}
src={
deltaScore > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
/>
)}
{score !== undefined && (
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{score.toLocaleString()}
</span>
)}
</div>
</div>
);
}
@@ -1,13 +0,0 @@
import { type ReactNode } from 'react';
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{children}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Gap';
export * from './components/Item';
export * from './components/Root';
@@ -1,5 +0,0 @@
export enum NoDataType {
BattlesInPeriod,
PlayersInPeriod,
TanksFound,
}
@@ -1,30 +0,0 @@
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { theme } from '../../stitches.config';
interface NoDataProps {
type:
| 'battles_in_period'
| 'players_in_period'
| 'tanks_found'
| 'battles_in_season';
locale: Locale;
}
export function NoData({ type, locale }: NoDataProps) {
const { strings } = translator(locale);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{strings.bot.common.no_data[type]}
</span>
</div>
);
}
@@ -1,26 +0,0 @@
import { Percentile } from '@blitzkit/core';
import {
amber,
blue,
green,
lime,
orange,
pink,
purple,
red,
slate,
teal,
} from '@radix-ui/colors';
export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9,
[Percentile.Bad]: red.red9,
[Percentile.BelowAverage]: orange.orange9,
[Percentile.Average]: amber.amber10,
[Percentile.AboveAverage]: lime.lime8,
[Percentile.Good]: green.green9,
[Percentile.VeryGood]: teal.teal9,
[Percentile.Great]: blue.blue9,
[Percentile.Unicum]: pink.pink9,
[Percentile.SuperUnicum]: purple.purple9,
};
@@ -1,19 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { PERCENTILE_COLORS } from './constants';
interface PercentileIndicatorProps {
percentile: Percentile;
}
export function PercentileIndicator({ percentile }: PercentileIndicatorProps) {
return (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: PERCENTILE_COLORS[percentile],
}}
/>
);
}
Loaded 100 of 844 files, more files were not shown because too many files have changed in this diff. Show more