Compare commits

...
166 Commits
Author SHA1 Message Date
TrèsAbhi 1f06b1fef4 fix: checkout 2025-10-12 18:00:51 -05:00
TrèsAbhi 7b0625fc8d fix: @protos path 2025-10-12 17:57:49 -05:00
TrèsAbhi 5dce234447 fix: dvp 2025-10-12 14:12:47 -05:00
TrèsAbhi 0f609717d0 adjacent 2025-10-12 13:40:32 -05:00
TrèsAbhi 3958709348 fix: adjacent submodule 2025-10-12 13:39:03 -05:00
TrèsAbhi 2d869c9fac Remove submodule submodules/blitzkit-closed 2025-10-12 13:36:15 -05:00
TrèsAbhi 82afaf9494 fix: lz4js 2025-10-12 13:24:03 -05:00
TrèsAbhi b1d82e624a fix: crc 2025-10-12 13:12:10 -05:00
TrèsAbhi 9b60d56a36 fix: immer 2025-10-12 12:26:19 -05:00
TrèsAbhi ee0d4f6996 fix: varuna 2025-10-12 17:22:26 +00:00
TrèsAbhi 2d43408822 fix: remove closed 2025-10-12 17:21:18 +00:00
TrèsAbhi 868f132105 fix: core and closed dep 2025-10-12 17:20:33 +00:00
TrèsAbhi ce428e093b fix: i18n dep 2025-10-12 17:18:39 +00:00
TrèsAbhi ab430dc0ca fix: bot imports 2025-10-12 17:17:05 +00:00
TrèsAbhi 03a51c47bb fix: self core imports 2025-10-12 15:05:58 +00:00
TrèsAbhi 1790443698 feat: rename 2025-10-12 09:51:21 -05:00
TrèsAbhi a60f95340c fix: deps 2025-10-12 09:49:31 -05:00
TrèsAbhi e407463ca7 New Crowdin updates (#173) 2025-10-12 09:47:24 -05:00
TrèsAbhi 61cc15aafc Update package.json 2025-10-12 09:38:02 -05:00
TrèsAbhi 1a947a6300 feat: logs 2025-10-11 21:54:11 -05:00
TrèsAbhi 75262647f7 Merge branch 'main' into dev 2025-10-11 21:50:24 -05:00
TrèsAbhi 9914371dd8 fix: tier list filters 2025-10-11 21:49:20 -05:00
TrèsAbhi a4e48a2feb fix: performance filters 2025-10-11 21:45:18 -05:00
TrèsAbhi 4ab03e56a2 feat: support transparency 2025-10-11 21:22:48 -05:00
TrèsAbhi 06a3791ddd fix: settings title 2025-10-11 20:51:12 -05:00
TrèsAbhi 36cce4b0c1 fix: make settings page responsive again 2025-10-11 20:48:33 -05:00
TrèsAbhi 158141fdc7 Update index.ts 2025-10-06 20:58:45 -05:00
TrèsAbhi 1d319551e7 Update literals.ts 2025-10-06 18:24:48 -05:00
TrèsAbhi 0552ac790e Update assets.yml 2025-10-06 11:10:39 -05:00
TrèsAbhi 5ad7566874 fix: show controls in all display modes 2025-10-05 18:34:03 -05:00
TrèsAbhi 638f178fbf fix: trimmer heading 2025-10-05 18:31:07 -05:00
TrèsAbhi 1e996cb970 New Crowdin updates (#172) 2025-10-04 22:15:25 -05:00
TrèsAbhi 8639b31b8f feat: logs 2025-10-04 22:13:50 -05:00
TrèsAbhi 889a92e98b fix: lighting toggler no longer fires on unload 2025-10-04 22:05:36 -05:00
TrèsAbhi e7fe3a4baf fix: signs of tankiopedia transofmr controls 2025-10-04 22:02:24 -05:00
TrèsAbhi ab10343704 fix: clearer controls of tankoepdia 2025-10-04 21:53:18 -05:00
TrèsAbhi 9412be2d7c feat: filter owned tanks 2025-10-04 21:35:12 -05:00
TrèsAbhi 66cf590f1c fix: hook call outside parent hook 2025-10-04 21:33:28 -05:00
TrèsAbhi 7c18549c00 fix: full screen table 2025-10-02 10:39:36 -05:00
TrèsAbhi 9b97d3e27d feat: guesser flag icons 2025-10-02 10:29:43 -05:00
TrèsAbhi aadc252e47 fix: use new quicklime controls enabled 2025-10-02 10:19:50 -05:00
TrèsAbhi 320a764d45 feat: migrate all 2025-10-02 10:15:38 -05:00
TrèsAbhi 5594c0ec3b feat: some progress 2025-10-01 22:45:21 -05:00
TrèsAbhi 3d69d464f5 feat: start on amending slugs 2025-10-01 22:41:23 -05:00
TrèsAbhi dd0b2bade7 feat: named slugs 2025-10-01 22:41:15 -05:00
TrèsAbhi 99c90cfa9e Merge branch 'dev' of https://github.com/blitzkit/blitzkit into dev 2025-10-01 22:24:10 -05:00
TrèsAbhi dd3ce21508 feat: gun rotation 2025-10-01 22:24:08 -05:00
TrèsAbhi b512cb2292 Update README.md 2025-09-30 20:22:06 -05:00
TrèsAbhi 907e71ce03 feat: turret rotation 2025-09-30 16:48:37 -05:00
TrèsAbhi 10942879a6 feat: start of rotation 2025-09-30 16:34:08 -05:00
TrèsAbhi d60551d30a fix: change blitzkit icons to big icons 2025-09-29 13:41:52 -05:00
TrèsAbhi b8c8528e1c Merge branch 'main' into dev 2025-09-29 13:39:13 -05:00
TrèsAbhi 0c9643016a feat: logs 2025-09-29 13:37:00 -05:00
TrèsAbhi 61d1ebbad6 fix: redirect 2025-09-29 13:35:47 -05:00
TrèsAbhi e9b7059cc0 feat: some linking 2025-09-29 13:30:10 -05:00
TrèsAbhi a359897a2a New Crowdin updates (#171) 2025-09-29 13:29:58 -05:00
TrèsAbhi e5f7d72df3 feat: rearrange 2025-09-28 14:11:33 -05:00
TrèsAbhi 57092d8df0 feat: env vars 2025-09-28 14:10:34 -05:00
TrèsAbhi 86508d5413 feat: submodules and packages 2025-09-28 13:44:15 -05:00
TrèsAbhi 33373c286f feat: logs 2025-09-28 13:13:08 -05:00
TrèsAbhi 7443f75347 fix: less aggressive light manager 2025-09-28 13:12:16 -05:00
TrèsAbhi d09f934b80 feat: start app 2025-09-28 13:09:03 -05:00
TrèsAbhi 7f919ebb87 feat: remove pwa 2025-09-28 13:02:48 -05:00
TrèsAbhi 0929825555 fix: glob and cache 2025-09-27 19:13:29 -05:00
TrèsAbhi 4de0fc5ec1 feat: concurrency 2025-09-27 18:37:51 -05:00
TrèsAbhi e66610f86e fix: side and out 2025-09-27 18:32:18 -05:00
TrèsAbhi 674abc4923 fix: dead code 2025-09-27 18:28:35 -05:00
TrèsAbhi 25cee8a36b fix: icons 2025-09-27 18:26:37 -05:00
TrèsAbhi dcbac5e017 fix: missing package 2025-09-27 18:16:05 -05:00
TrèsAbhi 0c94d42f3f fix: missing package 2025-09-27 18:15:00 -05:00
TrèsAbhi 7ab5e94323 fix: missing package 2025-09-27 18:13:59 -05:00
TrèsAbhi f9ea5ba74a fix: missing package 2025-09-27 18:13:00 -05:00
TrèsAbhi 1e7162d2da fix: missing package 2025-09-27 18:11:49 -05:00
TrèsAbhi cfba53a655 fix: missing package 2025-09-27 18:10:20 -05:00
TrèsAbhi 77a85641f0 fix: missing package 2025-09-27 18:09:10 -05:00
TrèsAbhi 5330a27e96 feat: logs 2025-09-27 18:07:43 -05:00
TrèsAbhi a791aff4a0 Merge branch 'main' into dev 2025-09-27 18:03:37 -05:00
TrèsAbhi df8a5967f0 feat: start of logs 2025-09-27 18:03:20 -05:00
TrèsAbhi 6261dfc9cd fix: more linting 2025-09-27 18:01:20 -05:00
TrèsAbhi 766801826c fix: canvases 2025-09-27 17:51:29 -05:00
TrèsAbhi 7b728596ca feat: storeDeferred 2025-09-27 17:32:20 -05:00
TrèsAbhi 0bf6ac33a5 feat: more promotion 2025-09-27 17:29:12 -05:00
TrèsAbhi 9a33b84b1c feat: install button 2025-09-27 17:05:06 -05:00
TrèsAbhi c14efe86cc fix: update errors 2025-09-27 16:07:45 -05:00
TrèsAbhi 4ff9a2ec7f feat: more pwa entries 2025-09-27 15:51:46 -05:00
TrèsAbhi 75d7b4554b feat: start of pew 2025-09-27 15:38:37 -05:00
TrèsAbhi 27c8b348bb feat: favicons 2025-09-27 13:39:08 -05:00
TrèsAbhi a55ab5e7ee feat: redo base setup 2025-09-27 13:34:11 -05:00
TrèsAbhi 12e90cc6ee feat: update dependencies 2025-09-26 14:51:49 -05:00
TrèsAbhi 01755a2a64 feat: start adding pwa 2025-09-26 14:46:07 -05:00
TrèsAbhi 080f0a0eb7 fix: make tankopedia hero section a bit smaller 2025-09-22 13:52:31 -05:00
TrèsAbhi a646cc1e57 feat: mixer buttons 2025-09-22 13:50:46 -05:00
TrèsAbhi d107813c37 feat: update closed 2025-09-22 13:27:38 -05:00
TrèsAbhi 27d3bbf346 feat: update closed 2025-09-22 13:20:47 -05:00
TrèsAbhi 675a613a0e New translations en.json (Russian) (#170) 2025-09-22 10:06:08 -05:00
TrèsAbhi 50c77e0ae1 fix: seo 2025-09-18 10:43:38 -05:00
TrèsAbhi 6e0429332d feat: mixer tank selector 2025-09-18 10:41:42 -05:00
TrèsAbhi 7afce6cbcb fix: keys 2025-09-18 10:19:41 -05:00
TrèsAbhi 17c7a5c7f5 feat: start of selector 2025-09-13 23:57:10 -05:00
TrèsAbhi f5aa2ff9de feat: reenable mixer 2025-09-13 23:42:26 -05:00
TrèsAbhi 96a8f225fc feat: start on api 2025-09-13 23:07:58 -05:00
TrèsAbhi d15a5aee69 fix: no overflow navbar on certain devices 2025-09-13 22:54:45 -05:00
TrèsAbhi 84dc5e4960 feat: data-driven tools 2025-09-13 22:53:29 -05:00
TrèsAbhi 58b5ee7f83 feat: data-driven tools 2025-09-13 21:54:43 -05:00
TrèsAbhi 8a99d34af9 fix: check intialization this mount 2025-09-13 21:33:16 -05:00
TrèsAbhi 7cd7bd83dc Merge branch 'main' into dev 2025-09-13 21:28:58 -05:00
TrèsAbhi c29d36a4b7 feat: update to main 2025-09-13 10:03:22 -05:00
TrèsAbhi 7d3b6221d7 feat: remake decodeAny 2025-09-13 10:02:46 -05:00
TrèsAbhi fd66f9ab17 feat: compiler based client 2025-09-12 16:49:26 -05:00
TrèsAbhi 6fbb0e81cb simplify error handling 2025-09-12 14:08:53 -05:00
TrèsAbhi 6dc82588c1 feat: cleanup 2025-09-12 13:42:33 -05:00
TrèsAbhi c6eae9ad51 feat: update closed 2025-09-12 13:29:10 -05:00
TrèsAbhi ebdd7629ff update 2025-09-12 13:02:43 -05:00
TrèsAbhi 646bccf370 feat: logs 2025-09-07 16:38:07 -05:00
TrèsAbhi 86206f2d89 Merge branch 'main' into dev 2025-09-07 16:29:28 -05:00
TrèsAbhi bc0dd87e31 Merge branch 'main' into dev 2025-09-07 16:29:09 -05:00
TrèsAbhi e26bb4efe0 feat: some changes to lighting... again 2025-09-06 12:40:35 -05:00
TrèsAbhi 66cf1fd223 feat: baked ao 2025-09-06 00:48:39 -05:00
TrèsAbhi 335cd4d699 feat: rewrite some systems 2025-09-05 23:34:59 -05:00
TrèsAbhi 95fa5bc1a5 fix: use base color map 2025-09-05 20:45:04 -05:00
TrèsAbhi 50eac9eba6 New translations en.json (French) (#169) 2025-09-04 08:04:17 -05:00
TrèsAbhi 1598ae973f feat: auto framerate 2025-09-02 22:44:46 -05:00
TrèsAbhi 69f3a95379 feat: use armor model 2025-09-02 22:16:48 -05:00
TrèsAbhi 3846cbab4a fix: save persistent data 2025-09-02 16:31:11 -05:00
TrèsAbhi 30e87743fc feat: upgrade rest of them 2025-09-02 16:24:13 -05:00
TrèsAbhi fe4afdcbf0 feat: upgrade CompareEphemeral 2025-09-01 23:05:22 -05:00
TrèsAbhi ef2a47e756 feat: upgrade ComparePersistent 2025-09-01 23:02:20 -05:00
TrèsAbhi c2d625e1ed feat: upgrade EmbedBreakdown 2025-09-01 23:00:44 -05:00
TrèsAbhi d33990bdb4 feat: upgrade Gallery 2025-09-01 22:58:26 -05:00
TrèsAbhi 96482d4268 feat: renaming 2025-09-01 22:56:39 -05:00
TrèsAbhi 8b527f2333 fix: last and next checking 2025-09-01 22:55:31 -05:00
TrèsAbhi f56c31fb48 feat: upgrade Guess 2025-09-01 19:22:38 -05:00
TrèsAbhi 7d164e415a feat: upgrade Mixer 2025-09-01 19:19:44 -05:00
TrèsAbhi b3a9f0889e feat: upgrade Session 2025-09-01 19:16:36 -05:00
TrèsAbhi 6d24ed92d0 feat: upgrade TankFilters 2025-09-01 19:13:08 -05:00
TrèsAbhi d287e66265 feat: upgrade tankopediaEphemeral 2025-09-01 19:01:01 -05:00
TrèsAbhi ac29d1ead0 feat: upgrade tankopediaEphemeral 2025-09-01 18:51:04 -05:00
TrèsAbhi b6c3fdc4e9 feat: upgrade performance 2025-09-01 18:16:41 -05:00
TrèsAbhi 4efdaaf7c9 feat: upgrade TankSort 2025-09-01 18:13:41 -05:00
TrèsAbhi 87b6a36520 feat: use custom state manager 2025-09-01 18:03:07 -05:00
TrèsAbhi cf38cb3a6e feat: optimize legacy redirect pages 2025-09-01 17:27:42 -05:00
TrèsAbhi 97fc386bc7 New Crowdin updates (#168)
* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Russian)
2025-08-31 09:54:49 -05:00
TrèsAbhi abfb3f8ddb fix: typo 2025-08-29 23:58:47 -05:00
TrèsAbhi 9cb5113f6e feat: haha 2025-08-29 23:58:14 -05:00
TrèsAbhi 7dd1c0c32a feat: logs 2025-08-29 23:57:33 -05:00
TrèsAbhi 03233f3fdc fix: align antagonist selector filters correctly 2025-08-29 23:46:57 -05:00
TrèsAbhi 377159c298 fix: consider tank models 2025-08-29 23:41:35 -05:00
TrèsAbhi d8431fb620 fix: gap alignemnt 2025-08-29 23:28:49 -05:00
TrèsAbhi 0546ddefbd fix: better shot card alignment 2025-08-29 23:24:12 -05:00
TrèsAbhi 26f6889012 feat: revert subtler advanced highlighting coloring 2025-08-29 23:10:20 -05:00
TrèsAbhi 3ad6088565 feat: skip button for reload visualizer 2025-08-29 23:08:44 -05:00
TrèsAbhi b6fd568cc6 fix: screenshot props 2025-08-29 23:00:02 -05:00
TrèsAbhi 3861b07aef fix: remove "show grid" 2025-08-29 22:53:04 -05:00
TrèsAbhi 2bf57e2c72 feat: better framing 2025-08-29 22:51:24 -05:00
TrèsAbhi 744c6654cf feat: cooler initial position 2025-08-29 22:39:34 -05:00
TrèsAbhi b82e905757 fix: dead code 2025-08-29 13:42:44 -05:00
TrèsAbhi c42e5b7dc3 feat: logs 2025-08-29 13:32:46 -05:00
TrèsAbhi b716218ecd fix: loading bar 2025-08-29 13:31:48 -05:00
TrèsAbhi 511346e4bb fix: select 2025-08-29 13:26:34 -05:00
TrèsAbhi 5c2e49453e feat: more lighting 2025-08-29 13:21:41 -05:00
TrèsAbhi 5d397de766 feat: more progress 2025-08-28 18:23:02 -05:00
TrèsAbhi aa763f65d1 feat: more progress 2025-08-28 17:59:31 -05:00
TrèsAbhi 94cbd95509 feat: start of new lighting 2025-08-28 16:52:23 -05:00
TrèsAbhi f3762e356f feat: start of mixer 2025-08-28 16:12:40 -05:00
TrèsAbhi 5747d7549f feat: tweak a few things and enable guess game 2025-08-27 17:43:42 -05:00
TrèsAbhi aea4905d20 fix: vars 2025-08-27 17:22:06 -05:00
360 changed files with 17249 additions and 8604 deletions

No files matched your search

+3 -6
View File
@@ -1,4 +1,4 @@
name: Build Assets
name: Assets
on:
push:
@@ -21,7 +21,7 @@ on:
description: Root dir of ZIP which houses the executable and Data dir
schedule:
- cron: '0 2 * * *'
- cron: "0 2 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.cdn_branch }}
@@ -67,7 +67,7 @@ jobs:
with:
root-reserve-mb: 512
swap-size-mb: 1024
remove-dotnet: 'true'
remove-dotnet: "true"
- uses: actions/checkout@v4
with:
@@ -131,9 +131,6 @@ jobs:
- name: Install dependencies
run: bun install
- name: Lint
run: bun run lint:website
- name: Download depots
uses: actions/download-artifact@v4
with:
+5 -1
View File
@@ -1,4 +1,4 @@
name: Build Bot
name: Bot
on:
push:
@@ -15,6 +15,10 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
+2 -2
View File
@@ -1,9 +1,9 @@
name: Discover IDs
name: IDs
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 1'
- cron: "0 0 * * 1"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
+15 -2
View File
@@ -1,10 +1,10 @@
name: Host Bot
name: Host
on:
push:
branches: main
schedule:
- cron: '0 */5 * * *'
- cron: "0 */5 * * *"
workflow_dispatch:
release:
types: [created, published, released, edited]
@@ -22,6 +22,19 @@ env:
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 }}
jobs:
host:
+1 -1
View File
@@ -5,7 +5,7 @@ on:
workflow_dispatch:
schedule:
- cron: '0 4 * * *'
- cron: "0 4 * * *"
permissions:
contents: read
+27 -2
View File
@@ -24,7 +24,32 @@ public/sitemap*
target
# packages/core/src/protos/*.ts
/.vercel
/.astro
.astro
.vercel
/src
/packages/bot/src/prisma
/packages/bot/src/prisma
dev-dist
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+3 -3
View File
@@ -1,9 +1,9 @@
[submodule "submodules/blitzkit-closed"]
path = submodules/blitzkit-closed
url = https://github.com/tresabhi/blitzkit-closed
[submodule "submodules/CUE4Parse"]
path = submodules/CUE4Parse
url = https://github.com/FabianFG/CUE4Parse
[submodule "submodules/blitzkit-internal"]
path = submodules/blitzkit-internal
url = https://github.com/blitzkit/internal
[submodule "packages/closed"]
path = packages/closed
url = https://github.com/blitzkit/closed.git
-7
View File
@@ -1,7 +0,0 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 80
}
+10 -7
View File
@@ -21,6 +21,7 @@
"AYUV",
"battleresults",
"bbox",
"beforeinstallprompt",
"bepaint",
"betakrieg",
"BINORMAL",
@@ -76,18 +77,22 @@
"echarts",
"Everytime",
"FASTNAME",
"favicons",
"flexdirection",
"fontsource",
"FOURCC",
"frameloop",
"fsformat",
"Fullscreen",
"fullstats",
"fuzzysort",
"gamemode",
"gameplay",
"glsl",
"GLTF",
"gtag",
"haha",
"Herdbreaker",
"highp",
"iife",
"imgur",
@@ -154,6 +159,7 @@
"Pyogenics",
"ratingleaderboard",
"raycaster",
"Redirector",
"researchable",
"Resvg",
"rizz",
@@ -167,6 +173,7 @@
"searchtanks",
"SHAREDEXP",
"SKLLD",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
@@ -186,6 +193,7 @@
"tankopedia",
"tankopedias",
"tankstats",
"tauri",
"texconv",
"TEXCOORD",
"tresabhi",
@@ -222,12 +230,7 @@
"yolo",
"youtuber",
"youtubers",
"zoomable",
"zustand"
"zoomable"
],
"deno.enable": false,
"deno.unstable": false,
"cSpell.enableFiletypes": ["glsl", "proto3"],
"typescript.tsdk": "node_modules\\typescript\\lib",
"liveServer.settings.multiRootWorkspaceName": "blitzkit"
"typescript.tsdk": "node_modules\\typescript\\lib"
}
+43 -7
View File
@@ -1,13 +1,49 @@
## BlitzKit
# BlitzKit
Your premier source for all things Blitz.
Tools for everything and anything, World of Tanks Blitz.
### https://blitzkit.app/
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
![BlitzKit website](https://i.imgur.com/KaxUxmt.png)
If you intend to contribute to the code, note that you may need to check out sensitive, closed-source submodules to locally develop certain aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or discuss sensitive content, such as environment variables or submodules.
## Invite the bot
## Packages
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
| Package | Title | Source | Memo |
| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| [packages/app](packages/app) | BlitzKit | [Play Store](https://play.google.com/store/apps/details?id=com.tresabhi.blitzkit), [App Store](https://apps.apple.com/app/4321), [Microsoft Store](https://apps.microsoft.com/detail/zyxw) | The native app of BlitzKit for all platforms. Unavailable from stores at this time. |
| [packages/bot](packages/bot) | BlitzKit Discord Bot | [Discord Application Directory](https://discord.com/discovery/applications/1097673957865443370) | The one-in-all Discord bot for statistics. |
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. |
![BlitzKit application directory](https://i.imgur.com/YWCtOuJ.png)
## Submodules
| Submodule | Title | Source | Memo |
| ------------------------------------------------------------ | ----------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [submodules/blitzkit-closed](submodules/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. |
| [submodules/blitzkit-internal](submodules/blitzkit-internal) | InternalKit | [blitzkit/internal](https://github.com/blitzkit/internal) | Closed source, sensitive internal documentation hidden from the public to prevent bad actors from abusing the game. Access to this submodule is **not negotiable**. |
| [submodules/CUE4Parse](submodules/CUE4Parse) | CUE4Parse | [FabianFG/CUE4Parse](https://github.com/FabianFG/CUE4Parse) | An Unreal Engine archives and packages parsing library in C#. |
## Environment Variables
| Name | Source | BlitzKit Default | Memo |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
| `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. |
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
| `PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
| `PUBLIC_BRANCH` | | `main` | The branch that the website is operating on. This is completely nominal. |
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
| `SERVICING` | | `false` | Wether the website is down for servicing. |
| `DISCORD_CLIENT_ID` | [Discord Developer Portal](https://discord.com/developers/applications) | `1097673957865443370` | The BlitzKit Discord Bot's user ID. |
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
| `DISCORD_TOKEN` | [Discord Developer Portal](https://discord.com/developers/applications) | | The Discord API Key for the BlitzKit Discord Bot. |
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
| `WOTB_DLC_CDN` | | | |
+3021
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.27.18
## Features
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
- Switched to a new renderer for better lighting for the Tankopedia
## Fixes
- The loading bar for the Tankopedia sandbox now shows up again
- Removed a lot of dead code, improving performance
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.27.19
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
## Features
- The camera is now lower down in the Tankopedia, giving the tank a more heroic feel
- Tanks are better framed across all devices, minimizing whitespace
- Added a skip button for the reload visualizer to skip to the next event
## Fixes
- Removed the useless "show grid" setting
- Taking screenshots now no longer includes the scene props like the floor
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
- The shot display card is now properly aligned as a proper table again
- Tank modules are once again considered in the penetration tester
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
+13
View File
@@ -0,0 +1,13 @@
# BlitzKit 1.27.20
It's all technical updates! The HD tanks will be rolled out over the span of few days.
## Technical Changes
- Shadows are disabled if your framerate drops below 30fps
- Pages that redirect to other pages (like the old /tools/tankopedia) now load a few hundred times faster
- Replaced Zustand with a custom state manager called Varuna with the goal to rewrite a significant portion of the website to not require any React for rendering increasing performance
- The transition wireframe in the Tankopedia sandbox now uses a lower quality model for performance reasons
- Tanks with PBR now use the correct maximum resolution textures
- This is a significant update as you can now literally see individual splats of dust on tanks and scratches haha
- Baked the ambient occlusion maps into the textures, hence reducing the overall model load times even with higher resolution textures
+16
View File
@@ -0,0 +1,16 @@
# BlitzKit 1.28.0
BlitzKit, as an app and the new Mixer tool!
## Features
- Created the BlitzKit app that can be downloaded on most devices
- Re-did the BlitzKit logo to be a bit warmer
- Added the Mixer tool which lets you combine tank models into Frankensteins
- Updated all dependencies to the latest version for a decent performance boost
## Fixes
- BlitzKit no longer forgets persistent data if the app crashes without properly closing
- The tools list in the navbar no longer overflows into the neighboring elements on certain devices
- Added keying to a few places, speeding up re-rendering
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.28.1
## Fixes
- The lights no longer crash the Mixer tool
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.28.2
## Fixes
- BlitzKit no longer tries to redirect translated pages to the same page, over and over again
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.28.3
## Features
- Turrets and guns can now be rotated in Mixer without any pitch or yaw limits
- Added nation icons to the search results for Guesser
- The table now occupies the whole screen in Compare
- Added the ability to log in with a Wargaming account in the navbar
- Added owned/unowned filters in the Tankopedia filters
- Revamped Tankopedia quick controls to be more accessible and obvious
## Fixes
- Missing tank icons across the website have been fixed
- Events of starting and stopping of the transform tanks now use an asynchronous event instead of a reactive state, making them faster
- Removed a hook call on root context, speeding up load times
- Tankopedia's pitch input now follows the same sign convention as the pitch and yaw visualizer
- Shadows are no longer disabled when the scene is unloaded when the user scrolls away from the canvas
+10
View File
@@ -0,0 +1,10 @@
# BlitzKit 1.28.4
## Fixes
- The tank names in the Tankopedia sandbox now have less margin from the navbar
- Controls in the Tankopedia sandbox are now shown in all modes, except for the advanced highlighting button which only appears in the dynamic armor mode
- The settings page now reacts to input again
- The settings page now has a tab title and description for search engines
- Added support for transparent textures, fixing tanks like the Herdbreaker and the WZ-132-1
- The tank filters in the Performance and Tier-List tools work once again
+18 -10
View File
@@ -1,7 +1,7 @@
{
"name": "blitzkit",
"private": true,
"version": "1.27.17",
"version": "1.28.4",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
@@ -12,15 +12,18 @@
"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": "bun build:prisma --no-hints && cd packages/website && bun run build",
"build:website": "bun build:prisma && cd packages/website && bun run build",
"build:prisma": "prisma generate",
"build:sitemap": "bun scripts/sitemap.ts",
"build:docs": "vitepress build docs",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"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",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website && bun run dev",
"dev:app": "cd packages/app && bun run dev",
"dev": "bun run dev:website",
"dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
@@ -41,19 +44,24 @@
},
"license": "MIT",
"dependencies": {
"@prisma/client": "^6.17.1",
"@types/xmpp__client": "^0.13.3",
"@xmpp/client": "^0.13.6",
"astro-favicons": "^3.1.5",
"cheerio": "^1.1.2",
"googleapis": "^144.0.0",
"googleapis": "^160.0.0",
"lodash-es": "^4.17.21",
"protobufjs": "^7.5.2",
"protobufjs": "^7.5.4",
"quicklime": "^1.2.0",
"sharp": "^0.33.5"
"sharp": "^0.34.4",
"yaml": "^2.8.1"
},
"devDependencies": {
"@types/bun": "latest",
"@types/bun": "^1.2.22",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"prettier": "^3.5.3",
"prisma": "^6.6.0",
"typescript": "^5.8.3"
"@types/node": "^24.5.2",
"prettier": "^3.6.2",
"prisma": "^6.16.2",
"typescript": "^5.9.2"
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@blitzkit/app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/cli": "^2",
"vite": "^6.0.3",
"typescript": "~5.6.2"
}
}
+7
View File
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
+5286
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "blitzkit"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "blitzkit_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
+14
View File
@@ -0,0 +1,14 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+27
View File
@@ -0,0 +1,27 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Command, Stdio};
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
let entry = app_handle
.path_resolver()
.resolve_resource("../../dist/website/server/entry.mjs")
.expect("failed to resolve server.ts");
Command::new("bun")
.arg("run")
.arg(entry)
.stderr(Stdio::null())
.spawn()
.expect("failed to start Bun server");
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running Tauri app");
}
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "blitzkit",
"version": "0.1.0",
"identifier": "com.tresabhi.blitzkit",
"build": {
"beforeDevCommand": "cd ../website && bun run dev",
"_beforeBuildCommand": "cd ../website && bun run build",
"frontendDist": "../../../dist/website/client",
"devUrl": "http://localhost:4321"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"url": "http://localhost:4321",
"title": "BlitzKit",
"fullscreen": false,
"resizable": true,
"maximized": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"extends": ["../../tsconfig.json"],
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
+8 -7
View File
@@ -6,15 +6,16 @@
"dependencies": {
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"satori": "^0.10.14",
"@resvg/resvg-js": "^2.6.2",
"discord.js": "^14.16.3",
"react": "^18.3.1",
"@radix-ui/colors": "^3.0.0",
"fuzzysort": "^2.0.4",
"@resvg/resvg-js": "^2.6.2",
"bepaint": "^0.1.0-beta.3",
"dotenv": "^16.4.5",
"markdown-escape": "^2.0.0"
"discord.js": "^14.22.1",
"dotenv": "^17.2.2",
"fuzzysort": "^3.1.0",
"markdown-escape": "^2.0.0",
"prisma": "^6.17.1",
"react": "^19.1.1",
"satori": "^0.18.3"
},
"scripts": {
"dev": "bun --watch src/main.ts",
-144
View File
@@ -1,144 +0,0 @@
import {
BlitzTankClass,
Region,
regionToRegionSubdomain,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises';
import { chunkLines } from '../core/discord/chunkLines';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
type Auction = null | {
count: number;
has_next: boolean;
results: Result[];
};
type Result =
| {
id?: number;
type: 'vehicle';
countable?: boolean;
entity?: Entity;
initial_count: number;
current_count: number;
saleable?: boolean;
available_from?: Date;
available_before?: Date;
price?: Price;
next_price: Price | null;
available: true;
display?: boolean;
next_price_datetime: Date;
next_price_timestamp: number;
}
| {
available: false;
type: 'vehicle';
};
interface Entity {
id: number;
name: string;
nation: string;
subnation: string;
use_subnation_flag: boolean;
type_slug: BlitzTankClass;
level: number;
roman_level: string;
user_string: string;
image_url: string;
preview_image_url: string;
is_premium: boolean;
is_collectible: boolean;
}
interface Price {
currency: Currency;
value: number;
}
interface Currency {
name: 'gold';
count: number;
title: 'Gold';
image_url: string;
sizes: unknown;
type: 'currency';
}
const WARNING_COUNT = 500;
export const auctionCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('auction'),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const region: Region = 'com';
const pageSize = 80;
const saleable = true;
const data = await fetch(
`https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/events/items/auction/?page_size=${
pageSize
}&type[]=vehicle&saleable=${saleable}`,
).then((response) => response.json() as Promise<Auction>);
if (data === null) {
return strings.bot.commands.auction.errors.no_auction;
}
const saleableTanks = data.results.filter((result) => result.available);
const nextPricesTimestamp = saleableTanks[0].next_price_timestamp;
const title = `# ${strings.bot.commands.auction.body.title}`;
const subtitle = `${literals(strings.bot.commands.auction.body.subtitle, [
`${saleableTanks.length} / ${data.results.length}`,
`<t:${nextPricesTimestamp}:R>`,
'<https://wotblitz.com/auction/>',
])}`;
const body = saleableTanks.map((data, index) => {
const tank = awaitedTankDefinitions.tanks[data.entity!.id];
if (!tank) return '';
const name = `[${tank.name}](<https://blitzkit.app/tanks/${
tank.slug
}>)`;
const next =
data.next_price === null
? ''
: `\n-# ${literals(strings.bot.commands.auction.body.next, [
`<:gold:1317173197082333244> ${data.next_price.value.toLocaleString(
interaction.locale,
)}`,
])}`;
const available = literals(
strings.bot.commands.auction.body.available,
[
data.current_count.toLocaleString(interaction.locale),
data.initial_count.toLocaleString(interaction.locale),
],
);
const isOut = data.current_count === 0;
const outString = isOut ? '~~' : '';
const isLow = data.current_count <= WARNING_COUNT;
return `${index + 1}. ${
outString
}${name} <:gold:1317173197082333244> ${data.price!.value.toLocaleString(
interaction.locale,
)}${outString}\n-#${isLow ? ' ‼️ ' : ' '}${available}${isOut ? '' : next}`;
});
const header = `${title}\n${subtitle}`;
const lines = [header, ...body];
return chunkLines(lines, '\n\n');
},
});
});
+40 -40
View File
@@ -6,23 +6,23 @@ import {
getTankStats,
getWN8Percentile,
TankDefinition,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { chunk } from 'lodash-es';
import * as Breakdown from '../../components/Breakdown';
import { CommandWrapper } from '../../components/CommandWrapper';
import { NoData } from '../../components/NoData';
import { TitleBar } from '../../components/TitleBar';
import { filtersToDescription } from '../../core/blitzkit/filtersToDescription';
import { tankDefinitions } from '../../core/blitzkit/nonBlockingPromises';
import { UserError } from '../../core/blitzkit/userError';
import { filterStats, StatFilters } from '../../core/blitzstars/filterStats';
import { getStatsInPeriod } from '../../core/blitzstars/getStatsInPeriod';
import { blitzStarsTankAverages } from '../../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand';
import { translator } from '../../core/localization/translator';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { chunk } from "lodash-es";
import * as Breakdown from "../../components/Breakdown";
import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import { tankDefinitions } from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import { filterStats, StatFilters } from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import { blitzStarsTankAverages } from "../../core/blitzstars/tankAverages";
import { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
@@ -31,7 +31,7 @@ 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;
@@ -39,7 +39,7 @@ export async function renderBreakdown(
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ['clan']);
const clanData = await getClanAccountInfo(region, id, ["clan"]);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: BlitzStats[] = [];
@@ -74,63 +74,63 @@ export async function renderBreakdown(
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, tank.all)
: undefined,
: 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: JSX.Element[] = [];
const children: React.JSX.Element[] = [];
if (filteredOrder.length > 0) {
children.push(
@@ -172,7 +172,7 @@ export async function renderBreakdown(
delta: currentDamage - careerDamage,
},
]}
/>,
/>
);
filteredOrder.forEach((id, index) => {
@@ -193,9 +193,9 @@ export async function renderBreakdown(
title={
tankDefinition
? unwrap(tankDefinition.name)
: literals(strings.bot.commands.breakdown.body.unknown_tank, [
`${id}`,
])
: literals(strings.bot.commands.breakdown.body.unknown_tank, {
id,
})
}
stats={[
{
@@ -206,7 +206,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 +230,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) {
+28 -28
View File
@@ -1,44 +1,44 @@
import { getAccountInfo, getClanInfo } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { CommandWrapper } from '../components/CommandWrapper';
import { GenericStats } from '../components/GenericStats';
import { NoData } from '../components/NoData';
import { TitleBar } from '../components/TitleBar';
import { addClanChoices } from '../core/discord/addClanChoices';
import { autocompleteClan } from '../core/discord/autocompleteClan';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolveClanFromCommand } from '../core/discord/resolveClanFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { getAccountInfo, getClanInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericStats } from "../components/GenericStats";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { addClanChoices } from "../core/discord/addClanChoices";
import { autocompleteClan } from "../core/discord/autocompleteClan";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_THRESHOLD = 7;
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('inactive')
command: createLocalizedCommand("inactive")
.addStringOption(addClanChoices)
.addNumberOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName('threshold')
.setName("threshold")
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.inactive.options.threshold.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.inactive.options.threshold.description,
strings.bot.commands.inactive.options.threshold.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.inactive.options.threshold.description,
),
strings.bot.commands.inactive.options.threshold.description
)
)
.setMinValue(0);
}),
@@ -47,7 +47,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(interaction.locale);
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
@@ -57,7 +57,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number],
] as [string, number]
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
@@ -65,10 +65,10 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
([name, days]) =>
[
name,
literals(strings.bot.commands.inactive.body.listing, [
days.toFixed(0),
]),
] as [string, string],
literals(strings.bot.commands.inactive.body.listing, {
days: days.toFixed(0),
}),
] as [string, string]
);
const hasInactiveMembers = inactive.length > 0;
@@ -79,7 +79,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${literals(
strings.bot.commands.inactive.body.subtitle,
[`${threshold}`],
{ days: threshold }
)} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
+22 -22
View File
@@ -5,31 +5,31 @@ import {
flags,
getAccountInfo,
getTankStats,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import markdownEscape from 'markdown-escape';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import {
gameDefinitions,
tankDefinitions,
} from '../core/blitzkit/nonBlockingPromises';
import { addTierChoices } from '../core/discord/addTierChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { chunkLines } from '../core/discord/chunkLines';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
} from "../core/blitzkit/nonBlockingPromises";
import { addTierChoices } from "../core/discord/addTierChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { chunkLines } from "../core/discord/chunkLines";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('owned-tanks')
command: createLocalizedCommand("owned-tanks")
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const tier = Number(interaction.options.getString('tier'));
const tier = Number(interaction.options.getString("tier"));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id);
@@ -43,7 +43,7 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id,
})),
}))
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {};
@@ -60,25 +60,25 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
const awaitedTankDefinitions = await tankDefinitions;
const awaitedGameDefinitions = await gameDefinitions;
const title = literals(strings.bot.commands.owned_tanks.body.title, [
markdownEscape(accountInfo.nickname),
TIER_ROMAN_NUMERALS[tier],
]);
const title = literals(strings.bot.commands.owned_tanks.body.title, {
name: markdownEscape(accountInfo.nickname),
tier: TIER_ROMAN_NUMERALS[tier],
});
const tankList = tankStats
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class)
)
.sort(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation),
awaitedGameDefinitions.nations.indexOf(b.nation)
)
.map(
(tank) =>
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)`
);
const lines = [title, ...tankList];
+13 -13
View File
@@ -1,11 +1,11 @@
import { literals } from '@blitzkit/i18n';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('permissions'),
command: createLocalizedCommand("permissions"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
@@ -14,33 +14,33 @@ export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has('ViewChannel'),
interaction.appPermissions?.has("ViewChannel"),
strings.bot.commands.permissions.body.view_channels,
],
[
interaction.appPermissions?.has('ReadMessageHistory'),
interaction.appPermissions?.has("ReadMessageHistory"),
strings.bot.commands.permissions.body.read_message_history,
],
[
interaction.appPermissions?.has('AttachFiles'),
interaction.appPermissions?.has("AttachFiles"),
strings.bot.commands.permissions.body.attach_files,
],
[
interaction.appPermissions?.has('UseExternalEmojis'),
interaction.appPermissions?.has("UseExternalEmojis"),
strings.bot.commands.permissions.body.external_emojis,
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
} else {
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
return `${isFridayThe13th ? "💀" : "❌"} ${description}`;
}
})
.join('\n');
.join("\n");
return literals(strings.bot.commands.permissions.body.title, [body]);
return literals(strings.bot.commands.permissions.body.title, { body });
},
});
});
+12 -12
View File
@@ -1,15 +1,15 @@
import { literals } from '@blitzkit/i18n';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
handlesInteraction: true,
command: createLocalizedCommand('ping', [
{ subcommand: 'blitzkit' },
{ subcommand: 'wotb' },
command: createLocalizedCommand("ping", [
{ subcommand: "blitzkit" },
{ subcommand: "wotb" },
]),
async handler(interaction) {
@@ -17,17 +17,17 @@ export const pingCommand = new Promise<CommandRegistry>((resolve) => {
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === 'blitzkit') {
if (subcommand === "blitzkit") {
await interaction.editReply(strings.bot.commands.ping.body.pong);
} else {
await fetch('https://api.wotblitz.com/');
await fetch("https://api.wotblitz.com/");
}
const executionTime = Date.now() - executionStart;
interaction.editReply(
literals(strings.bot.commands.ping.body.pong_with_time, [
`${Math.round(executionTime)}`,
]),
literals(strings.bot.commands.ping.body.pong_with_time, {
time: executionTime.toFixed(0),
})
);
},
});
+18 -18
View File
@@ -1,19 +1,19 @@
import { getAccountInfo } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import markdownEscape from 'markdown-escape';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { markdownTable } from '../core/discord/markdownTable';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { getAccountInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { markdownTable } from "../core/discord/markdownTable";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command:
createLocalizedCommand('player-info').addStringOption(addUsernameChoices),
createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
@@ -22,9 +22,9 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
literals(strings.bot.commands.player_info.body.title, [
markdownEscape(accountInfo.nickname),
]),
literals(strings.bot.commands.player_info.body.title, {
name: markdownEscape(accountInfo.nickname),
}),
markdownTable([
[
@@ -51,16 +51,16 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
[
strings.bot.commands.player_info.body.created,
new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale,
interaction.locale
),
],
[
strings.bot.commands.player_info.body.last_battle,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale,
interaction.locale
),
],
]),
])
);
},
+118 -118
View File
@@ -8,42 +8,42 @@ import {
getRatingInfo,
getRatingNeighbors,
imgur,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { SlashCommandSubcommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
import { CommandWrapper } from '../components/CommandWrapper';
import { DeltaCaret } from '../components/DeltaCaret';
import { TitleBar } from '../components/TitleBar';
import { normalizeLeagueIcon } from '../core/blitz/normalizeLeagueIcon';
import { iconPng } from '../core/blitzkit/iconPng';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { SlashCommandSubcommandBuilder } from "discord.js";
import markdownEscape from "markdown-escape";
import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
import { CommandWrapper } from "../components/CommandWrapper";
import { DeltaCaret } from "../components/DeltaCaret";
import { TitleBar } from "../components/TitleBar";
import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
import { iconPng } from "../core/blitzkit/iconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
const LEAGUE_ACCENT = [
'_purple',
'_sand',
'_amber',
'_mauve',
'_brown',
"_purple",
"_sand",
"_amber",
"_mauve",
"_brown",
] as const;
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('rating', [
command: createLocalizedCommand("rating", [
{
subcommand: 'today',
subcommand: "today",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
{
subcommand: 'season',
subcommand: "season",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
@@ -52,11 +52,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true) as
| 'season'
| 'today';
| "season"
| "today";
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
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 ratingInfo = await getRatingInfo(region);
@@ -67,20 +67,20 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const leaderboard = await getArchivedRatingLeaderboard(
region,
ratingInfo.current_season - (subcommand === 'today' ? 0 : 1),
ratingInfo.current_season - (subcommand === "today" ? 0 : 1)
);
if (leaderboard.version!.$case === 'v1') {
if (leaderboard.version!.$case === "v1") {
throw new Error(
'Encountered bkrl minimal format in latest season. Wait till next cron?',
"Encountered bkrl minimal format in latest season. Wait till next cron?"
);
}
const accountInfo = await getAccountInfo(region, id, [
'statistics.rating',
"statistics.rating",
]);
const statsIndex = leaderboard.version!.value.entries.findIndex(
(entry) => entry.id === id,
(entry) => entry.id === id
);
const statsA = leaderboard
? leaderboard.version!.value.entries[statsIndex!]
@@ -90,9 +90,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
if (statsB2Array === undefined) {
return literals(strings.bot.commands.rating.errors.no_participation, [
markdownEscape(accountInfo.nickname),
]);
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const [statsB2] = statsB2Array;
@@ -102,15 +102,15 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
return strings.bot.commands.rating.errors.unrecorded_stats;
}
if (!statsB1) {
return literals(strings.bot.commands.rating.errors.no_participation, [
markdownEscape(accountInfo.nickname),
]);
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const reward = ratingInfo.rewards.find(
(reward) =>
statsB2.number >= reward.from_position &&
statsB2.number <= reward.to_position,
statsB2.number <= reward.to_position
);
const positionDelta = leaderboard
? statsB2.number - positionA!
@@ -118,11 +118,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const delta = deltaBkrlBlitzStats(
statsA,
accountInfo.statistics.rating!,
statsB2,
statsB2
);
const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward
? reward.type === 'vehicle'
? reward.type === "vehicle"
? reward.vehicle.preview_image_url
: reward.stuff.image_url
: undefined;
@@ -131,7 +131,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const { ok } = await fetch(rewardImage);
if (!ok) {
rewardImage = imgur('kP88vqr');
rewardImage = imgur("kP88vqr");
}
}
@@ -147,24 +147,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
position: 'relative',
display: 'flex',
overflow: 'hidden',
position: "relative",
display: "flex",
overflow: "hidden",
}}
>
<div
style={{
position: 'relative',
position: "relative",
padding: 16,
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
borderRadius: 4,
border: `1px solid ${
theme.colors[`componentInteractive${leagueTheme}`]
}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
{reward === undefined && (
@@ -172,17 +172,17 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: 'translateX(-16px) rotate(90deg)',
transform: "translateX(-16px) rotate(90deg)",
}}
/>
)}
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
}}
>
<img
@@ -190,24 +190,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{
width: 64,
height: 64,
objectFit: 'contain',
objectFit: "contain",
}}
src={normalizeLeagueIcon(
ratingInfo.leagues[league.index].big_icon,
ratingInfo.leagues[league.index].big_icon
)}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
@@ -232,7 +232,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
{Math.abs(delta.score).toLocaleString(
interaction.locale,
interaction.locale
)}
</span>
</>
@@ -241,9 +241,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
@@ -267,7 +267,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
{Math.abs(positionDelta).toLocaleString(
interaction.locale,
interaction.locale
)}
</span>
</>
@@ -279,23 +279,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
{reward && (
<div
style={{
display: 'flex',
display: "flex",
gap: 8,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
@@ -303,13 +303,13 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 16,
maxWidth: 136,
textAlign: 'right',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
textAlign: "right",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{reward.type === 'vehicle'
{reward.type === "vehicle"
? reward.vehicle.user_string
: reward.stuff.title}
</span>
@@ -317,9 +317,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
@@ -329,7 +329,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
Reward
{reward.type === 'stuff' ? ` x${reward.count}` : ''}
{reward.type === "stuff" ? ` x${reward.count}` : ""}
</span>
</div>
</div>
@@ -338,7 +338,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{
width: 64,
height: 64,
objectFit: 'cover',
objectFit: "cover",
}}
src={await iconPng(rewardImage!)}
/>
@@ -350,7 +350,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: 'translateX(16px) rotate(-90deg)',
transform: "translateX(16px) rotate(-90deg)",
}}
/>
)}
@@ -362,10 +362,10 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
direction="reverse"
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: 'absolute',
position: "absolute",
top: -40,
left: '50%',
transform: 'translateX(-50%)',
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
@@ -373,32 +373,32 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<Glow
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: 'absolute',
position: "absolute",
bottom: -40,
left: '50%',
transform: 'translateX(-50%)',
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
</div>
{delta.battles > 0 && (
<div style={{ padding: '8px 0', display: 'flex' }}>
<div style={{ padding: "8px 0", display: "flex" }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{delta.battles.toLocaleString(interaction.locale)}
@@ -414,19 +414,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
@@ -442,23 +442,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{Math.round(delta.damage / delta.battles).toLocaleString(
interaction.locale,
interaction.locale
)}
</span>
<span
@@ -472,19 +472,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
@@ -500,19 +500,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: 'column',
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: "bold",
}}
>
{(delta.kills / delta.battles).toFixed(2)}
+79 -80
View File
@@ -13,27 +13,27 @@ import {
RatingInfo,
RatingLeaderboard,
Region,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from 'discord.js';
import { range } from 'lodash-es';
import markdownEscape from 'markdown-escape';
import { CommandWrapper } from '../components/CommandWrapper';
import * as Leaderboard from '../components/Leaderboard';
import { TitleBar } from '../components/TitleBar';
import { UserError } from '../core/blitzkit/userError';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
} from "discord.js";
import { range } from "lodash-es";
import markdownEscape from "markdown-escape";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Leaderboard from "../components/Leaderboard";
import { TitleBar } from "../components/TitleBar";
import { UserError } from "../core/blitzkit/userError";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
interface SimplifiedPlayer {
id: number;
@@ -51,7 +51,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range(
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
);
function addOptions(option: SlashCommandSubcommandBuilder) {
@@ -66,51 +66,50 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit.name,
undefined,
true,
),
true
)
)
.setDescription(
literals(
strings.bot.commands.rating_leaderboard.options.limit
.description,
[`${DEFAULT_LIMIT}`],
),
{ count: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit
.description,
[`${DEFAULT_LIMIT}`],
),
{ count: DEFAULT_LIMIT }
)
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
.setMaxValue(30)
)
.addStringOption((option) =>
option
.setName(
strings.bot.commands.rating_leaderboard.options.season.name,
strings.bot.commands.rating_leaderboard.options.season.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.season
.description,
strings.bot.commands.rating_leaderboard.options.season.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season
.description,
),
.description
)
)
.setRequired(false)
.addChoices(
@@ -124,7 +123,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? literals(
strings.bot.commands.rating_leaderboard.options.season
.choices.current,
[`${number}`],
{ season: number }
)
: `${number}`,
value: `${number}`,
@@ -133,27 +132,27 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
strings.bot.commands.rating_leaderboard.options
.season.choices.current,
[`${number}`],
{ season: number }
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse(),
),
.reverse()
)
);
}
resolve({
command: createLocalizedCommand('rating-leaderboard', [
command: createLocalizedCommand("rating-leaderboard", [
{
subcommand: 'neighbors',
subcommand: "neighbors",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: 'league',
subcommand: "league",
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
@@ -161,7 +160,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
return option
.setName(
strings.bot.commands.rating_leaderboard.options.league.name,
strings.bot.commands.rating_leaderboard.options.league.name
)
.setNameLocalizations(
localizationObject(
@@ -169,19 +168,19 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
strings.bot.commands.rating_leaderboard.options.league
.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.league
.description,
.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.description,
),
.description
)
)
.addChoices(
...LEAGUES.map(
@@ -195,10 +194,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
(
strings.common.leagues as Record<string, string>
)[name],
)[name]
),
}) satisfies APIApplicationCommandOptionChoice<string>,
),
}) satisfies APIApplicationCommandOptionChoice<string>
)
)
.setRequired(true);
})
@@ -213,10 +212,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbors';
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString('season');
| "league"
| "neighbors";
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString("season");
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
@@ -232,11 +231,11 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
let playerId: number | undefined;
let regionRatingInfo: RatingInfo | null;
if (subcommand === 'league') {
if (subcommand === "league") {
const leagueIndex = parseInt(
interaction.options.getString('league')!,
interaction.options.getString("league")!
);
const region = interaction.options.getString('region') as Region;
const region = interaction.options.getString("region") as Region;
regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region)
: null);
@@ -259,8 +258,8 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
}) satisfies SimplifiedPlayer
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
@@ -269,17 +268,17 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? 0
: players.version!.value.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
player.score < LEAGUES[leagueIndex - 1].minScore
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.version!.value.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
lastPlayerIndex + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
["clan"]
);
return trimmed.map(
@@ -294,16 +293,16 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals(
strings.bot.commands.rating_leaderboard.body
.deleted_player,
[`${player.id}`],
{ id: player.id }
),
}) satisfies SimplifiedPlayer,
}) satisfies SimplifiedPlayer
);
},
}
);
const leagueInfo = LEAGUES[leagueIndex];
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season,
season
);
players = result;
@@ -316,12 +315,12 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? (strings.common.leagues as Record<string, string>)[
LEAGUES[leagueInfo.index].name
]
: ''
: ""
} - ${strings.common.regions.short[region]}`;
titleImage = imgur(leagueInfo.icon);
titleDescription = literals(
strings.bot.commands.rating_leaderboard.body.top_players,
[`${limit}`],
{ count: limit }
);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
@@ -338,7 +337,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
}
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
@@ -350,13 +349,13 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
}) satisfies SimplifiedPlayer
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.version!.value.entries.findIndex(
(player) => player.id === id,
(player) => player.id === id
);
if (playerIndex === -1) {
@@ -364,20 +363,20 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals(
strings.bot.commands.rating_leaderboard.body
.no_participation,
[markdownEscape(accountInfo.nickname), `${season}`],
),
{ player: markdownEscape(accountInfo.nickname), season }
)
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.version!.value.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
playerIndex + halfRange + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
['clan'],
["clan"]
);
return trimmed.map((player, index) => ({
@@ -389,18 +388,18 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
},
}
);
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season,
season
);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : '';
titleName = neighbors ? accountInfo.nickname : "";
titleDescription =
strings.bot.commands.rating_leaderboard.body.neighbors;
playersAfter =
@@ -414,7 +413,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const items = players.map((player) => {
const midnightIndex =
archivedLeaderboard?.version!.value.entries.findIndex(
(item) => item.id === player.id,
(item) => item.id === player.id
);
const midnightScore =
midnightIndex === undefined
@@ -453,7 +452,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
<Leaderboard.Gap
message={literals(
strings.bot.commands.rating_leaderboard.body.more,
[`${playersAfter.toLocaleString(interaction.locale)}`],
{ count: playersAfter.toLocaleString(interaction.locale) }
)}
/>
)}
@@ -465,5 +464,5 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
autocomplete: autocompleteUsername,
});
},
}
);
+136 -137
View File
@@ -8,31 +8,31 @@ import {
TANK_ICONS,
TankType,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { escapeMarkdown, Locale } from 'discord.js';
import { CommandWrapper } from '../components/CommandWrapper';
import { TitleBar } from '../components/TitleBar';
import { resolveTankId } from '../core/blitz/resolveTankId';
import { buildTechTreeLine } from '../core/blitzkit/buildTechTreeLine';
import { iconPng } from '../core/blitzkit/iconPng';
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises';
import { resolveAncestry } from '../core/blitzkit/resolveAncestry';
import { tankIconPng } from '../core/blitzkit/tankIconPng';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteTanks } from '../core/discord/autocompleteTanks';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { escapeMarkdown, Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { resolveTankId } from "../core/blitz/resolveTankId";
import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
import { iconPng } from "../core/blitzkit/iconPng";
import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
import { tankIconPng } from "../core/blitzkit/tankIconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand('research')
command: createLocalizedCommand("research")
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.target_tank.name)
@@ -41,20 +41,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.research.options.target_tank.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.research.options.target_tank.description,
strings.bot.commands.research.options.target_tank.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.description,
),
strings.bot.commands.research.options.target_tank.description
)
)
.setAutocomplete(true)
.setRequired(true),
.setRequired(true)
)
.addStringOption((option) =>
option
@@ -64,20 +64,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.research.options.starting_tank.name,
undefined,
true,
),
true
)
)
.setDescription(
strings.bot.commands.research.options.starting_tank.description,
strings.bot.commands.research.options.starting_tank.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.description,
),
strings.bot.commands.research.options.starting_tank.description
)
)
.setAutocomplete(true)
.setRequired(false),
.setRequired(false)
)
.addStringOption(addUsernameChoices),
@@ -85,11 +85,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings, unwrap } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId(
interaction.options.getString('target-tank', true),
interaction.options.getString("target-tank", true),
interaction.locale,
true,
true
);
const startingTankRaw = interaction.options.getString('starting-tank');
const startingTankRaw = interaction.options.getString("starting-tank");
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
@@ -99,46 +99,45 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
true
);
if (targetTankId === startingTankId) {
return literals(
strings.bot.commands.research.errors.start_end_equal,
[escapeMarkdown(unwrap(targetTank.name))],
{ tank: escapeMarkdown(unwrap(targetTank.name)) }
);
}
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
if (startingTank.tier > targetTank.tier) {
return literals(strings.bot.commands.research.errors.unordered, [
escapeMarkdown(unwrap(startingTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
]);
return literals(strings.bot.commands.research.errors.unordered, {
tank_1: escapeMarkdown(unwrap(startingTank.name)),
tank_2: escapeMarkdown(unwrap(targetTank.name)),
});
}
if (!targetTankAncestry.includes(startingTankId)) {
return literals(
strings.bot.commands.research.errors.tanks_not_on_line,
[
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(startingTank.name)),
],
{
tank_1: escapeMarkdown(unwrap(targetTank.name)),
tank_2: escapeMarkdown(unwrap(startingTank.name)),
}
);
}
} else {
if (targetTank.type !== TankType.RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, [
escapeMarkdown(unwrap(targetTank.name)),
]);
return literals(strings.bot.commands.research.errors.non_tech_tree, {
tank: escapeMarkdown(unwrap(targetTank.name)),
});
}
if (targetTank.tier === 1) {
return literals(strings.bot.commands.research.errors.tier_1, [
escapeMarkdown(unwrap(targetTank.name)),
]);
return literals(strings.bot.commands.research.errors.tier_1, {
tank: escapeMarkdown(unwrap(targetTank.name)),
});
}
const tankStats = await getTankStats(region, id);
@@ -150,18 +149,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return literals(
strings.bot.commands.research.errors.already_researched,
[escapeMarkdown(unwrap(targetTank.name))],
{ tank: escapeMarkdown(unwrap(targetTank.name)) }
);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
awaitedTankDefinitions.tanks[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id),
tankStats.some(({ tank_id }) => tank_id === id)
);
if (foundAncestor === undefined) {
throw new Error('No first owned tech tree found.');
throw new Error("No first owned tech tree found.");
}
startingTankId = foundAncestor;
@@ -169,12 +168,12 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
targetTankId
);
const line = [...techTreeLine, startingTankId].reverse();
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 costs = await Promise.all(
line.map(async (id) => {
@@ -187,18 +186,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.turrets.forEach((turret) => {
if (
turret.research_cost !== undefined &&
typeof turret.research_cost.research_cost_type!.value === 'number'
typeof turret.research_cost.research_cost_type!.value === "number"
) {
turretXps.set(
turret.id,
turret.research_cost.research_cost_type!.value,
turret.research_cost.research_cost_type!.value
);
}
turret.guns.forEach((gun) => {
if (
gun.research_cost !== undefined &&
typeof gun.research_cost.research_cost_type!.value === 'number'
typeof gun.research_cost.research_cost_type!.value === "number"
) {
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
}
@@ -208,11 +207,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.engines.forEach((engine) => {
if (
engine.research_cost !== undefined &&
typeof engine.research_cost.research_cost_type!.value === 'number'
typeof engine.research_cost.research_cost_type!.value === "number"
) {
engineXps.set(
engine.id,
engine.research_cost.research_cost_type!.value,
engine.research_cost.research_cost_type!.value
);
}
});
@@ -220,11 +219,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.tracks.forEach((track) => {
if (
track.research_cost !== undefined &&
typeof track.research_cost.research_cost_type!.value === 'number'
typeof track.research_cost.research_cost_type!.value === "number"
) {
trackXps.set(
track.id,
track.research_cost.research_cost_type!.value,
track.research_cost.research_cost_type!.value
);
}
});
@@ -249,10 +248,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
purchase: tank.price.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
0
),
};
}),
})
);
const image = (
@@ -267,14 +266,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
flexDirection: 'column',
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<div
style={{
display: 'flex',
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
@@ -285,10 +284,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
right: 24,
}}
>
@@ -304,23 +303,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: '16px 0',
padding: "16px 0",
}}
>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset('icons/currencies/xp.webp'))}
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
@@ -330,7 +329,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Experience:{' '}
Experience:{" "}
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
@@ -339,14 +338,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
@@ -356,7 +355,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Purchase:{' '}
Purchase:{" "}
{costs
.reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)}
@@ -365,14 +364,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
@@ -382,7 +381,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Equipment:{' '}
Equipment:{" "}
{costs
.reduce((a, b) => a + b.equipment, 0)
.toLocaleString(interaction.locale)}
@@ -399,7 +398,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
key={id}
style={{
display: 'flex',
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
@@ -408,9 +407,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
width: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
@@ -426,9 +425,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
position: 'relative',
right: '5%',
display: "flex",
position: "relative",
right: "5%",
}}
>
<img
@@ -437,23 +436,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
style={{
height: 100,
width: 100,
objectFit: 'contain',
position: 'absolute',
left: '25%',
objectFit: "contain",
position: "absolute",
left: "25%",
top: 0,
}}
/>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
alignItems: "center",
justifyContent: "center",
position: "absolute",
bottom: 8,
left: '50%',
transform: 'translateX(-50%)',
left: "50%",
transform: "translateX(-50%)",
}}
>
<img
@@ -476,23 +475,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: '16px 0',
padding: "16px 0",
}}
>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset('icons/currencies/xp.webp'))}
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
@@ -504,11 +503,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.research,
[
`${costs[index].research?.toLocaleString(
interaction.locale,
)}`,
],
{
cost: costs[index].research?.toLocaleString(
interaction.locale
),
}
)}
</span>
</div>
@@ -516,15 +515,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
{costs[index].upgrades > 0 && (
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Free XP icon"
src={await iconPng(
asset('icons/currencies/free-xp.webp'),
asset("icons/currencies/free-xp.webp")
)}
width={16}
height={16}
@@ -537,7 +536,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.upgrades,
[costs[index].upgrades.toLocaleString()],
{ cost: costs[index].upgrades.toLocaleString() }
)}
</span>
</div>
@@ -545,15 +544,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset('icons/currencies/silver.webp'),
asset("icons/currencies/silver.webp")
)}
width={16}
height={16}
@@ -566,26 +565,26 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.purchase,
[
costs[index].purchase.toLocaleString(
interaction.locale,
{
cost: costs[index].purchase.toLocaleString(
interaction.locale
),
],
}
)}
</span>
</div>
<div
style={{
display: 'flex',
display: "flex",
gap: 4,
alignItems: 'center',
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset('icons/currencies/silver.webp'),
asset("icons/currencies/silver.webp")
)}
width={16}
height={16}
@@ -598,18 +597,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.equipment,
[
costs[index].equipment.toLocaleString(
interaction.locale,
{
cost: costs[index].equipment.toLocaleString(
interaction.locale
),
],
}
)}
</span>
</div>
</div>
</div>
);
}),
})
)}
</div>
</CommandWrapper>
@@ -621,7 +620,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]);
},
});
});
+28 -28
View File
@@ -1,23 +1,23 @@
import { ClanList, fetchBlitz, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { addClanChoices } from '../core/discord/addClanChoices';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { ClanList, fetchBlitz, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addClanChoices } from "../core/discord/addClanChoices";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('search-clans')
command: createLocalizedCommand("search-clans")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addClanChoices(option).setAutocomplete(false),
addClanChoices(option).setAutocomplete(false)
)
.addIntegerOption((option) => {
const { strings } = translator(Locale.EnglishUS);
@@ -28,21 +28,21 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
localizationObject(
(strings) => strings.bot.commands.search_clans.options.limit.name,
undefined,
true,
),
true
)
)
.setDescription(
literals(
strings.bot.commands.search_clans.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_clans.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100);
@@ -50,24 +50,24 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, 'clans/list', {
const region = interaction.options.getString("region") as Region;
const clan = interaction.options.getString("clan")!;
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, "clans/list", {
search: clan,
limit,
});
return embedInfo(
literals(strings.bot.commands.search_clans.body.title, [
markdownEscape(clan),
strings.common.regions.normal[region],
]),
literals(strings.bot.commands.search_clans.body.title, {
search: markdownEscape(clan),
region: strings.common.regions.normal[region],
}),
clanList.length === 0
? strings.bot.commands.search_clans.body.no_results
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join('\n')}\n\`\`\``,
.join("\n")}\n\`\`\``
);
},
});
+30 -30
View File
@@ -3,17 +3,17 @@ import {
fetchBlitz,
Region,
usernamePattern,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
@@ -21,10 +21,10 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand('search-players')
command: createLocalizedCommand("search-players")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
addUsernameChoices(option).setAutocomplete(false).setRequired(true)
)
.addIntegerOption((option) =>
option
@@ -34,49 +34,49 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.search_players.options.limit.name,
undefined,
true,
),
true
)
)
.setDescription(
literals(
strings.bot.commands.search_players.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.description,
[`${DEFAULT_LIMIT}`],
),
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100),
.setMaxValue(100)
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString('region') as Region;
const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const region = interaction.options.getString("region") as Region;
const name = interaction.options.getString("username")!;
const limit = interaction.options.getInteger("limit") ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(region, 'account/list', {
? await fetchBlitz<AccountList>(region, "account/list", {
search: trimmedSearch,
limit,
})
: [];
return embedInfo(
literals(strings.bot.commands.search_players.body.title, [
markdownEscape(trimmedSearch),
strings.common.regions.normal[region],
]),
literals(strings.bot.commands.search_players.body.title, {
search: markdownEscape(trimmedSearch),
region: strings.common.regions.normal[region],
}),
`\`\`\`${
players.length === 0
? strings.bot.commands.search_players.body.no_results
: players.map((player) => player.nickname).join('\n')
}\`\`\``,
: players.map((player) => player.nickname).join("\n")
}\`\`\``
);
},
});
+18 -16
View File
@@ -1,33 +1,33 @@
import { SEARCH_KEYS } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape';
import { tankDefinitions, tankNames } from '../blitzkit/nonBlockingPromises';
import { UserError } from '../blitzkit/userError';
import { tankNamesTechTreeOnly } from '../discord/autocompleteTanks';
import { translator } from '../localization/translator';
import { SEARCH_KEYS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { go } from "fuzzysort";
import markdownEscape from "markdown-escape";
import { tankDefinitions, tankNames } from "../blitzkit/nonBlockingPromises";
import { UserError } from "../blitzkit/userError";
import { tankNamesTechTreeOnly } from "../discord/autocompleteTanks";
import { translator } from "../localization/translator";
export async function resolveTankId(
tank: string | number,
locale: Locale,
techTreeOnly = false,
techTreeOnly = false
) {
const { strings } = translator(locale);
const number = typeof tank === 'string' ? Number(tank) : tank;
const number = typeof tank === "string" ? Number(tank) : tank;
if (Number.isNaN(number)) {
const searchResult = go(
`${tank}`,
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
{ keys: SEARCH_KEYS, limit: 1 },
{ keys: SEARCH_KEYS, limit: 1 }
);
if (searchResult.length === 0) {
throw new UserError(
literals(strings.bot.common.errors.tank_name_not_found, [
markdownEscape(`${tank}`),
]),
literals(strings.bot.common.errors.tank_name_not_found, {
search: markdownEscape(`${tank}`),
})
);
} else {
return searchResult[0].obj.id;
@@ -39,7 +39,9 @@ export async function resolveTankId(
return number;
} else {
throw new UserError(
literals(strings.bot.common.errors.tank_id_not_found, [`${number}`]),
literals(strings.bot.common.errors.tank_id_not_found, {
search: number,
})
);
}
}
@@ -1,12 +1,12 @@
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { StatFilters } from '../blitzstars/filterStats';
import { translator } from '../localization/translator';
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { StatFilters } from "../blitzstars/filterStats";
import { translator } from "../localization/translator";
export async function filtersToDescription(
{ nation, tier, type, class: tankClass, tank }: StatFilters,
locale: Locale,
locale: Locale
) {
const { strings, bkLocale } = translator(locale);
const awaitedTankDefinitions = await fetchTankDefinitions();
@@ -18,11 +18,13 @@ export async function filtersToDescription(
info.push((strings.common.nations as Record<string, string>)[nation]);
if (tier) {
info.push(
literals(strings.bot.common.filters.tier, [TIER_ROMAN_NUMERALS[tier]]),
literals(strings.bot.common.filters.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
})
);
}
if (tankClass) info.push(strings.common.tank_class_short[tankClass]);
if (type) info.push(strings.common.tree_type[type]);
return info.length === 0 ? strings.bot.common.filters.none : info.join(', ');
return info.length === 0 ? strings.bot.common.filters.none : info.join(", ");
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { jsxToSvg } from './jsxToSvg';
import { svgToPng } from './svgToPng';
import { jsxToSvg } from "./jsxToSvg";
import { svgToPng } from "./svgToPng";
export async function jsxToPng(jsx: JSX.Element) {
export async function jsxToPng(jsx: React.JSX.Element) {
const svg = await jsxToSvg(jsx);
const png = svgToPng(svg);
@@ -1,10 +1,10 @@
import { jsxToSvg } from './jsxToSvg';
import { RenderConfiguration } from './renderConfiguration';
import { svgToPngThreaded } from './svgToPngThreaded';
import { jsxToSvg } from "./jsxToSvg";
import { RenderConfiguration } from "./renderConfiguration";
import { svgToPngThreaded } from "./svgToPngThreaded";
export async function jsxToPngThreaded(
jsx: JSX.Element,
renderConfiguration: RenderConfiguration,
jsx: React.JSX.Element,
renderConfiguration: RenderConfiguration
) {
const svg = await jsxToSvg(jsx, renderConfiguration);
const png = await svgToPngThreaded(svg);
@@ -1,9 +1,9 @@
import { readFile } from 'fs/promises';
import satori from 'satori';
import { RenderConfiguration } from '../renderConfiguration';
import robotoBlack from './Roboto-Black.ttf';
import robotoBold from './Roboto-Bold.ttf';
import roboto from './Roboto.ttf';
import { readFile } from "fs/promises";
import satori from "satori";
import { RenderConfiguration } from "../renderConfiguration";
import robotoBlack from "./Roboto-Black.ttf";
import robotoBold from "./Roboto-Bold.ttf";
import roboto from "./Roboto.ttf";
const fontsPromise = [
{ file: roboto, weight: 400 as const },
@@ -11,12 +11,12 @@ const fontsPromise = [
{ file: robotoBlack, weight: 900 as const },
].map(async ({ file, weight }) => {
const data = await readFile(`${__dirname}/${file}`);
return { name: 'Roboto', weight, data };
return { name: "Roboto", weight, data };
});
export async function jsxToSvg(
jsx: JSX.Element,
renderConfiguration = new RenderConfiguration(),
jsx: React.JSX.Element,
renderConfiguration = new RenderConfiguration()
) {
const fonts = await Promise.all(fontsPromise);
return await satori(jsx, { width: renderConfiguration.width, fonts });
+17 -17
View File
@@ -1,13 +1,13 @@
import { numberToRomanNumeral } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { numberToRomanNumeral } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandStringOption,
} from 'discord.js';
import { range } from 'lodash-es';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
} from "discord.js";
import { range } from "lodash-es";
import { translator } from "../localization/translator";
import { localizationObject } from "./localizationObject";
export function addTierChoices(option: SlashCommandStringOption) {
const { strings } = translator(Locale.EnglishUS);
@@ -18,30 +18,30 @@ export function addTierChoices(option: SlashCommandStringOption) {
localizationObject(
(strings) => strings.bot.common.options.tier.name,
undefined,
true,
),
true
)
)
.setDescription(strings.bot.common.options.tier.description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.common.options.tier.description,
),
(strings) => strings.bot.common.options.tier.description
)
)
.setChoices(
...range(10, 0).map(
(tier) =>
({
name: literals(strings.bot.common.options.tier.choices.tier, [
`${tier}`,
numberToRomanNumeral(tier),
]),
name: literals(strings.bot.common.options.tier.choices.tier, {
tier_1: tier,
tier_2: numberToRomanNumeral(tier),
}),
value: `${tier}`,
name_localizations: localizationObject(
(strings) => strings.bot.common.options.tier.choices.tier,
[`${tier}`, numberToRomanNumeral(tier)],
{ tier_1: tier, tier_2: numberToRomanNumeral(tier) }
),
}) satisfies APIApplicationCommandOptionChoice,
),
}) satisfies APIApplicationCommandOptionChoice
)
)
.setRequired(true);
}
@@ -1,11 +1,11 @@
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { translator } from '../localization/translator';
import { PeriodSize } from './addPeriodSubCommands';
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { translator } from "../localization/translator";
import { PeriodSize } from "./addPeriodSubCommands";
export function getPeriodOptionName(period: PeriodSize, locale: Locale) {
const { strings } = translator(locale);
if (period === 'career') return strings.bot.common.periods.career;
if (period === 'today') return strings.bot.common.periods.today;
return literals(strings.bot.common.periods.days, [period]);
if (period === "career") return strings.bot.common.periods.career;
if (period === "today") return strings.bot.common.periods.today;
return literals(strings.bot.common.periods.days, { days: period });
}
@@ -1,15 +1,15 @@
import { BlitzKitStrings, literals as literalsFunc } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { SUPPORTED_LOCALES_DISCORD } from '../localization/strings/constants';
import { translator } from '../localization/translator';
import { BlitzKitStrings, literals as literalsFunc } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { SUPPORTED_LOCALES_DISCORD } from "../localization/strings/constants";
import { translator } from "../localization/translator";
const validNameRegex =
/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u;
export function localizationObject(
path: (strings: BlitzKitStrings) => string,
literals: string[] = [],
strict = false,
literals?: Record<string, string | number>,
strict = false
) {
return SUPPORTED_LOCALES_DISCORD.reduce<Partial<Record<Locale, string>>>(
(localizations, locale) => {
@@ -20,7 +20,7 @@ export function localizationObject(
if (!isValid) {
console.warn(
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`,
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`
);
return localizations;
}
@@ -30,6 +30,6 @@ export function localizationObject(
[locale]: translation,
};
},
{},
{}
);
}
@@ -6,14 +6,14 @@ import {
InteractionEditReplyOptions,
InteractionReplyOptions,
MessageEditOptions,
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate';
import { Writeable } from '../../types/writable';
import { jsxToPngThreaded } from '../blitzkit/jsxToPngThreaded';
import { RenderConfiguration } from '../blitzkit/renderConfiguration';
} from "discord.js";
import { InteractionReturnable } from "../../events/interactionCreate";
import { Writeable } from "../../types/writable";
import { jsxToPngThreaded } from "../blitzkit/jsxToPngThreaded";
import { RenderConfiguration } from "../blitzkit/renderConfiguration";
export async function normalizeInteractionReturnable(
returnable: InteractionReturnable,
returnable: InteractionReturnable
) {
const images: [number, Buffer][] = [];
const reply: InteractionEditReplyOptions &
@@ -27,7 +27,7 @@ export async function normalizeInteractionReturnable(
await Promise.all(
normalizedReturnable.map(async (item, index) => {
if (typeof item === 'string') {
if (typeof item === "string") {
reply.content = item;
} else if (item instanceof EmbedBuilder) {
if (!reply.embeds) reply.embeds = [];
@@ -36,7 +36,7 @@ export async function normalizeInteractionReturnable(
if (!reply.components)
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
(reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents(
item,
item
);
} else if (item instanceof AttachmentBuilder) {
if (!reply.files) reply.files = [];
@@ -49,14 +49,14 @@ export async function normalizeInteractionReturnable(
const image = await jsxToPngThreaded(item, renderConfiguration);
images.push([index, image]);
}
}),
})
);
if (images.length > 0) {
images.sort(([a], [b]) => b - a);
if (!reply.files) reply.files = [];
images.forEach(([, image]) =>
(reply.files as Writeable<typeof reply.files>)?.push(image),
(reply.files as Writeable<typeof reply.files>)?.push(image)
);
}
@@ -1,35 +1,35 @@
import { Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { searchClansAcrossRegionsBotWrapper } from '../blitz/searchClansAcrossRegionsBotWrapper';
import { UserError } from '../blitzkit/userError';
import { translator } from '../localization/translator';
import { Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { ChatInputCommandInteraction } from "discord.js";
import markdownEscape from "markdown-escape";
import { searchClansAcrossRegionsBotWrapper } from "../blitz/searchClansAcrossRegionsBotWrapper";
import { UserError } from "../blitzkit/userError";
import { translator } from "../localization/translator";
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export async function resolveClanFromCommand(
interaction: ChatInputCommandInteraction,
interaction: ChatInputCommandInteraction
) {
const { strings } = translator(interaction.locale);
const clan = interaction.options.getString('clan', true);
const clan = interaction.options.getString("clan", true);
if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/');
const [server, accountId] = clan.split("/");
return { region: server as Region, id: Number(accountId) };
} else {
const accounts = await searchClansAcrossRegionsBotWrapper(
clan,
interaction.locale,
interaction.locale
);
if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id };
} else {
throw new UserError(
literals(strings.bot.common.errors.clan_not_found, [
markdownEscape(clan),
]),
literals(strings.bot.common.errors.clan_not_found, {
search: markdownEscape(clan),
})
);
}
}
@@ -1,11 +1,11 @@
import { getTimeDaysAgo, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { getPeriodNow } from '../blitzkit/getPeriodNow';
import { getPeriodStart } from '../blitzkit/getPeriodStart';
import { translator } from '../localization/translator';
import { PeriodType } from './addPeriodSubCommands';
import { getPeriodOptionName } from './getPeriodOptionName';
import { getTimeDaysAgo, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from "discord.js";
import { getPeriodNow } from "../blitzkit/getPeriodNow";
import { getPeriodStart } from "../blitzkit/getPeriodStart";
import { translator } from "../localization/translator";
import { PeriodType } from "./addPeriodSubCommands";
import { getPeriodOptionName } from "./getPeriodOptionName";
export interface ResolvedPeriod {
name: string;
@@ -16,7 +16,7 @@ export interface ResolvedPeriod {
export function resolvePeriodFromCommand(
region: Region,
interaction: ChatInputCommandInteraction<CacheType>,
forcedPeriod?: PeriodType,
forcedPeriod?: PeriodType
) {
const { strings } = translator(interaction.locale);
let name: string;
@@ -26,16 +26,16 @@ export function resolvePeriodFromCommand(
? forcedPeriod
: (interaction.options.getSubcommand() as PeriodType);
if (periodSubcommand === 'custom') {
const startOption = interaction.options.getInteger('start', true);
const endOption = interaction.options.getInteger('end', true);
if (periodSubcommand === "custom") {
const startOption = interaction.options.getInteger("start", true);
const endOption = interaction.options.getInteger("end", true);
const startDaysAgoMin = Math.min(startOption, endOption);
const endDaysAgoMax = Math.max(startOption, endOption);
name = literals(strings.bot.common.periods.custom, [
`${startDaysAgoMin}`,
`${endDaysAgoMax}`,
]);
name = literals(strings.bot.common.periods.custom, {
day_1: startDaysAgoMin,
day_2: endDaysAgoMax,
});
start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin);
} else {
@@ -1,11 +1,11 @@
import { idToRegion, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { searchPlayersAcrossRegionsBotWrapper } from '../../blitz/searchPlayersAcrossRegionsBotWrapper';
import { getBlitzFromDiscord } from '../../blitzkit/getBlitzFromDiscord';
import { UserError } from '../../blitzkit/userError';
import { translator } from '../../localization/translator';
import { idToRegion, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from "discord.js";
import markdownEscape from "markdown-escape";
import { searchPlayersAcrossRegionsBotWrapper } from "../../blitz/searchPlayersAcrossRegionsBotWrapper";
import { getBlitzFromDiscord } from "../../blitzkit/getBlitzFromDiscord";
import { UserError } from "../../blitzkit/userError";
import { translator } from "../../localization/translator";
export interface ResolvedPlayer {
region: Region;
@@ -13,10 +13,10 @@ export interface ResolvedPlayer {
}
export async function resolvePlayerFromCommand(
interaction: ChatInputCommandInteraction<CacheType>,
interaction: ChatInputCommandInteraction<CacheType>
) {
const { strings } = translator(interaction.locale);
const commandUsername = interaction.options.getString('username');
const commandUsername = interaction.options.getString("username");
if (commandUsername !== null) {
const id = parseInt(commandUsername);
@@ -27,7 +27,7 @@ export async function resolvePlayerFromCommand(
} else {
const accounts = await searchPlayersAcrossRegionsBotWrapper(
commandUsername,
interaction.locale,
interaction.locale
);
if (accounts[0]) {
@@ -37,9 +37,9 @@ export async function resolvePlayerFromCommand(
} satisfies ResolvedPlayer;
} else {
throw new UserError(
literals(strings.bot.common.errors.player_not_found, [
markdownEscape(commandUsername),
]),
literals(strings.bot.common.errors.player_not_found, {
search: markdownEscape(commandUsername),
})
);
}
}
@@ -1,7 +1,7 @@
import { DEFAULT_LOCALE, SupportedLocale } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import locales from "@blitzkit/i18n/locales.json";
import { Locale } from "discord.js";
const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
const SUPPORTED_LOCALES_DISCORD_MAP: Record<string, Locale> = {
en: Locale.EnglishUS,
es: Locale.SpanishES,
pt: Locale.PortugueseBR,
@@ -14,12 +14,12 @@ const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
};
export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries(
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k]),
) as Record<Locale, SupportedLocale>;
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k])
) as Record<Locale, string>;
export const SUPPORTED_LOCALES_DISCORD = Object.values(
SUPPORTED_LOCALES_DISCORD_MAP,
SUPPORTED_LOCALES_DISCORD_MAP
);
export const DEFAULT_LOCALE_DISCORD =
SUPPORTED_LOCALES_DISCORD_MAP[DEFAULT_LOCALE];
SUPPORTED_LOCALES_DISCORD_MAP[locales.default];
+12 -14
View File
@@ -1,8 +1,8 @@
import { ReadStream } from '@blitzkit/core';
import AdmZip from 'adm-zip';
import { readFile, writeFile } from 'fs/promises';
import { Parser } from 'pickleparser';
import protobuf from 'protobufjs';
import { ReadStream } from "@blitzkit/core";
import AdmZip from "adm-zip";
import { readFile } from "fs/promises";
import { Parser } from "pickleparser";
import protobuf from "protobufjs";
interface WotbReplayMetaJson {
version: string;
@@ -24,26 +24,24 @@ export class WotbReplayReadStream extends ReadStream {
async wotbReplay() {
const pickleParser = new Parser();
const zip = new AdmZip(Buffer.from(this.buffer));
const metaJsonBuffer = zip.getEntry('meta.json')?.getData();
const battleResultsDat = zip.getEntry('battle_results.dat')?.getData();
const metaJsonBuffer = zip.getEntry("meta.json")?.getData();
const battleResultsDat = zip.getEntry("battle_results.dat")?.getData();
if (!metaJsonBuffer || !battleResultsDat)
throw new SyntaxError('meta.json not found');
throw new SyntaxError("meta.json not found");
const metaJson = JSON.parse(
metaJsonBuffer.toString(),
metaJsonBuffer.toString()
) as WotbReplayMetaJson;
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
const [, battleResultsString] = unpickledBattleResults as [number, string];
const root = await protobuf.load(
`${__dirname}/../../protos/BattleResultsClient.proto`,
`${__dirname}/../../protos/BattleResultsClient.proto`
);
const BattleResults = root.lookupType(
'BattleResultsGenerated.BattleResultsClient',
"BattleResultsGenerated.BattleResultsClient"
);
writeFile('test.dat', Buffer.from(battleResultsString));
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
// const decodedBattleResults = BattleResults.decodeDelimited(
@@ -53,7 +51,7 @@ export class WotbReplayReadStream extends ReadStream {
}
}
const replay = await readFile('temp/test3.wotbreplay');
const replay = await readFile("temp/test3.wotbreplay");
const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer);
stream.wotbReplay();
@@ -1,4 +1,4 @@
import { assertSecret } from '@blitzkit/core';
import { assertSecret } from "@blitzkit/core";
import {
AttachmentBuilder,
AutocompleteInteraction,
@@ -13,30 +13,30 @@ import {
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import { aboutCommand } from '../../commands/about';
import { breakdownCommand } from '../../commands/breakdown';
import { debugCommand } from '../../commands/debug';
import { evolutionCommand } from '../../commands/evolution';
import { fullStatsCommand } from '../../commands/fullStats';
import { inactiveCommand } from '../../commands/inactive';
import { verifyCommand } from '../../commands/link';
import { ownedTanksCommand } from '../../commands/ownedTanks';
import { permissionsCommand } from '../../commands/permissions';
import { pingCommand } from '../../commands/ping';
import { playerInfoCommand } from '../../commands/playerInfo';
import { ratingCommand } from '../../commands/rating';
import { ratingLeaderboardCommand } from '../../commands/ratingLeaderboard';
import { replayCommand } from '../../commands/replay';
import { researchCommand } from '../../commands/research';
import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers';
import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today';
import { RenderConfiguration } from '../../core/blitzkit/renderConfiguration';
import { handleAutocomplete } from './handlers/autocomplete';
import { handleButton } from './handlers/button';
import { handleChatInputCommand } from './handlers/chatInputCommand';
} from "discord.js";
import { aboutCommand } from "../../commands/about";
import { breakdownCommand } from "../../commands/breakdown";
import { debugCommand } from "../../commands/debug";
import { evolutionCommand } from "../../commands/evolution";
import { fullStatsCommand } from "../../commands/fullStats";
import { inactiveCommand } from "../../commands/inactive";
import { verifyCommand } from "../../commands/link";
import { ownedTanksCommand } from "../../commands/ownedTanks";
import { permissionsCommand } from "../../commands/permissions";
import { pingCommand } from "../../commands/ping";
import { playerInfoCommand } from "../../commands/playerInfo";
import { ratingCommand } from "../../commands/rating";
import { ratingLeaderboardCommand } from "../../commands/ratingLeaderboard";
import { replayCommand } from "../../commands/replay";
import { researchCommand } from "../../commands/research";
import { searchClansCommand } from "../../commands/searchClans";
import { searchPlayersCommand } from "../../commands/searchPlayers";
import { statsCommand } from "../../commands/stats";
import { todayCommand } from "../../commands/today";
import { RenderConfiguration } from "../../core/blitzkit/renderConfiguration";
import { handleAutocomplete } from "./handlers/autocomplete";
import { handleButton } from "./handlers/button";
import { handleChatInputCommand } from "./handlers/chatInputCommand";
export type InteractionRawReturnable =
| string
@@ -44,7 +44,7 @@ export type InteractionRawReturnable =
| ButtonBuilder
| AttachmentBuilder
| RenderConfiguration
| JSX.Element
| React.JSX.Element
| null;
export type InteractionIterableReturnable =
| InteractionRawReturnable
@@ -58,7 +58,7 @@ interface CommandRegistryBase {
| SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder
| SlashCommandOptionsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
| Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
@@ -72,7 +72,7 @@ interface CommandRegistryImplicit extends CommandRegistryBase {
interface CommandRegistryExplicit extends CommandRegistryBase {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
interaction: ChatInputCommandInteraction<CacheType>
) => InteractionReturnable;
}
@@ -104,16 +104,16 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry, index) => {
if (registry.status === 'rejected') {
if (registry.status === "rejected") {
console.warn(
`Command ${index} failed to load; skipping...`,
registry.reason,
registry.reason
);
return commands;
}
return { ...commands, [registry.value.command.name]: registry.value };
},
{},
{}
);
});
@@ -121,12 +121,12 @@ const rest = new REST().setToken(assertSecret(import.meta.env.DISCORD_TOKEN));
commands.then((awaitedCommands) => {
const body = Object.values(awaitedCommands).map((registry) =>
registry.command.toJSON(),
registry.command.toJSON()
);
rest.put(
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
{ body },
{ body }
);
});
+1
Submodule packages/closed added at 1caf47f66d.
+13 -11
View File
@@ -4,20 +4,22 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
"@octokit/plugin-throttling": "^8.2.0",
"@octokit/rest": "^20.1.1",
"@prisma/client": "^6.6.0",
"@blitzkit/i18n": "workspace:*",
"@bufbuild/protobuf": "^2.9.0",
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1",
"@types/three": "^0.180.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^16.4.5",
"dotenv": "^17.2.2",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.4.0",
"ts-proto": "^2.2.2",
"yaml": "^2.5.0"
},
"devDependencies": {
"@types/adm-zip": "^0.5.5",
"@types/lz4js": "^0.2.1"
"protobufjs": "^7.5.4",
"three": "^0.180.0",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { type ShellDefinition, ShellType } from '@blitzkit/core';
import { ShellDefinition, ShellType } from "../protos";
export function canSplash(type: ShellDefinition['type']): type is ShellType.HE {
export function canSplash(type: ShellDefinition["type"]): type is ShellType.HE {
return type === ShellType.HE;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { BlitzStats, calculateWN8 } from '@blitzkit/core';
import { BlitzStats, calculateWN8 } from "../statistics";
export type Stat = keyof ReturnType<typeof generateStats>;
@@ -1,5 +1,6 @@
import { fetchBlitz, Region } from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
interface IndividualAccountAchievements {
achievements: {
@@ -22,11 +23,11 @@ export async function getAccountAchievements<
>(region: Region, ids: Ids) {
const object = await fetchBlitz<AccountAchievements>(
region,
'account/achievements',
{ account_id: normalizeIds(ids) },
"account/achievements",
{ account_id: normalizeIds(ids) }
);
if (typeof ids === 'number') {
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
+8 -11
View File
@@ -1,10 +1,7 @@
import {
BlitzStats,
FetchBlitzParams,
Region,
fetchBlitz,
} from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
import { BlitzStats } from "../statistics";
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
export interface SupplementaryStats {
WN8?: number;
@@ -83,15 +80,15 @@ export async function getAccountInfo<
region: Region,
ids: Ids,
extra: string[] = [],
params: FetchBlitzParams = {},
params: FetchBlitzParams = {}
) {
const object = await fetchBlitz<AccountInfo>(region, 'account/info', {
const object = await fetchBlitz<AccountInfo>(region, "account/info", {
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(','),
extra: extra.length === 0 ? undefined : extra.join(","),
...params,
});
if (typeof ids === 'number') {
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
@@ -1,5 +1,6 @@
import { fetchBlitz, Region } from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
type IndividualClanAccountInfo = {
account_id: number;
@@ -30,14 +31,14 @@ export async function getClanAccountInfo<
>(region: Region, ids: Ids, extra: string[] = []) {
const object = await fetchBlitz<ClanAccountInfo>(
region,
'clans/accountinfo',
"clans/accountinfo",
{
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(','),
},
extra: extra.length === 0 ? undefined : extra.join(","),
}
);
if (typeof ids === 'number') {
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
+5 -4
View File
@@ -1,5 +1,6 @@
import { fetchBlitz, Region } from '@blitzkit/core';
import { normalizeIds } from './normalizeIds';
import { fetchBlitz } from "./fetchBlitz";
import { normalizeIds } from "./normalizeIds";
import { Region } from "./regions";
interface IndividualClanInfo {
recruiting_options: {
@@ -38,11 +39,11 @@ export async function getClanInfo<
Ids extends number | number[],
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) {
const object = await fetchBlitz<ClanInfo>(region, 'clans/info', {
const object = await fetchBlitz<ClanInfo>(region, "clans/info", {
clan_id: normalizeIds(ids),
});
if (typeof ids === 'number') {
if (typeof ids === "number") {
return object[ids as number] as ReturnType;
} else {
return ids.map((id) => object[id]) as ReturnType;
@@ -1,4 +1,4 @@
import { LEAGUES } from '@blitzkit/core';
import { LEAGUES } from "./rating";
export function getLeagueFromScore(score: number) {
return LEAGUES.find(({ minScore }) => score >= minScore)!;
+8 -7
View File
@@ -1,7 +1,8 @@
import { RatingLeague, RatingReward, Region } from '@blitzkit/core';
import { context } from '../blitzkit/context';
import { patientFetch } from '../blitzkit/patientFetch';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import { RatingLeague, RatingReward } from "./rating";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export type RatingInfo =
| {
@@ -26,12 +27,12 @@ export async function getRatingInfo(region: Region) {
if (cache[region]) return cache[region]!;
const response = await patientFetch(
context === 'website'
context === "website"
? `/api/${region}/rating/current/info`
: `https://${regionToRegionSubdomain(
region,
region
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
undefined,
undefined
);
const data = (await response.json()) as RatingInfo;
+13 -11
View File
@@ -1,27 +1,29 @@
import { LeagueTop, RatingPlayer, Region } from '@blitzkit/core';
import { context } from '../blitzkit/context';
import { patientFetch } from '../blitzkit/patientFetch';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
import { context } from "../blitzkit/context";
import { patientFetch } from "../blitzkit/patientFetch";
import { RatingPlayer } from "../statistics";
import { LeagueTop } from "./rating";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingLeague(
region: Region,
league: number,
usePatientFetcher = false,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
const response = await fetcher(
context === 'website'
context === "website"
? `/api/${region}/rating/current/league/${league}`
: `https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
region
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`
);
const json = await response.json();
return context === 'website'
return context === "website"
? (json as RatingPlayer[])
: (json as LeagueTop).result;
}
+11 -10
View File
@@ -1,24 +1,25 @@
import { RatingNeighbors, Region } from '@blitzkit/core';
import { patientFetch } from '../blitzkit/patientFetch';
import { withCORSProxy } from '../blitzkit/withCORSProxy';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
import { patientFetch } from "../blitzkit/patientFetch";
import { withCORSProxy } from "../blitzkit/withCORSProxy";
import { RatingNeighbors } from "./rating";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
export async function getRatingNeighbors(
region: Region,
id: number,
count: number,
usePatientFetcher = false,
usePatientFetcher = false
) {
const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
? patientFetch(url, undefined, { cache: "no-store" })
: fetch(url, { cache: "no-store" });
const response = await fetcher(
withCORSProxy(
`https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`,
),
region
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`
)
);
const json = (await response.json()) as RatingNeighbors;
+5 -8
View File
@@ -1,16 +1,13 @@
import {
fetchBlitz,
FetchBlitzParams,
Region,
TanksStats,
} from '@blitzkit/core';
import { TanksStats } from "../types";
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
import { Region } from "./regions";
export async function getTankStats(
region: Region,
id: number,
params?: FetchBlitzParams,
params?: FetchBlitzParams
) {
const tankStats = await fetchBlitz<TanksStats>(region, 'tanks/stats', {
const tankStats = await fetchBlitz<TanksStats>(region, "tanks/stats", {
account_id: id,
...params,
});
@@ -1,6 +1,6 @@
import { getRatingInfo } from '@blitzkit/core';
import { getRatingInfo } from "./getRatingInfo";
export async function isOnGoingRatingSeason() {
const ratingInfo = await getRatingInfo('com');
const ratingInfo = await getRatingInfo("com");
return ratingInfo.detail === undefined;
}
+10 -8
View File
@@ -1,4 +1,6 @@
import { Region, assertSecret, idToRegion } from '@blitzkit/core';
import { assertSecret } from "../blitzkit";
import { idToRegion } from "./idToRegion";
import { Region } from "./regions";
export async function isValidBlitzId(id: number, token?: string) {
let region: Region;
@@ -11,20 +13,20 @@ export async function isValidBlitzId(id: number, token?: string) {
const json = await fetch(
`https://api.wotblitz.${region}/wotb/account/info/?account_id=${id}&application_id=${assertSecret(
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID,
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
)}${
token === undefined ? '' : `&access_token=${token}`
}&fields=account_id%2C-account_id`,
token === undefined ? "" : `&access_token=${token}`
}&fields=account_id%2C-account_id`
).then(
(response) =>
response.json() as Promise<
| {
status: 'ok';
status: "ok";
data: Record<number, {} | null>;
}
| { status: 'error' }
>,
| { status: "error" }
>
);
return json.status === 'ok' && json.data[id] !== null;
return json.status === "ok" && json.data[id] !== null;
}
@@ -1,5 +1,5 @@
import { Region, RegionSubdomain } from '@blitzkit/core';
import { Region, RegionSubdomain } from "./regions";
export function regionSubdomainToRegion(regionDomain: RegionSubdomain): Region {
return regionDomain === 'na' ? 'com' : regionDomain;
return regionDomain === "na" ? "com" : regionDomain;
}
@@ -1,5 +1,5 @@
import { Region, RegionSubdomain } from '@blitzkit/core';
import { Region, RegionSubdomain } from "./regions";
export function regionToRegionSubdomain(regionDomain: Region): RegionSubdomain {
return regionDomain === 'com' ? 'na' : regionDomain;
return regionDomain === "com" ? "na" : regionDomain;
}
@@ -1,5 +1,6 @@
import { fetchBlitz, Region } from '@blitzkit/core';
import { ClanList } from './clans';
import { ClanList } from "./clans";
import { fetchBlitz } from "./fetchBlitz";
import { Region } from "./regions";
export async function searchClansAcrossRegions(search: string, limit = 25) {
const trimmed = search.trim();
@@ -10,38 +11,38 @@ export async function searchClansAcrossRegions(search: string, limit = 25) {
return (
await Promise.all([
fetchBlitz<ClanList>('com', 'clans/list', {
fetchBlitz<ClanList>("com", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: 'com' as Region,
region: "com" as Region,
}))
.splice(0, normalizedLimit),
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>('eu', 'clans/list', {
fetchBlitz<ClanList>("eu", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: 'eu' as Region,
region: "eu" as Region,
}))
.splice(0, normalizedLimit),
.splice(0, normalizedLimit)
),
fetchBlitz<ClanList>('asia', 'clans/list', {
fetchBlitz<ClanList>("asia", "clans/list", {
search,
limit: normalizedLimit,
}).then((value) =>
value
.map((account) => ({
...account,
region: 'asia' as Region,
region: "asia" as Region,
}))
.splice(0, normalizedLimit),
.splice(0, normalizedLimit)
),
])
)
@@ -1,6 +1,6 @@
import { Region } from '@blitzkit/core';
import { context } from '../blitzkit/context';
import { regionToRegionSubdomain } from './regionToRegionSubdomain';
import { context } from "../blitzkit/context";
import { Region } from "./regions";
import { regionToRegionSubdomain } from "./regionToRegionSubdomain";
type CurrentRatingPlayerSearch = Record<
number,
@@ -27,12 +27,12 @@ type CurrentRatingPlayerSearch = Record<
export async function searchCurrentRatingPlayers(region: Region, name: string) {
const encodedSearch = encodeURIComponent(name);
const response = await fetch(
context === 'website'
context === "website"
? `/api/${region}/rating/current/search/${encodedSearch}`
: `https://${regionToRegionSubdomain(
region,
region
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
{ cache: 'no-store' },
{ cache: "no-store" }
);
const accountList = (await response.json()) as CurrentRatingPlayerSearch;
@@ -1,4 +1,5 @@
import { fetchBlitz, Region } from '@blitzkit/core';
import { fetchBlitz } from '../fetchBlitz';
import { Region } from '../regions';
import { usernamePattern } from './constants';
export interface AccountListItem {
+1 -1
View File
@@ -1,4 +1,4 @@
import { NATION_IDS } from "@blitzkit/core";
import { NATION_IDS } from "./nations";
export function toUniqueId(nation: string, id: number) {
return (id << 8) + (NATION_IDS[nation] << 4) + 1;
+2 -2
View File
@@ -1,4 +1,4 @@
export function assertSecret(secret?: string) {
if (secret === undefined) throw new Error('Missing secret');
export function assertSecret<Type>(secret?: Type) {
if (secret === undefined) throw new Error("Missing secret");
return secret;
}
@@ -1,16 +1,12 @@
import {
GunDefinition,
ProvisionDefinitions,
TankDefinition,
} from '@blitzkit/core';
import { checkConsumableProvisionInclusivity } from './checkConsumableProvisionInclusivity';
import { GunDefinition, ProvisionDefinitions, TankDefinition } from "../protos";
import { checkConsumableProvisionInclusivity } from "./checkConsumableProvisionInclusivity";
export function availableProvisions(
tank: TankDefinition,
gun: GunDefinition,
provisionDefinitions: ProvisionDefinitions,
provisionDefinitions: ProvisionDefinitions
) {
return Object.values(provisionDefinitions.provisions).filter((provision) =>
checkConsumableProvisionInclusivity(provision, tank, gun),
checkConsumableProvisionInclusivity(provision, tank, gun)
);
}
@@ -1,5 +1,5 @@
import { SkillDefinitions } from '@blitzkit/core';
import { permanentSkills } from '../blitz/permanentSkills';
import { SkillDefinitions } from "..";
import { permanentSkills } from "../blitz/permanentSkills";
export function createDefaultSkills(skillDefinitions: SkillDefinitions) {
const skills: Record<string, number> = {};
@@ -1,4 +1,5 @@
import { assertSecret, getRatingInfo } from '@blitzkit/core';
import { getRatingInfo } from "../blitz/getRatingInfo";
import { assertSecret } from "./assertSecret";
interface GitHubTrees {
tree: { path: string; url: string }[];
@@ -11,21 +12,21 @@ export async function getArchivedLatestSeasonNumber() {
return cachedLatestArchivedSeasonNumber;
}
const ratingInfo = await getRatingInfo('com');
const ratingInfo = await getRatingInfo("com");
if (ratingInfo.detail) {
const regionsURL = (
(await fetch(
`https://api.github.com/repos/tresabhi/${assertSecret(
import.meta.env.PUBLIC_ASSET_REPO,
)}/git/trees/main`,
import.meta.env.PUBLIC_ASSET_REPO
)}/git/trees/main`
).then((response) => response.json())) as GitHubTrees
).tree.find(({ path }) => path === 'regions')!.url;
).tree.find(({ path }) => path === "regions")!.url;
const comURL = (
(await fetch(regionsURL).then((response) =>
response.json(),
response.json()
)) as GitHubTrees
).tree.find(({ path }) => path === 'com')!.url;
).tree.find(({ path }) => path === "com")!.url;
const ratingURL = (
(await fetch(comURL).then((response) => response.json())) as {
tree: [{ url: string }];
@@ -36,7 +37,7 @@ export async function getArchivedLatestSeasonNumber() {
(await fetch(ratingURL).then((response) => response.json())) as {
tree: { path: string }[];
}
).tree.map(({ path }) => Number(path)),
).tree.map(({ path }) => Number(path))
);
cachedLatestArchivedSeasonNumber = latestSeason;
@@ -1,4 +1,6 @@
import { asset, RatingLeaderboard, Region } from '@blitzkit/core';
import { Region } from "../blitz/regions";
import { RatingLeaderboard } from "../protos";
import { asset } from "./asset";
const cache: Record<Region, Record<number, RatingLeaderboard>> = {
asia: {},
@@ -8,7 +10,7 @@ const cache: Record<Region, Record<number, RatingLeaderboard>> = {
export async function getArchivedRatingLeaderboard(
region: Region,
season: number,
season: number
) {
if (!cache[region][season]) {
const info = await fetch(asset(`regions/${region}/rating/${season}.pb`))
+1 -1
View File
@@ -1,4 +1,4 @@
import { Region } from '@blitzkit/core';
import { Region } from "../blitz/regions";
const TIME_ZONE_MAPPINGS: Record<Region, number> = {
com: -5, // Central North American Time
+2 -2
View File
@@ -1,7 +1,7 @@
import { ShellDefinition, ShellType } from '@blitzkit/core';
import { ShellDefinition, ShellType } from "../protos";
export function isExplosive(
type: ShellDefinition['type'],
type: ShellDefinition["type"]
): type is ShellType.HEAT | ShellType.HE {
return type === ShellType.HEAT || type === ShellType.HE;
}
+6 -6
View File
@@ -1,5 +1,5 @@
import { TANK_CLASSES } from '../blitz/constants';
import { GameDefinitions, TankDefinition, TankType } from '../protos';
import { TANK_CLASSES } from "../blitz/constants";
import { GameDefinitions, TankDefinition, TankType } from "../protos";
const treeTypeOrder = [
TankType.RESEARCHABLE,
@@ -9,20 +9,20 @@ const treeTypeOrder = [
export function metaSortTank(
tanks: TankDefinition[],
gameDefinitions: GameDefinitions,
gameDefinitions: GameDefinitions
) {
return tanks
.sort((a, b) => b.tier - a.tier)
.sort(
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type),
(a, b) => treeTypeOrder.indexOf(b.type) - treeTypeOrder.indexOf(a.type)
)
.sort(
(a, b) => TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class),
(a, b) => TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class)
)
.sort(
(a, b) =>
gameDefinitions.nations.indexOf(b.nation) -
gameDefinitions.nations.indexOf(a.nation),
gameDefinitions.nations.indexOf(a.nation)
)
.sort((a, b) => a.tier - b.tier);
}
@@ -1,4 +1,5 @@
import { BoundingBox, Vector3 } from '@blitzkit/core';
import { Vector3 } from "three";
import { BoundingBox } from "../protos";
export function normalizeBoundingBox(boundingBox: BoundingBox) {
return {
+1 -1
View File
@@ -1,4 +1,4 @@
import { BKNI_COLORS, BkniPercentile, getBkniPercentile } from '@blitzkit/core';
import { BKNI_COLORS, BkniPercentile, getBkniPercentile } from "../statistics";
export function parseBkni(bkni: number) {
const bkniFraction = (bkni + 1) / 2;
+2 -5
View File
@@ -1,10 +1,7 @@
import { asset } from '@blitzkit/core';
import { asset } from "./asset";
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon(
id: number,
size: 'big' | 'small' | 'blitzkit' = 'blitzkit',
) {
export function tankIcon(id: number, size: "big" | "small" = "big") {
return asset(`icons/tanks/${size}/${id}.webp`);
}
+18 -16
View File
@@ -1,8 +1,8 @@
import { deburr } from 'lodash-es';
import { SUPPORTED_LOCALES } from '../../../i18n/src/strings';
import { I18nString } from '../protos';
import { fetchCamouflageDefinitions } from './camouflageDefinitions';
import { fetchTankDefinitions } from './tankDefinitions';
import locales from "@blitzkit/i18n/locales.json";
import { deburr } from "lodash-es";
import { I18nString } from "../protos";
import { fetchCamouflageDefinitions } from "./camouflageDefinitions";
import { fetchTankDefinitions } from "./tankDefinitions";
export async function fetchTankNames() {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
@@ -26,25 +26,27 @@ export async function fetchTankNames() {
searchableNameDeburr,
camouflages: tank.camouflages
?.map((id) =>
SUPPORTED_LOCALES.map(
(locale) =>
camouflageDefinitions.camouflages[id]?.name.locales[locale],
),
locales.supported.map(
({ locale }) =>
camouflageDefinitions.camouflages[id]?.name.locales[locale]
)
)
.flat()
.filter(Boolean)
.map(deburr)
.join(' '),
.join(" "),
treeType: tank.type,
};
}),
})
);
}
export const SEARCH_KEYS = [
...SUPPORTED_LOCALES.map((locale) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
]).flat(),
'camouflages',
...locales.supported
.map(({ locale }) => [
`searchableName.locales.${locale}`,
`searchableNameDeburr.locales.${locale}`,
])
.flat(),
"camouflages",
];
@@ -1,4 +1,4 @@
import { BoundingBox } from '@blitzkit/core';
import { BoundingBox } from "../protos";
export function unionBoundingBox(a: BoundingBox, b: BoundingBox) {
return {
+101 -102
View File
@@ -1,20 +1,19 @@
import { Vector3Tuple, Vector4Tuple } from 'three';
import { ScpgReadStream } from './scpg';
import { Vector3Tuple, Vector4Tuple } from "three";
import { ScpgReadStream } from "./scpg";
export interface SC2 {
'#dataNodes': DataNode[];
'#hierarchy': Hierarchy[];
'#sceneComponents': SceneComponents;
"#dataNodes": DataNode[];
"#hierarchy": Hierarchy[];
"#sceneComponents": SceneComponents;
}
type DataNode = {
'##name': 'NMaterial';
'#id': ArrayBuffer;
"##name": "NMaterial";
"#id": ArrayBuffer;
materialName: string;
parentMaterialKey?: bigint;
qualityGroup?: string;
fxName?: string;
textures?: Textures;
} & (
| ({
@@ -80,8 +79,8 @@ export interface Textures {
}
export interface Hierarchy {
'##name': 'Entity';
'#hierarchy'?: Hierarchy[];
"##name": "Entity";
"#hierarchy"?: Hierarchy[];
components: Components;
flags: number;
id: bigint;
@@ -97,12 +96,12 @@ type Components = {
type RoBatches = Record<
string,
{
'##name': 'RenderBatch';
'rb.aabbox': RbAabbox;
'rb.classname': 'RenderBatch';
'rb.datasource': bigint;
'rb.nmatname': bigint;
'rb.sortingKey': number;
"##name": "RenderBatch";
"rb.aabbox": RbAabbox;
"rb.classname": "RenderBatch";
"rb.datasource": bigint;
"rb.nmatname": bigint;
"rb.sortingKey": number;
}
>;
@@ -113,50 +112,50 @@ interface RbAabbox {
export type Component =
| {
'comp.typename': 'TransformComponent';
'tc.localRotation': Vector4Tuple;
'tc.localScale': Vector3Tuple;
'tc.localTranslation': Vector3Tuple;
'tc.worldRotation': Vector4Tuple;
'tc.worldScale': Vector3Tuple;
'tc.worldTranslation': Vector3Tuple;
"comp.typename": "TransformComponent";
"tc.localRotation": Vector4Tuple;
"tc.localScale": Vector3Tuple;
"tc.localTranslation": Vector3Tuple;
"tc.worldRotation": Vector4Tuple;
"tc.worldScale": Vector3Tuple;
"tc.worldTranslation": Vector3Tuple;
}
| {
'comp.typename': 'RenderComponent';
'rc.renderObj': {
'##name': 'Mesh';
'ro.batchCount': number;
'ro.batches': RoBatches;
'ro.debugflags': number;
'ro.flags': number;
'ro.notShadowOnly': boolean;
'ro.sOclCull': boolean;
'ro.sOclIndex': number;
} & Record<`rb${number}.${'lodIndex' | 'switchIndex'}`, number>;
"comp.typename": "RenderComponent";
"rc.renderObj": {
"##name": "Mesh";
"ro.batchCount": number;
"ro.batches": RoBatches;
"ro.debugflags": number;
"ro.flags": number;
"ro.notShadowOnly": boolean;
"ro.sOclCull": boolean;
"ro.sOclIndex": number;
} & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number>;
}
| {
'comp.typename': 'SlotComponent';
'sc.attachmentRotation': number[];
'sc.attachmentScale': number[];
'sc.attachmentTranslation': number[];
'sc.configFilePath': string;
'sc.slotName': string;
'sc.template': string;
'sc.typeFiltersCount': number;
"comp.typename": "SlotComponent";
"sc.attachmentRotation": number[];
"sc.attachmentScale": number[];
"sc.attachmentTranslation": number[];
"sc.configFilePath": string;
"sc.slotName": string;
"sc.template": string;
"sc.typeFiltersCount": number;
}
| {
'comp.typename': 'LodComponent';
'lc.loddist': Record<`distance${number}`, number>;
"comp.typename": "LodComponent";
"lc.loddist": Record<`distance${number}`, number>;
}
| {
'comp.typename': 'DecorItemComponent';
"comp.typename": "DecorItemComponent";
drawOrder: number;
shouldApplyCamo: boolean;
vanishTargetAlpha: number;
vanishWhenArmorShown: boolean;
}
| {
'comp.typename': 'NewSlotComponent';
"comp.typename": "NewSlotComponent";
slotItemsCount: number;
slots: {
[key: string]: {
@@ -171,109 +170,109 @@ export type Component =
};
}
| {
'comp.typename': 'ScenarioComponent';
'scenario.scriptId': string;
"comp.typename": "ScenarioComponent";
"scenario.scriptId": string;
}
| ({
'comp.typename': 'StateSwitcherComponent';
'ssc.activeState': number;
'ssc.statesCount': number;
"comp.typename": "StateSwitcherComponent";
"ssc.activeState": number;
"ssc.statesCount": number;
} & Record<`ssc.state${number}`, string>)
| {
'comp.typename': 'CustomPropertiesComponent';
'cpc.properties.archive': {
'editor.donotremove': number;
"comp.typename": "CustomPropertiesComponent";
"cpc.properties.archive": {
"editor.donotremove": number;
};
}
| ({
'comp.typename': 'ActionComponent';
'ac.actionCount': number;
"comp.typename": "ActionComponent";
"ac.actionCount": number;
} & Record<
string, // 0000, 0001, ...
{
'act.delay': number;
'act.delayVariation': number;
'act.entityName': string;
'act.event': number;
'act.eventToTrigger': string;
'act.motionFrameTime': number;
'act.motionSpeed': number;
'act.stateActivated': number;
'act.stopAfterNRepeats': number;
'act.stopWhenEmpty': boolean;
'act.switchIndex': number;
'act.type': number;
'act.userEventId': string;
"act.delay": number;
"act.delayVariation": number;
"act.entityName": string;
"act.event": number;
"act.eventToTrigger": string;
"act.motionFrameTime": number;
"act.motionSpeed": number;
"act.stateActivated": number;
"act.stopAfterNRepeats": number;
"act.stopWhenEmpty": boolean;
"act.switchIndex": number;
"act.type": number;
"act.userEventId": string;
}
>)
| {
'comp.typename': 'ParticleEffectComponent';
'pe.clearOnRestart': boolean;
'pe.effectDuration': number;
'pe.emitters': {
'##name': string;
'emitter.data': {
'emitter.id': `${number}`;
'emitter.quality': string;
"comp.typename": "ParticleEffectComponent";
"pe.clearOnRestart": boolean;
"pe.effectDuration": number;
"pe.emitters": {
"##name": string;
"emitter.data": {
"emitter.id": `${number}`;
"emitter.quality": string;
}[];
'emitter.position': Vector3Tuple;
'emitter.rotation': Vector4Tuple;
'emitter.scale': Vector3Tuple;
"emitter.position": Vector3Tuple;
"emitter.rotation": Vector4Tuple;
"emitter.scale": Vector3Tuple;
}[];
'pe.inheritScale': boolean;
'pe.nestedEmitters': boolean;
'pe.repeatsCount': number;
'pe.startFromTime': number;
'pe.stopWhenEmpty': boolean;
'pe.version': number;
'ro.flags': number;
"pe.inheritScale": boolean;
"pe.nestedEmitters": boolean;
"pe.repeatsCount": number;
"pe.startFromTime": number;
"pe.stopWhenEmpty": boolean;
"pe.version": number;
"ro.flags": number;
}
| {
'comp.typename': 'TankElementComponent';
"comp.typename": "TankElementComponent";
isDestructible: boolean;
shouldCastShadow: boolean;
shouldGenerateSilhouette: boolean;
}
| {
'comp.typename': 'MotionComponent';
'motion.filepath': string;
'motion.playbackRate': number;
'simpleMotion.repeatsCount': number;
"comp.typename": "MotionComponent";
"motion.filepath": string;
"motion.playbackRate": number;
"simpleMotion.repeatsCount": number;
}
| {
'comp.typename': 'SkeletonComponent';
"comp.typename": "SkeletonComponent";
joints: Record<
string, // 0000, 0001, ...
{
'joint.bbox.max': Vector3Tuple;
'joint.bbox.min': Vector3Tuple;
'joint.bindPose': [
"joint.bbox.max": Vector3Tuple;
"joint.bbox.min": Vector3Tuple;
"joint.bindPose": [
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
];
'joint.invBindPose': [
"joint.invBindPose": [
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
Vector4Tuple,
];
'joint.name': string;
'joint.parentIndex': number;
'joint.uid': string;
"joint.name": string;
"joint.parentIndex": number;
"joint.uid": string;
}
>;
jointsCount: number;
}
| {
'comp.typename': 'AnimationComponent';
"comp.typename": "AnimationComponent";
animation: bigint;
animationTimeScale: number;
repeatsCount: number;
}
| {
'comp.typename': 'SkeletonJointBindingComponent';
"comp.typename": "SkeletonJointBindingComponent";
count: number;
map: Record<
string, // 0000, 0001, ...
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
"ts-deepmerge": "^7.0.2"
"ts-deepmerge": "^7.0.3"
},
"devDependencies": {}
}
+4 -3
View File
@@ -1,3 +1,4 @@
export * from './literals';
export * from './strings';
export * from './unwrapper';
export * from "./literals";
export * from "./strings";
export * from "./unwrapper";
export * from "./wrapper";
+15 -4
View File
@@ -1,5 +1,16 @@
export function literals(string: string, literals: string[]) {
return literals.reduce((accumulator, literal, index) => {
return accumulator.replace(`%s${index + 1}`, literal);
}, string);
export function literals(
string: string,
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;
}
return string;
}
+50 -82
View File
@@ -1,98 +1,66 @@
import { merge } from 'ts-deepmerge';
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 { 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";
export type BlitzKitStrings = typeof en;
const stringsRaw = { en, es, fr, ja, pl, pt, ru, uk, zh };
const stringsRaw: Record<string, TranslationTree> = {
en,
es,
fr,
ja,
pl,
pt,
ru,
uk,
zh,
};
export const SUPPORTED_LOCALES = Object.keys(stringsRaw) as SupportedLocale[];
export const DEFAULT_LOCALE = SUPPORTED_LOCALES[0];
function redact(string: string) {
const matches = [...string.matchAll(/%s\d+/g)];
const characters = string
.split('')
.map((char) => (char === ' ' ? ' ' : '█')) as string[];
matches.forEach((match) => {
const start = match.index;
const matchText = match[0];
for (let i = 0; i < matchText.length; i++) {
characters[start + i] = matchText[i];
}
});
return characters.join('');
}
function nuke(object: TranslationTree) {
for (const key in object) {
const typedKey = key as keyof typeof object;
if (typeof object[typedKey] === 'string') {
const value = object[typedKey];
object[typedKey] = redact(value as string);
} else nuke(object[typedKey] as TranslationTree);
}
}
const stringsPartial: Partial<Record<SupportedLocale, TranslationTree>> = {};
export const STRINGS: Record<string, BlitzKitStrings> = {};
for (const locale in stringsRaw) {
if (locale === DEFAULT_LOCALE) {
stringsPartial[DEFAULT_LOCALE] = stringsRaw[DEFAULT_LOCALE];
if (locale === locales.default) {
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
continue;
}
const localeStrings = stringsRaw[locale as SupportedLocale];
stringsPartial[locale as SupportedLocale] = merge(
stringsRaw[DEFAULT_LOCALE],
localeStrings,
);
if (import.meta.env.PUBLIC_DEBUG_MISSING_I18N === 'true') {
nuke(stringsPartial[locale as SupportedLocale]!);
}
const localeStrings = stringsRaw[locale];
STRINGS[locale] = merge(
stringsRaw[locales.default],
localeStrings
) as BlitzKitStrings;
}
export const STRINGS = stringsPartial as Record<
SupportedLocale,
BlitzKitStrings
>;
export type SupportedLocale = keyof typeof stringsRaw;
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<SupportedLocale, string> = {
en: 'en',
es: 'es',
fr: 'fr',
ja: 'ja',
pt: 'pt',
ru: 'ru',
uk: 'uk',
zh: 'zh-Hans',
pl: 'pl',
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<SupportedLocale, string> = {
en: '🇺🇸',
es: '🇪🇸',
fr: '🇫🇷',
ja: '🇯🇵',
pt: '🇵🇹',
ru: '🇷🇺',
uk: '🇺🇦',
zh: '🇨🇳',
pl: '🇵🇱',
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
en: "🇺🇸",
es: "🇪🇸",
fr: "🇫🇷",
ja: "🇯🇵",
pt: "🇵🇹",
ru: "🇷🇺",
uk: "🇺🇦",
zh: "🇨🇳",
pl: "🇵🇱",
};
export type TranslationTree = { [key: string]: TranslationNode };
+4 -4
View File
@@ -1,12 +1,12 @@
import { I18nString } from '@blitzkit/core';
import { DEFAULT_LOCALE } from './strings';
import { I18nString } from "../../core/src";
import locales from "../locales.json";
export function unwrapper(locale: string) {
export function unwrapper(locale = locales.default) {
return function (i18nString: I18nString) {
if (locale in i18nString.locales) {
return i18nString.locales[locale];
}
return i18nString.locales[DEFAULT_LOCALE];
return i18nString.locales[locales.default];
};
}
+15
View File
@@ -0,0 +1,15 @@
import locales from "../locales.json";
import { 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;
}
Loaded 100 of 360 files, more files were not shown because too many files have changed in this diff. Show more