Compare commits

..
116 Commits
Author SHA1 Message Date
tresabhi 830bde1169 Merge branch 'main' of https://github.com/blitzkit/blitzkit into ue 2026-07-05 14:46:48 -05:00
tresabhi dc6c528167 logs 2026-07-05 14:06:46 -05:00
tresabhi b4d5d4c16e reduce ground brightness in static mode 2026-07-05 14:04:09 -05:00
tresabhi f2f6d4ff49 add persistent count for the number of model users to unload only when
uses drops to 0
2026-07-05 13:44:04 -05:00
tresabhi cdbb7647f0 fixed fill and clip rule naming 2026-07-05 13:41:21 -05:00
TrèsAbhi 41e40de791 New Crowdin updates (#251)
* New translations en.json (Ukrainian)

[ci skip]

* New translations en.json (Ukrainian)

[ci skip]

* New translations en.json (Ukrainian)

[ci skip]
2026-07-04 22:05:48 -05:00
tresabhi d55c9b4cd3 purge unused packages 2026-06-30 14:06:22 -05:00
tresabhi 2ee72b40ab purge some dead code 2026-06-30 14:03:34 -05:00
tresabhi c85cd36818 logs 2026-06-28 11:11:37 -05:00
tresabhi 5d2c63fed9 clean up some renderer stuff 2026-06-26 14:05:26 -05:00
tresabhi cead140855 optimize intro naimaiton 2026-06-26 14:01:39 -05:00
tresabhi 63a0143d4f optimize scene lighting and rnederer 2026-06-26 13:58:30 -05:00
tresabhi 19f22993f1 optimize shot display animator 2026-06-26 13:31:40 -05:00
tresabhi b2eb74914f optimize lighting loop 2026-06-26 13:22:24 -05:00
tresabhi 878ada0d10 some secret shit 2026-06-24 21:30:07 -05:00
tresabhi b285e9f1d0 discover servers 2026-06-24 20:21:05 -05:00
tresabhi c292ddcd6a update decorators 2026-06-23 14:41:44 -05:00
TrèsAbhi 05572918b1 exit 2026-06-23 10:35:27 -05:00
TrèsAbhi 1cc8b26b16 dispose 2026-06-23 10:30:56 -05:00
TrèsAbhi a8e8d20cf1 break 2026-06-23 10:26:05 -05:00
TrèsAbhi 55deb30607 logs 2026-06-23 10:20:22 -05:00
TrèsAbhi 03b8b89a17 log 2026-06-23 10:14:37 -05:00
TrèsAbhi 3e421c7eb7 done 2026-06-23 10:11:18 -05:00
TrèsAbhi fc27caabdc dvpl 2026-06-23 10:07:54 -05:00
tresabhi 3d35fae5ce dont log 2026-06-22 20:52:51 -05:00
tresabhi 81cc43bd08 setup 2026-06-22 20:44:09 -05:00
tresabhi f6dfc4f176 what 2026-06-22 20:32:14 -05:00
tresabhi f41eeba25e idk bro 2026-06-22 20:23:51 -05:00
tresabhi fdbbe2f356 Update applyPatches.ts 2026-06-22 20:18:25 -05:00
tresabhi cdf2aee43f debug 2026-06-22 20:13:52 -05:00
tresabhi 56933ddfac debug 2026-06-22 20:07:55 -05:00
tresabhi 1df2e818be dirs 2026-06-22 19:57:41 -05:00
tresabhi 1e4d00ea34 source 2026-06-22 19:55:39 -05:00
tresabhi 1bde50e0a3 garbage code 2026-06-22 19:54:09 -05:00
tresabhi 88290ec1ab garbage code 2026-06-22 19:54:01 -05:00
tresabhi e8325df832 lz4js 2026-06-22 19:44:03 -05:00
tresabhi 394f28e6ed remove proxyclient 2026-06-22 19:33:52 -05:00
tresabhi f5f170bcca switch to clean dava 2026-06-22 19:32:26 -05:00
tresabhi 2ffe3fc006 set up patches 2026-06-22 19:28:02 -05:00
TrèsAbhi 406d70b5b7 New translations en.json (Ukrainian) (#250)
[ci skip]
2026-06-22 09:38:55 -05:00
tresabhi 4e72ea567f Merge branch 'main' of https://github.com/blitzkit/blitzkit into ue 2026-06-17 12:35:55 -05:00
TrèsAbhi f801e81c8b New Crowdin updates (#249)
* New translations en.json (Russian)

[ci skip]

* New translations en.json (Russian)

[ci skip]
2026-06-17 09:26:26 -05:00
tresabhi b572ac5ea8 graphics and settings 2026-06-16 19:44:58 -05:00
tresabhi 7e2fba35d0 move characteristics 2026-06-16 14:12:59 -05:00
tresabhi e6fa17a911 tank model part as components 2026-06-14 14:37:52 -05:00
tresabhi a7d5bd6c75 center correction 2026-06-14 13:35:44 -05:00
tresabhi d424679257 undistrubed animation 2026-06-14 13:09:32 -05:00
tresabhi 4248e19f68 abstract away camera initials 2026-06-14 12:49:34 -05:00
tresabhi 59ef527967 more robust contorls 2026-06-14 12:40:42 -05:00
tresabhi 39aeba0766 first try at custom camera controls 2026-06-14 12:29:52 -05:00
TrèsAbhi a0e1d8cee0 New Crowdin updates (#247)
* New translations en.json (Russian)

[ci skip]

* New translations en.json (Russian)

[ci skip]

* New translations en.json (Russian)

[ci skip]

* New translations en.json (Russian)

[ci skip]
2026-06-14 09:49:53 -05:00
tresabhi c6f8c1e6a5 lighting and correct rotation ZY swap 2026-06-13 21:13:33 -05:00
tresabhi 333c310d5b model transformations 2026-06-13 21:06:37 -05:00
tresabhi c6c0e02fea ao maps 2026-06-13 20:26:43 -05:00
tresabhi 028f4230e5 fix banner paths 2026-06-13 20:02:27 -05:00
tresabhi cf31ad8f7a implement buffer proxy caching 2026-06-13 15:08:00 -05:00
tresabhi 9418526897 fix r3f types 2026-06-13 14:53:56 -05:00
TrèsAbhi dacdc9ae53 layout 2026-06-13 09:43:56 -05:00
TrèsAbhi 30296d020b encyclopedia 2026-06-13 09:35:15 -05:00
TrèsAbhi 43b7b4159f layout 2026-06-12 23:50:24 -05:00
TrèsAbhi 1aeae3b93b submodules 2026-06-12 23:41:28 -05:00
TrèsAbhi d657cd2360 clone 2026-06-12 23:38:56 -05:00
TrèsAbhi 222746be26 game 2026-06-12 23:34:38 -05:00
TrèsAbhi 95dd73de52 protoc 2026-06-12 23:29:36 -05:00
TrèsAbhi c893fa0a03 .net 2026-06-12 23:19:12 -05:00
TrèsAbhi c0e7f2f00a bun 2026-06-12 23:16:17 -05:00
TrèsAbhi 4dce8046cb Linux 2026-06-12 23:13:47 -05:00
TrèsAbhi 7df2425f76 readme 2026-06-12 23:06:35 -05:00
tresabhi e3cf2d51cc lighting 2026-06-12 20:59:43 -05:00
tresabhi 91ac93670f more control over model parts 2026-06-12 20:22:02 -05:00
tresabhi 8a9b02e584 okay, i swear, this is actually proper grouping 2026-06-12 20:12:19 -05:00
tresabhi c16ccc4b4c proper grouping 2026-06-12 19:45:37 -05:00
tresabhi 5dc11473bb format 2026-06-12 14:36:02 -05:00
tresabhi 2213f7d032 Merge branch 'ue' of https://github.com/blitzkit/blitzkit into ue 2026-06-12 14:18:05 -05:00
tresabhi 9d12c936b4 print 2026-06-12 14:17:43 -05:00
tresabhi 3f54e467d8 names groups 2026-06-12 14:17:29 -05:00
tresabhi 3dd0c4916d recursive mesh exporitng 2026-06-12 13:43:58 -05:00
TrèsAbhi d2ee528c52 reduce 2026-06-10 22:58:38 -05:00
tresabhi 0531fbf3ef always cancel 2026-06-10 20:23:56 -05:00
tresabhi 415aab3824 chunk upload 2026-06-10 20:23:00 -05:00
tresabhi f16e508e59 keys 2026-06-10 20:15:10 -05:00
tresabhi 3299ce069e texture caching 2026-06-10 20:12:18 -05:00
tresabhi 6395dd3f3d disable fail fast 2026-06-10 19:57:17 -05:00
tresabhi 9536cd2d39 update avatars 2026-06-10 19:55:53 -05:00
tresabhi 61abddc41b oodle 2026-06-10 13:50:13 -05:00
tresabhi b23f88b8a5 more debug 2026-06-10 13:43:09 -05:00
tresabhi 007ad3be15 more debug 2026-06-10 13:36:24 -05:00
tresabhi 218684b85c add debug 2026-06-10 13:30:45 -05:00
tresabhi 2e17ed16ed only use 9x 2026-06-10 13:18:45 -05:00
tresabhi 173c476fb4 Merge branch 'main' into ue 2026-06-10 13:11:59 -05:00
tresabhi e12ba1e78f Merge branch 'ue' of https://github.com/blitzkit/blitzkit into ue 2026-06-10 12:51:10 -05:00
tresabhi 648344d7cc module selection 2026-06-10 12:51:06 -05:00
TrèsAbhi a9a870c3e6 New translations en.json (Russian) (#245)
[ci skip]
2026-05-22 14:52:47 -05:00
TrèsAbhi 3d061a78de Merge pull request #242 from blitzkit/fixes
Fixes
2026-05-14 22:48:01 -05:00
TrèsAbhi a188a01244 Merge branch 'main' into fixes 2026-05-14 22:47:53 -05:00
TrèsAbhi 0828722876 logs 2026-05-15 03:47:40 +00:00
TrèsAbhi 0ccd9e45d8 Merge pull request #243 from karelpak1/fix-tank-icons-riff
fix: use RIFF sprite bounds for tank icon extraction
2026-05-14 22:36:09 -05:00
TrèsAbhi 2f745a16d1 clean up 2026-05-12 18:08:02 -05:00
TrèsAbhi 100c7391fb dispose models on unload 2026-05-12 17:54:50 -05:00
TrèsAbhi 4f42a31f53 Reduce interval timing from 60fps to 200ms 2026-05-11 17:52:23 -05:00
TrèsAbhi 0b2e322112 disbale low tiers 2026-05-11 17:14:22 -05:00
TrèsAbhi 042a1afa74 remove cache from useModel 2026-05-11 16:18:54 -05:00
TrèsAbhi 3edf4a25be impossible inspect element 2026-05-10 21:55:33 -05:00
TrèsAbhi 43237c2aee Merge pull request #244 from karelpak1/feat-tankopedia-modules-tooltips
feat: add localized tooltips to tankopedia modules tree
2026-05-02 10:03:15 -05:00
GonnaHetzMeandTrèsAbhi 2fffdc3ef5 Update packages/website/src/components/Tankopedia/CharacteristicsSection/components/Modules.tsx
Co-authored-by: TrèsAbhi <[email protected]>
2026-04-30 21:58:39 +02:00
GonnaHetzMe 76e748f0ed feat: add localized tooltips to tankopedia modules tree 2026-04-29 23:03:59 +02:00
GonnaHetzMe c353119962 fix: use RIFF sprite bounds for tank icon extraction 2026-04-29 21:19:43 +02:00
TrèsAbhi 5fa4d3b242 remove aeson 2026-04-28 16:17:40 -05:00
TrèsAbhi 068e0e26b0 Merge pull request #240 from karelpak1/fix-R5G6B5 2026-04-14 11:33:37 -05:00
TrèsAbhi f6ca77f8c8 Merge pull request #241 from karelpak1/chore-bump-version 2026-04-14 11:32:55 -05:00
TrèsAbhi 4c7eda81ca Merge pull request #239 from karelpak1/fix-alphatest-culling 2026-04-14 11:32:18 -05:00
TrèsAbhi 3948af9e39 fix: implement types and simplify code 2026-04-14 11:31:16 -05:00
GonnaHetzMe aec30e1944 chore: bump github actions versions 2026-04-13 02:13:53 +02:00
GonnaHetzMe 4205b16769 fix: correct RGB565 PVR decoding 2026-04-13 02:00:12 +02:00
GonnaHetzMe 4d13803b0c feat: culling 2026-04-12 22:35:55 +02:00
GonnaHetzMe b950ac1318 fix: sc2 alphatest threshold 2026-04-12 22:17:55 +02:00
762 changed files with 1775 additions and 48084 deletions

No files matched your search

+15 -9
View File
@@ -11,7 +11,7 @@ permissions:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
cancel-in-progress: true
env:
# blitz
@@ -31,11 +31,15 @@ env:
STEAM_OS: windows
STEAM_FINGERPRINT: Manifest_UFSFiles_Win64.txt
# debug
DOTNET_EnableDiagnostics: 1
jobs:
build:
name: Build chunk ${{ matrix.chunk }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
chunk: [pages, api, media, models, textures]
@@ -60,9 +64,7 @@ jobs:
- name: Set up Dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
10.0.x
9.0.x
dotnet-version: 9.0.x
- name: Set up Protoc
uses: arduino/setup-protoc@v3
@@ -77,13 +79,17 @@ jobs:
- name: Download Steam game
run: scripts/download_game.sh
- run: |
dotnet --info
dotnet --list-runtimes
bun --version
node --version
- name: Enable core dumps
run: |
ulimit -c unlimited
- name: Build
run: bun run build-website
env:
BUILD_CHUNK: ${{ matrix.chunk }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
name: chunk-${{ matrix.chunk }}
path: ${{ matrix.chunk == 'pages' && 'dist/website' || format('dist/website/{0}', matrix.chunk) }}
+95 -37
View File
@@ -1,47 +1,105 @@
# BlitzKit
Tools for everything and anything, World of Tanks Blitz.
Encyclopedia for World of Tanks Blitz.
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
## Development
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.
Setting up a local developer environment for BlitzKit is more complex than most projects, but this section will guide you through all the steps. If you're ever lost or confused, feel free to ask for help in [the official Discord server](https://discord.gg/nDt7AjGJQH).
## Packages
### Prerequisites
| 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. |
Before you touch any code, you'll need the following.
## Submodules
#### Linux
| Submodule | Title | Source | Memo |
| ------------------------------------------------------ | --------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [submodules/blitzkit-closed](packages/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. |
Because Node API for .NET doesn't resolve runtime-specific object files for libraries, the entirety of the BlitzKit project is "vendor-locked" to a single OS. GitHub Actions, responsible for BlitzKit CD/CI works best with Ubuntu, thus we must also use a Linux OS.
## Environment Variables
#### Windows (Optional)
| 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` | | | |
...
#### Bun
https://bun.com/
BlitzKit is written mostly in TypeScript, with packages fetched from NPM, managed by Bun.
#### .NET
https://dotnet.microsoft.com/download/dotnet/9.0/
You'll need the .NET 9.0 SDK to build and run the interface layer between BlitzKit and your local installation of World of Tanks Blitz.
#### Protocol Buffers
https://protobuf.dev/installation/
Both BlitzKit and World of Tanks Blitz encode their APIs with Protocol Buffers.
#### World of Tanks Blitz
https://store.steampowered.com/app/444200/
You'll need an up-to-date installation of World of Tanks Blitz on your computer. You may use the same installation that you play, even on a Windows partition if you dual boot.
#### UE4SS
...
#### protodump
...
#### FModel (Optional)
...
### Set Up
Now we can start installing and initiating the code of BlitzKit.
#### Cloning
BlitzKit is a massive monorepo with a lot of submodules. You'll likely not need all the branches and commit history. So, clone with depth 1.
```bash
git clone --depth 1 https://github.com/blitzkit/blitzkit.git
cd blitzkit
git submodule update --init --recursive --depth 1
```
#### Proto Files
...
#### Mappings
...
#### Environment Variables
...
#### The Big One
...
### Modifying The Code
This is where you can start changing things and proposing them through pull requests.
#### Server
...
#### Building
...
#### Branches & Pull Requests
...
#### Updating
...
+13 -14
View File
@@ -76,7 +76,6 @@
"dotenv": "^17.2.2",
"fast-xml-parser": "^5.3.2",
"lodash-es": "^4.17.21",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"three": "^0.184.0",
@@ -101,6 +100,7 @@
"dependencies": {
"@blitzkit/closed": "workspace:*",
"@blitzkit/core": "workspace:*",
"@blitzkit/game": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@gltf-transform/core": "^4.2.1",
"@gltf-transform/extensions": "^4.2.1",
@@ -122,7 +122,6 @@
"fast-xml-parser": "^5.2.5",
"googleapis": "^162.0.0",
"lodash-es": "^4.17.21",
"lz4js": "^0.2.0",
"progress": "^2.0.3",
"puppeteer": "^24.22.3",
"sharp": "^0.34.4",
@@ -206,8 +205,6 @@
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.184.1",
"@vite-pwa/astro": "^1.2.0",
"@wooorm/starry-night": "^3.9.0",
@@ -689,7 +686,7 @@
"@fontsource/inter": ["@fontsource/[email protected]", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
"@gltf-transform/core": ["@gltf-transform/core@4.3.0", "", { "dependencies": { "property-graph": "^4.0.0" } }, "sha512-ZeaQfszGJ9LYwELszu45CuDQCsE26lJNNe36FVmN8xclaT6WDdCj7fwGpQXo0/l/YgAVAHX+uO7YNBW75/SRYw=="],
"@gltf-transform/core": ["@gltf-transform/core@4.4.0", "", { "dependencies": { "property-graph": "^4.1.0" } }, "sha512-cOPxOhHFFz5hwmix+li1+Nnq5qMV/QD3fTCsVlApxxFACtFdjkt2R/juseD4gvZ7D2c/yl6OilKH0pvI735YyQ=="],
"@gltf-transform/extensions": ["@gltf-transform/[email protected]", "", { "dependencies": { "@gltf-transform/core": "^4.3.0", "ktx-parse": "^1.0.1" } }, "sha512-XDAjQPYVMHa/VDpSbfCBwI+/1muwRJCaXhUpLgnUzAjn0D//PgvIAcbNm1EwBl3LIWBSwjDUCn2LiMAjp+aXVw=="],
@@ -2469,7 +2466,7 @@
"prompts": ["[email protected]", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"property-graph": ["property-graph@4.0.0", "", {}, "sha512-I0hojAJfTbSCZy3y6xyK29eayxo14v1bj1VPiDkHjTdz33SV6RdfMz2AHnf4ai62Vng2mN5GkaKahkooBIo9gA=="],
"property-graph": ["property-graph@4.1.0", "", {}, "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ=="],
"property-information": ["[email protected]", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
@@ -3093,9 +3090,9 @@
"@blitzkit/scripts/@types/three": ["@types/[email protected]", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg=="],
"@blitzkit/scripts/three": ["[email protected]", "", {}, "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w=="],
"@blitzkit/scripts/lodash-es": ["[email protected]", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"@blitzkit/scripts/yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"@blitzkit/scripts/three": ["[email protected]", "", {}, "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w=="],
"@blitzkit/website/@types/three": ["@types/[email protected]", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg=="],
@@ -3107,10 +3104,6 @@
"@blitzkit/website-ue/@astrojs/node": ["@astrojs/[email protected]", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "send": "^1.2.1", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^5.17.3" } }, "sha512-rtU2BGU5u3SfGURpANfMxVzCIoR86MkaN05ncza9rbtuMKJ/XnRJt/BbyVknDbOJ71hoci0SIsJwKcJR8vvi/A=="],
"@blitzkit/website-ue/@gltf-transform/core": ["@gltf-transform/[email protected]", "", { "dependencies": { "property-graph": "^4.1.0" } }, "sha512-cOPxOhHFFz5hwmix+li1+Nnq5qMV/QD3fTCsVlApxxFACtFdjkt2R/juseD4gvZ7D2c/yl6OilKH0pvI735YyQ=="],
"@blitzkit/website-ue/@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
"@blitzkit/website-ue/astro": ["[email protected]", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.6", "@astrojs/markdown-remark": "6.3.11", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.27.3", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-TnFwLnAXty5MXKPDGuKXqK4AMBXG+FH6RUdK7Oyc3gyfNoFIthT+4eRbzOK43bdRlLaZuxgciDSjgtggZ3OtGQ=="],
"@blitzkit/website-ue/astro-favicons": ["[email protected]", "", { "dependencies": { "favilib": "^1.0.5", "ultrahtml": "^1.5.3" }, "peerDependencies": { "astro": ">= 4.0.0" } }, "sha512-9YfNWaBEFz1nCHy4P1l4wfPRvuLe4qHuHovIcqD5HYfZeW82wyVG1+CZUVGhcjwbRCVkGRJ3G+CKTyX9BcoI5w=="],
@@ -3125,6 +3118,10 @@
"@discordjs/ws/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@gltf-transform/extensions/@gltf-transform/core": ["@gltf-transform/[email protected]", "", { "dependencies": { "property-graph": "^4.0.0" } }, "sha512-ZeaQfszGJ9LYwELszu45CuDQCsE26lJNNe36FVmN8xclaT6WDdCj7fwGpQXo0/l/YgAVAHX+uO7YNBW75/SRYw=="],
"@gltf-transform/functions/@gltf-transform/core": ["@gltf-transform/[email protected]", "", { "dependencies": { "property-graph": "^4.0.0" } }, "sha512-ZeaQfszGJ9LYwELszu45CuDQCsE26lJNNe36FVmN8xclaT6WDdCj7fwGpQXo0/l/YgAVAHX+uO7YNBW75/SRYw=="],
"@jimp/core/file-type": ["[email protected]", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"@rollup/plugin-babel/@rollup/pluginutils": ["@rollup/[email protected]", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="],
@@ -3319,8 +3316,6 @@
"@blitzkit/website-ue/@astrojs/node/@astrojs/internal-helpers": ["@astrojs/[email protected]", "", {}, "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q=="],
"@blitzkit/website-ue/@gltf-transform/core/property-graph": ["[email protected]", "", {}, "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ=="],
"@blitzkit/website-ue/astro/@astrojs/internal-helpers": ["@astrojs/[email protected]", "", {}, "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q=="],
"@blitzkit/website-ue/astro/@astrojs/markdown-remark": ["@astrojs/[email protected]", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="],
@@ -3341,6 +3336,10 @@
"@blitzkit/website/vite/postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"@gltf-transform/extensions/@gltf-transform/core/property-graph": ["[email protected]", "", {}, "sha512-I0hojAJfTbSCZy3y6xyK29eayxo14v1bj1VPiDkHjTdz33SV6RdfMz2AHnf4ai62Vng2mN5GkaKahkooBIo9gA=="],
"@gltf-transform/functions/@gltf-transform/core/property-graph": ["[email protected]", "", {}, "sha512-I0hojAJfTbSCZy3y6xyK29eayxo14v1bj1VPiDkHjTdz33SV6RdfMz2AHnf4ai62Vng2mN5GkaKahkooBIo9gA=="],
"@rollup/plugin-babel/@rollup/pluginutils/@types/estree": ["@types/[email protected]", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="],
"@rollup/plugin-babel/@rollup/pluginutils/estree-walker": ["[email protected]", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="],
+8 -1
View File
@@ -10,7 +10,14 @@
"hocs",
"cooldown",
"Lods",
"Skia"
"Skia",
"RMAO",
"Verts",
"drei",
"frameloop",
"pointermove",
"pointerup",
"pointerdown"
],
"userWords": [
"WOTB",
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.30.3
Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions this update.
## Features
- [GonnaHetzMe] Added tooltips to the module selector in Tankopedia
## Fixes
- Added aggressive re-rendering for the Guess mini-game's score readout to make inspect-element harder
- Removed tank visual and collision model caching
- Guesser once again breaks streaks if not all tiers are selected
- BlitzKit now properly unloads tank models from memory when visually hidden in Guesser
- [GonnaHetzMe] Tanks with complex transparency like the Herdbreaker once again handle opacity correctly
- [GonnaHetzMe] Pieces of tank models that have transparency now handle culling properly (they're visible from both sides)
- [GonnaHetzMe] Tanks with "cardboard" textures have been fixed, thanks to better PVR file parsing
- [GonnaHetzMe] Tank icons are now scaled correctly so small tanks are now properly zoomed out in contrast to bigger ones
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.30.4
## Fixes
- BlitzKit now correctly implements minor WoTB updates
+8
View File
@@ -0,0 +1,8 @@
# BlitzKit 1.30.5
## Features
- Reduced the amount of code that executes for every rendered frame, hopefully improving performance
- Reduced the number of lights by 20% to decrease the amount of shadow calculations
- Decreased the ground size by 80% for an equal reduction in shadow texture sizes
- Added the ability for BlitzKit to dismount the components that handled the intro lighting animation when not animating, reducing checks running every frame
+7
View File
@@ -0,0 +1,7 @@
# BlitzKit 1.30.6
## Fixes
- Tanks no longer "randomly" appear dark in Tankopedia and Mixer
- The ground is now as dark as other modes when Static Armor is selected in Tankopedia
- Fixed some naming issues with the "equal" icon, improving performance a tiny tiny bit
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.6",
"version": "1.3.0",
"commands": [
"csharpier"
],
+2 -1
View File
@@ -7,7 +7,8 @@
"setup": "chmod a+x scripts/*.sh && ./scripts/setup.sh",
"dev": "./scripts/dev.sh",
"build-game": "./scripts/build_game.sh",
"build-website": "./scripts/build_website.sh"
"build-website": "./scripts/build_website.sh",
"discover-servers": "./scripts/discover_servers.sh"
},
"type": "module",
"workspaces": [
-18
View File
@@ -1,18 +0,0 @@
{
"name": "@blitzkit/app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/cli": "^2",
"vite": "^6.0.3",
"typescript": "~5.6.2"
}
}
-7
View File
@@ -1,7 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
-5286
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "blitzkit"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "blitzkit_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
@@ -1,10 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
-14
View File
@@ -1,14 +0,0 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
-27
View File
@@ -1,27 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Command, Stdio};
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
let entry = app_handle
.path_resolver()
.resolve_resource("../../dist/website/server/entry.mjs")
.expect("failed to resolve server.ts");
Command::new("bun")
.arg("run")
.arg(entry)
.stderr(Stdio::null())
.spawn()
.expect("failed to start Bun server");
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running Tauri app");
}
-38
View File
@@ -1,38 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "blitzkit",
"version": "0.1.0",
"identifier": "com.tresabhi.blitzkit",
"build": {
"beforeDevCommand": "cd ../website && bun run dev",
"_beforeBuildCommand": "cd ../website && bun run build",
"frontendDist": "../../../dist/website/client",
"devUrl": "http://localhost:4321"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"url": "http://localhost:4321",
"title": "BlitzKit",
"fullscreen": false,
"resizable": true,
"maximized": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"extends": ["../../tsconfig.json"],
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
-35
View File
@@ -1,35 +0,0 @@
{
"name": "@blitzkit/bot",
"dependencies": {
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/themes": "^3.3.0",
"@resvg/resvg-js": "^2.6.2",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.3.9",
"@types/lodash-es": "^4.17.12",
"@types/markdown-escape": "^1.1.3",
"@types/react": "^19.2.14",
"adm-zip": "^0.5.16",
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"fuzzysort": "^3.1.0",
"lodash-es": "^4.17.23",
"markdown-escape": "^2.0.0",
"pickleparser": "^0.2.1",
"prisma": "^6.19.2",
"protobufjs": "^7.5.4",
"react": "^19.2.4",
"satori": "^0.18.4",
"sharp": "^0.34.5"
},
"private": true,
"scripts": {
"dev": "bun --watch src/main.ts",
"start": "bun src/main.ts"
},
"type": "module",
"types": "src/types.d.ts"
}
-68
View File
@@ -1,68 +0,0 @@
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import {
commandsArray,
type CommandRegistry,
} from "../events/interactionCreate";
const subcommands = [
"embeds",
"introduction",
"invite",
"timezones",
"credits",
] as const;
type Subcommand = (typeof subcommands)[number];
const DOCS: Record<Subcommand, string> = {
embeds: "guide/embeds",
introduction: "guide/introduction",
invite: "guide/invite",
timezones: "guide/timezones",
credits: "about/credits",
};
export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
command: createLocalizedCommand("about", [
{ subcommand: "commands" },
...subcommands.map((subcommand) => ({ subcommand })),
]),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
const { strings } = translator(interaction.locale);
if (subcommand === "commands") {
return `${strings.bot.commands.about.subcommands.commands.body}${(
await Promise.all(commandsArray)
)
.sort((a, b) =>
(a.command.name_localizations?.[interaction.locale] ??
a.command.name) <
(b.command.name_localizations?.[interaction.locale] ??
b.command.name)
? -1
: 1,
)
.map(
(registry) =>
`- \`/${
registry.command.name_localizations?.[interaction.locale] ??
registry.command.name
}\`: ${
registry.command.description_localizations?.[
interaction.locale
] ?? registry.command.description
}`,
)
.join("\n")}`;
}
const url = `https://raw.githubusercontent.com/tresabhi/blitzkit/main/docs/${DOCS[subcommand as Subcommand]}.md`;
const content = fetch(url).then((response) => response.text());
return content;
},
});
});
@@ -1,65 +0,0 @@
import { addPeriodicFilterOptions } from "../../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../../core/discord/resolvePeriodFromButton";
import { resolvePlayerFromButton } from "../../core/discord/resolvePlayerFromButton";
import { translator } from "../../core/localization/translator";
import type { CommandRegistry } from "../../events/interactionCreate";
import { renderBreakdown } from "./render";
export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("breakdown"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return [
// ...(await renderBreakdown(
// player,
// period,
// filters,
// interaction.locale,
// )),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(player.region, player.id),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(
player,
period,
filters,
interaction.locale,
);
},
});
},
);
@@ -1,295 +0,0 @@
import {
type BlitzStats,
calculateWN8,
getAccountInfo,
getClanAccountInfo,
getTankStats,
getWN8Percentile,
TankDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { chunk } from "lodash-es";
import * as Breakdown from "../../components/Breakdown";
import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import {
averageDefinitions,
tankDefinitions,
} from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import {
filterStats,
type StatFilters,
} from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import type { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import type { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await averageDefinitions;
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ["clan"]);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: BlitzStats[] = [];
const orderedCareerStats: BlitzStats[] = [];
const orderedCurrentWN8: (number | undefined)[] = [];
const orderedCareerWN8: (number | undefined)[] = [];
const orderedCareerWN8Full: (number | undefined)[] = [];
if (tankStats === null) {
throw new UserError(strings.bot.common.errors.no_tank_stats);
}
filteredOrder.forEach((id) => {
const tankAveragesEntry = awaitedTankAverages.averages[id];
const current = statsInPeriod.diff[id];
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
const currentWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, current)
: undefined;
const careerWN8 = tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, career)
: undefined;
orderedCurrentStats.push(current);
orderedCareerStats.push(career);
orderedCurrentWN8.push(currentWN8);
orderedCareerWN8.push(careerWN8);
});
tankStats.forEach((tank) => {
const tankAveragesEntry = awaitedTankAverages.averages[tank.tank_id];
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.mu, tank.all)
: undefined,
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0,
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0,
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0,
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0,
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0,
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0,
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0,
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0,
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0,
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0,
) / careerBattles;
const children: React.JSX.Element[] = [];
if (filteredOrder.length > 0) {
children.push(
<Breakdown.Row
key={id}
displayType="summary"
title={strings.bot.commands.breakdown.body.total}
stats={[
{
title: strings.bot.commands.breakdown.body.battles,
current: currentBattles,
career: careerBattles,
},
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
},
{
title: strings.bot.commands.breakdown.body.wn8,
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
},
]}
/>,
);
filteredOrder.forEach((id, index) => {
const tankDefinition = awaitedTankDefinitions.tanks[id] as
| TankDefinition
| undefined;
const current = orderedCurrentStats[index];
const career = orderedCareerStats[index];
const currentWN8 = orderedCurrentWN8[index];
const careerWN8 = orderedCareerWN8[index];
children.push(
<Breakdown.Row
key={id}
displayType="tank"
class={tankDefinition?.class}
type={tankDefinition?.type}
title={
tankDefinition
? unwrap(tankDefinition.name)
: literals(strings.bot.commands.breakdown.body.unknown_tank, {
id,
})
}
stats={[
{
title: strings.bot.commands.breakdown.body.battles,
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
current.wins / current.battles - career.wins / career.battles,
},
{
title: strings.bot.commands.breakdown.body.wn8,
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>,
);
});
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<CommandWrapper>
{index === 0 && (
<TitleBar
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
)}
<Breakdown.Root>{page}</Breakdown.Root>
</CommandWrapper>
))
.reverse();
} else {
return [
<CommandWrapper>
<TitleBar
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
<NoData type="battles_in_period" locale={locale} />
</CommandWrapper>,
];
}
}
-37
View File
@@ -1,37 +0,0 @@
import packageJSON from "../../../../package.json";
import { client } from "../core/discord/client";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const executionStart = new Date().getTime();
export const debugCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("debug"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const currentTime = new Date().getTime();
const uptime = currentTime - executionStart;
const list = [
[strings.bot.commands.debug.body.version, packageJSON.version],
[
strings.bot.commands.debug.body.shard.label,
client.shard?.ids[0] ?? strings.bot.commands.debug.body.shard.default,
],
[
strings.bot.commands.debug.body.uptime,
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
(uptime / 1000 / 60) % 60
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
uptime % 1000
)}ms`,
],
];
return `${strings.bot.commands.debug.body.title}${list
.map(([key, value]) => `- ${key}: ${value}`)
.join("\n")}`;
},
});
});
-182
View File
@@ -1,182 +0,0 @@
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
import { Locale, SlashCommandSubcommandGroupBuilder } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Graph from "../components/Graph";
import { LineColor } from "../components/Graph/components/Line/constants";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { getPlayerHistories } from "../core/blitzstars/getPlayerHistories";
import { getTankHistories } from "../core/blitzstars/getTankHistories";
import { addPeriodSubCommands } from "../core/discord/addPeriodSubCommands";
import { addTankChoices } from "../core/discord/addTankChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
type EvolutionStatType = "player" | "tank";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
type: "player" | "tank",
tankId: number | null,
locale: Locale,
) {
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const logo = clan ? emblemURL(clan.emblem_set_id) : undefined;
const histories = await (type === "player"
? getPlayerHistories(region, id, {
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
})
: getTankHistories(region, id, locale, {
tankId: tankId!,
start,
end,
includeLatestHistories: true,
includePreviousHistories: true,
}));
const plot = histories.map(
(history) =>
[
history.all.battles,
(history.all.wins / history.all.battles) * 100,
] as Graph.PlotItem,
);
const times = histories.map((history) => history.last_battle_time);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const ys = plot.map(([, y]) => y);
const xs = plot.map(([x]) => x);
const maxY = Math.max(...ys);
const minY = Math.min(...ys);
const maxX = Math.max(...xs);
const minX = Math.min(...xs);
return (
<CommandWrapper>
<TitleBar title={accountInfo.nickname} image={logo} description={name} />
{plot.length > 0 && (
<Graph.Root
verticalMargin={{
min: minY,
max: maxY,
suffix: "%",
precision: 2,
}}
horizontalMargin={{
min: minX,
max: maxX,
precision: 0,
}}
xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
>
{Graph.Line({
color: LineColor.Red,
plot: plot,
minY,
maxY,
})}
</Graph.Root>
)}
{plot.length === 0 && <NoData type="battles_in_period" locale={locale} />}
</CommandWrapper>
);
}
export const evolutionCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: createLocalizedCommand("evolution", [
{
group: "player",
modify(option: SlashCommandSubcommandGroupBuilder) {
addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices),
);
},
},
{
group: "tank",
modify(option: SlashCommandSubcommandGroupBuilder) {
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameChoices),
);
},
},
]),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const commandGroup =
// interaction.options.getSubcommandGroup() as EvolutionStatType;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const tankIdRaw = interaction.options.getString("tank")!;
// const tankId =
// commandGroup === "tank"
// ? await resolveTankId(tankIdRaw, interaction.locale)
// : null;
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// tankId,
// });
// return [
// await render(
// player,
// period,
// commandGroup,
// tankId,
// interaction.locale
// ),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(
// player.region,
// player.id,
// tankId ?? undefined
// ),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const url = new URL(`https://example.com/${interaction.customId}`);
const path = url.pathname.split("/");
const commandGroup = path[2] as EvolutionStatType;
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
return await render(
player,
period,
commandGroup,
parseInt(url.searchParams.get("tankId") ?? "0") || null,
interaction.locale,
);
},
});
},
);
-117
View File
@@ -1,117 +0,0 @@
import {
emblemURL,
getAccountInfo,
getClanAccountInfo,
getTierWeights,
} from "@blitzkit/core";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericAllStats } from "../components/GenericAllStats";
import { NoData } from "../components/NoData";
import { TierWeights } from "../components/TierWeights";
import { TitleBar } from "../components/TitleBar";
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary, filteredOrder } = await filterStats(
diffedTankStats,
filters,
);
const filterDescriptions = await filtersToDescription(filters, locale);
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
return (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{stats.battles === 0 && (
<NoData type="battles_in_period" locale={locale} />
)}
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
{stats.battles > 0 && (
<GenericAllStats
stats={stats}
supplementaryStats={supplementary}
locale={locale}
/>
)}
</CommandWrapper>
);
}
export const fullStatsCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("full-stats"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return Promise.all([
// render(player, period, filters, interaction.locale),
// buttonRefresh(interaction, path),
// getBlitzStarsLinkButton(player.region, player.id),
// ]);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters, interaction.locale);
},
});
},
);
-96
View File
@@ -1,96 +0,0 @@
import { getAccountInfo, getClanInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericStats } from "../components/GenericStats";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { addClanChoices } from "../core/discord/addClanChoices";
import { autocompleteClan } from "../core/discord/autocompleteClan";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_THRESHOLD = 7;
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("inactive")
.addStringOption(addClanChoices)
.addNumberOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName("threshold")
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.inactive.options.threshold.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.inactive.options.threshold.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.inactive.options.threshold.description
)
)
.setMinValue(0);
}),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
const inactive = accountInfo
.map(
(account) =>
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number]
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
.map(
([name, days]) =>
[
name,
literals(strings.bot.commands.inactive.body.listing, {
days: days.toFixed(0),
}),
] as [string, string]
);
const hasInactiveMembers = inactive.length > 0;
return (
<CommandWrapper>
<TitleBar
title={clanInfo.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${literals(
strings.bot.commands.inactive.body.subtitle,
{ days: threshold }
)} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
{!hasInactiveMembers && (
<NoData type="players_in_period" locale={interaction.locale} />
)}
{hasInactiveMembers && <GenericStats stats={inactive} />}
</CommandWrapper>
);
},
autocomplete: autocompleteClan,
});
});
-193
View File
@@ -1,193 +0,0 @@
import { getAccountInfo, getClanAccountInfo } from '@blitzkit/core';
import { teal } from '@radix-ui/colors';
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
import { CommandWrapper } from '../components/CommandWrapper';
import { linkBlitzAndDiscord } from '../core/db/linkBlitzAndDiscord';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import type { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('link').addStringOption((option) =>
addUsernameChoices(option).setRequired(true),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = BigInt(interaction.user.id);
const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
await linkBlitzAndDiscord(discordId, id);
return [
<CommandWrapper fat>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
}}
>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
alt="avatar"
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<img
alt="accounts linked"
src="https://i.imgur.com/jIcRgog.png"
style={{
width: 16,
height: 16,
}}
/>
<span style={{ fontSize: 16 }}>
{strings.bot.commands.link.body.accounts_linked}
</span>
</div>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img
alt="clan emblem"
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
/>
) : null}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{accountInfo.nickname}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] • `
: ''}
{strings.common.regions.normal[region]}
</span>
</div>
</div>
</div>
<Glow color={teal.teal9} rotation={-90} />
</div>
</CommandWrapper>,
embedInfo(
strings.bot.commands.link.embed.title,
strings.bot.commands.link.embed.description,
),
];
},
autocomplete: autocompleteUsername,
});
});
-90
View File
@@ -1,90 +0,0 @@
import {
TANK_CLASSES,
TIER_ROMAN_NUMERALS,
TankDefinition,
flags,
getAccountInfo,
getTankStats,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import {
gameDefinitions,
tankDefinitions,
} from "../core/blitzkit/nonBlockingPromises";
import { addTierChoices } from "../core/discord/addTierChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { chunkLines } from "../core/discord/chunkLines";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("owned-tanks")
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const tier = Number(interaction.options.getString("tier"));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id);
if (tankStats === null) {
return strings.bot.common.errors.no_tank_stats;
}
const filteredTanks = (
await Promise.all(
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id,
})),
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = [];
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation);
} else {
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
}
});
const awaitedTankDefinitions = await tankDefinitions;
const awaitedGameDefinitions = await gameDefinitions;
const title = literals(strings.bot.commands.owned_tanks.body.title, {
name: markdownEscape(accountInfo.nickname),
tier: TIER_ROMAN_NUMERALS[tier],
});
const tankList = tankStats
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
)
.sort(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation),
)
.map(
(tank) =>
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name!))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
);
const lines = [title, ...tankList];
return chunkLines(lines);
},
autocomplete: autocompleteUsername,
});
});
-46
View File
@@ -1,46 +0,0 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("permissions"),
async handler(interaction) {
const { strings } = translator(interaction.locale);
// friday the 13th permissions easter egg
const isFridayThe13th =
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has("ViewChannel"),
strings.bot.commands.permissions.body.view_channels,
],
[
interaction.appPermissions?.has("ReadMessageHistory"),
strings.bot.commands.permissions.body.read_message_history,
],
[
interaction.appPermissions?.has("AttachFiles"),
strings.bot.commands.permissions.body.attach_files,
],
[
interaction.appPermissions?.has("UseExternalEmojis"),
strings.bot.commands.permissions.body.external_emojis,
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
} else {
return `${isFridayThe13th ? "💀" : "❌"} ${description}`;
}
})
.join("\n");
return literals(strings.bot.commands.permissions.body.title, { body });
},
});
});
-34
View File
@@ -1,34 +0,0 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
handlesInteraction: true,
command: createLocalizedCommand("ping", [
{ subcommand: "blitzkit" },
{ subcommand: "wotb" },
]),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === "blitzkit") {
await interaction.editReply(strings.bot.commands.ping.body.pong);
} else {
await fetch("https://api.wotblitz.com/");
}
const executionTime = Date.now() - executionStart;
interaction.editReply(
literals(strings.bot.commands.ping.body.pong_with_time, {
time: executionTime.toFixed(0),
})
);
},
});
});
-69
View File
@@ -1,69 +0,0 @@
import { getAccountInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { markdownTable } from "../core/discord/markdownTable";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command:
createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const account = await resolvePlayerFromCommand(interaction);
const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
literals(strings.bot.commands.player_info.body.title, {
name: markdownEscape(accountInfo.nickname),
}),
markdownTable([
[
strings.bot.commands.player_info.body.nickname,
`${accountInfo.nickname}`,
],
[
strings.bot.commands.player_info.body.battles,
`${accountInfo.statistics.all.battles}`,
],
[
strings.bot.commands.player_info.body.winrate,
`${(
100 *
(accountInfo.statistics.all.wins /
accountInfo.statistics.all.battles)
).toFixed(2)}%`,
],
[],
[
strings.bot.commands.player_info.body.account_id,
`${accountInfo.account_id}`,
],
[
strings.bot.commands.player_info.body.created,
new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale
),
],
[
strings.bot.commands.player_info.body.last_battle,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale
),
],
])
);
},
autocomplete: autocompleteUsername,
});
});
-535
View File
@@ -1,535 +0,0 @@
import {
deltaBkrlBlitzStats,
emblemURL,
getAccountInfo,
getArchivedRatingLeaderboard,
getClanAccountInfo,
getLeagueFromScore,
getRatingInfo,
getRatingNeighbors,
imgur,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { SlashCommandSubcommandBuilder } from "discord.js";
import markdownEscape from "markdown-escape";
import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
import { CommandWrapper } from "../components/CommandWrapper";
import { DeltaCaret } from "../components/DeltaCaret";
import { TitleBar } from "../components/TitleBar";
import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
import { iconPng } from "../core/blitzkit/iconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
const LEAGUE_ACCENT = [
"_purple",
"_sand",
"_amber",
"_mauve",
"_brown",
] as const;
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("rating", [
{
subcommand: "today",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
{
subcommand: "season",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
]),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true) as
| "season"
| "today";
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const ratingInfo = await getRatingInfo(region);
if (ratingInfo.detail) {
throw new Error(`Rating info not available for ${region}`);
}
const leaderboard = await getArchivedRatingLeaderboard(
region,
ratingInfo.current_season - (subcommand === "today" ? 0 : 1)
);
if (leaderboard.version!.$case === "v1") {
throw new Error(
"Encountered bkrl minimal format in latest season. Wait till next cron?"
);
}
const accountInfo = await getAccountInfo(region, id, [
"statistics.rating",
]);
const statsIndex = leaderboard.version!.value.entries.findIndex(
(entry) => entry.id === id
);
const statsA = leaderboard
? leaderboard.version!.value.entries[statsIndex!]
: null;
const positionA = leaderboard ? statsIndex! + 1 : undefined;
const statsB1 = accountInfo.statistics.rating!;
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
if (statsB2Array === undefined) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const [statsB2] = statsB2Array;
const league = getLeagueFromScore(statsB2.score);
if (!statsA) {
return strings.bot.commands.rating.errors.unrecorded_stats;
}
if (!statsB1) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
}
const reward = ratingInfo.rewards.find(
(reward) =>
statsB2.number >= reward.from_position &&
statsB2.number <= reward.to_position
);
const positionDelta = leaderboard
? statsB2.number - positionA!
: undefined;
const delta = deltaBkrlBlitzStats(
statsA,
accountInfo.statistics.rating!,
statsB2
);
const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward
? reward.type === "vehicle"
? reward.vehicle.preview_image_url
: reward.stuff.image_url
: undefined;
if (rewardImage) {
const { ok } = await fetch(rewardImage);
if (!ok) {
rewardImage = imgur("kP88vqr");
}
}
return (
<CommandWrapper>
<TitleBar
title={accountInfo.nickname}
image={clanImage}
description={`${
strings.bot.commands.rating.body.subtitle[subcommand]
} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
<div
style={{
position: "relative",
display: "flex",
overflow: "hidden",
}}
>
<div
style={{
position: "relative",
padding: 16,
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
borderRadius: 4,
border: `1px solid ${
theme.colors[`componentInteractive${leagueTheme}`]
}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
{reward === undefined && (
<Glow
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(-16px) rotate(90deg)",
}}
/>
)}
<div
style={{
display: "flex",
gap: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<img
alt={`League ${league.name}`}
style={{
width: 64,
height: 64,
objectFit: "contain",
}}
src={normalizeLeagueIcon(
ratingInfo.leagues[league.index].big_icon
)}
/>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
style={{
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 24,
fontWeight: 900,
}}
>
{statsB2.score.toLocaleString(interaction.locale)}
</span>
{delta.score !== 0 && (
<>
<DeltaCaret delta={delta.score} />
<span
style={{
color:
delta.score > 0
? theme.colors.textLowContrast_green
: theme.colors.textLowContrast_red,
fontSize: 16,
}}
>
{Math.abs(delta.score).toLocaleString(
interaction.locale
)}
</span>
</>
)}
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${leagueTheme}`],
fontSize: 16,
}}
>
#{statsB2.number.toLocaleString(interaction.locale)}
</span>
{positionDelta !== undefined && positionDelta !== 0 && (
<>
<DeltaCaret delta={-positionDelta} />
<span
style={{
color:
positionDelta > 0
? theme.colors.textLowContrast_red
: theme.colors.textLowContrast_green,
fontSize: 16,
}}
>
{Math.abs(positionDelta).toLocaleString(
interaction.locale
)}
</span>
</>
)}
</div>
</div>
</div>
{reward && (
<div
style={{
display: "flex",
gap: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
style={{
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 16,
maxWidth: 136,
textAlign: "right",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{reward.type === "vehicle"
? reward.vehicle.user_string
: reward.stuff.title}
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${leagueTheme}`],
fontSize: 16,
}}
>
Reward
{reward.type === "stuff" ? ` x${reward.count}` : ""}
</span>
</div>
</div>
<img
style={{
width: 64,
height: 64,
objectFit: "cover",
}}
src={await iconPng(rewardImage!)}
/>
</div>
)}
{reward === undefined && (
<Glow
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(16px) rotate(-90deg)",
}}
/>
)}
</div>
{/* separated because fragment middle man invalidates relative parent: https://github.com/vercel/satori/issues/605 */}
{reward && (
<Glow
direction="reverse"
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
top: -40,
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
{reward && (
<Glow
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
bottom: -40,
left: "50%",
transform: "translateX(-50%)",
}}
/>
)}
</div>
{delta.battles > 0 && (
<div style={{ padding: "8px 0", display: "flex" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{delta.battles.toLocaleString(interaction.locale)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Battles
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Winrate
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{Math.round(delta.damage / delta.battles).toLocaleString(
interaction.locale
)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Survival
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
flex: 1,
flexDirection: "column",
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
}}
>
{(delta.kills / delta.battles).toFixed(2)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
)}
</CommandWrapper>
);
},
});
});
@@ -1,468 +0,0 @@
import {
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
getAccountInfo,
getArchivedLatestSeasonNumber,
getArchivedRatingLeaderboard,
getClanAccountInfo,
getRatingInfo,
getRatingLeague,
getRatingNeighbors,
imgur,
isOnGoingRatingSeason,
LEAGUES,
type RatingInfo,
RatingLeaderboard,
type Region,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import {
type APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from "discord.js";
import { range } from "lodash-es";
import markdownEscape from "markdown-escape";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Leaderboard from "../components/Leaderboard";
import { TitleBar } from "../components/TitleBar";
import { UserError } from "../core/blitzkit/userError";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const DEFAULT_LIMIT = 10;
export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range(
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
).slice(-25);
function addOptions(option: SlashCommandSubcommandBuilder) {
const { strings } = translator(Locale.EnglishUS);
return option
.addIntegerOption((option) =>
option
.setName(strings.bot.commands.rating_leaderboard.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30)
)
.addStringOption((option) =>
option
.setName(
strings.bot.commands.rating_leaderboard.options.season.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.season.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season
.description
)
)
.setRequired(false)
.addChoices(
...seasonNumbers
.map((number, index) => {
const current =
index === seasonNumbers.length - 1 && onGoingSeason;
return {
name: current
? literals(
strings.bot.commands.rating_leaderboard.options.season
.choices.current,
{ season: number }
)
: `${number}`,
value: `${number}`,
name_localizations: current
? localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options
.season.choices.current,
{ season: number }
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse()
)
);
}
resolve({
command: createLocalizedCommand("rating-leaderboard", [
{
subcommand: "neighbors",
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: "league",
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName(
strings.bot.commands.rating_leaderboard.options.league.name
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.name,
undefined,
true
)
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.league
.description
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.description
)
)
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name: (
strings.common.leagues as Record<string, string>
)[name],
value: `${value}`,
name_localizations: localizationObject(
(strings) =>
(
strings.common.leagues as Record<string, string>
)[name]
),
} satisfies APIApplicationCommandOptionChoice<string>)
)
)
.setRequired(true);
})
.addStringOption(addRegionChoices);
addOptions(option);
},
},
]),
// BIG TODO: title seems to be in english
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| "league"
| "neighbors";
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString("season");
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string;
let titleImage: string | undefined;
let titleDescription: string;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let archivedLeaderboard: RatingLeaderboard | null;
let playerId: number | undefined;
let regionRatingInfo: RatingInfo | null;
if (subcommand === "league") {
const leagueIndex = parseInt(
interaction.options.getString("league")!
);
const region = interaction.options.getString("region") as Region;
regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region)
: null);
if (
regionRatingInfo !== null &&
regionRatingInfo.detail !== undefined
) {
return strings.bot.commands.rating_leaderboard.body
.no_ongoing_season;
}
const result = isCurrentSeason
? await getRatingLeague(region, leagueIndex).then(async (result) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
} satisfies SimplifiedPlayer)
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.version!.value.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.version!.value.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
["clan"]
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag,
nickname:
clanData[index]?.account_name ??
literals(
strings.bot.commands.rating_leaderboard.body
.deleted_player,
{ id: player.id }
),
} satisfies SimplifiedPlayer)
);
}
);
const leagueInfo = LEAGUES[leagueIndex];
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
);
players = result;
playersAfter =
(regionRatingInfo === null
? archivedLeaderboard.version!.value.entries.length
: regionRatingInfo.count) - result[result.length - 1].position;
titleName = `${
leagueInfo
? (strings.common.leagues as Record<string, string>)[
LEAGUES[leagueInfo.index].name
]
: ""
} - ${strings.common.regions.short[region]}`;
titleImage = imgur(leagueInfo.icon);
titleDescription = literals(
strings.bot.commands.rating_leaderboard.body.top_players,
{ count: limit }
);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingInfo = isCurrentSeason
? await getRatingInfo(region)
: null;
if (
regionRatingInfo !== null &&
regionRatingInfo.detail !== undefined
) {
return strings.bot.commands.rating_leaderboard.body
.no_ongoing_season;
}
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
} satisfies SimplifiedPlayer)
)
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.version!.value.entries.findIndex(
(player) => player.id === id
);
if (playerIndex === -1) {
throw new UserError(
literals(
strings.bot.commands.rating_leaderboard.body
.no_participation,
{ player: markdownEscape(accountInfo.nickname), season }
)
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.version!.value.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
["clan"]
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[index]?.clan?.tag,
nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
}
);
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : "";
titleDescription =
strings.bot.commands.rating_leaderboard.body.neighbors;
playersAfter =
(regionRatingInfo === null
? archivedLeaderboard.version!.value.entries.length
: regionRatingInfo.count) -
neighbors[neighbors.length - 1].position;
playerId = id;
}
const items = players.map((player) => {
const midnightIndex =
archivedLeaderboard?.version!.value.entries.findIndex(
(item) => item.id === player.id
);
const midnightScore =
midnightIndex === undefined
? player.score
: archivedLeaderboard!.version!.value.entries[midnightIndex]
.score;
const midnightPosition =
midnightIndex === undefined ? player.position : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.position}
clan={player.clan}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return (
<CommandWrapper>
<TitleBar
title={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={literals(
strings.bot.commands.rating_leaderboard.body.more,
{ count: playersAfter.toLocaleString(interaction.locale) }
)}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
);
},
autocomplete: autocompleteUsername,
});
}
);
-592
View File
@@ -1,592 +0,0 @@
import {
calculateWN8,
emblemURL,
getClanAccountInfo,
idToRegion,
tankIcon,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { Locale } from "discord.js";
import { clamp } from "lodash-es";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { iconPng } from "../core/blitzkit/iconPng";
import {
averageDefinitions,
mapDefinitions,
tankDefinitions,
} from "../core/blitzkit/nonBlockingPromises";
import { RenderConfiguration } from "../core/blitzkit/renderConfiguration";
import { buttonLink } from "../core/discord/buttonLink";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
type WotInspectorReplaySubmission =
| { status: "error"; data: unknown; error: { message: string } }
| {
status: "ok";
data: {
download_url: string;
view_url: string;
summary: {
battle_start_time: string;
map_name: string;
battle_start_timestamp: number;
battle_duration: number;
title: string;
arena_unique_id: string;
allies: number[];
enemies: number[];
protagonist: number;
player_name: string;
vehicle: string;
exp_base: number;
exp_total: number;
credits_base: number;
credits_total: number;
room_type: number;
battle_type: number;
mastery_badge: number;
battle_result: number;
vehicle_tier: number;
vehicle_type: number;
protagonist_team: number;
winner_team: number;
details: {
achievements: unknown[];
base_capture_points: number;
base_defend_points: number;
chassis_id: unknown | null;
clan_tag: string;
clanid: number;
credits: number;
damage_assisted: number;
damage_assisted_track: number;
damage_blocked: unknown | null;
damage_made: number;
damage_received: number;
dbid: number;
death_reason: number;
distance_travelled: number;
enemies_damaged: number;
enemies_destroyed: number;
enemies_spotted: number;
entity_id: number;
exp: number;
exp_for_assist: number;
exp_for_damage: number;
exp_team_bonus: number;
gun_id: unknown | null;
hero_bonus_credits: number;
hero_bonus_exp: number;
hitpoints_left: number;
hits_bounced: number;
hits_pen: number;
hits_received: number;
hits_splash: number;
killed_by: number;
name: string;
shots_hit: number;
shots_made: number;
shots_pen: number;
shots_splash: number;
squad_index: number;
team: number;
time_alive: number;
turret_id: unknown | null;
vehicle_descr: number;
wp_points_earned: number;
wp_points_stolen: number;
};
};
};
error: {};
};
interface WotInspectorReplay {
id: string;
map_id: number;
battle_duration: number;
title: unknown | null;
player_name: string;
protagonist: number;
vehicle_descr: number;
mastery_badge: number;
exp_base: number;
enemies_spotted: number;
enemies_destroyed: number;
damage_assisted: number;
damage_made: number;
details_url: string;
download_url: string;
game_version: {
name: string;
package: string;
};
arena_unique_id: string;
download_count: number;
data_version: number;
private: boolean;
private_clan: boolean;
battle_start_time: Date;
upload_time: Date;
allies: number[];
enemies: number[];
protagonist_clan: number;
protagonist_team: number;
battle_result: number;
credits_base: number;
tags: number[];
battle_type: number;
room_type: number;
last_accessed_time: Date;
winner_team: number;
finish_reason: number;
players_data: WotInspectorReplayPlayerData[];
exp_total: number;
credits_total: number;
repair_cost: number;
exp_free: number;
exp_free_base: number;
exp_penalty: number;
credits_penalty: number;
credits_contribution_in: number;
credits_contribution_out: number;
camouflage_id: number;
}
export interface WotInspectorReplayPlayerData {
team: number;
name: string;
entity_id: number;
dbid: number;
clanid: number | null;
clan_tag: null | string;
hitpoints_left: number;
credits: number;
exp: number;
shots_made: number;
shots_hit: number;
shots_splash: number;
shots_pen: number;
damage_made: number;
damage_received: number;
damage_assisted: number;
damage_assisted_track: number;
hits_received: number;
hits_bounced: number;
hits_splash: number;
hits_pen: number;
enemies_spotted: number;
enemies_damaged: number;
enemies_destroyed: number;
time_alive: number;
distance_travelled: number;
killed_by: number;
base_capture_points: number;
base_defend_points: number;
exp_for_damage: number;
exp_for_assist: number;
exp_team_bonus: number;
wp_points_earned: number;
wp_points_stolen: number;
hero_bonus_credits: number;
hero_bonus_exp: number;
death_reason: number;
achievements: any[];
vehicle_descr: number;
turret_id: number;
gun_id: number;
chassis_id: number;
squad_index: number;
damage_blocked: number;
}
function stat(value: string | number, flex = 2) {
return (
<div
style={{
flex,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
</div>
);
}
async function playerListing(
locale: Locale,
player: WotInspectorReplayPlayerData,
protagonistTeam: boolean,
winningTeam: number,
recordingPlayer = false,
) {
const { unwrap } = translator(locale);
const awaitedTankAverages = await averageDefinitions;
const awaitedTankDefinitions = await tankDefinitions;
const tank = awaitedTankDefinitions.tanks[player.vehicle_descr];
const blockAccent = recordingPlayer ? "_amber" : "";
const healthAccent = protagonistTeam
? recordingPlayer
? "_amber"
: "_green"
: "_red";
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
// TODO: 💀
const turret = tank.turrets.sort(
(a, b) =>
Math.abs(a.id - player.turret_id) - Math.abs(b.id - player.turret_id),
)[0];
const totalHealth = turret.health + tank.health;
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
const tankAveragesEntry = awaitedTankAverages.averages[player.vehicle_descr];
const wn8 =
tankAveragesEntry === undefined
? -1
: calculateWN8(tankAveragesEntry.mu, {
battles: 1,
damage_dealt: player.damage_made,
dropped_capture_points: player.base_defend_points,
frags: player.enemies_destroyed,
spotted: player.enemies_spotted,
wins: winningTeam === player.team ? 1 : 0,
});
return (
<div
key={player.entity_id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
}}
>
<div
style={{
display: "flex",
gap: 8,
flex: 1,
overflow: "hidden",
alignItems: "center",
}}
>
<img
alt={unwrap(tank.name!)}
src={await iconPng(tankIcon(tank.id))}
style={{
width: 32,
height: 32,
objectFit: "contain",
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
// align
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<span
style={{
color: theme.colors[`textLowContrast${blockAccent}`],
fontSize: 12,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
<span
style={{
color: theme.colors[`textHighContrast${blockAccent}`],
fontSize: 16,
}}
>
{unwrap(tank.name!)}
</span>
</div>
<span
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
whiteSpace: "nowrap",
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ""}
</span>
</div>
</div>
<div
style={{
display: "flex",
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat(wn8 === -1 ? "--" : Math.round(wn8).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
<div
style={{
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: "flex",
width: 4,
height: "100%",
transform: "scaleY(-1)",
}}
>
<div
style={{
width: "100%",
height: `${healthLeft * 100}%`,
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
}}
/>
</div>
</div>
);
}
function Header() {
return (
<div
style={{
padding: 8,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
flex: 1,
}}
>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
WN8
</span>
</div>
<div
style={{
flex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
XP
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
</div>
);
}
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("replay").addAttachmentOption((option) =>
option
.setName(strings.bot.commands.replay.options.file.name)
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.name,
undefined,
true,
),
)
.setDescription(strings.bot.commands.replay.options.file.description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.commands.replay.options.file.description,
),
)
.setRequired(true),
),
async handler(interaction) {
const awaitedMapDefinitions = await mapDefinitions;
const file = interaction.options.getAttachment("file")!;
const submissionData = (await fetch(
`https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent(
file.url,
)}`,
).then((response) => response.json())) as WotInspectorReplaySubmission;
if (submissionData.status === "error") {
return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`;
}
const replayId = submissionData.data.download_url.split("/").at(-1)!;
const replayData = (await fetch(
`https://api.wotinspector.com/v2/blitz/replays/${replayId}/`,
).then((response) => response.json())) as WotInspectorReplay;
const antagonistTeamId = 3 - replayData.protagonist_team;
const players = replayData.players_data.sort(
(a, b) => b.damage_made - a.damage_made,
);
const clan = (
await getClanAccountInfo(
idToRegion(replayData.protagonist),
replayData.protagonist,
["clan"],
)
)?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
return [
new RenderConfiguration({ width: 960 }),
<CommandWrapper>
<TitleBar
title={replayData.player_name}
image={clanImage}
description={`${
replayData.protagonist_team === replayData.winner_team
? "Victory"
: "Defeat"
} • ${awaitedMapDefinitions.maps[replayData.map_id].name}`}
/>
<div
style={{
display: "flex",
gap: 8,
}}
>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter(
(player) => player.team === replayData.protagonist_team,
)
.map((player) =>
playerListing(
interaction.locale,
player,
true,
replayData.winner_team,
player.dbid === replayData.protagonist,
),
),
)}
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter((player) => player.team === antagonistTeamId)
.map((player) =>
playerListing(
interaction.locale,
player,
false,
replayData.winner_team,
),
),
)}
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, "WoT Inspector"),
buttonLink(submissionData.data.download_url, "Download"),
];
},
});
});
-626
View File
@@ -1,626 +0,0 @@
import {
asset,
emblemURL,
equipmentPriceMatrix,
getAccountInfo,
getClanAccountInfo,
getTankStats,
TANK_ICONS,
TankType,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { escapeMarkdown, Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { resolveTankId } from "../core/blitz/resolveTankId";
import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
import { iconPng } from "../core/blitzkit/iconPng";
import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
import { tankIconPng } from "../core/blitzkit/tankIconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("research")
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.target_tank.name!)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.name,
undefined,
true,
),
)
.setDescription(
strings.bot.commands.research.options.target_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.description,
),
)
.setAutocomplete(true)
.setRequired(true),
)
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.starting_tank.name!)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.name,
undefined,
true,
),
)
.setDescription(
strings.bot.commands.research.options.starting_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.description,
),
)
.setAutocomplete(true)
.setRequired(false),
)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId(
interaction.options.getString("target-tank", true),
interaction.locale,
true,
);
const startingTankRaw = interaction.options.getString("starting-tank");
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
const targetTankAncestry = await resolveAncestry(targetTankId);
if (startingTankRaw) {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
);
if (targetTankId === startingTankId) {
return literals(
strings.bot.commands.research.errors.start_end_equal,
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
);
}
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
if (startingTank.tier > targetTank.tier) {
return literals(strings.bot.commands.research.errors.unordered, {
tank_1: escapeMarkdown(unwrap(startingtank.name!)),
tank_2: escapeMarkdown(unwrap(targettank.name!)),
});
}
if (!targetTankAncestry.includes(startingTankId)) {
return literals(
strings.bot.commands.research.errors.tanks_not_on_line,
{
tank_1: escapeMarkdown(unwrap(targettank.name!)),
tank_2: escapeMarkdown(unwrap(startingtank.name!)),
},
);
}
} else {
if (targetTank.type !== TankType.TANK_TYPE_RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, {
tank: escapeMarkdown(unwrap(targettank.name!)),
});
}
if (targetTank.tier === 1) {
return literals(strings.bot.commands.research.errors.tier_1, {
tank: escapeMarkdown(unwrap(targettank.name!)),
});
}
const tankStats = await getTankStats(region, id);
if (tankStats === null) {
return strings.bot.common.errors.no_tank_stats;
}
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return literals(
strings.bot.commands.research.errors.already_researched,
{ tank: escapeMarkdown(unwrap(targettank.name!)) },
);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
awaitedTankDefinitions.tanks[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id),
);
if (foundAncestor === undefined) {
throw new Error("No first owned tech tree found.");
}
startingTankId = foundAncestor;
}
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
);
const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const costs = await Promise.all(
line.map(async (id) => {
const tank = awaitedTankDefinitions.tanks[id];
const turretXps = new Map<number, number>();
const gunXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
tank.turrets.forEach((turret) => {
if (
turret.research_cost !== undefined &&
typeof turret.research_cost.research_cost_type!.value === "number"
) {
turretXps.set(
turret.id,
turret.research_cost.research_cost_type!.value,
);
}
turret.guns.forEach((gun) => {
if (
gun.research_cost !== undefined &&
typeof gun.research_cost.research_cost_type!.value === "number"
) {
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
}
});
});
tank.engines.forEach((engine) => {
if (
engine.research_cost !== undefined &&
typeof engine.research_cost.research_cost_type!.value === "number"
) {
engineXps.set(
engine.id,
engine.research_cost.research_cost_type!.value,
);
}
});
tank.tracks.forEach((track) => {
if (
track.research_cost !== undefined &&
typeof track.research_cost.research_cost_type!.value === "number"
) {
trackXps.set(
track.id,
track.research_cost.research_cost_type!.value,
);
}
});
let moduleXp = 0;
turretXps.forEach((value) => {
moduleXp += value;
});
gunXps.forEach((value) => {
moduleXp += value;
});
engineXps.forEach((value) => {
moduleXp += value;
});
trackXps.forEach((value) => {
moduleXp += value;
});
return {
research: tank.research_cost!.research_cost_type!.value as number,
upgrades: moduleXp,
purchase: tank.price!.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
),
};
}),
);
const image = (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${strings.bot.commands.research.body.subtitle} • ${
targetTank.name
}`}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<div
style={{
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div style={{ width: 48 }}></div>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
right: 24,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{strings.bot.commands.research.body.total}
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: "16px 0",
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Experience:{" "}
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Purchase:{" "}
{costs
.reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
Equipment:{" "}
{costs
.reduce((a, b) => a + b.equipment, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
</div>
</div>
{await Promise.all(
line.map(async (id, index) => {
const tank = awaitedTankDefinitions.tanks[id];
return (
<div
key={id}
style={{
display: "flex",
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div
style={{
width: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
</div>
<div
style={{
flex: 1,
display: "flex",
position: "relative",
right: "5%",
}}
>
<img
alt="Tank icon"
src={await tankIconPng(id)}
style={{
height: 100,
width: 100,
objectFit: "contain",
position: "absolute",
left: "25%",
top: 0,
}}
/>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
justifyContent: "center",
position: "absolute",
bottom: 8,
left: "50%",
transform: "translateX(-50%)",
}}
>
<img
alt={unwrap(tank.name!)}
src={TANK_ICONS[tank.class]}
width={16}
height={16}
/>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{unwrap(tank.name!)}
</span>
</div>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 8,
padding: "16px 0",
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.research,
{
cost: costs[index].research?.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
{costs[index].upgrades > 0 && (
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Free XP icon"
src={await iconPng(
asset("icons/currencies/free-xp.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.upgrades,
{ cost: costs[index].upgrades.toLocaleString() },
)}
</span>
</div>
)}
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.purchase,
{
cost: costs[index].purchase.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp"),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{literals(
strings.bot.commands.research.body.equipment,
{
cost: costs[index].equipment.toLocaleString(
interaction.locale,
),
},
)}
</span>
</div>
</div>
</div>
);
}),
)}
</div>
</CommandWrapper>
);
return startingTankRaw
? image
: [strings.bot.commands.research.body.estimation, image];
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]);
},
});
});
-74
View File
@@ -1,74 +0,0 @@
import { type ClanList, fetchBlitz, type Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addClanChoices } from "../core/discord/addClanChoices";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("search-clans")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addClanChoices(option).setAutocomplete(false)
)
.addIntegerOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName(strings.bot.commands.search_clans.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.search_clans.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100);
}),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const clan = interaction.options.getString("clan")!;
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, "clans/list", {
search: clan,
limit,
});
return embedInfo(
literals(strings.bot.commands.search_clans.body.title, {
search: markdownEscape(clan),
region: strings.common.regions.normal[region],
}),
clanList.length === 0
? strings.bot.commands.search_clans.body.no_results
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join("\n")}\n\`\`\``
);
},
});
});
@@ -1,83 +0,0 @@
import {
type AccountList,
fetchBlitz,
type Region,
usernamePattern,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25;
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("search-players")
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true)
)
.addIntegerOption((option) =>
option
.setName(strings.bot.commands.search_players.options.limit.name)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.name,
undefined,
true
)
)
.setDescription(
literals(
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
)
.setMinValue(1)
.setMaxValue(100)
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const name = interaction.options.getString("username")!;
const limit = interaction.options.getInteger("limit") ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(region, "account/list", {
search: trimmedSearch,
limit,
})
: [];
return embedInfo(
literals(strings.bot.commands.search_players.body.title, {
search: markdownEscape(trimmedSearch),
region: strings.common.regions.normal[region],
}),
`\`\`\`${
players.length === 0
? strings.bot.commands.search_players.body.no_results
: players.map((player) => player.nickname).join("\n")
}\`\`\``
);
},
});
});
-102
View File
@@ -1,102 +0,0 @@
import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core";
import { Locale } from "discord.js";
import { AllStatsOverview } from "../components/AllStatsOverview";
import { CommandWrapper } from "../components/CommandWrapper";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { filtersToDescription } from "../core/blitzkit/filtersToDescription";
import { filterStats, type StatFilters } from "../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod";
import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import type { CommandRegistry } from "../events/interactionCreate";
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
const filterDescriptions = await filtersToDescription(filters, locale);
return (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{!stats.battles && <NoData type="battles_in_period" locale={locale} />}
{stats.battles > 0 && (
<AllStatsOverview
locale={locale}
stats={stats}
supplementaryStats={supplementary}
/>
)}
</CommandWrapper>
);
}
export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
command: await addPeriodicFilterOptions(
createLocalizedCommand("stats"),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(player.region, interaction);
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction),
// ...filters,
// });
// return Promise.all([
// render(player, period, filters, interaction.locale),
// buttonRefresh(interaction, path),
// getBlitzStarsLinkButton(player.region, player.id),
// ]);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters, interaction.locale);
},
});
});
-60
View File
@@ -1,60 +0,0 @@
import { gameDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { addFilterOptions } from "../core/discord/addFilterOptions";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { getFiltersFromButton } from "../core/discord/getFiltersFromButton";
import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton";
import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton";
import { translator } from "../core/localization/translator";
import { type CommandRegistry } from "../events/interactionCreate";
import { renderBreakdown } from "./breakdown/render";
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
const awaitedGameDefinitions = await gameDefinitions;
resolve({
command: addFilterOptions(
createLocalizedCommand("today"),
awaitedGameDefinitions.nations,
).addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
return strings.bot.commands.dead;
// const player = await resolvePlayerFromCommand(interaction);
// const period = resolvePeriodFromCommand(
// player.region,
// interaction,
// 'today',
// );
// const filters = await getFiltersFromCommand(interaction);
// const path = commandToURL(interaction, {
// ...player,
// ...getCustomPeriodParams(interaction, true),
// ...filters,
// });
// return [
// ...(await renderBreakdown(player, period, filters, interaction.locale)),
// buttonRefresh(interaction, path),
// await getBlitzStarsLinkButton(player.region, player.id),
// ];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters, interaction.locale);
},
});
});
@@ -1,50 +0,0 @@
import { type ComponentProps } from "react";
export interface GlowProps extends ComponentProps<"div"> {
color: string;
direction?: "default" | "reverse";
rotation?: number;
startOpacity?: number;
endOpacity?: number;
}
const width = 16;
export function Glow({
color,
direction = "default",
rotation = 0,
style,
startOpacity = 0,
endOpacity = 1,
...props
}: GlowProps) {
const color1 = `${color}${Math.round(endOpacity * 255)
.toString(16)
.padStart(2, "0")}`;
const color2 = `${color}${Math.round(startOpacity * 255)
.toString(16)
.padStart(2, "0")}`;
const background = `linear-gradient(${
direction === "default" ? 0 : 180
}deg, ${color1} 0%, ${color2} 100%)`;
return (
<div
style={{
display: "flex",
alignItems: direction === "default" ? "flex-end" : "flex-start",
justifyContent: "center",
transform: `rotate(${rotation}deg)`,
...style,
}}
{...props}
>
<div style={{ height: 54, width, background }} />
<div style={{ height: 72, width, background }} />
<div style={{ height: 80, width, background }} />
<div style={{ height: 72, width, background }} />
<div style={{ height: 54, width, background }} />
</div>
);
}
@@ -1,72 +0,0 @@
import { theme } from '../../../../stitches.config';
import { Glow } from './components/Glow';
export interface WN8DisplayProps {
subtitle: string;
color: string;
value: string | number;
}
export function HeroStat({ color, subtitle, value }: WN8DisplayProps) {
return (
<div
style={{
width: 128 * 2, // idk why twice the width turns into the correct width
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Glow color={color} style={{ marginBottom: -16 }} />
<div
style={{
width: 128,
height: 80,
backgroundColor: color,
borderRadius: 40,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
}}
>
<span
style={{
display: 'block',
fontWeight: 900,
fontSize: 32,
color: theme.colors.textHighContrast,
lineHeight: '26px',
letterSpacing: '-0.03em',
}}
>
{value}
</span>
<span
style={{
display: 'block',
fontSize: 16,
lineHeight: '16px',
color: theme.colors.textHighContrast,
}}
>
{subtitle}
</span>
</div>
</div>
<Glow color={color} direction="reverse" style={{ marginTop: -16 }} />
</div>
);
}
@@ -1,47 +0,0 @@
import { theme } from '../../../stitches.config';
export enum WN8SurroundingStatAlign {
Left,
Right,
}
export interface WN8SurroundingStatProps {
label: string;
value: string | number;
align?: WN8SurroundingStatAlign;
padded?: boolean;
}
export function WN8SurroundingStat({
label,
value,
align = WN8SurroundingStatAlign.Left,
padded = false,
}: WN8SurroundingStatProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems:
align === WN8SurroundingStatAlign.Left ? 'flex-start' : 'flex-end',
paddingLeft: padded && align === WN8SurroundingStatAlign.Left ? 32 : 0,
paddingRight:
padded && align === WN8SurroundingStatAlign.Right ? 32 : 0,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
fontWeight: 700,
}}
>
{value}
</span>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{label}
</span>
</div>
);
}
@@ -1,130 +0,0 @@
import {
getWN8Percentile,
Percentile,
type SupplementaryStats,
} from '@blitzkit/core';
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { PERCENTILE_COLORS } from '../PercentileIndicator/constants';
import { HeroStat } from './components/HeroStat';
import {
WN8SurroundingStat,
WN8SurroundingStatAlign,
} from './components/WN8SurroundingStat';
export interface AllStatsOverviewProps {
stats: {
battles: number;
wins: number;
damage_dealt: number;
damage_received: number;
survived_battles: number;
hits: number;
shots: number;
frags: number;
};
supplementaryStats: SupplementaryStats;
locale: Locale;
}
export function AllStatsOverview({
stats,
locale,
supplementaryStats,
}: AllStatsOverviewProps) {
const { strings } = translator(locale);
const percentile =
typeof supplementaryStats.WN8 === 'number'
? getWN8Percentile(supplementaryStats.WN8)
: Percentile.VeryBad;
const color = PERCENTILE_COLORS[percentile];
const heroStat =
typeof supplementaryStats.WN8 === 'number'
? supplementaryStats.WN8.toFixed(0)
: '--';
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: -64,
width: '100%',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
alignItems: 'flex-end',
flex: 1,
}}
>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.battles}
value={stats.battles.toLocaleString()}
align={WN8SurroundingStatAlign.Right}
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.winrate}
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.damage}
value={Math.round(
stats.damage_dealt / stats.battles,
).toLocaleString()}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.damage_ratio}
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
align={WN8SurroundingStatAlign.Right}
/>
</div>
<HeroStat
value={heroStat}
subtitle={strings.common.wn8_percentile[percentile]}
color={color}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
flex: 1,
}}
>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.tier}
value={
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
}
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.survival}
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
2,
)}%`}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.accuracy}
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
padded
/>
<WN8SurroundingStat
label={strings.bot.common.hero_stats.kills}
value={(stats.frags / stats.battles).toFixed(2)}
/>
</div>
</div>
);
}
@@ -1,16 +0,0 @@
import { type ReactNode } from "react";
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
className="session-tracker-root"
style={{ display: "flex", flexDirection: "column", gap: 8 }}
>
{children}
</div>
);
}
@@ -1,126 +0,0 @@
import {
Percentile,
TankClass,
TankType,
TREE_TYPE_ICONS,
} from "@blitzkit/core";
import { createColors } from "bepaint";
import {
type AccentColor,
type GrayColor,
PALETTES,
} from "../../../core/radix/radixColors";
import { RowStat } from "./RowStat";
export interface RowStatItem {
title: string;
current?: number | string;
career?: number | string;
delta?: number;
percentile?: Percentile;
}
interface RowProps {
displayType?: "tank" | "summary";
naked?: boolean;
title: string;
minimized?: boolean;
type?: TankType;
class?: TankClass;
color?: AccentColor | GrayColor;
stats: (RowStatItem | undefined)[];
}
export function Row({
color = "slate",
stats,
title,
minimized,
class: tankClass,
type,
naked = false,
displayType,
}: RowProps) {
const palette = PALETTES[`${color}Dark`];
const theme = createColors(palette);
return (
<div
className="session-tracker-row"
style={{
maxWidth: naked ? 600 : Infinity,
display: "flex",
flexDirection: "column",
borderRadius: 4,
backgroundColor: naked
? `${theme.appBackground2}c0`
: theme.appBackground2,
overflow: "hidden",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 8,
gap: 4,
backgroundColor: naked
? `${theme.componentInteractive}c0`
: theme.componentInteractive,
}}
>
{displayType === "tank" && tankClass !== undefined && (
<img
alt={TankClass[tankClass]}
src={
TREE_TYPE_ICONS[type ?? TankType.TANK_TYPE_RESEARCHABLE][
tankClass
]
}
style={{ width: 16, height: 16 }}
/>
)}
<span
style={{
color:
displayType === "tank"
? type === TankType.TANK_TYPE_COLLECTOR
? theme.textLowContrast_blue
: type === TankType.TANK_TYPE_PREMIUM
? theme.textLowContrast_amber
: theme.textHighContrast
: theme.textLowContrast,
fontWeight: 900,
fontSize: 16,
}}
>
{title}
</span>
</div>
<div
style={{
display: "flex",
paddingTop: 8,
paddingBottom: 8,
}}
>
{stats.map(
(row, index) =>
row && (
<RowStat
minimized={minimized}
key={index}
name={`${row.title} • ${row.career ?? "--"}`}
value={row.current ?? "--"}
delta={row.delta}
percentile={row.percentile}
/>
),
)}
</div>
</div>
);
}
@@ -1,89 +0,0 @@
import { Percentile } from "@blitzkit/core";
import { createColors } from "bepaint";
import {
type AccentColor,
type GrayColor,
PALETTES,
} from "../../../core/radix/radixColors";
import { PercentileIndicator } from "../../PercentileIndicator";
interface RowStatProps {
name: string;
value?: string | number;
delta?: number;
percentile?: Percentile;
minimized?: boolean;
color?: AccentColor | GrayColor;
}
export function RowStat({
name,
value,
delta,
percentile,
minimized = false,
color = "slate",
}: RowStatProps) {
const theme = createColors(PALETTES[`${color}Dark`]);
return (
<div
className="session-tracker-row-stat"
style={{
display: "flex",
flexDirection: "column",
gap: 4,
alignItems: "center",
justifyContent: "center",
flex: 1,
}}
>
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
justifyContent: "center",
}}
>
{percentile === undefined && delta !== undefined && delta !== 0 && (
<img
alt={delta > 0 ? "Increase" : "Decrease"}
src={
(delta ?? 0) > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
style={{ width: 12, height: 12 }}
/>
)}
{percentile !== undefined && (
<PercentileIndicator percentile={percentile} />
)}
<span
style={{
color: theme.textHighContrast,
fontWeight: 700,
fontSize: 16,
textAlign: "center",
}}
>
{value}
</span>
</div>
{!minimized && (
<span
style={{
color: theme.textLowContrast,
textAlign: "center",
fontSize: 12,
}}
>
{name}
</span>
)}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Root';
export * from './components/Row';
export * from './components/RowStat';
@@ -1,26 +0,0 @@
import { type ReactNode } from "react";
import { theme } from "../stitches.config";
interface CommandWrapperProps {
children: ReactNode;
fat?: boolean;
}
export function CommandWrapper({ children, fat }: CommandWrapperProps) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
padding: `16px ${fat ? 0 : 16}px`,
gap: 16,
width: "100%",
color: theme.colors.textHighContrast,
backgroundColor: theme.colors.appBackground1,
position: "relative",
}}
>
{children}
</div>
);
}
@@ -1,21 +0,0 @@
interface DeltaCaretProps {
delta: number;
}
export function DeltaCaret({ delta }: DeltaCaretProps) {
return (
<img
alt={delta > 0 ? 'Increase' : 'Decrease'}
style={{
width: 16,
height: 16,
objectFit: 'contain',
}}
src={
delta > 0
? 'https://i.imgur.com/gjI4a5f.png'
: 'https://i.imgur.com/lLGZygN.png'
}
/>
);
}
-19
View File
@@ -1,19 +0,0 @@
import { asset } from '@blitzkit/core';
interface FlagProps {
nation: string;
}
export function Flag({ nation }: FlagProps) {
return (
<div
style={{
width: 22,
height: 22,
overflow: 'hidden',
}}
>
<img alt={nation} src={asset(`flags/circle/${nation}.webp`)} />
</div>
);
}
@@ -1,95 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { theme } from '../../../stitches.config';
import { PercentileIndicator } from '../../PercentileIndicator';
interface CardItem {
label: string;
value: string | number | undefined;
percentile?: Percentile;
}
interface CardProps {
items: CardItem[];
title: string;
}
export function Card({ items, title }: CardProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
flex: 1,
borderRadius: 4,
backgroundColor: theme.colors.componentInteractive,
padding: 8,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
}}
>
{title}
</span>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
{items.map((item) => (
<div
key={item.label}
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{item.label}
</span>
<div
style={{
flex: 1,
height: 1,
borderRadius: 0.5,
backgroundColor: theme.colors.borderNonInteractive,
}}
/>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
{item.percentile !== undefined && (
<PercentileIndicator percentile={item.percentile} />
)}
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{item.value === undefined || Number.isNaN(item.value)
? '--'
: item.value}
</span>
</div>
</div>
))}
</div>
</div>
);
}
@@ -1,17 +0,0 @@
interface RootProps {
children: React.ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
{children}
</div>
);
}
@@ -1,7 +0,0 @@
interface RowProps {
children: React.ReactNode;
}
export function Row({ children }: RowProps) {
return <div style={{ display: 'flex', gap: 8 }}>{children}</div>;
}
@@ -1,149 +0,0 @@
import {
type BlitzStats,
getWN8Percentile,
isNumber,
type SupplementaryStats,
} from '@blitzkit/core';
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { Card } from './components/Card';
import { Root } from './components/Root';
import { Row } from './components/Row';
interface GenericAllStatsProps {
stats: BlitzStats;
supplementaryStats?: SupplementaryStats;
locale: Locale;
}
export function GenericAllStats({
stats,
supplementaryStats,
locale,
}: GenericAllStatsProps) {
const { strings } = translator(locale);
const damageRatio = stats.damage_dealt / stats.damage_received;
const killsToDeathRatio =
stats.frags / (stats.battles - stats.survived_battles);
return (
<Root>
<Row>
<Card
title={strings.bot.commands.full_stats.body.cards.games.title}
items={[
{
label: strings.bot.commands.full_stats.body.cards.games.winrate,
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
},
{
label: strings.bot.commands.full_stats.body.cards.games.battles,
value: stats.battles,
},
{
label:
strings.bot.commands.full_stats.body.cards.games.wins_losses,
value: `${stats.wins} / ${stats.losses}`,
},
]}
/>
<Card
title={strings.bot.commands.full_stats.body.cards.efficiency.title}
items={[
{
label: strings.bot.commands.full_stats.body.cards.efficiency.wn8,
value:
supplementaryStats && isNumber(supplementaryStats.WN8)
? supplementaryStats.WN8!.toFixed(0)
: undefined,
percentile: isNumber(supplementaryStats?.WN8)
? getWN8Percentile(supplementaryStats!.WN8!)
: undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.efficiency.survival,
value: `${(
100 *
(stats.survived_battles / stats.battles)
).toFixed(2)}%`,
},
{
label:
strings.bot.commands.full_stats.body.cards.efficiency.accuracy,
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
},
]}
/>
</Row>
<Row>
<Card
title={strings.bot.commands.full_stats.body.cards.lethality.title}
items={[
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.average_damage,
value: (stats.damage_dealt / stats.battles).toFixed(0),
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.damage_per_tier,
value: supplementaryStats?.tier
? (
stats.damage_dealt /
stats.battles /
supplementaryStats.tier
).toFixed(0)
: undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.damage_ratio,
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
},
{
label:
strings.bot.commands.full_stats.body.cards.lethality
.average_kills,
value: (stats.frags / stats.battles).toFixed(0),
},
]}
/>
<Card
title={strings.bot.commands.full_stats.body.cards.miscellaneous.title}
items={[
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_tier,
value: supplementaryStats?.tier?.toFixed(2),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_spots,
value: (stats.spotted / stats.battles).toFixed(2),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.average_xp,
value: (stats.xp / stats.battles).toFixed(0),
},
{
label:
strings.bot.commands.full_stats.body.cards.miscellaneous
.kills_to_death_ratio,
value: isFinite(killsToDeathRatio)
? killsToDeathRatio.toFixed(2)
: undefined,
},
]}
/>
</Row>
</Root>
);
}
@@ -1,16 +0,0 @@
import { type GenericStat } from "..";
import { GenericStatsRow } from "./GenericStatsRow";
interface GenericStatsColumnProps {
stats: GenericStat[];
}
export function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
return (
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 8 }}>
{stats.map((stat) => (
<GenericStatsRow key={stat[0]} stat={stat} />
))}
</div>
);
}
@@ -1,46 +0,0 @@
import { type GenericStat } from "..";
import { theme } from "../../../stitches.config";
import { PercentileIndicator } from "../../PercentileIndicator";
interface GenericStatsRowProps {
stat: GenericStat;
}
export function GenericStatsRow({ stat }: GenericStatsRowProps) {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
whiteSpace: "nowrap",
maxWidth: 128,
overflow: "hidden",
}}
>
{stat[0]}
</span>
<span
style={{
color: theme.colors.textHighContrast,
fontWeight: 700,
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
{stat[2] !== undefined && <PercentileIndicator percentile={stat[2]} />}
{stat[1]}
</span>
</div>
);
}
@@ -1,34 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { GenericStatsColumn } from './components/GenericStatsColumn';
export type GenericStat =
| [string, string | number | undefined]
| [string, string | number | undefined, Percentile | undefined];
export interface GenericStatsProps {
stats: GenericStat[];
columnCount?: number;
}
export function GenericStats({ stats, columnCount = 2 }: GenericStatsProps) {
const filteredStats = stats.filter(([, value]) => value !== undefined);
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
const columns: GenericStat[][] = [];
filteredStats.forEach((stat, index) => {
const column = Math.floor(index / itemsPerRow);
if (!columns[column]) columns[column] = [];
columns[column].push(stat);
});
return (
<div style={{ display: 'flex', gap: 32 }}>
{columns.map((stats) => (
<GenericStatsColumn
key={stats.map((stat) => stat[0]).join('-')}
stats={stats}
/>
))}
</div>
);
}
@@ -1,11 +0,0 @@
export enum LineColor {
Red,
Green,
Blue,
White,
}
export enum LineStyle {
Solid,
Dashes,
DashesAndDots,
}
@@ -1,65 +0,0 @@
import { blue, green, red, slateDark } from '@radix-ui/colors';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
import { LineColor, LineStyle } from './constants';
const LINE_COLORS = {
[LineColor.Red]: red.red9,
[LineColor.Green]: green.green9,
[LineColor.Blue]: blue.blue9,
[LineColor.White]: slateDark.slate11,
};
const LINE_STYLES = {
[LineStyle.Solid]: undefined,
[LineStyle.Dashes]: '8, 8',
[LineStyle.DashesAndDots]: '1, 4, 8, 4',
};
export type PlotItem = [number, number];
type Plot = PlotItem[];
interface LineProps {
plot: Plot;
minY?: number;
maxY?: number;
color?: LineColor;
style?: LineStyle;
lineWidth?: number;
}
export function Line({
plot,
minY: providedMinY,
maxY: providedMaxY,
color = LineColor.Red,
style = LineStyle.Solid,
lineWidth = 2,
}: LineProps) {
const xs = plot.map(([x]) => x);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const ys = plot.map(([, y]) => y);
const minY = providedMinY ?? Math.min(...ys);
const maxY = providedMaxY ?? Math.max(...ys);
const rangeX = maxX - minX;
const rangeY = maxY - minY;
const points = plot
.map(
([x, y]) =>
`${(x - minX) * (GRAPH_WIDTH / rangeX)},${
GRAPH_HEIGHT - (y - minY) * (GRAPH_HEIGHT / rangeY)
}`,
)
.join(' ');
return (
<polyline
vector-effect="non-scaling-stroke"
fill="none"
stroke={LINE_COLORS[color]}
strokeWidth={lineWidth}
stroke-dasharray={LINE_STYLES[style]}
points={points}
/>
);
}
@@ -1,4 +0,0 @@
export enum MarginOrientation {
Vertical,
Horizontal,
}
@@ -1,49 +0,0 @@
import { times } from 'lodash-es';
import { theme } from '../../../../stitches.config';
import { type MarginInputProps } from '../Root';
import { MarginOrientation } from './constants';
interface MarginProps extends MarginInputProps {
verticalSeparations: number;
orientation?: MarginOrientation;
}
export function Margin({
verticalSeparations,
max,
min,
suffix,
precision = 0,
orientation = MarginOrientation.Vertical,
}: MarginProps) {
const isVertical = orientation === MarginOrientation.Vertical;
return (
<div
style={{
paddingLeft: isVertical ? 0 : 64,
width: isVertical ? 48 : '100%',
display: 'flex',
flexDirection: isVertical ? 'column' : 'row-reverse',
justifyContent: 'space-between',
}}
>
{times(verticalSeparations, (index) => (
<span
key={index}
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{`${(
(max - min) *
((verticalSeparations - (index + 1)) /
(verticalSeparations - 1)) +
min
).toFixed(precision)}${suffix ?? ''}`}
</span>
))}
</div>
);
}
@@ -1,2 +0,0 @@
export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320;
@@ -1,109 +0,0 @@
import { times } from "lodash-es";
import { type ReactNode } from "react";
import { theme } from "../../../../stitches.config";
import { Margin } from "../Margin";
import { MarginOrientation } from "../Margin/constants";
import { GRAPH_HEIGHT, GRAPH_WIDTH } from "./constants";
export interface MarginInputProps {
min: number;
max: number;
suffix?: string;
precision?: number;
}
interface RootProps {
children: ReactNode;
verticalMargin?: MarginInputProps;
horizontalMargin?: MarginInputProps;
separations?: number;
xMinLabel: string;
xMaxLabel: string;
xTitle?: string;
}
export function Root({
children,
verticalMargin,
horizontalMargin,
separations = 5,
xMinLabel,
xMaxLabel,
xTitle,
}: RootProps) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div
style={{
paddingLeft: 64,
display: "flex",
justifyContent: "space-between",
}}
>
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
{xMinLabel}
</span>
<span style={{ fontSize: 16, color: theme.colors.textHighContrast }}>
{xMaxLabel}
</span>
</div>
<div style={{ display: "flex", gap: 16 }}>
{verticalMargin && (
<Margin {...verticalMargin} verticalSeparations={separations} />
)}
<svg
viewBox={`0 0 ${GRAPH_WIDTH} ${GRAPH_HEIGHT}`}
preserveAspectRatio="none"
style={{
flex: 1,
backgroundColor: theme.colors.appBackground2,
borderRadius: 4,
border: theme.borderStyles.nonInteractive,
}}
>
{times(separations - 2, (index) => {
const y = GRAPH_HEIGHT * ((index + 1) / (separations - 1));
return (
<polyline
key={index}
vector-effect="non-scaling-stroke"
fill="none"
stroke={theme.colors.borderNonInteractive}
strokeWidth="2"
points={`0,${y}, ${GRAPH_WIDTH},${y}`}
/>
);
})}
{children}
</svg>
</div>
{horizontalMargin && (
<Margin
{...horizontalMargin}
verticalSeparations={separations}
orientation={MarginOrientation.Horizontal}
/>
)}
{xTitle !== undefined && (
<div
style={{
paddingLeft: 64,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{xTitle}
</span>
</div>
)}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Line';
export * from './components/Margin';
export * from './components/Root';
@@ -1,29 +0,0 @@
import { theme } from '../../../stitches.config';
interface GapProps {
message: string;
}
export function Gap({ message: number }: GapProps) {
return (
<div
style={{
display: 'flex',
padding: 8,
backgroundColor: theme.colors.componentNonInteractive,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{number.toLocaleString()}
</span>
</div>
);
}
@@ -1,171 +0,0 @@
import { type ComponentProps } from "react";
import { theme } from "../../../stitches.config";
interface ItemProps extends ComponentProps<"div"> {
position: number;
deltaPosition?: number;
score?: number;
deltaScore?: number;
nickname: string;
clan?: string;
highlight?: boolean;
}
export function Item({
position,
deltaPosition,
score,
deltaScore,
nickname,
clan,
highlight,
...props
}: ItemProps) {
return (
<div
{...props}
style={{
cursor: "pointer",
display: "flex",
padding: 8,
backgroundColor: highlight
? theme.colors.componentInteractive_blue
: theme.colors.componentInteractive,
borderRadius: 4,
alignItems: "center",
justifyContent: "center",
gap: 8,
}}
>
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{position.toLocaleString()}.
</span>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{nickname}
</span>
{clan && (
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
[{clan}]
</span>
)}
</div>
{deltaPosition !== undefined && deltaPosition !== 0 && (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 4,
}}
>
<img
alt={deltaPosition > 0 ? "Increase" : "Decrease"}
style={{
width: 8,
height: 8,
}}
src={
deltaPosition > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
/>
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{Math.abs(deltaPosition).toLocaleString()}
</span>
</div>
)}
{/* TODO: lol remove this spacer div for a better solution */}
<div style={{ flex: 1 }} />
<div
style={{
display: "flex",
gap: 4,
alignItems: "center",
width: 96,
justifyContent: "flex-end",
}}
>
{deltaScore !== 0 && deltaScore !== undefined && (
<span
style={{
color: highlight
? theme.colors.textLowContrast_blue
: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{Math.abs(deltaScore).toLocaleString()}
</span>
)}
{deltaScore !== 0 && deltaScore !== undefined && (
<img
alt={deltaScore > 0 ? "Increase" : "Decrease"}
style={{
width: 8,
height: 8,
}}
src={
deltaScore > 0
? "https://i.imgur.com/qbjiXa1.png"
: "https://i.imgur.com/3uyNhun.png"
}
/>
)}
{score !== undefined && (
<span
style={{
color: highlight
? theme.colors.textHighContrast_blue
: theme.colors.textHighContrast,
fontSize: 16,
}}
>
{score.toLocaleString()}
</span>
)}
</div>
</div>
);
}
@@ -1,13 +0,0 @@
import { type ReactNode } from 'react';
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{children}
</div>
);
}
@@ -1,3 +0,0 @@
export * from './components/Gap';
export * from './components/Item';
export * from './components/Root';
@@ -1,5 +0,0 @@
export enum NoDataType {
BattlesInPeriod,
PlayersInPeriod,
TanksFound,
}
@@ -1,30 +0,0 @@
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { theme } from '../../stitches.config';
interface NoDataProps {
type:
| 'battles_in_period'
| 'players_in_period'
| 'tanks_found'
| 'battles_in_season';
locale: Locale;
}
export function NoData({ type, locale }: NoDataProps) {
const { strings } = translator(locale);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{strings.bot.common.no_data[type]}
</span>
</div>
);
}
@@ -1,26 +0,0 @@
import { Percentile } from '@blitzkit/core';
import {
amber,
blue,
green,
lime,
orange,
pink,
purple,
red,
slate,
teal,
} from '@radix-ui/colors';
export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9,
[Percentile.Bad]: red.red9,
[Percentile.BelowAverage]: orange.orange9,
[Percentile.Average]: amber.amber10,
[Percentile.AboveAverage]: lime.lime8,
[Percentile.Good]: green.green9,
[Percentile.VeryGood]: teal.teal9,
[Percentile.Great]: blue.blue9,
[Percentile.Unicum]: pink.pink9,
[Percentile.SuperUnicum]: purple.purple9,
};
@@ -1,19 +0,0 @@
import { Percentile } from '@blitzkit/core';
import { PERCENTILE_COLORS } from './constants';
interface PercentileIndicatorProps {
percentile: Percentile;
}
export function PercentileIndicator({ percentile }: PercentileIndicatorProps) {
return (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: PERCENTILE_COLORS[percentile],
}}
/>
);
}
@@ -1,20 +0,0 @@
import { type ReactNode } from 'react';
interface ColumnProps {
children: ReactNode;
}
export function Column({ children }: ColumnProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
flex: 1,
}}
>
{children}
</div>
);
}
@@ -1,75 +0,0 @@
import { TankClass, TankType, TREE_TYPE_ICONS } from "@blitzkit/core";
import { theme } from "../../../../stitches.config";
export interface ItemProps {
tankClass?: TankClass;
name: string;
tankType: TankType;
}
const TREE_TYPE_COLOR = {
[TankType.TANK_TYPE_RESEARCHABLE]: "",
[TankType.TANK_TYPE_PREMIUM]: "_amber",
[TankType.TANK_TYPE_COLLECTOR]: "_blue",
} as const;
export function Item({
tankClass: tankType,
name,
tankType: treeType,
}: ItemProps) {
return (
<div
style={{
display: "flex",
height: 32,
paddingLeft: 8,
paddingRight: 8,
overflow: "hidden",
alignItems: "center",
borderRadius: 4,
backgroundColor:
theme.colors[`componentInteractive${TREE_TYPE_COLOR[treeType]}`],
}}
>
<div
style={{
display: "flex",
gap: 4,
flex: 1,
alignItems: "center",
justifyContent: "flex-start",
overflow: "hidden",
}}
>
{tankType && (
<img
alt={TankType[tankType]}
src={TREE_TYPE_ICONS[treeType][tankType]}
style={{ width: 14, height: 14 }}
/>
)}
<span
style={{
fontSize: 16,
whiteSpace: "nowrap",
color: theme.colors[`textLowContrast${TREE_TYPE_COLOR[treeType]}`],
}}
>
{name}
</span>
</div>
{/* {image && (
// TODO: remove hardcoded dimensions when satori fixes non-width unloadable images
<img
alt={name}
src={image}
width={106}
height={32}
style={{ objectFit: 'cover' }}
/>
)} */}
</div>
);
}
@@ -1,19 +0,0 @@
import { type ReactNode } from "react";
interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 16,
}}
>
{children}
</div>
);
}
@@ -1,18 +0,0 @@
import { type ReactNode } from "react";
interface RowProps {
children: ReactNode;
}
export function Row({ children }: RowProps) {
return (
<div
style={{
display: "flex",
gap: 8,
}}
>
{children}
</div>
);
}
@@ -1,19 +0,0 @@
import { theme } from '../../../stitches.config';
interface TitleProps {
children: string;
}
export function Title({ children }: TitleProps) {
return (
<span
style={{
fontWeight: 900,
color: theme.colors.textHighContrast,
fontSize: 24,
}}
>
{children}
</span>
);
}
@@ -1,5 +0,0 @@
export * from './components/Column';
export * from './components/Item';
export * from './components/Root';
export * from './components/Row';
export * from './components/Title';
@@ -1,32 +0,0 @@
import type { TierWeightsRecord } from "@blitzkit/core";
import { theme } from "../stitches.config";
interface TierWeightsProps {
weights: TierWeightsRecord;
}
export function TierWeights({ weights }: TierWeightsProps) {
return (
<div style={{ display: "flex", gap: 8 }}>
{Object.entries(weights).map(([tier, weight]) => (
<div
key={tier}
style={{
backgroundColor: theme.colors.appBackground1_light,
flex: weight,
color: theme.colors.textHighContrast_light,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 16,
overflow: "hidden",
borderRadius: 8,
minWidth: 16,
}}
>
{tier}
</div>
))}
</div>
);
}
-54
View File
@@ -1,54 +0,0 @@
import { theme } from '../stitches.config';
interface TitleBarProps {
title: string;
image?: string;
description?: string;
}
export function TitleBar({ title, image, description }: TitleBarProps) {
return (
<div
style={{
gap: 4,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: '8px 0px',
}}
>
<div
style={{
gap: 4,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{image && (
<img
alt={title}
style={{ width: 32, height: 32, objectFit: 'contain' }}
src={image}
/>
)}
<span
style={{
fontSize: 32,
fontWeight: 900,
background: `linear-gradient(180deg, ${theme.colors.textHighContrast} 0%, ${theme.colors.textLowContrast} 100%)`,
backgroundClip: 'text',
}}
>
{title}
</span>
</div>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{description}
</span>
</div>
);
}
@@ -1,3 +0,0 @@
export function normalizeLeagueIcon(url: string) {
return url.startsWith('http') ? url : `https:${url}`;
}
@@ -1,48 +0,0 @@
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
) {
const { strings } = translator(locale);
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 }
);
if (searchResult.length === 0) {
throw new UserError(
literals(strings.bot.common.errors.tank_name_not_found, {
search: markdownEscape(`${tank}`),
})
);
} else {
return searchResult[0].obj.id;
}
} else {
const awaitedTankDefinitions = await tankDefinitions;
if (awaitedTankDefinitions.tanks[number]) {
return number;
} else {
throw new UserError(
literals(strings.bot.common.errors.tank_id_not_found, {
search: number,
})
);
}
}
}
@@ -1,23 +0,0 @@
import { searchClansAcrossRegions } from '@blitzkit/core';
import { Locale } from 'discord.js';
import { UserError } from '../blitzkit/userError';
import { translator } from '../localization/translator';
export async function searchClansAcrossRegionsBotWrapper(
search: string,
locale: Locale,
limit?: number,
) {
const { strings } = translator(locale);
const trimmed = search.trim();
if (trimmed.length < 3) {
throw new UserError(strings.bot.common.errors.query_too_small);
}
if (trimmed.length > 100) {
throw new UserError(strings.bot.common.errors.query_too_long);
}
return await searchClansAcrossRegions(trimmed, limit);
}
@@ -1,23 +0,0 @@
import { searchPlayersAcrossRegions } from '@blitzkit/core';
import { Locale } from 'discord.js';
import { UserError } from '../blitzkit/userError';
import { translator } from '../localization/translator';
export async function searchPlayersAcrossRegionsBotWrapper(
search: string,
locale: Locale,
limit?: number,
) {
const { strings } = translator(locale);
const trimmed = search.trim();
if (trimmed.length < 3) {
throw new UserError(strings.bot.common.errors.query_too_small);
}
if (trimmed.length > 100) {
throw new UserError(strings.bot.common.errors.query_too_long);
}
return await searchPlayersAcrossRegions(trimmed, limit);
}
@@ -1,20 +0,0 @@
import { fetchTankDefinitions } from '@blitzkit/core';
import { resolveAncestry } from './resolveAncestry';
export async function buildTechTreeLine(start: number, end: number) {
const tankDefinitions = await fetchTankDefinitions();
const endTank = tankDefinitions.tanks[end];
if (!endTank.ancestors) throw new Error('End tank has no ancestors');
const line = [end];
if (start === end) return line;
for (const ancestorId of endTank.ancestors) {
if ((await resolveAncestry(ancestorId)).includes(start)) {
const ancestorTree = await buildTechTreeLine(start, ancestorId);
line.push(...ancestorTree);
break;
}
}
return line;
}
@@ -1,30 +0,0 @@
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { type StatFilters } from "../blitzstars/filterStats";
import { translator } from "../localization/translator";
export async function filtersToDescription(
{ nation, tier, type, class: tankClass, tank }: StatFilters,
locale: Locale,
) {
const { strings, bkLocale } = translator(locale);
const awaitedTankDefinitions = await fetchTankDefinitions();
const info: string[] = [];
// TODO: somehow localize tank names?
if (tank) return awaitedTankDefinitions.tanks[tank].name!.locales[bkLocale];
if (nation)
info.push((strings.common.nations as Record<string, string>)[nation]);
if (tier) {
info.push(
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(", ");
}
@@ -1,16 +0,0 @@
import { idToRegion } from '@blitzkit/core';
import { flagUserActivity } from '../db/flagUserActivity';
import { usersDatabase } from '../db/users';
export async function getBlitzFromDiscord(discordId: bigint) {
const unique = await usersDatabase.discordUser.findUnique({
where: { discord_id: discordId },
});
if (unique === null) return null;
const id = Number(unique.blitz_id);
await flagUserActivity(id);
return { id, region: idToRegion(id) };
}
@@ -1,3 +0,0 @@
export function getPeriodNow() {
return Date.now() / 1000;
}
@@ -1,8 +0,0 @@
import { getTimeDaysAgo, type Region } from '@blitzkit/core';
import type { PeriodSize } from '../discord/addPeriodSubCommands';
export function getPeriodStart(region: Region, period: PeriodSize) {
if (period === 'career') return 0;
if (period === 'today') return getTimeDaysAgo(region, 1);
return getTimeDaysAgo(region, parseInt(period));
}
-16
View File
@@ -1,16 +0,0 @@
import sharp from "sharp";
const cache: Record<string, Buffer> = {};
export async function iconPng(url: string) {
if (!url.endsWith(".webp")) return url;
if (!cache[url]) {
const imageResponse = await fetch(url);
const imageBufferWEBP = await imageResponse.arrayBuffer();
const imageBufferPNG = await sharp(imageBufferWEBP).png().toBuffer();
cache[url] = imageBufferPNG;
}
return `data:image/png;base64,${cache[url].toString("base64")}`;
}
@@ -1,9 +0,0 @@
import { jsxToSvg } from "./jsxToSvg";
import { svgToPng } from "./svgToPng";
export async function jsxToPng(jsx: React.JSX.Element) {
const svg = await jsxToSvg(jsx);
const png = svgToPng(svg);
return png;
}
@@ -1,12 +0,0 @@
import { jsxToSvg } from "./jsxToSvg";
import { RenderConfiguration } from "./renderConfiguration";
import { svgToPngThreaded } from "./svgToPngThreaded";
export async function jsxToPngThreaded(
jsx: React.JSX.Element,
renderConfiguration: RenderConfiguration
) {
const svg = await jsxToSvg(jsx, renderConfiguration);
const png = await svgToPngThreaded(svg);
return png;
}
Binary file not shown.
@@ -1,23 +0,0 @@
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 },
{ file: robotoBold, weight: 700 as const },
{ file: robotoBlack, weight: 900 as const },
].map(async ({ file, weight }) => {
const data = await readFile(`${__dirname}/${file}`);
return { name: "Roboto", weight, data };
});
export async function jsxToSvg(
jsx: React.JSX.Element,
renderConfiguration = new RenderConfiguration()
) {
const fonts = await Promise.all(fontsPromise);
return await satori(jsx, { width: renderConfiguration.width, fonts });
}
@@ -1,15 +0,0 @@
import {
fetchAverageDefinitions,
fetchDiscoveredIdsDefinitions,
fetchGameDefinitions,
fetchMapDefinitions,
fetchTankDefinitions,
fetchTankNames,
} from "@blitzkit/core";
export const averageDefinitions = fetchAverageDefinitions();
export const tankDefinitions = fetchTankDefinitions();
export const gameDefinitions = fetchGameDefinitions();
export const tankNames = fetchTankNames();
export const mapDefinitions = fetchMapDefinitions();
export const discoveredIdsDefinitions = fetchDiscoveredIdsDefinitions();
@@ -1,37 +0,0 @@
import { usersDatabase } from "../db/users";
import { client } from "../discord/client";
const EXIT_EVENTS = ["SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2"];
let cleaningUp = false;
async function cleanup(signal: string) {
if (cleaningUp) return;
cleaningUp = true;
console.log(`Cleaning up... (${signal})`);
try {
await Promise.all([usersDatabase.$disconnect(), client.destroy()]);
} catch (err) {
console.error("Cleanup error:", err);
}
console.log("Gracefully exiting...");
process.exit(0);
}
export function registerProcesses() {
process.on("uncaughtException", (err) => {
console.error("uncaughtException:", err);
});
process.on("unhandledRejection", (err) => {
console.error("unhandledRejection:", err);
});
client.on("error", console.error);
EXIT_EVENTS.forEach((event) => process.on(event, () => cleanup(event)));
}
Loaded 100 of 762 files, more files were not shown because too many files have changed in this diff. Show more