Compare commits

...
261 Commits
Author SHA1 Message Date
TrèsAbhi d88f7ad539 fix: arrays 2026-03-20 19:51:27 -05:00
TrèsAbhi b5d61cebb3 fix: omit non-source files 2026-03-20 19:33:43 -05:00
TrèsAbhi 551108e047 feat: some more docs 2026-03-20 19:13:37 -05:00
TrèsAbhi 5feec9bf1e fix: types 2026-03-20 14:57:00 -05:00
TrèsAbhi 313691d3e9 feat: better gutter and headings 2026-03-20 14:52:06 -05:00
TrèsAbhi 950c99d3d7 feat: clean 2026-03-20 14:48:38 -05:00
TrèsAbhi 80cc74a945 feat: typescript types 2026-03-20 14:43:12 -05:00
TrèsAbhi 4e586253c0 feat: smaller heaings 2026-03-19 18:48:50 -05:00
TrèsAbhi 08980158e4 fix: tool banner alignment 2026-03-19 18:47:58 -05:00
TrèsAbhi 1e917c7b89 feat: big 2026-03-19 18:45:30 -05:00
TrèsAbhi 8d3678ddcb feat: macro names 2026-03-19 18:44:00 -05:00
TrèsAbhi 6a58c5ea1d feat: form fields 2026-03-19 18:31:00 -05:00
TrèsAbhi b9bbf22f2c feat: more decoration 2026-03-19 18:26:08 -05:00
TrèsAbhi c99ed0cb53 feat: types 2026-03-19 18:17:36 -05:00
TrèsAbhi cef18d4f64 feat: see macro 2026-03-19 17:17:23 -05:00
TrèsAbhi 824169fc25 feat: zooweemama 2026-03-19 13:26:34 -05:00
TrèsAbhi 841b5708ae feat: in-heading testing 2026-03-18 20:43:41 -05:00
TrèsAbhi 8ad110f73b test 2026-03-18 18:30:06 -05:00
TrèsAbhi 0869ea09c8 fix: gap 2026-03-18 18:03:47 -05:00
TrèsAbhi 0e57943d33 feat: cleaner api rendering 2026-03-15 23:29:33 -05:00
TrèsAbhi 8b01b7c192 feat: cleaner api rendering 2026-03-15 23:23:29 -05:00
TrèsAbhi a7bd671309 feat: tank card links 2026-03-15 22:42:13 -05:00
TrèsAbhi 406ad52c8d feat: flesh out popular tanks 2026-03-15 22:37:46 -05:00
tresabhi b6c4c4e852 Merge branch 'main' into ue 2026-03-11 16:53:56 -05:00
tresabhi 8394f2ec1e feat: logs 2026-03-11 16:51:32 -05:00
tresabhi f14e73d811 fix: alpha clipping 2026-03-11 16:50:18 -05:00
tresabhi f193d5c503 feat: port to linux 2026-03-11 16:43:58 -05:00
tresabhi 6a06cb17a3 Merge branch 'main' into ue 2026-03-06 14:11:24 -06:00
TrèsAbhi 427b189a34 New Crowdin updates (#217) 2026-03-04 17:42:01 -06:00
TrèsAbhi bf218b0975 Merge branch 'main' into ue 2026-02-21 22:36:19 -06:00
TrèsAbhi eab4266fd1 feat: logs 2026-02-21 22:27:48 -06:00
TrèsAbhi 86b4b63a7c feat: dead commands 2026-02-21 22:26:29 -06:00
TrèsAbhi 5412c479c8 Merge branch 'main' into ue 2026-02-18 19:19:21 -06:00
TrèsAbhi 547614fa89 fix: react versouins 2026-02-18 19:11:58 -06:00
TrèsAbhi e803d96f20 feat: lgos 2026-02-18 19:06:49 -06:00
TrèsAbhi 009caeeaf1 fix: left align filters on small screens 2026-02-18 19:00:09 -06:00
TrèsAbhi bfa80cd549 fix: optional teritery shell 2026-02-18 18:57:17 -06:00
TrèsAbhi 987c798b7e feat: increase max zoom distance 2026-02-18 18:55:38 -06:00
TrèsAbhi 8dbe7cb155 fix: short hand compare toggles 2026-02-18 18:54:23 -06:00
TrèsAbhi 053f1e3ac8 fix: ricochet visualizer doesn't drag whole page 2026-02-18 18:53:29 -06:00
TrèsAbhi 588c0a809b fix: static armor updates on thickness change 2026-02-18 18:48:59 -06:00
TrèsAbhi dde059b1f9 fix: remove debug logs 2026-02-18 18:46:41 -06:00
TrèsAbhi 102072746a fix: static armor show on deselect 2026-02-18 18:45:39 -06:00
TrèsAbhi 10d7fe1e75 feat: auto crop image 2026-02-18 18:38:40 -06:00
TrèsAbhi 4cc4e664df fix: high end consumables gives 33% extra not 70% 2026-02-18 18:22:56 -06:00
TrèsAbhi d5269e763b fix: transparency checks and depth writing 2026-02-18 18:20:37 -06:00
TrèsAbhi bae7ca2720 fix: turret angles round back to \pm 180deg 2026-02-18 18:09:05 -06:00
TrèsAbhi a0b81bfb83 bruh 2026-02-18 16:52:47 -06:00
TrèsAbhi 629e9f814e bruh 2026-02-18 16:49:08 -06:00
TrèsAbhi f39d1bab4e wait wait wait no no no 2026-02-18 16:45:06 -06:00
TrèsAbhi bf61cb17de typo 2026-02-18 16:36:42 -06:00
TrèsAbhi c8ba6b1eca feat: 1.29.12 2026-02-18 16:36:12 -06:00
dependabot[bot] 1d8c823703 build(deps): bump bytes from 1.10.1 to 1.11.1 in /packages/app/src-tauri (#212)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 09:57:06 -06:00
dependabot[bot] 3594c9a971 build(deps): bump time from 0.3.44 to 0.3.47 in /packages/app/src-tauri (#213)
Bumps [time](https://github.com/time-rs/time) from 0.3.44 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.44...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 09:56:52 -06:00
TrèsAbhi f478a08bd9 New Crowdin updates (#211)
* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Russian)
2026-02-13 07:25:48 -06:00
TrèsAbhi 115fa5acad Remove locale redirection logic
Removed locale redirection logic from LocaleRedirector component.
2026-02-12 07:14:11 -06:00
TrèsAbhi c24a2989fa Fix locale redirection logic in LocaleRedirector.astro 2026-02-12 06:31:43 -06:00
TrèsAbhi a6cfb522b3 fix: remove cue4parse 2026-02-11 15:22:30 -06:00
TrèsAbhi 12e645f6e2 fix: legacy redirector ensure props 2026-02-11 15:20:09 -06:00
TrèsAbhi ccb86ae824 feat: start migration to proto for popular tanks 2026-02-11 15:13:23 -06:00
TrèsAbhi 9a3f64fcd6 Refactor locale redirection logic in LocaleRedirector 2026-02-11 09:31:48 -06:00
TrèsAbhi 001709c25c Merge branch 'main' into ue 2026-02-10 08:58:21 -06:00
TrèsAbhi e50b8d11bf feat: comment for future 2026-02-03 15:20:10 -06:00
TrèsAbhi ea782af079 feat: popular tanks 2026-02-03 09:04:36 -06:00
TrèsAbhi 0ebf9f6c3d feat: tank icon api 2026-02-02 22:00:46 -06:00
TrèsAbhi 6fec467ec8 feat: blitzkit cue4parse branch 2026-02-02 21:53:48 -06:00
TrèsAbhi 8b0f1cdfed update skiasharp version to latest 2026-02-02 21:42:19 -06:00
TrèsAbhi f6008bd288 feat: image 2026-02-02 20:09:46 -06:00
TrèsAbhi 1776c8fc15 feat: capture some error 2026-02-02 19:57:15 -06:00
TrèsAbhi 99f41b9805 feat: exporting 2026-02-02 19:04:54 -06:00
TrèsAbhi d4f11fc52a make mount sync 2026-02-02 18:17:25 -06:00
TrèsAbhi 750aeb39c9 reinit cue4parse 2026-02-02 18:14:02 -06:00
TrèsAbhi 0f355bc2fc make mount truely syncronous 2026-02-02 18:07:20 -06:00
TrèsAbhi 6ebd3063ed fix: remove oodle sync 2026-02-02 17:37:36 -06:00
TrèsAbhi b070cad92a SynchronousOodleHelper 2026-02-02 17:35:02 -06:00
TrèsAbhi 204fbbff59 feat: some cleanup 2026-02-02 17:28:24 -06:00
TrèsAbhi 98e7760142 feat: some syncronouse stuff 2026-02-02 17:24:20 -06:00
TrèsAbhi 6c1c088d44 feat: custom provider 2026-02-02 17:08:37 -06:00
TrèsAbhi 0b7f717057 feat: cue4parse submodule 2026-02-01 21:34:03 -06:00
TrèsAbhi 728cb43f05 feat: reading some files 2026-02-01 21:05:38 -06:00
TrèsAbhi ff02f06cbc feat: tanksDirectoryNations 2026-02-01 19:34:36 -06:00
TrèsAbhi 7a817cbe8c feat: smooth curtains 2026-02-01 12:42:52 -06:00
TrèsAbhi 58e0876aa0 feat: curtains 2026-02-01 12:41:29 -06:00
TrèsAbhi f84f846e76 feat: scroll functionality 2026-02-01 12:31:51 -06:00
TrèsAbhi ddb869e767 feat: start of scroller 2026-02-01 12:24:55 -06:00
TrèsAbhi 015d457d23 feat: proper api 2026-02-01 00:14:58 -06:00
TrèsAbhi ba6a83a9cb fix: docs 2026-02-01 00:09:06 -06:00
TrèsAbhi 689985ed93 feat: delete workflows 2026-02-01 00:06:39 -06:00
TrèsAbhi bf9262f1f1 feat: some work on footer 2026-01-31 23:21:43 -06:00
TrèsAbhi 821bd363ad feat: more groups 2026-01-31 23:06:46 -06:00
TrèsAbhi 4a9fc65e71 feat: tanks section 2026-01-31 22:54:00 -06:00
TrèsAbhi 81877f5abd feat: tank cards 2026-01-31 22:32:59 -06:00
TrèsAbhi 01eed3e045 fix: remove unused class test 2026-01-31 21:46:16 -06:00
TrèsAbhi 7bc423b570 fix: stack overflow on closing wss 2026-01-31 21:46:05 -06:00
TrèsAbhi dba56f8946 feat: blitz provider 2026-01-27 08:59:34 -06:00
TrèsAbhi c1f765d5f7 feat: setup interop 2026-01-25 21:27:24 -06:00
TrèsAbhi 7f5b5b614f feat: setup game interface 2026-01-25 19:32:30 -06:00
TrèsAbhi 5cd149b2b9 feat: higher constrast low contrast 2026-01-25 19:21:07 -06:00
TrèsAbhi 0bc2d0aaf6 feat: home page tools 2026-01-25 16:13:36 -06:00
TrèsAbhi 1dc9a341d1 fix: inconsistentcies 2026-01-25 15:57:44 -06:00
TrèsAbhi 2ad1e71176 feat: revert navbar 2026-01-25 15:55:30 -06:00
TrèsAbhi ca6973f529 feat: update protos to pre-ut4 2026-01-23 10:25:45 -06:00
TrèsAbhi 323d624ec7 feat: idk bro 2026-01-18 18:38:26 -06:00
TrèsAbhi 49f84db473 feat: recently viewed 2026-01-18 17:55:38 -06:00
TrèsAbhi 7399c402af feat: ahhhhhhhhhh 2026-01-18 15:14:26 -06:00
TrèsAbhi cbfcb29691 feat: new navbar 2026-01-18 14:51:55 -06:00
TrèsAbhi ae616fbaf3 feat: cleaner popup 2026-01-18 14:12:20 -06:00
TrèsAbhi 35275d4aa8 fix: use animation 2026-01-17 13:54:19 -06:00
TrèsAbhi dda63830ec feat: glow 2026-01-17 13:50:53 -06:00
TrèsAbhi 79b4f05809 feat: start of popup 2026-01-17 13:36:17 -06:00
TrèsAbhi b50c47b744 feat: skeleton 2026-01-16 20:37:11 -06:00
TrèsAbhi f7c6fdd861 fix: onChange 2026-01-16 20:14:47 -06:00
TrèsAbhi 47b583821e fix: hydration 2026-01-16 20:13:56 -06:00
TrèsAbhi 369ac113fa feat: custom loader 2026-01-16 19:28:10 -06:00
TrèsAbhi fc4d8b5ad0 fix: some cleanup 2026-01-16 13:17:17 -06:00
TrèsAbhi 72e1cf7d6a feat: postcss 2026-01-16 13:11:23 -06:00
TrèsAbhi dd96556e2a Change Section import to named import 2026-01-15 14:43:34 -06:00
TrèsAbhi a7ee51526a fix: reset count on change 2026-01-15 14:36:52 -06:00
TrèsAbhi 610b59f4c6 feat: search functionality 2026-01-15 14:30:40 -06:00
TrèsAbhi 63ce327458 feat: avatars search bar ui 2026-01-15 14:13:34 -06:00
TrèsAbhi 6ec7a50320 feat: lint fixes 2026-01-15 13:58:34 -06:00
TrèsAbhi 60a29ef2e7 feat: fancier navbar 2026-01-15 13:48:37 -06:00
TrèsAbhi 52c3a0abe0 feat: number indexed 2026-01-15 12:58:05 -06:00
TrèsAbhi 5bbe5c2720 feat: count badge 2026-01-15 12:55:18 -06:00
TrèsAbhi c0c6a21e43 feat: groups 2026-01-14 13:58:53 -06:00
TrèsAbhi a7f93698a6 feat: tool banners 2026-01-14 13:30:12 -06:00
TrèsAbhi 1a18058228 feat: split hooks 2026-01-14 12:41:51 -06:00
TrèsAbhi 67024daa25 fix: some cleanup 2026-01-14 12:37:42 -06:00
TrèsAbhi de6137d1a5 feat: incremtal loading 2026-01-14 12:34:28 -06:00
TrèsAbhi 11e87bd519 feat: sort 2026-01-14 11:56:44 -06:00
TrèsAbhi 3b4e089756 fix: store trimmed only in cache 2026-01-14 11:48:55 -06:00
TrèsAbhi 4a6f5845f9 feat: shadows for outlines 2026-01-13 20:49:40 -06:00
TrèsAbhi e9684ce033 feat: navbar rewrite 2026-01-13 20:33:14 -06:00
TrèsAbhi 27abf1b0de feat: string hooks 2026-01-13 20:15:55 -06:00
TrèsAbhi f5f3f70e35 feat: client side game strings 2026-01-13 13:30:09 -06:00
TrèsAbhi 2d282a3261 feat: game strings api 2026-01-13 13:18:55 -06:00
TrèsAbhi a2d0d2d8c0 feat: card styles 2026-01-13 12:46:34 -06:00
TrèsAbhi ea412086d4 feat: start of avatars 2026-01-12 14:13:21 -06:00
TrèsAbhi 455679a981 feat: some footer 2026-01-12 14:00:13 -06:00
TrèsAbhi a21bc6174d feat: some navbar 2026-01-12 13:46:58 -06:00
TrèsAbhi fae1a89402 feat: some more apis 2026-01-12 13:20:08 -06:00
TrèsAbhi 5a1a0c59f1 feat: profile and backgroud apis 2026-01-12 13:12:11 -06:00
TrèsAbhi c98271f783 feat: darker input 2026-01-12 12:48:47 -06:00
TrèsAbhi 033660c2d9 fix: webp 2026-01-11 13:48:27 -06:00
TrèsAbhi 1a639c51e1 feat: update backgrounds 2026-01-11 13:46:54 -06:00
TrèsAbhi 1366579dd1 feat: some unification 2026-01-11 13:40:01 -06:00
TrèsAbhi f5ed2c2c40 feat: concurrency 2026-01-11 11:52:28 -06:00
TrèsAbhi 1598de9f5f feat: imageProxy 2026-01-11 11:51:02 -06:00
TrèsAbhi 6f47f5cc99 feat: bumps 2026-01-10 13:49:11 -06:00
TrèsAbhi 1117dcb935 feat: links 2026-01-10 13:19:59 -06:00
TrèsAbhi 01a58f9aa8 feat: finalize tool cards 2026-01-10 13:07:59 -06:00
TrèsAbhi 9096644d51 feat: cards 2026-01-10 12:43:39 -06:00
TrèsAbhi d42fc0e3f6 Merge branch 'main' into ue 2026-01-09 14:06:57 -06:00
TrèsAbhi 17d1526b6f Merge branch 'main' into ue 2026-01-09 14:05:43 -06:00
TrèsAbhi db8ac195d4 feat: start of cards 2026-01-09 14:04:59 -06:00
TrèsAbhi c5f5f69fd6 feat: more home page 2026-01-09 13:57:28 -06:00
TrèsAbhi 44203223fd feat: server side strings 2026-01-09 13:38:00 -06:00
TrèsAbhi bdabadcd49 feat: some more styles 2026-01-09 13:02:58 -06:00
TrèsAbhi 220e4388d4 feat: start on header 2026-01-08 20:39:44 -06:00
TrèsAbhi 51b2d1ce86 feat: cleaner output 2026-01-08 20:17:11 -06:00
TrèsAbhi f3c1825c57 fix: complete protos implementation 2026-01-08 20:16:01 -06:00
TrèsAbhi 359dfcd102 feat: move some 2026-01-08 20:06:14 -06:00
TrèsAbhi 649cdd893f feat: styles 2026-01-08 20:02:24 -06:00
TrèsAbhi 0cb974acf5 feat: scales 2026-01-07 20:36:55 -06:00
TrèsAbhi 9bca5dccc7 feat: start of new protos 2026-01-04 15:35:58 -06:00
TrèsAbhi d707f1cc5d Merge branch 'main' into ue 2026-01-04 15:23:43 -06:00
TrèsAbhi 4ed4395535 feat: start of strings 2026-01-03 13:29:15 -06:00
TrèsAbhi 0209d48475 feat: backgrounds 2026-01-02 20:17:24 -06:00
TrèsAbhi 437bee4a85 feat: proxy avatars 2026-01-02 20:04:19 -06:00
TrèsAbhi 4b8e73fa57 feat: cache 2026-01-02 14:26:05 -06:00
TrèsAbhi b96f925d9a feat: avatars 2026-01-02 14:20:29 -06:00
TrèsAbhi 1551dc50fd Merge branch 'main' into ue 2026-01-02 12:51:20 -06:00
TrèsAbhi daf4cef61c feat: mode apis 2025-12-28 12:20:45 -06:00
TrèsAbhi 496e127f64 Merge branch 'main' into ue 2025-12-28 12:11:40 -06:00
TrèsAbhi da1af66c77 feat: start of mechanics 2025-12-26 20:07:22 -06:00
TrèsAbhi 01409985a2 feat: list 2025-12-26 19:43:44 -06:00
TrèsAbhi a86ac0619a feat: stop working on ranking since a lot of tanks are broken 2025-12-26 19:37:52 -06:00
TrèsAbhi 385d165512 feat: optimize tankList 2025-12-26 19:21:14 -06:00
TrèsAbhi fcfc97b964 feat: start of all tanks 2025-12-26 14:59:09 -06:00
TrèsAbhi 53d05e73a6 feat: some docs 2025-12-26 14:21:04 -06:00
TrèsAbhi 40a1eb861d feat: meta section 2025-12-26 14:14:29 -06:00
TrèsAbhi 1d3cb95b89 fix: assert wotb server 2025-12-26 11:33:04 -06:00
TrèsAbhi 010303a818 feat: camo 2025-12-24 20:30:19 -06:00
TrèsAbhi 7281b9fed0 feat: power_to_weight 2025-12-24 20:10:06 -06:00
TrèsAbhi c5785c7ae6 feat: more controllers 2025-12-24 19:57:26 -06:00
TrèsAbhi 698731cb9a feat: generalize render chars 2025-12-24 19:48:01 -06:00
TrèsAbhi 99bae8f2c5 fix: array rendering 2025-12-24 19:39:26 -06:00
TrèsAbhi 5034ec1aa0 feat: terrain hardness 2025-12-24 19:37:21 -06:00
TrèsAbhi be821c1f8b Merge branch 'ue' of https://github.com/blitzkit/blitzkit into ue 2025-12-24 19:33:04 -06:00
TrèsAbhi a364b79a9b feat: autoreloader dom 2025-12-24 19:33:01 -06:00
TrèsAbhi 0aa6e1e79a Update deployment condition for GitHub Pages 2025-12-20 20:03:27 -06:00
TrèsAbhi d2a6379131 feat: abstracted meta 2025-12-20 19:10:11 -06:00
TrèsAbhi 07ce61d19f fix: depression 2025-12-20 19:05:42 -06:00
TrèsAbhi ad9f2d8609 feat: start of pitch yaw limits 2025-12-20 19:03:57 -06:00
TrèsAbhi 57702d8a43 fix: repo 2025-12-20 18:10:24 -06:00
TrèsAbhi 9a30af3818 fix: proper pitch limits 2025-12-20 10:26:25 -06:00
TrèsAbhi 23751fbdf3 feat: dynamic dispersion 2025-12-20 10:15:23 -06:00
TrèsAbhi db6d181f78 feat: use max stage 2025-12-20 09:59:26 -06:00
TrèsAbhi 56ca4dcc61 feat: more accents 2025-12-19 22:23:03 -06:00
TrèsAbhi d08b537721 feat: some styling 2025-12-19 21:25:55 -06:00
TrèsAbhi c1a2edb489 feat: baselayout 2025-12-19 17:55:26 -06:00
TrèsAbhi 164b7fa454 feat: api renderer 2025-12-19 17:50:59 -06:00
TrèsAbhi 428483cf4c fix: disable minifySyntax 2025-12-19 16:14:36 -06:00
TrèsAbhi 89f02eb399 minifySyntax 2025-12-19 16:11:26 -06:00
TrèsAbhi 4ce127516c minifyWhitespace 2025-12-19 16:09:27 -06:00
TrèsAbhi 6f4f487dfe minifyIdentifiers 2025-12-19 16:07:34 -06:00
TrèsAbhi 57dd60e804 feat: no minify 2025-12-19 16:01:58 -06:00
TrèsAbhi d24fd2250c feat: deploy on ue 2025-12-19 15:53:30 -06:00
TrèsAbhi e00bcdfc06 feat: latest node 2025-12-19 15:51:47 -06:00
TrèsAbhi 67dd204ee6 fix: typo 2025-12-19 15:48:07 -06:00
TrèsAbhi e130583fa4 fix: build:catalogitemaccesssor 2025-12-19 15:46:40 -06:00
TrèsAbhi 6ad20a0fcf feat: cron 2025-12-19 15:44:32 -06:00
TrèsAbhi 9fad644d7a feat: workflow 2025-12-19 15:42:38 -06:00
TrèsAbhi 8cb523dd06 fix: remove pwa 2025-12-19 15:35:50 -06:00
TrèsAbhi b50ee23a26 feat: re-install 2025-12-19 15:34:16 -06:00
TrèsAbhi a973cf699b fix: cleaner stage0 hack 2025-12-19 15:26:47 -06:00
TrèsAbhi 102abf49c4 fix: default isPump 2025-12-19 15:20:58 -06:00
TrèsAbhi 8929e503ea fix: pump times 2025-12-19 15:17:45 -06:00
TrèsAbhi a2a794eb74 fix: use order instead of group 2025-12-19 15:11:33 -06:00
TrèsAbhi 2e95f5dd95 fix: unit conversion 2025-12-19 15:09:46 -06:00
TrèsAbhi 23a42230cb feat: units 2025-12-19 15:04:35 -06:00
TrèsAbhi e66daed7ae feat: dpm 2025-12-19 14:51:18 -06:00
TrèsAbhi f2d0f68f07 feat: primary_armor 2025-12-19 14:34:17 -06:00
TrèsAbhi be4af0bf84 feat: implement some 2025-12-18 21:29:10 -06:00
TrèsAbhi 15172e8b33 fix: fetch correct stages 2025-12-18 17:00:06 -06:00
TrèsAbhi c6b7918525 feat: shells 2025-12-18 16:45:02 -06:00
TrèsAbhi 4f1c4421e5 feat: some more 2025-12-18 16:29:13 -06:00
TrèsAbhi 7b830f5a71 feat: start of more attrs 2025-12-18 16:12:01 -06:00
TrèsAbhi 6ce1135835 feat: rename a bit 2025-12-18 15:30:18 -06:00
TrèsAbhi 66eaaef6de feat: implement shell attr 2025-12-18 15:07:04 -06:00
TrèsAbhi 48fbf59036 feat: override common data 2025-12-18 13:20:35 -06:00
TrèsAbhi 0e0f4fbe9f fix: find att0 correctly 2025-12-18 13:16:20 -06:00
TrèsAbhi 6ba6e10108 feat: rename 2025-12-18 13:08:42 -06:00
TrèsAbhi cf8fa4ffb4 feat: remaining stagers 2025-12-18 12:59:12 -06:00
TrèsAbhi 2b0180dce2 Merge branch 'main' into ue 2025-12-18 12:45:10 -06:00
TrèsAbhi caee848a47 Merge branch 'main' into ue 2025-12-15 22:29:11 -06:00
TrèsAbhi d01b7cf707 feat: shells 2025-12-14 22:00:45 -06:00
TrèsAbhi 8921f84c38 feat: verbose 2025-12-14 21:41:26 -06:00
TrèsAbhi 6830df01fa penetration_groups_upgrades 2025-12-14 21:38:18 -06:00
TrèsAbhi 67da59ef41 feat: start of stages 2025-12-13 23:35:38 -06:00
TrèsAbhi 036cc9915d feat: start of atts 2025-12-13 23:10:51 -06:00
TrèsAbhi 57b6ba19f0 feat: rename 2025-12-13 22:15:48 -06:00
TrèsAbhi 32b5593d07 feat: lenient strings 2025-12-13 13:16:42 -06:00
TrèsAbhi 3b757562d2 Merge branch 'main' into ue 2025-12-10 22:52:49 -06:00
TrèsAbhi 4c8c2d0868 feat: auto pick localization entity 2025-12-05 21:35:58 -06:00
TrèsAbhi b2f266401f Merge branch 'main' into ue 2025-12-05 21:23:39 -06:00
TrèsAbhi 5541eca23d feat: start of derived statistics 2025-12-01 16:51:11 -06:00
TrèsAbhi 122d65840e feat: aggregateAttributes 2025-12-01 13:09:55 -06:00
TrèsAbhi ed4b72d85c feat: upgrade stages 2025-11-30 22:26:13 -06:00
TrèsAbhi 4033e8618f feat: useAwait 2025-11-30 22:12:46 -06:00
TrèsAbhi 42cada1811 feat: move and api 2025-11-30 21:06:17 -06:00
TrèsAbhi ff7ed213c8 feat: complete 2025-11-30 21:01:53 -06:00
TrèsAbhi a0367af06b feat: start of stats 2025-11-30 20:54:36 -06:00
TrèsAbhi 58c2965c53 Merge branch 'main' into ue 2025-11-30 20:27:42 -06:00
TrèsAbhi edae41409a feat: tank data 2025-11-29 14:22:32 -06:00
TrèsAbhi 383d3a36ef feat: load tank data 2025-11-28 14:21:26 -06:00
TrèsAbhi 6a958da390 feat: implement strings 2025-11-28 14:13:03 -06:00
TrèsAbhi ef154f99d5 feat: start of scraping 2025-11-28 13:51:17 -06:00
TrèsAbhi 4a20102de3 feat: base work 2025-11-28 13:19:15 -06:00
TrèsAbhi 4e170f2917 feat: port minimal files 2025-11-28 13:03:33 -06:00
TrèsAbhi a70d788f6b feat: init ue website 2025-11-28 13:00:41 -06:00
261 changed files with 8569 additions and 3443 deletions

No files matched your search

-82
View File
@@ -1,82 +0,0 @@
name: Assets
on:
push:
branches: main
workflow_dispatch:
inputs:
asset_repo:
type: choice
required: false
description: What asset repo to write to
options:
- blitzkit/assets
- blitzkit/assets-preview
- blitzkit/assets-dev
default: blitzkit/assets
asset_source:
type: string
required: false
description: "Archive URL or Steam to download (example: steam:APP_ID/DEPOT_ID or zip:URL_TO_ZIP)"
default: "steam:444200/444202"
schedule:
- cron: "0 2 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.asset_repo }}
cancel-in-progress: true
env:
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_DLC_CDN: ${{ secrets.WOTB_DLC_CDN }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_REPO: ${{ github.event.inputs.asset_repo != '' && github.event.inputs.asset_repo || secrets.PUBLIC_ASSET_REPO }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: true
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
PUBLIC_PATREON_CLIENT_ID: ${{ secrets.PUBLIC_PATREON_CLIENT_ID }}
PUBLIC_PATREON_REDIRECT_URI: ${{ secrets.PUBLIC_PATREON_REDIRECT_URI }}
PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
PUBLIC_NITROPAY_ID: ${{ secrets.PUBLIC_NITROPAY_ID }}
PUBLIC_DEBUG_MISSING_I18N: false
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_FAST_USER: blitzkit
PUBLIC_ASSET_FAST_REPO: assets
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
jobs:
build_assets:
name: Build assets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- run: bun run setup
- name: AppArmor hack
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
- name: Build assets
run: bun run build:assets --all-targets --source=${{ github.event.inputs.asset_source || 'steam:444200/444202' }}
-54
View File
@@ -1,54 +0,0 @@
name: Averages
on:
workflow_dispatch:
schedule:
- cron: "0 8 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Build averages
env:
PUBLIC_ASSET_BRANCH: main
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
run: bun run build:averages --production
-34
View File
@@ -1,34 +0,0 @@
name: Bot
on:
push:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build bot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Build bot
run: bun run build:bot
- name: Archive build
uses: actions/upload-artifact@v5
with:
name: bot
path: dist/bot
-36
View File
@@ -1,36 +0,0 @@
name: IDs
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 1"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Discover IDs
run: bun build:ids --production
-67
View File
@@ -1,67 +0,0 @@
name: Host
on:
push:
branches: main
schedule:
- cron: "0 */5 * * *"
workflow_dispatch:
release:
types: [created, published, released, edited]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- name: Download release assets
run: |
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/blitzkit/blitzkit/releases/latest \
| grep "browser_download_url.*bot.zip" \
| cut -d : -f 2,3 \
| tr -d \")
curl -L -o bot.zip $DOWNLOAD_URL
mkdir -p dist/bot
unzip bot.zip -d dist/bot
- name: Install sharp
run: bun init -y && bun add sharp
- name: Move directories
run: mv dist/bot/prisma .
- name: Run bot
run: bun run dist/bot/main.js
-46
View File
@@ -1,46 +0,0 @@
name: Rating Asia
on:
workflow_dispatch:
schedule:
- cron: "0 17 * * *"
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Generate leaderboard
run: bun build:ratingleaderboard --region=asia
-46
View File
@@ -1,46 +0,0 @@
name: Rating Europe
on:
workflow_dispatch:
schedule:
- cron: "0 23 * * *"
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Generate leaderboard
run: bun build:ratingleaderboard --region=eu
-46
View File
@@ -1,46 +0,0 @@
name: Rating North America
on:
workflow_dispatch:
schedule:
- cron: "0 5 * * *"
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: "true"
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Generate leaderboard
run: bun build:ratingleaderboard --region=com
-38
View File
@@ -1,38 +0,0 @@
name: Reviews
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 2
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- run: bun run setup
- name: Find reviews
run: bun run build:reviews
@@ -17,36 +17,30 @@ concurrency:
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
WOTB_SERVER: ${{ secrets.WOTB_SERVER }}
jobs:
build:
name: Build website
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v6
with:
node-version: latest
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GH_TOKEN }}
- uses: actions/configure-pages@v5
- run: bun run setup
- name: Build website
@@ -58,7 +52,7 @@ jobs:
path: dist/website
deploy-github-pages:
if: ${{ github.ref == 'refs/heads/main' }}
if: ${{ github.ref == 'refs/heads/ue' && github.repository == 'blitzkit/host-beta' }}
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: build
+1 -1
View File
@@ -22,7 +22,7 @@ depots
public/sitemap*
/firebase.json
target
# packages/core/src/protos/*.ts
packages/website-ue/src/protos/*.ts
/.vercel
.astro
.vercel
+3
View File
@@ -1,3 +1,6 @@
[submodule "packages/closed"]
path = packages/closed
url = https://github.com/blitzkit/closed.git
[submodule "packages/cue4parse"]
path = packages/cue4parse
url = https://github.com/FabianFG/CUE4Parse.git
+11
View File
@@ -17,6 +17,7 @@
"astrojs",
"astrolib",
"atgm",
"autocrop",
"Autoloader",
"Autoreloader",
"AYUV",
@@ -61,6 +62,7 @@
"Ddpf",
"Ddsd",
"deadrail",
"DEBUFF",
"debugflags",
"deburr",
"decalmask",
@@ -146,6 +148,7 @@
"noout",
"normalmap",
"oneof",
"Oodle",
"oopsies",
"Oopsy",
"opentest",
@@ -161,6 +164,7 @@
"playerachievements",
"playerinfo",
"playform",
"Playtest",
"pngjs",
"POSITIVEX",
"POSITIVEY",
@@ -177,6 +181,7 @@
"ratingleaderboard",
"raycaster",
"Redirector",
"REGEN",
"Relogin",
"researchable",
"Resvg",
@@ -190,11 +195,14 @@
"searchplayers",
"searchtanks",
"SHAREDEXP",
"Skia",
"SKLLD",
"sluggify",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
"stackable",
"startcase",
"statkit",
"statsfull",
@@ -218,6 +226,7 @@
"tungstentip",
"Turretless",
"TYPELESS",
"uasset",
"ubjson",
"Unicum",
"UNLOCALIZED",
@@ -227,6 +236,7 @@
"unrotate",
"unwrapper",
"userns",
"usmap",
"vercel",
"vickers",
"vitepress",
@@ -239,6 +249,7 @@
"winrate",
"wireframe",
"Woah",
"wooorm",
"WOTB",
"wotblitz",
"wotbreplay",
+1101 -477
View File
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
# BlitzKit 1.29.12
## Fixes
- BlitzKit no longer uses BlitzStars for tank averages for some commands
- BlitzKit Performance was created to completely replace BlitzStars but BlitzStars was still used for some older commands
- But since BlitzStars keeps having downtime, it has been completely removed
- Upgraded all dependencies to the latest versions
- The replay command is now properly localized across all languages
- Special Chinese characters are now properly handled
- Commands are now registered synchronously, allowing errors to reveal their exact origins
- Error handling now properly exists the process after logging the error
- The bot now uses Discord's new `clientReady` instead of the ambiguous `ready` event
- Both the armor and model extractors now use the new BlitzKit `SteamVFS`
+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.29.13
## Features
- The screenshot button now automatically crops your image so you don't have excessive blank space all around
- Increased the max zoom out distance in Tankopedia to 50m
## Fixes
- Turrets in Tankopedia now stay within a range of -180 and +180 degrees
- Tanks with transparency like the T34 and Herdbreaker are now properly rendered
- This will take a few days to roll out for all tanks (see screenshots below for the fixed models)
- High End Consumables now give a 33% boost instead of 70%
- Static Armor now properly deselects all irrelevant armor plates
- Removed a few debug loggers left behind
- Static Armor now properly rerenders armor plates when their thicknesses are changed
- The Ricochet Visualizer now stays in place when dragging around on mobile
- Reduced the text in the Tankopedia Compare segmented display to fit on smaller screens
- Sorting by tertiary shells no longer crashes for some tanks
- The Tank Filters are now left aligned on smaller screens for readability
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.29.14
## Changes
- BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API
- Disabled the following commands:
- `/stats`
- `/today`
- `/breakdown`
- `/evolution`
- `/full-stats`
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.29.15
## Fixes
- Tanks now use alpha clipping to render translucency, fixing a lot of "ghostly" looking tanks
+11 -8
View File
@@ -1,41 +1,42 @@
{
"name": "blitzkit",
"version": "1.29.11",
"version": "2.0.0-beta.2",
"license": "BlitzKit License",
"private": true,
"scripts": {
"build:game": "cd packages/game && bun run build",
"build:assets": "cd packages/scripts && bun run build:assets",
"build:averages": "bun --filter=@blitzkit/scripts run build:averages",
"build:bot": "cd packages/scripts && bun run build:bot",
"build:website": "cd packages/website && bun run build",
"build:website": "cd packages/website-ue && bun run build",
"build:prisma": "prisma generate",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"build:catalogitemaccesssor": "cd packages/scripts && bun run build:catalogitemaccesssor",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"build:app": "cd packages/app && bun run build",
"dev:bot": "bun run build:bot --dev && bun start:bot",
"dev:bot": "bun run build:bot --dev && bun start:bot --dev",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website && bun run dev",
"dev:website": "cd packages/website-ue && bun run dev",
"dev:app": "cd packages/app && bun run dev",
"dev": "bun run dev:website",
"dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
"setup:patches": "bun --filter=@blitzkit/scripts run setup:patches",
"start:website": "bun --filter=@blitzkit/website run start",
"start:website": "bun --filter=@blitzkit/website-ue run start",
"host:docs": "vitepress preview docs",
"lint": "tsc",
"lint:website": "cd packages/website && bun run lint",
"lint:website": "cd packages/website-ue && bun run lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"update": "yarn upgrade --latest",
"tunnel": "cloudflared tunnel --url http://localhost:3000",
"discoverids": "scripts/discoverIds.ts",
"build:protos": "cd packages/scripts && bun run build:protos",
"preview:website": "cd packages/website && bun run preview",
"preview:website": "cd packages/website-ue && bun run preview",
"build:reviews": "cd packages/scripts && bun build:reviews",
"build:ids": "cd packages/scripts && bun build:ids",
"prunestrings": "cd packages/scripts && bun prunestrings",
"setup": "bun install && bun run build:prisma && bun run build:protos && bun run build:proxyclient",
"setup": "bun install && bun run build:prisma && bun run build:protos && bun run build:proxyclient && bun run build:catalogitemaccesssor",
"patchnotes": "cd packages/scripts && bun run patchnotes"
},
"type": "module",
@@ -43,8 +44,10 @@
"packages/*"
],
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@prisma/client": "^6.18.0",
"prisma": "^6.18.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
}
}
+11 -11
View File
@@ -356,9 +356,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde",
]
@@ -2105,9 +2105,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-traits"
@@ -3907,30 +3907,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.44"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.24"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
+10 -9
View File
@@ -4,25 +4,26 @@
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/themes": "^3.2.1",
"@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.2",
"@types/react": "^19.2.14",
"adm-zip": "^0.5.16",
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.22.1",
"dotenv": "^17.2.2",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"fuzzysort": "^3.1.0",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.23",
"markdown-escape": "^2.0.0",
"pickleparser": "^0.2.1",
"prisma": "^6.17.1",
"prisma": "^6.19.2",
"protobufjs": "^7.5.4",
"react": "^19.2.0",
"satori": "^0.18.3",
"sharp": "^0.34.4"
"react": "^19.2.4",
"satori": "^0.18.4",
"sharp": "^0.34.5"
},
"private": true,
"scripts": {
+4 -4
View File
@@ -1,7 +1,7 @@
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import {
COMMANDS_RAW,
commandsArray,
type CommandRegistry,
} from "../events/interactionCreate";
@@ -35,7 +35,7 @@ export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
if (subcommand === "commands") {
return `${strings.bot.commands.about.subcommands.commands.body}${(
await Promise.all(COMMANDS_RAW)
await Promise.all(commandsArray)
)
.sort((a, b) =>
(a.command.name_localizations?.[interaction.locale] ??
@@ -43,7 +43,7 @@ export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
(b.command.name_localizations?.[interaction.locale] ??
b.command.name)
? -1
: 1
: 1,
)
.map(
(registry) =>
@@ -54,7 +54,7 @@ export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
registry.command.description_localizations?.[
interaction.locale
] ?? registry.command.description
}`
}`,
)
.join("\n")}`;
}
+33 -36
View File
@@ -1,49 +1,46 @@
import { getBlitzStarsLinkButton } from '../../core/blitzstars/getBlitzStarsLinkButton';
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 { buttonRefresh } from '../../core/discord/buttonRefresh';
import { commandToURL } from '../../core/discord/commandToURL';
import { createLocalizedCommand } from '../../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../../core/discord/getFiltersFromCommand';
import { resolvePeriodFromButton } from '../../core/discord/resolvePeriodFromButton';
import { resolvePeriodFromCommand } from '../../core/discord/resolvePeriodFromCommand';
import { resolvePlayerFromButton } from '../../core/discord/resolvePlayerFromButton';
import { resolvePlayerFromCommand } from '../../core/discord/resolvePlayerFromCommand';
import type { CommandRegistry } from '../../events/interactionCreate';
import { renderBreakdown } from './render';
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'),
createLocalizedCommand("breakdown"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
return [
...(await renderBreakdown(
player,
period,
filters,
interaction.locale,
)),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(player.region, player.id),
];
// 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) => {
+32 -27
View File
@@ -15,11 +15,16 @@ import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import { tankDefinitions } from "../../core/blitzkit/nonBlockingPromises";
import {
averageDefinitions,
tankDefinitions,
} from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import { filterStats, type StatFilters } from "../../core/blitzstars/filterStats";
import {
filterStats,
type StatFilters,
} from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import { blitzStarsTankAverages } from "../../core/blitzstars/tankAverages";
import type { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import type { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
@@ -31,11 +36,11 @@ export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale
locale: Locale,
) {
const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await blitzStarsTankAverages;
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);
@@ -53,14 +58,14 @@ export async function renderBreakdown(
}
filteredOrder.forEach((id) => {
const tankAveragesEntry = awaitedTankAverages[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.all, current)
? calculateWN8(tankAveragesEntry.mu, current)
: undefined;
const careerWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, career)
? calculateWN8(tankAveragesEntry.mu, career)
: undefined;
orderedCurrentStats.push(current);
@@ -69,65 +74,65 @@ export async function renderBreakdown(
orderedCareerWN8.push(careerWN8);
});
tankStats.forEach((tank) => {
const tankAveragesEntry = awaitedTankAverages[tank.tank_id];
const tankAveragesEntry = awaitedTankAverages.averages[tank.tank_id];
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, tank.all)
: undefined
? calculateWN8(tankAveragesEntry.mu, tank.all)
: undefined,
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0
0,
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0
0,
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0
0,
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0
0,
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0
0,
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0
0,
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0
0,
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0
0,
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0
0,
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0
0,
) / careerBattles;
const children: React.JSX.Element[] = [];
@@ -172,7 +177,7 @@ export async function renderBreakdown(
delta: currentDamage - careerDamage,
},
]}
/>
/>,
);
filteredOrder.forEach((id, index) => {
@@ -206,7 +211,7 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
@@ -230,24 +235,24 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(
current.damage_dealt / current.battles
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles
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)
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
+41 -48
View File
@@ -5,8 +5,6 @@ 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 { resolveTankId } from "../core/blitz/resolveTankId";
import { getBlitzStarsLinkButton } from "../core/blitzstars/getBlitzStarsLinkButton";
import { getPlayerHistories } from "../core/blitzstars/getPlayerHistories";
import { getTankHistories } from "../core/blitzstars/getTankHistories";
import { addPeriodSubCommands } from "../core/discord/addPeriodSubCommands";
@@ -14,20 +12,12 @@ 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 { buttonRefresh } from "../core/discord/buttonRefresh";
import { commandToURL } from "../core/discord/commandToURL";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getCustomPeriodParams } from "../core/discord/getCustomPeriodParams";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import {
type ResolvedPeriod,
resolvePeriodFromCommand,
} from "../core/discord/resolvePeriodFromCommand";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import {
type ResolvedPlayer,
resolvePlayerFromCommand,
} from "../core/discord/resolvePlayerFromCommand";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
type EvolutionStatType = "player" | "tank";
@@ -37,7 +27,7 @@ async function render(
{ start, end, name }: ResolvedPeriod,
type: "player" | "tank",
tankId: number | null,
locale: Locale
locale: Locale,
) {
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
@@ -61,7 +51,7 @@ async function render(
[
history.all.battles,
(history.all.wins / history.all.battles) * 100,
] as Graph.PlotItem
] as Graph.PlotItem,
);
const times = histories.map((history) => history.last_battle_time);
const minTime = Math.min(...times);
@@ -115,7 +105,7 @@ export const evolutionCommand = new Promise<CommandRegistry>(
group: "player",
modify(option: SlashCommandSubcommandGroupBuilder) {
addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices)
option.addStringOption(addUsernameChoices),
);
},
},
@@ -125,43 +115,46 @@ export const evolutionCommand = new Promise<CommandRegistry>(
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameChoices)
.addStringOption(addUsernameChoices),
);
},
},
]),
async handler(interaction) {
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,
});
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
return [
await render(
player,
period,
commandGroup,
tankId,
interaction.locale
),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(
player.region,
player.id,
tankId ?? undefined
),
];
// 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) => {
@@ -181,9 +174,9 @@ export const evolutionCommand = new Promise<CommandRegistry>(
period,
commandGroup,
parseInt(url.searchParams.get("tankId") ?? "0") || null,
interaction.locale
interaction.locale,
);
},
});
}
},
);
+40 -47
View File
@@ -3,38 +3,28 @@ import {
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 { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import { getStatsInPeriod } from '../core/blitzstars/getStatsInPeriod';
import { addPeriodicFilterOptions } from '../core/discord/addPeriodicFilterOptions';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteTanks } from '../core/discord/autocompleteTanks';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import { commandToURL } from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import { resolvePeriodFromButton } from '../core/discord/resolvePeriodFromButton';
import {
type ResolvedPeriod,
resolvePeriodFromCommand,
} from '../core/discord/resolvePeriodFromCommand';
import { resolvePlayerFromButton } from '../core/discord/resolvePlayerFromButton';
import {
type ResolvedPlayer,
resolvePlayerFromCommand,
} from '../core/discord/resolvePlayerFromCommand';
import type { CommandRegistry } from '../events/interactionCreate';
} 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,
@@ -43,7 +33,7 @@ async function render(
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(
region,
@@ -86,25 +76,28 @@ export const fullStatsCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand('full-stats'),
createLocalizedCommand("full-stats"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
return Promise.all([
render(player, period, filters, interaction.locale),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
// 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) => {
+92 -77
View File
@@ -5,28 +5,29 @@ import {
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';
} 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 { blitzStarsTankAverages } from '../core/blitzstars/tankAverages';
import { buttonLink } from '../core/discord/buttonLink';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { type CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
} 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: "error"; data: unknown; error: { message: string } }
| {
status: 'ok';
status: "ok";
data: {
download_url: string;
view_url: string;
@@ -209,9 +210,9 @@ function stat(value: string | number, flex = 2) {
<div
style={{
flex,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
@@ -227,15 +228,15 @@ async function playerListing(
recordingPlayer = false,
) {
const { unwrap } = translator(locale);
const awaitedTankAverages = await blitzStarsTankAverages;
const awaitedTankAverages = await averageDefinitions;
const awaitedTankDefinitions = await tankDefinitions;
const tank = awaitedTankDefinitions.tanks[player.vehicle_descr];
const blockAccent = recordingPlayer ? '_amber' : '';
const blockAccent = recordingPlayer ? "_amber" : "";
const healthAccent = protagonistTeam
? recordingPlayer
? '_amber'
: '_green'
: '_red';
? "_amber"
: "_green"
: "_red";
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
// TODO: 💀
@@ -245,11 +246,11 @@ async function playerListing(
)[0];
const totalHealth = turret.health + tank.health;
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
const tankAveragesEntry = awaitedTankAverages[player.vehicle_descr];
const tankAveragesEntry = awaitedTankAverages.averages[player.vehicle_descr];
const wn8 =
tankAveragesEntry === undefined
? -1
: calculateWN8(tankAveragesEntry.all, {
: calculateWN8(tankAveragesEntry.mu, {
battles: 1,
damage_dealt: player.damage_made,
dropped_capture_points: player.base_defend_points,
@@ -262,8 +263,8 @@ async function playerListing(
<div
key={player.entity_id}
style={{
display: 'flex',
alignItems: 'center',
display: "flex",
alignItems: "center",
gap: 8,
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
@@ -271,11 +272,11 @@ async function playerListing(
>
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
flex: 1,
overflow: 'hidden',
alignItems: 'center',
overflow: "hidden",
alignItems: "center",
}}
>
<img
@@ -284,22 +285,22 @@ async function playerListing(
style={{
width: 32,
height: 32,
objectFit: 'contain',
objectFit: "contain",
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
// align
}}
>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<span
@@ -324,23 +325,23 @@ async function playerListing(
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
whiteSpace: 'nowrap',
whiteSpace: "nowrap",
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ''}
{player.clan_tag ? ` [${player.clan_tag}]` : ""}
</span>
</div>
</div>
<div
style={{
display: 'flex',
display: "flex",
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat(wn8 === -1 ? '--' : Math.round(wn8).toLocaleString())}
{stat(wn8 === -1 ? "--" : Math.round(wn8).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
@@ -349,15 +350,15 @@ async function playerListing(
style={{
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: 'flex',
display: "flex",
width: 4,
height: '100%',
transform: 'scaleY(-1)',
height: "100%",
transform: "scaleY(-1)",
}}
>
<div
style={{
width: '100%',
width: "100%",
height: `${healthLeft * 100}%`,
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
@@ -373,8 +374,8 @@ function Header() {
<div
style={{
padding: 8,
display: 'flex',
alignItems: 'center',
display: "flex",
alignItems: "center",
gap: 8,
}}
>
@@ -382,16 +383,16 @@ function Header() {
<div
style={{
display: 'flex',
display: "flex",
flex: 1,
}}
>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -406,9 +407,9 @@ function Header() {
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -423,9 +424,9 @@ function Header() {
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -440,9 +441,9 @@ function Header() {
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -460,28 +461,42 @@ function Header() {
}
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand('replay').addAttachmentOption((option) =>
option.setName('file').setDescription('Replay file').setRequired(true),
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 file = {
// url: 'https://cdn.discordapp.com/ephemeral-attachments/1232434891652861972/1232781496016834581/20240328_1530__TresAbhi_M60_1157057448155406708.wotbreplay?ex=662ab4bc&is=6629633c&hm=c784cd10e80232350ddaf291fa987b879c4d0a8083b78298c9eee4fa0d507f6c&',
// };
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') {
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 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;
@@ -493,7 +508,7 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
await getClanAccountInfo(
idToRegion(replayData.protagonist),
replayData.protagonist,
['clan'],
["clan"],
)
)?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
@@ -506,22 +521,22 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
image={clanImage}
description={`${
replayData.protagonist_team === replayData.winner_team
? 'Victory'
: 'Defeat'
? "Victory"
: "Defeat"
} • ${awaitedMapDefinitions.maps[replayData.map_id].name}`}
/>
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
}}
>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
@@ -547,8 +562,8 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
@@ -569,8 +584,8 @@ export const replayCommand = new Promise<CommandRegistry>((resolve) => {
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, 'WoT Inspector'),
buttonLink(submissionData.data.download_url, 'Download'),
buttonLink(submissionData.data.view_url, "WoT Inspector"),
buttonLink(submissionData.data.download_url, "Download"),
];
},
});
+22 -29
View File
@@ -6,35 +6,25 @@ 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 { getBlitzStarsLinkButton } from "../core/blitzstars/getBlitzStarsLinkButton";
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { buttonRefresh } from "../core/discord/buttonRefresh";
import { commandToURL } from "../core/discord/commandToURL";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getCustomPeriodParams } from "../core/discord/getCustomPeriodParams";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { getFiltersFromCommand } from "../core/discord/getFiltersFromCommand";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import {
type ResolvedPeriod,
resolvePeriodFromCommand,
} from "../core/discord/resolvePeriodFromCommand";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import {
type ResolvedPlayer,
resolvePlayerFromCommand,
} from "../core/discord/resolvePlayerFromCommand";
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
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
@@ -44,7 +34,7 @@ async function render(
id,
start,
end,
locale
locale,
);
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
const filterDescriptions = await filtersToDescription(filters, locale);
@@ -73,24 +63,27 @@ export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("stats"),
(option) => option.addStringOption(addUsernameChoices)
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
return Promise.all([
render(player, period, filters, interaction.locale),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
// 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) => {
+33 -36
View File
@@ -1,50 +1,47 @@
import { gameDefinitions } from '../core/blitzkit/nonBlockingPromises';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import { addFilterOptions } from '../core/discord/addFilterOptions';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteTanks } from '../core/discord/autocompleteTanks';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import { commandToURL } from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import { resolvePeriodFromButton } from '../core/discord/resolvePeriodFromButton';
import { resolvePeriodFromCommand } from '../core/discord/resolvePeriodFromCommand';
import { resolvePlayerFromButton } from '../core/discord/resolvePlayerFromButton';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { type CommandRegistry } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown/render';
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'),
createLocalizedCommand("today"),
awaitedGameDefinitions.nations,
).addStringOption(addUsernameChoices),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(
player.region,
interaction,
'today',
);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction, true),
...filters,
});
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
return [
...(await renderBreakdown(player, period, filters, interaction.locale)),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(player.region, player.id),
];
// 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) => {
@@ -1,11 +1,13 @@
import {
fetchAverageDefinitions,
fetchDiscoveredIdsDefinitions,
fetchGameDefinitions,
fetchMapDefinitions,
fetchTankDefinitions,
fetchTankNames,
} from '@blitzkit/core';
} from "@blitzkit/core";
export const averageDefinitions = fetchAverageDefinitions();
export const tankDefinitions = fetchTankDefinitions();
export const gameDefinitions = fetchGameDefinitions();
export const tankNames = fetchTankNames();
+26 -18
View File
@@ -1,29 +1,37 @@
import { usersDatabase } from '../db/users';
import { client } from '../discord/client';
import { usersDatabase } from "../db/users";
import { client } from "../discord/client";
const EXIT_EVENTS = [
'beforeExit',
'exit',
'SIGINT',
'SIGTERM',
'SIGUSR1',
'SIGUSR2',
];
const EXIT_EVENTS = ["SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2"];
async function cleanup() {
console.log('Cleaning up...');
let cleaningUp = false;
await Promise.all([usersDatabase.$disconnect(), client.destroy()]);
async function cleanup(signal: string) {
if (cleaningUp) return;
cleaningUp = true;
console.log('Gracefully exiting...');
console.log(`Cleaning up... (${signal})`);
try {
await Promise.all([usersDatabase.$disconnect(), client.destroy()]);
} catch (err) {
console.error("Cleanup error:", err);
}
console.log("Gracefully exiting...");
EXIT_EVENTS.forEach((event) => process.off(event, cleanup));
process.exit(0);
}
export function registerProcesses() {
process.on('uncaughtException', console.error);
client.on('error', console.error);
process.on("uncaughtException", (err) => {
console.error("uncaughtException:", err);
});
EXIT_EVENTS.forEach((event) => process.on(event, cleanup));
process.on("unhandledRejection", (err) => {
console.error("unhandledRejection:", err);
});
client.on("error", console.error);
EXIT_EVENTS.forEach((event) => process.on(event, () => cleanup(event)));
}
+15 -11
View File
@@ -3,8 +3,10 @@ import type { SupplementaryStats } from "@blitzkit/core/src/blitz/getAccountInfo
import { calculateWN8 } from "@blitzkit/core/src/statistics/calculateWN8";
import type { BlitzStats } from "@blitzkit/core/src/statistics/compositeStats";
import { sumAllStats } from "@blitzkit/core/src/statistics/sumAllStats";
import { tankDefinitions } from "../blitzkit/nonBlockingPromises";
import { blitzStarsTankAverages } from "./tankAverages";
import {
averageDefinitions,
tankDefinitions,
} from "../blitzkit/nonBlockingPromises";
export interface StatFilters {
nation?: string;
@@ -16,13 +18,13 @@ export interface StatFilters {
export async function filterStats(
{ diff, order }: DiffedTankStats,
filters: StatFilters
filters: StatFilters,
) {
// tank, if provided, takes priority over all other filters
if (filters.tank) filters = { tank: filters.tank };
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await blitzStarsTankAverages;
const awaitedTankAverages = await averageDefinitions;
const filteredOrder = order.filter((id) => {
const entry = awaitedTankDefinitions.tanks[id];
@@ -44,26 +46,28 @@ export async function filterStats(
const stats = sumAllStats(filteredOrder.map((id) => diff[id]));
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id] ? accumulator + diff[id].battles : accumulator,
0
awaitedTankAverages.averages[id]
? accumulator + diff[id].battles
: accumulator,
0,
);
const battlesOfTanksWithTankDefinition = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankDefinitions.tanks[id]
? accumulator + diff[id].battles
: accumulator,
0
0,
);
const supplementary = {
WN8:
filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id]
awaitedTankAverages.averages[id]
? accumulator +
calculateWN8(awaitedTankAverages[id].all, diff[id]) *
calculateWN8(awaitedTankAverages.averages[id].mu, diff[id]) *
diff[id].battles
: accumulator,
0
0,
) / battlesOfTanksWithAverages,
tier:
filteredOrder.reduce<number>(
@@ -72,7 +76,7 @@ export async function filterStats(
? accumulator +
awaitedTankDefinitions.tanks[id]!.tier * diff[id].battles
: accumulator,
0
0,
) / battlesOfTanksWithTankDefinition,
} satisfies SupplementaryStats;
@@ -1,101 +0,0 @@
import { BlitzStats } from '@blitzkit/core';
import { context } from '@blitzkit/core/src/blitzkit/context';
interface SpecialStats {
winrate: number;
damageRatio: number;
kdr: number;
damagePerBattle: number;
killsPerBattle: number;
hitsPerBattle: number;
spotsPerBattle: number;
wpm: number;
dpm: number;
kpm: number;
hitRate: number;
survivalRate: number;
}
export type Percentiles = [
number,
number,
number,
number,
number,
number,
number,
number,
number,
];
export interface SpecialPercentiles {
winRate: Percentiles;
damageRatio: Percentiles;
kdr: Percentiles;
damagePerBattle: Percentiles;
killsPerBattle: Percentiles;
hitsPerBattle: Percentiles;
spotsPerBattle: Percentiles;
wpm: Percentiles;
dpm: Percentiles;
kpm: Percentiles;
hitRate: Percentiles;
survivalRate: Percentiles;
}
export interface MeanSd {
winrateMean: number;
winrateSd: number;
hitRateMean: number;
hitRateSd: number;
survivalRateMean: number;
survivalRateSd: number;
damageRatioMean: number;
damageRatioSd: number;
kdrMean: number;
kdrSd: number;
dpbMean: number;
dpbSd: number;
kpbMean: number;
kpbSd: number;
hpbMean: number;
hpbSd: number;
spbMean: number;
spbSd: number;
}
export interface IndividualTankAverage {
_id: number;
battle_life_time: number;
total_battle_life_time: number;
number_of_players: number;
tank_id: number;
all: BlitzStats;
special: SpecialStats;
percentiles: SpecialPercentiles;
percentiles_player_count: number;
meanSd: MeanSd;
shotEff: number;
last_update: number;
}
export type BlitzStarsTankAverages = Record<number, IndividualTankAverage>;
export const blitzStarsTankAverages = fetch(
'https://www.blitzstars.com/api/tankaverages.json',
)
.then((response) => response.json())
.then((json) => {
if (context === 'server') {
const partialTankAverages: BlitzStarsTankAverages = {};
(json as IndividualTankAverage[]).forEach((individualTankAverage) => {
partialTankAverages[individualTankAverage.tank_id] =
individualTankAverage;
});
return partialTankAverages;
} else {
return json as BlitzStarsTankAverages;
}
});
+5 -5
View File
@@ -1,9 +1,9 @@
import { Client, GatewayIntentBits } from 'discord.js';
import { interactionCreate } from '../../events/interactionCreate';
import { ready } from '../../events/ready';
import { Client, GatewayIntentBits } from "discord.js";
import { interactionCreate } from "../../events/interactionCreate";
import { ready } from "../../events/ready";
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('interactionCreate', interactionCreate);
.on("clientReady", ready)
.on("interactionCreate", interactionCreate);
@@ -22,11 +22,11 @@ export function createLocalizedCommand(
command: string,
sub?:
| CreateLocalizedCommandSubcommand[]
| CreateLocalizedCommandSubcommandGroup[]
| CreateLocalizedCommandSubcommandGroup[],
) {
const commandPathItem = command.replaceAll(
"-",
"_"
"_",
) as keyof BlitzKitStrings["bot"]["commands"];
const { strings } = translator(Locale.EnglishUS);
const slashCommand = new SlashCommandBuilder()
@@ -35,14 +35,14 @@ export function createLocalizedCommand(
localizationObject(
(strings) => strings.bot.commands[commandPathItem].name,
undefined,
true
)
true,
),
)
.setDescription(strings.bot.commands[commandPathItem].description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.commands[commandPathItem].description
)
(strings) => strings.bot.commands[commandPathItem].description,
),
);
if (sub) {
@@ -56,7 +56,7 @@ export function createLocalizedCommand(
}
).subcommands[subItem.subcommand].name,
undefined,
true
true,
);
const subcommandDescriptionLocalizations = localizationObject(
(strings) =>
@@ -64,7 +64,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
subcommands: Record<string, { description: string }>;
}
).subcommands[subItem.subcommand].description
).subcommands[subItem.subcommand].description,
);
slashCommand.addSubcommand((option) => {
@@ -78,7 +78,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
subcommands: Record<string, { description: string }>;
}
).subcommands[subItem.subcommand].description
).subcommands[subItem.subcommand].description,
)
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
});
@@ -91,7 +91,7 @@ export function createLocalizedCommand(
}
).groups[subItem.group].name,
undefined,
true
true,
);
const subcommandDescriptionLocalizations = localizationObject(
(strings) =>
@@ -99,7 +99,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
groups: Record<string, { description: string }>;
}
).groups[subItem.group].description
).groups[subItem.group].description,
);
slashCommand.addSubcommandGroup((option) => {
@@ -113,7 +113,7 @@ export function createLocalizedCommand(
strings.bot.commands[commandPathItem] as unknown as {
groups: Record<string, { description: string }>;
}
).groups[subItem.group].description
).groups[subItem.group].description,
)
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
});
@@ -1,10 +1,10 @@
import { AutocompleteInteraction, CacheType } from 'discord.js';
import { commands } from '..';
import { AutocompleteInteraction, CacheType } from "discord.js";
import { commands } from "..";
export async function handleAutocomplete(
interaction: AutocompleteInteraction<CacheType>,
) {
const command = (await commands)[interaction.commandName];
const command = commands[interaction.commandName];
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
@@ -1,13 +1,13 @@
import { ButtonInteraction, CacheType } from 'discord.js';
import { commands } from '..';
import { normalizeInteractionReturnable } from '../../../core/discord/normalizeInteractionReturnable';
import { ButtonInteraction, CacheType } from "discord.js";
import { commands } from "..";
import { normalizeInteractionReturnable } from "../../../core/discord/normalizeInteractionReturnable";
export async function handleButton(interaction: ButtonInteraction<CacheType>) {
await interaction.deferUpdate();
const url = new URL(`https://example.com/${interaction.customId}`);
const commandName = url.pathname.split('/')[1];
const button = (await commands)[commandName]?.button;
const commandName = url.pathname.split("/")[1];
const button = commands[commandName]?.button;
if (!button) {
throw new Error(`Button handler not found for "${commandName}"`);
@@ -16,7 +16,7 @@ export async function handleButton(interaction: ButtonInteraction<CacheType>) {
if (interaction.message.interaction?.user.id !== interaction.user.id) {
interaction.followUp({
ephemeral: true,
content: 'You are not the author of this command.',
content: "You are not the author of this command.",
});
return;
}
@@ -17,12 +17,11 @@ import { translator } from "../../../core/localization/translator";
import type { Writeable } from "../../../types/writable";
export async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>
interaction: ChatInputCommandInteraction<CacheType>,
) {
await interaction.deferReply();
const awaitedCommands = await commands;
const registry = awaitedCommands[interaction.commandName];
const registry = commands[interaction.commandName];
try {
const returnable = await registry.handler(interaction);
@@ -36,7 +35,7 @@ export async function handleChatInputCommand(
if (typeof object === "string") {
chunks.push([object]);
} else chunks.at(-1)?.push(object!);
}
},
);
let index = 0;
@@ -59,7 +58,7 @@ export async function handleChatInputCommand(
} else if (psa.data.type === "image") {
if (!reply.files) reply.files = [];
(reply.files as Writeable<typeof reply.files>).push(
new AttachmentBuilder(psa.data.image)
new AttachmentBuilder(psa.data.image),
);
reply.content = reply.content ?? psa.data.title;
}
@@ -72,7 +71,7 @@ export async function handleChatInputCommand(
(
reply.components[0] as ActionRowBuilder<ButtonBuilder>
).addComponents(
psa.data.links.map(({ title, url }) => buttonLink(url, title))
psa.data.links.map(({ title, url }) => buttonLink(url, title)),
);
}
}
@@ -103,7 +102,7 @@ export async function handleChatInputCommand(
} else if (psa.data.type === "image") {
if (!followUp.files) followUp.files = [];
(followUp.files as Writeable<typeof followUp.files>).push(
new AttachmentBuilder(psa.data.image)
new AttachmentBuilder(psa.data.image),
);
followUp.content = followUp.content ?? psa.data.title;
}
@@ -116,7 +115,7 @@ export async function handleChatInputCommand(
(
followUp.components[0] as ActionRowBuilder<ButtonBuilder>
).addComponents(
psa.data.links.map(({ title, url }) => buttonLink(url, title))
psa.data.links.map(({ title, url }) => buttonLink(url, title)),
);
}
@@ -133,7 +132,7 @@ export async function handleChatInputCommand(
new ButtonBuilder()
.setLabel(strings.bot.common.errors.get_help)
.setURL("https://discord.gg/nDt7AjGJQH")
.setStyle(ButtonStyle.Link)
.setStyle(ButtonStyle.Link),
),
];
if (error instanceof UserError) {
@@ -72,13 +72,13 @@ interface CommandRegistryImplicit extends CommandRegistryBase {
interface CommandRegistryExplicit extends CommandRegistryBase {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>
interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable;
}
export type CommandRegistry = CommandRegistryImplicit | CommandRegistryExplicit;
export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const commandsArray: Promise<CommandRegistry>[] = [
// auctionCommand,
permissionsCommand,
debugCommand,
@@ -101,34 +101,28 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
replayCommand,
];
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry, index) => {
if (registry.status === "rejected") {
console.warn(
`Command ${index} failed to load; skipping...`,
registry.reason
);
return commands;
}
return { ...commands, [registry.value.command.name]: registry.value };
},
{}
);
});
export const commands: Record<string, CommandRegistry> = {};
const commandsFulfilled: CommandRegistry[] = [];
let index = 0;
for (const registry of await Promise.allSettled(commandsArray)) {
if (registry.status === "rejected") {
console.warn(`Command ${index} rejected`, registry.reason);
continue;
}
commandsFulfilled.push(registry.value);
commands[registry.value.command.name] = registry.value;
index++;
}
const rest = new REST().setToken(assertSecret(import.meta.env.DISCORD_TOKEN));
const body = commandsFulfilled.map((registry) => registry.command.toJSON());
const applicationCommandsRoute = Routes.applicationCommands(
assertSecret(import.meta.env.DISCORD_CLIENT_ID),
);
commands.then((awaitedCommands) => {
const body = Object.values(awaitedCommands).map((registry) =>
registry.command.toJSON()
);
rest.put(
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
{ body }
);
});
await rest.put(applicationCommandsRoute, { body });
export async function interactionCreate(interaction: Interaction<CacheType>) {
if (interaction.isAutocomplete()) {
+14 -14
View File
@@ -1,27 +1,27 @@
import { assertSecret } from '@blitzkit/core';
import { ActivityType, ShardingManager } from 'discord.js';
import { assertSecret } from "@blitzkit/core";
import { ActivityType, ShardingManager } from "discord.js";
import {
discoveredIdsDefinitions,
tankDefinitions,
} from './core/blitzkit/nonBlockingPromises';
} from "./core/blitzkit/nonBlockingPromises";
let manager: ShardingManager;
let iteration = 0;
const interval = setInterval(async () => {
iteration++;
console.log(`Attempting launch on iteration ${++iteration}`);
console.log(`Attempting launch on iteration ${iteration}`);
const manager = new ShardingManager('dist/bot/workers/bot.js', {
manager?.respawnAll();
manager = new ShardingManager("dist/bot/workers/bot.js", {
token: assertSecret(import.meta.env.DISCORD_TOKEN),
});
const shards = await manager
.on('shardCreate', (shard) => {
.on("shardCreate", (shard) => {
clearInterval(interval);
console.log(`🟡 Launching shard ${shard.id}`);
shard.on('ready', () => console.log(`🟢 Launched shard ${shard.id}`));
shard.on("ready", () => console.log(`🟢 Launched shard ${shard.id}`));
})
.spawn();
.spawn({ timeout: 60000 });
try {
let servers = 0;
@@ -29,7 +29,7 @@ const interval = setInterval(async () => {
let users = 0;
for (const [, shard] of shards) {
const guilds = (await shard.fetchClientValue('guilds.cache')) as any;
const guilds = (await shard.fetchClientValue("guilds.cache")) as any;
servers += guilds.length;
@@ -66,7 +66,7 @@ const interval = setInterval(async () => {
for (const guild of client.guilds.cache.values()) {
try {
const botMember = await guild.members.fetch(client.user.id);
await botMember.setNickname('BlitzKit');
await botMember.setNickname("BlitzKit");
} catch {}
}
},
@@ -74,8 +74,8 @@ const interval = setInterval(async () => {
);
} catch (error) {
console.warn(
'An error occurred when setting up presence, ignoring to keep the process alive',
"An error occurred when setting up presence, ignoring to keep the process alive",
error,
);
}
}, 5000);
}, 15000);
+3 -3
View File
@@ -1,6 +1,6 @@
import { assertSecret } from '@blitzkit/core';
import { registerProcesses } from '../core/blitzkit/processes';
import { client } from '../core/discord/client';
import { assertSecret } from "@blitzkit/core";
import { registerProcesses } from "../core/blitzkit/processes";
import { client } from "../core/discord/client";
console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
+42 -41
View File
@@ -1,49 +1,50 @@
export * from './assertSecret';
export * from './asset';
export * from './availableProvisions';
export * from './averageDefinitions';
export * from "./assertSecret";
export * from "./asset";
export * from "./availableProvisions";
export * from "./averageDefinitions";
// export * from './camouflageDefinitions';
// export * from './checkConsumableProvisionInclusivity';
export * from './coefficient';
export * from './consumableDefinitions';
export * from "./coefficient";
export * from "./consumableDefinitions";
// export * from './context';
export * from './createDefaultSkills';
export * from './degressiveStat';
export * from './discoveredIdDefinitions';
export * from './emblemURL';
export * from './equipmentDefinitions';
export * from './eventManager';
export * from './gameDefinitions';
export * from './getArchivedLatestSeasonNumber';
export * from "./createDefaultSkills";
export * from "./degressiveStat";
export * from "./discoveredIdDefinitions";
export * from "./emblemURL";
export * from "./equipmentDefinitions";
export * from "./eventManager";
export * from "./gameDefinitions";
export * from "./getArchivedLatestSeasonNumber";
// export * from './getArchivedRatingInfo';
export * from './getArchivedRatingLeaderboard';
export * from "./getArchivedRatingLeaderboard";
// export * from './getArchivedRatingMidnightLeaderboard';
export * from './getTimeDaysAgo';
export * from './isExplosive';
export * from './mapDefinitions';
export * from './modelDefinitions';
export * from './normalizeBoundingBox';
export * from "./getTimeDaysAgo";
export * from "./isExplosive";
export * from "./mapDefinitions";
export * from "./modelDefinitions";
export * from "./normalizeBoundingBox";
// export * from './parseBkni';
// export * from './patientFetch';
export * from './patientFetchJSON';
export * from './progressiveStat';
export * from './provisionDefinitions';
export * from './resolveDpm';
export * from './resolvePenetrationCoefficient';
export * from './reviews';
export * from './skillDefinitions';
export * from './sum';
export * from './tankDefinitions';
export * from './tankIcon';
export * from './tankNames';
export * from './unionBoundingBox';
export * from './uniqueGuns';
export * from './unwrapBlitzkitResponse';
export * from './votes';
export * from "./patientFetchJSON";
export * from "./progressiveStat";
export * from "./provisionDefinitions";
export * from "./resolveDpm";
export * from "./resolvePenetrationCoefficient";
export * from "./reviews";
export * from "./skillDefinitions";
export * from "./sum";
export * from "./tankDefinitions";
export * from "./tankIcon";
export * from "./tankNames";
export * from "./unionBoundingBox";
export * from "./uniqueGuns";
export * from "./unwrapBlitzkitResponse";
export * from "./votes";
// export * from './withCORSProxy';
export * from './createDefaultProvisions';
export * from './gallery';
export * from './metaSortTank';
export * from './popularTanks';
export * from './romanize';
export * from './youtubers';
export * from "./createDefaultProvisions";
export * from "./gallery";
export * from "./metaSortTank";
export * from "./popularTanks";
export * from "./romanize";
export * from "./sluggify.ts";
export * from "./youtubers";
+16
View File
@@ -0,0 +1,16 @@
import { deburr } from "lodash-es";
const nonAlphanumericRegex = /[^a-z0-9]/g;
const multipleDashesRegex = /--+/g;
const trailingDashRegex = /-$/g;
const leadingDashRegex = /^-/g;
export function sluggify(value: string) {
let slug = deburr(value).toLowerCase();
slug = slug.replaceAll(nonAlphanumericRegex, "-");
slug = slug.replaceAll(multipleDashesRegex, "-");
slug = slug.replaceAll(trailingDashRegex, "");
slug = slug.replaceAll(leadingDashRegex, "");
return slug;
}
-1
View File
@@ -4,7 +4,6 @@ export * from "./imgur";
export * from "./math";
export * from "./patreon";
export * from "./protobuf";
export * from "./protos";
export * from "./statistics";
export * from "./streams";
export * from "./types";
+1
View File
@@ -3,6 +3,7 @@ export * from "./fisherYates";
export * from "./formatCompact";
export * from "./isNumber";
export * from "./NaNFallback";
export * from "./normalizeAngleDeg";
export * from "./normalizeAngleRad";
export * from "./normalToEuler";
export * from "./numberToRomanNumeral";
@@ -0,0 +1,6 @@
export function normalizeAngleDeg(angle: number) {
const newAngle = angle % 360;
if (newAngle > 360) return newAngle - 360;
if (newAngle < -360) return newAngle + 360;
return newAngle;
}
@@ -0,0 +1,3 @@
export function bufferToBigInt(buffer: ArrayBuffer) {
return new DataView(buffer).getBigUint64(0, true);
}
+2
View File
@@ -1,4 +1,5 @@
export * from './buffer';
export * from './bufferToBigInt';
export * from './dds';
export * from './dids';
export * from './pvr';
@@ -7,3 +8,4 @@ export * from './sc2';
export * from './scg';
export * from './scpg';
export * from './windows';
Submodule packages/cue4parse added at b8c9dab500.
+3
View File
@@ -0,0 +1,3 @@
[*.cs]
indent_style = space
indent_size = 2
+57
View File
@@ -0,0 +1,57 @@
## A streamlined .gitignore for modern .NET projects
## including temporary files, build results, and
## files generated by popular .NET tools. If you are
## developing with Visual Studio, the VS .gitignore
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## has more thorough IDE-specific entries.
##
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# dotenv environment variables file
.env
# Others
~$*
*~
CodeCoverage/
# MSBuild Binary and Structured Log
*.binlog
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
+70
View File
@@ -0,0 +1,70 @@
using BlitzKit.Game.Models;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Objects.UObject;
using Microsoft.JavaScript.NodeApi;
namespace BlitzKit.Game.JSExport;
[JSExport]
public class GameInterface
{
private readonly BlitzFileProvider provider;
public string[] files;
public string[] tanksDirectoryNations;
public GameInterface(string directory)
{
provider = new(directory);
files = [.. provider.Files.Keys];
var tanksBase = "Blitz/Content/Tanks/";
tanksDirectoryNations =
[
.. files
.Where(path => path.StartsWith(tanksBase))
.Select(path => path[tanksBase.Length..])
.Select(path => path.Split('/')[0])
.Where(nation => !nation.EndsWith(".uasset") && nation != "TankStub")
.Distinct(),
];
}
public string[] Files => files;
public string[] TanksDirectoryNations => tanksDirectoryNations;
public byte[] TankBigIcon(string tankId, string pdaName)
{
foreach (var nation in tanksDirectoryNations)
{
var path = $"Blitz/Content/Tanks/{nation}/{tankId}/{pdaName}.{pdaName}";
if (provider.TryLoadPackageObject<UPrimaryDataAsset>(path, out var pda))
{
try
{
return pda.Get<FSoftObjectPath>("BigIcon")
.Load<UTexture2D>()
.Decode(ETexturePlatform.DesktopMobile)!
.ToSkBitmap()
.Encode(SkiaSharp.SKEncodedImageFormat.Webp, 80)
.ToArray();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
else
{
continue;
}
}
throw new FileNotFoundException($"Tank {tankId}/{pdaName} not found in any nation");
}
}
+41
View File
@@ -0,0 +1,41 @@
using System.Runtime.InteropServices;
using CUE4Parse.Compression;
using CUE4Parse.FileProvider;
using CUE4Parse.MappingsProvider;
using CUE4Parse.UE4.Versions;
namespace BlitzKit.Game.Models;
public class BlitzFileProvider : DefaultFileProvider
{
public BlitzFileProvider(string directory)
: base(
directory: directory,
searchOption: SearchOption.AllDirectories,
versions: new(EGame.GAME_UE5_5),
pathComparer: StringComparer.OrdinalIgnoreCase
)
{
MappingsContainer = new FileUsmapTypeMappingsProvider("../../packages/closed/blitz.usmap");
Initialize();
Mount();
string libraryExtension;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
libraryExtension = "dll";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
libraryExtension = "so";
}
else
{
throw new Exception("Unsupported OS");
}
OodleHelper.DownloadOodleDll($"../../temp/oodle.{libraryExtension}");
OodleHelper.Initialize($"../../temp/oodle.{libraryExtension}");
}
}
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>BlitzKit.Game</RootNamespace>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NodeApiAssemblyJSModuleType>esm</NodeApiAssemblyJSModuleType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../cue4parse/CUE4Parse/CUE4Parse.csproj" />
<ProjectReference Include="../cue4parse/CUE4Parse-Conversion/CUE4Parse-Conversion.csproj" />
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.9.19" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.19" />
</ItemGroup>
</Project>
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@blitzkit/game",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"build": "dotnet build"
},
"dependencies": {
"node-api-dotnet": "^0.9.19"
}
}
+1
View File
@@ -0,0 +1 @@
export * from "../bin/Debug/net9.0/win-x64/game";
-2
View File
@@ -1,4 +1,2 @@
export * from "./literals";
export * from "./strings";
export * from "./unwrapper";
export * from "./wrapper";
+37 -8
View File
@@ -1,16 +1,45 @@
export function literals(
string: string,
literals?: Record<string, string | number | undefined>
literals?: Record<string, string | number | undefined>,
) {
for (const key in literals) {
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
if (import.meta.env.DEV && mutated === string) {
console.warn(`Could not find literal {${key}} in string "${string}"`);
}
string = mutated;
string = string.replaceAll(`{${key}}`, `${literals[key]}`);
}
return string;
}
export function literalsArray<Type, Literal = Type | string>(
string: string,
literals?: Record<string, Literal>,
) {
if (!literals) return [string];
let array: (string | Literal)[] = [string];
for (const key in literals) {
const value = literals[key];
const next: (string | Literal)[] = [];
for (const chunk of array) {
if (typeof chunk !== "string") {
next.push(chunk);
continue;
}
const parts = chunk.split(`{${key}}`);
for (let index = 0; index < parts.length; index++) {
if (parts[index]) next.push(parts[index]);
if (index < parts.length - 1) {
next.push(value);
}
}
}
array = next;
}
return array;
}
+2 -67
View File
@@ -1,68 +1,3 @@
import { merge } from "ts-deepmerge";
import locales from "../locales.json";
import en from "../strings/en.json";
import es from "../strings/es.json";
import fr from "../strings/fr.json";
import ja from "../strings/ja.json";
import pl from "../strings/pl.json";
import pt from "../strings/pt.json";
import ru from "../strings/ru.json";
import uk from "../strings/uk.json";
import zh from "../strings/zh.json";
import type en from "../strings/en.json";
export type BlitzKitStrings = typeof en;
const stringsRaw: Record<string, TranslationTree> = {
en,
es,
fr,
ja,
pl,
pt,
ru,
uk,
zh,
};
export const STRINGS: Record<string, BlitzKitStrings> = {};
for (const locale in stringsRaw) {
if (locale === locales.default) {
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
continue;
}
const localeStrings = stringsRaw[locale];
STRINGS[locale] = merge(
stringsRaw[locales.default],
localeStrings
) as BlitzKitStrings;
}
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
en: "en",
es: "es",
fr: "fr",
ja: "ja",
pt: "pt",
ru: "ru",
uk: "uk",
zh: "zh-Hans",
pl: "pl",
};
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
en: "🇺🇸",
es: "🇪🇸",
fr: "🇫🇷",
ja: "🇯🇵",
pt: "🇵🇹",
ru: "🇷🇺",
uk: "🇺🇦",
zh: "🇨🇳",
pl: "🇵🇱",
};
export type TranslationTree = { [key: string]: TranslationNode };
export type TranslationNode = string | TranslationTree;
export type Strings = typeof en;
-12
View File
@@ -1,12 +0,0 @@
import { I18nString } from "../../core/src";
import locales from "../locales.json";
export function unwrapper(locale = locales.default) {
return function (i18nString: I18nString) {
if (locale in i18nString.locales) {
return i18nString.locales[locale];
}
return i18nString.locales[locales.default];
};
}
-15
View File
@@ -1,15 +0,0 @@
import locales from "../locales.json";
import { type BlitzKitStrings, STRINGS } from "./strings";
export function wrapper(slicer: (strings: BlitzKitStrings) => string) {
const defaultString = slicer(STRINGS[locales.default] as BlitzKitStrings);
const object = { [locales.default]: defaultString };
for (const { locale } of locales.supported) {
const string = slicer(STRINGS[locale] as BlitzKitStrings);
if (string !== defaultString) object[locale] = string;
}
return object;
}
File diff suppressed because it is too large. Load diff
+10 -3
View File
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": "# Stat Commands Are Dead\n\nThe BlitzKit Discord bot used to use BlitzStars to fetch periodic statistics. BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API. This has impacted the following commands:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nPlease use [Aftermath](<https://amth.one/invite>) as an alternative which is completely independent of BlitzStars. BlitzKit-hosted stat commands will return in the near future.",
"replay": {
"name": "repetición",
"description": "Mostrar resultados de la repetición"
"description": "Mostrar resultados de la repetición",
"options": {
"file": {
"name": "file",
"description": "Replay file"
}
}
},
"research": {
"name": "desarollar",
@@ -1005,8 +1012,8 @@
"title": "Características",
"compare": {
"label": "Comparar con",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "{tier} {class}",
"tier": "All tier {tier}",
"all": "Todos los tanques"
},
"values": {
+10 -3
View File
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": "# Stat Commands Are Dead\n\nThe BlitzKit Discord bot used to use BlitzStars to fetch periodic statistics. BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API. This has impacted the following commands:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nPlease use [Aftermath](<https://amth.one/invite>) as an alternative which is completely independent of BlitzStars. BlitzKit-hosted stat commands will return in the near future.",
"replay": {
"name": "rejouer",
"description": "Afficher les résultats de la relecture"
"description": "Afficher les résultats de la relecture",
"options": {
"file": {
"name": "file",
"description": "Replay file"
}
}
},
"research": {
"name": "recherche",
@@ -1005,8 +1012,8 @@
"title": "Characteristics",
"compare": {
"label": "Compare against",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "{tier} {class}",
"tier": "All tier {tier}",
"all": "All tanks"
},
"values": {
+10 -3
View File
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": "# Stat Commands Are Dead\n\nThe BlitzKit Discord bot used to use BlitzStars to fetch periodic statistics. BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API. This has impacted the following commands:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nPlease use [Aftermath](<https://amth.one/invite>) as an alternative which is completely independent of BlitzStars. BlitzKit-hosted stat commands will return in the near future.",
"replay": {
"name": "replay",
"description": "Display replay results"
"description": "Display replay results",
"options": {
"file": {
"name": "file",
"description": "Replay file"
}
}
},
"research": {
"name": "研究",
@@ -1005,8 +1012,8 @@
"title": "性能値",
"compare": {
"label": "比較対象",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "{tier} {class}",
"tier": "All tier {tier}",
"all": "全車輌"
},
"values": {
+15 -8
View File
@@ -1,7 +1,7 @@
{
"test": {
"default": "",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": ".",
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
},
"common": {
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": "# Stat Commands Are Dead\n\nThe BlitzKit Discord bot used to use BlitzStars to fetch periodic statistics. BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API. This has impacted the following commands:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nPlease use [Aftermath](<https://amth.one/invite>) as an alternative which is completely independent of BlitzStars. BlitzKit-hosted stat commands will return in the near future.",
"replay": {
"name": "powtórz",
"description": "Wyświetl wyniki powtórki"
"description": "Wyświetl wyniki powtórki",
"options": {
"file": {
"name": "file",
"description": "Replay file"
}
}
},
"research": {
"name": "badanie",
@@ -493,7 +500,7 @@
"description": "Search clans",
"options": {
"limit": {
"name": "limit",
"name": "",
"description": "The max number of clan results (default: {limit})"
}
},
@@ -507,7 +514,7 @@
"description": "Search players",
"options": {
"limit": {
"name": "limit",
"name": "",
"description": "The max number of player results (default: {limit})"
}
},
@@ -531,7 +538,7 @@
},
"options": {
"limit": {
"name": "limit",
"name": "",
"description": "How many neighbors to display (default: {count})"
},
"season": {
@@ -675,7 +682,7 @@
"active": "{name} (Active)",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
"search_again": "",
"clear_filters": "Try clearing filters."
},
"sort": {
@@ -1005,8 +1012,8 @@
"title": "Characteristics",
"compare": {
"label": "Compare against",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "{tier} {class}",
"tier": "All tier {tier}",
"all": "All tanks"
},
"values": {
+10 -3
View File
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": "# Stat Commands Are Dead\n\nThe BlitzKit Discord bot used to use BlitzStars to fetch periodic statistics. BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API. This has impacted the following commands:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nPlease use [Aftermath](<https://amth.one/invite>) as an alternative which is completely independent of BlitzStars. BlitzKit-hosted stat commands will return in the near future.",
"replay": {
"name": "replay",
"description": "Exibir resultados de replays"
"description": "Exibir resultados de replays",
"options": {
"file": {
"name": "file",
"description": "Replay file"
}
}
},
"research": {
"name": "pesquisa",
@@ -1005,8 +1012,8 @@
"title": "Características",
"compare": {
"label": "Comparar com",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "{tier} {class}",
"tier": "All tier {tier}",
"all": "Todos os tanques"
},
"values": {
+12 -5
View File
@@ -24,7 +24,7 @@
"shells": {
"ap": "ББ",
"ap_cr": "БП",
"hc": "ОФ",
"hc": "КС",
"he": "ОФ"
},
"player_count_period": {
@@ -39,7 +39,7 @@
"branches": {
"local": "dev",
"dev": "бета",
"opentest": "открытый тест",
"opentest": "общий тест",
"preview": "превью"
},
"locales": {
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": ".",
"replay": {
"name": "повтор",
"description": "Показать результаты повтора"
"description": "Показать результаты повтора",
"options": {
"file": {
"name": "Файл",
"description": "Запись повтора"
}
}
},
"research": {
"name": "исследование",
@@ -1005,8 +1012,8 @@
"title": "Характеристики",
"compare": {
"label": "Сравнивать с",
"class": "{class} {tier} уровня",
"tier": "Все {tier} уровня",
"class": "Уровень Тип",
"tier": "Все уровни",
"all": "Все танки"
},
"values": {
+21 -14
View File
@@ -1,7 +1,7 @@
{
"test": {
"default": "тест",
"long": "{fruit}",
"long": "{color}{fruit}",
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
},
"common": {
@@ -23,7 +23,7 @@
},
"shells": {
"ap": "ББ",
"ap_cr": "APCR",
"ap_cr": "",
"hc": "HEAT",
"he": "ОФ"
},
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": ".",
"replay": {
"name": "реплей",
"description": "Показати результати реплею"
"description": "Показати результати реплею",
"options": {
"file": {
"name": "",
"description": "Replay filehttps://crowdin.com/editor/blitzkrieg/80/en-uk?view=side-by-side#9706"
}
}
},
"research": {
"name": "дослідження",
@@ -660,7 +667,7 @@
},
"tank_search": {
"select_dialog_title": "Знайти танк",
"swap_dialog_title": "Swap tank",
"swap_dialog_title": "Резервуар підміни",
"clear_filters": "Очистити",
"login": "Увійдіть, щоб скористатися",
"search_bar_hint": "Шукайте техніку...",
@@ -671,8 +678,8 @@
"sorting_by": "Відсортувати за {name}",
"clear": "Clear",
"tier": "Рівень {tier}",
"passive": "{name} (Passive)",
"active": "{name} (Active)",
"passive": "",
"active": "{name}{name}",
"no_tanks_found": {
"body": "Нічого не знайдено.",
"search_again": "Спробуйте шукати заново.",
@@ -735,11 +742,11 @@
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"title": "",
"new": "",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"checking": "",
"instructions": ".",
"install": "",
"dismiss": "Dismiss"
},
"discord": {
@@ -823,7 +830,7 @@
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"cancel": "",
"reveal": "Reveal"
},
"clear": "Clear",
@@ -1005,8 +1012,8 @@
"title": "Characteristics",
"compare": {
"label": "Compare against",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "{tier} {class}",
"tier": "All tier {tier}",
"all": "Усі танки"
},
"values": {
@@ -1274,7 +1281,7 @@
"button": "Generate URL",
"title": "Generate URL",
"subtitle": "Search an account to track",
"cancel": "Cancel",
"cancel": "",
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
+10 -3
View File
@@ -320,9 +320,16 @@
}
},
"commands": {
"dead": "# Stat Commands Are Dead\n\nThe BlitzKit Discord bot used to use BlitzStars to fetch periodic statistics. BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API. This has impacted the following commands:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nPlease use [Aftermath](<https://amth.one/invite>) as an alternative which is completely independent of BlitzStars. BlitzKit-hosted stat commands will return in the near future.",
"replay": {
"name": "回放",
"description": "显示重播结果"
"description": "显示重播结果",
"options": {
"file": {
"name": "file",
"description": "Replay file"
}
}
},
"research": {
"name": "研发",
@@ -1005,8 +1012,8 @@
"title": "属性",
"compare": {
"label": "比较",
"class": "{tier}阶 {class}",
"tier": "所有 {tier} 阶",
"class": "{tier} {class}",
"tier": "All tier {tier}",
"all": "所有坦克"
},
"values": {
@@ -12,12 +12,14 @@ export async function tankModels() {
using uploader = new AssetUploader("tank models");
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const nations = vfs.dir(`Data/XML/item_defs/vehicles`).filter((nation) => nation !== "common")
const nations = vfs
.dir(`Data/XML/item_defs/vehicles`)
.filter((nation) => nation !== "common");
for (const nationIndex in nations) {
const nation = nations[nationIndex];
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
`Data/XML/item_defs/vehicles/${nation}/list.xml`
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
);
const entries = Object.entries(tanks.root);
@@ -26,11 +28,11 @@ export async function tankModels() {
const id = toUniqueId(nation, tank.id);
const parameters = await vfs.yaml<TankParameters>(
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
);
const model = await extractModel(
vfs,
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "")
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ""),
);
const content = Buffer.from(await nodeIO.writeBinary(model));
@@ -39,4 +41,4 @@ export async function tankModels() {
}
await uploader.flush();
}
}
+12 -19
View File
@@ -1,15 +1,10 @@
import { build } from "esbuild";
import { cp, readdir, rm } from "fs/promises";
import { argv } from "process";
const dev = argv.includes("--dev");
const projectDir = "../..";
const srcRoot = `${projectDir}/packages/bot`;
const distRoot = `${projectDir}/dist/bot`;
const modulesRoot = dev
? "../../packages/bot/node_modules"
: "../../node_modules";
const modulesRoot = "../../node_modules";
const workers = await readdir(`${srcRoot}/src/workers`);
await rm(distRoot, { recursive: true, force: true });
@@ -30,9 +25,9 @@ build({
},
external: ["sharp"],
minifyIdentifiers: false, // causes goofy ahh issues
minifySyntax: !dev,
minifyWhitespace: !dev,
sourcemap: !dev,
minifySyntax: true,
minifyWhitespace: true,
sourcemap: true,
jsxImportSource: "react",
jsx: "automatic",
});
@@ -44,18 +39,16 @@ for (const module of await readdir(`${modulesRoot}/@resvg`)) {
.map((file) =>
cp(
`${modulesRoot}/@resvg/${module}/${file}`,
`${distRoot}/workers/${file}`
)
)
`${distRoot}/workers/${file}`,
),
),
);
}
if (!dev) {
await cp(`${projectDir}/prisma`, `${distRoot}/prisma`, { recursive: true });
await cp(`${projectDir}/prisma`, `${distRoot}/prisma`, { recursive: true });
for (const file of await readdir(`${modulesRoot}/prisma`).then((files) =>
files.filter((file) => file.endsWith(".node"))
)) {
cp(`${modulesRoot}/prisma/${file}`, `${distRoot}/prisma/${file}`);
}
for (const file of await readdir(`${modulesRoot}/prisma`).then((files) =>
files.filter((file) => file.endsWith(".node")),
)) {
cp(`${modulesRoot}/prisma/${file}`, `${distRoot}/prisma/${file}`);
}
+13 -13
View File
@@ -6,20 +6,20 @@ import {
} from "@blitzkit/core";
import { Accessor, Document, Node, Scene } from "@gltf-transform/core";
import { times } from "lodash-es";
import { SteamVFS } from "../vfs/vfs";
import { AbstractVFS } from "../vfs/abstract";
import {
vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes,
} from "./extractModel/constants";
export async function extractArmor(vfs: SteamVFS, fileName: string) {
export async function extractArmor(vfs: AbstractVFS, fileName: string) {
const sc2Path = `Data/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
const sc2 = new Sc2ReadStream(
(await vfs.file(sc2Path)).buffer as ArrayBuffer
(await vfs.file(sc2Path)).buffer as ArrayBuffer,
).sc2();
const scg = new ScgReadStream(
(await vfs.file(scgPath)).buffer as ArrayBuffer
(await vfs.file(scgPath)).buffer as ArrayBuffer,
).scg();
const document = new Document();
const scene = document.createScene();
@@ -29,7 +29,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
hierarchies.forEach((hierarchy) => {
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, "0")]
(index) => hierarchy.components[index.toString().padStart(4, "0")],
);
components.forEach((component) => {
@@ -43,7 +43,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
if (!polygonGroup) {
console.warn(
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`
`Missing polygon group ${batch["rb.datasource"]} (${hierarchy.name}); skipping...`,
);
break;
@@ -73,7 +73,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
VertexAttribute.TANGENT,
attributes
.get(VertexAttribute.TANGENT)!
.map((tangent) => [...tangent, 1])
.map((tangent) => [...tangent, 1]),
);
}
@@ -94,7 +94,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
hardJointIndices.forEach((hardJointIndex) => {
const node = document.createNode(
`${hierarchy.name}_armor_${hardJointIndex}`
`${hierarchy.name}_armor_${hardJointIndex}`,
);
const mesh = document.createMesh(batch["##name"]);
const indicesAccessor = document
@@ -104,9 +104,9 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
new Uint16Array(
polygonGroup.indices.filter(
(index) =>
vertexHardJointIndices.get(index) === hardJointIndex
)
)
vertexHardJointIndices.get(index) === hardJointIndex,
),
),
)
.setBuffer(buffer);
const primitive = document
@@ -127,7 +127,7 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
default:
throw new TypeError(
`Unhandled component type: ${component["comp.typename"]}`
`Unhandled component type: ${component["comp.typename"]}`,
);
}
});
@@ -141,4 +141,4 @@ export async function extractArmor(vfs: SteamVFS, fileName: string) {
parseHierarchies(sc2["#hierarchy"], scene);
return document;
}
}
@@ -1,4 +1,5 @@
import {
bufferToBigInt,
Hierarchy,
Sc2ReadStream,
ScgReadStream,
@@ -9,7 +10,7 @@ import { Document, Material, Node, Scene } from "@gltf-transform/core";
import { dedup, prune } from "@gltf-transform/functions";
import { times } from "lodash-es";
import { dirname } from "path";
import { SteamVFS } from "../../vfs/vfs";
import { AbstractVFS } from "../../vfs/abstract";
import { readBaseColor } from "../readBaseColor";
import { readNormal } from "../readNormal";
import { readRoughnessMetallic } from "../readRoughnessMetallic";
@@ -25,14 +26,14 @@ const omitMeshNames = {
end: ["_POINT"],
};
export async function extractModel(vfs: SteamVFS, path: string) {
export async function extractModel(vfs: AbstractVFS, path: string) {
const sc2Path = `Data/3d/${path}.sc2`;
const scgPath = `Data/3d/${path}.scg`;
const sc2 = new Sc2ReadStream(
(await vfs.file(sc2Path)).buffer as ArrayBuffer
(await vfs.file(sc2Path)).buffer as ArrayBuffer,
).sc2();
const scg = new ScgReadStream(
(await vfs.file(scgPath)).buffer as ArrayBuffer
(await vfs.file(scgPath)).buffer as ArrayBuffer,
).scg();
const document = new Document();
const scene = document.createScene();
@@ -40,7 +41,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
const materials = new Map<bigint, Material | bigint | undefined>();
for (const node of sc2["#dataNodes"]) {
const id = new DataView(node["#id"]).getBigUint64(0, true);
const id = bufferToBigInt(node["#id"]);
if (node.parentMaterialKey !== undefined) {
/**
@@ -61,20 +62,17 @@ export async function extractModel(vfs: SteamVFS, path: string) {
}
if (textures) {
const { image: baseColor, hasAlpha } = await readBaseColor(
`Data/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
textures.miscMap
? `Data/3d/${dirname(path)}/${textures.miscMap}`
: undefined,
);
material.setBaseColorTexture(
document
.createTexture(node.materialName)
.setMimeType("image/webp")
.setImage(
await readBaseColor(
`Data/3d/${dirname(path)}/${
textures.baseColorMap ?? textures.albedo
}`,
textures.miscMap
? `Data/3d/${dirname(path)}/${textures.miscMap}`
: undefined
)
)
.setImage(baseColor),
);
if (node.configCount) {
@@ -92,7 +90,11 @@ export async function extractModel(vfs: SteamVFS, path: string) {
continue;
}
material.setAlphaMode("BLEND").setDoubleSided(true);
const view = new DataView(archive.properties.alphatestThreshold);
const _ = view.getUint8(0); // only god knows what these flags are
const alphaCutoff = view.getFloat32(1, true);
material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff);
}
}
@@ -103,9 +105,9 @@ export async function extractModel(vfs: SteamVFS, path: string) {
.setMimeType("image/webp")
.setImage(
await readRoughnessMetallic(
`Data/3d/${dirname(path)}/${textures.baseRMMap}`
)
)
`Data/3d/${dirname(path)}/${textures.baseRMMap}`,
),
),
);
}
@@ -121,13 +123,13 @@ export async function extractModel(vfs: SteamVFS, path: string) {
`Data/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap
}`,
isBase
)
)
isBase,
),
),
);
}
materials.set(new DataView(node["#id"]).getBigUint64(0, true), material);
materials.set(id, material);
}
}
@@ -157,7 +159,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
const node = document.createNode(hierarchy.name);
const components = times(
hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, "0")]
(index) => hierarchy.components[index.toString().padStart(4, "0")],
);
components.forEach((component) => {
@@ -194,7 +196,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
}
if (polygonGroup === undefined) {
throw new Error(
`Missing polygon group ${batch["rb.datasource"]}`
`Missing polygon group ${batch["rb.datasource"]}`,
);
}
@@ -255,7 +257,7 @@ export async function extractModel(vfs: SteamVFS, path: string) {
default: {
if (ERROR_ON_UNKNOWN_COMPONENT) {
throw new Error(
`Unhandled component type: ${component["comp.typename"]}`
`Unhandled component type: ${component["comp.typename"]}`,
);
}
}
@@ -277,4 +279,4 @@ export async function extractModel(vfs: SteamVFS, path: string) {
await document.transform(prune({ keepAttributes: true }), dedup());
return document;
}
}
@@ -2,6 +2,7 @@ import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readBaseColor(path: string, occlusionPath?: string) {
let hasAlpha = false;
const baseRaw = await readTexture(path);
const occlusionRaw = occlusionPath
? await readTexture(occlusionPath)
@@ -30,10 +31,13 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
c = occlusion![occlusionI] / 255;
}
const alpha = base[i * 4 + 3];
hasAlpha ||= alpha < 255;
combined[i * 4 + 0] = Math.round(base[i * 4 + 0] * c);
combined[i * 4 + 1] = Math.round(base[i * 4 + 1] * c);
combined[i * 4 + 2] = Math.round(base[i * 4 + 2] * c);
combined[i * 4 + 3] = base[i * 4 + 3];
combined[i * 4 + 3] = alpha;
}
const image = await sharp(combined, {
@@ -42,5 +46,5 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
.webp()
.toBuffer();
return image;
return { hasAlpha, image };
}
+31 -40
View File
@@ -1,21 +1,16 @@
import { exec as execSync } from "child_process";
import { execFile } from "child_process";
import { existsSync } from "fs";
import { readdir, rm } from "fs/promises";
import { promisify } from "util";
import { mkdir, readdir, rm, writeFile } from "fs/promises";
const MAX_COMMAND_LENGTH = 2 ** 11;
const TS_PROTO_EXECUTABLE_LOCATIONS = [
"./node_modules/.bin/protoc-gen-ts_proto",
"../../node_modules/.bin/protoc-gen-ts_proto",
];
const roots = [
// "../../packages/core/src/protos",
const ROOTS = [
"../../packages/closed/protos",
"../../packages/website-ue/src/protos",
];
const exec = promisify(execSync);
let tsProtoExecutableLocation: string | undefined = undefined;
for (const location of TS_PROTO_EXECUTABLE_LOCATIONS) {
@@ -36,42 +31,38 @@ if (!tsProtoExecutableLocation) {
);
}
for (const root of roots) {
await mkdir("temp", { recursive: true });
for (const root of ROOTS) {
let args = "";
for (const include of ROOTS) {
args += `-I=${include}\n`;
}
args += `--plugin=${tsProtoExecutableLocation}\n`;
args += "--ts_proto_opt=esModuleInterop=true\n";
args += "--ts_proto_opt=oneof=unions-value\n";
args += "--ts_proto_opt=unrecognizedEnum=false\n";
args += "--ts_proto_opt=snakeToCamel=false\n";
args += `--ts_proto_out=${root}\n`;
const filesRaw = await readdir(`${root}`);
const files: string[] = [];
for (const file of filesRaw) {
if (file.endsWith(".proto")) files.push(file);
if (file.endsWith(".proto")) {
files.push(file);
args += `${root}/${file}\n`;
}
if (file.endsWith(".ts")) await rm(`${root}/${file}`);
}
while (files.length > 0) {
let command = [
"protoc",
`--plugin=${tsProtoExecutableLocation}`,
"--ts_proto_opt=esModuleInterop=true",
"--ts_proto_opt=oneof=unions-value",
// "--ts_proto_opt=removeEnumPrefix=true",
"--ts_proto_opt=unrecognizedEnum=false",
"--ts_proto_opt=snakeToCamel=false",
`--ts_proto_out=${root}`,
`-I=${root}`,
].join(" ");
while (true) {
if (files.length === 0) break;
const file = files.at(-1);
const newLine = ` ${root}/${file}`;
if (command.length + newLine.length < MAX_COMMAND_LENGTH) {
command += newLine;
files.pop();
} else {
break;
}
}
await exec(command);
}
await writeFile("temp/protoc.txt", args);
execFile("protoc", ["@temp/protoc.txt"], (error, stdout, stderr) => {
if (error) throw new Error(error.message);
if (stderr) console.error(stderr);
if (stdout) console.log(stdout);
});
}
+104
View File
@@ -0,0 +1,104 @@
import node from "@astrojs/node";
import react from "@astrojs/react";
import locales from "@blitzkit/i18n/locales.json";
import strings from "@blitzkit/i18n/strings/en.json";
import { mauve, mauveDark } from "@radix-ui/colors";
import favicons from "astro-favicons";
import { defineConfig } from "astro/config";
import packageJSON from "../../package.json";
const POSSIBLE_TARGETS = ["app", "web"];
const target = process.env.PUBLIC_TARGET;
if (!target || !POSSIBLE_TARGETS.includes(target)) {
throw new Error("Invalid target");
}
export default defineConfig({
devToolbar: { enabled: false },
vite: {
server: { allowedHosts: [] },
esbuild: {
minifyIdentifiers: true,
minifySyntax: false,
minifyWhitespace: true,
},
},
output: target === "web" ? "static" : "server",
site: "https://blitzkit.app",
outDir: "../../dist/website",
prefetch: { defaultStrategy: "hover", prefetchAll: true },
build: { concurrency: 4 },
adapter: target === "app" ? node({ mode: "standalone" }) : undefined,
integrations: [
favicons({
name: strings.common.name,
short_name: strings.common.name,
background: mauveDark.mauve1,
themes: [mauve.mauve1, mauveDark.mauve1],
appleStatusBarStyle: "black-translucent",
version: packageJSON.version,
// shortcuts: Object.values(tools)
// .filter((tool) => !tool.disabled && !tool.href)
// .map((tool) => {
// const stringId = (tool.strings ??
// tool.id) as keyof typeof strings.website.tools;
// return {
// name: strings.website.tools[stringId].name,
// name_localized: wrapper(
// (strings) => strings.website.tools[stringId].name
// ),
// description: strings.website.tools[stringId].description,
// description_localized: wrapper(
// (strings) => strings.website.tools[stringId].description
// ),
// short_name: strings.website.tools[stringId].name,
// short_name_localized: wrapper(
// (strings) => strings.website.tools[stringId].name
// ),
// url: `/${tool.id}`,
// icon: `public/assets/tools/${tool.id}.webp`,
// };
// }),
manifest: {
// description: strings.website.home.seo_description,
// description_localized: wrapper(
// (strings) => strings.website.home.seo_description
// ),
dir: "ltr",
categories: ["utilities", "games", "developer", "developer tools"],
id: "com.tresabhi.blitzkit",
start_url: "/",
launch_handler: { client_mode: "navigate-new" },
display: "fullscreen",
display_override: ["fullscreen", "standalone", "minimal-ui"],
lang: locales.default,
orientation: "any",
},
icons: {
windows: true,
android: true,
appleIcon: true,
appleStartup: true,
favicons: true,
yandex: true,
},
}),
react(),
],
});
+59
View File
@@ -0,0 +1,59 @@
{
"name": "@blitzkit/website-ue",
"type": "module",
"scripts": {
"dev": "PUBLIC_TARGET=web astro dev",
"build": "PUBLIC_TARGET=web astro build",
"preview": "PUBLIC_TARGET=web astro preview",
"lint": "PUBLIC_TARGET=web astro check",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "^9.4.4",
"@astrojs/react": "^4.4.0",
"@astrolib/seo": "^1.0.0-beta.8",
"@blitzkit/closed": "workspace:*",
"@blitzkit/core": "workspace:*",
"@blitzkit/game": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@bufbuild/protobuf": "^2.10.2",
"@fontsource-variable/roboto-flex": "^5.2.8",
"@fontsource-variable/roboto-mono": "^5.2.8",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-icons": "^1.3.2",
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/three": "^0.180.0",
"@vite-pwa/astro": "^1.1.0",
"@wooorm/starry-night": "^3.9.0",
"astro": "^5.14.4",
"astro-favicons": "^3.1.5",
"autoprefixer": "^10.4.23",
"cssnano": "^7.1.2",
"fuzzysort": "^3.1.0",
"googleapis": "^171.1.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"immer": "^10.1.3",
"lodash-es": "^4.17.21",
"postcss": "^8.5.6",
"postcss-load-config": "^6.0.1",
"postcss-nested": "^7.0.2",
"postcss-preset-env": "^11.1.1",
"quicklime": "^1.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"react-promise-suspense": "^0.3.4",
"sharp": "^0.34.5",
"three": "^0.180.0",
"three-stdlib": "^2.36.0",
"typescript": "^5.9.3",
"varuna": "workspace:*",
"vite": "^7.1.9",
"yaml": "^2.8.1"
}
}
+18
View File
@@ -0,0 +1,18 @@
import cssnano from "cssnano";
import type { Config } from "postcss-load-config";
import postcssPresetEnv from "postcss-preset-env";
export default {
plugins: [
require("autoprefixer"),
require("postcss-nested"),
cssnano({
preset: "default",
}),
postcssPresetEnv({
features: {},
}),
],
} satisfies Config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

@@ -0,0 +1,3 @@
<svg width="21" height="54" viewBox="0 0 21 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 38L10 49H0L11 38L0 27H10L21 38ZM21 1V21L20 22H10L21 11L10 0H20L21 1ZM10 11L0 21V1L10 11Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1,3 @@
<svg width="30" height="52" viewBox="0 0 30 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.8564 44V52H0V36L13.8564 44ZM30 52H16.1436V44L30 36V52ZM28.8564 18V34L15 42L1.14355 34V18L15 10L28.8564 18ZM13.8564 8L0 16V0H13.8564V8ZM30 16L16.1436 8V0H30V16Z" fill="#EEEEF0"/>
</svg>

After

Width:  |  Height:  |  Size: 294 B

@@ -0,0 +1,3 @@
<svg width="70" height="35" viewBox="0 0 70 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M70 32L64 35H0L70 0V32ZM0 32V0H64L0 32Z" fill="#EEEEF0"/>
</svg>

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Loaded 100 of 261 files, more files were not shown because too many files have changed in this diff. Show more