Compare commits

...
456 Commits
Author SHA1 Message Date
TrèsAbhi 6a1b786dae fix: [] 2024-05-03 09:19:29 -05:00
TrèsAbhi ea1eeb4366 fix: first time tank deltas 2024-05-03 09:17:09 -05:00
TrèsAbhi 15bec37a33 feat: logs 2024-05-03 09:10:23 -05:00
TrèsAbhi 4bba794bfb feat: disable login 2024-05-03 09:04:35 -05:00
TrèsAbhi 82014845cb feat: add column 2024-05-03 09:02:06 -05:00
TrèsAbhi 74a83d17e7 feat: better layout 2024-05-02 18:27:27 -05:00
TrèsAbhi 26ea06b091 fix: undef window 2024-05-02 18:08:58 -05:00
TrèsAbhi 352cb70830 feat: totals 2024-05-01 22:11:12 -05:00
TrèsAbhi ff92b3dd45 feat: login auth 2024-05-01 21:04:31 -05:00
TrèsAbhi 99ed269723 fix: old code 2024-05-01 20:56:38 -05:00
TrèsAbhi c24639dd0c feat: deltas 2024-05-01 20:54:27 -05:00
TrèsAbhi cceebd1980 fix: redirect to tracker 2024-05-01 20:36:35 -05:00
TrèsAbhi 596018b3b2 feat: ghost login button 2024-05-01 20:27:10 -05:00
TrèsAbhi 8bc3e2c6b9 feat: stat generalzation 2024-05-01 17:55:13 -05:00
TrèsAbhi aafba7b6c8 feat: link 2024-05-01 17:29:40 -05:00
TrèsAbhi d56be26db2 feat: since timer 2024-05-01 17:23:33 -05:00
TrèsAbhi e740274e5a feat: table 2024-05-01 17:20:08 -05:00
TrèsAbhi 0634804914 Merge pull request #116 from tresabhi/main 2024-05-01 16:37:22 -05:00
TrèsAbhi 4d908493f1 feat: experimental 2024-05-01 16:36:34 -05:00
TrèsAbhi 23d43f131b fix: capitalize K 2024-05-01 16:35:28 -05:00
TrèsAbhi 8a1b8673dc feat: updated favicon 2024-05-01 16:34:46 -05:00
TrèsAbhi 8682b2ea78 feat: rearranged navbar 2024-05-01 16:33:22 -05:00
TrèsAbhi 6e32b41b15 feat: convinient login 2024-05-01 13:04:31 -05:00
TrèsAbhi 6223773c71 feat: move health to right 2024-04-30 22:20:31 -05:00
TrèsAbhi 7fd4c931e5 feat: remove flair 2024-04-30 10:01:23 -05:00
TrèsAbhi 1011c76cda feat: add n90 2024-04-30 09:57:37 -05:00
TrèsAbhi 38fa15ab97 feat: cc account tracking 2 2024-04-28 19:16:08 -05:00
TrèsAbhi d2b18360ed feat: cc account tracking 2024-04-28 18:43:12 -05:00
TrèsAbhi 6ae86a7a70 feat: some protobuf parsing 2024-04-28 17:06:31 -05:00
TrèsAbhi 6b175238b2 feat: subtitle 2024-04-27 11:07:58 -05:00
TrèsAbhi 2a2a7a067c feat: map definitions 2024-04-27 10:50:09 -05:00
TrèsAbhi 25788dedf4 feat: merge types 2024-04-26 12:51:35 -05:00
TrèsAbhi 391990676f feat: wn8 and labels 2024-04-26 12:34:59 -05:00
TrèsAbhi 8c16fdf9b5 fix 2024-04-25 14:00:13 -05:00
TrèsAbhi c30358248f fix: use production videos 2024-04-25 14:00:00 -05:00
TrèsAbhi 62b218a44d feat: some more functionality 2024-04-25 13:47:30 -05:00
TrèsAbhi 1f30d1d2c3 feat: mostly complete rating command 2024-04-24 16:22:48 -05:00
TrèsAbhi b1917c73f2 Update Party3.tsx 2024-04-24 08:13:18 -05:00
TrèsAbhi 2d3325f13a fix: dir 2024-04-23 16:08:13 -05:00
TrèsAbhi 983b96fd99 feat: some work 2024-04-23 16:07:59 -05:00
TrèsAbhi 2c105d7a89 feat: some unfinished ideas 2024-04-23 15:51:16 -05:00
TrèsAbhi 28348552f0 fix: watermark 2024-04-23 15:07:21 -05:00
TrèsAbhi 449f2b0677 fix: more updates to docs 2024-04-23 15:06:24 -05:00
TrèsAbhi 5346ab576f fix: realign 2024-04-23 10:33:24 -05:00
TrèsAbhi d041b353c9 feat: logs 2024-04-23 10:25:59 -05:00
TrèsAbhi 77575168a0 feat: better message 2024-04-23 10:24:00 -05:00
TrèsAbhi 051997d2d3 feat: kit 3 2024-04-23 10:19:32 -05:00
TrèsAbhi e53bdd8e19 fix: core 2024-04-23 09:57:59 -05:00
TrèsAbhi 4013192faa feat: kit 2 2024-04-23 09:51:32 -05:00
TrèsAbhi 3da009f859 feat: kit 2024-04-23 09:42:08 -05:00
TrèsAbhi 2a5888c033 feat: hero 2024-04-22 12:51:46 -05:00
TrèsAbhi ded98d78d4 fix: link 2024-04-22 12:48:07 -05:00
TrèsAbhi f4b231de10 fix: more paths 2024-04-22 12:46:47 -05:00
TrèsAbhi 9fda3c6861 fix: videos 2024-04-22 12:46:15 -05:00
TrèsAbhi d3f58d9cda Merge pull request #111 from tresabhi/dev-rinth 2024-04-22 12:43:26 -05:00
TrèsAbhi 3ef793a776 fix: rinth package 2024-04-22 12:43:23 -05:00
TrèsAbhi 13a901c839 Merge branch 'main' into dev-rinth 2024-04-22 12:43:22 -05:00
TrèsAbhi 5ede2324b3 Update Party3.tsx 2024-04-22 12:27:43 -05:00
TrèsAbhi 4e8f1a4a71 fix: ? 2024-04-22 10:27:46 -05:00
TrèsAbhi eae9cbf971 feat: stop excessive tank armor and model name spam 2024-04-22 10:07:23 -05:00
TrèsAbhi 0eac25f1c8 Merge pull request #108 from tresabhi/imgbot 2024-04-22 10:01:36 -05:00
TrèsAbhi c281e9b66f feat: cumulative video indexing 2024-04-22 09:38:00 -05:00
TrèsAbhi eb7fc62e00 Merge pull request #110 from tresabhi/main 2024-04-22 09:09:54 -05:00
TrèsAbhi d5f83c444e feat; gap 2024-04-21 19:45:47 -05:00
TrèsAbhi c890bfa8bd feat: move up 2024-04-21 19:41:42 -05:00
TrèsAbhi 9ead3eb627 feat: automatic videos 2024-04-21 19:40:38 -05:00
TrèsAbhi b8db78c328 fix: secrets 2024-04-21 18:26:15 -05:00
TrèsAbhi 8b65b94851 feat: force 2024-04-21 18:01:02 -05:00
TrèsAbhi 9da723ed31 feat: logs 2024-04-21 17:40:04 -05:00
TrèsAbhi 2f48dbc25c feat: more shell properties 2024-04-21 17:39:24 -05:00
TrèsAbhi 730d6f2bb8 feat: logs 2024-04-21 17:25:35 -05:00
TrèsAbhi 335ae4493f fix: render normal armor 2024-04-21 17:16:25 -05:00
TrèsAbhi eb83039967 feat: include lower part of barrel 2024-04-21 17:15:23 -05:00
TrèsAbhi 6e91810565 fix: select all rows 2024-04-21 17:05:35 -05:00
TrèsAbhi a55c259205 feat: videos automation 2024-04-21 17:05:07 -05:00
TrèsAbhi b5188331bd feat: remove textual reference 2024-04-21 12:17:33 -05:00
TrèsAbhi 2b354770c7 fix: fragment key 2024-04-21 11:16:46 -05:00
TrèsAbhi 11e4009303 fix: skill keys 2024-04-21 11:13:32 -05:00
TrèsAbhi 9bce5e5d75 fix: duplicate keys 2024-04-21 11:12:52 -05:00
TrèsAbhi 8e8d68a5b5 fix: antagonist bar keys 2024-04-21 11:10:44 -05:00
TrèsAbhi 2c6fcdf21f fix: navbar menu key 2024-04-21 11:09:53 -05:00
TrèsAbhi abb72fb674 fix: jsx tree node uuids 2024-04-21 11:09:24 -05:00
TrèsAbhi b805c2e3b4 fix: video key 2024-04-21 11:07:19 -05:00
TrèsAbhi 7e6aa41ebb fix: no undefined clipping planes 2024-04-21 11:06:40 -05:00
TrèsAbhi a2cfbda932 feat: start work of text 2024-04-21 11:05:42 -05:00
TrèsAbhi 0b222be6ef Merge pull request #109 from tresabhi/main 2024-04-21 10:50:25 -05:00
TrèsAbhi 1548813a21 feat: horizontal ad 2024-04-21 10:41:51 -05:00
TrèsAbhi c615284d87 feat: re-enable adsense 2024-04-21 10:37:44 -05:00
TrèsAbhi 6a48638171 fix: use client 2024-04-21 10:26:08 -05:00
TrèsAbhi 0b4829abd3 fix: use use effect 2024-04-21 10:25:43 -05:00
TrèsAbhi 57cd3aae50 feat: pub ads 2024-04-21 10:21:52 -05:00
TrèsAbhi 6298d6230f Update ads.txt 2024-04-21 09:13:56 -05:00
TrèsAbhi df7a5bb8a0 fix: fullscreen card 2024-04-20 21:40:41 -05:00
TrèsAbhi dd41cac3ac feat: videos 2024-04-20 21:01:28 -05:00
TrèsAbhi f17b275aa7 feat: add blitzkrieg name to older logs 2024-04-20 12:09:23 -05:00
TrèsAbhi 4a04f8a8f3 feat: generalized asset repo 2024-04-20 12:08:43 -05:00
TrèsAbhi 0f7d682782 feat: logos 2024-04-20 12:07:20 -05:00
TrèsAbhi 14cecdebe3 feat: reset asset name to krieg temporarily 2024-04-20 12:02:33 -05:00
TrèsAbhi c2b936c925 feat: rename to blitzrinth 2024-04-20 12:00:48 -05:00
TrèsAbhi 0fd17d9c96 feat: logs 2024-04-20 11:50:16 -05:00
TrèsAbhi b0645c39a0 fix: high end consumables increase 2024-04-20 11:45:41 -05:00
TrèsAbhi 1d947399ab fix: intra-clip for auto loaders 2024-04-20 11:43:15 -05:00
TrèsAbhi 659dddbe8e feat: aggressive donate button 2024-04-20 11:40:10 -05:00
TrèsAbhi a5c1223f7d feat: simpler footer 2024-04-20 11:20:52 -05:00
TrèsAbhi dc084f8d34 feat: compare button 2024-04-19 10:08:00 -05:00
TrèsAbhi 44b5907435 feat: tankopedia button 2024-04-19 09:59:21 -05:00
ImgBotApp 052fbec628 [ImgBot] Optimize images
*Total -- 6,696.66kb -> 4,836.70kb (27.77%)

/public/assets/banners/discord.png -- 1,632.36kb -> 921.18kb (43.57%)
/public/assets/banners/compare.png -- 2,571.28kb -> 1,799.75kb (30.01%)
/docs/public/assets/images/hero.png -- 183.03kb -> 147.76kb (19.27%)
/public/assets/banners/statistics.png -- 2,309.99kb -> 1,968.00kb (14.8%)

