Compare commits

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

[ci skip]

* New translations en.json (Ukrainian)

[ci skip]

* New translations en.json (Ukrainian)

[ci skip]
2026-07-04 22:05:48 -05:00
tresabhi c85cd36818 logs 2026-06-28 11:11:37 -05:00
tresabhi 5d2c63fed9 clean up some renderer stuff 2026-06-26 14:05:26 -05:00
tresabhi cead140855 optimize intro naimaiton 2026-06-26 14:01:39 -05:00
tresabhi 63a0143d4f optimize scene lighting and rnederer 2026-06-26 13:58:30 -05:00
tresabhi 19f22993f1 optimize shot display animator 2026-06-26 13:31:40 -05:00
tresabhi b2eb74914f optimize lighting loop 2026-06-26 13:22:24 -05:00
TrèsAbhi 05572918b1 exit 2026-06-23 10:35:27 -05:00
TrèsAbhi 1cc8b26b16 dispose 2026-06-23 10:30:56 -05:00
TrèsAbhi a8e8d20cf1 break 2026-06-23 10:26:05 -05:00
TrèsAbhi 55deb30607 logs 2026-06-23 10:20:22 -05:00
TrèsAbhi 03b8b89a17 log 2026-06-23 10:14:37 -05:00
TrèsAbhi 3e421c7eb7 done 2026-06-23 10:11:18 -05:00
TrèsAbhi fc27caabdc dvpl 2026-06-23 10:07:54 -05:00
tresabhi 3d35fae5ce dont log 2026-06-22 20:52:51 -05:00
tresabhi f6dfc4f176 what 2026-06-22 20:32:14 -05:00
tresabhi fdbbe2f356 Update applyPatches.ts 2026-06-22 20:18:25 -05:00
tresabhi cdf2aee43f debug 2026-06-22 20:13:52 -05:00
tresabhi 56933ddfac debug 2026-06-22 20:07:55 -05:00
tresabhi 1df2e818be dirs 2026-06-22 19:57:41 -05:00
tresabhi 1e4d00ea34 source 2026-06-22 19:55:39 -05:00
tresabhi 1bde50e0a3 garbage code 2026-06-22 19:54:09 -05:00
tresabhi 88290ec1ab garbage code 2026-06-22 19:54:01 -05:00
tresabhi e8325df832 lz4js 2026-06-22 19:44:03 -05:00
tresabhi 394f28e6ed remove proxyclient 2026-06-22 19:33:52 -05:00
tresabhi f5f170bcca switch to clean dava 2026-06-22 19:32:26 -05:00
tresabhi 2ffe3fc006 set up patches 2026-06-22 19:28:02 -05:00
TrèsAbhi 406d70b5b7 New translations en.json (Ukrainian) (#250)
[ci skip]
2026-06-22 09:38:55 -05:00
828 changed files with 8289 additions and 36914 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
indentSize: 2
+45
View File
@@ -0,0 +1,45 @@
name: Discover IDs
on:
push:
branches: production
schedule:
- cron: 0 */6 * * *
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
discover:
name: Discover IDs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: production
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Setup Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Discover IDs
run: bun run discover-ids
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.IDS_TOKEN }}
+120 -46
View File
@@ -2,46 +2,98 @@ name: Website
on:
push:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
inputs:
branch:
description: Branch
type: choice
required: true
default: production
options:
- production
- main
environment:
description: Environment
type: choice
required: true
default: production
options:
- development
- beta
- production
- preview
- opentest
client_provider:
description: Client provider
type: choice
required: true
default: steam
options:
- steam
- zip
- wgpkg
client_source:
description: Client source
type: string
required: true
default: 444200/windows
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ inputs.environment || github.ref }}
cancel-in-progress: true
env:
# blitz
WOTB_SERVER: ${{ secrets.WOTB_SERVER }}
WOTB_VERSION: ${{ secrets.WOTB_VERSION }}
PUBLIC_WOTB_CLIENT: ../../temp/game
WOTB_USMAP: ../closed/blitz.usmap
# google
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
# steam
# client
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
STEAM_APP: 3341250
STEAM_OS: windows
STEAM_FINGERPRINT: Manifest_UFSFiles_Win64.txt
CLIENT_PROVIDER: ${{ inputs.client_provider || 'steam' }}
CLIENT_SOURCE: ${{ inputs.client_source || '444200/windows' }}
CLIENT_DIR: ../../temp/game
# debug
DOTNET_EnableDiagnostics: 1
# blitzkit
PUBLIC_ENVIRONMENT: ${{ inputs.environment || 'production' }}
PUBLIC_PROMOTE_OPENTEST: false
PUBLIC_STATIC_ASSET_BASE: https://assets.blitzkit.app
PUBLIC_GUIDE_WEBSITE: https://guidesblitz.com
# blitz
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
# google
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
# wargaming
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
jobs:
build:
name: Build chunk ${{ matrix.chunk }}
name: Build website
runs-on: ubuntu-latest
strategy:
fail-fast: false
fail-fast: true
matrix:
chunk: [pages, api, media, models, textures]
chunk: [default, api]
steps:
- name: Free disk space
@@ -50,46 +102,68 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch || 'production' }}
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Set up Bun
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: latest
- name: Set up Dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.0.x
- name: Set up Protoc
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- name: Set up project
- name: Setup CLIs
run: |
bun run setup
./scripts/setup_depot_downloader.sh
sudo apt-get update
sudo apt-get install -y aria2 libarchive-tools
- name: Download Steam game
run: scripts/download_game.sh
- name: Install dependencies
run: bun install
- name: Enable core dumps
run: |
ulimit -c unlimited
- name: Download Client
run: bun run download-client
- name: Build
run: bun run build-website
run: bun run --bun build
env:
BUILD_CHUNK: ${{ matrix.chunk }}
CHUNK: ${{ matrix.chunk }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
id: upload
uses: actions/upload-artifact@v4
with:
name: chunk-${{ matrix.chunk }}
path: ${{ matrix.chunk == 'pages' && 'dist/website' || format('dist/website/{0}', matrix.chunk) }}
path: dist/website
- name: Trigger deployment
if: ${{ github.ref == 'refs/heads/production' }}
env:
TOKEN: ${{ secrets.GH_NODE_DEPLOY_TOKEN }}
CHUNK: ${{ matrix.chunk }}
run: |
ENV_FILE="environments/${PUBLIC_ENVIRONMENT}.json"
TARGET_REPO=$(jq -r ".nodes.\"$CHUNK\"" "$ENV_FILE")
if [ "$TARGET_REPO" == "null" ] || [ -z "$TARGET_REPO" ]; then
exit 1
fi
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/$TARGET_REPO/dispatches" \
-d "{
\"event_type\": \"deploy-pages\",
\"client_payload\": {
\"source_repo\": \"${{ github.repository }}\",
\"run_id\": \"${{ github.run_id }}\",
\"artifact_id\": \"${{ steps.upload.outputs.artifact-id }}\",
\"chunk\": \"$CHUNK\"
}
}"
+2 -4
View File
@@ -1,7 +1,7 @@
node_modules
_node_modules
package-lock.json
*.env
.env
/test-results/
/playwright-report/
/playwright/.cache/
@@ -22,12 +22,11 @@ depots
public/sitemap*
/firebase.json
target
packages/website-ue/src/protos/*.ts
# packages/core/src/protos/*.ts
/.vercel
.astro
.vercel
/src
/packages/bot/src/prisma
dev-dist
# Logs
logs
@@ -53,4 +52,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.lscache
-3
View File
@@ -1,6 +1,3 @@
[submodule "packages/closed"]
path = packages/closed
url = https://github.com/blitzkit/closed.git
[submodule "packages/cue4parse"]
path = packages/cue4parse
url = https://github.com/tresabhi/CUE4Parse.git
-3
View File
@@ -1,3 +0,0 @@
{
"tabWidth": 2
}
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
+258
View File
@@ -0,0 +1,258 @@
{
"cSpell.words": [
"AABBOX",
"Abhi",
"absarc",
"accountinfo",
"adsense",
"aeson",
"alphatest",
"analyticsdata",
"analyticsreporting",
"APAC",
"apcr",
"appinfo",
"applypatches",
"artefacts",
"astrojs",
"astrolib",
"atgm",
"autocrop",
"Autoloader",
"Autoreloader",
"AYUV",
"battleresults",
"bbox",
"beforeinstallprompt",
"bepaint",
"betakrieg",
"BINORMAL",
"Binormals",
"bkni",
"BKRL",
"BKRTS",
"BKRTSMANIFEST",
"bkwd",
"blitzkit",
"blitzrinth",
"blitzstars",
"bufbuild",
"cacheable",
"Callouts",
"Camo",
"Carro",
"CCPAOPTOUT",
"CDON",
"clanid",
"classname",
"cleandist",
"closedkrieg",
"cockroackdb",
"Coef",
"Combattimento",
"cooldown",
"corelations",
"corsproxy",
"CUBEMAP",
"CUBETEXCOORD",
"customperiod",
"datasource",
"Dava",
"dbid",
"Ddpf",
"Ddsd",
"deadrail",
"debugflags",
"deburr",
"decalmask",
"decompressor",
"décor",
"dedup",
"defence",
"degressive",
"depotdownloader",
"devkrieg",
"diacriticless",
"DIDS",
"donotremove",
"drei",
"Droodles",
"dvpd",
"dvpl",
"DVPLCRC",
"dvpm",
"dxgi",
"easimon",
"echarts",
"Everytime",
"FASTNAME",
"favicons",
"flexdirection",
"fontsource",
"FOURCC",
"frameloop",
"fsformat",
"Fullscreen",
"fullstats",
"fuzzysort",
"gamemode",
"gameplay",
"glsl",
"GLTF",
"GSOR",
"gtag",
"haha",
"Herdbreaker",
"Hetz",
"highp",
"iife",
"imgur",
"immer",
"Inclusivity",
"JOINTINDEX",
"JOINTWEIGHT",
"kranvagn",
"krieg",
"Leaderboard",
"leaderboards",
"lerp",
"LINEARSIZE",
"linktarget",
"lmao",
"loadouts",
"loddist",
"lods",
"lossrate",
"Maddoxkkm",
"malum",
"mantlets",
"mediump",
"metallicness",
"metalness",
"minitelrose",
"Minotauro",
"MIPMAPCOUNT",
"mišek",
"mmnow",
"modeltransform",
"MURICA",
"nanostores",
"NEGATIVEX",
"NEGATIVEY",
"NEGATIVEZ",
"networkidle",
"NITROPAY",
"nivo",
"nmatname",
"noout",
"normalmap",
"oneof",
"oopsies",
"Oopsy",
"opentest",
"OPTML",
"otemp",
"outdir",
"outfile",
"ownedtanks",
"padder",
"partytown",
"patchnotes",
"PIXELFORMAT",
"playerachievements",
"playerinfo",
"playform",
"pngjs",
"POSITIVEX",
"POSITIVEY",
"POSITIVEZ",
"precompute",
"Promisable",
"proto",
"protobuf",
"protoc",
"protos",
"proxyclient",
"prunestrings",
"Pyogenics",
"ratingleaderboard",
"raycaster",
"Redirector",
"Relogin",
"researchable",
"Resvg",
"rizz",
"roboto",
"rtsc",
"RTSM",
"SCPG",
"scroller",
"searchclans",
"searchplayers",
"searchtanks",
"SHAREDEXP",
"SKLLD",
"sluggify",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
"startcase",
"statkit",
"statsfull",
"steamapps",
"Stringifying",
"struct",
"structs",
"strvk",
"subnation",
"Synx",
"tankaverages",
"Tankerch",
"tankmen",
"tankopedia",
"tankopedias",
"tankstats",
"tauri",
"texconv",
"TEXCOORD",
"tresabhi",
"tungstentip",
"Turretless",
"TYPELESS",
"ubjson",
"Unicum",
"UNLOCALIZED",
"UNORM",
"unpickled",
"Unregistering",
"unrotate",
"unwrapper",
"userns",
"vercel",
"vickers",
"vitepress",
"Vovko",
"wargaming",
"webp",
"wgpkg",
"whitespaces",
"WHSS",
"winrate",
"wireframe",
"Woah",
"WOTB",
"wotblitz",
"wotbreplay",
"WOTINSPECTOR",
"xffn",
"xkdkw",
"XMLDVPL",
"YAMLDVPL",
"yolo",
"youtuber",
"youtubers",
"zoomable"
],
"typescript.tsdk": "node_modules\\typescript\\lib"
}
+31 -95
View File
@@ -1,105 +1,41 @@
# BlitzKit
Encyclopedia for World of Tanks Blitz.
Tools for everything and anything, World of Tanks Blitz.
## Development
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
Setting up a local developer environment for BlitzKit is more complex than most projects, but this section will guide you through all the steps. If you're ever lost or confused, feel free to ask for help in [the official Discord server](https://discord.gg/nDt7AjGJQH).
If you intend to contribute to the code, note that you may need to check out sensitive, closed-source submodules to locally develop certain aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or discuss sensitive content, such as environment variables or submodules.
### Prerequisites
## Packages
Before you touch any code, you'll need the following.
| Package | Title | Source | Memo |
| ------------------------------------ | -------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. |
#### Linux
## Submodules
Because Node API for .NET doesn't resolve runtime-specific object files for libraries, the entirety of the BlitzKit project is "vendor-locked" to a single OS. GitHub Actions, responsible for BlitzKit CD/CI works best with Ubuntu, thus we must also use a Linux OS.
| Submodule | Title | Source | Memo |
| ------------------------------------------------------ | --------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [submodules/blitzkit-closed](packages/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. |
#### Windows (Optional)
## Environment Variables
...
#### Bun
https://bun.com/
BlitzKit is written mostly in TypeScript, with packages fetched from NPM, managed by Bun.
#### .NET
https://dotnet.microsoft.com/download/dotnet/9.0/
You'll need the .NET 9.0 SDK to build and run the interface layer between BlitzKit and your local installation of World of Tanks Blitz.
#### Protocol Buffers
https://protobuf.dev/installation/
Both BlitzKit and World of Tanks Blitz encode their APIs with Protocol Buffers.
#### World of Tanks Blitz
https://store.steampowered.com/app/444200/
You'll need an up-to-date installation of World of Tanks Blitz on your computer. You may use the same installation that you play, even on a Windows partition if you dual boot.
#### UE4SS
...
#### protodump
...
#### FModel (Optional)
...
### Set Up
Now we can start installing and initiating the code of BlitzKit.
#### Cloning
BlitzKit is a massive monorepo with a lot of submodules. You'll likely not need all the branches and commit history. So, clone with depth 1.
```bash
git clone --depth 1 https://github.com/blitzkit/blitzkit.git
cd blitzkit
git submodule update --init --recursive --depth 1
```
#### Proto Files
...
#### Mappings
...
#### Environment Variables
...
#### The Big One
...
### Modifying The Code
This is where you can start changing things and proposing them through pull requests.
#### Server
...
#### Building
...
#### Branches & Pull Requests
...
#### Updating
...
| Name | Source | BlitzKit Default | Memo |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
| `PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
| `SERVICING` | | `false` | Wether the website is down for servicing. |
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
| `WOTB_DLC_CDN` | | | |
+10
View File
@@ -0,0 +1,10 @@
{
"folders": [
{
"path": ".",
},
{
"path": "C:/Program Files/WindowsApps/7458BE2C.WorldofTanksBlitz_11.1.256.0_x64__x4tje2y229k00/Data",
},
],
}
+537 -1313
View File
File diff suppressed because it is too large. Load diff
+2 -23
View File
@@ -1,27 +1,6 @@
{
"words": [
"gltf",
"amogos",
"Tankopedia",
"uasset",
"oodle",
"blitzkit",
"webp",
"hocs",
"cooldown",
"Lods",
"Skia",
"RMAO",
"Verts",
"drei",
"frameloop",
"pointermove",
"pointerup",
"pointerdown"
],
"userWords": [
"WOTB",
"USMAP",
"favicons"
"gamemodes",
"Tankopedia"
]
}
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.30.4
## Fixes
- BlitzKit now correctly implements minor WoTB updates
+8
View File
@@ -0,0 +1,8 @@
# BlitzKit 1.30.5
## Features
- Reduced the amount of code that executes for every rendered frame, hopefully improving performance
- Reduced the number of lights by 20% to decrease the amount of shadow calculations
- Decreased the ground size by 80% for an equal reduction in shadow texture sizes
- Added the ability for BlitzKit to dismount the components that handled the intro lighting animation when not animating, reducing checks running every frame
+7
View File
@@ -0,0 +1,7 @@
# BlitzKit 1.30.6
## Fixes
- Tanks no longer "randomly" appear dark in Tankopedia and Mixer
- The ground is now as dark as other modes when Static Armor is selected in Tankopedia
- Fixed some naming issues with the "equal" icon, improving performance a tiny tiny bit
+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.30.7
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Dragging items in the Tier List now automatically scrolls the screen if you reach the top/bottom edge of the screen
## Fixes
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
- The spot light angle has been increased to more evenly light up larger tanks
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
- Increased the floor size and light decay to create the illusion that the floor is infinite
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
+9
View File
@@ -0,0 +1,9 @@
# BlitzKit 1.30.8
Support for tanks with Chinese names.
## Fixes
- The URL generator for BlitzKit now handles names that have only Chinese characters
- The Mixed Virtual File System now handles non-existent directories correctly
- Updated equipments to match the game again
+40
View File
@@ -0,0 +1,40 @@
# BlitzKit 1.31.0
This update massively upgrades BlitzKit's graphical fidelity (while making it faster) and updates the way BlitzKit deploys updates, speeding up future versions.
## Features
- Improved the performance of the Tank Sandbox in Tankopedia
- Reduced the number of lights by 25%
- Reduced the resolution of shadows by 50%
- Improved the graphical fidelity of tanks in the Tankopedia
- Added soft contact shadows below tanks
- Added sky and ground reflected lighting to the top and bottom of tanks
- Removed the fog that used to darken tanks when zoomed far away
- Made BlitzKitLand™ fade out circularly to hide the sharp edges
- Pointed the lights 1.5m above the ground
- This genuinely makes BlitzKit almost photo-realistic in my opinion
- Non-PBR tanks are now lit up brighter to accommodate for their dark textures
## Fixes
- Fixed a bug where the ambient occlusion map (a measure of how dark a part of a tank gets due to low light bounces) was applied to the color of the tank directly, making many tanks appear dark even in direct light
- Tanks with chinese characters now generate urls through transliteration
## Known Issues
- The flags found behind the selected tank in the tech tree section of the Tankopedia is offset incorrectly
## Technical Features
- BlitzKit can now calculate and store performance statistics and account details for up 10,000,000,000 players
- This will help make the BlitzKit Performance tool more accurate, and play well with an upcoming, unannounced major feature
## Technical Changes
- The `production` branch now deploys updates to `blitzkit.app` instead of the `main` branch
- `robots.txt` now disables indexing of BlitzKit variants based on the environment
- The website is now compiled in "chunks" of which the `api` chunk now takes on the task of mangling what was previously stored in `assets3`
- `assets3` will be removed in `1.31.1`, please migrate to the new `api` if you use any BlitzKit assets
- Removed the `bot` and `app` packages
- Removed Prisma
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.1
This update adds capabilities to release BlitzKit OpenTest and Preview.
-11
View File
@@ -2,17 +2,6 @@
🎉 Services for everything and anything World of Tanks Blitz
## Bot
BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite).
- Track statistics
- Find inactive players in a clan
- Lookup owned tanks
- Create embeds for streams
- Track a player/tanks statistical evolution
- ...and a variety of other similar things
## Website
Provides a bunch of very helpful tools; https://blitzkit.app/
-3
View File
@@ -1,3 +0,0 @@
# Inviting the Bot
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
-13
View File
@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-beta.blitzkit.app",
"api": "blitzkit/deploy-node-api.beta.blitzkit.app"
},
"aliases": {
"api": "https://api.beta.blitzkit.app",
"static": "https://static.beta.blitzkit.app"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": "api"
},
"nodes": {},
"aliases": {
"api": "/api",
"static": "https://static.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-opentest.blitzkit.app",
"api": "blitzkit/deploy-node-api.opentest.blitzkit.app"
},
"aliases": {
"api": "https://api.opentest.blitzkit.app",
"static": "https://static.opentest.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": false,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-preview.blitzkit.app",
"api": "blitzkit/deploy-node-api.preview.blitzkit.app"
},
"aliases": {
"api": "https://api.preview.blitzkit.app",
"static": "https://static.preview.blitzkit.app"
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"robots": true,
"canonical": "https://blitzkit.app",
"paths": {
"default": null,
"api": null
},
"nodes": {
"default": "blitzkit/deploy-node-blitzkit.app",
"api": "blitzkit/deploy-node-api.blitzkit.app"
},
"aliases": {
"api": "https://api.blitzkit.app",
"static": "https://static.blitzkit.app"
}
}
+10 -6
View File
@@ -1,20 +1,24 @@
{
"name": "blitzkit",
"version": "2.0.0-beta.3",
"version": "1.31.1",
"license": "BlitzKit License",
"private": true,
"scripts": {
"setup": "chmod a+x scripts/*.sh && ./scripts/setup.sh",
"dev": "./scripts/dev.sh",
"build-game": "./scripts/build_game.sh",
"build-website": "./scripts/build_website.sh"
"dev": "cd packages/website && bun run dev",
"build": "cd packages/website && bun run build",
"discover-ids": "cd packages/scripts && bun run discover-ids",
"build-protos": "cd packages/scripts && bun run build-protos",
"record-performance": "cd packages/scripts && bun run record-performance",
"download-client": "cd packages/scripts && bun run download-client"
},
"type": "module",
"workspaces": [
"packages/*"
],
"dependencies": {
"prettier": "^3.8.3",
"@gltf-transform/extensions": "^4.4.2",
"lz4js": "^0.2.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
}
}
-18
View File
@@ -1,18 +0,0 @@
{
"name": "@blitzkit/app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/cli": "^2",
"vite": "^6.0.3",
"typescript": "~5.6.2"
}
}
-7
View File
@@ -1,7 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
-5286
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "blitzkit"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "blitzkit_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
@@ -1,10 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
-14
View File
@@ -1,14 +0,0 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
-27
View File
@@ -1,27 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Command, Stdio};
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
let entry = app_handle
.path_resolver()
.resolve_resource("../../dist/website/server/entry.mjs")
.expect("failed to resolve server.ts");
Command::new("bun")
.arg("run")
.arg(entry)
.stderr(Stdio::null())
.spawn()
.expect("failed to start Bun server");
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running Tauri app");
}
-38
View File
@@ -1,38 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "blitzkit",
"version": "0.1.0",
"identifier": "com.tresabhi.blitzkit",
"build": {
"beforeDevCommand": "cd ../website && bun run dev",
"_beforeBuildCommand": "cd ../website && bun run build",
"frontendDist": "../../../dist/website/client",
"devUrl": "http://localhost:4321"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"url": "http://localhost:4321",
"title": "BlitzKit",
"fullscreen": false,
"resizable": true,
"maximized": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"extends": ["../../tsconfig.json"],
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
-35
View File
@@ -1,35 +0,0 @@
{
"name": "@blitzkit/bot",
"dependencies": {
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/themes": "^3.3.0",
"@resvg/resvg-js": "^2.6.2",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.9",
"@types/lodash-es": "^4.17.12",
"@types/markdown-escape": "^1.1.3",
"@types/react": "^19.2.14",
"adm-zip": "^0.5.16",
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"fuzzysort": "^3.1.0",
"lodash-es": "^4.17.23",
"markdown-escape": "^2.0.0",
"pickleparser": "^0.2.1",
"prisma": "^6.19.2",
"protobufjs": "^7.5.4",
"react": "^19.2.4",
"satori": "^0.18.4",
"sharp": "^0.34.5"
},
"private": true,
"scripts": {
"dev": "bun --watch src/main.ts",
"start": "bun src/main.ts"
},
"type": "module",
"types": "src/types.d.ts"
}
-68
View File
@@ -1,68 +0,0 @@
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import {
commandsArray,
type CommandRegistry,
} from "../events/interactionCreate";
const subcommands = [
"embeds",
"introduction",
"invite",
"timezones",
"credits",
] as const;
type Subcommand = (typeof subcommands)[number];
const DOCS: Record<Subcommand, string> = {
embeds: "guide/embeds",
introduction: "guide/introduction",
invite: "guide/invite",
timezones: "guide/timezones",
credits: "about/credits",
};
export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
command: createLocalizedCommand("about", [
{ subcommand: "commands" },
...subcommands.map((subcommand) => ({ subcommand })),
]),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
const { strings } = translator(interaction.locale);
if (subcommand === "commands") {
return `${strings.bot.commands.about.subcommands.commands.body}${(
await Promise.all(commandsArray)
)
.sort((a, b) =>
(a.command.name_localizations?.[interaction.locale] ??
a.command.name) <
(b.command.name_localizations?.[interaction.locale] ??
b.command.name)
? -1
: 1,
)
.map(
(registry) =>
`- \`/${
registry.command.name_localizations?.[interaction.locale] ??
registry.command.name
}\`: ${
registry.command.description_localizations?.[
interaction.locale
] ?? registry.command.description
}`,
)
.join("\n")}`;
}
const url = `https://raw.githubusercontent.com/tresabhi/blitzkit/main/docs/${DOCS[subcommand as Subcommand]}.md`;
const content = fetch(url).then((response) => response.text());
return content;
},
});
});
@@ -1,65 +0,0 @@
import { addPeriodicFilterOptions } from "../../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../../core/discord/resolvePeriodFromButton";
import { resolvePlayerFromButton } from "../../core/discord/resolvePlayerFromButton";
import { translator } from "../../core/localization/translator";
import type { CommandRegistry } from "../../events/interactionCreate";
import { renderBreakdown } from "./render";
export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("breakdown"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return [
// ...(await renderBreakdown(
// player,
// period,
// filters,
// interaction.locale,
// )),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(player.region, player.id),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(
player,
period,
filters,
interaction.locale,
);
},
});
},
);
@@ -1,295 +0,0 @@
import {
type BlitzStats,
calculateWN8,
getAccountInfo,
getClanAccountInfo,
getTankStats,
getWN8Percentile,
TankDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { chunk } from "lodash-es";
import * as Breakdown from "../../components/Breakdown";
import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import {
averageDefinitions,
tankDefinitions,
} from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import {
filterStats,
type StatFilters,
} from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import type { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import type { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await averageDefinitions;
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ["clan"]);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: BlitzStats[] = [];
const orderedCareerStats: BlitzStats[] = [];
const orderedCurrentWN8: (number | undefined)[] = [];
const orderedCareerWN8: (number | undefined)[] = [];
const orderedCareerWN8Full: (number | undefined)[] = [];
if (tankStats === null) {
throw new UserError(strings.bot.common.errors.no_tank_stats);
}
filteredOrder.forEach((id) => {
const tankAveragesEntry = awaitedTankAverages.averages[id];
const current = statsInPeriod.diff[id];
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
const currentWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, current)
: undefined;
const careerWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, career)
: undefined;
orderedCurrentStats.push(current);
orderedCareerStats.push(career);
orderedCurrentWN8.push(currentWN8);
orderedCareerWN8.push(careerWN8);
});
tankStats.forEach((tank) => {
const tankAveragesEntry = awaitedTankAverages.averages[tank.tank_id];
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, tank.all)
: undefined,
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0,
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0,
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0,
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0,
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0,
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0,
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0,
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0,
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0,
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0,
) / careerBattles;
const children: React.JSX.Element[] = [];
if (filteredOrder.length > 0) {
children.push(
<Breakdown.Row
key={id}
displayType="summary"
title={strings.bot.commands.breakdown.body.total}
stats={[
{
title: strings.bot.commands.breakdown.body.battles,
current: currentBattles,
career: careerBattles,
},
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
},
{
title: strings.bot.commands.breakdown.body.wn8,
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
},
]}
/>,
);
filteredOrder.forEach((id, index) => {
const tankDefinition = awaitedTankDefinitions.tanks[id] as
| TankDefinition
| undefined;
const current = orderedCurrentStats[index];
const career = orderedCareerStats[index];
const currentWN8 = orderedCurrentWN8[index];
const careerWN8 = orderedCareerWN8[index];
children.push(
<Breakdown.Row
key={id}
displayType="tank"
class={tankDefinition?.class}
type={tankDefinition?.type}
title={
tankDefinition
? unwrap(tankDefinition.name)
: literals(strings.bot.commands.breakdown.body.unknown_tank, {
id,
})
}
stats={[
{
title: strings.bot.commands.breakdown.body.battles,
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
current.wins / current.battles - career.wins / career.battles,
},
{
title: strings.bot.commands.breakdown.body.wn8,
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>,
);
});
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<CommandWrapper>
{index === 0 && (
<TitleBar
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
)}
<Breakdown.Root>{page}</Breakdown.Root>
</CommandWrapper>
))
.reverse();
} else {
return [
<CommandWrapper>
<TitleBar
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
<NoData type="battles_in_period" locale={locale} />
</CommandWrapper>,
];
}
}
-37
View File
@@ -1,37 +0,0 @@
import packageJSON from "../../../../package.json";
import { client } from "../core/discord/client";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const executionStart = new Date().getTime();
export const debugCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("debug"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const currentTime = new Date().getTime();
const uptime = currentTime - executionStart;
const list = [
[strings.bot.commands.debug.body.version, packageJSON.version],
[
strings.bot.commands.debug.body.shard.label,
client.shard?.ids[0] ?? strings.bot.commands.debug.body.shard.default,
],
[
strings.bot.commands.debug.body.uptime,
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
(uptime / 1000 / 60) % 60
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
uptime % 1000
)}ms`,
],
];
return `${strings.bot.commands.debug.body.title}${list
.map(([key, value]) => `- ${key}: ${value}`)
.join("\n")}`;
},
});
});
-182
View File
@@ -1,182 +0,0 @@
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
import { Locale, SlashCommandSubcommandGroupBuilder } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Graph from "../components/Graph";
import { LineColor } from "../components/Graph/components/Line/constants";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { getPlayerHistories } from "../core/blitzstars/getPlayerHistories";
import { getTankHistories } from "../core/blitzstars/getTankHistories";
import { addPeriodSubCommands } from "../core/discord/addPeriodSubCommands";
import { addTankChoices } from "../core/discord/addTankChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
type EvolutionStatType = "player" | "tank";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
type: "player" | "tank",
tankId: number | null,
locale: Locale,
) {
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const logo = clan ? emblemURL(clan.emblem_set_id) : undefined;
const histories = await (type === "player"
? getPlayerHistories(region, id, {
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
})
: getTankHistories(region, id, locale, {
tankId: tankId!,
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
}));
const plot = histories.map(
(history) =>
[
history.all.battles,
(history.all.wins / history.all.battles) * 100,
] as Graph.PlotItem,
);
const times = histories.map((history) => history.last_battle_time);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const ys = plot.map(([, y]) => y);
const xs = plot.map(([x]) => x);
const maxY = Math.max(...ys);
const minY = Math.min(...ys);
const maxX = Math.max(...xs);
const minX = Math.min(...xs);
return (
<CommandWrapper>
<TitleBar title={accountInfo.nickname} image={logo} description={name} />
{plot.length > 0 && (
<Graph.Root
verticalMargin={{
min: minY,
max: maxY,
suffix: "%",
precision: 2,
}}
horizontalMargin={{
min: minX,
max: maxX,
precision: 0,
}}
xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
>
{Graph.Line({
color: LineColor.Red,
plot: plot,
minY,
maxY,
})}
</Graph.Root>
)}
{plot.length === 0 && <NoData type="battles_in_period" locale={locale} />}
</CommandWrapper>
);
}
export const evolutionCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: createLocalizedCommand("evolution", [
{
group: "player",
modify(option: SlashCommandSubcommandGroupBuilder) {
addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices),
);
},
},
{
group: "tank",
modify(option: SlashCommandSubcommandGroupBuilder) {
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameChoices),
);
},
},
]),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const commandGroup =
// interaction.options.getSubcommandGroup() as EvolutionStatType;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const tankIdRaw = interaction.options.getString("tank")!;
// const tankId =
// commandGroup === "tank"
// ? await resolveTankId(tankIdRaw, interaction.locale)
// : null;
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// tankId,
// });
// return [
// await render(
// player,
// period,
// commandGroup,
// tankId,
// interaction.locale
// ),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(
// player.region,
// player.id,
// tankId ?? undefined
// ),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const url = new URL(`https://example.com/${interaction.customId}`);
const path = url.pathname.split("/");
const commandGroup = path[2] as EvolutionStatType;
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
return await render(
player,
period,
commandGroup,
parseInt(url.searchParams.get("tankId") ?? "0") || null,
interaction.locale,
);
},
});
},
);
-117
View File
@@ -1,117 +0,0 @@
import {
emblemURL,
getAccountInfo,
getClanAccountInfo,
getTierWeights,
} from "@blitzkit/core";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericAllStats } from "../components/GenericAllStats";
import { NoData } from "../components/NoData";
import { TierWeights } from "../components/TierWeights";
import { TitleBar } from "../components/TitleBar";
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary, filteredOrder } = await filterStats(
diffedTankStats,
filters,
);
const filterDescriptions = await filtersToDescription(filters, locale);
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
return (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{stats.battles === 0 && (
<NoData type="battles_in_period" locale={locale} />
)}
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
{stats.battles > 0 && (
<GenericAllStats
stats={stats}
supplementaryStats={supplementary}
locale={locale}
/>
)}
</CommandWrapper>
);
}
export const fullStatsCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("full-stats"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return Promise.all([
// render(player, period, filters, interaction.locale),
// buttonRefresh(interaction, path),
// getBlitzStarsLinkButton(player.region, player.id),
// ]);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters, interaction.locale);
},
});
},
);
-96
View File
@@ -1,96 +0,0 @@
import { getAccountInfo, getClanInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericStats } from "../components/GenericStats";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { addClanChoices } from "../core/discord/addClanChoices";
import { autocompleteClan } from "../core/discord/autocompleteClan";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_THRESHOLD = 7;
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("inactive")
.addStringOption(addClanChoices)
.addNumberOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName("threshold")
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.inactive.options.threshold.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.inactive.options.threshold.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.inactive.options.threshold.description
)
)
.setMinValue(0);
}),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
const inactive = accountInfo
.map(
(account) =>
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number]
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
.map(
([name, days]) =>
[
name,
literals(strings.bot.commands.inactive.body.listing, {
days: days.toFixed(0),
}),
] as [string, string]
);
const hasInactiveMembers = inactive.length > 0;
return (
<CommandWrapper>
<TitleBar
title={clanInfo.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${literals(
strings.bot.commands.inactive.body.subtitle,
{ days: threshold }
)} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
{!hasInactiveMembers && (
<NoData type="players_in_period" locale={interaction.locale} />
)}
{hasInactiveMembers && <GenericStats stats={inactive} />}
</CommandWrapper>
);
},
autocomplete: autocompleteClan,
});
});
-193
View File
@@ -1,193 +0,0 @@
import { getAccountInfo, getClanAccountInfo } from '@blitzkit/core';
import { teal } from '@radix-ui/colors';
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
import { CommandWrapper } from '../components/CommandWrapper';
import { linkBlitzAndDiscord } from '../core/db/linkBlitzAndDiscord';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import type { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('link').addStringOption((option) =>
addUsernameChoices(option).setRequired(true),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = BigInt(interaction.user.id);
const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
await linkBlitzAndDiscord(discordId, id);
return [
<CommandWrapper fat>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
}}
>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
alt="avatar"
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<img
alt="accounts linked"
src="https://i.imgur.com/jIcRgog.png"
style={{
width: 16,
height: 16,
}}
/>
<span style={{ fontSize: 16 }}>
{strings.bot.commands.link.body.accounts_linked}
</span>
</div>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img
alt="clan emblem"
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
/>
) : null}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{accountInfo.nickname}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] • `
: ''}
{strings.common.regions.normal[region]}
</span>
</div>
</div>
</div>
<Glow color={teal.teal9} rotation={-90} />
</div>
</CommandWrapper>,
embedInfo(
strings.bot.commands.link.embed.title,
strings.bot.commands.link.embed.description,
),
];
},
autocomplete: autocompleteUsername,
});
});
-90
View File
@@ -1,90 +0,0 @@
import {
TANK_CLASSES,
TIER_ROMAN_NUMERALS,
TankDefinition,
flags,
getAccountInfo,
getTankStats,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import {
gameDefinitions,
tankDefinitions,
} from "../core/blitzkit/nonBlockingPromises";
import { addTierChoices } from "../core/discord/addTierChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { chunkLines } from "../core/discord/chunkLines";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("owned-tanks")
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const tier = Number(interaction.options.getString("tier"));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id);
if (tankStats === null) {
return strings.bot.common.errors.no_tank_stats;
}
const filteredTanks = (
await Promise.all(
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id,
})),
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = [];
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation);
} else {
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
}
});
const awaitedTankDefinitions = await tankDefinitions;
const awaitedGameDefinitions = await gameDefinitions;
const title = literals(strings.bot.commands.owned_tanks.body.title, {
name: markdownEscape(accountInfo.nickname),
tier: TIER_ROMAN_NUMERALS[tier],
});
const tankList = tankStats
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
)
.sort(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation),
)
.map(
(tank) =>
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name!))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
);
const lines = [title, ...tankList];
return chunkLines(lines);
},
autocomplete: autocompleteUsername,
});
});
-46
View File
@@ -1,46 +0,0 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("permissions"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
// friday the 13th permissions easter egg
const isFridayThe13th =
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has("ViewChannel"),
strings.bot.commands.permissions.body.view_channels,
],
[
interaction.appPermissions?.has("ReadMessageHistory"),
strings.bot.commands.permissions.body.read_message_history,
],
[
interaction.appPermissions?.has("AttachFiles"),
strings.bot.commands.permissions.body.attach_files,
],
[
interaction.appPermissions?.has("UseExternalEmojis"),
strings.bot.commands.permissions.body.external_emojis,
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
} else {
return `${isFridayThe13th ? "💀" : "❌"} ${description}`;
}
})
.join("\n");
return literals(strings.bot.commands.permissions.body.title, { body });
},
});
});
-34
View File
@@ -1,34 +0,0 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
handlesInteraction: true,
command: createLocalizedCommand("ping", [
{ subcommand: "blitzkit" },
{ subcommand: "wotb" },
]),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === "blitzkit") {
await interaction.editReply(strings.bot.commands.ping.body.pong);
} else {
await fetch("https://api.wotblitz.com/");
}
const executionTime = Date.now() - executionStart;
interaction.editReply(
literals(strings.bot.commands.ping.body.pong_with_time, {
time: executionTime.toFixed(0),
})
);
},
});
});
-69
View File
@@ -1,69 +0,0 @@
import { getAccountInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { markdownTable } from "../core/discord/markdownTable";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command:
createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const account = await resolvePlayerFromCommand(interaction);
const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
literals(strings.bot.commands.player_info.body.title, {
name: markdownEscape(accountInfo.nickname),
}),
markdownTable([
[
strings.bot.commands.player_info.body.nickname,
`${accountInfo.nickname}`,
],
[
strings.bot.commands.player_info.body.battles,
`${accountInfo.statistics.all.battles}`,
],
[
strings.bot.commands.player_info.body.winrate,
`${(
100 *
(accountInfo.statistics.all.wins /
accountInfo.statistics.all.battles)
).toFixed(2)}%`,
],
[],
[
strings.bot.commands.player_info.body.account_id,
`${accountInfo.account_id}`,
],
[
strings.bot.commands.player_info.body.created,
new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale
),
],
[
strings.bot.commands.player_info.body.last_battle,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale
),
],
])
);
},
autocomplete: autocompleteUsername,
});
});
-535
View File
@@ -1,535 +0,0 @@
import {
deltaBkrlBlitzStats,
emblemURL,
getAccountInfo,
getArchivedRatingLeaderboard,
getClanAccountInfo,
getLeagueFromScore,
getRatingInfo,
getRatingNeighbors,
imgur,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { SlashCommandSubcommandBuilder } from "discord.js";
import markdownEscape from "markdown-escape";
import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
import { CommandWrapper } from "../components/CommandWrapper";
import { DeltaCaret } from "../components/DeltaCaret";
import { TitleBar } from "../components/TitleBar";
import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
import { iconPng } from "../core/blitzkit/iconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
const LEAGUE_ACCENT = [
"_purple",
"_sand",
"_amber",
"_mauve",
"_brown",
] as const;
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("rating", [
{
subcommand: "today",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
{
subcommand: "season",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
]),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true) as
| "season"
| "today";
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const ratingInfo = await getRatingInfo(region);
if (ratingInfo.detail) {
throw new Error(`Rating info not available for ${region}`);
}
const leaderboard = await getArchivedRatingLeaderboard(
region,
ratingInfo.current_season - (subcommand === "today" ? 0 : 1)
);
if (leaderboard.version!.$case === "v1") {
throw new Error(
"Encountered bkrl minimal format in latest season. Wait till next cron?"
);
}
const accountInfo = await getAccountInfo(region, id, [
"statistics.rating",
]);
const statsIndex = leaderboard.version!.value.entries.findIndex(
(entry) => entry.id === id
);
const statsA = leaderboard
? leaderboard.version!.value.entries[statsIndex!]
: null;
const positionA = leaderboard ? statsIndex! + 1 : undefined;
const statsB1 = accountInfo.statistics.rating!;
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
if (statsB2Array === undefined) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const [statsB2] = statsB2Array;
const league = getLeagueFromScore(statsB2.score);
if (!statsA) {
return strings.bot.commands.rating.errors.unrecorded_stats;
}
if (!statsB1) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const reward = ratingInfo.rewards.find(
(reward) =>
statsB2.number >= reward.from_position &&
statsB2.number <= reward.to_position
);
const positionDelta = leaderboard
? statsB2.number - positionA!
: undefined;
const delta = deltaBkrlBlitzStats(
statsA,
accountInfo.statistics.rating!,
statsB2
);
const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward
? reward.type === "vehicle"
? reward.vehicle.preview_image_url
: reward.stuff.image_url
: undefined;
if (rewardImage) {
const { ok } = await fetch(rewardImage);
if (!ok) {
rewardImage = imgur("kP88vqr");
}
}
return (
<CommandWrapper>
<TitleBar
title={accountInfo.nickname}
image={clanImage}
description={`${
strings.bot.commands.rating.body.subtitle[subcommand]
} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
<div
style={{
position: "relative",
display: "flex",
overflow: "hidden",
}}
>
<div
style={{
position: "relative",
padding: 16,
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
borderRadius: 4,
border: `1px solid ${
theme.colors[`componentInteractive${leagueTheme}`]
}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
{reward === undefined && (
<Glow
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(-16px) rotate(90deg)",
}}
/>
)}
<div
style={{
display: "flex",
gap: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<img
alt={`League ${league.name}`}
style={{
width: 64,
height: 64,
objectFit: "contain",
}}
src={normalizeLeagueIcon(
ratingInfo.leagues[league.index].big_icon
)}
/>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
style={{
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 24,
fontWeight: 900,
}}
>
{statsB2.score.toLocaleString(interaction.locale)}
</span>
{delta.score !== 0 && (
<>
<DeltaCaret delta={delta.score} />
<span
style={{
color:
delta.score > 0
? theme.colors.textLowContrast_green
: theme.colors.textLowContrast_red,
fontSize: 16,
}}
>
{Math.abs(delta.score).toLocaleString(
interaction.locale
)}
</span>
</>
)}
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${leagueTheme}`],
fontSize: 16,
}}
>
#{statsB2.number.toLocaleString(interaction.locale)}
</span>
{positionDelta !== undefined && positionDelta !== 0 && (
<>
<DeltaCaret delta={-positionDelta} />
<span
style={{
color:
positionDelta > 0
? theme.colors.textLowContrast_red
: theme.colors.textLowContrast_green,
fontSize: 16,
}}
>
{Math.abs(positionDelta).toLocaleString(
interaction.locale
)}
</span>
</>
)}
</div>
</div>
</div>
{reward && (
<div
style={{
display: "flex",
gap: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
style={{
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 16,
maxWidth: 136,
textAlign: "right",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{reward.type === "vehicle"
? reward.vehicle.user_string
: reward.stuff.title}
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${leagueTheme}`],
fontSize: 16,
}}
>
Reward
{reward.type === "stuff" ? ` x${reward.count}` : ""}
</span>
</div>
</div>
<img
style={{
width: 64,
height: 64,
objectFit: "cover",
}}
src={await iconPng(rewardImage!)}
/>
</div>
)}
{reward === undefined && (
<Glow
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(16px) rotate(-90deg)",
}}
/>
)}
</div>
{/* separated because fragment middle man invalidates relative parent: https://github.com/vercel/satori/issues/605 */}
{reward && (
<Glow
direction="reverse"
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
top: -40,
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
{reward && (
<Glow
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
bottom: -40,
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
</div>
{delta.battles > 0 && (
<div style={{ padding: "8px 0", display: "flex" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{delta.battles.toLocaleString(interaction.locale)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Battles
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Winrate
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{Math.round(delta.damage / delta.battles).toLocaleString(
interaction.locale
)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Survival
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(delta.kills / delta.battles).toFixed(2)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
)}
</CommandWrapper>
);
},
});
});
@@ -1,468 +0,0 @@
import {
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
getAccountInfo,
getArchivedLatestSeasonNumber,
getArchivedRatingLeaderboard,
getClanAccountInfo,
getRatingInfo,
getRatingLeague,
getRatingNeighbors,
imgur,
isOnGoingRatingSeason,
LEAGUES,
type RatingInfo,
RatingLeaderboard,
type Region,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
type APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from "discord.js";
import { range } from "lodash-es";
import markdownEscape from "markdown-escape";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Leaderboard from "../components/Leaderboard";
import { TitleBar } from "../components/TitleBar";
import { UserError } from "../core/blitzkit/userError";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const DEFAULT_LIMIT = 10;
export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range(
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
).slice(-25);
function addOptions(option: SlashCommandSubcommandBuilder) {
const { strings } = translator(Locale.EnglishUS);
return option
.addIntegerOption((option) =>
option
.setName(strings.bot.commands.rating_leaderboard.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30)
)
.addStringOption((option) =>
option
.setName(
strings.bot.commands.rating_leaderboard.options.season.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.season.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season
.description
)
)
.setRequired(false)
.addChoices(
...seasonNumbers
.map((number, index) => {
const current =
index === seasonNumbers.length - 1 && onGoingSeason;
return {
name: current
? literals(
strings.bot.commands.rating_leaderboard.options.season
.choices.current,
{ season: number }
)
: `${number}`,
value: `${number}`,
name_localizations: current
? localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options
.season.choices.current,
{ season: number }
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse()
)
);
}
resolve({
command: createLocalizedCommand("rating-leaderboard", [
{
subcommand: "neighbors",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: "league",
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName(
strings.bot.commands.rating_leaderboard.options.league.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.league
.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.description
)
)
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name: (
strings.common.leagues as Record<string, string>
)[name],
value: `${value}`,
name_localizations: localizationObject(
(strings) =>
(
strings.common.leagues as Record<string, string>
)[name]
),
} satisfies APIApplicationCommandOptionChoice<string>)
)
)
.setRequired(true);
})
.addStringOption(addRegionChoices);
addOptions(option);
},
},
]),
// BIG TODO: title seems to be in english
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| "league"
| "neighbors";
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString("season");
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string;
let titleImage: string | undefined;
let titleDescription: string;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let archivedLeaderboard: RatingLeaderboard | null;
let playerId: number | undefined;
let regionRatingInfo: RatingInfo | null;
if (subcommand === "league") {
const leagueIndex = parseInt(
interaction.options.getString("league")!
);
const region = interaction.options.getString("region") as Region;
regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region)
: null);
if (
regionRatingInfo !== null &&
regionRatingInfo.detail !== undefined
) {
return strings.bot.commands.rating_leaderboard.body
.no_ongoing_season;
}
const result = isCurrentSeason
? await getRatingLeague(region, leagueIndex).then(async (result) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
} satisfies SimplifiedPlayer)
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.version!.value.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.version!.value.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
["clan"]
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag,
nickname:
clanData[index]?.account_name ??
literals(
strings.bot.commands.rating_leaderboard.body
.deleted_player,
{ id: player.id }
),
} satisfies SimplifiedPlayer)
);
}
);
const leagueInfo = LEAGUES[leagueIndex];
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
);
players = result;
playersAfter =
(regionRatingInfo === null
? archivedLeaderboard.version!.value.entries.length
: regionRatingInfo.count) - result[result.length - 1].position;
titleName = `${
leagueInfo
? (strings.common.leagues as Record<string, string>)[
LEAGUES[leagueInfo.index].name
]
: ""
} - ${strings.common.regions.short[region]}`;
titleImage = imgur(leagueInfo.icon);
titleDescription = literals(
strings.bot.commands.rating_leaderboard.body.top_players,
{ count: limit }
);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingInfo = isCurrentSeason
? await getRatingInfo(region)
: null;
if (
regionRatingInfo !== null &&
regionRatingInfo.detail !== undefined
) {
return strings.bot.commands.rating_leaderboard.body
.no_ongoing_season;
}
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
} satisfies SimplifiedPlayer)
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.version!.value.entries.findIndex(
(player) => player.id === id
);
if (playerIndex === -1) {
throw new UserError(
literals(
strings.bot.commands.rating_leaderboard.body
.no_participation,
{ player: markdownEscape(accountInfo.nickname), season }
)
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.version!.value.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
["clan"]
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[index]?.clan?.tag,
nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
}
);
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : "";
titleDescription =
strings.bot.commands.rating_leaderboard.body.neighbors;
playersAfter =
(regionRatingInfo === null
? archivedLeaderboard.version!.value.entries.length
: regionRatingInfo.count) -
neighbors[neighbors.length - 1].position;
playerId = id;
}
const items = players.map((player) => {
const midnightIndex =
archivedLeaderboard?.version!.value.entries.findIndex(
(item) => item.id === player.id
);
const midnightScore =
midnightIndex === undefined
? player.score
: archivedLeaderboard!.version!.value.entries[midnightIndex]
.score;
const midnightPosition =
midnightIndex === undefined ? player.position : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.position}
clan={player.clan}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return (
<CommandWrapper>
<TitleBar
title={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={literals(
strings.bot.commands.rating_leaderboard.body.more,
{ count: playersAfter.toLocaleString(interaction.locale) }
)}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
);
},
autocomplete: autocompleteUsername,
});
}
);
-592
View File
@@ -1,592 +0,0 @@
import {
calculateWN8,
emblemURL,
getClanAccountInfo,
idToRegion,
tankIcon,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { Locale } from "discord.js";
import { clamp } from "lodash-es";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { iconPng } from "../core/blitzkit/iconPng";
import {
averageDefinitions,
mapDefinitions,
tankDefinitions,
} from "../core/blitzkit/nonBlockingPromises";
import { RenderConfiguration } from "../core/blitzkit/renderConfiguration";
import { buttonLink } from "../core/discord/buttonLink";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
type WotInspectorReplaySubmission =
| { status: "error"; data: unknown; error: { message: string } }
| {
status: "ok";
data: {
download_url: string;
view_url: string;
summary: {
battle_start_time: string;
map_name: string;
battle_start_timestamp: number;
battle_duration: number;
title: string;
arena_unique_id: string;
allies: number[];
enemies: number[];
protagonist: number;
player_name: string;
vehicle: string;
exp_base: number;
exp_total: number;
credits_base: number;
credits_total: number;
room_type: number;
battle_type: number;
mastery_badge: number;
battle_result: number;
vehicle_tier: number;
vehicle_type: number;
protagonist_team: number;
winner_team: number;
details: {
achievements: unknown[];
base_capture_points: number;
base_defend_points: number;
chassis_id: unknown | null;
clan_tag: string;
clanid: number;
credits: number;
damage_assisted: number;
damage_assisted_track: number;
damage_blocked: unknown | null;
damage_made: number;
damage_received: number;
dbid: number;
death_reason: number;
distance_travelled: number;
enemies_damaged: number;
enemies_destroyed: number;
enemies_spotted: number;
entity_id: number;
exp: number;
exp_for_assist: number;
exp_for_damage: number;
exp_team_bonus: number;
gun_id: unknown | null;
hero_bonus_credits: number;
hero_bonus_exp: number;
hitpoints_left: number;
hits_bounced: number;
hits_pen: number;
hits_received: number;
hits_splash: number;
killed_by: number;
name: string;
shots_hit: number;
shots_made: number;
shots_pen: number;
shots_splash: number;
squad_index: number;
team: number;
time_alive: number;
turret_id: unknown | null;
vehicle_descr: number;
wp_points_earned: number;
wp_points_stolen: number;
};
};
};
error: {};
};
interface WotInspectorReplay {
id: string;
map_id: number;
battle_duration: number;
title: unknown | null;
player_name: string;
protagonist: number;
vehicle_descr: number;
mastery_badge: number;
exp_base: number;
enemies_spotted: number;
enemies_destroyed: number;
damage_assisted: number;
damage_made: number;
details_url: string;
download_url: string;
game_version: {
name: string;
package: string;
};
arena_unique_id: string;
download_count: number;
data_version: number;
private: boolean;
private_clan: boolean;
battle_start_time: Date;
upload_time: Date;
allies: number[];
enemies: number[];
protagonist_clan: number;
protagonist_team: number;
battle_result: number;
credits_base: number;
tags: number[];
battle_type: number;
room_type: number;
last_accessed_time: Date;
winner_team: number;
finish_reason: number;
players_data: WotInspectorReplayPlayerData[];
exp_total: number;
credits_total: number;
repair_cost: number;
exp_free: number;
exp_free_base: number;
exp_penalty: number;
credits_penalty: number;
credits_contribution_in: number;
credits_contribution_out: number;
camouflage_id: number;
}
export interface WotInspectorReplayPlayerData {
team: number;
name: string;
entity_id: number;
dbid: number;
clanid: number | null;
clan_tag: null | string;
hitpoints_left: number;
credits: number;
exp: number;
shots_made: number;
shots_hit: number;
shots_splash: number;
shots_pen: number;
damage_made: number;
damage_received: number;
damage_assisted: number;
damage_assisted_track: number;
hits_received: number;
hits_bounced: number;
hits_splash: number;
hits_pen: number;
enemies_spotted: number;
enemies_damaged: number;
enemies_destroyed: number;
time_alive: number;
distance_travelled: number;
killed_by: number;
base_capture_points: number;
base_defend_points: number;
exp_for_damage: number;
exp_for_assist: number;
exp_team_bonus: number;
wp_points_earned: number;
wp_points_stolen: number;
hero_bonus_credits: number;
hero_bonus_exp: number;
death_reason: number;
achievements: any[];
vehicle_descr: number;
turret_id: number;
gun_id: number;
chassis_id: number;
squad_index: number;
damage_blocked: number;
}
function stat(value: string | number, flex = 2) {
return (
<div
style={{
flex,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
</div>
);
}
async function playerListing(
locale: Locale,
player: WotInspectorReplayPlayerData,
protagonistTeam: boolean,
winningTeam: number,
recordingPlayer = false,
) {
const { unwrap } = translator(locale);
const awaitedTankAverages = await averageDefinitions;
const awaitedTankDefinitions = await tankDefinitions;
const tank = awaitedTankDefinitions.tanks[player.vehicle_descr];
const blockAccent = recordingPlayer ? "_amber" : "";
const healthAccent = protagonistTeam
? recordingPlayer
? "_amber"
: "_green"
: "_red";
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
// TODO: 💀
const turret = tank.turrets.sort(
(a, b) =>
Math.abs(a.id - player.turret_id) - Math.abs(b.id - player.turret_id),
)[0];
const totalHealth = turret.health + tank.health;
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
const tankAveragesEntry = awaitedTankAverages.averages[player.vehicle_descr];
const wn8 =
tankAveragesEntry === undefined
? -1
: calculateWN8(tankAveragesEntry.mu, {
battles: 1,
damage_dealt: player.damage_made,
dropped_capture_points: player.base_defend_points,
frags: player.enemies_destroyed,
spotted: player.enemies_spotted,
wins: winningTeam === player.team ? 1 : 0,
});
return (
<div
key={player.entity_id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
}}
>
<div
style={{
display: "flex",
gap: 8,
flex: 1,
overflow: "hidden",
alignItems: "center",
}}
>
<img
alt={unwrap(tank.name!)}
src={await iconPng(tankIcon(tank.id))}
style={{
width: 32,
height: 32,
objectFit: "contain",
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
// align
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${blockAccent}`],
fontSize: 12,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
<span
style={{
color: theme.colors[`textHighContrast${blockAccent}`],
fontSize: 16,
}}
>
{unwrap(tank.name!)}
</span>
</div>
<span
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
whiteSpace: "nowrap",
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ""}
</span>
</div>
</div>
<div
style={{
display: "flex",
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat(wn8 === -1 ? "--" : Math.round(wn8).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
<div
style={{
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: "flex",
width: 4,
height: "100%",
transform: "scaleY(-1)",
}}
>
<div
style={{
width: "100%",
height: `${healthLeft * 100}%`,
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
}}
/>
</div>
</div>
);
}
function Header() {
return (
<div
style={{
padding: 8,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
flex: 1,
}}
>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
WN8
</span>
</div>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
XP
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
</div>
);
}
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("replay").addAttachmentOption((option) =>
option
.setName(strings.bot.commands.replay.options.file.name)
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.name,
undefined,
true,
),
)
.setDescription(strings.bot.commands.replay.options.file.description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.description,
),
)
.setRequired(true),
),
async handler(interaction) {
const awaitedMapDefinitions = await mapDefinitions;
const file = interaction.options.getAttachment("file")!;
const submissionData = (await fetch(
`https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent(
file.url,
)}`,
).then((response) => response.json())) as WotInspectorReplaySubmission;
if (submissionData.status === "error") {
return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`;
}
const replayId = submissionData.data.download_url.split("/").at(-1)!;
const replayData = (await fetch(
`https://api.wotinspector.com/v2/blitz/replays/${replayId}/`,
).then((response) => response.json())) as WotInspectorReplay;
const antagonistTeamId = 3 - replayData.protagonist_team;
const players = replayData.players_data.sort(
(a, b) => b.damage_made - a.damage_made,
);
const clan = (
await getClanAccountInfo(
idToRegion(replayData.protagonist),
replayData.protagonist,
["clan"],
)
)?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
return [
new RenderConfiguration({ width: 960 }),
<CommandWrapper>
<TitleBar
title={replayData.player_name}
image={clanImage}
description={`${
replayData.protagonist_team === replayData.winner_team
? "Victory"
: "Defeat"
} • ${awaitedMapDefinitions.maps[replayData.map_id].name}`}
/>
<div
style={{
display: "flex",
gap: 8,
}}
>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter(
(player) => player.team === replayData.protagonist_team,
)
.map((player) =>
playerListing(
interaction.locale,
player,
true,
replayData.winner_team,
player.dbid === replayData.protagonist,
),
),
)}
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter((player) => player.team === antagonistTeamId)
.map((player) =>
playerListing(
interaction.locale,
player,
false,
replayData.winner_team,
),
),
)}
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, "WoT Inspector"),
buttonLink(submissionData.data.download_url, "Download"),
];
},
});
});
-626
View File
@@ -1,626 +0,0 @@
import {
asset,
emblemURL,
equipmentPriceMatrix,
getAccountInfo,
getClanAccountInfo,
getTankStats,
TANK_ICONS,
TankType,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { escapeMarkdown, Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { resolveTankId } from "../core/blitz/resolveTankId";
import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
import { iconPng } from "../core/blitzkit/iconPng";
import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
import { tankIconPng } from "../core/blitzkit/tankIconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("research")
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.target_tank.name!)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.name,
undefined,
true,
),
)
.setDescription(
strings.bot.commands.research.options.target_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.description,
),
)
.setAutocomplete(true)
.setRequired(true),
)
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.starting_tank.name!)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.name,
undefined,
true,
),
)
.setDescription(
strings.bot.commands.research.options.starting_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.description,
),
)
.setAutocomplete(true)
.setRequired(false),
)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId(
interaction.options.getString("target-tank", true),
interaction.locale,
true,
);
const startingTankRaw = interaction.options.getString("starting-tank");
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
const targetTankAncestry = await resolveAncestry(targetTankId);
if (startingTankRaw) {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
);
if (targetTankId === startingTankId) {
return literals(
strings.bot.commands.research.errors.start_end_equal,
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
);
}
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
if (startingTank.tier > targetTank.tier) {
return literals(strings.bot.commands.research.errors.unordered, {
tank_1: escapeMarkdown(unwrap(startingtank.name!)),
tank_2: escapeMarkdown(unwrap(targettank.name!)),
});
}
if (!targetTankAncestry.includes(startingTankId)) {
return literals(
strings.bot.commands.research.errors.tanks_not_on_line,
{
tank_1: escapeMarkdown(unwrap(targettank.name!)),
tank_2: escapeMarkdown(unwrap(startingtank.name!)),
},
);
}
} else {
if (targetTank.type !== TankType.TANK_TYPE_RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, {
tank: escapeMarkdown(unwrap(targettank.name!)),
});
}
if (targetTank.tier === 1) {
return literals(strings.bot.commands.research.errors.tier_1, {
tank: escapeMarkdown(unwrap(targettank.name!)),
});
}
const tankStats = await getTankStats(region, id);
if (tankStats === null) {
return strings.bot.common.errors.no_tank_stats;
}
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return literals(
strings.bot.commands.research.errors.already_researched,
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
awaitedTankDefinitions.tanks[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id),
);
if (foundAncestor === undefined) {
throw new Error("No first owned tech tree found.");
}
startingTankId = foundAncestor;
}
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
);
const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const costs = await Promise.all(
line.map(async (id) => {
const tank = awaitedTankDefinitions.tanks[id];
const turretXps = new Map<number, number>();
const gunXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
tank.turrets.forEach((turret) => {
if (
turret.research_cost !== undefined &&
typeof turret.research_cost.research_cost_type!.value === "number"
) {
turretXps.set(
turret.id,
turret.research_cost.research_cost_type!.value,
);
}
turret.guns.forEach((gun) => {
if (
gun.research_cost !== undefined &&
typeof gun.research_cost.research_cost_type!.value === "number"
) {
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
}
});
});
tank.engines.forEach((engine) => {
if (
engine.research_cost !== undefined &&
typeof engine.research_cost.research_cost_type!.value === "number"
) {
engineXps.set(
engine.id,
engine.research_cost.research_cost_type!.value,
);
}
});
tank.tracks.forEach((track) => {
if (
track.research_cost !== undefined &&
typeof track.research_cost.research_cost_type!.value === "number"
) {
trackXps.set(
track.id,
track.research_cost.research_cost_type!.value,
);
}
});
let moduleXp = 0;
turretXps.forEach((value) => {
moduleXp += value;
});
gunXps.forEach((value) => {
moduleXp += value;
});
engineXps.forEach((value) => {
moduleXp += value;
});
trackXps.forEach((value) => {
moduleXp += value;
});
return {
research: tank.research_cost!.research_cost_type!.value as number,
upgrades: moduleXp,
purchase: tank.price!.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
),
};
}),
);
const image = (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${strings.bot.commands.research.body.subtitle} • ${
targetTank.name
}`}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<div
style={{
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div style={{ width: 48 }}></div>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
right: 24,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{strings.bot.commands.research.body.total}
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: "16px 0",
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Experience:{" "}
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Purchase:{" "}
{costs
.reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Equipment:{" "}
{costs
.reduce((a, b) => a + b.equipment, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
</div>
</div>
{await Promise.all(
line.map(async (id, index) => {
const tank = awaitedTankDefinitions.tanks[id];
return (
<div
key={id}
style={{
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div
style={{
width: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
position: "relative",
right: "5%",
}}
>
<img
alt="Tank icon"
src={await tankIconPng(id)}
style={{
height: 100,
width: 100,
objectFit: "contain",
position: "absolute",
left: "25%",
top: 0,
}}
/>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
justifyContent: "center",
position: "absolute",
bottom: 8,
left: "50%",
transform: "translateX(-50%)",
}}
>
<img
alt={unwrap(tank.name!)}
src={TANK_ICONS[tank.class]}
width={16}
height={16}
/>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{unwrap(tank.name!)}
</span>
</div>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: "16px 0",
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.research,
{
cost: costs[index].research?.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
{costs[index].upgrades > 0 && (
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Free XP icon"
src={await iconPng(
asset("icons/currencies/free-xp.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.upgrades,
{ cost: costs[index].upgrades.toLocaleString() },
)}
</span>
</div>
)}
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.purchase,
{
cost: costs[index].purchase.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.equipment,
{
cost: costs[index].equipment.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
</div>
</div>
);
}),
)}
</div>
</CommandWrapper>
);
return startingTankRaw
? image
: [strings.bot.commands.research.body.estimation, image];
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]);
},
});
});
-74
View File
@@ -1,74 +0,0 @@
import { type ClanList, fetchBlitz, type Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addClanChoices } from "../core/discord/addClanChoices";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("search-clans")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addClanChoices(option).setAutocomplete(false)
)
.addIntegerOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName(strings.bot.commands.search_clans.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.search_clans.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100);
}),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const clan = interaction.options.getString("clan")!;
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, "clans/list", {
search: clan,
limit,
});
return embedInfo(
literals(strings.bot.commands.search_clans.body.title, {
search: markdownEscape(clan),
region: strings.common.regions.normal[region],
}),
clanList.length === 0
? strings.bot.commands.search_clans.body.no_results
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join("\n")}\n\`\`\``
);
},
});
});
@@ -1,83 +0,0 @@
import {
type AccountList,
fetchBlitz,
type Region,
usernamePattern,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("search-players")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true)
)
.addIntegerOption((option) =>
option
.setName(strings.bot.commands.search_players.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100)
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const name = interaction.options.getString("username")!;
const limit = interaction.options.getInteger("limit") ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(region, "account/list", {
search: trimmedSearch,
limit,
})
: [];
return embedInfo(
literals(strings.bot.commands.search_players.body.title, {
search: markdownEscape(trimmedSearch),
region: strings.common.regions.normal[region],
}),
`\`\`\`${
players.length === 0
? strings.bot.commands.search_players.body.no_results
: players.map((player) => player.nickname).join("\n")
}\`\`\``
);
},
});
});
-102
View File
@@ -1,102 +0,0 @@
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
import { Locale } from "discord.js";
import { AllStatsOverview } from "../components/AllStatsOverview";
import { CommandWrapper } from "../components/CommandWrapper";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
const filterDescriptions = await filtersToDescription(filters, locale);
return (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{!stats.battles && <NoData type="battles_in_period" locale={locale} />}
{stats.battles > 0 && (
<AllStatsOverview
locale={locale}
stats={stats}
supplementaryStats={supplementary}
/>
)}
</CommandWrapper>
);
}
export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("stats"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return Promise.all([
// render(player, period, filters, interaction.locale),
// buttonRefresh(interaction, path),
// getBlitzStarsLinkButton(player.region, player.id),
// ]);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters, interaction.locale);
},
});
});
-60
View File
@@ -1,60 +0,0 @@
import { gameDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { addFilterOptions } from "../core/discord/addFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { renderBreakdown } from "./breakdown/render";
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
const awaitedGameDefinitions = await gameDefinitions;
resolve({
command: addFilterOptions(
createLocalizedCommand("today"),
awaitedGameDefinitions.nations,
).addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(
// player.region,
// interaction,
// 'today',
// );
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction, true),
// ...filters,
// });
// return [
// ...(await renderBreakdown(player, period, filters, interaction.locale)),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(player.region, player.id),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters, interaction.locale);
},
});
});
@@ -1,50 +0,0 @@
import { type ComponentProps } from "react";
export interface GlowProps extends ComponentProps<"div"> {
color: string;
direction?: "default" | "reverse";
rotation?: number;
startOpacity?: number;
endOpacity?: number;
}
const width = 16;
export function Glow({
color,
direction = "default",
rotation = 0,
style,
startOpacity = 0,
endOpacity = 1,
...props
}: GlowProps) {
const color1 = `${color}${Math.round(endOpacity * 255)
.toString(16)
.padStart(2, "0")}`;
const color2 = `${color}${Math.round(startOpacity * 255)
.toString(16)
.padStart(2, "0")}`;
const background = `linear-gradient(${
direction === "default" ? 0 : 180
}deg, ${color1} 0%, ${color2} 100%)`;
return (
<div
style={{
display: "flex",
alignItems: direction === "default" ? "flex-end" : "flex-start",
justifyContent: "center",
transform: `rotate(${rotation}deg)`,
...style,
}}
{...props}
>
<div style={{ height: 54, width, background }} />
<div style={{ height: 72, width, background }} />
<div style={{ height: 80, width, background }} />
<div style={{ height: 72, width, background }} />
<div style={{ height: 54, width, background }} />
</div>
);
}
@@ -1,72 +0,0 @@
import { theme } from '../../../../stitches.config';
import { Glow } from './components/Glow';
export interface WN8DisplayProps {
subtitle: string;
color: string;
value: string | number;
}
export function HeroStat({ color, subtitle, value }: WN8DisplayProps) {
return (
<div
style={{
width: 128 * 2, // idk why twice the width turns into the correct width
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Glow color={color} style={{ marginBottom: -16 }} />
<div
style={{
width: 128,
height: 80,
backgroundColor: color,
borderRadius: 40,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
}}
>
<span
style={{
display: 'block',
fontWeight: 900,
fontSize: 32,
color: theme.colors.textHighContrast,
lineHeight: '26px',
letterSpacing: '-0.03em',
}}
>
{value}
</span>
<span
style={{
display: 'block',
fontSize: 16,
lineHeight: '16px',
color: theme.colors.textHighContrast,
}}
>
{subtitle}
</span>
</div>
</div>
<Glow color={color} direction="reverse" style={{ marginTop: -16 }} />
</div>
);
}
@@ -1,47 +0,0 @@
import { theme } from '../../../stitches.config';
export enum WN8SurroundingStatAlign {
Left,
Right,
}
export interface WN8SurroundingStatProps {
label: string;
value: string | number;
align?: WN8SurroundingStatAlign;
padded?: boolean;
}
export function WN8SurroundingStat({
label,
value,
align = WN8SurroundingStatAlign.Left,
padded = false,
}: WN8SurroundingStatProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems:
align === WN8SurroundingStatAlign.Left ? 'flex-start' : 'flex-end',
paddingLeft: padded && align === WN8SurroundingStatAlign.Left ? 32 : 0,
paddingRight:
padded && align === WN8SurroundingStatAlign.Right ? 32 : 0,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
fontWeight: 700,
}}
>
{value}
</span>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{label}
</span>
</div>
);
}
@@ -1,130 +0,0 @@
import {
getWN8Percentile,
Percentile,
type SupplementaryStats,
} from '@blitzkit/core';
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { PERCENTILE_COLORS } from '../PercentileIndicator/constants';
import { HeroStat } from './components/HeroStat';
import {
WN8SurroundingStat,
WN8SurroundingStatAlign,
} from './components/WN8SurroundingStat';
export interface AllStatsOverviewProps {
stats: {
battles: number;
wins: number;
damage_dealt: number;
damage_received: number;
survived_battles: number;
hits: number;
shots: number;
frags: number;
};
supplementaryStats: SupplementaryStats;
locale: Locale;
}
export function AllStatsOverview({
stats,
locale,
supplementaryStats,
}: AllStatsOverviewProps) {
const { strings } = translator(locale);
const percentile =
typeof supplementaryStats.WN8 === 'number'
? getWN8Percentile(supplementaryStats.WN8)
: Percentile.VeryBad;
const color = PERCENTILE_COLORS[percentile];
const heroStat =
typeof supplementaryStats.WN8 === 'number'
? supplementaryStats.WN8.toFixed(0)
: '--';
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: -64,
width: '100%',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
alignItems: 'flex-end',
flex: 1,
}}
>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.battles}
value={stats.battles.toLocaleString()}
align={WN8SurroundingStatAlign.Right}
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.winrate}
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.damage}
value={Math.round(
stats.damage_dealt / stats.battles,
).toLocaleString()}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.damage_ratio}
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
align={WN8SurroundingStatAlign.Right}
/>
</div>
<HeroStat
value={heroStat}
subtitle={strings.common.wn8_percentile[percentile]}
color={color}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
flex: 1,
}}
>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.tier}
value={
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
}
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.survival}
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
2,
)}%`}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.accuracy}
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.kills}
value={(stats.frags / stats.battles).toFixed(2)}
/>
</div>
</div>
);
}
@@ -1,16 +0,0 @@
import { type ReactNode } from "react";
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
className="session-tracker-root"
style={{ display: "flex", flexDirection: "column", gap: 8 }}
>
{children}
</div>
);
}
@@ -1,126 +0,0 @@
import {
Percentile,
TankClass,
TankType,
TREE_TYPE_ICONS,
} from "@blitzkit/core";
import { createColors } from "bepaint";
import {
type AccentColor,
type GrayColor,
PALETTES,
} from "../../../core/radix/radixColors";
import { RowStat } from "./RowStat";
export interface RowStatItem {
title: string;
current?: number | string;
career?: number | string;
delta?: number;
percentile?: Percentile;
}
interface RowProps {
displayType?: "tank" | "summary";
naked?: boolean;
title: string;
minimized?: boolean;
type?: TankType;
class?: TankClass;
color?: AccentColor | GrayColor;
stats: (RowStatItem | undefined)[];
}
export function Row({
color = "slate",
stats,
title,
minimized,
class: tankClass,
type,
naked = false,
displayType,
}: RowProps) {
const palette = PALETTES[`${color}Dark`];
const theme = createColors(palette);
return (
<div
className="session-tracker-row"
style={{
maxWidth: naked ? 600 : Infinity,
display: "flex",
flexDirection: "column",
borderRadius: 4,
backgroundColor: naked
? `${theme.appBackground2}c0`
: theme.appBackground2,
overflow: "hidden",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 8,
gap: 4,
backgroundColor: naked
? `${theme.componentInteractive}c0`
: theme.componentInteractive,
}}
>
{displayType === "tank" && tankClass !== undefined && (
<img
alt={TankClass[tankClass]}
src={
TREE_TYPE_ICONS[type ?? TankType.TANK_TYPE_RESEARCHABLE][
tankClass
]
}
style={{ width: 16, height: 16 }}
/>
)}
<span
style={{
color:
displayType === "tank"
? type === TankType.TANK_TYPE_COLLECTOR
? theme.textLowContrast_blue
: type === TankType.TANK_TYPE_PREMIUM
? theme.textLowContrast_amber
: theme.textHighContrast
: theme.textLowContrast,
fontWeight: 900,
fontSize: 16,
}}
>
{title}
</span>
</div>
<div
style={{
display: "flex",
paddingTop: 8,
paddingBottom: 8,
}}
>
{stats.map(
(row, index) =>
row && (
<RowStat
minimized={minimized}
key={index}
name={`${row.title} • ${row.career ?? "--"}`}
value={row.current ?? "--"}
delta={row.delta}
percentile={row.percentile}
/>
),
)}
</div>
</div>
);
}
@@ -1,89 +0,0 @@
import { Percentile } from "@blitzkit/core";
import { createColors } from "bepaint";
import {
type AccentColor,
type GrayColor,
PALETTES,
} from "../../../core/radix/radixColors";
import { PercentileIndicator } from "../../PercentileIndicator";
interface RowStatProps {
name: string;
value?: string | number;
delta?: number;
percentile?: Percentile;
minimized?: boolean;
color?: AccentColor | GrayColor;
}
export function RowStat({
name,
value,
delta,
percentile,
minimized = false,
color = "slate",
}: RowStatProps) {
const theme = createColors(PALETTES[`${color}Dark`]);
return (
<div
className="session-tracker-row-stat"
style={{
display: "flex",
flexDirection: "column",
gap: 4,
alignItems: "center",
justifyContent: "center",
flex: 1,
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
justifyContent: "center",
}}
>
{percentile === undefined && delta !== undefined && delta !== 0 && (
<img
alt={delta > 0 ? "Increase" : "Decrease"}
src={
(delta ?? 0) > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
style={{ width: 12, height: 12 }}
/>
)}
{percentile !== undefined && (
<PercentileIndicator percentile={percentile} />
)}
<span
style={{
color: theme.textHighContrast,
fontWeight: 700,
fontSize: 16,
textAlign: "center",
}}
>
{value}
</span>
</div>
{!minimized && (
<span
style={{
color: theme.textLowContrast,
textAlign: "center",
fontSize: 12,
}}
>
{name}
</span>
)}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Root';
export * from './components/Row';
export * from './components/RowStat';
@@ -1,26 +0,0 @@
import { type ReactNode } from "react";
import { theme } from "../stitches.config";
interface CommandWrapperProps {
children: ReactNode;
fat?: boolean;
}
export function CommandWrapper({ children, fat }: CommandWrapperProps) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
padding: `16px ${fat ? 0 : 16}px`,
gap: 16,
width: "100%",
color: theme.colors.textHighContrast,
backgroundColor: theme.colors.appBackground1,
position: "relative",
}}
>
{children}
</div>
);
}
@@ -1,21 +0,0 @@
interface DeltaCaretProps {
delta: number;
}
export function DeltaCaret({ delta }: DeltaCaretProps) {
return (
<img
alt={delta > 0 ? 'Increase' : 'Decrease'}
style={{
width: 16,
height: 16,
objectFit: 'contain',
}}
src={
delta > 0
? 'https://i.imgur.com/gjI4a5f.png'
: 'https://i.imgur.com/lLGZygN.png'
}
/>
);
}
-19
View File
@@ -1,19 +0,0 @@
import { asset } from '@blitzkit/core';
interface FlagProps {
nation: string;
}
export function Flag({ nation }: FlagProps) {
return (
<div
style={{
width: 22,
height: 22,
overflow: 'hidden',
}}
>
<img alt={nation} src={asset(`flags/circle/${nation}.webp`)} />
</div>
);
}
@@ -1,95 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { theme } from '../../../stitches.config';
import { PercentileIndicator } from '../../PercentileIndicator';
interface CardItem {
label: string;
value: string | number | undefined;
percentile?: Percentile;
}
interface CardProps {
items: CardItem[];
title: string;
}
export function Card({ items, title }: CardProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
flex: 1,
borderRadius: 4,
backgroundColor: theme.colors.componentInteractive,
padding: 8,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
}}
>
{title}
</span>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
{items.map((item) => (
<div
key={item.label}
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{item.label}
</span>
<div
style={{
flex: 1,
height: 1,
borderRadius: 0.5,
backgroundColor: theme.colors.borderNonInteractive,
}}
/>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
{item.percentile !== undefined && (
<PercentileIndicator percentile={item.percentile} />
)}
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{item.value === undefined || Number.isNaN(item.value)
? '--'
: item.value}
</span>
</div>
</div>
))}
</div>
</div>
);
}
@@ -1,17 +0,0 @@
interface RootProps {
children: React.ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
{children}
</div>
);
}
@@ -1,7 +0,0 @@
interface RowProps {
children: React.ReactNode;
}
export function Row({ children }: RowProps) {
return <div style={{ display: 'flex', gap: 8 }}>{children}</div>;
}
@@ -1,149 +0,0 @@
import {
type BlitzStats,
getWN8Percentile,
isNumber,
type SupplementaryStats,
} from '@blitzkit/core';
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { Card } from './components/Card';
import { Root } from './components/Root';
import { Row } from './components/Row';
interface GenericAllStatsProps {
stats: BlitzStats;
supplementaryStats?: SupplementaryStats;
locale: Locale;
}
export function GenericAllStats({
stats,
supplementaryStats,
locale,
}: GenericAllStatsProps) {
const { strings } = translator(locale);
const damageRatio = stats.damage_dealt / stats.damage_received;
const killsToDeathRatio =
stats.frags / (stats.battles - stats.survived_battles);
return (
<Root>
<Row>
<Card
title={strings.bot.commands.full_stats.body.cards.games.title}
items={[
{
label: strings.bot.commands.full_stats.body.cards.games.winrate,
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
},
{
label: strings.bot.commands.full_stats.body.cards.games.battles,
value: stats.battles,
},
{
label:
strings.bot.commands.full_stats.body.cards.games.wins_losses,
value: `${stats.wins} / ${stats.losses}`,
},
]}
/>
<Card
title={strings.bot.commands.full_stats.body.cards.efficiency.title}
items={[
{
label: strings.bot.commands.full_stats.body.cards.efficiency.wn8,
value:
supplementaryStats && isNumber(supplementaryStats.WN8)
? supplementaryStats.WN8!.toFixed(0)
: undefined,
percentile: isNumber(supplementaryStats?.WN8)
? getWN8Percentile(supplementaryStats!.WN8!)
: undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.efficiency.survival,
value: `${(
100 *
(stats.survived_battles / stats.battles)
).toFixed(2)}%`,
},
{
label:
strings.bot.commands.full_stats.body.cards.efficiency.accuracy,
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
},
]}
/>
</Row>
<Row>
<Card
title={strings.bot.commands.full_stats.body.cards.lethality.title}
items={[
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.average_damage,
value: (stats.damage_dealt / stats.battles).toFixed(0),
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.damage_per_tier,
value: supplementaryStats?.tier
? (
stats.damage_dealt /
stats.battles /
supplementaryStats.tier
).toFixed(0)
: undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.damage_ratio,
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.average_kills,
value: (stats.frags / stats.battles).toFixed(0),
},
]}
/>
<Card
title={strings.bot.commands.full_stats.body.cards.miscellaneous.title}
items={[
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_tier,
value: supplementaryStats?.tier?.toFixed(2),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_spots,
value: (stats.spotted / stats.battles).toFixed(2),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_xp,
value: (stats.xp / stats.battles).toFixed(0),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.kills_to_death_ratio,
value: isFinite(killsToDeathRatio)
? killsToDeathRatio.toFixed(2)
: undefined,
},
]}
/>
</Row>
</Root>
);
}
@@ -1,16 +0,0 @@
import { type GenericStat } from "..";
import { GenericStatsRow } from "./GenericStatsRow";
interface GenericStatsColumnProps {
stats: GenericStat[];
}
export function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
return (
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 8 }}>
{stats.map((stat) => (
<GenericStatsRow key={stat[0]} stat={stat} />
))}
</div>
);
}
@@ -1,46 +0,0 @@
import { type GenericStat } from "..";
import { theme } from "../../../stitches.config";
import { PercentileIndicator } from "../../PercentileIndicator";
interface GenericStatsRowProps {
stat: GenericStat;
}
export function GenericStatsRow({ stat }: GenericStatsRowProps) {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
whiteSpace: "nowrap",
maxWidth: 128,
overflow: "hidden",
}}
>
{stat[0]}
</span>
<span
style={{
color: theme.colors.textHighContrast,
fontWeight: 700,
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
{stat[2] !== undefined && <PercentileIndicator percentile={stat[2]} />}
{stat[1]}
</span>
</div>
);
}
@@ -1,34 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { GenericStatsColumn } from './components/GenericStatsColumn';
export type GenericStat =
| [string, string | number | undefined]
| [string, string | number | undefined, Percentile | undefined];
export interface GenericStatsProps {
stats: GenericStat[];
columnCount?: number;
}
export function GenericStats({ stats, columnCount = 2 }: GenericStatsProps) {
const filteredStats = stats.filter(([, value]) => value !== undefined);
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
const columns: GenericStat[][] = [];
filteredStats.forEach((stat, index) => {
const column = Math.floor(index / itemsPerRow);
if (!columns[column]) columns[column] = [];
columns[column].push(stat);
});
return (
<div style={{ display: 'flex', gap: 32 }}>
{columns.map((stats) => (
<GenericStatsColumn
key={stats.map((stat) => stat[0]).join('-')}
stats={stats}
/>
))}
</div>
);
}
@@ -1,11 +0,0 @@
export enum LineColor {
Red,
Green,
Blue,
White,
}
export enum LineStyle {
Solid,
Dashes,
DashesAndDots,
}
@@ -1,65 +0,0 @@
import { blue, green, red, slateDark } from '@radix-ui/colors';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
import { LineColor, LineStyle } from './constants';
const LINE_COLORS = {
[LineColor.Red]: red.red9,
[LineColor.Green]: green.green9,
[LineColor.Blue]: blue.blue9,
[LineColor.White]: slateDark.slate11,
};
const LINE_STYLES = {
[LineStyle.Solid]: undefined,
[LineStyle.Dashes]: '8, 8',
[LineStyle.DashesAndDots]: '1, 4, 8, 4',
};
export type PlotItem = [number, number];
type Plot = PlotItem[];
interface LineProps {
plot: Plot;
minY?: number;
maxY?: number;
color?: LineColor;
style?: LineStyle;
lineWidth?: number;
}
export function Line({
plot,
minY: providedMinY,
maxY: providedMaxY,
color = LineColor.Red,
style = LineStyle.Solid,
lineWidth = 2,
}: LineProps) {
const xs = plot.map(([x]) => x);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const ys = plot.map(([, y]) => y);
const minY = providedMinY ?? Math.min(...ys);
const maxY = providedMaxY ?? Math.max(...ys);
const rangeX = maxX - minX;
const rangeY = maxY - minY;
const points = plot
.map(
([x, y]) =>
`${(x - minX) * (GRAPH_WIDTH / rangeX)},${
GRAPH_HEIGHT - (y - minY) * (GRAPH_HEIGHT / rangeY)
}`,
)
.join(' ');
return (
<polyline
vector-effect="non-scaling-stroke"
fill="none"
stroke={LINE_COLORS[color]}
strokeWidth={lineWidth}
stroke-dasharray={LINE_STYLES[style]}
points={points}
/>
);
}
@@ -1,4 +0,0 @@
export enum MarginOrientation {
Vertical,
Horizontal,
}
@@ -1,49 +0,0 @@
import { times } from 'lodash-es';
import { theme } from '../../../../stitches.config';
import { type MarginInputProps } from '../Root';
import { MarginOrientation } from './constants';
interface MarginProps extends MarginInputProps {
verticalSeparations: number;
orientation?: MarginOrientation;
}
export function Margin({
verticalSeparations,
max,
min,
suffix,
precision = 0,
orientation = MarginOrientation.Vertical,
}: MarginProps) {
const isVertical = orientation === MarginOrientation.Vertical;
return (
<div
style={{
paddingLeft: isVertical ? 0 : 64,
width: isVertical ? 48 : '100%',
display: 'flex',
flexDirection: isVertical ? 'column' : 'row-reverse',
justifyContent: 'space-between',
}}
>
{times(verticalSeparations, (index) => (
<span
key={index}
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{`${(
(max - min) *
((verticalSeparations - (index + 1)) /
(verticalSeparations - 1)) +
min
).toFixed(precision)}${suffix ?? ''}`}
</span>
))}
</div>
);
}
@@ -1,2 +0,0 @@
export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320;
@@ -1,109 +0,0 @@
import { times } from "lodash-es";
import { type ReactNode } from "react";
import { theme } from "../../../../stitches.config";
import { Margin } from "../Margin";
import { MarginOrientation } from "../Margin/constants";
import { GRAPH_HEIGHT, GRAPH_WIDTH } from "./constants";
export interface MarginInputProps {
min: number;
max: number;
suffix?: string;
precision?: number;
}
interface RootProps {
children: ReactNode;
verticalMargin?: MarginInputProps;
horizontalMargin?: MarginInputProps;
separations?: number;
xMinLabel: string;
xMaxLabel: string;
xTitle?: string;
}
export function Root({
children,
verticalMargin,
horizontalMargin,
separations = 5,
xMinLabel,
xMaxLabel,
xTitle,
}: RootProps) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div
style={{
paddingLeft: 64,
display: "flex",
justifyContent: "space-between",
}}
>
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
{xMinLabel}
</span>
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
{xMaxLabel}
</span>
</div>
<div style={{ display: "flex", gap: 16 }}>
{verticalMargin && (
<Margin {...verticalMargin} verticalSeparations={separations} />
)}
<svg
viewBox={`0 0 ${GRAPH_WIDTH} ${GRAPH_HEIGHT}`}
preserveAspectRatio="none"
style={{
flex: 1,
backgroundColor: theme.colors.appBackground2,
borderRadius: 4,
border: theme.borderStyles.nonInteractive,
}}
>
{times(separations - 2, (index) => {
const y = GRAPH_HEIGHT * ((index + 1) / (separations - 1));
return (
<polyline
key={index}
vector-effect="non-scaling-stroke"
fill="none"
stroke={theme.colors.borderNonInteractive}
strokeWidth="2"
points={`0,${y}, ${GRAPH_WIDTH},${y}`}
/>
);
})}
{children}
</svg>
</div>
{horizontalMargin && (
<Margin
{...horizontalMargin}
verticalSeparations={separations}
orientation={MarginOrientation.Horizontal}
/>
)}
{xTitle !== undefined && (
<div
style={{
paddingLeft: 64,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{xTitle}
</span>
</div>
)}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Line';
export * from './components/Margin';
export * from './components/Root';
@@ -1,29 +0,0 @@
import { theme } from '../../../stitches.config';
interface GapProps {
message: string;
}
export function Gap({ message: number }: GapProps) {
return (
<div
style={{
display: 'flex',
padding: 8,
backgroundColor: theme.colors.componentNonInteractive,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{number.toLocaleString()}
</span>
</div>
);
}
@@ -1,171 +0,0 @@
import { type ComponentProps } from "react";
import { theme } from "../../../stitches.config";
interface ItemProps extends ComponentProps<"div"> {
position: number;
deltaPosition?: number;
score?: number;
deltaScore?: number;
nickname: string;
clan?: string;
highlight?: boolean;
}
export function Item({
position,
deltaPosition,
score,
deltaScore,
nickname,
clan,
highlight,
...props
}: ItemProps) {
return (
<div
{...props}
style={{
cursor: "pointer",
display: "flex",
padding: 8,
backgroundColor: highlight
? theme.colors.componentInteractive_blue
: theme.colors.componentInteractive,
borderRadius: 4,
alignItems: "center",
justifyContent: "center",
gap: 8,
}}
>
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{position.toLocaleString()}.
</span>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{nickname}
</span>
{clan && (
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
[{clan}]
</span>
)}
</div>
{deltaPosition !== undefined && deltaPosition !== 0 && (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
<img
alt={deltaPosition > 0 ? "Increase" : "Decrease"}
style={{
width: 8,
height: 8,
}}
src={
deltaPosition > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
/>
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{Math.abs(deltaPosition).toLocaleString()}
</span>
</div>
)}
{/* TODO: lol remove this spacer div for a better solution */}
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
width: 96,
justifyContent: "flex-end",
}}
>
{deltaScore !== 0 && deltaScore !== undefined && (
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{Math.abs(deltaScore).toLocaleString()}
</span>
)}
{deltaScore !== 0 && deltaScore !== undefined && (
<img
alt={deltaScore > 0 ? "Increase" : "Decrease"}
style={{
width: 8,
height: 8,
}}
src={
deltaScore > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
/>
)}
{score !== undefined && (
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{score.toLocaleString()}
</span>
)}
</div>
</div>
);
}
@@ -1,13 +0,0 @@
import { type ReactNode } from 'react';
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{children}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Gap';
export * from './components/Item';
export * from './components/Root';
@@ -1,5 +0,0 @@
export enum NoDataType {
BattlesInPeriod,
PlayersInPeriod,
TanksFound,
}
@@ -1,30 +0,0 @@
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { theme } from '../../stitches.config';
interface NoDataProps {
type:
| 'battles_in_period'
| 'players_in_period'
| 'tanks_found'
| 'battles_in_season';
locale: Locale;
}
export function NoData({ type, locale }: NoDataProps) {
const { strings } = translator(locale);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{strings.bot.common.no_data[type]}
</span>
</div>
);
}
@@ -1,26 +0,0 @@
import { Percentile } from '@blitzkit/core';
import {
amber,
blue,
green,
lime,
orange,
pink,
purple,
red,
slate,
teal,
} from '@radix-ui/colors';
export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9,
[Percentile.Bad]: red.red9,
[Percentile.BelowAverage]: orange.orange9,
[Percentile.Average]: amber.amber10,
[Percentile.AboveAverage]: lime.lime8,
[Percentile.Good]: green.green9,
[Percentile.VeryGood]: teal.teal9,
[Percentile.Great]: blue.blue9,
[Percentile.Unicum]: pink.pink9,
[Percentile.SuperUnicum]: purple.purple9,
};
@@ -1,19 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { PERCENTILE_COLORS } from './constants';
interface PercentileIndicatorProps {
percentile: Percentile;
}
export function PercentileIndicator({ percentile }: PercentileIndicatorProps) {
return (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: PERCENTILE_COLORS[percentile],
}}
/>
);
}
@@ -1,20 +0,0 @@
import { type ReactNode } from 'react';
interface ColumnProps {
children: ReactNode;
}
export function Column({ children }: ColumnProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
flex: 1,
}}
>
{children}
</div>
);
}
@@ -1,75 +0,0 @@
import { TankClass, TankType, TREE_TYPE_ICONS } from "@blitzkit/core";
import { theme } from "../../../../stitches.config";
export interface ItemProps {
tankClass?: TankClass;
name: string;
tankType: TankType;
}
const TREE_TYPE_COLOR = {
[TankType.TANK_TYPE_RESEARCHABLE]: "",
[TankType.TANK_TYPE_PREMIUM]: "_amber",
[TankType.TANK_TYPE_COLLECTOR]: "_blue",
} as const;
export function Item({
tankClass: tankType,
name,
tankType: treeType,
}: ItemProps) {
return (
<div
style={{
display: "flex",
height: 32,
paddingLeft: 8,
paddingRight: 8,
overflow: "hidden",
alignItems: "center",
borderRadius: 4,
backgroundColor:
theme.colors[`componentInteractive${TREE_TYPE_COLOR[treeType]}`],
}}
>
<div
style={{
display: "flex",
gap: 4,
flex: 1,
alignItems: "center",
justifyContent: "flex-start",
overflow: "hidden",
}}
>
{tankType && (
<img
alt={TankType[tankType]}
src={TREE_TYPE_ICONS[treeType][tankType]}
style={{ width: 14, height: 14 }}
/>
)}
<span
style={{
fontSize: 16,
whiteSpace: "nowrap",
color: theme.colors[`textLowContrast${TREE_TYPE_COLOR[treeType]}`],
}}
>
{name}
</span>
</div>
{/* {image && (
// TODO: remove hardcoded dimensions when satori fixes non-width unloadable images
<img
alt={name}
src={image}
width={106}
height={32}
style={{ objectFit: 'cover' }}
/>
)} */}
</div>
);
}
@@ -1,19 +0,0 @@
import { type ReactNode } from "react";
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 16,
}}
>
{children}
</div>
);
}
@@ -1,18 +0,0 @@
import { type ReactNode } from "react";
interface RowProps {
children: ReactNode;
}
export function Row({ children }: RowProps) {
return (
<div
style={{
display: "flex",
gap: 8,
}}
>
{children}
</div>
);
}
@@ -1,19 +0,0 @@
import { theme } from '../../../stitches.config';
interface TitleProps {
children: string;
}
export function Title({ children }: TitleProps) {
return (
<span
style={{
fontWeight: 900,
color: theme.colors.textHighContrast,
fontSize: 24,
}}
>
{children}
</span>
);
}
@@ -1,5 +0,0 @@
export * from './components/Column';
export * from './components/Item';
export * from './components/Root';
export * from './components/Row';
export * from './components/Title';
@@ -1,32 +0,0 @@
import type { TierWeightsRecord } from "@blitzkit/core";
import { theme } from "../stitches.config";
interface TierWeightsProps {
weights: TierWeightsRecord;
}
export function TierWeights({ weights }: TierWeightsProps) {
return (
<div style={{ display: "flex", gap: 8 }}>
{Object.entries(weights).map(([tier, weight]) => (
<div
key={tier}
style={{
backgroundColor: theme.colors.appBackground1_light,
flex: weight,
color: theme.colors.textHighContrast_light,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 16,
overflow: "hidden",
borderRadius: 8,
minWidth: 16,
}}
>
{tier}
</div>
))}
</div>
);
}
Loaded 100 of 828 files, more files were not shown because too many files have changed in this diff. Show more