Signed-off-by: ImgBotApp <[email protected]>
2024-04-19 14:37:54 +00:00
TrèsAbhi 23d8c54366 feat: super duper secret map 2024-04-19 09:26:53 -05:00
TrèsAbhi dd88d42884 feat: logs 2024-04-19 09:17:22 -05:00
TrèsAbhi ed676f73b2 feat: og descs 2024-04-19 09:14:22 -05:00
TrèsAbhi adb0a9af64 fix: title 2024-04-19 09:11:36 -05:00
TrèsAbhi ccded1b2f8 feat: hide statistics 2024-04-19 09:10:33 -05:00
TrèsAbhi 89d43b4791 feat: better titles 2024-04-19 09:09:12 -05:00
TrèsAbhi 2226ac9852 feat: gutted some navbar features 2024-04-19 09:03:45 -05:00
TrèsAbhi 29bace875e fix: remove commit 2024-04-18 20:27:48 -05:00
TrèsAbhi 04107c10b7 feat: hamburger meny 2024-04-18 20:26:35 -05:00
TrèsAbhi 0a1e823ce4 feat: first developer mode enabling 2024-04-18 19:16:30 -05:00
TrèsAbhi c656d31618 feat: a few settings 2024-04-18 19:11:50 -05:00
TrèsAbhi 7eda4825ea feat: more navbar buttons 2024-04-18 18:42:57 -05:00
TrèsAbhi 60f0eb9a3c feat: tankopedia title 2024-04-18 18:29:29 -05:00
TrèsAbhi ce5c9f4afa feat: page exclusive titles 2024-04-18 18:28:46 -05:00
TrèsAbhi e834d31d92 feat: use lz4js types instead of home brewed 2024-04-18 18:23:23 -05:00
TrèsAbhi b679393b8a Merge branch 'main' into dev 2024-04-17 10:16:31 -05:00
TrèsAbhi eb5f79228b fix: bring back test tanks button 2024-04-17 09:52:15 -05:00
TrèsAbhi 6082576b60 feat: start of a meny 2024-04-17 09:50:46 -05:00
TrèsAbhi 92e9073939 things 2024-04-17 09:31:47 -05:00
TrèsAbhi d6669a84d6 Update index.ts 2024-04-17 08:34:20 -05:00
TrèsAbhi 0c4debe327 Update index.ts 2024-04-17 08:18:56 -05:00
TrèsAbhi 9f41bab28a feat: disabled stats 2024-04-16 22:13:21 -05:00
TrèsAbhi b906b42998 feat: search bar 2024-04-16 22:12:51 -05:00
TrèsAbhi 728a11efa4 feat: reduced gap 2024-04-16 22:06:24 -05:00
TrèsAbhi c35916c291 feat: colors 2024-04-16 22:05:36 -05:00
TrèsAbhi 2fd2e93f55 feat: shadows 2024-04-16 21:58:08 -05:00
TrèsAbhi b566d5982c feat: better icons 2024-04-16 21:49:30 -05:00
TrèsAbhi bef43e9ae6 feat: tank icons 2024-04-16 19:36:49 -05:00
TrèsAbhi e6404d5c77 feat: better spacing 2024-04-16 19:07:06 -05:00
TrèsAbhi f4d58015f5 feat: somethings 2024-04-16 19:03:37 -05:00
TrèsAbhi cd77d24dc6 feat: statistics 2024-04-16 18:51:20 -05:00
TrèsAbhi e25a3cd868 feat: title 2024-04-16 13:53:01 -05:00
TrèsAbhi 68923fe800 feat: icons 2024-04-16 13:49:33 -05:00
TrèsAbhi ccc6ddd5c9 Merge branch 'main' of https://github.com/tresabhi/blitzkrieg 2024-04-16 13:45:33 -05:00
TrèsAbhi 1c28b33da3 Merge branch 'dev' of https://github.com/tresabhi/blitzkrieg into dev 2024-04-16 13:45:18 -05:00
TrèsAbhi 900337f3ce feat: log 2024-04-16 13:45:17 -05:00
TrèsAbhi fae9f8bded feat: time in manifest 2024-04-16 13:45:03 -05:00
TrèsAbhi a0d78f89ff fix: remove logo on small screens 2024-04-16 13:45:03 -05:00
TrèsAbhi 6853d63bbb feat: change to utc 2024-04-16 13:45:03 -05:00
TrèsAbhi 2b27954a3b fix: max dpm on double shot auto reloaders 2024-04-16 13:45:03 -05:00
TrèsAbhi 7aa0b65d1d feat: null handling 2024-04-16 13:45:03 -05:00
TrèsAbhi 3e7a73c528 feat: cleaner fetch blitz func 2024-04-16 13:45:02 -05:00
TrèsAbhi 076785a9ad feat: more efficient 2024-04-16 13:45:02 -05:00
TrèsAbhi 74828e3c45 feat: rts 2024-04-16 13:45:02 -05:00
TrèsAbhi 310640c1ec feat: base work 2024-04-16 13:45:02 -05:00
TrèsAbhi c49c56fba3 Merge pull request #107 from tresabhi/dev 2024-04-16 13:44:16 -05:00
TrèsAbhi cc744f6154 Merge branch 'main' into dev 2024-04-16 13:44:09 -05:00
TrèsAbhi 93bf4a4742 Dev (#106) 2024-04-16 13:43:44 -05:00
TrèsAbhi badeb588dd feat: time in manifest 2024-04-16 09:42:48 -05:00
TrèsAbhi 5876bf35b4 fix: remove logo on small screens 2024-04-16 09:35:10 -05:00
TrèsAbhi 73903114c9 feat: change to utc 2024-04-16 09:28:01 -05:00
TrèsAbhi b29f7ca7f5 fix: max dpm on double shot auto reloaders 2024-04-16 09:24:28 -05:00
TrèsAbhi 9d715c23e5 redacted 2024-04-16 09:18:12 -05:00
TrèsAbhi 4ff5f7cdeb feat: null handling 2024-04-15 13:25:35 -05:00
TrèsAbhi dccfe3d652 feat: cleaner fetch blitz func 2024-04-15 13:19:25 -05:00
TrèsAbhi d780d32291 feat: more efficient 2024-04-15 09:27:59 -05:00
TrèsAbhi ebbdbc53a4 feat: rts 2024-04-13 22:13:57 -05:00
TrèsAbhi 9bc1635680 feat: base work 2024-04-13 18:20:49 -05:00
TrèsAbhi d7d19a0b50 Merge branch 'main' of https://github.com/tresabhi/blitzkrieg 2024-04-13 10:43:20 -05:00
TrèsAbhi 1b6845abf8 fix: grammar 2024-04-13 10:43:09 -05:00
TrèsAbhi 545ee70cd0 feat: logs 2 2024-04-13 10:42:31 -05:00
TrèsAbhi c92729d0e0 feat: logs 2024-04-13 10:42:17 -05:00
TrèsAbhi 71c3b5d2ef feat: undefined mask handling 2024-04-13 10:38:13 -05:00
TrèsAbhi 7372075ba3 fix: remove duplicate mantlets 2024-04-13 10:36:13 -05:00
TrèsAbhi f119b06dd8 fix: grammar 2024-04-12 09:24:15 -05:00
TrèsAbhi e23a440508 feat: logs 2024-04-12 09:23:34 -05:00
TrèsAbhi 6a9fa08500 fix: /research tank icon alignment 2024-04-12 09:17:05 -05:00
TrèsAbhi 8e86e81e91 fix: max probisions 2024-04-10 19:35:28 -05:00
TrèsAbhi 8b359e220a feat: more icon gap 2024-04-10 17:04:33 -05:00
TrèsAbhi 690bc5dbb5 feat: wrapping controls 2024-04-10 16:57:38 -05:00
TrèsAbhi d6e96e17ad feat: spall liner 2024-04-10 16:54:16 -05:00
TrèsAbhi ab26b346e6 feat: lenient psa 2024-04-10 16:29:12 -05:00
TrèsAbhi f02db6ef0b fix: clipping hero stat description 2024-04-10 16:19:24 -05:00
TrèsAbhi 55ba2525d1 fix: query ignorance on filter change 2024-04-10 16:13:39 -05:00
TrèsAbhi ea07d2646d feat: name draggable 2024-04-10 16:10:34 -05:00
TrèsAbhi ca111510f6 feat: remove caret icons 2024-04-10 16:05:30 -05:00
TrèsAbhi 519558dd28 fix: drag on mobile 2024-04-10 16:04:50 -05:00
TrèsAbhi c4d1e305f6 feat: grabbing cursor 2024-04-10 09:41:55 -05:00
TrèsAbhi 59772afb04 feat: drag and drop 2024-04-10 09:40:52 -05:00
TrèsAbhi 8c9dcfa551 feat: change name to just compare 2024-04-10 09:03:56 -05:00
TrèsAbhi 8bbf03a128 feat: version 2024-04-09 16:55:09 -05:00
TrèsAbhi 519fba84dc feat: ordered asset building 2024-04-09 16:03:07 -05:00
TrèsAbhi e7e74f706e feat: more provisions 2024-04-09 10:51:12 -05:00
TrèsAbhi 12e2793c92 feat: dynamic armor 2024-04-09 10:39:26 -05:00
TrèsAbhi 69040dc453 feat: reactive armor 2024-04-09 10:35:32 -05:00
TrèsAbhi 511151e101 feat: shells 2024-04-09 10:23:10 -05:00
TrèsAbhi 7fc993c2eb feat: shells 2024-04-09 10:04:43 -05:00
TrèsAbhi 030ca744e9 fix: correct shell count 2024-04-09 10:03:30 -05:00
TrèsAbhi 74756ab7bf feat: crew actions 2024-04-09 09:56:31 -05:00
TrèsAbhi e9a61d579d fix: update stats with crew skills 2024-04-09 09:51:22 -05:00
TrèsAbhi 7c3cad2c7d feat: crew button and missing traverse stats 2024-04-09 09:47:57 -05:00
TrèsAbhi 5f39ba55f6 feat: skip bot definitions 2024-04-08 22:04:50 -05:00
TrèsAbhi 2bfd8286a4 feat: cleanup 2024-04-08 21:50:29 -05:00
TrèsAbhi f3421b6f70 fix: types 2024-04-08 21:44:52 -05:00
TrèsAbhi abbbf11b3d feat: adsense 2024-04-08 21:43:47 -05:00
TrèsAbhi 797f5a90d2 feat: watermark 2 2024-04-08 21:24:24 -05:00
TrèsAbhi 818d13fade feat: watermark 2024-04-08 21:17:59 -05:00
TrèsAbhi 129d7c9688 feat: skills title 2024-04-08 19:26:49 -05:00
TrèsAbhi 28407b0f05 feat: universal crew skills 2024-04-08 19:24:49 -05:00
TrèsAbhi 8a9d72d0e6 feat: order 2024-04-08 19:12:29 -05:00
TrèsAbhi f98a4c7caa fix: button popover order 2024-04-08 19:10:02 -05:00
TrèsAbhi 92b329a0fb feat: module quick buttons 2024-04-08 19:04:07 -05:00
TrèsAbhi f3d32a4e63 feat: module swapping 2024-04-08 19:01:31 -05:00
TrèsAbhi 0e73d79c9c feat: trimmed module icons 2024-04-08 09:23:26 -05:00
TrèsAbhi 526b5785b7 feat: modules button 2024-04-08 09:22:27 -05:00
TrèsAbhi 02129feb25 fix: NaN deltas 2024-04-08 09:13:14 -05:00
TrèsAbhi 0a38c164a4 fix: removed debug console logs 2024-04-06 22:01:52 -05:00
TrèsAbhi 2e0d52e30e feat: enter to select first tank 2024-04-06 22:00:40 -05:00
TrèsAbhi 6d48f9fe7f idk 2024-04-06 21:40:21 -05:00
TrèsAbhi aba52c9968 feat: consistent icons 2024-04-06 20:35:30 -05:00
TrèsAbhi b96b9fd574 fix: damage decimals 2024-04-06 20:29:36 -05:00
TrèsAbhi fe4ee8b611 feat: more compact 2024-04-06 19:40:40 -05:00
TrèsAbhi 94558393d9 feat: consumables 2024-04-06 19:37:57 -05:00
TrèsAbhi db1e4af20c feat: clear provisions button 2024-04-06 19:19:46 -05:00
TrèsAbhi be20173acd feat: missing provisions 2024-04-06 19:16:16 -05:00
TrèsAbhi d90a70ed2a fix: max selected 2024-04-06 19:09:37 -05:00
TrèsAbhi 85d699dbfc feat: provisions 2024-04-06 19:04:57 -05:00
TrèsAbhi 5d0fe8726b feat: provision buttons 2024-04-06 18:55:34 -05:00
TrèsAbhi 850308b2ac feat: provision assignment 2024-04-06 18:37:21 -05:00
TrèsAbhi 88a85ef152 feat: rename to absolute 2024-04-06 18:30:36 -05:00
TrèsAbhi 8160935081 feat: better control layout 2024-04-06 18:28:16 -05:00
TrèsAbhi c057e295d2 feat: discard sorting on mutate 2024-04-06 18:19:45 -05:00
TrèsAbhi 28d0c26268 feat: change to clear 2024-04-06 17:55:39 -05:00
TrèsAbhi 52cb73a575 feat: gray sort buttons 2024-04-06 17:54:50 -05:00
TrèsAbhi 9f244452da feat: max tank name size 2024-04-06 17:53:22 -05:00
TrèsAbhi d46d948c66 feat: sort 2024-04-06 17:46:30 -05:00
TrèsAbhi 11b99ef211 feat: better compact search chunking 2024-04-06 17:28:45 -05:00
TrèsAbhi 85d350a912 feat: less CLS loader 2024-04-06 17:21:38 -05:00
TrèsAbhi 145bc84fb1 feat: no player selected 2024-04-06 13:18:17 -05:00
TrèsAbhi b7486022eb feat: cleaner home page 2024-04-06 13:12:07 -05:00
TrèsAbhi eadb4bb7b1 fix: no padding in embeds 2024-04-06 13:06:48 -05:00
TrèsAbhi 79723157d2 feat: ghost equipment buttons 2024-04-06 13:03:36 -05:00
TrèsAbhi 4214ff0de5 feat: trash button functionality 2024-04-06 12:51:52 -05:00
TrèsAbhi f516c9275e feat: flex 1 pages 2024-04-06 12:49:38 -05:00
TrèsAbhi f0b905593c feat: better loading spinners 2024-04-06 12:43:54 -05:00
TrèsAbhi 11440bc654 feat: non nested spans 2024-04-06 12:37:50 -05:00
TrèsAbhi 2dab957a05 fix: scroll padding 2024-04-06 12:28:29 -05:00
TrèsAbhi 60f3951e7a feat: remove scrollbar 2 2024-04-06 12:23:08 -05:00
TrèsAbhi 4f03788f15 feat: remove scrollbar 2024-04-06 12:22:56 -05:00
TrèsAbhi 17f4435a30 feat: isolated footer 2024-04-06 12:10:54 -05:00
TrèsAbhi 04e42c929c feat: blitz version 2024-04-06 11:15:39 -05:00
TrèsAbhi 4ba45b8fd8 feat: game definitions 2024-04-06 11:14:07 -05:00
TrèsAbhi 0ece1f4b18 feat: no tanks selected message 2024-04-06 10:57:02 -05:00
TrèsAbhi 49d3e1d1c2 feat: select all option 2024-04-06 10:36:07 -05:00
TrèsAbhi 184ca522dd feat: proper keying 2024-04-06 10:17:48 -05:00
TrèsAbhi 9f07d910b8 feat: generalized equipment matrix manager 2024-04-06 10:06:59 -05:00
TrèsAbhi ac3f2ea93a feat: themed equipment preview 2024-04-06 09:52:28 -05:00
TrèsAbhi a77ff1a638 fix: pen 0 decimals 2024-04-06 09:51:16 -05:00
TrèsAbhi 96d802a09f feat: more compact icons 2024-04-05 17:55:07 -05:00
TrèsAbhi 1babd66649 feat: loop icon 2024-04-05 17:53:56 -05:00
TrèsAbhi 2f2996a40c feat: equipment popover 2024-04-05 17:52:41 -05:00
TrèsAbhi 9d1e059ea5 feat: solid backgrounds 2024-04-05 17:44:31 -05:00
TrèsAbhi 48603b918f feat: equipment preview 2024-04-05 17:40:14 -05:00
TrèsAbhi c1253e8121 feat: equipment matrix visual start 2024-04-05 17:28:58 -05:00
TrèsAbhi ecc029402e feat: percentage 2024-04-05 14:05:15 -05:00
TrèsAbhi 1b88773e28 feat: nominal deltas 2024-04-05 13:59:30 -05:00
TrèsAbhi cf419a9ebb feat: better titles 2024-04-05 13:40:18 -05:00
TrèsAbhi 8565d75747 feat: double gain 2024-04-05 09:51:22 -05:00
TrèsAbhi 186ea8b458 feat: round 2024-04-05 09:38:48 -05:00
TrèsAbhi 4292081df6 feat: stronger colors 2024-04-05 09:35:55 -05:00
TrèsAbhi 4b54b61c35 feat: gradient interpolation 2024-04-05 09:35:03 -05:00
TrèsAbhi 5d098c46fa feat: titles 2024-04-05 09:31:21 -05:00
TrèsAbhi 7575f4581c feat: decimal shorthand 2024-04-05 09:20:07 -05:00
TrèsAbhi 4669fa6ec5 feat: significant change colors 2024-04-05 09:13:13 -05:00
TrèsAbhi 49294d1906 feat: volume 2024-04-05 09:07:29 -05:00
TrèsAbhi 3eee946978 feat: split volume 2024-04-05 09:06:44 -05:00
TrèsAbhi 7fff1c9737 feat: volume 2024-04-04 22:23:06 -05:00
TrèsAbhi 5b4c3119dd feat: average shell reload 2024-04-04 22:17:36 -05:00
TrèsAbhi 0a41ee15ed feat: lower is better 2024-04-04 22:15:33 -05:00
TrèsAbhi ad57d284bb feat: more stats 3 2024-04-04 22:14:30 -05:00
TrèsAbhi b929e9449d feat: more stats 2 2024-04-04 21:56:00 -05:00
TrèsAbhi 948ea0bb8d fix: types 2024-04-04 21:17:40 -05:00
TrèsAbhi c1b1b8d304 feat: more stats 2024-04-04 19:14:22 -05:00
TrèsAbhi 3bc5a6d740 fix: some bugs 2024-04-04 18:56:51 -05:00
TrèsAbhi d25da724e1 feat: some button functionality 2024-04-04 16:14:01 -05:00
TrèsAbhi 753804b8cf feat: some more stats 2 2024-04-04 15:52:05 -05:00
TrèsAbhi 096122d43c feat: some more stats 2024-04-04 15:47:27 -05:00
TrèsAbhi 3448427de0 feat: reintroduced links 2024-04-04 15:35:27 -05:00
TrèsAbhi d8bbd02715 feat: center aline 2024-04-04 13:51:39 -05:00
TrèsAbhi f696c5f3c9 feat: some stats 2024-04-04 13:51:07 -05:00
TrèsAbhi bd412cdaa2 feat: generalized tank stats 2024-04-04 13:16:12 -05:00
TrèsAbhi 913411275b feat: better header alignemnt 2024-04-04 11:05:56 -05:00
TrèsAbhi 17ff8e0819 feat: better sizing 2024-04-04 11:04:27 -05:00
TrèsAbhi b5fdc4d5cf feat: add functionality 2024-04-04 10:46:48 -05:00
TrèsAbhi 59c5d95aff feat: compare page 2024-04-04 10:21:37 -05:00
TrèsAbhi 16babbad90 feat: og tool icon 2024-04-04 10:18:04 -05:00
TrèsAbhi a9dc9e7ebd feat: cleaner tool shadows 2024-04-04 10:16:47 -05:00
TrèsAbhi 52e8df560e feat: added tank compare link 2024-04-04 10:15:01 -05:00
TrèsAbhi 87259ad515 feat: robots 2024-04-03 22:57:28 -05:00
TrèsAbhi c96655a6c0 feat: xml 2024-04-03 22:47:10 -05:00
TrèsAbhi 43894567b8 / 2024-04-03 22:37:38 -05:00
TrèsAbhi 76a952a8e6 fix: change to tests 2024-04-03 22:28:12 -05:00
TrèsAbhi e2efe428a2 feat: use require 2024-04-03 22:24:01 -05:00
TrèsAbhi 887cec2dd4 feat: use fs 2024-04-03 22:22:13 -05:00
TrèsAbhi 04ad48bd33 feat: use proper write file 2024-04-03 22:20:30 -05:00
TrèsAbhi 3613f5d13b feat: log 2024-04-03 22:18:41 -05:00
TrèsAbhi 3362e3b091 feat: js next config 2024-04-03 22:16:16 -05:00
TrèsAbhi 3322fa97d4 feat: ts next config 2024-04-03 22:14:55 -05:00
TrèsAbhi 1dc7748a04 feat: use script 2024-04-03 22:08:22 -05:00
TrèsAbhi 6176b3b0d8 feat: use sync 2024-04-03 22:04:36 -05:00
TrèsAbhi 81c2af08c6 fix: build sitemap before build 2024-04-03 22:02:02 -05:00
TrèsAbhi 2d83519f93 Merge branch 'dev' of https://github.com/tresabhi/blitzkrieg into dev 2024-04-03 21:58:49 -05:00
TrèsAbhi 869c1f0def feat: sitemap 2024-04-03 21:58:38 -05:00
TrèsAbhi 7a64fb8176 feat: preview image 2024-04-03 21:58:38 -05:00
TrèsAbhi 9256244286 fix: remove title from svg 2024-04-03 21:58:38 -05:00
TrèsAbhi 85e54e4a07 fix: build db 2 2024-04-03 21:58:38 -05:00
TrèsAbhi 8075dc8d9f fix: build db 2024-04-03 21:58:38 -05:00
TrèsAbhi 57dced109a feat: page descriptions 2024-04-03 21:58:38 -05:00
TrèsAbhi dd19d5b979 feat: gen prisma (#105) 2024-04-03 21:57:39 -05:00
TrèsAbhi 68d3fe6805 feat: sitemap 2024-04-03 21:56:20 -05:00
TrèsAbhi 70f7698466 feat: preview image 2024-04-03 19:48:28 -05:00
TrèsAbhi 26a5fdea0e fix: remove title from svg 2024-04-03 19:34:32 -05:00
TrèsAbhi 367bd1386b fix: build db 2 2024-04-03 19:31:10 -05:00
TrèsAbhi fac773a8f8 fix: build db 2024-04-03 19:28:25 -05:00
TrèsAbhi 6bd8b7d42f feat: page descriptions 2024-04-03 19:27:24 -05:00
TrèsAbhi 7093662130 feat: gen prisma 2024-04-03 15:34:39 -05:00
TrèsAbhi 4062e56b4c feat: migrated to new db 2024-04-03 15:19:48 -05:00
TrèsAbhi 0885e7e68a feat: logs 2024-04-02 10:37:39 -05:00
TrèsAbhi 3cfa837682 Merge branch 'main' of https://github.com/tresabhi/blitzkrieg into dev 2024-04-02 10:26:31 -05:00
TrèsAbhi 1982604878 fix: some more floating component removals 2024-04-02 10:21:51 -05:00
TrèsAbhi 9ef6eea4a9 fix: remove useless tooltips 2024-04-02 10:10:15 -05:00
TrèsAbhi 78e6b2696c fix: word tolerant pitch and yaw 2024-04-02 10:04:54 -05:00
TrèsAbhi 40b6f30092 fix: synced armor states 2 2024-04-02 10:02:21 -05:00
TrèsAbhi de2d9a1a3e fix: synced armor states 2024-04-02 09:52:46 -05:00
TrèsAbhi b7ad12c698 Create ads.txt 2024-04-02 08:24:37 -05:00
TrèsAbhi 886c2aa1d1 fix: access document on client 2 2024-04-01 22:02:15 -05:00
TrèsAbhi 9818c9868a fix: access document on client 2024-04-01 22:00:38 -05:00
TrèsAbhi 31055f1d66 fix: use link for big tank search 2024-04-01 21:57:54 -05:00
TrèsAbhi 6cef753053 feat: donation icons 2024-04-01 21:46:17 -05:00
TrèsAbhi 5b889f8483 feat: more accessible full screen mode 2024-04-01 21:33:47 -05:00
TrèsAbhi d0d905ccfb feat: better antagonist bar 2024-04-01 19:30:06 -05:00
TrèsAbhi 9a91a8bb13 fix: excessive antagonist bar gap 2024-04-01 19:17:59 -05:00
TrèsAbhi c12cc9b8c5 feat: length sorting 2024-04-01 19:12:59 -05:00
TrèsAbhi fdbfd0bc10 feat: rename size to volume 2024-04-01 19:10:42 -05:00
TrèsAbhi 286b178218 feat: some cleanup 2024-04-01 19:09:21 -05:00
TrèsAbhi 54e012f736 feat: dropdown modules 2024-04-01 19:05:55 -05:00
TrèsAbhi cc6de50de6 feat: upgrade to radix 3.0 2024-04-01 18:36:14 -05:00
TrèsAbhi 194e5fceca feat: crew training info 2024-04-01 09:31:02 -05:00
TrèsAbhi 879a5d534c feat: consumable info 2024-04-01 09:27:54 -05:00
TrèsAbhi 29aaf60da0 feat: cleaner info 2024-04-01 09:24:12 -05:00
TrèsAbhi 3c8f8b49e4 feat: condensed 2 2024-03-31 22:32:43 -05:00
TrèsAbhi 0b88f71f26 feat: condensed 2024-03-31 22:27:11 -05:00
TrèsAbhi 65e239a22b feat: hide module actions on no upgrades 2024-03-31 22:16:29 -05:00
TrèsAbhi 7727e64931 feat: quick actions 2024-03-31 22:14:14 -05:00
TrèsAbhi 7b0465322d feat: special skills 2024-03-31 19:27:37 -05:00
TrèsAbhi 099e5eb228 feat: skill stats 2024-03-31 19:17:30 -05:00
TrèsAbhi eaff26c8d9 feat: crew skills ui 2024-03-31 18:49:17 -05:00
TrèsAbhi d79129df91 feat: skill definitions 2024-03-31 18:24:19 -05:00
TrèsAbhi b363c76639 feat: crew skill icons 2024-03-31 18:17:48 -05:00
TrèsAbhi 034429cd47 Merge branch 'main' of https://github.com/tresabhi/blitzkrieg 2024-03-31 17:48:30 -05:00
TrèsAbhi 57867317cc fix: missing info 2024-03-31 17:48:15 -05:00
TrèsAbhi c2e314b193 feat: log 2024-03-31 17:48:01 -05:00
TrèsAbhi 9d2af1de13 fix: quick equipments 2024-03-31 17:45:22 -05:00
TrèsAbhi 232823566b fix: missing info 2024-03-30 12:48:17 -05:00
TrèsAbhi 958f0f2a52 fix: log version 2024-03-30 12:44:46 -05:00
TrèsAbhi b34fef2147 feat: logs 2024-03-30 12:44:34 -05:00
TrèsAbhi fe661124fa Merge branch 'dev' of https://github.com/tresabhi/blitzkrieg into dev 2024-03-30 12:39:19 -05:00
TrèsAbhi 457dcbcad5 fix: 0 battle handling 2024-03-30 12:38:53 -05:00
TrèsAbhi c18ee70eba fix: /rating use midnight leaderboard 2024-03-30 12:38:53 -05:00
TrèsAbhi 095ffc3d6a fix: sync on click inspector 2024-03-30 12:38:53 -05:00
TrèsAbhi e8b91648b1 fix: click through post inputs 2024-03-30 12:38:53 -05:00
TrèsAbhi 1b43f6ba99 fix: use final thickness 2024-03-30 12:38:53 -05:00
TrèsAbhi fc5360363c fix: uniform inv proj mat 2024-03-30 12:38:53 -05:00
TrèsAbhi 47b3c3029e fix: more accurate HE 2024-03-30 12:38:53 -05:00
TrèsAbhi dea7f34ca3 fix: use normal proj mat 2024-03-30 12:38:53 -05:00
TrèsAbhi 1e5026ea4a fix: persistent equipment 2024-03-30 12:38:53 -05:00
TrèsAbhi 98f568ff6a fix: use three js inverse view proj 2024-03-30 12:38:53 -05:00
TrèsAbhi 10a6f8371d fix: 0 battle handling 2024-03-30 12:33:17 -05:00
TrèsAbhi 0933aa6615 fix: /rating use midnight leaderboard 2024-03-30 12:32:48 -05:00
TrèsAbhi 746592418d Update cd-rating-latest.yml 2024-03-30 12:22:20 -05:00
TrèsAbhi e2404b063c fix: sync on click inspector 2024-03-30 12:14:49 -05:00
TrèsAbhi bae1301556 fix: click through post inputs 2024-03-30 12:13:47 -05:00
TrèsAbhi c2de4952ad fix: use final thickness 2024-03-30 11:34:39 -05:00
TrèsAbhi cce61d8075 fix: uniform inv proj mat 2024-03-30 11:31:44 -05:00
TrèsAbhi 34a0388f45 fix: more accurate HE 2024-03-30 11:28:34 -05:00
TrèsAbhi 9d62965fc3 fix: use normal proj mat 2024-03-30 10:39:58 -05:00
TrèsAbhi 892434e28f fix: persistent equipment 2024-03-30 10:30:30 -05:00
TrèsAbhi 2597fe4bcd fix: use three js inverse view proj 2024-03-30 10:23:23 -05:00
TrèsAbhi f85b4b36bd fix: async exec 2024-03-29 16:45:17 -05:00
TrèsAbhi f777dafefc feat: generated list 2024-03-29 16:42:56 -05:00
TrèsAbhi 6ef65ccea3 fix: don't install fixed sharp twice 2024-03-29 16:40:07 -05:00
TrèsAbhi 974b27a313 fix: production flag 2024-03-29 16:38:51 -05:00
TrèsAbhi 5124d8abcd Merge branch 'dev' of https://github.com/tresabhi/blitzkrieg into dev 2024-03-29 16:33:15 -05:00
TrèsAbhi 849bd356c0 feat: logs 2024-03-29 16:33:13 -05:00
TrèsAbhi 6406486441 New Crowdin updates (#104) 2024-03-28 10:33:46 -05:00
TrèsAbhi 2993ada2e5 fix: always absolute deltas 2024-03-28 10:33:05 -05:00
TrèsAbhi f083c18dd0 feat: more optimal fetching 2024-03-28 10:31:36 -05:00
TrèsAbhi c7021c5689 feat: finalized 2024-03-28 10:27:33 -05:00
TrèsAbhi 815369805a fix: null rating type safety 2024-03-25 21:11:04 -05:00
TrèsAbhi d8feb5c3a8 fix: null rating type safety 2024-03-25 19:22:21 -05:00
TrèsAbhi cb101da8a8 fix: types 2024-03-25 18:43:23 -05:00
TrèsAbhi 71481f5971 feat: renaming 2024-03-25 18:22:37 -05:00
TrèsAbhi 817796ed3c feat: full command 2024-03-25 18:12:33 -05:00
TrèsAbhi ebb7618f2d feat: hero stat 2024-03-25 18:00:07 -05:00
TrèsAbhi 9717f153cd New Crowdin updates (#103) 2024-03-25 10:20:01 -05:00
TrèsAbhi afa0cfaeef feat: start on final design 2024-03-24 22:06:48 -05:00
TrèsAbhi 298b89b227 feat: new rating stat style 2024-03-21 22:32:01 -05:00
TrèsAbhi 3b94034767 feat: change ratings path to rating 2024-03-21 16:44:47 -05:00
TrèsAbhi 444b4c21af feat: sitemao 2024-03-21 16:29:59 -05:00
TrèsAbhi 3e9c828781 New Crowdin updates (#102) 2024-03-21 14:19:41 -05:00
TrèsAbhi ca3a1fbe3f feat: ratings stats 2024-03-20 20:40:01 -05:00
TrèsAbhi 5f28b541e3 feat: started work on rating stats 2024-03-20 18:03:00 -05:00
TrèsAbhi 18edd349f2 fix: only exclude root test files 2024-03-20 17:37:56 -05:00
TrèsAbhi f7c6a065c1 feat: renamed /ratings to /ratings-leaderboard 2024-03-20 17:33:21 -05:00
TrèsAbhi 91d48eeadd feat: remove reward section 2024-03-20 17:04:59 -05:00
TrèsAbhi 21bafa1b7b feat: added more stats 2024-03-20 16:58:43 -05:00
TrèsAbhi 38af2f2817 feat: extended ratings stats 2024-03-19 18:21:56 -05:00
TrèsAbhi 8faa1143e9 fix: only build assets on push to main 2024-03-18 18:13:34 -05:00
TrèsAbhi 8f34ebbca4 feat: grouped ratings workflows 2024-03-18 18:12:55 -05:00
TrèsAbhi 1c03701e17 feat: ratings leaderboard disk format 2024-03-18 18:06:45 -05:00
TrèsAbhi c8edd16eb3 feat: logs 2024-03-18 16:47:53 -05:00
TrèsAbhi b96ff2e239 New Crowdin updates (#101) 2024-03-18 16:36:55 -05:00
TrèsAbhi a575da3005 fix: move script to head 2024-03-18 10:09:46 -05:00
TrèsAbhi 3ca2dece3e feat: google analytics 2024-03-18 10:02:09 -05:00
TrèsAbhi 191d6133b2 feat: togglable spaced armor 2024-03-17 22:11:56 -05:00
TrèsAbhi b15e73e3a8 Update build.ts 2024-03-17 21:53:09 -05:00
TrèsAbhi 9a6b8ab979 Update cd-host.yml 2024-03-17 21:51:48 -05:00
TrèsAbhi 87786d41a7 feat: logs 2024-03-17 21:48:23 -05:00
TrèsAbhi ee7ffe7bc8 feat: include sharp in build 2024-03-17 21:45:55 -05:00
TrèsAbhi 379a33375d feat: better totals 2024-03-17 21:40:21 -05:00
TrèsAbhi bd3f5dea05 feat: optimized png cache 2024-03-17 21:23:57 -05:00
TrèsAbhi 146d36234e New Crowdin updates (#99) 2024-03-17 21:19:24 -05:00
TrèsAbhi f3edcb8077 Update cd-assets.yml 2024-03-17 20:17:11 -05:00
TrèsAbhi 9e492015ff feat: jpg texture files 2024-03-16 15:34:48 -05:00
TrèsAbhi d2977182d9 fix: grammer 2024-03-16 15:24:25 -05:00
TrèsAbhi 7d2d65bab8 fix: install sharp 2024-03-16 15:18:23 -05:00
TrèsAbhi 415c3bd066 fix: fixed sharp 2024-03-16 15:15:34 -05:00
TrèsAbhi 990ab1520c Merge pull request #100 from tresabhi/dev 2024-03-16 15:12:54 -05:00
TrèsAbhi 81cb1afeff fix: types 2024-03-16 15:12:47 -05:00
TrèsAbhi dba5a0a2cd feat: logs 2024-03-16 15:10:55 -05:00
TrèsAbhi 04cd205fee feat: preview servers 2024-03-16 15:07:05 -05:00
TrèsAbhi ec93d8e9ab feat: better words 2024-03-16 10:41:25 -05:00
TrèsAbhi 56321d9e33 feat: simplified owned tanks command 2024-03-16 00:06:44 -05:00
TrèsAbhi d39ab43e74 feat: error handling for /research 2024-03-15 21:50:19 -05:00
TrèsAbhi 787d3ae6ea feat: functionality and icons 2024-03-15 12:28:29 -05:00
TrèsAbhi 035f751bf1 feat: remove protobuf 2024-03-15 10:47:05 -05:00
TrèsAbhi 43442ac829 feat: start of protobuf 2024-03-15 00:21:58 -05:00
TrèsAbhi da02a089b9 feat: xp definitions 2024-03-14 23:31:50 -05:00
TrèsAbhi 476d8e804a feat: some ui 2024-03-14 17:31:11 -05:00
TrèsAbhi 43e1d53d39 feat: resolving lines 2024-03-14 16:43:28 -05:00
TrèsAbhi 5372068830 feat: added trees 2024-03-13 23:23:06 -05:00
TrèsAbhi 025cec3687 Merge pull request #98 from tresabhi/main 2024-03-13 20:30:50 -05:00
TrèsAbhi c47c629a83 Merge pull request #95 from tresabhi/l10n 2024-03-13 20:30:09 -05:00
TrèsAbhi 2b7907f8f7 fix: use main 2024-03-13 20:29:28 -05:00
TrèsAbhi 903d4e4d95 fix: multiple equipments 2024-03-13 18:50:08 -05:00
TrèsAbhi 8857b2baae feat: production ready 2024-03-13 18:36:48 -05:00
TrèsAbhi 7169e0d91a New translations en-us.json (Spanish) 2024-03-13 05:37:55 -05:00
TrèsAbhi 1c9b7463fc New translations en-us.json (Croatian (disambiguation)) 2024-03-12 23:04:08 -05:00
TrèsAbhi 80741d66fc New translations en-us.json (Spanish) 2024-03-12 23:04:07 -05:00
374 changed files with 12730 additions and 5080 deletions

No files matched your search

+6 -3
View File
@@ -2,9 +2,10 @@ name: CD Build Assets
on: on:
push: push:
branches: main
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 2 * * *'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -52,7 +53,7 @@ jobs:
run: | run: |
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}" $env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}" $env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}" $env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
$env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}" $env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}"
yarn setup:patches --depot yarn setup:patches --depot
@@ -60,5 +61,7 @@ jobs:
run: | run: |
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}" $env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}" $env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}" $env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
$env:VIDEOS_SHEET = "${{ secrets.VIDEOS_SHEET }}"
$env:GOOGLE_APPLICATION_CREDENTIALS = "${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}"
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }} yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
- name: Download release assets - name: Download release assets
run: | run: |
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \ DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkit/releases/latest \
| grep "browser_download_url.*bot.zip" \ | grep "browser_download_url.*bot.zip" \
| cut -d : -f 2,3 \ | cut -d : -f 2,3 \
| tr -d \") | tr -d \")
@@ -33,4 +33,4 @@ jobs:
unzip bot.zip unzip bot.zip
- name: Run bot - name: Run bot
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} node --enable-source-maps index.cjs
@@ -1,11 +1,7 @@
name: CD Ratings Asia name: CD Rating Asia
# latest: '0 0 * * *'
# asia midnight: '0 17 * * *'
on: on:
schedule: schedule:
- cron: '0 0 * * *'
- cron: '0 17 * * *' - cron: '0 17 * * *'
jobs: jobs:
@@ -35,4 +31,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=asia --target=midnight --production
@@ -1,11 +1,7 @@
name: CD Ratings Europe name: CD Rating Europe
# latest: '0 0 * * *'
# eu midnight: '0 23 * * *'
on: on:
schedule: schedule:
- cron: '0 0 * * *'
- cron: '0 23 * * *' - cron: '0 23 * * *'
jobs: jobs:
@@ -35,4 +31,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=eu --target=midnight --production
+34
View File
@@ -0,0 +1,34 @@
name: CD Rating Latest
on:
schedule:
- cron: '0 0 * * *'
jobs:
host:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- name: Install Yarn
run: npm install --location=global yarn
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Generate all leaderboards
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=na,asia,eu --target=latest --production
@@ -1,11 +1,7 @@
name: CD Ratings North America name: CD Rating North America
# latest: '0 0 * * *'
# na midnight: '0 5 * * *'
on: on:
schedule: schedule:
- cron: '0 0 * * *'
- cron: '0 5 * * *' - cron: '0 5 * * *'
jobs: jobs:
@@ -35,4 +31,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DATABASE_URL=${{ secrets.DATABASE_URL }} yarn publish:ratingleaderboard --region=na --target=midnight --production
+4 -1
View File
@@ -33,8 +33,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Build Prisma
run: yarn prisma generate
- name: Build - name: Build
run: yarn build:bot run: yarn build:bot --production
- name: Archive build - name: Archive build
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+3 -2
View File
@@ -8,7 +8,6 @@ dist/
dist.* dist.*
yarn-error.log yarn-error.log
/test.* /test.*
test.*
/docs/.vitepress/dist /docs/.vitepress/dist
/docs/.vitepress/cache /docs/.vitepress/cache
servers.csv servers.csv
@@ -18,4 +17,6 @@ temp/
/.yarn /.yarn
/.VSCodeCounter /.VSCodeCounter
/.yarn /.yarn
depots depots
public/sitemap*
/firebase.json
+3 -3
View File
@@ -1,3 +1,3 @@
[submodule "submodules/closedkrieg"] [submodule "submodules/blitzkit-closed"]
path = submodules/closedkrieg path = submodules/blitzkit-closed
url = https://github.com/tresabhi/closedkrieg url = https://github.com/tresabhi/blitzkit-closed
+27 -2
View File
@@ -3,6 +3,7 @@
"AABBOX", "AABBOX",
"Abhi", "Abhi",
"accountinfo", "accountinfo",
"adsense",
"aeson", "aeson",
"alphatest", "alphatest",
"APAC", "APAC",
@@ -10,13 +11,20 @@
"applypatches", "applypatches",
"artefacts", "artefacts",
"atgm", "atgm",
"Avearges",
"AYUV", "AYUV",
"battleresults",
"bbox", "bbox",
"bepaint", "bepaint",
"betakrieg", "betakrieg",
"BINORMAL", "BINORMAL",
"Binormals", "Binormals",
"BKRL",
"BKRTS",
"BKRTSMANIFEST",
"bkwd", "bkwd",
"blitzkit",
"blitzrinth",
"blitzstars", "blitzstars",
"Camo", "Camo",
"CDON", "CDON",
@@ -31,6 +39,7 @@
"CUBETEXCOORD", "CUBETEXCOORD",
"customperiod", "customperiod",
"datasource", "datasource",
"dbid",
"Ddpf", "Ddpf",
"Ddsd", "Ddsd",
"debugflags", "debugflags",
@@ -71,12 +80,16 @@
"leaderboards", "leaderboards",
"lerp", "lerp",
"LINEARSIZE", "LINEARSIZE",
"loadouts",
"loddist", "loddist",
"lods", "lods",
"lossrate",
"mantlets",
"metallicness", "metallicness",
"minitelrose", "minitelrose",
"Minotauro", "Minotauro",
"MIPMAPCOUNT", "MIPMAPCOUNT",
"mišek",
"modeltransform", "modeltransform",
"MURICA", "MURICA",
"NEGATIVEX", "NEGATIVEX",
@@ -98,14 +111,19 @@
"POSITIVEY", "POSITIVEY",
"POSITIVEZ", "POSITIVEZ",
"Promisable", "Promisable",
"protobuf",
"protos",
"Pyogenics", "Pyogenics",
"ratingsleaderboard", "ratingleaderboard",
"ratingsplot",
"raycaster", "raycaster",
"researchable", "researchable",
"Resvg", "Resvg",
"rizz",
"roboto", "roboto",
"rtsc",
"RTSM",
"SCPG", "SCPG",
"scroller",
"searchclans", "searchclans",
"searchplayers", "searchplayers",
"searchtanks", "searchtanks",
@@ -125,6 +143,7 @@
"Synx", "Synx",
"tankaverages", "tankaverages",
"Tankerch", "Tankerch",
"tankmen",
"tankopedia", "tankopedia",
"tankopedias", "tankopedias",
"tankstats", "tankstats",
@@ -137,20 +156,26 @@
"Unicum", "Unicum",
"UNLOCALIZED", "UNLOCALIZED",
"UNORM", "UNORM",
"unpickled",
"Unregistering", "Unregistering",
"vickers", "vickers",
"vitepress", "vitepress",
"Vovko", "Vovko",
"wargaming", "wargaming",
"webp", "webp",
"whitespaces",
"winrate", "winrate",
"wireframe", "wireframe",
"Woah",
"WOTB", "WOTB",
"wotblitz", "wotblitz",
"wotbreplay",
"WOTINSPECTOR",
"xffn", "xffn",
"xkdkw", "xkdkw",
"XMLDVPL", "XMLDVPL",
"YAMLDVPL", "YAMLDVPL",
"youtubers",
"zustand" "zustand"
], ],
"deno.enable": false, "deno.enable": false,
+7 -7
View File
@@ -1,4 +1,4 @@
# Blitzkrieg # BlitzKit
[![built with Codeium](https://codeium.com/badges/main)](https://codeium.com) [![built with Codeium](https://codeium.com/badges/main)](https://codeium.com)
@@ -6,18 +6,18 @@ Tools for everything World of Tanks Blitz
## Invite the bot ## Invite the bot
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go. Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
![Blitzkrieg application directory](https://i.imgur.com/JlEA0J1.png) ![BlitzKit application directory](https://i.imgur.com/YWCtOuJ.png)
## Visit the website ## Visit the website
### https://blitz-krieg.vercel.app/ ### https://blitzkit.vercel.app/
![Blitzkrieg website](https://i.imgur.com/eRIZe5P.png) ![BlitzKit website](https://i.imgur.com/bdYMUCL.png)
## Visit the documentation website ## Visit the documentation website
### https://tresabhi.github.io/blitzkrieg/ ### https://tresabhi.github.io/blitzkit/
![Blitzkrieg documentation website](https://i.imgur.com/TOt5fMx.png) ![BlitzKit documentation website](https://i.imgur.com/hm3KTsF.png)
+3 -3
View File
@@ -5,9 +5,9 @@ import { defineConfig } from 'vitepress';
import { getSidebar } from 'vitepress-plugin-auto-sidebar'; import { getSidebar } from 'vitepress-plugin-auto-sidebar';
export default defineConfig({ export default defineConfig({
title: 'blitzkrieg', title: 'BlitzKit',
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz', description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
base: '/blitzkrieg/', base: '/blitzkit/',
outDir: '../dist/docs', outDir: '../dist/docs',
themeConfig: { themeConfig: {
@@ -33,7 +33,7 @@ export default defineConfig({
], ],
socialLinks: [ socialLinks: [
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' }, { icon: 'github', link: 'https://github.com/tresabhi/blitzkit' },
], ],
}, },
}); });
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.17.0
A preview command and a touchup.
## Features
- Added `/research` in preview only accessible in the official Discord server.
- More details will be posted when the command is made public.
## Changes
- `/owned-tanks` now returns a simple text message listing all the tanks instead of an image.
- This is meant to improve the readability and ease of copying.
+8
View File
@@ -0,0 +1,8 @@
# Blitzkrieg 1.17.1
Some stability fixes.
## Technical Changes
- `sharp` is now included in the build instead of having being installed before runtime.
- Png converter cache is now more memory efficient.
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.17.2
Introducing the new `/research` command!
## Features
- Added the `/research` command
- Type in the name of a tech tree tank you desire to research and this command will list all the expenses in researching all tanks in that line
- The starting tank is inferred automatically by the tanks you own
- This may be a bit inaccurate because of the lack of accurate information Wargaming provides about owned tanks
- You are also manually provide a starting tank and a user to see the progress on a line for
- Replaced Vercel Analytics (which collapsed under the insane amount of traffic on the website) with Google Analytics
- Spaced armor can now be toggled in the Tankopedia armor sandbox
+19
View File
@@ -0,0 +1,19 @@
# Blitzkrieg 1.18.0
Introducing the new `/rating` command!
## Features
- `/rating` command
- Features statistics on today's performance in ratings
- The included stats are battles, winrate, damage, survival, and kills
- This command also features the score, rank, and league you are at plus the reward you'll get if you are high enough on the leaderboard
- `/ratings`, the old leaderboard command, has been renamed `/rating-leaderboard`
- Reward icons have been removed from the leaderboard
## Technical Changes
- Rating leaderboards are now built in a more action-efficient manner.
- This version also introduces the BKRL (Blitzkrieg rating leaderboard) file format which is an efficient way to store entire leaderboards of regions to disk (1 MB per 40,000 users).
- Latest archival actions now build all regions at once in a single action.
- The sharp module is now included in the build.
+11
View File
@@ -0,0 +1,11 @@
# Blitzkrieg 1.18.1
HE gets more accurate and say goodbye to NaNaNaNaNaN.
## Fixes
- Armor highlighting now works on Apple MacBooks
- Applied equipments now persist after switching shells
- The bottom of the tank sandbox panel can now be interacted with as it was previously blocked by an invisible layer
- `/rating` now uses the midnight leaderboard instead of the absolute latest one
- 0-battle stats are now hidden
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.18.2
Redefining how micro a micro-path is.
## Fixes
- The pitch and yaw controls, along with the quick equipment buttons, can once again be interacted with
+29
View File
@@ -0,0 +1,29 @@
# Blitzkrieg 1.18.3
Making Tankopedia more pleasing to use. This update only affects the website.
## Features
- Added crew skills
- Added quick actions
- For example, crew skills have the "clear" and "maximize" actions and equipment simply has a "clear" action
- Added info buttons
- Beside the titles of a few sections, a new info buttons have been added for extended descriptions
- Tank configurations have been condensed thanks to a new layout that works well on most screen sizes
- The Tankopedia now takes advantage of wider screens than before; hence, getting rid of the useless whitespaces on the sides
- Tank modules are now neatly wrapped into dropdown menus
- Sorting tanks by "size" has been renamed to "volume" and length has been added as a new option
- The antagonist (the shooting tank) bar has been shrunk to only fit the its contents
- Fullscreen mode now features all the missing dropdowns and options that were previously hidden
- Added icons for donations on Patreon and Ko-fi in the navigation bar
- Tank search now uses regular links allowing for things like opening in new tabs, copying links, etc.
## Fixes
- Armor highlighting once again works properly even after switching around tank modules
- Inputting invalid numbers into the pitch and yaw fields is now handled correctly
- Removed unnecessary tooltips that didn't show up anyway due to Radix UI 3
## Technical Changes
- Upgraded to Radix UI 3
+23
View File
@@ -0,0 +1,23 @@
# Blitzkrieg 1.19.0
Introducing the Blitzkrieg Compare tool!
## Features
- Added Compare
- Allows you to compare multiple tanks
- Shows deltas in the form of differences between values or percentages
- All stats from the Tankopedia are available here
- [CLS](https://web.dev/articles/cls) was reduced due to the new loading screens
- Clicking enter when searching for tanks now selects the first result
- The first tank name is also displayed in the search input
## Changes
- Removed the donation banner instead for a little note in the footer
- The footer now shows the version of Blitzkrieg and the version of WoTB its running on
- Removed frosted glass effect from most components apart from the navigation bar for performance reasons (I am looking at you, Apple!)
## Fixes
- Tank search is now not ignored when changing filters
+11
View File
@@ -0,0 +1,11 @@
# Blitzkrieg 1.19.1
A minor-major fix to the armor sandbox. This update only affects the website.
## Fixes
- Guns are now clipped properly allowing some tanks like the 252U and T34's mantlets to be penetrable (usually with premium shells) as they are in-game
| Before | After |
| ------------------------------------------ | ----------------------------------------- |
| ![before](https://i.imgur.com/wB0kuk0.png) | ![after](https://i.imgur.com/vxp8wB0.png) |
+6
View File
@@ -0,0 +1,6 @@
# Blitzkrieg 1.19.2
## Fixes
- Maximum DPM for double shot auto-reloaders is now calculate correctly with intra-clip in mind
- Blitzkrieg logo is now removed on lower resolutions to allow for other buttons to fit in the title bar of the Tankopedia
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.19.3
Google rizz. This version only affects the website.
## Features
- The titles and descriptions of the Tankopedia tank pages are now more informative
- Added a settings page
- Added a hamburger menu to the navigation bar
## Fixes
- Tank icons in URL previews are now presented properly
+18
View File
@@ -0,0 +1,18 @@
# Blitzkrieg 1.19.4
I genuinely have no idea how these bugs sneaked in here. This update only affects the website.
## Features
- Added a button in Compare which links to the corresponding Tankopedia page
- Added a button in Tankopedia that links to the corresponding Compare page
- If the two tanks (the shooter and the one of which armor is displayed) in the Tankopedia are different, they will be both added by default in the Compare page
- Simplified the footer and added a link to my website and the credits page
- Added an aggressive donate button right at the top of the home page
## Fixes
Thanks to mišek for these bug reports!
- The intra-clip time is now displayed for auto-loaders
- High End Consumables now increase the duration of consumables correctly instead of decreasing it
+19
View File
@@ -0,0 +1,19 @@
# Blitzkrieg 1.19.5
The tankopedia, now more helpful than ever! This update only affects the website.
## Features
- The Tankopedia pages for tanks now include videos from YouTubers
- The selection of tanks that have videos is very small currently but you can help expand it by dropping suggestions on [the official Discord server](https://discord.gg/nDt7AjGJQH)
- Added shell normalization and ricochet angles to the Tankopedia and Compare
## Fixes
- Removed undefined clipping planes
- Added proper keying to all JSX tree nodes, navbar menu items, antagonist bar shells, skill rows and fragments of tank stats in the Tankopedia
- The second half of barrels for tanks like the T-100 LT are now properly included
## Technical Features
- The list of videos is fetched transpiled from a Google Sheets spreadsheet into a CSV file in the asset repository
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.19.6
Renamed Blitzkrieg to BlitzKit.
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.20.0
**IMPORTANT FOR STREAMERS:** This version removed the session tracker embed. Embeds will be reintroduced in the future.
## Features
- Reworked session tracker
- Played tanks are now displayed as a clean table that fits the theme of the website
- Tank images are now displayed along with them being linked to their respective tankopedia pages
- The UI has been simplified
- You can also change what columns are shown in the table but this is a work in progress feature
- Added the `/replay` command
- This command uploads your replays to WOTINSPECTOR and displays the battle results
## Fixes
- Fixed website logo to be "BK"
- Fixed the logo of the website to have a capital "K"
+45
View File
@@ -0,0 +1,45 @@
# Blitzkit Rating Leaderboard
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
```cpp
primary BlitzkitRatingLeaderboard {
utf8"BKRL" magic;
Header header;
primary Body<header> body;
}
Header {
uint16 version; // always 1
Format format;
uint32 count;
}
Format enum<uint8> {
Base,
Superset1,
}
Body<Header header> match<header.format> {
Base {
primary {
uint32 id;
uint16 score;
}[header.count] entries;
};
Superset1 {
primary {
uint32 id;
uint16 score;
uint32 battles;
uint32 wins;
uint32 survived;
uint32 damage;
uint32 kills;
}[header.count] entries;
};
}
```
+1 -3
View File
@@ -1,7 +1,5 @@
# Cedilla Object Notation # Cedilla Object Notation
This documentation uses an experimental binary notation syntax called "Buffer Up".
1. JSON compressed as CDON, written directly to disk (a direct 50% reduction). 1. JSON compressed as CDON, written directly to disk (a direct 50% reduction).
2. CDON further compressed with LZ4 (a clean 90% reduction). 2. CDON further compressed with LZ4 (a clean 90% reduction).
3. The raw JSON file as fetched from a random data source. 3. The raw JSON file as fetched from a random data source.
@@ -19,7 +17,7 @@ primary CedillaObjectNotation {
} }
Header { Header {
uint16 version; uint16 version; // always 1
FSFormat fsFormat; FSFormat fsFormat;
StringTable<fsFormat> stringTable; StringTable<fsFormat> stringTable;
} }
@@ -1,15 +1,16 @@
# Team # Team
- TrèsAbhi: creator & programmer - TrèsAbhi: creator & programmer
- Prince_NA: game advisor & tester
# Contributors # Contributors
- S0AP: bot verification process & initial advertizement platform - Prince_NA: general game advisor and tester
- S0AP: bot verification and initial advertizement platform
- Vovko: discord bot rendering pipeline - Vovko: discord bot rendering pipeline
- Maddox: game file decompression - Maddox: game file decompression
- Pyogenics: tank model decompression - Pyogenics: tank model parsing
- Aeson: game files layout - Aeson: game files layout
- Minitelrose, ChickenMan7777: 3-caliber rule edge cases - Minitelrose, ChickenMan7777: 3-caliber rule edge cases
- \_34, HisRoyalFatness, Synx, xkdkw: embed UX - \_34, HisRoyalFatness, Synx, xkdkw: embed UX
- [OPTML], Aeson, \_34, Droodles: beta testing tankopedia - [OPTML], Aeson, \_34, Droodles: beta testing Tankopedia
- [OPTML], Droodles: beta testing Compare
+3 -3
View File
@@ -4,7 +4,7 @@
## Bot ## Bot
Blitzkrieg is available as a Discord bot and can be [installed very easily](/guide/invite). BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
- Track statistics - Track statistics
- Find inactive players in a clan - Find inactive players in a clan
@@ -15,8 +15,8 @@ Blitzkrieg is available as a Discord bot and can be [installed very easily](/gui
## Website ## Website
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/ Provides a bunch of very helpful tools; https://blitzkit.vercel.app/
- Session tracker - Session tracker
- Ratings leaderboard - Rating leaderboard
- Way more to come - Way more to come
+1 -1
View File
@@ -1,3 +1,3 @@
# Inviting the Bot # Inviting the Bot
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go. Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
+71
View File
@@ -0,0 +1,71 @@
# BlitzKit Regular Tank Stats
A straightforward way of storing the entire World of Tanks Blitz tank statistics for players to disk.
## Manifest `manifest.rtsm`
```cpp
primary RegularTankStatisticsManifest {
utf8"RTSM" magic;
Header header;
primary Body body;
}
Header {
uint16 version; // always 1
}
Body {
uint32 time;
uint8 base;
}
```
## Chunk `*.rtsc`
```cpp
primary RegularTankStatisticsChunk {
utf8"RTSC" magic;
Header header;
primary Body body<header.playerCount>;
}
Header {
uint16 version; // always 1
uint32 playerCount;
}
Body<uint32 playerCount> {
Player[playerCount] players;
}
Player {
uint32 id;
uint16 tankCount;
Tank[tankCount] tanks;
}
Tank {
uint32 id;
uint32 battleLifeTime;
uint32 lastBattleTime;
uint8 markOfMastery;
uint32 battles;
uint32 capturePoints;
uint32 damageDealt;
uint32 damageReceived;
uint32 droppedCapturePoints;
uint32 frags;
uint32 frags8p;
uint32 hits;
uint32 losses;
uint8 maxFrags;
uint16 maxXp;
uint32 shots;
uint32 spotted;
uint32 survivedBattles;
uint32 winAndSurvived;
uint32 wins;
uint32 xp;
}
```
+4 -4
View File
@@ -3,12 +3,12 @@
layout: home layout: home
hero: hero:
name: 'blitzkrieg' name: 'BlitzKit'
text: Everything Blitz, right in Discord text: Everything Blitz, right in Discord
tagline: All-in-one Discord bot for everything World of Tanks Blitz tagline: All-in-one Discord bot for everything World of Tanks Blitz
image: image:
src: '/assets/images/hero.png' src: '/assets/images/hero.png'
alt: 'blitzkrieg' alt: 'blitzkit'
actions: actions:
- theme: brand - theme: brand
text: Add to Discord Server text: Add to Discord Server
@@ -20,11 +20,11 @@ hero:
features: features:
- title: Clean and Efficient UI - title: Clean and Efficient UI
icon: 🚀 icon: 🚀
details: Blitzkrieg is where concise meets efficient. Get the information you need in the way you want. details: BlitzKit is where concise meets efficient. Get the information you need in the way you want.
- title: Endless Possibilities - title: Endless Possibilities
icon: 🛠 icon: 🛠
details: With endless ways to customize the output, it's guaranteed that you'll always get the information you need. details: With endless ways to customize the output, it's guaranteed that you'll always get the information you need.
- title: Easy to Use - title: Easy to Use
icon: 💻 icon: 💻
details: Leveraging Discord's, Blitz Stars', and Wargaming's powerful APIS, Blitzkrieg does all the hard work for you. details: Leveraging Discord's, Blitz Stars', and Wargaming's powerful APIS, Blitzkit does all the hard work for you.
--- ---
+4 -8
View File
@@ -1,22 +1,18 @@
# Blitzkrieg Privacy Policy # BlitzKit Privacy Policy
Blitzkrieg collects little to no data. Regardless, here is an exhaustive list of Blitzkrieg's data collection practices. BlitzKit collects little to no data. Regardless, here is an exhaustive list of BlitzKit's data collection practices.
## Logs ## Logs
All Blitzkrieg logs are available to the public as an effort for open source transparency. A history of all logs [can be found on GitHub](https://github.com/tresabhi/blitzkrieg/actions/workflows/host.yml). All logs expire automatically and are permanently deleted after 30 days. However, anyone may chose to save a local copy of the logs which is not tracked by GitHub. All BlitzKit logs are available to the public as an effort for open source transparency. A history of all logs [can be found on GitHub](https://github.com/tresabhi/blitzkit/actions/workflows/host.yml). All logs expire automatically and are permanently deleted after 30 days. However, anyone may chose to save a local copy of the logs which is not tracked by GitHub.
## Commands ## Commands
Everytime a command a command is used, the command name and parameters provided is logged. Any identifying information like manually provided in-game usernames are also recorded. Everytime a command a command is used, the command name and parameters provided is logged. Any identifying information like manually provided in-game usernames are also recorded.
## Errors
Everytime an error occurs, a detailed stack trace, scope of the error, and the error message is logged both in the GitHub logs and in the public channel [#🔲︱blitzkrieg-logs](https://discord.com/channels/734786591205359697/880650462540476416) in [the official Discord server](https://discord.gg/nDt7AjGJQH).
## Databases ## Databases
No databases are used by Blitzkrieg meaning that no data is collected outside of what has already been mentioned above. This is no permanent state. No databases are used by BlitzKit meaning that no data is collected outside of what has already been mentioned above. This is no permanent state.
## 3rd Party ## 3rd Party
+3 -3
View File
@@ -1,12 +1,12 @@
# Blitzkrieg Terms of Service # BlitzKit Terms of Service
## About ## About
Blitzkrieg is a multi-function Discord bot that serves users with needs specific to the game World of Tanks Blitz. BlitzKit is a multi-function Discord bot that serves users with needs specific to the game World of Tanks Blitz.
## License ## License
Blitzkrieg is licensed under the MIT License. BlitzKit is licensed under the MIT License.
## 3rd Party ## 3rd Party
Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+21 -15
View File
@@ -1,24 +1,26 @@
{ {
"name": "blitzkrieg", "name": "blitzkit",
"version": "1.16.4", "version": "1.20.0",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build:bot": "vite-node --script scripts/build", "build:db": "prisma generate",
"build:website": "next build", "build:bot": "vite-node --script scripts/build --production",
"build:website": "yarn build:db && yarn build:sitemap && next build",
"build:sitemap": "vite-node --script scripts/sitemap",
"build:docs": "vitepress build docs", "build:docs": "vitepress build docs",
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard", "build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index", "build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
"build:test": "vite-node --script scripts/buildTest", "test": "vite-node --script scripts/test",
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot", "test2": "vite-node --script scripts/test2",
"dev:bot": "yarn build:bot && yarn start:bot", "dev:bot": "vite-node --script scripts/build && yarn start:bot",
"dev:docs": "vitepress dev docs", "dev:docs": "vitepress dev docs",
"dev:website": "next dev", "dev:website": "next dev",
"start:bot": "node --enable-source-maps dist/bot/index.cjs", "start:bot": "node --enable-source-maps dist/bot/index.cjs",
"start:website": "next start", "start:website": "next start",
"host:docs": "vitepress preview docs", "host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish", "publish:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard --publish",
"setup:patches": "vite-node --script scripts/applyPatches", "setup:patches": "vite-node --script scripts/applyPatches",
"lint": "npx tsc", "lint": "npx tsc",
"lint:website": "next lint", "lint:website": "next lint",
@@ -35,15 +37,15 @@
"@octokit/plugin-throttling": "^8.1.3", "@octokit/plugin-throttling": "^8.1.3",
"@octokit/rest": "^20.0.1", "@octokit/rest": "^20.0.1",
"@prisma/client": "^5.4.1", "@prisma/client": "^5.4.1",
"@radix-ui/colors": "^3.0.0-rc.4", "@radix-ui/colors": "^3.0.0",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^2.0.1", "@radix-ui/themes": "^3.0.1",
"@react-three/drei": "^9.93.0", "@react-three/drei": "^9.93.0",
"@react-three/fiber": "^8.15.14", "@react-three/fiber": "^8.15.14",
"@resvg/resvg-js": "^2.4.1", "@resvg/resvg-js": "^2.4.1",
"@types/three": "^0.160.0", "@types/three": "^0.160.0",
"@vanilla-extract/css": "^1.13.0", "@vanilla-extract/css": "^1.13.0",
"@vercel/analytics": "^1.2.2", "adm-zip": "^0.5.12",
"bepaint": "^0.1.0-beta.3", "bepaint": "^0.1.0-beta.3",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"discord.js": "^14.13.0", "discord.js": "^14.13.0",
@@ -52,6 +54,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"false": "^0.0.4", "false": "^0.0.4",
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"googleapis": "^134.0.0",
"immer": "^10.0.2", "immer": "^10.0.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -59,15 +62,16 @@
"markdown-escape": "^2.0.0", "markdown-escape": "^2.0.0",
"next": "^14.1.0", "next": "^14.1.0",
"parse-dds": "^1.2.1", "parse-dds": "^1.2.1",
"pickleparser": "^0.2.1",
"postprocessing": "^6.34.1", "postprocessing": "^6.34.1",
"protobufjs": "^7.2.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-fuzzysort": "^1.0.1", "react-fuzzysort": "^1.0.1",
"react-promise-suspense": "^0.3.4", "react-promise-suspense": "^0.3.4",
"react-spinners": "^0.13.8", "satori": "^0.10.13",
"satori": "^0.10.8", "sharp": "^0.33.3",
"sharp": "0.33.1",
"three": "0.159.0", "three": "0.159.0",
"three-stdlib": "^2.29.4", "three-stdlib": "^2.29.4",
"vite-node": "^0.34.5", "vite-node": "^0.34.5",
@@ -78,12 +82,14 @@
}, },
"devDependencies": { "devDependencies": {
"@total-typescript/ts-reset": "^0.5.1", "@total-typescript/ts-reset": "^0.5.1",
"@types/adm-zip": "^0.5.5",
"@types/decompress": "^4.2.7", "@types/decompress": "^4.2.7",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/express": "^4.17.18", "@types/express": "^4.17.18",
"@types/js-yaml": "^4.0.8", "@types/js-yaml": "^4.0.8",
"@types/lodash": "^4.14.199", "@types/lodash": "^4.14.199",
"@types/lz4": "^0.6.4", "@types/lz4": "^0.6.4",
"@types/lz4js": "^0.2.1",
"@types/markdown-escape": "^1.1.0", "@types/markdown-escape": "^1.1.0",
"@types/node": "^20.6.5", "@types/node": "^20.6.5",
"@types/parse-dds": "^1.0.3", "@types/parse-dds": "^1.0.3",
@@ -1,8 +0,0 @@
-- CreateTable
CREATE TABLE "discord_blitz" (
"discord" INT8 NOT NULL,
"region" STRING NOT NULL,
"blitz" INT4 NOT NULL,
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
);
-3
View File
@@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "cockroachdb"
+7 -3
View File
@@ -4,11 +4,15 @@ generator client {
datasource db { datasource db {
provider = "cockroachdb" provider = "cockroachdb"
url = env("DISCORD_BLITZ_DB") url = env("DATABASE_URL")
} }
model discord_blitz { model Users {
blitz Int @id
last_used DateTime? @default(now())
}
model DiscordUsers {
discord BigInt @id discord BigInt @id
region String
blitz Int blitz Int
} }
+3
View File
@@ -0,0 +1,3 @@
google.com, pub-1289406790225052, DIRECT, f08c47fec0942fa0
appnexus.com, 14163, RESELLER
pubcenter.microsoft.com, 253617120, DIRECT
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 921 KiB

File renamed without changes.
File renamed without changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+7
View File
@@ -0,0 +1,7 @@
User-agent: *
Allow: /
Host: https://blitzkit.vercel.app
Sitemap: https://blitzkit.vercel.app/sitemap.txt
Sitemap: https://blitzkit.vercel.app/sitemap.xml
+2 -2
View File
@@ -3,8 +3,8 @@ import { parse as parsePath } from 'path';
import { parse as parseYaml } from 'yaml'; import { parse as parseYaml } from 'yaml';
import { readStringDVPL } from '../src/core/blitz/readStringDVPL'; import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
import { writeDVPL } from '../src/core/blitz/writeDVPL'; import { writeDVPL } from '../src/core/blitz/writeDVPL';
import { secrets } from '../src/core/blitzkrieg/secrets'; import { secrets } from '../src/core/blitzkit/secrets';
import { dvp } from '../submodules/closedkrieg/src/dvp'; import { dvp } from '../submodules/blitzkit-closed/src/dvp';
import { DATA } from './buildAssets/constants'; import { DATA } from './buildAssets/constants';
const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`); const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`);
+20 -3
View File
@@ -1,7 +1,13 @@
import { exec } from 'child_process';
import { build } from 'esbuild'; import { build } from 'esbuild';
import { cp, readdir } from 'fs/promises'; import { cp, readdir } from 'fs/promises';
import { argv } from 'process';
import { promisify } from 'util';
readdir('src/workers').then((workers) => { const execPromise = promisify(exec);
const isProduction = argv.includes('--production');
await readdir('src/workers').then((workers) => {
const files = [ const files = [
'bot', 'bot',
'index', 'index',
@@ -25,6 +31,7 @@ readdir('src/workers').then((workers) => {
'.js': '.cjs', '.js': '.cjs',
}, },
external: ['sharp'],
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
minifyIdentifiers: false, // causes goofy errors minifyIdentifiers: false, // causes goofy errors
@@ -36,9 +43,19 @@ readdir('src/workers').then((workers) => {
}); });
// copy schema.prisma // copy schema.prisma
cp('prisma/schema.prisma', 'dist/bot/schema.prisma'); await cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
readdir('node_modules/prisma').then((files) => await readdir('node_modules/prisma').then((files) =>
files files
.filter((file) => file.endsWith('.node')) .filter((file) => file.endsWith('.node'))
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)), .map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
); );
if (isProduction) {
// install fixed sharp
console.log('Installing fixed sharp...');
await execPromise('yarn init -y && yarn add [email protected] --ignore-engines', {
cwd: 'dist/bot',
});
}
console.log('Generated:', await readdir('dist/bot'));
+2 -2
View File
@@ -2,8 +2,8 @@ import { readdir } from 'fs/promises';
import sharp from 'sharp'; import sharp from 'sharp';
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
export async function boosterIcons(production: boolean) { export async function boosterIcons(production: boolean) {
+2 -2
View File
@@ -1,6 +1,6 @@
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
export async function camouflageIcons(production: boolean) { export async function camouflageIcons(production: boolean) {
+2 -2
View File
@@ -1,7 +1,7 @@
import { readdir } from 'fs/promises'; import { readdir } from 'fs/promises';
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
export async function circleFlags(production: boolean) { export async function circleFlags(production: boolean) {
@@ -3,8 +3,8 @@ import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { ConsumablesCommon, ProvisionsCommon } from './definitions'; import { ConsumablesCommon, ProvisionsCommon } from './definitions';
+39
View File
@@ -0,0 +1,39 @@
import sharp from 'sharp';
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA } from './constants';
const ICONS = [
['currency_silver_m.packed.webp', 'silver'],
['currency_premium_xl.packed.webp', 'premium'],
['currency_gold_m.packed.webp', 'gold'],
['currency_free-xp_xl.packed.webp', 'free-xp'],
['currency_elite-xp_xl.packed.webp', 'elite-xp'],
['currency_crew-xp_xl.packed.webp', 'crew-xp'],
['currency_battle-xp_xl.packed.webp', 'xp'],
];
export async function currencies(production: boolean) {
console.log('Building currency icons...');
const changes = await Promise.all(
ICONS.map(async ([file, name]) => {
const content = (
await sharp(
await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}.dvpl`),
)
.trim()
.toBuffer()
).toString('base64');
return {
content,
encoding: 'base64',
path: `icons/currencies/${name}.webp`,
} satisfies FileChange;
}),
);
await commitAssets('currency icons', changes, production);
}
+255 -53
View File
@@ -3,25 +3,29 @@ import { parse as parsePath } from 'path';
import { Vector3Tuple } from 'three'; import { Vector3Tuple } from 'three';
import { parse as parseYaml } from 'yaml'; import { parse as parseYaml } from 'yaml';
import { TankClass } from '../../src/components/Tanks'; import { TankClass } from '../../src/components/Tanks';
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId'; import { toUniqueId } from '../../src/core/blitz/toUniqueId';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { import {
ConsumableDefinitions, ConsumableDefinitions,
TankFilterDefinitionCategory, TankFilterDefinitionCategory,
} from '../../src/core/blitzkrieg/consumableDefinitions'; } from '../../src/core/blitzkit/consumableDefinitions';
import { import {
EquipmentDefinitions, EquipmentDefinitions,
EquipmentPreset, EquipmentPreset,
EquipmentRow, EquipmentRow,
} from '../../src/core/blitzkrieg/equipmentDefinitions'; } from '../../src/core/blitzkit/equipmentDefinitions';
import { GameDefinitions } from '../../src/core/blitzkit/gameDefinitions';
import { MapDefinitions } from '../../src/core/blitzkit/mapDefinitions';
import { import {
ModelArmor, ModelArmor,
ModelDefinitions, ModelDefinitions,
} from '../../src/core/blitzkrieg/modelDefinitions'; } from '../../src/core/blitzkit/modelDefinitions';
import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions'; import { ProvisionDefinitions } from '../../src/core/blitzkit/provisionDefinitions';
import { superCompress } from '../../src/core/blitzkrieg/superCompress'; import { SkillDefinitions } from '../../src/core/blitzkit/skillDefinitions';
import { superCompress } from '../../src/core/blitzkit/superCompress';
import { import {
Crew, Crew,
CrewMember, CrewMember,
@@ -30,11 +34,12 @@ import {
TankDefinitionPrice, TankDefinitionPrice,
TankDefinitions, TankDefinitions,
Tier, Tier,
} from '../../src/core/blitzkrieg/tankDefinitions'; } from '../../src/core/blitzkit/tankDefinitions';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { Avatar } from './skillIcons';
import { TankParameters } from './tankIcons'; import { TankParameters } from './tankIcons';
interface Strings { export interface BlitzStrings {
[key: string]: string; [key: string]: string;
} }
type ShellKind = type ShellKind =
@@ -79,6 +84,16 @@ type VehicleDefinitionArmor = Record<
string, string,
number | { vehicleDamageFactor: 0; '#text': number } number | { vehicleDamageFactor: 0; '#text': number }
>; >;
interface UnlocksInner {
cost: number;
'#text': number;
}
type Unlocks = {
[key in 'vehicle' | 'engine' | 'chassis' | 'turret' | 'gun']:
| UnlocksInner
| UnlocksInner[];
};
type UnlocksListing = Unlocks | undefined;
interface VehicleDefinitions { interface VehicleDefinitions {
invisibility: { invisibility: {
moving: number; moving: number;
@@ -92,7 +107,7 @@ interface VehicleDefinitions {
}; };
consumableSlots: number; consumableSlots: number;
provisionSlots: number; provisionSlots: number;
optDevicePreset: string; optDevicePreset: string | string[];
hull: { hull: {
armor: VehicleDefinitionArmor; armor: VehicleDefinitionArmor;
turretPositions: { turret: string }; turretPositions: { turret: string };
@@ -102,9 +117,11 @@ interface VehicleDefinitions {
}; };
chassis: { chassis: {
[key: string]: { [key: string]: {
unlocks: UnlocksListing;
weight: number; weight: number;
terrainResistance: string; terrainResistance: string;
rotationSpeed: number; rotationSpeed: number;
userString: string;
shotDispersionFactors: { shotDispersionFactors: {
vehicleMovement: number; vehicleMovement: number;
vehicleRotation: number; vehicleRotation: number;
@@ -130,10 +147,13 @@ interface VehicleDefinitions {
}; };
}; };
engines: { engines: {
[key: string]: unknown; [key: string]: {
unlocks: UnlocksListing;
};
}; };
turrets0: { turrets0: {
[key: string]: { [key: string]: {
unlocks: UnlocksListing;
rotationSpeed: number; rotationSpeed: number;
weight: number; weight: number;
circularVisionRadius: number; circularVisionRadius: number;
@@ -149,6 +169,7 @@ interface VehicleDefinitions {
}; };
guns: { guns: {
[key: string]: { [key: string]: {
unlocks: UnlocksListing;
armor: VehicleDefinitionArmor & { armor: VehicleDefinitionArmor & {
gun?: gun?:
| number | number
@@ -305,7 +326,32 @@ type ConsumablesVehicleFilter =
| { name: string } | { name: string }
| { extendedTags: string }; | { extendedTags: string };
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = { export interface Maps {
maps: {
[key: string]: {
id: number;
tags?: string;
localName: string;
avaliableInTrainingRoom: boolean; // lol wg typo
spriteFrame: number;
supremacyPointsThreshold?: number;
availableModes: number[];
shadowMapsAvailable?: boolean;
assaultRespawnPoints: {
allies: MapAlly[];
enemies: MapAlly[];
};
levels?: number[];
};
};
}
export interface MapAlly {
respawnNumber: number;
points: Array<number[]>;
}
const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
ARMOR_PIERCING: 'ap', ARMOR_PIERCING: 'ap',
ARMOR_PIERCING_CR: 'ap_cr', ARMOR_PIERCING_CR: 'ap_cr',
HIGH_EXPLOSIVE: 'he', HIGH_EXPLOSIVE: 'he',
@@ -314,28 +360,40 @@ const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
const missingStrings: Record<string, string> = { const missingStrings: Record<string, string> = {
'#artefacts:tungstentip/name': 'Tungsten Shells', '#artefacts:tungstentip/name': 'Tungsten Shells',
}; };
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
export async function definitions(production: boolean) { export async function definitions(production: boolean) {
console.log('Building definitions...'); console.log('Building definitions...');
const gameDefinitions: GameDefinitions = {
version: (await readStringDVPL(`${DATA}/version.txt.dvpl`))
.split(' ')[0]
.split('.')
.slice(0, 3)
.join('.'),
};
const tankDefinitions: TankDefinitions = {}; const tankDefinitions: TankDefinitions = {};
const modelDefinitions: ModelDefinitions = {}; const modelDefinitions: ModelDefinitions = {};
const mapDefinitions: MapDefinitions = {};
const equipmentDefinitions: EquipmentDefinitions = { const equipmentDefinitions: EquipmentDefinitions = {
presets: {}, presets: {},
equipments: {}, equipments: {},
}; };
const consumableDefinitions: ConsumableDefinitions = {}; const consumableDefinitions: ConsumableDefinitions = {};
const provisionDefinitions: ProvisionDefinitions = {}; const provisionDefinitions: ProvisionDefinitions = {};
const skillDefinitions: SkillDefinitions = {
classes: { lightTank: [], mediumTank: [], heavyTank: [], 'AT-SPG': [] },
};
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
(nations) => nations.filter((nation) => nation !== 'common'), (nations) => nations.filter((nation) => nation !== 'common'),
); );
const tankStringIdMap: Record<string, number> = {}; const tankStringIdMap: Record<string, number> = {};
const stringsPreInstalled = await readYAMLDVPL<Strings>( const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
`${DATA}/${POI.strings}/en.yaml.dvpl`, `${DATA}/${POI.strings}/en.yaml.dvpl`,
); );
const stringsCache = await fetch(POI.cachedStrings) const stringsCache = await fetch(POI.cachedStrings)
.then((response) => response.text()) .then((response) => response.text())
.then((string) => parseYaml(string) as Strings); .then((string) => parseYaml(string) as BlitzStrings);
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
`${DATA}/${POI.optionalDevices}.dvpl`, `${DATA}/${POI.optionalDevices}.dvpl`,
); );
@@ -352,6 +410,11 @@ export async function definitions(production: boolean) {
const provisions = await readXMLDVPL<{ root: ProvisionsCommon }>( const provisions = await readXMLDVPL<{ root: ProvisionsCommon }>(
`${DATA}/${POI.provisionsCommon}.dvpl`, `${DATA}/${POI.provisionsCommon}.dvpl`,
); );
const avatar = await readXMLDVPL<{ root: Avatar }>(
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
);
const maps = await readYAMLDVPL<Maps>(`${DATA}/maps.yaml.dvpl`);
const tankXps = new Map<number, number>();
await Promise.all( await Promise.all(
nations.map(async (nation) => { nations.map(async (nation) => {
@@ -385,6 +448,12 @@ export async function definitions(production: boolean) {
); );
for (const tankKey in tankList.root) { for (const tankKey in tankList.root) {
if (botPattern.test(tankKey)) continue;
const gunXps = new Map<number, number>();
const turretXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
const tank = tankList.root[tankKey]; const tank = tankList.root[tankKey];
const tankPrice: TankDefinitionPrice = const tankPrice: TankDefinitionPrice =
typeof tank.price === 'number' typeof tank.price === 'number'
@@ -427,6 +496,7 @@ export async function definitions(production: boolean) {
}); });
const crew: Crew[] = []; const crew: Crew[] = [];
const fixedCamouflage = tankTags.includes('eventCamouflage_user'); const fixedCamouflage = tankTags.includes('eventCamouflage_user');
const totalUnlocks: UnlocksListing[] = [];
Object.entries(tankDefinition.root.crew).forEach(([key, value]) => { Object.entries(tankDefinition.root.crew).forEach(([key, value]) => {
let entry: Crew; let entry: Crew;
@@ -473,7 +543,8 @@ export async function definitions(production: boolean) {
forwards: tankDefinition.root.speedLimits.forward, forwards: tankDefinition.root.speedLimits.forward,
backwards: tankDefinition.root.speedLimits.backward, backwards: tankDefinition.root.speedLimits.backward,
}, },
equipment, equipment:
typeof equipment === 'string' ? equipment : equipment.at(-1)!,
consumables: tankDefinition.root.consumableSlots, consumables: tankDefinition.root.consumableSlots,
provisions: tankDefinition.root.provisionSlots, provisions: tankDefinition.root.provisionSlots,
name: name:
@@ -531,9 +602,11 @@ export async function definitions(production: boolean) {
.split(' ') .split(' ')
.map(Number) as Vector3Tuple; .map(Number) as Vector3Tuple;
totalUnlocks.push(track.unlocks);
tankDefinitions[tankId].tracks.push({ tankDefinitions[tankId].tracks.push({
id: trackId, id: trackId,
weight: track.weight, weight: track.weight,
name: strings[track.userString],
traverseSpeed: track.rotationSpeed, traverseSpeed: track.rotationSpeed,
dispersion: { dispersion: {
move: track.shotDispersionFactors.vehicleMovement, move: track.shotDispersionFactors.vehicleMovement,
@@ -557,16 +630,18 @@ export async function definitions(production: boolean) {
}); });
Object.keys(tankDefinition.root.engines).forEach((engineKey) => { Object.keys(tankDefinition.root.engines).forEach((engineKey) => {
const engine = enginesList.root.shared[engineKey]; const engine = tankDefinition.root.engines[engineKey];
const engineListEntry = enginesList.root.shared[engineKey];
const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]); const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]);
totalUnlocks.push(engine.unlocks);
tankDefinitions[tankId].engines.push({ tankDefinitions[tankId].engines.push({
id: engineId, id: engineId,
name: strings[engine.userString], name: strings[engineListEntry.userString],
fireChance: engine.fireStartingChance, fireChance: engineListEntry.fireStartingChance,
tier: engine.level as Tier, tier: engineListEntry.level as Tier,
weight: engine.weight, weight: engineListEntry.weight,
power: engine.power, power: engineListEntry.power,
}); });
}); });
@@ -593,6 +668,7 @@ export async function definitions(production: boolean) {
.map(Number) as Vector3Tuple; .map(Number) as Vector3Tuple;
const turretArmor: ModelArmor = { thickness: {} }; const turretArmor: ModelArmor = { thickness: {} };
totalUnlocks.push(turret.unlocks);
Object.keys(turret.armor) Object.keys(turret.armor)
.filter((name) => name.startsWith('armor_')) .filter((name) => name.startsWith('armor_'))
.forEach((name) => { .forEach((name) => {
@@ -645,11 +721,11 @@ export async function definitions(production: boolean) {
}; };
Object.keys(turret.guns).forEach((gunKey, gunIndex) => { Object.keys(turret.guns).forEach((gunKey, gunIndex) => {
const turretGunEntry = turret.guns[gunKey]; const gun = turret.guns[gunKey];
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
const gunListEntry = gunList.root.shared[gunKey]; const gunListEntry = gunList.root.shared[gunKey];
const pitchLimitsRaw = const pitchLimitsRaw =
turretGunEntry.pitchLimits ?? gunListEntry.pitchLimits; gun.pitchLimits ?? gunListEntry.pitchLimits;
const gunPitch = ( const gunPitch = (
typeof pitchLimitsRaw === 'string' typeof pitchLimitsRaw === 'string'
? pitchLimitsRaw ? pitchLimitsRaw
@@ -658,43 +734,52 @@ export async function definitions(production: boolean) {
.split(' ') .split(' ')
.map(Number) as [number, number]; .map(Number) as [number, number];
const gunModel = Number( const gunModel = Number(
parsePath(turretGunEntry.models.undamaged).name.split('_')[1], parsePath(gun.models.undamaged).name.split('_')[1],
); );
const gunName = const gunName =
strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' '); strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' ');
const gunType = const gunType =
'clip' in turretGunEntry 'clip' in gun
? turretGunEntry.pumpGunMode ? gun.pumpGunMode
? 'autoReloader' ? 'autoReloader'
: 'autoLoader' : 'autoLoader'
: 'regular'; : 'regular';
const gunReload = const gunReload =
gunType === 'autoReloader' gunType === 'autoReloader'
? turretGunEntry.pumpGunReloadTimes!.split(' ').map(Number) ? gun.pumpGunReloadTimes!.split(' ').map(Number)
: turretGunEntry.reloadTime; : gun.reloadTime;
const gunClipCount = const gunClipCount =
gunType === 'regular' ? undefined : turretGunEntry.clip!.count; gunType === 'regular' ? undefined : gun.clip!.count;
const gunIntraClip = const gunIntraClip =
gunType === 'regular' gunType === 'regular' ? undefined : 60 / gun.clip!.rate;
? undefined const front = gun.extraPitchLimits?.front
: 60 / turretGunEntry.clip!.rate; ? gun.extraPitchLimits.front.split(' ').map(Number)
const front = turretGunEntry.extraPitchLimits?.front
? turretGunEntry.extraPitchLimits.front.split(' ').map(Number)
: undefined; : undefined;
const back = turretGunEntry.extraPitchLimits?.back const back = gun.extraPitchLimits?.back
? turretGunEntry.extraPitchLimits.back.split(' ').map(Number) ? gun.extraPitchLimits.back.split(' ').map(Number)
: undefined; : undefined;
const transition = turretGunEntry.extraPitchLimits?.transition const transition = gun.extraPitchLimits?.transition
? typeof turretGunEntry.extraPitchLimits.transition === 'number' ? typeof gun.extraPitchLimits.transition === 'number'
? turretGunEntry.extraPitchLimits.transition ? gun.extraPitchLimits.transition
: turretGunEntry.extraPitchLimits.transition.at(-1)! : gun.extraPitchLimits.transition.at(-1)!
: undefined; : undefined;
const gunArmor: ModelArmor = { thickness: {} }; const gunArmor: ModelArmor = { thickness: {} };
const shotDispersionFactors = const shotDispersionFactors =
turretGunEntry.shotDispersionFactors ?? gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors;
gunListEntry.shotDispersionFactors; const maskName = `gun_${gunModel.toString().padStart(2, '0')}`;
const maskEnabled =
tankParameters.maskSlice?.[maskName]?.enabled ?? false;
let mask: number | undefined;
Object.keys(turretGunEntry.armor) if (maskEnabled) {
const maskRaw = tankParameters.maskSlice![maskName]!;
mask = maskRaw.planePosition[1];
} else {
mask = undefined;
}
totalUnlocks.push(gun.unlocks);
Object.keys(gun.armor)
.filter((name) => name.startsWith('armor_')) .filter((name) => name.startsWith('armor_'))
.forEach((name) => { .forEach((name) => {
const armorIdString = name.match(/armor_(\d+)/)?.[1]; const armorIdString = name.match(/armor_(\d+)/)?.[1];
@@ -702,7 +787,7 @@ export async function definitions(production: boolean) {
throw new SyntaxError(`Invalid armor id: ${name}`); throw new SyntaxError(`Invalid armor id: ${name}`);
} }
const armorId = parseInt(armorIdString); const armorId = parseInt(armorIdString);
const armorRaw = turretGunEntry.armor[name]; const armorRaw = gun.armor[name];
if (typeof armorRaw === 'number') { if (typeof armorRaw === 'number') {
gunArmor.thickness[armorId] = armorRaw; gunArmor.thickness[armorId] = armorRaw;
} else { } else {
@@ -724,13 +809,13 @@ export async function definitions(production: boolean) {
count: gunClipCount, count: gunClipCount,
intraClip: gunIntraClip, intraClip: gunIntraClip,
camouflageLoss: camouflageLoss:
typeof turretGunEntry.invisibilityFactorAtShot === 'number' typeof gun.invisibilityFactorAtShot === 'number'
? turretGunEntry.invisibilityFactorAtShot ? gun.invisibilityFactorAtShot
: turretGunEntry.invisibilityFactorAtShot.at(-1)!, : gun.invisibilityFactorAtShot.at(-1)!,
aimTime: turretGunEntry.aimingTime ?? gunListEntry.aimingTime, aimTime: gun.aimingTime ?? gunListEntry.aimingTime,
dispersion: { dispersion: {
base: base:
turretGunEntry.shotDispersionRadius ?? gun.shotDispersionRadius ??
gunListEntry.shotDispersionRadius, gunListEntry.shotDispersionRadius,
damaged: shotDispersionFactors.whileGunDamaged, damaged: shotDispersionFactors.whileGunDamaged,
shot: shotDispersionFactors.afterShot, shot: shotDispersionFactors.afterShot,
@@ -741,12 +826,13 @@ export async function definitions(production: boolean) {
modelDefinitions[tankId].turrets[turretId].guns[gunId] = { modelDefinitions[tankId].turrets[turretId].guns[gunId] = {
armor: gunArmor, armor: gunArmor,
model: gunModel, model: gunModel,
mask,
thickness: thickness:
turretGunEntry.armor.gun === undefined gun.armor.gun === undefined
? 0 ? 0
: typeof turretGunEntry.armor.gun === 'number' : typeof gun.armor.gun === 'number'
? turretGunEntry.armor.gun ? gun.armor.gun
: turretGunEntry.armor.gun['#text'], : gun.armor.gun['#text'],
pitch: { pitch: {
min: gunPitch[0], min: gunPitch[0],
max: gunPitch[1], max: gunPitch[1],
@@ -793,7 +879,7 @@ export async function definitions(production: boolean) {
caliber: shell.caliber, caliber: shell.caliber,
normalization: shell.normalizationAngle, normalization: shell.normalizationAngle,
ricochet: shell.ricochetAngle, ricochet: shell.ricochetAngle,
type: blitzShellKindToBLitzkrieg[shell.kind], type: blitzShellKindToBlitzkit[shell.kind],
explosionRadius: explosionRadius:
shell.kind === 'HIGH_EXPLOSIVE' shell.kind === 'HIGH_EXPLOSIVE'
? shell.explosionRadius ?? 0 ? shell.explosionRadius ?? 0
@@ -808,10 +894,100 @@ export async function definitions(production: boolean) {
}); });
}, },
); );
totalUnlocks.forEach((unlocks) => {
if (unlocks === undefined) return;
Object.entries(unlocks).forEach(([key, value]) => {
(Array.isArray(value) ? value : [value]).forEach((vehicle) => {
switch (key as keyof Unlocks) {
case 'vehicle': {
const tankListEntry = tankList.root[vehicle['#text']];
const currentTank = tankDefinitions[tankId];
const successorId = toUniqueId(nation, tankListEntry.id);
tankXps.set(successorId, vehicle.cost);
if (currentTank.successors === undefined) {
currentTank.successors = [];
}
if (!currentTank.successors!.includes(successorId)) {
currentTank.successors!.push(successorId);
}
break;
}
case 'gun': {
gunXps.set(
toUniqueId(nation, gunList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
case 'turret': {
turretXps.set(
toUniqueId(nation, turretList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
case 'engine': {
engineXps.set(
toUniqueId(nation, enginesList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
case 'chassis': {
trackXps.set(
toUniqueId(nation, chassisList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
}
});
});
});
Object.values(tankDefinitions[tankId].turrets).forEach((turret) => {
turret.xp = turretXps.get(turret.id);
Object.values(turret.guns).forEach((gun) => {
gun.xp = gunXps.get(gun.id);
});
});
Object.values(tankDefinitions[tankId].engines).forEach((engine) => {
engine.xp = engineXps.get(engine.id);
});
Object.values(tankDefinitions[tankId].tracks).forEach((track) => {
track.xp = trackXps.get(track.id);
});
} }
}), }),
); );
Object.values(tankDefinitions).forEach((tank) => {
tank.xp = tankXps.get(tank.id);
});
Object.values(tankDefinitions).forEach((tank) => {
tank.successors?.forEach((predecessorId) => {
if (tankDefinitions[predecessorId].ancestors === undefined) {
tankDefinitions[predecessorId].ancestors = [];
}
if (!tankDefinitions[predecessorId].ancestors?.includes(tank.id)) {
tankDefinitions[predecessorId].ancestors?.push(tank.id);
}
});
});
Object.entries(optionalDevices.root).forEach( Object.entries(optionalDevices.root).forEach(
([optionalDeviceKey, optionalDeviceEntry]) => { ([optionalDeviceKey, optionalDeviceEntry]) => {
if (optionalDeviceKey === 'nextAvailableId') return; if (optionalDeviceKey === 'nextAvailableId') return;
@@ -969,9 +1145,25 @@ export async function definitions(production: boolean) {
} }
}); });
Object.entries(avatar.root.skillsByClasses).forEach(([tankClass, skills]) => {
skillDefinitions.classes[tankClass as TankClass] = skills.split(' ');
});
Object.entries(maps.maps).forEach(([key, map]) => {
mapDefinitions[map.id] = {
id: map.id,
name: strings[`#maps:${key}:${map.localName}`],
};
});
await commitAssets( await commitAssets(
'definitions', 'definitions',
[ [
{
content: superCompress(gameDefinitions),
encoding: 'base64',
path: 'definitions/game.cdon.lz4',
},
{ {
content: superCompress(tankDefinitions), content: superCompress(tankDefinitions),
encoding: 'base64', encoding: 'base64',
@@ -997,6 +1189,16 @@ export async function definitions(production: boolean) {
encoding: 'base64', encoding: 'base64',
path: 'definitions/provisions.cdon.lz4', path: 'definitions/provisions.cdon.lz4',
}, },
{
content: superCompress(skillDefinitions),
encoding: 'base64',
path: 'definitions/skills.cdon.lz4',
},
{
content: superCompress(mapDefinitions),
encoding: 'base64',
path: 'definitions/maps.cdon.lz4',
},
], ],
production, production,
); );
+2 -2
View File
@@ -3,8 +3,8 @@ import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { OptionalDevices } from './definitions'; import { OptionalDevices } from './definitions';
+7 -1
View File
@@ -14,21 +14,26 @@ import { boosterIcons } from './boosterIcons';
import { camouflageIcons } from './camouflageIcons'; import { camouflageIcons } from './camouflageIcons';
import { circleFlags } from './circleFlags'; import { circleFlags } from './circleFlags';
import { consumableProvisionIcons } from './consumableProvisionIcons'; import { consumableProvisionIcons } from './consumableProvisionIcons';
import { currencies } from './currencies';
import { definitions } from './definitions'; import { definitions } from './definitions';
import { equipmentIcons } from './equipmentIcons'; import { equipmentIcons } from './equipmentIcons';
import { moduleIcons } from './moduleIcons'; import { moduleIcons } from './moduleIcons';
import { scratchedFlags } from './scratchedFlags'; import { scratchedFlags } from './scratchedFlags';
import { shellIcons } from './shellIcons'; import { shellIcons } from './shellIcons';
import { skillIcons } from './skillIcons';
import { tankArmors } from './tankArmors'; import { tankArmors } from './tankArmors';
import { tankIcons } from './tankIcons'; import { tankIcons } from './tankIcons';
import { tankModels } from './tankModels'; import { tankModels } from './tankModels';
import { videos } from './videos';
if (!targets && !allTargets) throw new Error('No target(s) specified'); if (!targets && !allTargets) throw new Error('No target(s) specified');
const methods = [ const methods = [
definitions, definitions,
tankIcons, videos,
scratchedFlags, scratchedFlags,
currencies,
skillIcons,
circleFlags, circleFlags,
shellIcons, shellIcons,
moduleIcons, moduleIcons,
@@ -36,6 +41,7 @@ const methods = [
consumableProvisionIcons, consumableProvisionIcons,
camouflageIcons, camouflageIcons,
boosterIcons, boosterIcons,
tankIcons,
tankArmors, tankArmors,
tankModels, tankModels,
]; ];
+9 -6
View File
@@ -1,7 +1,8 @@
import { readdir } from 'fs/promises'; import { readdir } from 'fs/promises';
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import sharp from 'sharp';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
export async function moduleIcons(production: boolean) { export async function moduleIcons(production: boolean) {
@@ -16,9 +17,11 @@ export async function moduleIcons(production: boolean) {
.map(async (file) => { .map(async (file) => {
const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1]; const nameRaw = file.match(/vehicle(.+)\.packed\.webp\.dvpl/)![1];
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1); const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
const content = await readBase64DVPL( const content = (
`${DATA}/${POI.moduleIcons}/${file}`, await sharp(await readDVPLFile(`${DATA}/${POI.moduleIcons}/${file}`))
); .trim()
.toBuffer()
).toString('base64');
return { return {
content, content,
+2 -2
View File
@@ -1,7 +1,7 @@
import { readdir } from 'fs/promises'; import { readdir } from 'fs/promises';
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
export async function scratchedFlags(production: boolean) { export async function scratchedFlags(production: boolean) {
+2 -2
View File
@@ -2,8 +2,8 @@ import { readdir } from 'fs/promises';
import sharp from 'sharp'; import sharp from 'sharp';
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; import { readStringDVPL } from '../../src/core/blitz/readStringDVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
export async function shellIcons(production: boolean) { export async function shellIcons(production: boolean) {
+51
View File
@@ -0,0 +1,51 @@
import sharp from 'sharp';
import { TankClass } from '../../src/components/Tanks';
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA } from './constants';
interface SkillIcon {
name: string;
state: number;
}
export interface Avatar {
roles: unknown;
skillsByClasses: Record<TankClass, string>;
skills: {
[name: string]: {
userString: string;
effectDescription: string;
tipDescription: string;
icon: SkillIcon | SkillIcon[];
type: 'continuous' | 'trigger';
};
};
}
export async function skillIcons(production: boolean) {
console.log('Building skill icons...');
const avatar = await readXMLDVPL<{ root: Avatar }>(
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
);
const changes = await Promise.all(
Object.values(avatar.root.skills).map(async (skill) => {
const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon;
const name = icon.name.split('/').at(-1)!.replace(/_\d$/, '');
const path = `${DATA}${icon.name.replace('~res:', '')}.packed.webp.dvpl`;
const image = sharp(await readDVPLFile(path)).trim();
const content = (await image.toBuffer()).toString('base64');
return {
content,
encoding: 'base64',
path: `icons/skills/${name}.webp`,
} satisfies FileChange;
}),
);
commitAssets('skill icons', changes, production);
}
+2 -5
View File
@@ -3,8 +3,8 @@ import { readdir } from 'fs/promises';
import { extractArmor } from '../../src/core/blitz/extractArmor'; import { extractArmor } from '../../src/core/blitz/extractArmor';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId'; import { toUniqueId } from '../../src/core/blitz/toUniqueId';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { VehicleDefinitionList } from './definitions'; import { VehicleDefinitionList } from './definitions';
@@ -28,9 +28,6 @@ export async function tankArmors(production: boolean) {
if (tankKey.includes('tutorial_bot')) return; if (tankKey.includes('tutorial_bot')) return;
const id = toUniqueId(nation, tank.id); const id = toUniqueId(nation, tank.id);
console.log(`Building armor ${id} @ ${nation}/${tankKey}`);
const model = await extractArmor(DATA, `${nation}-${tankKey}`); const model = await extractArmor(DATA, `${nation}-${tankKey}`);
changes.push({ changes.push({
+28 -7
View File
@@ -1,11 +1,12 @@
import { readdir } from 'fs/promises'; import { readdir } from 'fs/promises';
import sharp from 'sharp';
import { Vector3Tuple } from 'three'; import { Vector3Tuple } from 'three';
import { NATION_IDS } from '../../src/constants/nations'; import { NATION_IDS } from '../../src/constants/nations';
import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { VehicleDefinitionList } from './definitions'; import { VehicleDefinitionList } from './definitions';
@@ -25,6 +26,20 @@ export interface TankParameters {
averageThickness?: Record<string, number>; averageThickness?: Record<string, number>;
}; };
}; };
maskSlice?: {
[key: string]:
| {
enabled: boolean;
planePosition: Vector3Tuple;
planeNormal: Vector3Tuple;
planeAxis: Vector3Tuple;
frustumConeBaseRadius: number;
frustumConeAngleDegrees: number;
planeSliceMode: number;
frustumConeSliceMode: number;
}
| undefined;
};
} }
export async function tankIcons(production: boolean) { export async function tankIcons(production: boolean) {
@@ -50,23 +65,29 @@ export async function tankIcons(production: boolean) {
const parameters = await readYAMLDVPL<TankParameters>( const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`, `${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
); );
const small = `${DATA}/${parameters.resourcesPath.smallIconPath const smallPath = `${DATA}/${parameters.resourcesPath.smallIconPath
.replace(/~res:\//, '') .replace(/~res:\//, '')
.replace(/\..+/, '')}.packed.webp.dvpl`; .replace(/\..+/, '')}.packed.webp.dvpl`;
const big = `${DATA}/${parameters.resourcesPath.bigIconPath const bigPath = `${DATA}/${parameters.resourcesPath.bigIconPath
.replace(/~res:\//, '') .replace(/~res:\//, '')
.replace(/\..+/, '')}.packed.webp.dvpl`; .replace(/\..+/, '')}.packed.webp.dvpl`;
const big = await sharp(await readDVPLFile(bigPath))
.trim()
.toBuffer();
const small = await sharp(await readDVPLFile(smallPath))
.trim()
.toBuffer();
if (big) { if (big) {
changes.push({ changes.push({
content: await readBase64DVPL(big), content: big.toString('base64'),
encoding: 'base64', encoding: 'base64',
path: `icons/tanks/big/${id}.webp`, path: `icons/tanks/big/${id}.webp`,
}); });
} }
if (small) { if (small) {
changes.push({ changes.push({
content: await readBase64DVPL(small), content: small.toString('base64'),
encoding: 'base64', encoding: 'base64',
path: `icons/tanks/small/${id}.webp`, path: `icons/tanks/small/${id}.webp`,
}); });
+2 -5
View File
@@ -4,8 +4,8 @@ import { extractModel } from '../../src/core/blitz/extractModel';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId'; import { toUniqueId } from '../../src/core/blitz/toUniqueId';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkit/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { VehicleDefinitionList } from './definitions'; import { VehicleDefinitionList } from './definitions';
import { TankParameters } from './tankIcons'; import { TankParameters } from './tankIcons';
@@ -32,9 +32,6 @@ export async function tankModels(production: boolean) {
if (tankKey.includes('tutorial_bot')) return; if (tankKey.includes('tutorial_bot')) return;
const id = toUniqueId(nation, tank.id); const id = toUniqueId(nation, tank.id);
console.log(`Building model ${id} @ ${nation}/${tankKey}`);
const parameters = await readYAMLDVPL<TankParameters>( const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`, `${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
); );
+142
View File
@@ -0,0 +1,142 @@
import { readdir } from 'fs/promises';
import { google } from 'googleapis';
import { cloneDeep, uniqBy } from 'lodash';
import { parse as parseYaml } from 'yaml';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import { VideoDefinitions } from '../../src/core/blitzkit/videos';
import { DATA, POI } from './constants';
import { BlitzStrings, botPattern, VehicleDefinitionList } from './definitions';
const youtubers = [
'UCKBYXp4Xn2I2tL1UL4fpbhw', // droodles
'UCrQ-Dy8lVsm11u8pCJ8W7Tw', // fatness
'UCxRpZqMElssTdPYDK3x7wyg', // n90
];
export async function videos(production: boolean) {
console.log('Building videos...');
const currentVideos = await fetch(
'https://raw.githubusercontent.com/tresabhi/blitzkit-assets/main/definitions/videos.csv',
)
.then((response) => response.text())
.then((lines) => {
return lines
.split('\n')
.map((line) => {
const [idString, lastUpdatedString, ...videos] = line.split(',');
const id = Number(idString);
const lastUpdated = Number(lastUpdatedString);
return { id, lastUpdated, videos };
})
.reduce<VideoDefinitions>(
(accumulator, { id, lastUpdated, videos }) => {
accumulator[id] = { lastUpdated, videos };
return accumulator;
},
{},
);
});
const auth = await google.auth.getClient({
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
});
const youtube = google.youtube({ version: 'v3', auth });
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
(nations) => nations.filter((nation) => nation !== 'common'),
);
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
`${DATA}/${POI.strings}/en.yaml.dvpl`,
);
const stringsCache = await fetch(POI.cachedStrings)
.then((response) => response.text())
.then((string) => parseYaml(string) as BlitzStrings);
const strings = {
...stringsPreInstalled,
...stringsCache,
};
const tanks: { id: number; name: string; tier: number }[] = [];
await Promise.all(
nations.map(async (nation) => {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
);
await Promise.all(
Object.entries(tankList.root).map(async ([tankKey, tank]) => {
if (botPattern.test(tankKey)) return null;
const id = toUniqueId(nation, tank.id);
const name =
(tank.shortUserString
? strings[tank.shortUserString]
: undefined) ?? strings[tank.userString];
const tier = tank.level;
tanks.push({ id, name, tier });
}),
);
}),
);
const tanksSanitized = tanks
.sort((a, b) => b.tier - a.tier)
.sort((a, b) => {
const lastUpdatedA = currentVideos[a.id]?.lastUpdated ?? 0;
const lastUpdatedB = currentVideos[b.id]?.lastUpdated ?? 0;
return lastUpdatedA - lastUpdatedB;
});
const videos: VideoDefinitions = cloneDeep(currentVideos);
let done = 0;
for (const tank of tanksSanitized) {
try {
const results = await youtube.search.list({
part: ['snippet'],
q: `World of Tanks Blitz ${tank.name}`,
});
if (!results.data.items || results.data.items.length === 0) continue;
const items = uniqBy(
results.data.items.filter(
(item) =>
item.snippet?.channelId &&
item.id?.videoId &&
youtubers.includes(item.snippet.channelId),
),
(item) => item.snippet!.channelId,
);
videos[tank.id] = {
lastUpdated: Math.round(Date.now() / 1000),
videos: items.map((item) => item.id!.videoId!),
};
console.log(
`${++done} / ${tanks.length} (${Math.round(100 * (done / tanks.length))}%) done; found ${items.length} for ${tank.name}`,
);
await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
} catch (error) {
console.warn('An error occurred, safely exiting prematurely...', error);
break;
}
}
const content = Object.entries(videos)
.map(
([id, { videos, lastUpdated }]) =>
`${id},${lastUpdated},${videos.join(',')}`,
)
.join('\n');
await commitAssets(
'videos',
[{ content, encoding: 'utf-8', path: 'definitions/videos.csv' }],
production,
);
}
+199
View File
@@ -0,0 +1,199 @@
import { chunk } from 'lodash';
import { argv } from 'process';
import {
RatingInfo,
RatingNeighbors,
RatingPlayer,
} from '../src/commands/ratingLeaderboard';
import { RegionSubdomain } from '../src/constants/regions';
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
import regionSubdomainToRegion from '../src/core/blitz/regionSubdomainToRegion';
import { commitAssets } from '../src/core/blitzkit/commitAssets';
import { FileChange } from '../src/core/blitzkit/commitMultipleFiles.js';
import { patientFetchJSON } from '../src/core/blitzkit/patientFetchJSON';
import { superCompress } from '../src/core/blitzkit/superCompress';
import {
BkrlBaseEntry,
BkrlFormat,
BkrlSuperset1Entry,
BkrlWriteStream,
} from '../src/core/streams/bkrl';
import { CdonValue, CdonWriteStream } from '../src/core/streams/cdon';
/*
* Central North American Time (UTC-5): use 0 5 * * *.
* Central European Time (UTC+1): use 0 23 * * *.
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
*/
const production = argv.includes('--production');
const target = argv
.find((arg) => arg.startsWith('--target='))
?.split('=')[1] as 'midnight' | 'latest' | undefined;
const regionSubdomains = argv
.find((arg) => arg.startsWith('--region='))
?.split('=')[1];
if (!target) throw new Error('Target parameter not specified');
if (!regionSubdomains) throw new Error('Region parameter not specified');
regionSubdomains.split(',').forEach(async (regionSubdomain) => {
const region = regionSubdomainToRegion(regionSubdomain as RegionSubdomain);
console.log(`Building ${target} rating leaderboard for ${region}`);
const NEIGHBORS = 2 ** 8;
const PLAYERS = await patientFetchJSON<RatingInfo>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) {
throw new Error('No current rating season running');
}
const players: Record<number, RatingPlayer> = {};
const leaderboard: BkrlBaseEntry[] = [];
let registered = 0;
async function branchFromPlayer(id: number) {
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
);
const firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1];
const branches: Promise<void>[] = [];
if (!players[firstPlayer.number])
branches.push(branchFromPlayer(firstPlayer.spa_id));
if (!players[lastPlayer.number])
branches.push(branchFromPlayer(lastPlayer.spa_id));
neighbors.forEach((neighbor) => {
if (players[neighbor.number]) return;
players[neighbor.number] = neighbor;
registered++;
});
console.log(
`Registered ${registered} / ${PLAYERS} (${(
(registered / PLAYERS!) *
100
).toFixed(2)}%)`,
);
await Promise.all(branches);
}
async function branchFromLeague(id: number) {
const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
);
const firstPlayer = result[0];
const lastPlayer = result.at(-1)!;
await Promise.all([
branchFromPlayer(firstPlayer.spa_id),
branchFromPlayer(lastPlayer.spa_id),
]);
}
console.log('Branching from leagues...');
await Promise.all([
branchFromLeague(0),
branchFromLeague(1),
branchFromLeague(2),
branchFromLeague(3),
branchFromLeague(4),
]);
console.log('Converting to an array...');
for (let index = 0; index < PLAYERS; index++) {
const listing = players[index + 1];
if (listing) {
leaderboard[index] = { id: listing.spa_id, score: listing.score };
} else {
console.warn(`Position ${index + 1} had no player`);
leaderboard[index] = { id: 0, score: 0 };
}
}
console.log('Requesting further stats...');
const comprehensiveLeaderboard: BkrlSuperset1Entry[] = [];
let chunkIndex = 0;
for (const leaderboardChunk of chunk(leaderboard, 100)) {
console.log(
`Chunk ${chunkIndex + 1} of ${Math.ceil(leaderboard.length / 100)}`,
);
const stats = await getAccountInfo(
region,
leaderboardChunk.map(({ id }) => id),
['statistics.rating'],
);
stats.forEach((stat, index) => {
const trueIndex = chunkIndex * 100 + index;
comprehensiveLeaderboard[trueIndex] =
leaderboardChunk[index].id === 0 ||
stat?.statistics.rating === undefined
? {
battles: 0,
damage: 0,
id: 0,
kills: 0,
score: 0,
survived: 0,
wins: 0,
}
: {
id: leaderboardChunk[index].id,
score: leaderboardChunk[index].score,
battles: stat.statistics.rating.battles,
damage: stat.statistics.rating.damage_dealt,
kills: stat.statistics.rating.frags,
survived: stat.statistics.rating.survived_battles,
wins: stat.statistics.rating.wins,
};
});
chunkIndex++;
}
const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
);
const normalizedServer = regionSubdomain === 'na' ? 'com' : regionSubdomain;
const infoContent = new CdonWriteStream().cdon(
info as unknown as CdonValue,
).uint8Array;
const leaderboardWriteStream = new BkrlWriteStream();
leaderboardWriteStream.bkrl({
format: BkrlFormat.Superset1,
entries: comprehensiveLeaderboard,
});
const changes: FileChange[] = [
{
path: `regions/${normalizedServer}/rating/${info.current_season}/info.cdon.lz4`,
content: superCompress(infoContent),
encoding: 'base64',
},
{
path: `regions/${normalizedServer}/rating/${info.current_season}/${target}.bkrl`,
content: Buffer.from(leaderboardWriteStream.uint8Array).toString(
'base64',
),
encoding: 'base64',
},
];
commitAssets(
`rating leaderboard ${regionSubdomain} ${target}`,
changes,
production,
);
});
-140
View File
@@ -1,140 +0,0 @@
import { rm, writeFile } from 'fs/promises';
import { argv } from 'process';
import {
RatingsInfo,
RatingsNeighbors,
RatingsPlayer,
} from '../src/commands/ratings';
import commitMultipleFiles from '../src/core/blitzkrieg/commitMultipleFiles.js';
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
/*
* Central North American Time (UTC-5): use 0 5 * * *.
* Central European Time (UTC+1): use 0 23 * * *.
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
*
* [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
* [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
* [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
*/
export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
const publish = argv.includes('--publish');
const target = argv
.find((arg) => arg.startsWith('--target='))
?.split('=')[1] as 'midnight' | 'latest' | undefined;
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
if (!target) throw new Error('Target parameter not specified');
if (!server) throw new Error('Region parameter not specified');
const NEIGHBORS = 2 ** 8;
const PLAYERS = await patientFetchJSON<RatingsInfo>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) {
throw new Error('No current ratings season running');
}
const players: Record<number, RatingsPlayer> = {};
const leaderboard: BlitzkriegRatingsLeaderboard = [];
let registered = 0;
async function branchFromPlayer(id: number) {
const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
);
const firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1];
const branches: Promise<void>[] = [];
if (!players[firstPlayer.number])
branches.push(branchFromPlayer(firstPlayer.spa_id));
if (!players[lastPlayer.number])
branches.push(branchFromPlayer(lastPlayer.spa_id));
neighbors.forEach((neighbor) => {
if (players[neighbor.number]) return;
players[neighbor.number] = neighbor;
registered++;
});
console.log(
`Registered ${registered} / ${PLAYERS} (${(
(registered / PLAYERS!) *
100
).toFixed(2)}%)`,
);
await Promise.all(branches);
}
async function branchFromLeague(id: number) {
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
);
const firstPlayer = result[0];
const lastPlayer = result[result.length - 1];
await Promise.all([
branchFromPlayer(firstPlayer.spa_id),
branchFromPlayer(lastPlayer.spa_id),
]);
}
console.log('Branching from leagues...');
await Promise.all([
branchFromLeague(0),
branchFromLeague(1),
branchFromLeague(2),
branchFromLeague(3),
branchFromLeague(4),
]);
console.log('Converting to an array...');
for (let index = 0; index < PLAYERS; index++) {
const listing = players[index + 1];
if (!listing) {
console.warn(`Position ${index + 1} had no player`);
continue;
}
leaderboard[index] = {
id: listing.spa_id,
score: listing.score,
};
}
const leaderboardJSON = JSON.stringify(leaderboard);
if (publish) {
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
);
const normalizedServer = server === 'na' ? 'com' : server;
const infoPath = `regions/${normalizedServer}/ratings/${info.current_season}/info.json`;
const leaderboardPath = `regions/${normalizedServer}/ratings/${info.current_season}/${target}.json`;
const infoJSON = JSON.stringify(info);
console.log(`Publishing to season ${info.current_season}...`);
commitMultipleFiles(
'tresabhi',
'blitzkrieg-assets',
'main',
new Date().toString(),
[
{ path: infoPath, content: infoJSON, encoding: 'utf-8' },
{ path: leaderboardPath, content: leaderboardJSON, encoding: 'utf-8' },
],
);
} else {
console.log('Writing to disk...');
await rm('dist.json', { force: true });
await writeFile('dist.json', leaderboardJSON);
}
-46
View File
@@ -1,46 +0,0 @@
import { writeFile } from 'fs/promises';
import { Region } from '../src/constants/regions';
import { getRatingsNeighbors } from '../src/core/blitz/getRatingsNeighbors';
const REGION: Region = 'com';
const PLAYER = 1041988373;
const ITERATIONS = 2 ** 4;
const MAX_NEIGHBORS = 2 ** 7;
const data: [number, number][] = [];
let finished = 0;
for (let index = 0; index < ITERATIONS; index++) {
const neighbors = Math.round((Math.random() * MAX_NEIGHBORS) / 2) * 2;
const start = Date.now();
console.log(`🟡 Iteration ${index} with ${neighbors} neighbors`);
try {
await getRatingsNeighbors(REGION, PLAYER, neighbors);
finished++;
console.log(
`🟢 (${Math.round(
100 * (finished / ITERATIONS),
)}%) Iteration ${index} with ${neighbors} neighbors`,
);
const end = Date.now();
const time = end - start;
data.push([neighbors, time]);
} catch (error) {
console.log(
`🔴 Iteration ${index} with ${neighbors} neighbors\n\n${error}`,
);
}
}
console.log('🟢 Done, sorting');
data.sort(([neighborsA], [neighborsB]) => neighborsA - neighborsB);
console.log('🟢 Writing');
writeFile(
'dist/ratingsPlot.csv',
data.map(([neighbors, time]) => `${neighbors},${time}`).join('\n'),
);
+26
View File
@@ -0,0 +1,26 @@
import { tankDefinitions } from '../src/core/blitzkit/tankDefinitions';
// random vercel bug forces require import
const { writeFileSync } = require('fs');
console.log('Building sitemap...');
const awaitedTankDefinitions = await tankDefinitions;
const values = Object.values(awaitedTankDefinitions);
const txt = values
.map((tank) => `https://blitzkit.vercel.app/tools/tankopedia/${tank.id}/`)
.join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${values
.map(
(tank) =>
`<url><loc>https://blitzkit.vercel.app/tools/tankopedia/${tank.id}/</loc></url>`,
)
.join('\n')}
</urlset>`;
writeFileSync('public/sitemap.txt', txt);
writeFileSync('public/sitemap.xml', xml);
console.log(`Built sitemap for ${values.length} tanks`);
+96
View File
@@ -0,0 +1,96 @@
import { chunk, times } from 'lodash';
import { compress } from 'lz4js';
import { argv } from 'process';
import { Region, REGIONS } from '../src/constants/regions';
import getTankStats from '../src/core/blitz/getTankStats';
import { idToRegion } from '../src/core/blitz/idToRegion';
import { commitAssets } from '../src/core/blitzkit/commitAssets';
import { FileChange } from '../src/core/blitzkit/commitMultipleFiles';
import {
RtscWritePlayer,
RtscWriteStream,
RtsmWriteStream,
} from '../src/core/streams/rts';
import usersRaw from '../test.users.json';
const PLAYERS_PER_CHUNK = 256;
const CHUNKS_PER_BATCH = 128;
const production = argv.includes('--production');
const region = argv
.find((argument) => argument.startsWith('--region'))
?.split('=')[1] as Region | undefined;
if (!region || !REGIONS.includes(region)) {
throw new Error('No valid region specified');
}
const today = new Date();
const todayPath = `${today.getUTCFullYear()}/${today.getUTCMonth() + 1}/${today.getUTCDate()}`;
const users = usersRaw.filter(({ blitz }) => idToRegion(blitz) === region);
const base = Math.ceil(users.length / PLAYERS_PER_CHUNK);
const chunks = times(base, (offset) => ({
offset,
users: users.filter((user) => user.blitz % base === offset),
}));
const batches = chunk(chunks, CHUNKS_PER_BATCH);
const manifestWriteStream = new RtsmWriteStream();
console.log(`Found ${users.length} players in ${region}`);
await commitAssets(
`regular tank stats manifests ${todayPath}`,
[
{
encoding: 'base64',
path: `regions/${region}/regular/tanks/${todayPath}/manifest.rtsm`,
content: Buffer.from(
compress(manifestWriteStream.rtsm(base).uint8Array),
).toString('base64'),
},
],
production,
);
let batchesDone = 0;
for (const batch of batches) {
console.log(`Batch ${++batchesDone} of ${batches.length}`);
const changes: FileChange[] = [];
let chunksDone = 0;
for (const chunk of batch) {
console.log(`Chunk ${++chunksDone} of ${chunks.length}`);
let playersDone = 0;
const players = await Promise.all(
chunk.users.map(async (user) => {
const tanks = await getTankStats(region, user.blitz);
console.log(`Player ${++playersDone} of ${chunk.users.length}`);
if (tanks === null) return null;
return {
id: user.blitz,
tanks,
} satisfies RtscWritePlayer;
}),
).then((players) => players.filter((player) => player !== null));
const writeStream = new RtscWriteStream();
writeStream.rtsc(players);
changes.push({
encoding: 'base64',
content: Buffer.from(compress(writeStream.uint8Array)).toString('base64'),
path: `regions/${region}/regular/tanks/${todayPath}/${chunk.offset}.rtsc.lz4`,
});
}
// do not await; commit in background
commitAssets(
`regular tank stats ${todayPath} batch ${batches.indexOf(batch) + 1}`,
changes,
production,
);
}
+36
View File
@@ -0,0 +1,36 @@
import AdmZip from 'adm-zip';
import { readFile } from 'fs/promises';
import { BattleResultsReadStream } from '../src/core/streams/battleResults';
interface WoTBReplayMeta {
version: string;
title: string;
dbid: string;
playerName: string;
battleStartTime: string;
playerVehicleName: string;
mapName: string;
arenaUniqueId: string;
battleDuration: number;
vehicleCompDescriptor: number;
camouflageId: number;
mapId: number;
arenaBonusType: number;
}
const file = await readFile('temp/test.wotbreplay');
const zip = new AdmZip(file);
const entries = zip.getEntries();
const metaRaw = entries.find((entry) => entry.name === 'meta.json');
const battleResultsRaw = entries.find(
(entry) => entry.name === 'battle_results.dat',
);
if (!metaRaw) throw new Error('No meta.json found');
if (!battleResultsRaw) throw new Error('No battle_results.dat found');
const meta = JSON.parse(metaRaw.getData().toString()) as WoTBReplayMeta;
const battleResults = new BattleResultsReadStream(
battleResultsRaw.getData().buffer,
).battleResults();
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"resolveJsonModule": true,
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"incremental": true,
"isolatedModules": true
}
}
@@ -1,10 +1,10 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { Region } from '../../../../../../constants/regions'; import { Region } from '../../../../../../constants/regions';
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo'; import getRatingInfo from '../../../../../../core/blitz/getRatingInfo';
export async function GET( export async function GET(
request: Request, request: Request,
{ params }: { params: { region: Region } }, { params }: { params: { region: Region } },
) { ) {
return NextResponse.json(await getRatingsInfo(params.region)); return NextResponse.json(await getRatingInfo(params.region));
} }
@@ -1,12 +1,12 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions'; import { Region } from '../../../../../../../constants/regions';
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague'; import { getRatingLeague } from '../../../../../../../core/blitz/getRatingLeague';
export async function GET( export async function GET(
request: Request, request: Request,
{ params }: { params: { region: Region; league: number } }, { params }: { params: { region: Region; league: number } },
) { ) {
return NextResponse.json( return NextResponse.json(
await getRatingsLeague(params.region, params.league, true), await getRatingLeague(params.region, params.league, true),
); );
} }
@@ -1,12 +1,12 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions'; import { Region } from '../../../../../../../constants/regions';
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers'; import { searchCurrentRatingPlayers } from '../../../../../../../core/blitz/searchCurrentRatingPlayers';
export async function GET( export async function GET(
request: Request, request: Request,
{ params }: { params: { region: Region; username: string } }, { params }: { params: { region: Region; username: string } },
) { ) {
return NextResponse.json( return NextResponse.json(
await searchCurrentRatingsPlayers(params.region, params.username), await searchCurrentRatingPlayers(params.region, params.username),
); );
} }
+62
View File
@@ -0,0 +1,62 @@
'use client';
import { Flex, Heading, Text } from '@radix-ui/themes';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { mutateApp } from '../../../stores/app';
export default function Page({
params,
searchParams,
}: {
params: { provider: string };
searchParams: { [key: string]: string };
}) {
const router = useRouter();
useEffect(() => {
switch (params.provider) {
case 'wargaming': {
if (
!searchParams.expires_at ||
!searchParams.access_token ||
!searchParams.account_id
) {
break;
}
mutateApp((draft) => {
draft.login = {
expiresAt: Number(searchParams.expires_at),
id: Number(searchParams.account_id),
token: searchParams.access_token,
};
});
break;
}
default: {
console.error(`Unknown provider: ${params.provider}`);
}
}
router.push(searchParams.return ? searchParams.return : '/');
});
return (
<PageWrapper>
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="2"
>
<Heading>Verifying you...</Heading>
<Text color="gray">You'll be on your way shortly.</Text>
</Flex>
</PageWrapper>
);
}
-339
View File
@@ -1,339 +0,0 @@
'use client';
import { ContextMenu } from '@radix-ui/themes';
import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { resetSession } from '../../../core/blitzkrieg/resetSession';
import {
TankDefinition,
tankDefinitions,
} from '../../../core/blitzkrieg/tankDefinitions';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import mutateSession, { useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
const REFRESH_RATE = 1 / 5;
interface SessionPageProps {
naked?: boolean;
}
export default function SessionPage({ naked = true }: SessionPageProps) {
const session = useSession();
const since = session.isTracking ? new Date(session.time) : undefined;
const [diff, setDiff] = useState<
| {
careerWN8: number;
list: {
stats: IndividualTankStats;
tankDefinitions?: TankDefinition;
career: IndividualTankStats;
currentWN8?: number;
careerWN8?: number;
}[];
}
| undefined
>(undefined);
const sessionBattles = diff?.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.battles,
0,
);
const tankStatsArray = session.isTracking
? (Object.values(session.tankStats) as IndividualTankStats[])
: [];
const sessionWinrate = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.wins,
0,
) / sessionBattles!
: 0;
const careerBattles = tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.battles,
0,
);
const careerWinrate =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.wins,
0,
) / careerBattles;
const currentDamage = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.damage_dealt,
0,
) / sessionBattles!
: 0;
const careerDamage =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.damage_dealt,
0,
) / careerBattles;
useEffect(() => {
if (session.title === 'Untitled session') {
mutateSession((draft) => {
if (draft.isTracking) draft.title = draft.nickname;
});
}
}, [session.title]);
useEffect(() => {
async function recalculateDiff() {
if (session.isTracking) {
const { id, region } = session;
const careerRaw = await fetchBlitz<TanksStats>(
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const career = careerRaw[id].reduce<
Record<number, IndividualTankStats>
>(
(accumulator, curr) => ({
...accumulator,
[curr.tank_id]: curr,
}),
{},
);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await tankAverages;
const careerWN8 =
careerRaw[id].reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles *
calculateWN8(awaitedTankAverages[tank_id].all, all)
: 0),
0,
) /
careerRaw[id].reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles
: 0),
0,
);
setDiff({
careerWN8,
list: (
Object.values(
deltaTankStats(session.tankStats, career, session.time / 1000),
) as IndividualTankStats[]
).map((stats) => {
return {
stats,
tankDefinitions: awaitedTankDefinitions[stats.tank_id],
career: career[stats.tank_id],
currentWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
stats.all,
)
: undefined,
careerWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
career[stats.tank_id].all,
)
: undefined,
};
}),
});
}
}
recalculateDiff();
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
return () => clearInterval(interval);
}, [session]);
const totalRowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: sessionBattles?.toLocaleString(),
career: tankStatsArray
.reduce((accumulator, stats) => accumulator + stats.all.battles, 0)
.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: sessionBattles ? `${(100 * sessionWinrate).toFixed(2)}%` : '--',
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
},
wn8: (() => {
if (!diff) return undefined;
const battlesWithWN8 = diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
(currentWN8 === undefined ? 0 : stats.all.battles) + accumulator,
0,
);
const currentWN8 =
diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
stats.all.battles * (currentWN8 ?? 0) + accumulator,
0,
) / battlesWithWN8!;
return {
title: 'WN8',
current: battlesWithWN8
? Math.round(currentWN8!).toLocaleString()
: '--',
percentile: battlesWithWN8 ? getWN8Percentile(currentWN8!) : undefined,
career: Math.round(diff.careerWN8).toLocaleString(),
};
})(),
damage: {
title: 'Damage',
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
current: sessionBattles
? Math.round(currentDamage).toLocaleString()
: '--',
career: Math.round(careerDamage).toLocaleString(),
},
none: undefined,
};
return (
<ContextMenu.Root>
<ContextMenu.Trigger>
<div>
<Breakdown.Root>
{session.showTotal &&
session.isTracking &&
since !== undefined &&
diff !== undefined && (
<Breakdown.Row
naked={naked}
color={session.color}
minimized={!session.showCareer}
title={session.title}
stats={session.customColumns.map((customColumn) => {
const rowStat = totalRowStats[customColumn.display];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
)}
{diff?.list
.sort(
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
)
.map(
({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => {
const rowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: stats.all.battles.toLocaleString(),
career: career.all.battles.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: `${(
100 *
(stats.all.wins / stats.all.battles)
).toFixed(2)}%`,
career: `${(
100 *
(career.all.wins / career.all.battles)
).toFixed(2)}%`,
delta:
stats.all.wins / stats.all.battles -
career.all.wins / career.all.battles,
},
wn8: {
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
damage: {
title: 'Damage',
current: Math.round(
stats.all.damage_dealt / stats.all.battles,
).toLocaleString(),
career: Math.round(
career.all.damage_dealt / career.all.battles,
).toLocaleString(),
delta:
stats.all.damage_dealt / stats.all.battles -
career.all.damage_dealt / career.all.battles,
},
none: undefined,
};
return (
<Breakdown.Row
naked={naked}
color={session.color}
key={stats.tank_id}
minimized={!session.showCareer}
title={
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
}
type="tank"
tankType={tankDefinitions?.class}
treeType={tankDefinitions?.treeType}
stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
);
},
)}
</Breakdown.Root>
</div>
</ContextMenu.Trigger>
<Menu reset={resetSession} Builder={ContextMenu} />
</ContextMenu.Root>
);
}
+68 -102
View File
@@ -1,20 +1,19 @@
'use client'; 'use client';
import { CaretRightIcon } from '@radix-ui/react-icons'; import { AlertDialog, Button, Flex, Theme } from '@radix-ui/themes';
import { AlertDialog, Button, Flex, Text, Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css'; import '@radix-ui/themes/styles.css';
import { Analytics } from '@vercel/analytics/react';
import { config } from 'dotenv'; import { config } from 'dotenv';
import { Roboto_Flex } from 'next/font/google'; import { Roboto_Flex } from 'next/font/google';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { ReactNode, useEffect, useState } from 'react'; import { ReactNode, Suspense, useEffect, useState } from 'react';
import { SkyLine } from '../assets/art/SkyLine'; import { Footer } from '../components/Footer';
import { Loader } from '../components/Loader';
import Navbar from '../components/Navbar'; import Navbar from '../components/Navbar';
import PageWrapper from '../components/PageWrapper'; import { Party3 } from '../components/Party3';
import isDev from '../core/blitzkrieg/isDev'; import isDev from '../core/blitzkit/isDev';
import { isLocalhost } from '../core/blitzkrieg/isLocalhost'; import { isLocalhost } from '../core/blitzkit/isLocalhost';
import { useFullScreen } from '../hooks/useFullScreen';
import { useApp } from '../stores/app'; import { useApp } from '../stores/app';
import { mintTheme } from '../themes/mint';
config(); config();
@@ -32,7 +31,10 @@ const robotoFlex = Roboto_Flex({
export default function RootLayout({ children }: RootLayoutProps) { export default function RootLayout({ children }: RootLayoutProps) {
const pathname = usePathname(); const pathname = usePathname();
const isEmbed = pathname.split('/')[1] === 'embeds'; const isEmbed = pathname.split('/')[1] === 'embeds';
const isRoot = pathname === '/';
const darkMode = useApp((state) => state.darkMode);
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false); const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
const isFullScreen = useFullScreen();
useEffect(() => { useEffect(() => {
setShowDevBuildAlert( setShowDevBuildAlert(
@@ -46,116 +48,80 @@ export default function RootLayout({ children }: RootLayoutProps) {
return ( return (
<html lang="en" className={robotoFlex.className}> <html lang="en" className={robotoFlex.className}>
<head> <head>
<title>Blitzkrieg</title> {isRoot && (
<meta <>
name="description" <title>BlitzKit</title>
content="🎉 Tools for everything World of Tanks Blitz" <meta
/> name="description"
content="🎉 Tools for everything World of Tanks Blitz"
/>
</>
)}
<meta property="og:site_name" content="BlitzKit" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<Party3 />
</head> </head>
<body <body
style={{ style={{
margin: 0, margin: 0,
paddingTop: isEmbed ? 0 : '3.25rem',
backgroundColor: isEmbed ? 'transparent' : undefined, backgroundColor: isEmbed ? 'transparent' : undefined,
}} }}
> >
<Analytics />
<Theme <Theme
appearance="dark" appearance={darkMode ? 'dark' : 'light'}
panelBackground="translucent" panelBackground="solid"
radius="full" radius="full"
suppressHydrationWarning suppressHydrationWarning
suppressContentEditableWarning suppressContentEditableWarning
> >
{!isEmbed && <Navbar />} <Flex
<AlertDialog.Root open={showDevBuildAlert}> direction="column"
<AlertDialog.Content> style={{ minHeight: '100vh', paddingTop: isEmbed ? undefined : 64 }}
<AlertDialog.Title>Experimental version!</AlertDialog.Title> >
<AlertDialog.Description> {!isEmbed && <Navbar />}
This version may have a lot of issues. Report issues to{' '} <AlertDialog.Root open={showDevBuildAlert}>
<a href="https://discord.gg/nDt7AjGJQH" target="_blank"> <AlertDialog.Content>
the official Discord server <AlertDialog.Title>Experimental version!</AlertDialog.Title>
</a> <AlertDialog.Description>
. Also consider using{' '} This version may have a lot of issues. Report issues to{' '}
<a href="https://blitz-krieg.vercel.app/"> <a href="https://discord.gg/nDt7AjGJQH" target="_blank">
the more stable release version the official Discord server
</a> </a>
. You will be asked again in 8 days. . Also consider using{' '}
</AlertDialog.Description> <a href="https://blitzkit.vercel.app/">
the more stable release version
</a>
. You will be asked again in 8 days.
</AlertDialog.Description>
<Flex justify="end"> <Flex justify="end">
<Button <Button
variant="solid" variant="solid"
onClick={() => { onClick={() => {
setShowDevBuildAlert(false); setShowDevBuildAlert(false);
useApp.setState({ devBuildAgreementTime: Date.now() }); useApp.setState({ devBuildAgreementTime: Date.now() });
}}
>
Continue
</Button>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<Flex direction="column">{children}</Flex>
{!isEmbed && (
<PageWrapper>
<a
target="_blank"
href="https://ko-fi.com/tresabhi"
style={{ textDecoration: 'none' }}
>
<Flex
align="center"
style={{
padding: 16,
borderRadius: 8,
backgroundColor: mintTheme.colors.componentInteractive,
position: 'relative',
overflow: 'hidden',
}}
>
<Flex
direction="column"
gap="1"
style={{
flex: 1,
zIndex: 1,
}} }}
> >
<Text size="4" color="mint"> Continue
Blitzkrieg. Free forever. For everyone. </Button>
</Text>
<Text color="mint">
<b>Consider supporting today 🍀</b>
</Text>
</Flex>
<Text
color="mint"
style={{
zIndex: 1,
}}
>
<Flex>
<CaretRightIcon width={32} height={32} />
</Flex>
</Text>
<SkyLine
style={{
position: 'absolute',
right: 'min(8vw, 15%)',
top: '5%',
}}
/>
</Flex> </Flex>
</a> </AlertDialog.Content>
</PageWrapper> </AlertDialog.Root>
)}
<Suspense
fallback={
<div style={{ flex: 1 }}>
<Loader />
</div>
}
>
{children}
</Suspense>
{!isEmbed && !isFullScreen && <Footer />}
</Flex>
</Theme> </Theme>
</body> </body>
</html> </html>
+1 -1
View File
@@ -3,7 +3,7 @@ import { theme } from '../stitches.config';
export const tool = styleVariants({ export const tool = styleVariants({
enabled: { enabled: {
boxShadow: `inset 0 -256px 512px -128px ${theme.colors.appBackground1}`, boxShadow: `inset 0 -200px 128px -128px ${theme.colors.appBackground1}`,
':hover': { ':hover': {
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`, boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
+75 -79
View File
@@ -1,95 +1,91 @@
'use client'; 'use client';
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { CaretRightIcon } from '@radix-ui/react-icons';
import { Flex, Text, TextField } from '@radix-ui/themes'; import { Button, Flex, Link as LinkRadix } from '@radix-ui/themes';
import Link from 'next/link'; import Link from 'next/link';
import { useRef } from 'react';
import { Search, SearchItem } from 'react-fuzzysort';
import PageWrapper from '../components/PageWrapper'; import PageWrapper from '../components/PageWrapper';
import { TOOLS } from '../constants/tools'; import { TOOLS } from '../constants/tools';
import { PatreonIcon } from '../icons/Patreon';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
import * as styles from './page.css'; import * as styles from './page.css';
export default function Page() { export default function Page() {
const input = useRef<HTMLInputElement>(null);
return ( return (
<PageWrapper> <PageWrapper>
<TextField.Root> <Flex
<TextField.Slot> direction="column"
<MagnifyingGlassIcon height="16" width="16" /> gap="5"
</TextField.Slot> justify="center"
<TextField.Input ref={input} placeholder="Search tools..." /> align="center"
</TextField.Root> style={{ flex: 1 }}
>
<LinkRadix href="https://www.patreon.com/tresabhi" target="_blank">
<Button
color="ruby"
style={{
boxShadow: `0 0 8px ${theme.colors.solidBackground_ruby}f0`,
}}
>
<PatreonIcon style={{ width: '1em', height: '1em' }} /> Consider
donating on Patreon
</Button>
</LinkRadix>
<Flex gap="3" wrap="wrap" align="center" justify="center"> <Flex gap="3" wrap="wrap" align="center" justify="center">
<Search {TOOLS.map((tool) => (
input={input} <Link
list={TOOLS.map( href={tool.href ?? `/tools/${tool.id}`}
(tool) => target={tool.href ? '_blank' : undefined}
({ className={
node: ( tool.disabled ? styles.tool.disabled : styles.tool.enabled
<Link }
href={tool.href ?? `/tools/${tool.id}`} style={{
target={tool.href ? '_blank' : undefined} backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
className={ cursor: tool.disabled ? 'default' : 'pointer',
tool.disabled ? styles.tool.disabled : styles.tool.enabled opacity: tool.disabled ? 0.25 : 1,
} height: 128,
style={{ minWidth: 256,
backgroundImage: `url(/assets/banners/${tool.id}.webp)`, flex: 1,
cursor: tool.disabled ? 'default' : 'pointer', borderRadius: 8,
opacity: tool.disabled ? 0.25 : 1, textDecoration: 'none',
height: 128, backgroundSize: 'cover',
minWidth: 256, backgroundPosition: 'center',
flex: 1, overflow: 'hidden',
borderRadius: 8, display: 'flex',
textDecoration: 'none', flexDirection: 'column',
backgroundSize: 'cover', alignItems: 'flex-start',
backgroundPosition: 'center', justifyContent: 'flex-end',
overflow: 'hidden', padding: 16,
display: 'flex', transition: `box-shadow ${theme.durations.regular}`,
flexDirection: 'column', }}
alignItems: 'flex-start', onClick={(event) => {
justifyContent: 'flex-end', if (tool.disabled) event.preventDefault();
padding: 16, }}
gap: 4, >
transition: `box-shadow ${theme.durations.regular}`, <span
}} style={{
onClick={(event) => { fontSize: 32,
if (tool.disabled) event.preventDefault(); color: theme.colors.textHighContrast,
}} display: 'flex',
> alignItems: 'center',
<span justifyContent: 'center',
style={{ }}
fontSize: 32, >
color: theme.colors.textHighContrast, {tool.title}
display: 'flex', <CaretRightIcon style={{ width: '1em', height: '1em' }} />
alignItems: 'center', </span>
justifyContent: 'center', <span
}} style={{
> color: theme.colors.textLowContrast,
{tool.title} fontSize: 16,
<CaretRightIcon style={{ width: '1em', height: '1em' }} /> }}
</span> >
<span {tool.disabled ? 'Coming soon!' : tool.description}
style={{ </span>
color: theme.colors.textLowContrast, </Link>
fontSize: 16, ))}
}} </Flex>
>
{tool.disabled ? 'Coming soon!' : tool.description}
</span>
</Link>
),
query: `${tool.title} ${tool.description}`,
}) satisfies SearchItem,
)}
/>
</Flex> </Flex>
<Text size="1" color="gray" align="center">
Images courtesy of Wargaming Art Department
</Text>
</PageWrapper> </PageWrapper>
); );
} }
+44
View File
@@ -0,0 +1,44 @@
'use client';
import { Flex, Heading, Switch, Text } from '@radix-ui/themes';
import PageWrapper from '../../components/PageWrapper';
import { useApp } from '../../stores/app';
export default function Page() {
const developerMode = useApp((state) => state.developerMode);
const darkMode = useApp((state) => state.darkMode);
return (
<PageWrapper justify="center" align="center">
<Flex direction="column" gap="4">
<Flex gap="3" direction="column">
<Heading size="5">Appearance</Heading>
<Flex align="center" gap="2">
<Text>Dark mode (experimental)</Text>
<Switch
checked={darkMode}
onCheckedChange={(checked) =>
useApp.setState({ darkMode: checked })
}
/>
</Flex>
</Flex>
<Flex gap="3" direction="column">
<Heading size="5">Advanced</Heading>
<Flex align="center" gap="2">
<Text>Developer mode</Text>
<Switch
checked={developerMode}
onCheckedChange={(checked) =>
useApp.setState({ developerMode: checked })
}
/>
</Flex>
</Flex>
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,76 @@
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
import Link from 'next/link';
import { use, useState } from 'react';
import { provisionDefinitions } from '../../../../core/blitzkit/provisionDefinitions';
import { tankToCompareMember } from '../../../../core/blitzkit/tankToCompareMember';
import { mutateCompareTemporary } from '../../../../stores/compare';
import { TankSearch } from '../../tankopedia/components/TankSearch';
interface TankControlProps {
index: number;
id: number;
}
export function TankControl({ index, id }: TankControlProps) {
const awaitedProvisionDefinitions = use(provisionDefinitions);
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
return (
<Flex gap="3" justify="center" style={{ width: '100%' }}>
<IconButton
variant="ghost"
onClick={() => {
mutateCompareTemporary((draft) => {
draft.members.splice(index, 1);
draft.sorting = undefined;
});
}}
>
<TrashIcon />
</IconButton>
<Dialog.Root
open={switchTankDialogOpen}
onOpenChange={setSwitchTankDialogOpen}
>
<Dialog.Trigger>
<IconButton variant="ghost">
<LoopIcon />
</IconButton>
</Dialog.Trigger>
<Dialog.Content>
<Flex gap="4" direction="column">
<Flex
direction="column"
gap="4"
style={{ flex: 1 }}
justify="center"
>
<TankSearch
compact
onSelect={(tank) => {
mutateCompareTemporary((draft) => {
draft.members[index] = tankToCompareMember(
tank,
awaitedProvisionDefinitions,
);
draft.sorting = undefined;
});
setSwitchTankDialogOpen(false);
}}
/>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
<Link href={`/tools/tankopedia/${id}`}>
<IconButton variant="ghost">
<ExternalLinkIcon />
</IconButton>
</Link>
</Flex>
);
}
File diff suppressed because it is too large. Load diff
-52
View File
@@ -1,52 +0,0 @@
import { blackA } from '@radix-ui/colors';
import { Flex, Text } from '@radix-ui/themes';
import { PuffLoader } from 'react-spinners';
import { theme } from '../../../stitches.config';
const NAV_HEIGHT = 52.65;
interface LoaderProps {
naked?: boolean;
color?: string;
progress?: number;
}
export function Loader({ naked, color, progress }: LoaderProps) {
const animation = (
<div style={{ position: 'relative' }}>
{progress !== undefined && (
<Text
style={{
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(calc(-50% + 5px), calc(-50% + 5px))',
}}
>
{Math.round(progress * 100)}%
</Text>
)}
<PuffLoader color={color ?? theme.colors.solidBackground} />
</div>
);
if (naked) return animation;
return (
<Flex
style={{
backgroundColor: blackA.blackA10,
position: 'fixed',
top: NAV_HEIGHT,
left: 0,
width: '100%',
height: `calc(100% - ${NAV_HEIGHT}px)`,
padding: theme.spaces.paddingMajor,
}}
align="center"
justify="center"
>
{animation}
</Flex>
);
}
+26 -4
View File
@@ -2,10 +2,9 @@
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { ReactNode, Suspense } from 'react'; import { ReactNode } from 'react';
import { TOOLS } from '../../constants/tools'; import { TOOLS } from '../../constants/tools';
import { theme } from '../../stitches.config'; import { theme } from '../../stitches.config';
import { Loader } from './components/Loader';
interface ToolsLayoutProps { interface ToolsLayoutProps {
children: ReactNode; children: ReactNode;
@@ -15,11 +14,34 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
const pathname = usePathname(); const pathname = usePathname();
const pathSplit = pathname.split('/'); const pathSplit = pathname.split('/');
const toolId = pathSplit.at(2); const toolId = pathSplit.at(2);
const isToolRoot = pathname === `/tools/${toolId}`;
const tool = TOOLS.find((tool) => tool.id === toolId); const tool = TOOLS.find((tool) => tool.id === toolId);
return ( return (
<> <>
{tool && <title>{tool.title}</title>} {tool && (
<>
{isToolRoot && (
<>
<title>{tool.title}</title>
<meta
property="og:image"
content={`/assets/banners/${toolId}.webp`}
/>
{tool.pageDescription !== undefined && (
<>
<meta name="description" content={tool.pageDescription} />
<meta
property="og:description"
content={tool.pageDescription}
/>
</>
)}
</>
)}
</>
)}
{pathSplit.length === 3 && ( {pathSplit.length === 3 && (
<div <div
@@ -73,7 +95,7 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
</div> </div>
)} )}
<Suspense fallback={<Loader />}>{children}</Suspense> {children}
</> </>
); );
} }
@@ -53,29 +53,26 @@ export function PageTurner({
<CaretLeftIcon /> <CaretLeftIcon />
)} )}
</Button> </Button>
<TextField.Root>
<TextField.Root
type="number"
ref={pageInput}
style={{ textAlign: 'center' }}
min={1}
max={pages}
onBlur={(event) => {
onPageChange(
Math.max(0, Math.min(pages - 1, event.target.valueAsNumber - 1)),
false,
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
(event.target as HTMLInputElement).blur();
}
}}
>
<TextField.Slot>Page</TextField.Slot> <TextField.Slot>Page</TextField.Slot>
<TextField.Input
type="number"
ref={pageInput}
style={{ width: 64, textAlign: 'center' }}
min={1}
max={pages}
onBlur={(event) => {
onPageChange(
Math.max(
0,
Math.min(pages - 1, event.target.valueAsNumber - 1),
),
false,
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
(event.target as HTMLInputElement).blur();
}
}}
/>
<TextField.Slot>out of {pages}</TextField.Slot> <TextField.Slot>out of {pages}</TextField.Slot>
</TextField.Root> </TextField.Root>
<Button <Button
@@ -15,30 +15,29 @@ import { produce } from 'immer';
import { debounce, range } from 'lodash'; import { debounce, range } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { create } from 'zustand'; import { create } from 'zustand';
import { RatingsInfo, RatingsPlayer } from '../../../commands/ratings'; import { RatingInfo, RatingPlayer } from '../../../commands/ratingLeaderboard';
import PageWrapper from '../../../components/PageWrapper'; import PageWrapper from '../../../components/PageWrapper';
import { Skeleton } from '../../../components/Skeleton'; import { Skeleton } from '../../../components/Skeleton';
import { LEAGUES } from '../../../constants/leagues'; import { LEAGUES } from '../../../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings'; import { FIRST_MINIMAL_ARCHIVED_RATING_SEASON } from '../../../constants/rating';
import { import {
REGIONS, REGIONS,
Region, Region,
UNLOCALIZED_REGION_NAMES, UNLOCALIZED_REGION_NAMES,
} from '../../../constants/regions'; } from '../../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz'; import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../../../core/blitz/getRatingsInfo'; import getRatingInfo from '../../../core/blitz/getRatingInfo';
import { getRatingsLeague } from '../../../core/blitz/getRatingsLeague'; import { getRatingLeague } from '../../../core/blitz/getRatingLeague';
import { getRatingsNeighbors } from '../../../core/blitz/getRatingsNeighbors'; import { getRatingNeighbors } from '../../../core/blitz/getRatingNeighbors';
import { searchCurrentRatingsPlayers } from '../../../core/blitz/searchCurrentRatingsPlayers'; import { searchCurrentRatingPlayers } from '../../../core/blitz/searchCurrentRatingPlayers';
import { import {
AccountList, AccountList,
AccountListItem, AccountListItem,
} from '../../../core/blitz/searchPlayersAcrossRegions'; } from '../../../core/blitz/searchPlayersAcrossRegions';
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber'; import { getArchivedLatestSeasonNumber } from '../../../core/blitzkit/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../../../core/blitzkrieg/getArchivedRatingsInfo'; import getArchivedRatingInfo from '../../../core/blitzkit/getArchivedRatingInfo';
import { getArchivedRatingsLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboard'; import { getArchivedRatingLeaderboard } from '../../../core/blitzkit/getArchivedRatingLeaderboard';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
import { PageTurner } from './components/PageTurner'; import { PageTurner } from './components/PageTurner';
@@ -51,7 +50,7 @@ interface NameCacheEntry {
} }
type NameCache = Record<Region, Record<number, NameCacheEntry | null>>; type NameCache = Record<Region, Record<number, NameCacheEntry | null>>;
type LeaderboardCache = Record<Region, Record<number, Record<number, number>>>; type LeaderboardCache = Record<Region, Record<number, Record<number, number>>>;
type InfoCache = Record<Region, Record<number, RatingsInfo | undefined>>; type InfoCache = Record<Region, Record<number, RatingInfo | undefined>>;
type ScoreCache = Record<Region, Record<number, Record<number, number>>>; type ScoreCache = Record<Region, Record<number, Record<number, number>>>;
const useNameCache = create<NameCache>(() => ({ const useNameCache = create<NameCache>(() => ({
@@ -93,7 +92,7 @@ export default function Page() {
{ type: 'id'; id: number } | { type: 'position'; position: number } | null { type: 'id'; id: number } | { type: 'position'; position: number } | null
>(null); >(null);
const ratingsInfo = useInfoCache()[region][season]; const ratingInfo = useInfoCache()[region][season];
const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState< const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState<
number | null number | null
>(null); >(null);
@@ -111,7 +110,7 @@ export default function Page() {
}); });
const pages = Math.ceil( const pages = Math.ceil(
(ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) / (ratingInfo && ratingInfo.detail === undefined ? ratingInfo?.count : 1) /
ROWS_PER_PAGE, ROWS_PER_PAGE,
); );
@@ -122,8 +121,8 @@ export default function Page() {
(async () => { (async () => {
const newInfo = const newInfo =
season === 0 season === 0
? await getRatingsInfo(region) ? await getRatingInfo(region)
: await getArchivedRatingsInfo(region, season); : await getArchivedRatingInfo(region, season);
useInfoCache.setState( useInfoCache.setState(
produce((draft: InfoCache) => { produce((draft: InfoCache) => {
draft[region][season] = newInfo; draft[region][season] = newInfo;
@@ -140,7 +139,7 @@ export default function Page() {
const results = ( const results = (
await Promise.all( await Promise.all(
range(0, 5).map(async (league: number) => { range(0, 5).map(async (league: number) => {
const data = await getRatingsLeague(region, league); const data = await getRatingLeague(region, league);
leaguePositionCache.current[region][league] = data[0].number - 1; leaguePositionCache.current[region][league] = data[0].number - 1;
return data; return data;
@@ -178,12 +177,12 @@ export default function Page() {
); );
})(); })();
} else { } else {
getArchivedRatingsLeaderboard(region, season).then((results) => { getArchivedRatingLeaderboard(region, season).then((results) => {
useLeaderboardCache.setState( useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => { produce((draft: LeaderboardCache) => {
if (!(season in draft[region])) draft[region][season] = {}; if (!(season in draft[region])) draft[region][season] = {};
results.forEach((result, index) => { results.entries.forEach((result, index) => {
draft[region][season][index] = result.id; draft[region][season][index] = result.id;
}); });
}), }),
@@ -192,7 +191,7 @@ export default function Page() {
produce((draft: ScoreCache) => { produce((draft: ScoreCache) => {
if (!(season in draft[region])) draft[region][season] = {}; if (!(season in draft[region])) draft[region][season] = {};
results.forEach((result) => { results.entries.forEach((result) => {
draft[region][season][result.id] = result.score; draft[region][season][result.id] = result.score;
}); });
}), }),
@@ -213,7 +212,7 @@ export default function Page() {
}, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]); }, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]);
// neighboring page caching for all seasons // neighboring page caching for all seasons
useEffect(() => { useEffect(() => {
if (!ratingsInfo || ratingsInfo?.detail) return; if (!ratingInfo || ratingInfo?.detail) return;
if (season === 0) { if (season === 0) {
const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2; const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2;
@@ -236,7 +235,7 @@ export default function Page() {
const ids = range( const ids = range(
Math.max(0, (page - 1) * ROWS_PER_PAGE), Math.max(0, (page - 1) * ROWS_PER_PAGE),
Math.min( Math.min(
ratingsInfo.count - 1, ratingInfo.count - 1,
(page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE, (page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE,
), ),
) )
@@ -265,7 +264,7 @@ export default function Page() {
page, page,
region, region,
season, season,
ratingsInfo?.detail, ratingInfo?.detail,
leaderboard[region][season] !== undefined && leaderboard[region][season] !== undefined &&
0 in leaderboard[region][season], 0 in leaderboard[region][season],
]); ]);
@@ -279,7 +278,7 @@ export default function Page() {
async function cacheNeighbors( async function cacheNeighbors(
id: number, id: number,
size: number, size: number,
): Promise<RatingsPlayer[] | null> { ): Promise<RatingPlayer[] | null> {
const radius = Math.round(size / 2); const radius = Math.round(size / 2);
const targetPosition = Object.entries(leaderboard[region][season]).find( const targetPosition = Object.entries(leaderboard[region][season]).find(
([, player]) => id === player, ([, player]) => id === player,
@@ -303,7 +302,7 @@ export default function Page() {
} }
} }
const { neighbors } = await getRatingsNeighbors(region, id, size, true); const { neighbors } = await getRatingNeighbors(region, id, size, true);
useLeaderboardCache.setState( useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => { produce((draft: LeaderboardCache) => {
@@ -341,11 +340,11 @@ export default function Page() {
return neighbors; return neighbors;
} }
async function jumpToPosition(position: number) { async function jumpToPosition(position: number) {
if (!ratingsInfo || ratingsInfo?.detail) return; if (!ratingInfo || ratingInfo?.detail) return;
const targetPosition = Math.min( const targetPosition = Math.min(
ratingsInfo.count - 1, ratingInfo.count - 1,
Math.max(0, Math.min(position, ratingsInfo.count - 1)), Math.max(0, Math.min(position, ratingInfo.count - 1)),
); );
if (season === 0) { if (season === 0) {
@@ -378,7 +377,7 @@ export default function Page() {
const loadingDirection = Math.sign(targetPosition - closestPosition); const loadingDirection = Math.sign(targetPosition - closestPosition);
let seedingPlayer = closestPlayerId as number; let seedingPlayer = closestPlayerId as number;
let targetPositionAcquired = false; let targetPositionAcquired = false;
let neighbors: RatingsPlayer[] | null; let neighbors: RatingPlayer[] | null;
setLoadingProgress([0, closestPositionDistance]); setLoadingProgress([0, closestPositionDistance]);
@@ -423,7 +422,7 @@ export default function Page() {
} }
} }
async function jumpToScore() { async function jumpToScore() {
if (!ratingsInfo || ratingsInfo?.detail) return; if (!ratingInfo || ratingInfo?.detail) return;
const targetScore = scoreInput.current!.valueAsNumber; const targetScore = scoreInput.current!.valueAsNumber;
@@ -522,7 +521,7 @@ export default function Page() {
} else { } else {
let playerIndex = -1; let playerIndex = -1;
for (let index = ratingsInfo.count; index >= 0; index--) { for (let index = ratingInfo.count; index >= 0; index--) {
if (!(index in leaderboard[region][season])) continue; if (!(index in leaderboard[region][season])) continue;
if (scores[leaderboard[region][season][index]] >= targetScore) { if (scores[leaderboard[region][season][index]] >= targetScore) {
@@ -545,7 +544,7 @@ export default function Page() {
} }
const totalPlayers = const totalPlayers =
!ratingsInfo?.detail && ratingsInfo?.count ? ratingsInfo.count : undefined; !ratingInfo?.detail && ratingInfo?.count ? ratingInfo.count : undefined;
return ( return (
<PageWrapper color="orange"> <PageWrapper color="orange">
@@ -573,7 +572,7 @@ export default function Page() {
<Select.Trigger style={{ flex: 1 }} /> <Select.Trigger style={{ flex: 1 }} />
<Select.Content> <Select.Content>
{ratingsInfo?.detail === undefined && ( {ratingInfo?.detail === undefined && (
<Select.Item value="0">Current season</Select.Item> <Select.Item value="0">Current season</Select.Item>
)} )}
@@ -582,7 +581,7 @@ export default function Page() {
{latestArchivedSeasonNumber && {latestArchivedSeasonNumber &&
range( range(
FIRST_ARCHIVED_RATINGS_SEASON, FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + 1, latestArchivedSeasonNumber + 1,
) )
.map((season) => ( .map((season) => (
@@ -605,7 +604,7 @@ export default function Page() {
<Dialog.Content> <Dialog.Content>
<Flex gap="4" justify="center"> <Flex gap="4" justify="center">
<TextField.Input <TextField.Root
ref={scoreInput} ref={scoreInput}
type="number" type="number"
placeholder="Type a score..." placeholder="Type a score..."
@@ -638,7 +637,7 @@ export default function Page() {
<Dialog.Content> <Dialog.Content>
<Flex gap="4" justify="center"> <Flex gap="4" justify="center">
<TextField.Input <TextField.Root
ref={positionInput} ref={positionInput}
type="number" type="number"
placeholder="Type a position..." placeholder="Type a position..."
@@ -681,8 +680,8 @@ export default function Page() {
<Select.Trigger /> <Select.Trigger />
<Select.Content> <Select.Content>
{ratingsInfo?.detail === undefined && {ratingInfo?.detail === undefined &&
ratingsInfo?.leagues.map(({ index, title }) => ( ratingInfo?.leagues.map(({ index, title }) => (
<Select.Item key={index} value={`${index}`}> <Select.Item key={index} value={`${index}`}>
{title} {title}
</Select.Item> </Select.Item>
@@ -714,11 +713,7 @@ export default function Page() {
); );
} }
} else { } else {
if ( if (!ratingInfo || ratingInfo.detail || !leaderboard)
!ratingsInfo ||
ratingsInfo.detail ||
!leaderboard
)
return; return;
const minScore = const minScore =
@@ -727,7 +722,7 @@ export default function Page() {
for ( for (
let index = 0; let index = 0;
index < ratingsInfo.count; index < ratingInfo.count;
index++ index++
) { ) {
if ( if (
@@ -763,105 +758,102 @@ export default function Page() {
<Dialog.Content> <Dialog.Content>
<Flex gap="4" direction="column"> <Flex gap="4" direction="column">
<TextField.Root> <TextField.Root
onChange={debounce(
async (event: ChangeEvent<HTMLInputElement>) => {
const trimmedSearch = event.target.value.trim();
const encodedSearch = encodeURIComponent(trimmedSearch);
if (trimmedSearch) {
if (season === 0) {
setSearchLoading(true);
const accountList = await searchCurrentRatingPlayers(
region,
trimmedSearch,
);
const values = Object.values(accountList).filter(
(searchedPlayer) => !searchedPlayer.skip,
);
const valuesWithTypes =
values as ((typeof values)[number] & {
skip: false;
})[];
useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => {
if (draft) {
values.forEach((player) => {
const playerWithTypes =
player as typeof player & { skip: false };
draft[region][season][
playerWithTypes.number - 1
] = playerWithTypes.spa_id;
});
}
}),
);
useNameCache.setState(
produce((draft: typeof names) => {
valuesWithTypes.forEach((searchedPlayer) => {
if (searchedPlayer.nickname) {
draft[region][searchedPlayer.spa_id] = {
nickname: searchedPlayer.nickname,
clan: searchedPlayer.clan_tag,
};
}
});
}),
);
setSearchResults(
valuesWithTypes.map((searchedPlayer) => ({
account_id: searchedPlayer.spa_id,
nickname: searchedPlayer.nickname,
number: searchedPlayer.number,
})),
);
setSearchLoading(false);
} else {
setSearchLoading(true);
const accountList = await fetchBlitz<AccountList>(
region,
'account/list',
{ search: encodedSearch },
);
const playerKeys = Object.keys(
leaderboard[region][season],
);
setSearchResults(
accountList
.map((searchedPlayer) => ({
...searchedPlayer,
number: parseInt(
playerKeys.find(
(indexAsString) =>
leaderboard[region][season][
parseInt(indexAsString)
] === searchedPlayer.account_id,
)!,
),
}))
.filter((player) => !Number.isNaN(player.number)),
);
setSearchLoading(false);
}
} else {
setSearchResults(undefined);
}
},
500,
)}
placeholder="Search for player..."
>
<TextField.Slot> <TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" /> <MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot> </TextField.Slot>
<TextField.Input
onChange={debounce(
async (event: ChangeEvent<HTMLInputElement>) => {
const trimmedSearch = event.target.value.trim();
const encodedSearch = encodeURIComponent(trimmedSearch);
if (trimmedSearch) {
if (season === 0) {
setSearchLoading(true);
const accountList =
await searchCurrentRatingsPlayers(
region,
trimmedSearch,
);
const values = Object.values(accountList).filter(
(searchedPlayer) => !searchedPlayer.skip,
);
const valuesWithTypes =
values as ((typeof values)[number] & {
skip: false;
})[];
useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => {
if (draft) {
values.forEach((player) => {
const playerWithTypes =
player as typeof player & { skip: false };
draft[region][season][
playerWithTypes.number - 1
] = playerWithTypes.spa_id;
});
}
}),
);
useNameCache.setState(
produce((draft: typeof names) => {
valuesWithTypes.forEach((searchedPlayer) => {
if (searchedPlayer.nickname) {
draft[region][searchedPlayer.spa_id] = {
nickname: searchedPlayer.nickname,
clan: searchedPlayer.clan_tag,
};
}
});
}),
);
setSearchResults(
valuesWithTypes.map((searchedPlayer) => ({
account_id: searchedPlayer.spa_id,
nickname: searchedPlayer.nickname,
number: searchedPlayer.number,
})),
);
setSearchLoading(false);
} else {
setSearchLoading(true);
const accountList = await fetchBlitz<AccountList>(
`https://api.wotblitz.${region}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}`,
);
const playerKeys = Object.keys(
leaderboard[region][season],
);
setSearchResults(
accountList
.map((searchedPlayer) => ({
...searchedPlayer,
number: parseInt(
playerKeys.find(
(indexAsString) =>
leaderboard[region][season][
parseInt(indexAsString)
] === searchedPlayer.account_id,
)!,
),
}))
.filter(
(player) => !Number.isNaN(player.number),
),
);
setSearchLoading(false);
}
} else {
setSearchResults(undefined);
}
},
500,
)}
placeholder="Search for player..."
/>
</TextField.Root> </TextField.Root>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
@@ -885,12 +877,12 @@ export default function Page() {
id: player.account_id, id: player.account_id,
}); });
} else { } else {
if (!ratingsInfo || ratingsInfo.detail) return; if (!ratingInfo || ratingInfo.detail) return;
let playerIndex = -1; let playerIndex = -1;
for ( for (
let index = 0; let index = 0;
index < ratingsInfo.count; index < ratingInfo.count;
index++ index++
) { ) {
if ( if (
@@ -951,9 +943,7 @@ export default function Page() {
</Flex> </Flex>
<PageTurner <PageTurner
totalPlayers={ totalPlayers={(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0}
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
}
leaderboard={leaderboard[region][season] ?? {}} leaderboard={leaderboard[region][season] ?? {}}
rowsPerPage={ROWS_PER_PAGE} rowsPerPage={ROWS_PER_PAGE}
page={page} page={page}
@@ -982,7 +972,7 @@ export default function Page() {
page * ROWS_PER_PAGE, page * ROWS_PER_PAGE,
Math.min( Math.min(
page * ROWS_PER_PAGE + ROWS_PER_PAGE, page * ROWS_PER_PAGE + ROWS_PER_PAGE,
ratingsInfo?.detail ? 0 : (ratingsInfo?.count ?? 1) - 1, ratingInfo?.detail ? 0 : (ratingInfo?.count ?? 1) - 1,
), ),
).map((index) => { ).map((index) => {
const id = leaderboard[region][season]?.[index]; const id = leaderboard[region][season]?.[index];
@@ -994,8 +984,8 @@ export default function Page() {
: highlightedPlayer.position === index : highlightedPlayer.position === index
: false; : false;
const reward = const reward =
!ratingsInfo?.detail && ratingsInfo?.rewards !ratingInfo?.detail && ratingInfo?.rewards
? ratingsInfo.rewards.find( ? ratingInfo.rewards.find(
(reward) => (reward) =>
reward.from_position <= position && reward.from_position <= position &&
reward.to_position >= position, reward.to_position >= position,
@@ -1090,9 +1080,7 @@ export default function Page() {
</Table.Root> </Table.Root>
<PageTurner <PageTurner
totalPlayers={ totalPlayers={(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0}
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
}
rowsPerPage={ROWS_PER_PAGE} rowsPerPage={ROWS_PER_PAGE}
leaderboard={leaderboard[region][season] ?? {}} leaderboard={leaderboard[region][season] ?? {}}
page={page} page={page}
@@ -1,61 +0,0 @@
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
import mutateSession, { useSession } from '../../../../stores/session';
export type CustomColumnDisplay =
| 'battles'
| 'winrate'
| 'wn8'
| 'damage'
| 'none';
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
interface CustomColumn {
Builder: typeof DropdownMenu | typeof ContextMenu;
column: number;
}
export function CustomColumn({ column, Builder }: CustomColumn) {
const customColumn = useSession((state) => state.customColumns[column]);
return (
<Builder.Sub>
<Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger>
<Builder.SubContent>
<Builder.RadioGroup
value={customColumn.display}
onValueChange={(event) =>
mutateSession((draft) => {
draft.customColumns[column].display =
event as CustomColumnDisplay;
})
}
>
<Builder.RadioItem value="battles">Battles</Builder.RadioItem>
<Builder.RadioItem value="winrate">Winrate</Builder.RadioItem>
<Builder.RadioItem value="wn8">WN8</Builder.RadioItem>
<Builder.RadioItem value="damage">Damage</Builder.RadioItem>
<Builder.RadioItem value="none">None</Builder.RadioItem>
</Builder.RadioGroup>
{!NO_DELTA.includes(customColumn.display) && (
<>
<Builder.Separator />
<Builder.CheckboxItem
checked={customColumn.showDelta}
onCheckedChange={(checked) =>
mutateSession((draft) => {
draft.customColumns[column].showDelta = checked;
})
}
>
Show delta
</Builder.CheckboxItem>
</>
)}
</Builder.SubContent>
</Builder.Sub>
);
}
-136
View File
@@ -1,136 +0,0 @@
import {
Button,
ContextMenu,
Dialog,
DropdownMenu,
Flex,
Heading,
TextField,
} from '@radix-ui/themes';
import { createColors } from 'bepaint';
import { useRef, useState } from 'react';
import {
ACCENT_COLORS,
AccentColor,
GRAY_COLORS,
GrayColor,
PALETTES,
} from '../../../../constants/radixColors';
import { useSession } from '../../../../stores/session';
import { CustomColumn } from './CustomColumn';
interface ContextMenuProps {
Builder: typeof DropdownMenu | typeof ContextMenu;
reset: () => void;
}
export function Menu({ Builder, reset }: ContextMenuProps) {
const session = useSession();
const [showTitleEditor, setShowTitleEditor] = useState(false);
const titleInput = useRef<HTMLInputElement>(null);
return (
<>
<Dialog.Root open={showTitleEditor} onOpenChange={setShowTitleEditor}>
<Dialog.Content>
<Flex direction="column" gap="4">
<Heading size="4">Edit title</Heading>
<Flex gap="2" style={{ width: '100%' }}>
<TextField.Root style={{ flex: 1 }}>
<TextField.Input
ref={titleInput}
defaultValue={session.title}
placeholder="Enter title..."
onKeyDown={(event) => {
if (event.key === 'Enter' && titleInput.current) {
setShowTitleEditor(false);
useSession.setState({ title: titleInput.current!.value });
}
}}
/>
</TextField.Root>
<Dialog.Close>
<Button
onClick={() =>
useSession.setState({ title: titleInput.current!.value })
}
>
Set
</Button>
</Dialog.Close>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
<Builder.Content>
<Builder.Sub>
<Builder.SubTrigger>Accent color</Builder.SubTrigger>
<Builder.SubContent>
<Builder.RadioGroup
value={session.color}
onValueChange={(value) =>
useSession.setState({ color: value as AccentColor | GrayColor })
}
>
{[...ACCENT_COLORS, ...GRAY_COLORS].map((color) => (
<Builder.RadioItem value={color} key={color}>
<Flex gap="2" align="center">
<div
style={{
backgroundColor: createColors(PALETTES[color])
.solidBackground,
width: 16,
height: 16,
borderRadius: '100%',
}}
/>
{`${color.charAt(0).toUpperCase()}${color.slice(1)}`}
</Flex>
</Builder.RadioItem>
))}
</Builder.RadioGroup>
</Builder.SubContent>
</Builder.Sub>
<Builder.Separator />
<Builder.Item onClick={() => setShowTitleEditor(true)}>
Edit title...
</Builder.Item>
<CustomColumn Builder={Builder} column={0} />
<CustomColumn Builder={Builder} column={1} />
<CustomColumn Builder={Builder} column={2} />
<CustomColumn Builder={Builder} column={3} />
<Builder.Separator />
<Builder.CheckboxItem
checked={session.showTotal}
onCheckedChange={(checked) =>
useSession.setState({ showTotal: checked })
}
>
Show total
</Builder.CheckboxItem>
<Builder.CheckboxItem
checked={session.showCareer}
onCheckedChange={(checked) =>
useSession.setState({ showCareer: checked })
}
>
Show career
</Builder.CheckboxItem>
<Builder.Separator />
<Builder.Item color="red" onClick={reset}>
Reset
</Builder.Item>
</Builder.Content>
</>
);
}
-40
View File
@@ -1,40 +0,0 @@
import { style } from '@vanilla-extract/css';
import { theme } from '../../../stitches.config';
export const toolBar = style({
'@media': {
'screen and (min-width: 640px)': {
flexDirection: 'row',
},
},
flexDirection: 'column',
display: 'flex',
gap: 8,
});
export const toolbarButton = style({
'@media': {
'screen and (min-width: 640px)': {
flex: 0,
},
},
flex: 1,
});
export const searchButton = style({
border: 'none',
background: 'none',
fontSize: 16,
display: 'flex',
padding: 8,
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer',
':hover': {
backgroundColor: theme.colors.componentInteractiveHover,
borderRadius: 4,
},
});
+456 -210
View File
@@ -1,256 +1,502 @@
'use client'; 'use client';
import { import {
CaretDownIcon, ArrowDownIcon,
CopyIcon, MagnifyingGlassIcon,
PersonIcon, PlusIcon,
ReloadIcon,
} from '@radix-ui/react-icons'; } from '@radix-ui/react-icons';
import { import {
AlertDialog, AlertDialog,
Button, Button,
Checkbox, Card,
DropdownMenu, DropdownMenu,
Flex, Flex,
Heading,
IconButton,
Link,
Select,
Table,
Text, Text,
TextField, TextField,
} from '@radix-ui/themes'; } from '@radix-ui/themes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { use, useEffect, useMemo, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper'; import PageWrapper from '../../../components/PageWrapper';
import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions'; import { UNLOCALIZED_REGION_NAMES_SHORT } from '../../../constants/regions';
import {
generateStats,
prettifyStats,
Stat,
STAT_KEYS,
STAT_NAMES,
sumStats,
} from '../../../core/blitz/generateStats';
import {
getAccountInfo,
IndividualAccountInfo,
} from '../../../core/blitz/getAccountInfo';
import getTankStats from '../../../core/blitz/getTankStats';
import { idToRegion } from '../../../core/blitz/idToRegion';
import searchPlayersAcrossRegions, { import searchPlayersAcrossRegions, {
AccountListWithServer, AccountListWithServer,
} from '../../../core/blitz/searchPlayersAcrossRegions'; } from '../../../core/blitz/searchPlayersAcrossRegions';
import { resetSession } from '../../../core/blitzkrieg/resetSession'; import { tankDefinitions } from '../../../core/blitzkit/tankDefinitions';
import { setSession } from '../../../core/blitzkrieg/setSession'; import { tankIcon } from '../../../core/blitzkit/tankIcon';
import { useSession } from '../../../stores/session'; import { tankAverages } from '../../../core/blitzstars/tankAverages';
import SessionPage from '../../embeds/session/page'; import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import { Menu } from './components/Menu'; import { useWideFormat } from '../../../hooks/useWideFormat';
import * as styles from './page.css'; import { theme } from '../../../stitches.config';
import mutateSession, {
SessionTracking,
useSession,
} from '../../../stores/session';
import { IndividualTankStats } from '../../../types/tanksStats';
export default function Page() { export default function Page() {
const wideFormat = useWideFormat(640);
const awaitedTankDefinitions = use(tankDefinitions);
const awaitedTankAvearges = use(tankAverages);
const [showSearch, setShowSearch] = useState(false);
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState<AccountListWithServer>([]);
const [showCCInaccessibilityPrompt, setShowCCInaccessibilityPrompt] =
useState(false);
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState<
AccountListWithServer | undefined
>(undefined);
const [showSearchResults, setShowSearchResults] = useState(false);
const handleChange = debounce(
async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.value) {
setSearchResults(await searchPlayersAcrossRegions(event.target.value));
} else {
setSearchResults(undefined);
setShowSearchResults(false);
}
},
500,
);
const session = useSession(); const session = useSession();
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
null,
);
const accountInfoPromise = useMemo(
() =>
new Promise<IndividualAccountInfo | null>(async (resolve) => {
resolve(
session.tracking
? await getAccountInfo(session.player.region, session.player.id)
: null,
);
}),
[session.tracking && session.player.id],
);
const accountInfo = use(accountInfoPromise);
const delta = useMemo(
() =>
session.tracking && tankStatsB
? deltaTankStats(session.player.stats, tankStatsB)
.sort((a, b) => b.last_battle_time - a.last_battle_time)
.map((entry) => {
const tank = awaitedTankDefinitions[entry.tank_id];
const average = awaitedTankAvearges[tank.id];
const stats = generateStats(entry.all, average?.all);
const statsPretty = prettifyStats(stats);
return {
tank,
stats,
statsPretty,
};
})
: undefined,
[session.tracking && session.player, tankStatsB],
);
const total = useMemo(
() =>
delta
? prettifyStats(sumStats(delta.map(({ stats }) => stats)))
: undefined,
[delta],
);
const search = debounce(async () => {
if (!input.current) return;
setSearchResults(await searchPlayersAcrossRegions(input.current.value, 9));
setSearching(false);
}, 500);
useEffect(() => { useEffect(() => {
if (session.isTracking) input.current!.value = session.nickname; async function update() {
}, [session]); if (!session.tracking) return;
const tankStats = await getTankStats(
idToRegion(session.player.id),
session.player.id,
);
setTankStatsB(tankStats);
}
update();
const interval = setInterval(update, 5 * 1000);
return () => clearInterval(interval);
}, [session.tracking && session.player.id]);
return ( return (
<PageWrapper color="blue"> <PageWrapper>
<div className={styles.toolBar}> <AlertDialog.Root
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}> open={showCCInaccessibilityPrompt}
<TextField.Root> onOpenChange={setShowCCInaccessibilityPrompt}
>
<AlertDialog.Content>
<AlertDialog.Title>
You're attempting to track a CC account
</AlertDialog.Title>
<AlertDialog.Description>
CC (community contributor) accounts do not have public tank
statistics. We are in the process of adding embeds for CC accounts,
stay tuned.
</AlertDialog.Description>
<Flex mt="4" justify="end">
<AlertDialog.Action>
<Button variant="solid">Alright</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
{!session.tracking && (
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="3"
>
<Flex gap="2" align="center">
<Text color="gray">Look up a player to get started</Text>
<ArrowDownIcon />
</Flex>
<TextField.Root
size="3"
style={{
position: 'relative',
width: '75vw',
maxWidth: 480,
}}
ref={input}
placeholder="Search players..."
onChange={() => {
if (!input.current) return;
const sanitized = input.current.value.trim();
if (sanitized.length > 0) {
setShowSearch(true);
setSearching(true);
search();
} else {
setShowSearch(false);
}
}}
>
<TextField.Slot> <TextField.Slot>
<PersonIcon height="16" width="16" /> <MagnifyingGlassIcon />
</TextField.Slot> </TextField.Slot>
<TextField.Input {showSearch && (
defaultValue={session.isTracking ? session.nickname : undefined} <Card
ref={input} mt="2"
onChange={(event) => { style={{
event.stopPropagation(); position: 'absolute',
top: '100%',
left: 0,
width: '100%',
}}
>
{searching && (
<Flex justify="center">
<Text color="gray">Searching...</Text>
</Flex>
)}
if (event.target.value) { {!searching && searchResults.length === 0 && (
setShowSearchResults(true); <Flex justify="center">
setSearchResults(undefined); <Text color="gray">No players found</Text>
} else { </Flex>
setShowSearchResults(false); )}
}
handleChange(event); {!searching && searchResults.length > 0 && (
}} <Flex direction="column" gap="2">
onBlur={(event) => { {searchResults.map((player) => (
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 is fixed <Button
if (showSearchResults) event.target.focus(); key={player.account_id}
}} variant="ghost"
onKeyDown={(event) => { onClick={async () => {
if (event.key === 'Escape') { const stats = await getTankStats(
setShowSearchResults(false); player.region,
input.current?.blur(); player.account_id,
} );
}}
placeholder="Search for a player..." if (stats === null) {
/> setShowCCInaccessibilityPrompt(true);
return;
}
setShowSearch(false);
mutateSession((draft) => {
if (!input.current) return;
draft.tracking = true;
(draft as SessionTracking).player = {
id: player.account_id,
region: player.region,
since: Date.now(),
stats,
};
input.current.value = player.nickname;
});
}}
>
{player.nickname} (
{UNLOCALIZED_REGION_NAMES_SHORT[player.region]})
</Button>
))}
</Flex>
)}
</Card>
)}
</TextField.Root> </TextField.Root>
</Flex>
)}
<DropdownMenu.Root open={showSearchResults} modal={false}> {session.tracking && (
<DropdownMenu.Trigger> <Flex
<div /> mt="2"
</DropdownMenu.Trigger> mb="2"
direction={wideFormat ? 'row' : 'column'}
align="center"
justify="between"
gap="2"
>
<Flex mt="2" mb="2" direction="column" gap="2">
<Heading size="5">Tracking {accountInfo?.nickname}</Heading>
<Text color="gray">
Since {new Date(session.player.since).toLocaleString()}
</Text>
</Flex>
<DropdownMenu.Content> <Flex gap="2" align="center" justify="center">
{searchResults === undefined ? ( <Button
<DropdownMenu.Item disabled>Searching...</DropdownMenu.Item> color="red"
) : searchResults.length === 0 ? ( onClick={async () => {
<DropdownMenu.Item disabled>No results</DropdownMenu.Item> const stats = await getTankStats(
) : ( session.player.region,
searchResults?.map( session.player.id,
({ account_id: id, nickname, region }, index) => ( );
<DropdownMenu.Item
key={id}
onClick={() => {
setShowSearchResults(false);
setSession(region, id, nickname);
}}
shortcut={UNLOCALIZED_REGION_NAMES[region]}
>
{nickname}
</DropdownMenu.Item>
),
)
)}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div style={{ display: 'flex', gap: 8 }}> if (stats === null) {
<AlertDialog.Root> setShowCCInaccessibilityPrompt(true);
<AlertDialog.Trigger> return;
<Button }
variant="soft"
onClick={(event) => {
if (!session.resetPrompt) {
event.preventDefault();
resetSession();
}
}}
className={styles.toolbarButton}
color="red"
>
<ReloadIcon width="16" height="16" /> Reset
</Button>
</AlertDialog.Trigger>
<AlertDialog.Content> mutateSession((draft) => {
<Flex direction="column" gap="4"> (draft as SessionTracking).player.stats = stats;
<Flex direction="column" gap="0"> });
<AlertDialog.Title> }}
Are you sure you want to reset? >
</AlertDialog.Title> Reset
<AlertDialog.Description> </Button>
This'll discard your current session and start tracking from <Button
scratch again. onClick={() => {
</AlertDialog.Description> mutateSession((draft) => {
</Flex> draft.tracking = false;
});
}}
>
Change
</Button>
</Flex>
</Flex>
)}
<Text size="2"> {session.tracking && delta && delta.length > 0 && (
<Checkbox <Table.Root variant="surface">
mr="1" <Table.Header>
onCheckedChange={(checked) => <Table.Row>
useSession.setState({ <Table.ColumnHeaderCell>Tank</Table.ColumnHeaderCell>
resetPrompt: !checked, {session.columns.map((column, index) => (
}) <Table.ColumnHeaderCell
} width="0"
/> key={column}
Don't ask me again
</Text>
</Flex>
<Flex gap="3" mt="4">
<AlertDialog.Cancel>
<Button variant="soft" color="gray">
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={resetSession}>
Reset
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button
variant="soft"
className={styles.toolbarButton}
onClick={(event) => {
if (!session.embedPrompt) {
event.preventDefault();
resetSession();
}
navigator.clipboard.writeText(
`${location.origin}/embeds/session`,
);
}}
>
<CopyIcon width="16" height="16" /> Embed
</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Title>
Copied link to your clipboard
</AlertDialog.Title>
<AlertDialog.Description>
Instructions for streamers:
<iframe
style={{ style={{
width: '100%', position: 'relative',
aspectRatio: '16 / 9',
border: 'none',
}} }}
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N" >
title="YouTube video player" {index === 0 && session.columns.length < 4 && (
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" <DropdownMenu.Root>
></iframe> <DropdownMenu.Trigger>
</AlertDialog.Description> <IconButton
size="1"
variant="surface"
style={{
position: 'absolute',
left: 0,
top: '50%',
transform: 'translate(-100%, -50%)',
zIndex: 1,
}}
>
<PlusIcon />
</IconButton>
</DropdownMenu.Trigger>
<Text size="2"> <DropdownMenu.Content>
<Checkbox {STAT_KEYS.filter(
mr="1" (item) => !session.columns.includes(item),
onCheckedChange={(checked) => ).map((key) => (
useSession.setState({ <DropdownMenu.Item
embedPrompt: !checked, key={key}
}) onClick={() => {
} mutateSession((draft) => {
/> (draft as SessionTracking).columns.unshift(
Don't show this again key as Stat,
</Text> );
});
}}
>
{STAT_NAMES[key]}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
)}
<Flex gap="3" mt="4"> <Select.Root
<AlertDialog.Cancel> value={column}
<Button variant="soft">Got it!</Button> onValueChange={(value) => {
</AlertDialog.Cancel> mutateSession((draft) => {
</Flex> if (value === 'remove') {
</AlertDialog.Content> (draft as SessionTracking).columns.splice(index, 1);
</AlertDialog.Root> } else {
(draft as SessionTracking).columns[index] =
value as Stat;
}
});
}}
>
<Select.Trigger variant="ghost" />
<DropdownMenu.Root> <Select.Content>
<DropdownMenu.Trigger> {STAT_KEYS.filter(
<Button variant="soft" className={styles.toolbarButton}> (item) =>
Options !session.columns.includes(item) || item === column,
<CaretDownIcon /> ).map((key) => (
</Button> <Select.Item key={key} value={key}>
</DropdownMenu.Trigger> {STAT_NAMES[key]}
</Select.Item>
))}
<Menu Builder={DropdownMenu} reset={resetSession} /> <Select.Separator />
</DropdownMenu.Root>
</div>
</div>
<SessionPage naked={false} /> <Select.Item value="remove">Remove</Select.Item>
</Select.Content>
</Select.Root>
</Table.ColumnHeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row
style={{
overflow: 'hidden',
}}
>
<Table.RowHeaderCell
style={{
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
Total
</Table.RowHeaderCell>
{session.columns.map((column) => (
<Table.Cell key={column}>{total![column]}</Table.Cell>
))}
</Table.Row>
{delta.map(({ statsPretty, tank }) => {
return (
<Table.Row
key={tank.id}
style={{
overflow: 'hidden',
}}
>
<Table.RowHeaderCell
style={{
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
<Link href={`/tools/tankopedia/${tank.id}`}>
<img
draggable={false}
src={tankIcon(tank.id)}
style={{
position: 'absolute',
width: 128 + 32,
height: '200%',
top: '-50%',
left: 0,
objectFit: 'contain',
objectPosition: '50% 50%',
overflow: 'hidden',
}}
/>
<div
style={{
backgroundColor: 'red',
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: 128,
background:
'linear-gradient(90deg, #00000080, #00000000)',
}}
/>
<Text
style={{
color: theme.colors.textHighContrast,
position: 'relative',
textWrap: 'nowrap',
textShadow: 'black 0 0 4px',
}}
>
{tank.name}
</Text>
</Link>
</Table.RowHeaderCell>
{session.columns.map((column) => (
<Table.Cell key={column}>{statsPretty[column]}</Table.Cell>
))}
</Table.Row>
);
})}
</Table.Body>
</Table.Root>
)}
{session.tracking && delta && delta.length === 0 && (
<Flex
align="center"
justify="center"
direction="column"
style={{
flex: 1,
}}
>
<Heading color="gray">No battles</Heading>
<Text color="gray">Go ahead and play a game</Text>
</Flex>
)}
</PageWrapper> </PageWrapper>
); );
} }
+107
View File
@@ -0,0 +1,107 @@
import { CaretDownIcon, CaretRightIcon } from '@radix-ui/react-icons';
import { Button, Flex, Table, Text } from '@radix-ui/themes';
import { random } from 'lodash';
import { use, useMemo, useState } from 'react';
import { tankDefinitions } from '../../../../core/blitzkit/tankDefinitions';
import { tankIcon } from '../../../../core/blitzkit/tankIcon';
interface DayProps {
offset: number;
}
export function Day({ offset }: DayProps) {
const timeTo = new Date(Date.now() - offset * 24 * 60 * 60 * 1000);
const timeFrom = new Date(Date.now() - (offset + 1) * 24 * 60 * 60 * 1000);
const awaitedTankDefinitions = use(tankDefinitions);
const tanksList = useMemo(
() => Object.values(awaitedTankDefinitions),
[awaitedTankDefinitions],
);
const [expanded, setExpanded] = useState(offset === 0);
const tanks = tanksList
.filter((tank) => tank.tier === 10)
.sort(() => Math.random() - 0.5)
.slice(0, random(2, 5));
return (
<>
<Table.Row>
<Table.RowHeaderCell>
<Button
style={{
width: '100%',
}}
variant="ghost"
onClick={() => setExpanded((state) => !state)}
>
<Flex
align="center"
gap="1"
style={{
width: '100%',
}}
>
{expanded ? <CaretDownIcon /> : <CaretRightIcon />}
{timeTo.toLocaleString('default', {
month: 'short',
day: 'numeric',
})}
</Flex>
</Button>
</Table.RowHeaderCell>
<Table.Cell align="right">{random(1, 10)}</Table.Cell>
<Table.Cell align="right">
{random(60, 80, true).toFixed(0)}%
</Table.Cell>
</Table.Row>
{expanded &&
tanks.map((tank) => (
<Table.Row
key={tank.id}
style={{
overflow: 'hidden',
}}
>
<Table.RowHeaderCell
style={{
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
<img
draggable={false}
src={tankIcon(tank.id)}
style={{
position: 'absolute',
width: 128 + 32,
height: '200%',
top: '-50%',
left: 0,
boxShadow: 'inset 128px 0 64px -64px #00000040',
objectFit: 'contain',
objectPosition: '50% 50%',
overflow: 'hidden',
}}
/>
<Text
style={{
position: 'relative',
textWrap: 'nowrap',
textShadow: 'black 0 0 4px',
}}
>
{tank.name}
</Text>
</Table.RowHeaderCell>
<Table.Cell align="right">{random(1, 10)}</Table.Cell>
<Table.Cell align="right">
{random(60, 80, true).toFixed(0)}%
</Table.Cell>
</Table.Row>
))}
</>
);
}
+40
View File
@@ -0,0 +1,40 @@
'use client';
import { Flex, Heading, Table } from '@radix-ui/themes';
import { random, times } from 'lodash';
import { HeroStat } from '../../../components/AllStatsOverview/components/HeroStat';
import PageWrapper from '../../../components/PageWrapper';
import { Day } from './components/Day';
export default function Page() {
return (
<PageWrapper color="iris">
<Flex gap="2">
<Heading>TresAbhi</Heading>
<Heading color="gray">[OPTML]</Heading>
</Flex>
<HeroStat color="#8E4EC6" subtitle="Super Unicum" value={3142} />
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Day</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell width="0" align="right">
Battles
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell width="0" align="right">
Winrate
</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{times(random(5, 10), (index) => (
<Day offset={index} key={index} />
))}
</Table.Body>
</Table.Root>
</PageWrapper>
);
}
@@ -1,25 +1,15 @@
import { CaretRightIcon, Cross1Icon, ShuffleIcon } from '@radix-ui/react-icons'; import { Cross1Icon, ShuffleIcon } from '@radix-ui/react-icons';
import { import { Button, Card, Dialog, Flex, Heading, Tabs } from '@radix-ui/themes';
Button,
Card,
Dialog,
Flex,
Heading,
Tabs,
Text,
Tooltip,
} from '@radix-ui/themes';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { ModuleButton } from '../../../../../components/ModuleButtons/ModuleButton'; import { ModuleButton } from '../../../../../components/ModuleButtons/ModuleButton';
import { ShellButton } from '../../../../../components/ModuleButtons/ShellButton'; import { ShellButton } from '../../../../../components/ModuleButtons/ShellButton';
import { SmallTankIcon } from '../../../../../components/SmallTankIcon'; import { SmallTankIcon } from '../../../../../components/SmallTankIcon';
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath'; import { pushTankopediaPath } from '../../../../../core/blitzkit/pushTankopediaPath';
import { import {
SHELL_NAMES, SHELL_NAMES,
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
} from '../../../../../core/blitzkrieg/tankDefinitions/constants'; } from '../../../../../core/blitzkit/tankDefinitions/constants';
import { mutateDuel, useDuel } from '../../../../../stores/duel'; import { mutateDuel, useDuel } from '../../../../../stores/duel';
import { import {
mutateTankopediaTemporary, mutateTankopediaTemporary,
@@ -37,58 +27,70 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
const [tab, setTab] = useState('search'); const [tab, setTab] = useState('search');
const [antagonistSelectorOpen, setAntagonistSelectorVisible] = const [antagonistSelectorOpen, setAntagonistSelectorVisible] =
useState(false); useState(false);
const router = useRouter();
if (mode !== 'armor') return null; if (mode !== 'armor') return null;
return ( return (
<Card <Flex justify="center">
style={ <Card
floating style={
? { floating
position: 'absolute', ? {
bottom: 8, position: 'absolute',
left: '50%', bottom: 8,
transform: 'translateX(-50%)', left: '50%',
} transform: 'translateX(-50%)',
: undefined }
} : undefined
> }
<Flex align="center" justify="between" gap="6"> >
<Flex> <Flex
{antagonist.gun.shells.map((shell, index) => { align="center"
return ( justify="center"
<ShellButton wrap="wrap"
selected={antagonist.shell.id === shell.id} style={{
shell={shell.icon} gap: 'min(5vw, 32px)',
rowChild }}
first={index === 0} >
key={shell.id} <Flex>
last={index === antagonist.gun.shells.length - 1} {antagonist.gun.shells.map((shell, index) => {
onClick={() => { return (
mutateDuel((draft) => { <ShellButton
draft.antagonist!.shell = shell; selected={antagonist.shell.id === shell.id}
mutateTankopediaTemporary((draft) => { shell={shell.icon}
draft.shot = undefined; rowChild
first={index === 0}
key={shell.id}
last={index === antagonist.gun.shells.length - 1}
onClick={() => {
mutateDuel((draft) => {
draft.antagonist!.shell = shell;
mutateTankopediaTemporary((draft) => {
draft.shot = undefined;
});
}); });
}); }}
}} />
/> );
); })}
})} </Flex>
</Flex>
<Flex align="center" gap="4"> <Button
{!floating && <Text>Versus</Text>} variant="ghost"
onClick={() => {
mutateDuel((draft) => {
[draft.protagonist, draft.antagonist] = [
draft.antagonist,
draft.protagonist,
];
pushTankopediaPath(draft.antagonist!.tank.id);
});
}}
>
<ShuffleIcon /> Swap
</Button>
{floating && ( <Flex align="center" gap="4">
<Flex gap="2" align="center">
Versus {antagonist.tank.name}
<SmallTankIcon id={antagonist.tank.id} size={16} />
</Flex>
)}
{!floating && (
<Dialog.Root <Dialog.Root
open={antagonistSelectorOpen} open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorVisible} onOpenChange={setAntagonistSelectorVisible}
@@ -96,9 +98,8 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
<Dialog.Trigger> <Dialog.Trigger>
<Button variant="ghost"> <Button variant="ghost">
<Flex gap="2" align="center"> <Flex gap="2" align="center">
{antagonist.tank.name} Versus {antagonist.tank.name}
<SmallTankIcon id={antagonist.tank.id} size={16} /> <SmallTankIcon id={antagonist.tank.id} size={16} />
<CaretRightIcon />
</Flex> </Flex>
</Button> </Button>
</Dialog.Trigger> </Dialog.Trigger>
@@ -160,61 +161,52 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
<Flex gap="2" wrap="wrap"> <Flex gap="2" wrap="wrap">
<Flex> <Flex>
{antagonist.tank.turrets.map((turret, index) => ( {antagonist.tank.turrets.map((turret, index) => (
<Tooltip content={turret.name} key={turret.id}> <ModuleButton
<ModuleButton rowChild
rowChild first={index === 0}
first={index === 0} last={
last={ index === antagonist.tank.turrets.length - 1
index === }
antagonist.tank.turrets.length - 1 key={turret.id}
} onClick={() => {
key={turret.id} mutateDuel((draft) => {
onClick={() => { draft.antagonist!.turret = turret;
mutateDuel((draft) => { draft.antagonist!.gun =
draft.antagonist!.turret = turret; turret.guns.at(-1)!;
draft.antagonist!.gun = draft.antagonist!.shell =
turret.guns.at(-1)!; draft.antagonist!.gun.shells[0];
draft.antagonist!.shell = });
draft.antagonist!.gun.shells[0]; mutateTankopediaTemporary((draft) => {
}); draft.shot = undefined;
mutateTankopediaTemporary((draft) => { });
draft.shot = undefined; }}
}); selected={antagonist.turret.id === turret.id}
}} discriminator={
selected={ TIER_ROMAN_NUMERALS[turret.tier]
antagonist.turret.id === turret.id }
} module="turret"
discriminator={ />
TIER_ROMAN_NUMERALS[turret.tier]
}
module="turret"
/>
</Tooltip>
))} ))}
</Flex> </Flex>
<Flex> <Flex>
{antagonist.turret.guns.map((gun, index) => ( {antagonist.turret.guns.map((gun, index) => (
<Tooltip content={gun.name} key={gun.id}> <ModuleButton
<ModuleButton key={gun.id}
rowChild rowChild
first={index === 0} first={index === 0}
last={ last={
index === index === antagonist.turret.guns.length - 1
antagonist.turret.guns.length - 1 }
} onClick={() => {
onClick={() => { mutateDuel((draft) => {
mutateDuel((draft) => { draft.antagonist!.gun = gun;
draft.antagonist!.gun = gun; draft.antagonist!.shell = gun.shells[0];
draft.antagonist!.shell = gun.shells[0]; });
}); }}
}} selected={antagonist.gun.id === gun.id}
selected={antagonist.gun.id === gun.id} discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
discriminator={ module="gun"
TIER_ROMAN_NUMERALS[gun.tier] />
}
module="gun"
/>
</Tooltip>
))} ))}
</Flex> </Flex>
</Flex> </Flex>
@@ -251,24 +243,9 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
</Tabs.Root> </Tabs.Root>
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>
)} </Flex>
</Flex> </Flex>
</Card>
<Button </Flex>
variant="ghost"
onClick={() => {
mutateDuel((draft) => {
[draft.protagonist, draft.antagonist] = [
draft.antagonist,
draft.protagonist,
];
pushTankopediaPath(draft.antagonist!.tank.id);
});
}}
>
<ShuffleIcon /> Swap
</Button>
</Flex>
</Card>
); );
} }
Loaded 100 of 374 files, more files were not shown because too many files have changed in this diff. Show more