Compare commits

..
121 Commits
Author SHA1 Message Date
TrèsAbhi c54ceb944e Merge pull request #94 from tresabhi/1.16.3 2024-03-12 22:43:51 -05:00
TrèsAbhi 37a2ba3566 feat: docs 2024-03-12 18:07:32 -05:00
TrèsAbhi caff7bb6d0 feat: improved vertical stablizer model compat 2024-03-12 18:00:00 -05:00
TrèsAbhi 01c491aee0 feat: 1 more decimal in pitch and yaw 2024-03-12 17:40:58 -05:00
TrèsAbhi fd408d6777 fix: use initial gun pitches correctly 2024-03-12 17:39:46 -05:00
TrèsAbhi 6d1ed1e56e fix: reveals a few hidden values 2024-03-12 17:09:21 -05:00
TrèsAbhi 46f9d605f7 fix: intra clip spelling 2024-03-12 16:53:42 -05:00
TrèsAbhi a65ea42b81 Update cd-ratings-na.yml 2024-03-12 13:22:03 -05:00
TrèsAbhi 40f8c6a915 Update cd-ratings-eu.yml 2024-03-12 13:21:19 -05:00
TrèsAbhi 4678359f36 Update cd-ratings-asia.yml 2024-03-12 13:20:58 -05:00
TrèsAbhi f91bd63646 whooped 2024-03-10 12:50:25 -05:00
TrèsAbhi c1872262f5 huh 2024-03-10 12:29:04 -05:00
TrèsAbhi 082e112758 Update constants.ts 2024-03-10 10:54:27 -05:00
TrèsAbhi 4cb7d50eb5 Update cd-assets.yml 2024-03-10 10:53:34 -05:00
TrèsAbhi 8c050add23 my butt 2024-03-09 23:33:14 -06:00
TrèsAbhi 2610697d86 wee ooo wee oo 2024-03-09 23:04:09 -06:00
TrèsAbhi 64f1de37ad help me 2024-03-09 22:50:55 -06:00
TrèsAbhi 58f6862279 aaaaaaaaaaaaaaaaaaaaaa 2024-03-09 22:46:21 -06:00
TrèsAbhi b47f6fdb32 i dunno xd 2024-03-09 22:42:03 -06:00
TrèsAbhi ce5a53d0ff i dunno 2024-03-09 22:39:04 -06:00
TrèsAbhi 6ee1fa13d3 fix: use require 2024-03-09 22:15:22 -06:00
TrèsAbhi 196f780bb3 Merge pull request #93 from tresabhi/1.16.2 2024-03-09 21:54:58 -06:00
TrèsAbhi 088d5a2941 Merge pull request #92 from tresabhi/l10n_1.16.2 2024-03-09 21:22:36 -06:00
TrèsAbhi 47abdaed6f feat: changelogs 2024-03-09 20:56:22 -06:00
TrèsAbhi 30f6055a69 New translations en-us.json (Croatian (disambiguation)) 2024-03-09 20:51:33 -06:00
TrèsAbhi 9d4b750dfd New translations en-us.json (Spanish) 2024-03-09 14:55:24 -06:00
TrèsAbhi d1308bc625 New translations en-us.json (Spanish) 2024-03-09 13:37:08 -06:00
TrèsAbhi db40256bb4 New translations en-us.json (Croatian (disambiguation)) 2024-03-09 12:10:50 -06:00
TrèsAbhi 70f1ebadfc New translations en-us.json (Spanish) 2024-03-09 12:10:49 -06:00
TrèsAbhi 43adaf85f5 fix: typos 2024-03-09 12:10:44 -06:00
TrèsAbhi 8d5e78a510 feat: button translations 2024-03-09 12:05:28 -06:00
TrèsAbhi 9d6d893373 feat: removed preview commands 2024-03-09 12:02:17 -06:00
TrèsAbhi 9840982aa2 feat: more strings 2024-03-09 12:01:41 -06:00
TrèsAbhi 8e216ebe46 fix: tighter numerical tank name inference 2024-03-09 11:50:47 -06:00
TrèsAbhi 889ae39e3b feat: user error strings 2024-03-09 11:48:35 -06:00
TrèsAbhi 265eff98bf fix: change name to rad 2024-03-09 11:31:45 -06:00
TrèsAbhi ef7d1dcfb3 Merge pull request #91 from tresabhi/l10n_1.16.2 2024-03-09 11:30:44 -06:00
TrèsAbhi 607ebc0025 New translations en-us.json (Croatian (disambiguation)) 2024-03-09 10:47:33 -06:00
TrèsAbhi 0f0bc9569d New translations en-us.json (Spanish) 2024-03-09 10:47:32 -06:00
TrèsAbhi 169e892678 New translations en-us.json (Spanish) 2024-03-09 09:37:16 -06:00
TrèsAbhi fe249e2704 New translations en-us.json (Croatian (disambiguation)) 2024-03-09 07:21:28 -06:00
TrèsAbhi b8be5d5e38 New translations en-us.json (Spanish) 2024-03-09 05:11:15 -06:00
TrèsAbhi 77d6c93b90 New translations en-us.json (Croatian (disambiguation)) 2024-03-08 20:43:31 -06:00
TrèsAbhi 4fedaf1cea feat: removed search tanks 2024-03-08 20:43:30 -06:00
TrèsAbhi b2220f885e New translations en-us.json (Spanish) 2024-03-08 20:43:30 -06:00
TrèsAbhi 3181f9c4e7 feat: removed player acheivements 2024-03-08 20:32:06 -06:00
TrèsAbhi 153d0d326d New translations en-us.json (Croatian (disambiguation)) 2024-03-08 20:29:58 -06:00
TrèsAbhi 7f9b349de6 New translations en-us.json (Spanish) 2024-03-08 20:29:58 -06:00
TrèsAbhi b3a50f2b27 feat: more command locales 2024-03-08 20:29:35 -06:00
TrèsAbhi fb49628375 New translations en-us.json (Croatian (disambiguation)) 2024-03-08 10:57:50 -06:00
TrèsAbhi 394a74c3a0 New translations en-us.json (Croatian) 2024-03-08 10:50:09 -06:00
TrèsAbhi 370bf826c0 New translations en-us.json (Spanish) 2024-03-08 10:08:12 -06:00
TrèsAbhi 38ca1499e2 Update en-US.json 2024-03-08 10:07:57 -06:00
TrèsAbhi 2deced699b New translations en-us.json (Spanish) 2024-03-08 10:05:16 -06:00
TrèsAbhi 6405af7ec7 Update en-US.json 2024-03-08 10:05:03 -06:00
TrèsAbhi fa2bdfca12 New translations en-us.json (Spanish) 2024-03-08 10:04:04 -06:00
TrèsAbhi 872e7ce704 Update en-US.json 2024-03-08 10:03:49 -06:00
TrèsAbhi 110d3c1007 New translations en-us.json (Spanish) 2024-03-08 10:02:56 -06:00
TrèsAbhi 7a27b70c29 Update en-US.json 2024-03-08 10:02:30 -06:00
TrèsAbhi 2ffe5a137c New translations en-us.json (Spanish) 2024-03-07 22:45:56 -06:00
TrèsAbhi bf0246ec26 feat: rename 2024-03-07 21:38:38 -06:00
TrèsAbhi b71feca802 Merge pull request #89 from tresabhi/l10n_1.16.2 2024-03-07 21:37:58 -06:00
TrèsAbhi da1fbeeca4 New translations en-us.json (Spanish) 2024-03-07 21:37:14 -06:00
TrèsAbhi 53174db351 feat: rename tank types to tank classes 2024-03-07 18:30:02 -06:00
TrèsAbhi 19d2ad4cb2 New translations en-us.json (Spanish) 2024-03-07 18:29:53 -06:00
TrèsAbhi 3325536bbc feat: random localizations 2024-03-07 18:27:05 -06:00
TrèsAbhi 4220177b02 New translations en-us.json (Spanish) 2024-03-07 18:27:00 -06:00
TrèsAbhi 796aca21cf feat: breakdown locale 2024-03-07 17:02:37 -06:00
TrèsAbhi 3510768786 New translations en-us.json (Spanish) 2024-03-07 15:13:37 -06:00
TrèsAbhi 6197642d5e Merge pull request #88 from tresabhi/l10n_1.16.2 2024-03-07 13:49:52 -06:00
TrèsAbhi b60f8bfd57 feat: filter strings 2024-03-07 13:49:38 -06:00
TrèsAbhi 9437d16908 New translations en-us.json (Spanish) 2024-03-07 13:49:36 -06:00
TrèsAbhi de9077e0d2 New translations en-us.json (Spanish) 2024-03-06 20:44:06 -06:00
TrèsAbhi 4a6e946809 New translations en-us.json (Spanish) 2024-03-06 19:30:57 -06:00
TrèsAbhi 935c578ef6 Update en-US.json 2024-03-06 19:30:47 -06:00
TrèsAbhi 0e06ff52ba New translations en-us.json (Spanish) 2024-03-06 17:44:42 -06:00
TrèsAbhi fa4590d8f6 Merge pull request #87 from tresabhi/l10n_1.16.2 2024-03-06 17:41:02 -06:00
TrèsAbhi 5620b118f4 feat: some full stats locale 2024-03-06 17:41:05 -06:00
TrèsAbhi d53b31ca40 New translations en-us.json (Spanish) 2024-03-06 17:40:53 -06:00
TrèsAbhi a4a5d53f66 New translations en-us.json (Spanish) 2024-03-06 17:39:44 -06:00
TrèsAbhi 9f4c8c9088 feat: inactive command localization 2024-03-06 17:19:00 -06:00
TrèsAbhi 472172c504 New translations en-us.json (Spanish) 2024-03-06 17:18:51 -06:00
TrèsAbhi bc08656806 New translations en-us.json (Spanish) 2024-03-06 17:13:57 -06:00
TrèsAbhi bd6f1f3570 feat: no data translations 2024-03-06 16:26:36 -06:00
TrèsAbhi 716b446272 fix: remove refresh button if not enough permissions 2024-03-06 16:19:29 -06:00
TrèsAbhi 96a9e5b938 feat: filter localizations 2024-03-06 16:11:08 -06:00
TrèsAbhi 83e31e6352 New translations en-us.json (Spanish) 2024-03-06 16:11:07 -06:00
TrèsAbhi 49d0dbe86d feat: simpler command types 2024-03-06 15:38:53 -06:00
TrèsAbhi e141a7ca3f New translations en-us.json (Spanish) 2024-03-06 15:35:14 -06:00
TrèsAbhi adad668157 feat: some period translations 2024-03-06 15:34:10 -06:00
TrèsAbhi 267f23cad8 fix: use dot in /link 2024-03-06 09:53:28 -06:00
TrèsAbhi f605801555 feat: better titlebar 2024-03-06 09:52:09 -06:00
TrèsAbhi 19e81ff78a Merge pull request #86 from tresabhi/l10n_1.16.2 2024-03-05 21:15:33 -06:00
TrèsAbhi 9232a95e00 New translations en-us.json (Spanish) 2024-03-05 12:32:21 -06:00
TrèsAbhi 6dcef6b199 feat: default shard localew 2024-03-05 11:00:53 -06:00
TrèsAbhi e355bd10b1 feat: debug command 2024-03-05 10:58:41 -06:00
TrèsAbhi ceb4ed52c0 New translations en-us.json (Spanish) 2024-03-05 10:58:35 -06:00
TrèsAbhi 227750efad New translations en-us.json (Spanish) 2024-03-05 10:51:17 -06:00
TrèsAbhi 8a76558f01 feat: period subcommands 2024-03-05 10:51:14 -06:00
TrèsAbhi db7c865c76 feat: partially translated /about 2024-03-05 09:59:46 -06:00
TrèsAbhi 8e35e0d6d9 Merge pull request #85 from tresabhi/l10n_1.16.2 2024-03-05 09:47:53 -06:00
TrèsAbhi c9d50f39b5 New translations en-us.json (Spanish) 2024-03-05 09:47:34 -06:00
TrèsAbhi 77ac893a7c feat: es fix 2024-03-05 09:30:20 -06:00
TrèsAbhi ac60a436f1 feat: started implementation of translations 2024-03-04 10:33:02 -06:00
TrèsAbhi 2ba9f0f4c8 Merge pull request #83 from tresabhi/l10n_1.16.2
New Crowdin updates
2024-03-04 08:18:52 -06:00
TrèsAbhi 47c3aa4842 New translations en-us.json (Spanish) 2024-03-04 05:42:48 -06:00
TrèsAbhi d2edf4b89d New translations en-us.json (Spanish) 2024-03-04 02:54:51 -06:00
TrèsAbhi 11a0c246dc feat: error tolerant command registeration 2024-03-03 22:21:06 -06:00
TrèsAbhi cba93b3d55 feat: translate funcs 2024-03-03 22:13:22 -06:00
TrèsAbhi fa9b6c6d97 Merge pull request #82 from tresabhi/l10n_1.16.2 2024-03-03 21:47:33 -06:00
TrèsAbhi bef48eca62 New translations en-us.json (Japanese) 2024-03-03 21:44:38 -06:00
TrèsAbhi e42ff03c4b New translations en-us.json (Spanish) 2024-03-03 21:44:38 -06:00
TrèsAbhi ec628e4a0d fix: rename to en us 2024-03-03 21:42:08 -06:00
TrèsAbhi 8865a48936 feat: base lang 2024-03-03 21:41:12 -06:00
TrèsAbhi d3a9c3bd95 Create crowdin.yml 2024-03-03 20:24:31 -06:00
TrèsAbhi 90bff59af3 feat: more muted colors 2024-03-03 17:59:39 -06:00
TrèsAbhi 9efe9860ae feat: static grid 2024-03-03 17:42:53 -06:00
TrèsAbhi f6a418499b feat: direct depot 2024-03-03 16:08:29 -06:00
TrèsAbhi 66740bcb8d feat: vocab 2024-03-03 15:58:21 -06:00
TrèsAbhi 035e257926 feat: add closedkrieg 2024-03-01 22:54:03 -06:00
TrèsAbhi e63549158b feat: use glsl 3 2024-03-01 10:27:11 -06:00
105 changed files with 4023 additions and 2029 deletions

No files matched your search

+4 -3
View File
@@ -2,10 +2,10 @@ name: CD Build Assets
on: on:
push: push:
branches: dev branches: [dev, main]
workflow_dispatch: workflow_dispatch:
# schedule: schedule:
# - cron: '0 0 * * *' - cron: '0 0 * * *'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -50,4 +50,5 @@ jobs:
run: | run: |
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}" $env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}" $env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }} yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
+1 -1
View File
@@ -35,4 +35,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
+1 -1
View File
@@ -35,4 +35,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
+1 -1
View File
@@ -35,4 +35,4 @@ jobs:
run: yarn run: yarn
- name: Generate leaderboard - name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
-32
View File
@@ -1,32 +0,0 @@
name: CI Version
on: push
jobs:
host:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install Yarn
run: npm install --location=global yarn
- name: Use node_modules cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
run: yarn
- name: Generate leaderboard
run: yarn lint:version
+3
View File
@@ -0,0 +1,3 @@
[submodule "submodules/closedkrieg"]
path = submodules/closedkrieg
url = https://github.com/tresabhi/closedkrieg
+6
View File
@@ -5,6 +5,7 @@
"accountinfo", "accountinfo",
"aeson", "aeson",
"alphatest", "alphatest",
"APAC",
"apcr", "apcr",
"artefacts", "artefacts",
"atgm", "atgm",
@@ -40,8 +41,10 @@
"donotremove", "donotremove",
"drei", "drei",
"Droodles", "Droodles",
"dvpd",
"dvpl", "dvpl",
"DVPLCRC", "DVPLCRC",
"dvpm",
"dxgi", "dxgi",
"Everytime", "Everytime",
"FASTNAME", "FASTNAME",
@@ -70,6 +73,7 @@
"lods", "lods",
"metallicness", "metallicness",
"minitelrose", "minitelrose",
"Minotauro",
"MIPMAPCOUNT", "MIPMAPCOUNT",
"modeltransform", "modeltransform",
"MURICA", "MURICA",
@@ -129,6 +133,7 @@
"TYPELESS", "TYPELESS",
"ubjson", "ubjson",
"Unicum", "Unicum",
"UNLOCALIZED",
"UNORM", "UNORM",
"Unregistering", "Unregistering",
"vickers", "vickers",
@@ -138,6 +143,7 @@
"webp", "webp",
"winrate", "winrate",
"wireframe", "wireframe",
"WOTB",
"wotblitz", "wotblitz",
"xffn", "xffn",
"xkdkw", "xkdkw",
+3
View File
@@ -0,0 +1,3 @@
files:
- source: /src/lang/en-US.json
translation: /src/lang/%locale%.json
+36
View File
@@ -0,0 +1,36 @@
# Blitzkrieg 1.16.2
¡Hola desde el mundo del español!
## Features
- Added translations for Spanish to the bot
- Your language will be inferred automatically from your Discord settings
- Any untranslated languages and words we forgot to translate will be left in English
- If you have your language set to Spanish and see English in any places, please let us know on [the official Discord server](https://discord.gg/nDt7AjGJQH)
- All `/about` commands except for `commands` remain in English
- Tank names are still in English
- The website remains unchanged
- The title bar (featured as the big text on top of most commands) has now been made more compact
## Changes
- "Tank type" is now called "tank class"
- Removed `/player-achievements` and `search-tanks`
- They will be reintroduced in the future in a far better way
- Also removed a `SKLLD` server exclusive `/eligible` command
- This will be generalized for all clans in the future
## Fixes
- Fixed the red glowing floor underneath the tank sandbox
- This has been replaced with a static image
- The refresh button is now properly removed when lacking permissions
- Tank names that start with a number are now properly inferred from command inputs
## Technical Changes
- Command registration is now error tolerant meaning that if a single command fails to initiate, it will be discarded until the next restart
- We are currently looking into how we can allow the command to retry registration after a fixed amount of time
- Due to the implications of the code added which can be very easily exploited by hackers, a single file has been added to the repository as a closed source submodule and will remain closed unless a third party open-source implementation is made public
- Removed preview commands
+36
View File
@@ -0,0 +1,36 @@
# Blitzkrieg 1.16.2
¡Hola desde el mundo del español!
## Features
- Added translations for Spanish to the bot
- Your language will be inferred automatically from your Discord settings
- Any untranslated languages and words we forgot to translate will be left in English
- If you have your language set to Spanish and see English in any places, please let us know on [the official Discord server](https://discord.gg/nDt7AjGJQH)
- All `/about` commands except for `commands` remain in English
- Tank names are still in English
- The website remains unchanged
- The title bar (featured as the big text on top of most commands) has now been made more compact
## Changes
- "Tank type" is now called "tank class"
- Removed `/player-achievements` and `search-tanks`
- They will be reintroduced in the future in a far better way
- Also removed a `SKLLD` server exclusive `/eligible` command
- This will be generalized for all clans in the future
## Fixes
- Fixed the red glowing floor underneath the tank sandbox
- This has been replaced with a static image
- The refresh button is now properly removed when lacking permissions
- Tank names that start with a number are now properly inferred from command inputs
## Technical Changes
- Command registration is now error tolerant meaning that if a single command fails to initiate, it will be discarded until the next restart
- We are currently looking into how we can allow the command to retry registration after a fixed amount of time
- Due to the implications of the code added which can be very easily exploited by hackers, a single file has been added to the repository as a closed source submodule and will remain closed unless a third party open-source implementation is made public
- Removed preview commands
+21
View File
@@ -0,0 +1,21 @@
# Blitzkrieg 1.16.3
Further polishing and corrections for the Tankopedia.
## Features
- Added a few more stats to view for the Tankopedia
- Added the count of shells in the magazine for auto-reloaders
- The optimal shell index was moved below this stats
- Shell reloads for auto-reloaders now abide crew stats
- Intra-clip is now also shown for auto-reloaders
- Added 1 more decimal place worth of precision when viewing and changing gun pitch and yaw
## Fixes
- "Inter-clip" is now correctly spelled as "intra-clip"
- Tanks with initial gun pitches (like the Minotauro) are now handled correctly
- The stats now reflect the fact that the gun is naturally pitched downwards
- The view model now compensates for the natural pitch
- Tanks with the improved vertical stabilizer equipment are now handled correctly
- The view model now allows you to pitch the gun further as intended when the equipment is on
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkrieg", "name": "blitzkrieg",
"version": "1.16.0", "version": "1.16.2",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
@@ -25,7 +25,8 @@
"lint:version": "vite-node --script scripts/checkVersionMissMatch", "lint:version": "vite-node --script scripts/checkVersionMissMatch",
"knip": "knip", "knip": "knip",
"update": "yarn upgrade --latest", "update": "yarn upgrade --latest",
"setup:depotdownloader": "vite-node --script scripts/setupDepotDownloader" "setup:depotdownloader": "vite-node --script scripts/setupDepotDownloader",
"tunnel": "cloudflared tunnel --url http://localhost:3000"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+3 -2
View File
@@ -2,10 +2,11 @@ import { readdir } from 'fs/promises';
import { argv } from 'process'; import { argv } from 'process';
const isDepot = argv.includes('--depot'); const isDepot = argv.includes('--depot');
export const WOTB_WIN32_DEPOT = 444202;
export const DATA = isDepot export const DATA = isDepot
? await (async () => { ? await (async () => {
const [installationVersion] = await readdir('depots/444202'); const [installationVersion] = await readdir(`depots/${WOTB_WIN32_DEPOT}`);
return `depots/444202/${installationVersion}/Data`; return `depots/${WOTB_WIN32_DEPOT}/${installationVersion}/Data`;
})() })()
: 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data'; : 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
+4 -18
View File
@@ -2,7 +2,7 @@ import { readdir } from 'fs/promises';
import { parse as parsePath } from 'path'; import { parse as parsePath } from 'path';
import { Vector3Tuple } from 'three'; import { Vector3Tuple } from 'three';
import { parse as parseYaml } from 'yaml'; import { parse as parseYaml } from 'yaml';
import { TankType } from '../../src/components/Tanks'; import { TankClass } from '../../src/components/Tanks';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId'; import { toUniqueId } from '../../src/core/blitz/toUniqueId';
@@ -34,7 +34,6 @@ import {
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { TankParameters } from './tankIcons'; import { TankParameters } from './tankIcons';
type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
interface Strings { interface Strings {
[key: string]: string; [key: string]: string;
} }
@@ -306,19 +305,6 @@ type ConsumablesVehicleFilter =
| { name: string } | { name: string }
| { extendedTags: string }; | { extendedTags: string };
const consumableProvisionEffectSuffixes = [
'Increase',
'Factor',
'Bias',
'Coef',
'Percent',
];
const blitzTankTypeToBlitzkrieg: Record<BlitzTankType, TankType> = {
'AT-SPG': 'tankDestroyer',
lightTank: 'light',
mediumTank: 'medium',
heavyTank: 'heavy',
};
const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = { const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
ARMOR_PIERCING: 'ap', ARMOR_PIERCING: 'ap',
ARMOR_PIERCING_CR: 'ap_cr', ARMOR_PIERCING_CR: 'ap_cr',
@@ -502,7 +488,7 @@ export async function definitions(production: boolean) {
? 'premium' ? 'premium'
: 'researchable', : 'researchable',
tier: tank.level as Tier, tier: tank.level as Tier,
type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType], class: tankTags[0] as TankClass,
testing: tankTags.includes('testTank') ? true : undefined, testing: tankTags.includes('testTank') ? true : undefined,
price: tankPrice, price: tankPrice,
camouflage: { camouflage: {
@@ -688,7 +674,7 @@ export async function definitions(production: boolean) {
: turretGunEntry.reloadTime; : turretGunEntry.reloadTime;
const gunClipCount = const gunClipCount =
gunType === 'regular' ? undefined : turretGunEntry.clip!.count; gunType === 'regular' ? undefined : turretGunEntry.clip!.count;
const gunInterClip = const gunIntraClip =
gunType === 'regular' gunType === 'regular'
? undefined ? undefined
: 60 / turretGunEntry.clip!.rate; : 60 / turretGunEntry.clip!.rate;
@@ -736,7 +722,7 @@ export async function definitions(production: boolean) {
type: gunType, type: gunType,
reload: gunReload, reload: gunReload,
count: gunClipCount, count: gunClipCount,
interClip: gunInterClip, intraClip: gunIntraClip,
camouflageLoss: camouflageLoss:
typeof turretGunEntry.invisibilityFactorAtShot === 'number' typeof turretGunEntry.invisibilityFactorAtShot === 'number'
? turretGunEntry.invisibilityFactorAtShot ? turretGunEntry.invisibilityFactorAtShot
+1 -1
View File
@@ -311,7 +311,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}` tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
} }
type="tank" type="tank"
tankType={tankDefinitions?.type} tankType={tankDefinitions?.class}
treeType={tankDefinitions?.treeType} treeType={tankDefinitions?.treeType}
stats={session.customColumns.map((customColumn) => { stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display]; const rowStat = rowStats[customColumn.display];
+6 -2
View File
@@ -20,7 +20,11 @@ import PageWrapper from '../../../components/PageWrapper';
import { Skeleton } from '../../../components/Skeleton'; import { Skeleton } from '../../../components/Skeleton';
import { LEAGUES } from '../../../constants/leagues'; import { LEAGUES } from '../../../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings'; import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings';
import { REGIONS, REGION_NAMES, Region } from '../../../constants/regions'; import {
REGIONS,
Region,
UNLOCALIZED_REGION_NAMES,
} from '../../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz'; import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
@@ -556,7 +560,7 @@ export default function Page() {
<Select.Content> <Select.Content>
{REGIONS.map((region) => ( {REGIONS.map((region) => (
<Select.Item key={region} value={region}> <Select.Item key={region} value={region}>
{REGION_NAMES[region]} {UNLOCALIZED_REGION_NAMES[region]}
</Select.Item> </Select.Item>
))} ))}
</Select.Content> </Select.Content>
+2 -2
View File
@@ -18,7 +18,7 @@ import {
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { ChangeEvent, useEffect, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper'; import PageWrapper from '../../../components/PageWrapper';
import { REGION_NAMES } from '../../../constants/regions'; import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
import searchPlayersAcrossRegions, { import searchPlayersAcrossRegions, {
AccountListWithServer, AccountListWithServer,
} from '../../../core/blitz/searchPlayersAcrossRegions'; } from '../../../core/blitz/searchPlayersAcrossRegions';
@@ -107,7 +107,7 @@ export default function Page() {
setShowSearchResults(false); setShowSearchResults(false);
setSession(region, id, nickname); setSession(region, id, nickname);
}} }}
shortcut={REGION_NAMES[region]} shortcut={UNLOCALIZED_REGION_NAMES[region]}
> >
{nickname} {nickname}
</DropdownMenu.Item> </DropdownMenu.Item>
@@ -4,6 +4,7 @@ import { debounce } from 'lodash';
import { use, useEffect, useRef, useState } from 'react'; import { use, useEffect, useRef, useState } from 'react';
import { lerp } from 'three/src/math/MathUtils'; import { lerp } from 'three/src/math/MathUtils';
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton'; import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { isExplosive } from '../../../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { coefficient } from '../../../../../../core/blitzkrieg/coefficient'; import { coefficient } from '../../../../../../core/blitzkrieg/coefficient';
@@ -56,6 +57,7 @@ export function Characteristics() {
turretModelDefinition.boundingBox, turretModelDefinition.boundingBox,
), ),
); );
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
const weight = const weight =
tank.weight + engine.weight + track.weight + turret.weight + gun.weight; tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
const weightTons = weight / 1000; const weightTons = weight / 1000;
@@ -113,7 +115,7 @@ export function Characteristics() {
// TODO: add max rolls // TODO: add max rolls
[hasTungsten, 0.15], [hasTungsten, 0.15],
); );
const interClipCoefficient = coefficient([hasShellReloadBoost, -0.3]); const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const shellVelocityCoefficient = coefficient( const shellVelocityCoefficient = coefficient(
[hasSupercharger, 0.3], [hasSupercharger, 0.3],
[hasImprovedGunPowder, 0.3], [hasImprovedGunPowder, 0.3],
@@ -136,12 +138,13 @@ export function Characteristics() {
]); ]);
const enginePowerCoefficient = coefficient( const enginePowerCoefficient = coefficient(
[ [
hasEngineAccelerator && (tank.type === 'light' || tank.type === 'medium'), hasEngineAccelerator &&
(tank.class === 'lightTank' || tank.class === 'mediumTank'),
0.05, 0.05,
], ],
[ [
hasEngineAccelerator && hasEngineAccelerator &&
(tank.type === 'heavy' || tank.type === 'tankDestroyer'), (tank.class === 'heavyTank' || tank.class === 'AT-SPG'),
0.07, 0.07,
], ],
[hasEnginePowerBoost, 0.2], [hasEnginePowerBoost, 0.2],
@@ -163,7 +166,7 @@ export function Characteristics() {
const viewRangeCoefficient = const viewRangeCoefficient =
coefficient([ coefficient([
hasImprovedOptics, hasImprovedOptics,
tank.type === 'tankDestroyer' ? 0.05 : tank.type === 'heavy' ? 0.07 : 0.1, tank.class === 'AT-SPG' ? 0.05 : tank.class === 'heavyTank' ? 0.07 : 0.1,
]) * coefficient([true, progressiveStat(commanderMastery)]); ]) * coefficient([true, progressiveStat(commanderMastery)]);
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
@@ -180,38 +183,26 @@ export function Characteristics() {
const camouflageSumMoving = sum( const camouflageSumMoving = sum(
[ [
hasCamouflageNet, hasCamouflageNet,
tank.type === 'heavy' tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05,
? 0.03
: tank.type === 'tankDestroyer'
? 0.07
: 0.05,
], ],
[ [
camouflage, camouflage,
tank.type === 'tankDestroyer' tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
? 0.04
: tank.type === 'heavy'
? 0.03
: 0.02,
], ],
); );
const camouflageSumStill = sum( const camouflageSumStill = sum(
[ [
hasCamouflageNet, hasCamouflageNet,
2 * 2 *
(tank.type === 'heavy' (tank.class === 'heavyTank'
? 0.03 ? 0.03
: tank.type === 'tankDestroyer' : tank.class === 'AT-SPG'
? 0.07 ? 0.07
: 0.05), : 0.05),
], ],
[ [
camouflage, camouflage,
tank.type === 'tankDestroyer' tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
? 0.04
: tank.type === 'heavy'
? 0.03
: 0.02,
], ],
); );
@@ -221,7 +212,7 @@ export function Characteristics() {
shell, shell,
damageCoefficient, damageCoefficient,
reloadCoefficient, reloadCoefficient,
interClipCoefficient, intraClipCoefficient,
); );
useEffect(() => { useEffect(() => {
@@ -229,6 +220,17 @@ export function Characteristics() {
penetrationDistanceInput.current.value = `${penetrationDistance}`; penetrationDistanceInput.current.value = `${penetrationDistance}`;
} }
}, [penetrationDistance]); }, [penetrationDistance]);
useEffect(() => {
mutateDuel((draft) => {
[draft.protagonist!.pitch, draft.protagonist!.yaw] = applyPitchYawLimits(
draft.protagonist!.pitch,
draft.protagonist!.yaw,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
});
}, [hasImprovedVerticalStabilizer]);
return ( return (
<Flex direction="column" gap="8" style={{ width: '100%' }}> <Flex direction="column" gap="8" style={{ width: '100%' }}>
@@ -266,7 +268,7 @@ export function Characteristics() {
<> <>
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min"> <InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
{gun.reload.at(-1)! < gun.reload.at(-2)! {gun.reload.at(-1)! < gun.reload.at(-2)!
? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) * ? (shell.damage.armor / (gun.reload.at(-1)! + gun.intraClip)) *
60 60
: (shell.damage.armor / gun.reload[0]) * 60} : (shell.damage.armor / gun.reload[0]) * 60}
</InfoWithDelta> </InfoWithDelta>
@@ -277,12 +279,12 @@ export function Characteristics() {
unit="hp / min" unit="hp / min"
> >
{gun.reload.at(-1)! < gun.reload.at(-2)! {gun.reload.at(-1)! < gun.reload.at(-2)!
? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) * ? (shell.damage.armor / (gun.reload.at(-1)! + gun.intraClip)) *
(60 - (60 -
(gun.reload.slice(0, -1).length - 1) * gun.interClip) + (gun.reload.slice(0, -1).length - 1) * gun.intraClip) +
shell.damage.armor * gun.reload.slice(0, -1).length shell.damage.armor * gun.reload.slice(0, -1).length
: (shell.damage.armor / (gun.reload[0] + gun.interClip)) * : (shell.damage.armor / (gun.reload[0] + gun.intraClip)) *
(60 - (gun.reload.slice(1).length - 1) * gun.interClip) + (60 - (gun.reload.slice(1).length - 1) * gun.intraClip) +
shell.damage.armor * gun.reload.slice(1).length} shell.damage.armor * gun.reload.slice(1).length}
</InfoWithDelta> </InfoWithDelta>
<Info <Info
@@ -296,14 +298,15 @@ export function Characteristics() {
</a> </a>
} }
/> />
<Info indent name="Optimal shell index">
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1}
</Info>
</> </>
)} )}
{gun.type === 'autoReloader' ? ( {gun.type === 'autoReloader' ? (
<> <>
<Info name="Reload" unit="s" /> <InfoWithDelta name="Shells">{gun.reload.length}</InfoWithDelta>
<Info indent name="Most optimal">
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1}
</Info>
<Info name="Shell reloads" unit="s" />
{gun.reload.map((reload, index) => ( {gun.reload.map((reload, index) => (
<InfoWithDelta <InfoWithDelta
key={index} key={index}
@@ -312,7 +315,7 @@ export function Characteristics() {
decimals={2} decimals={2}
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{reload} {reload * reloadCoefficient}
</InfoWithDelta> </InfoWithDelta>
))} ))}
</> </>
@@ -326,17 +329,18 @@ export function Characteristics() {
{gun.reload * reloadCoefficient} {gun.reload * reloadCoefficient}
</InfoWithDelta> </InfoWithDelta>
)} )}
{gun.type === 'autoLoader' && ( {gun.type === 'autoLoader' ||
<InfoWithDelta (gun.type === 'autoReloader' && (
indent <InfoWithDelta
decimals={2} indent
name="Inter-clip reload" decimals={2}
unit="s" name="Intra-clip"
deltaType="lowerIsBetter" unit="s"
> deltaType="lowerIsBetter"
{gun.interClip * interClipCoefficient} >
</InfoWithDelta> {gun.intraClip * intraClipCoefficient}
)} </InfoWithDelta>
))}
<InfoWithDelta name="Caliber" decimals={0} unit="mm"> <InfoWithDelta name="Caliber" decimals={0} unit="mm">
{shell.caliber} {shell.caliber}
</InfoWithDelta> </InfoWithDelta>
@@ -468,10 +472,12 @@ export function Characteristics() {
<Info name="Gun flexibility" unit="°" /> <Info name="Gun flexibility" unit="°" />
<InfoWithDelta decimals={1} indent name="Depression"> <InfoWithDelta decimals={1} indent name="Depression">
{gunModelDefinition.pitch.max + {gunModelDefinition.pitch.max +
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)} (hasImprovedVerticalStabilizer ? 3 : 0)}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Elevation"> <InfoWithDelta decimals={1} indent name="Elevation">
{-gunModelDefinition.pitch.min + {-gunModelDefinition.pitch.min -
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)} (hasImprovedVerticalStabilizer ? 3 : 0)}
</InfoWithDelta> </InfoWithDelta>
{gunModelDefinition.pitch.front && ( {gunModelDefinition.pitch.front && (
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react';
import { Vector3 } from 'three'; import { Vector3 } from 'three';
import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../core/blitzkrieg/hasEquipment';
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose'; import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose';
import { useAwait } from '../../../../../hooks/useAwait'; import { useAwait } from '../../../../../hooks/useAwait';
@@ -67,7 +68,9 @@ export function Controls() {
}, },
); );
function handlePoseEvent(event: Pose) { async function handlePoseEvent(event: Pose) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
switch (event) { switch (event) {
case Pose.HullDown: { case Pose.HullDown: {
const [pitch] = applyPitchYawLimits( const [pitch] = applyPitchYawLimits(
@@ -75,6 +78,7 @@ export function Controls() {
0, 0,
protagonistGunModelDefinition.pitch, protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw, protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
camera.position camera.position
@@ -106,6 +110,7 @@ export function Controls() {
0, 0,
protagonistGunModelDefinition.pitch, protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw, protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
camera.position camera.position
@@ -11,6 +11,7 @@ import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinit
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose'; import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon'; import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useWideFormat } from '../../../../../../hooks/useWideFormat'; import { useWideFormat } from '../../../../../../hooks/useWideFormat';
import { mutateDuel, useDuel } from '../../../../../../stores/duel'; import { mutateDuel, useDuel } from '../../../../../../stores/duel';
import mutateTankopediaPersistent, { import mutateTankopediaPersistent, {
@@ -29,6 +30,7 @@ import { TankModel } from './components/TankModel';
export function TankSandbox() { export function TankSandbox() {
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122);
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const canvasWrapper = useRef<HTMLDivElement>(null); const canvasWrapper = useRef<HTMLDivElement>(null);
const [isFullScreen, setIsFullScreen] = useState(false); const [isFullScreen, setIsFullScreen] = useState(false);
@@ -70,6 +72,7 @@ export function TankSandbox() {
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
@@ -87,6 +90,7 @@ export function TankSandbox() {
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
@@ -104,6 +108,7 @@ export function TankSandbox() {
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
@@ -131,6 +136,7 @@ export function TankSandbox() {
draft.protagonist!.yaw, draft.protagonist!.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
}); });
}, [protagonist.gun, protagonist.turret]); }, [protagonist.gun, protagonist.turret]);
@@ -4,13 +4,14 @@ import { Euler, Group, Vector3 } from 'three';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils';
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis'; import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../../../core/blitzkrieg/hasEquipment';
import { jsxTree } from '../../../../../../../core/blitzkrieg/jsxTree'; import { jsxTree } from '../../../../../../../core/blitzkrieg/jsxTree';
import { modelDefinitions } from '../../../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../../../core/blitzkrieg/modelDefinitions';
import { import {
ModelTransformEventData, ModelTransformEventData,
modelTransformEvent, modelTransformEvent,
} from '../../../../../../../core/blitzkrieg/modelTransform'; } from '../../../../../../../core/blitzkrieg/modelTransform';
import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180'; import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngleRad';
import { useAwait } from '../../../../../../../hooks/useAwait'; import { useAwait } from '../../../../../../../hooks/useAwait';
import { useModel } from '../../../../../../../hooks/useModel'; import { useModel } from '../../../../../../../hooks/useModel';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel'; import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
@@ -106,7 +107,6 @@ export const TankModel = memo(() => {
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id]; tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const { gltf } = useModel(protagonist.tank.id); const { gltf } = useModel(protagonist.tank.id);
const nodes = Object.values(gltf.nodes); const nodes = Object.values(gltf.nodes);
@@ -161,19 +161,21 @@ export const TankModel = memo(() => {
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
} }
function handlePointerMove(event: PointerEvent) { async function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
[pitch, yaw] = applyPitchYawLimits( [pitch, yaw] = applyPitchYawLimits(
pitch, pitch,
yaw + event.movementX * (Math.PI / canvas.width), yaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
} }
function handlePointerUp() { function handlePointerUp() {
mutateDuel((draft) => { mutateDuel((draft) => {
draft.protagonist!.pitch = normalizeAnglePI(pitch); draft.protagonist!.pitch = normalizeAngleRad(pitch);
draft.protagonist!.yaw = normalizeAnglePI(yaw); draft.protagonist!.yaw = normalizeAngleRad(yaw);
}); });
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.controlsEnabled = true; draft.model.visual.controlsEnabled = true;
@@ -220,12 +222,14 @@ export const TankModel = memo(() => {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { async function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
[pitch, yaw] = applyPitchYawLimits( [pitch, yaw] = applyPitchYawLimits(
pitch - event.movementY * (Math.PI / canvas.height), pitch - event.movementY * (Math.PI / canvas.height),
yaw + event.movementX * (Math.PI / canvas.width), yaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
} }
@@ -234,8 +238,8 @@ export const TankModel = memo(() => {
draft.model.visual.controlsEnabled = true; draft.model.visual.controlsEnabled = true;
}); });
mutateDuel((draft) => { mutateDuel((draft) => {
draft.protagonist!.pitch = normalizeAnglePI(pitch); draft.protagonist!.pitch = normalizeAngleRad(pitch);
draft.protagonist!.yaw = normalizeAnglePI(yaw); draft.protagonist!.yaw = normalizeAngleRad(yaw);
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
@@ -1,5 +1,6 @@
import { Flex, TextField } from '@radix-ui/themes'; import { Flex, TextField } from '@radix-ui/themes';
import { use, useEffect, useRef } from 'react'; import { use, useEffect, useRef } from 'react';
import { degToRad, radToDeg } from 'three/src/math/MathUtils';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
@@ -20,22 +21,21 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
const mode = useTankopediaPersistent((state) => state.mode); const mode = useTankopediaPersistent((state) => state.mode);
const hasCalibratedShells = useEquipment(103, true); const hasCalibratedShells = useEquipment(103, true);
const hasEnhancedArmor = useEquipment(110); const hasEnhancedArmor = useEquipment(110);
useEffect(() => {
turretYawInput.current!.value = `${-Math.round(
protagonist.yaw * (180 / Math.PI),
)}`;
}, [protagonist.yaw]);
useEffect(() => {
gunPitchInput.current!.value = `${-Math.round(
protagonist.pitch * (180 / Math.PI),
)}`;
}, [protagonist.pitch]);
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id]; tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const initialGunPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
const hasImprovedVerticalStabilizer = useEquipment(122);
useEffect(() => {
turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
}, [protagonist.yaw]);
useEffect(() => {
gunPitchInput.current!.value = (
-radToDeg(protagonist.pitch) + initialGunPitch
).toFixed(1);
}, [protagonist.pitch]);
return ( return (
<Flex <Flex
@@ -68,22 +68,22 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
Yaw Yaw
</TextField.Slot> </TextField.Slot>
<TextField.Input <TextField.Input
defaultValue={Math.round(protagonist.yaw * (180 / Math.PI))}
onBlur={() => { onBlur={() => {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
protagonist.pitch, protagonist.pitch,
-Number(turretYawInput.current?.value) * (Math.PI / 180), degToRad(Number(turretYawInput.current!.value)),
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
mutateDuel((state) => { mutateDuel((state) => {
state.protagonist!.pitch = pitch; state.protagonist!.pitch = pitch;
state.protagonist!.yaw = yaw; state.protagonist!.yaw = yaw;
}); });
turretYawInput.current!.value = `${Math.round( turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(
yaw * (180 / Math.PI), 1,
)}`; );
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@@ -116,22 +116,24 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
Pitch Pitch
</TextField.Slot> </TextField.Slot>
<TextField.Input <TextField.Input
defaultValue={-Math.round(protagonist.pitch * (180 / Math.PI))}
onBlur={() => { onBlur={() => {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
-Number(gunPitchInput.current?.value) * (Math.PI / 180), degToRad(
-Number(gunPitchInput.current!.value) + initialGunPitch,
),
protagonist.yaw, protagonist.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
); );
modelTransformEvent.emit({ pitch, yaw }); modelTransformEvent.emit({ pitch, yaw });
mutateDuel((state) => { mutateDuel((state) => {
state.protagonist!.pitch = pitch; state.protagonist!.pitch = pitch;
state.protagonist!.yaw = yaw; state.protagonist!.yaw = yaw;
}); });
gunPitchInput.current!.value = `${Math.round( gunPitchInput.current!.value = (
pitch * (180 / Math.PI), -radToDeg(protagonist.pitch) + initialGunPitch
)}`; ).toFixed(1);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@@ -1,32 +1,30 @@
import { Color } from 'three'; import { useFrame, useLoader } from '@react-three/fiber';
import InfiniteGridHelper from '../../../../../components/InfiniteGridHelper'; import { clamp } from 'lodash';
import { useRef } from 'react';
import { MeshStandardMaterial, TextureLoader } from 'three';
import { useTankopediaPersistent } from '../../../../../stores/tankopedia'; import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
export function SceneProps() { export function SceneProps() {
const showGrid = useTankopediaPersistent( const show = useTankopediaPersistent(
(state) => state.model.visual.showGrid, (state) =>
); state.model.visual.showGrid && !state.model.visual.showEnvironment,
const showEnvironment = useTankopediaPersistent(
(state) => state.model.visual.showEnvironment,
); );
const texture = useLoader(TextureLoader, 'https://i.imgur.com/C28Z8nU.png');
const material = useRef<MeshStandardMaterial>(null);
if (!showGrid || showEnvironment) return null; useFrame(({ camera }) => {
if (!material.current) return;
material.current.opacity = clamp(0.1 * camera.position.y, 0, 1);
});
texture.anisotropy = 2;
if (!show) return null;
return ( return (
<> <mesh rotation={[-Math.PI / 2, 0, 0]}>
<InfiniteGridHelper <planeGeometry args={[10, 10]} />
size1={1 / 5} <meshStandardMaterial ref={material} map={texture} transparent />
size2={1} </mesh>
distance={20}
color={new Color('#ffffff')}
/>
<InfiniteGridHelper
position={[0, 1e-4, 0]}
size1={0}
size2={100}
distance={25}
color={new Color('red')}
/>
</>
); );
} }
@@ -30,7 +30,7 @@ export function Title() {
<img <img
src={ src={
TREE_TYPE_ICONS[protagonist.tank.treeType][ TREE_TYPE_ICONS[protagonist.tank.treeType][
protagonist.tank.type protagonist.tank.class
] ]
} }
style={{ style={{
@@ -5,7 +5,7 @@ import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
import { use } from 'react'; import { use } from 'react';
import { Flag } from '../../../../../../../components/Flag'; import { Flag } from '../../../../../../../components/Flag';
import { import {
TANK_TYPES, TANK_CLASSES,
TREE_TYPES, TREE_TYPES,
TREE_TYPE_ICONS, TREE_TYPE_ICONS,
TREE_TYPE_IMAGES, TREE_TYPE_IMAGES,
@@ -121,7 +121,7 @@ export function Options() {
))} ))}
</Flex> </Flex>
<Flex> <Flex>
{[...TANK_TYPES].map((type, index) => ( {[...TANK_CLASSES].map((type, index) => (
<Button <Button
key={type} key={type}
color={filters.types.includes(type) ? undefined : 'gray'} color={filters.types.includes(type) ? undefined : 'gray'}
@@ -132,9 +132,9 @@ export function Options() {
borderTopLeftRadius: index === 0 ? undefined : 0, borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: index === 0 ? undefined : 0, borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius: borderTopRightRadius:
index === TANK_TYPES.length - 1 ? undefined : 0, index === TANK_CLASSES.length - 1 ? undefined : 0,
borderBottomRightRadius: borderBottomRightRadius:
index === TANK_TYPES.length - 1 ? undefined : 0, index === TANK_CLASSES.length - 1 ? undefined : 0,
}} }}
onClick={() => onClick={() =>
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
@@ -15,7 +15,7 @@ import {
} from '@radix-ui/themes'; } from '@radix-ui/themes';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import { use, useEffect, useMemo, useRef, useState } from 'react'; import { use, useEffect, useMemo, useRef, useState } from 'react';
import { TankType, TreeType } from '../../../../../components/Tanks'; import { TANK_CLASSES, TreeType } from '../../../../../components/Tanks';
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
import { asset } from '../../../../../core/blitzkrieg/asset'; import { asset } from '../../../../../core/blitzkrieg/asset';
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
@@ -49,7 +49,6 @@ interface TankSearchProps {
onSelect?: (tank: TankDefinition) => void; onSelect?: (tank: TankDefinition) => void;
} }
const tankTypeOrder: TankType[] = ['light', 'medium', 'heavy', 'tankDestroyer'];
const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector']; const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector'];
export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) { export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
@@ -70,7 +69,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
) )
.sort( .sort(
(a, b) => (a, b) =>
(tankTypeOrder.indexOf(a.type) - tankTypeOrder.indexOf(b.type)) * (TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class)) *
(sort.direction === 'descending' ? -1 : 1), (sort.direction === 'descending' ? -1 : 1),
) )
.sort( .sort(
@@ -89,7 +88,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
: filters.tiers.includes(tank.tier)) && : filters.tiers.includes(tank.tier)) &&
(filters.types.length === 0 (filters.types.length === 0
? true ? true
: filters.types.includes(tank.type)) && : filters.types.includes(tank.class)) &&
(filters.treeTypes.length === 0 (filters.treeTypes.length === 0
? true ? true
: filters.treeTypes.includes(tank.treeType)) && : filters.treeTypes.includes(tank.treeType)) &&
@@ -844,7 +843,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
? TANK_ICONS_COLLECTOR ? TANK_ICONS_COLLECTOR
: tank.treeType === 'premium' : tank.treeType === 'premium'
? TANK_ICONS_PREMIUM ? TANK_ICONS_PREMIUM
: TANK_ICONS)[tank.type] : TANK_ICONS)[tank.class]
} }
style={{ width: '1em', height: '1em' }} style={{ width: '1em', height: '1em' }}
/> />
+54 -52
View File
@@ -1,67 +1,69 @@
import { import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
SlashCommandBuilder, import { translator } from '../core/localization/translator';
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate'; import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`; const subcommands = [
const DOCS = { 'embeds',
'introduction',
'invite',
'timezones',
'creators',
] as const;
type Subcommand = (typeof subcommands)[number];
const DOCS: Record<Subcommand, string> = {
embeds: 'guide/embeds', embeds: 'guide/embeds',
introduction: 'guide/introduction', introduction: 'guide/introduction',
invite: 'guide/invite', invite: 'guide/invite',
timezones: 'guide/timezones', timezones: 'guide/timezones',
creators: 'guide/creators', creators: 'guide/creators',
}; };
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
embeds: 'How embeds work',
introduction: 'About the bot',
invite: 'How to invite the bot',
timezones: 'How Blitzkrieg infers and uses your timezone',
creators: 'Who created the bot',
};
function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) { export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
Object.entries(DOC_DESCRIPTIONS).forEach(([key, value]) => { resolve({
option.addSubcommand((sub) => sub.setName(key).setDescription(value)); inProduction: true,
}); inPublic: true,
return option; command: createLocalizedCommand('about', [
} { subcommand: 'commands' },
...subcommands.map((subcommand) => ({ subcommand })),
]),
export const aboutCommand: CommandRegistry = { async handler(interaction) {
inProduction: true, const subcommand = interaction.options.getSubcommand();
inPublic: true, const { t } = translator(interaction.locale);
command: addDocsSubcommands( if (subcommand === 'commands') {
new SlashCommandBuilder() return t`bot.commands.about.subcommands.commands.body${(
.setName('about') await Promise.all(COMMANDS_RAW)
.setDescription('All the info you need about the bot')
.addSubcommand((option) =>
option
.setName('commands')
.setDescription('List of all commands and their descriptions'),
),
),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'commands') {
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
await Promise.all(COMMANDS_RAW)
)
.filter((registry) => registry.inPublic && registry.inProduction)
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
.map(
(registry) =>
`- \`/${registry.command.name}\`: ${registry.command.description}`,
) )
.join('\n')}`; .filter((registry) => registry.inPublic && registry.inProduction)
} .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 = `${RAW_PATH}${DOCS[subcommand as keyof typeof DOCS]}.md`; const url = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/${DOCS[subcommand as Subcommand]}.md`;
const content = fetch(url).then((response) => response.text()); const content = fetch(url).then((response) => response.text());
return content; return content;
}, },
}; });
});
+51 -38
View File
@@ -1,9 +1,9 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import { chunk } from 'lodash'; import { chunk } from 'lodash';
import * as Breakdown from '../components/Breakdown'; import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData'; import CommandWrapper from '../components/CommandWrapper';
import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo'; import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats'; import getTankStats from '../core/blitz/getTankStats';
@@ -19,8 +19,9 @@ import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary'; import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL'; import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton'; import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand'; import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
@@ -32,10 +33,11 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, { import resolvePlayerFromCommand, {
ResolvedPlayer, ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import calculateWN8 from '../core/statistics/calculateWN8'; import calculateWN8 from '../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile'; import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8; const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9; const MAX_PAGES = 9;
@@ -44,15 +46,17 @@ export async function renderBreakdown(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
filters: StatFilters, filters: StatFilters,
locale: Locale,
) { ) {
const { t, translate } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions; const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await tankAverages; const awaitedTankAverages = await tankAverages;
const statsInPeriod = await getStatsInPeriod(region, id, start, end); const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters); const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ['clan']); const clanData = await getClanAccountInfo(region, id, ['clan']);
const tankStats = await getTankStats(region, id); const tankStats = await getTankStats(region, id, locale);
const filterDescriptions = await filtersToDescription(filters); const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: AllStats[] = []; const orderedCurrentStats: AllStats[] = [];
const orderedCareerStats: AllStats[] = []; const orderedCareerStats: AllStats[] = [];
const orderedCurrentWN8: (number | undefined)[] = []; const orderedCurrentWN8: (number | undefined)[] = [];
@@ -144,21 +148,21 @@ export async function renderBreakdown(
<Breakdown.Row <Breakdown.Row
key={id} key={id}
type="summary" type="summary"
title="Total" title={t`bot.commands.breakdown.body.total`}
stats={[ stats={[
{ {
title: 'Battles', title: t`bot.commands.breakdown.body.battles`,
current: currentBattles, current: currentBattles,
career: careerBattles, career: careerBattles,
}, },
{ {
title: 'Winrate', title: t`bot.commands.breakdown.body.winrate`,
current: `${(100 * currentWinrate).toFixed(2)}%`, current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`, career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate, delta: currentWinrate - careerWinrate,
}, },
{ {
title: 'WN8', title: t`bot.commands.breakdown.body.wn8`,
current: current:
currentWN8 === undefined currentWN8 === undefined
? undefined ? undefined
@@ -173,7 +177,7 @@ export async function renderBreakdown(
: getWN8Percentile(currentWN8), : getWN8Percentile(currentWN8),
}, },
{ {
title: 'Damage', title: t`bot.commands.breakdown.body.damage`,
current: Math.round(currentDamage).toLocaleString(), current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(), career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage, delta: currentDamage - careerDamage,
@@ -195,17 +199,20 @@ export async function renderBreakdown(
<Breakdown.Row <Breakdown.Row
key={id} key={id}
type="tank" type="tank"
tankType={tankDefinition?.type} tankType={tankDefinition?.class}
treeType={tankDefinition?.treeType} treeType={tankDefinition?.treeType}
title={tankDefinition?.name ?? `Unknown tank ${id}`} title={
tankDefinition?.name ??
translate('bot.commands.breakdown.body.unknown_tank', [`${id}`])
}
stats={[ stats={[
{ {
title: 'Battles', title: t`bot.commands.breakdown.body.battles`,
current: current.battles.toLocaleString(), current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(), career: career.battles.toLocaleString(),
}, },
{ {
title: 'Winrate', title: t`bot.commands.breakdown.body.winrate`,
current: `${(100 * (current.wins / current.battles)).toFixed( current: `${(100 * (current.wins / current.battles)).toFixed(
2, 2,
)}%`, )}%`,
@@ -214,7 +221,7 @@ export async function renderBreakdown(
current.wins / current.battles - career.wins / career.battles, current.wins / current.battles - career.wins / career.battles,
}, },
{ {
title: 'WN8', title: t`bot.commands.breakdown.body.wn8`,
current: current:
currentWN8 === undefined currentWN8 === undefined
? undefined ? undefined
@@ -229,7 +236,7 @@ export async function renderBreakdown(
: getWN8Percentile(currentWN8), : getWN8Percentile(currentWN8),
}, },
{ {
title: 'Damage', title: t`bot.commands.breakdown.body.damage`,
current: Math.round( current: Math.round(
current.damage_dealt / current.battles, current.damage_dealt / current.battles,
).toLocaleString(), ).toLocaleString(),
@@ -254,10 +261,10 @@ export async function renderBreakdown(
if (filteredOrder.length > 0) { if (filteredOrder.length > 0) {
return pages return pages
.map((page, index) => ( .map((page, index) => (
<Wrapper> <CommandWrapper>
{index === 0 && ( {index === 0 && (
<TitleBar <TitleBar
name={accountInfo.nickname} title={accountInfo.nickname}
image={ image={
clanData?.clan clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
@@ -268,14 +275,14 @@ export async function renderBreakdown(
)} )}
<Breakdown.Root>{page}</Breakdown.Root> <Breakdown.Root>{page}</Breakdown.Root>
</Wrapper> </CommandWrapper>
)) ))
.reverse(); .reverse();
} else { } else {
return [ return [
<Wrapper> <CommandWrapper>
<TitleBar <TitleBar
name={accountInfo.nickname} title={accountInfo.nickname}
image={ image={
clanData?.clan clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
@@ -284,26 +291,22 @@ export async function renderBreakdown(
description={`${name} • ${filterDescriptions}`} description={`${name} • ${filterDescriptions}`}
/> />
<NoData type={NoDataType.BattlesInPeriod} /> <NoData type="battles_in_period" locale={locale} />
</Wrapper>, </CommandWrapper>,
]; ];
} }
} }
export const breakdownCommand = new Promise<CommandRegistryPromisable>( export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => { async (resolve) => {
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('breakdown')
.setDescription("A period's breakdown by tanks played"),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command, command: await addPeriodicFilterOptions(
createLocalizedCommand('breakdown'),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) { async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction); const player = await resolvePlayerFromCommand(interaction);
@@ -316,8 +319,13 @@ export const breakdownCommand = new Promise<CommandRegistryPromisable>(
}); });
return [ return [
...(await renderBreakdown(player, period, filters)), ...(await renderBreakdown(
buttonPrimary(path, 'Refresh'), player,
period,
filters,
interaction.locale,
)),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(player.region, player.id), await getBlitzStarsLinkButton(player.region, player.id),
]; ];
}, },
@@ -332,7 +340,12 @@ export const breakdownCommand = new Promise<CommandRegistryPromisable>(
const period = resolvePeriodFromButton(player.region, interaction); const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction); const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters); return await renderBreakdown(
player,
period,
filters,
interaction.locale,
);
}, },
}); });
}, },
+32 -28
View File
@@ -1,36 +1,40 @@
import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' }; import packageJSON from '../../package.json' assert { type: 'json' };
import { client } from '../core/discord/client'; import { client } from '../core/discord/client';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
const executionStart = new Date().getTime(); const executionStart = new Date().getTime();
export const debugCommand: CommandRegistry = { export const debugCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('debug'),
.setName('debug')
.setDescription('Debug information about the bot'),
async handler() { async handler(interaction) {
const currentTime = new Date().getTime(); const { t } = translator(interaction.locale);
const uptime = currentTime - executionStart; const currentTime = new Date().getTime();
const list = [ const uptime = currentTime - executionStart;
['Version', packageJSON.version], const list = [
['Shard', client.shard?.ids[0] ?? 'default'], [t`bot.commands.debug.body.version`, packageJSON.version],
[ [
'Uptime', t`bot.commands.debug.body.shard`,
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor( client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`,
(uptime / 1000 / 60) % 60, ],
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor( [
uptime % 1000, t`bot.commands.debug.body.uptime`,
)}ms`, `${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
], (uptime / 1000 / 60) % 60,
] )}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
.map(([key, value]) => `- ${key}: ${value}`) uptime % 1000,
.join('\n'); )}ms`,
],
return `# Debug\n\n${list}`; ];
}, return t`bot.commands.debug.body.title${list
}; .map(([key, value]) => `- ${key}: ${value}`)
.join('\n')}`;
},
});
});
+185 -178
View File
@@ -1,198 +1,205 @@
import { SlashCommandBuilder } from 'discord.js'; // _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
import { getAccountInfo } from '../core/blitz/getAccountInfo'; // |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
import getPeriodNow from '../core/blitzkrieg/getPeriodNow'; // | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo'; // | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions'; // _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; // |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
import { tankAverages } from '../core/blitzstars/tankAverages';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import embedNegative from '../core/discord/embedNegative';
import embedPositive from '../core/discord/embedPositive';
import markdownTable, { TableInput } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import calculateWN8 from '../core/statistics/calculateWN8';
import sumStats from '../core/statistics/sumStats';
import { CommandRegistry } from '../events/interactionCreate';
import { PossiblyPromise } from '../types/possiblyPromise';
export type SkilledClan = 'SKLLD' | 'SMRI'; // import { SlashCommandBuilder } from 'discord.js';
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
// import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
// import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
// import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions';
// import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
// import { tankAverages } from '../core/blitzstars/tankAverages';
// import addUsernameChoices from '../core/discord/addUsernameChoices';
// import embedNegative from '../core/discord/embedNegative';
// import embedPositive from '../core/discord/embedPositive';
// import markdownTable, { TableInput } from '../core/discord/markdownTable';
// import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
// import calculateWN8 from '../core/statistics/calculateWN8';
// import sumStats from '../core/statistics/sumStats';
// import { CommandRegistry } from '../events/interactionCreate';
// import { PossiblyPromise } from '../types/possiblyPromise';
export const SKILLED_CLANS: Record<SkilledClan, string> = { // export type SkilledClan = 'SKLLD' | 'SMRI';
SKLLD: 'Skilled',
SMRI: 'Samurai',
};
export const eligibleCommand: CommandRegistry = { // export const SKILLED_CLANS: Record<SkilledClan, string> = {
inProduction: true, // SKLLD: 'Skilled',
inPublic: false, // SMRI: 'Samurai',
// };
command: new SlashCommandBuilder() // export const eligibleCommand: CommandRegistry = {
.setName('eligible') // inProduction: true,
.setDescription('Checks eligibility for any Skilled clan') // inPublic: false,
.addStringOption((option) =>
option
.setName('clan')
.setDescription('The clan to check')
.setChoices(
{
name: SKILLED_CLANS.SKLLD,
value: 'SKLLD' satisfies SkilledClan,
},
{ name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan },
)
.setRequired(true),
)
.addStringOption(addUsernameChoices),
async handler(interaction) { // command: new SlashCommandBuilder()
const clan = interaction.options.getString('clan') as SkilledClan; // .setName('eligible')
const { id, region: region } = await resolvePlayerFromCommand(interaction); // .setDescription('Checks eligibility for any Skilled clan')
const accountInfo = await getAccountInfo(region, id); // .addStringOption((option) =>
const problems: TableInput = []; // option
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`; // .setName('clan')
let isEligible = false; // .setDescription('The clan to check')
let body = ''; // .setChoices(
// {
// name: SKILLED_CLANS.SKLLD,
// value: 'SKLLD' satisfies SkilledClan,
// },
// { name: SKILLED_CLANS.SMRI, value: 'SMRI' satisfies SkilledClan },
// )
// .setRequired(true),
// )
// .addStringOption(addUsernameChoices),
if (clan === 'SKLLD') { // async handler(interaction) {
const { diff: diffed } = await getStatsInPeriod( // const clan = interaction.options.getString('clan') as SkilledClan;
region, // const { id, region: region } = await resolvePlayerFromCommand(interaction);
id, // const accountInfo = await getAccountInfo(region, id);
getTimeDaysAgo(region, 30), // const problems: TableInput = [];
getPeriodNow(), // const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
); // let isEligible = false;
const entries = Object.entries(diffed); // let body = '';
const stats = sumStats(entries.map(([, stats]) => stats));
const averageTier =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankDefinition = (await tankDefinitions)[tankId];
if (!tankDefinition) return accumulator; // if (clan === 'SKLLD') {
// const { diff: diffed } = await getStatsInPeriod(
// region,
// id,
// getTimeDaysAgo(region, 30),
// getPeriodNow(),
// );
// const entries = Object.entries(diffed);
// const stats = sumStats(entries.map(([, stats]) => stats));
// const averageTier =
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
// const tankDefinition = (await tankDefinitions)[tankId];
const tankTier = tankDefinition.tier; // if (!tankDefinition) return accumulator;
return (await accumulator) + tankTier * stats.battles; // const tankTier = tankDefinition.tier;
},
0,
)) /
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankDefinition = (await tankDefinitions)[tankId];
if (!tankDefinition) return accumulator; // return (await accumulator) + tankTier * stats.battles;
// },
// 0,
// )) /
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
// const tankDefinition = (await tankDefinitions)[tankId];
return stats.battles; // if (!tankDefinition) return accumulator;
},
0,
));
const WN8 =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
// edge case where new tanks don't have averages // return stats.battles;
if ((await tankAverages)[tankId]) { // },
const tankWN8 = calculateWN8( // 0,
(await tankAverages)[tankId].all, // ));
stats, // const WN8 =
); // (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
if (isNaN(tankWN8)) return accumulator; // // edge case where new tanks don't have averages
return (await accumulator) + tankWN8 * stats.battles; // if ((await tankAverages)[tankId]) {
} else return accumulator; // const tankWN8 = calculateWN8(
}, // (await tankAverages)[tankId].all,
0, // stats,
)) / stats.battles; // );
const tier10Damage =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
if ((await tankDefinitions)[tankId]?.tier === 10) { // if (isNaN(tankWN8)) return accumulator;
const tankStats = diffed[tankId]; // return (await accumulator) + tankWN8 * stats.battles;
return (await accumulator) + tankStats.damage_dealt; // } else return accumulator;
} else return accumulator; // },
}, // 0,
0, // )) / stats.battles;
)) / // const tier10Damage =
(await entries.reduce<PossiblyPromise<number>>( // (await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => { // async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString); // const tankId = parseInt(tankIdString);
if ((await tankDefinitions)[tankId]?.tier === 10) { // if ((await tankDefinitions)[tankId]?.tier === 10) {
const tankStats = diffed[tankId]; // const tankStats = diffed[tankId];
return (await accumulator) + tankStats.battles; // return (await accumulator) + tankStats.damage_dealt;
} else return accumulator; // } else return accumulator;
}, // },
0, // 0,
)); // )) /
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
if (stats.battles < 126) { // if ((await tankDefinitions)[tankId]?.tier === 10) {
problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]); // const tankStats = diffed[tankId];
} // return (await accumulator) + tankStats.battles;
if (averageTier < 8) { // } else return accumulator;
problems.push([ // },
'Average tier', // 0,
`${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`, // ));
]);
}
if (stats.wins / stats.battles < 0.6) {
problems.push([
'Winrate',
`${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
100 *
(stats.wins / stats.battles - 0.6)
).toFixed(2)}%)`,
]);
}
if (WN8 < 2450) {
problems.push([
'WN8',
`${WN8.toFixed(0)} (${(WN8 - 2450).toFixed(0)})`,
]);
}
if (tier10Damage < 1250) {
problems.push([
'Tier 10 damage',
`${tier10Damage.toFixed(0)} (${tier10Damage - 1250})`,
]);
}
isEligible = problems.length === 0; // if (stats.battles < 126) {
body = isEligible // problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]);
? 'Player is fully eligible.' // }
: `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable( // if (averageTier < 8) {
problems, // problems.push([
)}`; // 'Average tier',
} else if (clan === 'SMRI') { // `${averageTier.toFixed(2)} (${(averageTier - 8).toFixed(2)})`,
const stats = accountInfo.statistics.all; // ]);
// }
// if (stats.wins / stats.battles < 0.6) {
// problems.push([
// 'Winrate',
// `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
// 100 *
// (stats.wins / stats.battles - 0.6)
// ).toFixed(2)}%)`,
// ]);
// }
// if (WN8 < 2450) {
// problems.push([
// 'WN8',
// `${WN8.toFixed(0)} (${(WN8 - 2450).toFixed(0)})`,
// ]);
// }
// if (tier10Damage < 1250) {
// problems.push([
// 'Tier 10 damage',
// `${tier10Damage.toFixed(0)} (${tier10Damage - 1250})`,
// ]);
// }
if (stats.battles < 1500) { // isEligible = problems.length === 0;
problems.push([ // body = isEligible
'Battles', // ? 'Player is fully eligible.'
`${stats.battles} (${stats.battles - 1500})`, // : `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable(
]); // problems,
} // )}`;
if (stats.wins / stats.battles <= 0.53) { // } else if (clan === 'SMRI') {
problems.push([ // const stats = accountInfo.statistics.all;
'Winrate',
`${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
100 *
(stats.wins / stats.battles - 0.53)
).toFixed(2)}%)`,
]);
}
isEligible = problems.length === 0; // if (stats.battles < 1500) {
body = isEligible // problems.push([
? 'Player is fully eligible.' // 'Battles',
: `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable( // `${stats.battles} (${stats.battles - 1500})`,
problems, // ]);
)}`; // }
} // if (stats.wins / stats.battles <= 0.53) {
// problems.push([
// 'Winrate',
// `${(100 * (stats.wins / stats.battles)).toFixed(2)}% (${(
// 100 *
// (stats.wins / stats.battles - 0.53)
// ).toFixed(2)}%)`,
// ]);
// }
return (isEligible ? embedPositive : embedNegative)(title, body); // isEligible = problems.length === 0;
}, // body = isEligible
}; // ? 'Player is fully eligible.'
// : `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable(
// problems,
// )}`;
// }
// return (isEligible ? embedPositive : embedNegative)(title, body);
// },
// };
+48 -52
View File
@@ -1,11 +1,8 @@
import { import { Locale, SlashCommandSubcommandGroupBuilder } from 'discord.js';
SlashCommandBuilder, import CommandWrapper from '../components/CommandWrapper';
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import * as Graph from '../components/Graph'; import * as Graph from '../components/Graph';
import NoData, { NoDataType } from '../components/NoData'; import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import resolveTankId from '../core/blitz/resolveTankId'; import resolveTankId from '../core/blitz/resolveTankId';
@@ -13,13 +10,14 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import getTankHistories from '../core/blitzkrieg/getTankHistories'; import getTankHistories from '../core/blitzkrieg/getTankHistories';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getPlayerHistories from '../core/blitzstars/getPlayerHistories'; import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
import addPeriodSubCommands from '../core/discord/addPeriodSubCommands'; import { addPeriodSubCommands } from '../core/discord/addPeriodSubCommands';
import addTankChoices from '../core/discord/addTankChoices'; import addTankChoices from '../core/discord/addTankChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary'; import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL'; import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton'; import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand, { import resolvePeriodFromCommand, {
@@ -29,7 +27,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, { import resolvePlayerFromCommand, {
ResolvedPlayer, ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
type EvolutionStatType = 'player' | 'tank'; type EvolutionStatType = 'player' | 'tank';
@@ -38,6 +36,7 @@ async function render(
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
type: 'player' | 'tank', type: 'player' | 'tank',
tankId: number | null, tankId: number | null,
locale: Locale,
) { ) {
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
@@ -49,7 +48,7 @@ async function render(
includeLatestHistories: true, includeLatestHistories: true,
includePreviousHistories: true, includePreviousHistories: true,
}) })
: getTankHistories(region, id, { : getTankHistories(region, id, locale, {
tankId: tankId!, tankId: tankId!,
start, start,
end, end,
@@ -74,8 +73,8 @@ async function render(
const minX = Math.min(...xs); const minX = Math.min(...xs);
return ( return (
<Wrapper> <CommandWrapper>
<TitleBar name={accountInfo.nickname} image={logo} description={name} /> <TitleBar title={accountInfo.nickname} image={logo} description={name} />
{plot.length > 0 && ( {plot.length > 0 && (
<Graph.Root <Graph.Root
@@ -90,8 +89,8 @@ async function render(
max: maxX, max: maxX,
precision: 0, precision: 0,
}} }}
xMinLabel={new Date(minTime * 1000).toDateString()} xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
xMaxLabel={new Date(maxTime * 1000).toDateString()} xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
> >
{Graph.Line({ {Graph.Line({
color: Graph.LineColor.Red, color: Graph.LineColor.Red,
@@ -102,47 +101,37 @@ async function render(
</Graph.Root> </Graph.Root>
)} )}
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />} {plot.length === 0 && <NoData type="battles_in_period" locale={locale} />}
</Wrapper> </CommandWrapper>
); );
} }
export const evolutionCommand = new Promise<CommandRegistryPromisable>( export const evolutionCommand = new Promise<CommandRegistry>(
async (resolve) => { async (resolve) => {
let playerGroup: SlashCommandSubcommandGroupBuilder;
let tankGroup: SlashCommandSubcommandGroupBuilder;
const command = new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics')
.addSubcommandGroup((option) => {
playerGroup = option
.setName('player')
.setDescription('Player statistics');
return playerGroup;
})
.addSubcommandGroup((option) => {
tankGroup = option.setName('tank').setDescription('Tank statistics');
return tankGroup;
});
await Promise.all([
addPeriodSubCommands(playerGroup!, (option) =>
option.addStringOption(addUsernameChoices),
),
addPeriodSubCommands(tankGroup!, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameChoices),
),
]);
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command, 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) { async handler(interaction) {
const commandGroup = const commandGroup =
@@ -151,18 +140,24 @@ export const evolutionCommand = new Promise<CommandRegistryPromisable>(
const period = resolvePeriodFromCommand(player.region, interaction); const period = resolvePeriodFromCommand(player.region, interaction);
const tankIdRaw = interaction.options.getString('tank')!; const tankIdRaw = interaction.options.getString('tank')!;
const tankId = const tankId =
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null; commandGroup === 'tank'
? await resolveTankId(tankIdRaw, interaction.locale)
: null;
const path = commandToURL(interaction, { const path = commandToURL(interaction, {
...player, ...player,
...getCustomPeriodParams(interaction), ...getCustomPeriodParams(interaction),
tankId, tankId,
}); });
getBlitzStarsLinkButton;
return [ return [
await render(player, period, commandGroup, tankId), await render(
buttonPrimary(path, 'Refresh'), player,
period,
commandGroup,
tankId,
interaction.locale,
),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton( await getBlitzStarsLinkButton(
player.region, player.region,
player.id, player.id,
@@ -188,6 +183,7 @@ export const evolutionCommand = new Promise<CommandRegistryPromisable>(
period, period,
commandGroup, commandGroup,
parseInt(url.searchParams.get('tankId') ?? '0') || null, parseInt(url.searchParams.get('tankId') ?? '0') || null,
interaction.locale,
); );
}, },
}); });
+32 -22
View File
@@ -1,9 +1,9 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import CommandWrapper from '../components/CommandWrapper';
import GenericAllStats from '../components/GenericAllStats'; import GenericAllStats from '../components/GenericAllStats';
import NoData, { NoDataType } from '../components/NoData'; import NoData from '../components/NoData';
import TierWeights from '../components/TierWeights'; import TierWeights from '../components/TierWeights';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
@@ -16,6 +16,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import { buttonRefresh } from '../core/discord/buttonRefresh'; import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL'; import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton'; import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand'; import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
@@ -29,55 +30,64 @@ import resolvePlayerFromCommand, {
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { getTierWeights } from '../core/statistics/getTierWeights'; import { getTierWeights } from '../core/statistics/getTierWeights';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
filters: StatFilters, filters: StatFilters,
locale: Locale,
) { ) {
const { nickname } = await getAccountInfo(region, id); const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined; const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(region, id, start, end); const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary, filteredOrder } = await filterStats( const { stats, supplementary, filteredOrder } = await filterStats(
diffedTankStats, diffedTankStats,
filters, filters,
); );
const filterDescriptions = await filtersToDescription(filters); const filterDescriptions = await filtersToDescription(filters, locale);
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder); const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
return ( return (
<Wrapper> <CommandWrapper>
<TitleBar <TitleBar
name={nickname} title={nickname}
image={clanImage} image={clanImage}
description={`${name} • ${filterDescriptions}`} description={`${name} • ${filterDescriptions}`}
/> />
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />} {stats.battles === 0 && (
<NoData type="battles_in_period" locale={locale} />
)}
{stats.battles > 0 && <TierWeights weights={tierWeights!} />} {stats.battles > 0 && <TierWeights weights={tierWeights!} />}
{stats.battles > 0 && ( {stats.battles > 0 && (
<GenericAllStats stats={stats} supplementaryStats={supplementary} /> <GenericAllStats
stats={stats}
supplementaryStats={supplementary}
locale={locale}
/>
)} )}
</Wrapper> </CommandWrapper>
); );
} }
export const fullStatsCommand = new Promise<CommandRegistryPromisable>( export const fullStatsCommand = new Promise<CommandRegistry>(
async (resolve) => { async (resolve) => {
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('full-stats')
.setDescription('Full in-game statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command, command: await addPeriodicFilterOptions(
createLocalizedCommand('full-stats'),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) { async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction); const player = await resolvePlayerFromCommand(interaction);
@@ -90,7 +100,7 @@ export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
}); });
return Promise.all([ return Promise.all([
render(player, period, filters), render(player, period, filters, interaction.locale),
buttonRefresh(interaction, path), buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id), getBlitzStarsLinkButton(player.region, player.id),
]); ]);
@@ -106,7 +116,7 @@ export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
const period = resolvePeriodFromButton(player.region, interaction); const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction); const filters = getFiltersFromButton(interaction);
return await render(player, period, filters); return await render(player, period, filters, interaction.locale);
}, },
}); });
}, },
+75 -53
View File
@@ -1,69 +1,91 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import CommandWrapper from '../components/CommandWrapper';
import GenericStats from '../components/GenericStats'; import GenericStats from '../components/GenericStats';
import NoData, { NoDataType } from '../components/NoData'; import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanInfo } from '../core/blitz/getClanInfo'; import { getClanInfo } from '../core/blitz/getClanInfo';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan'; 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 resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_THRESHOLD = 7; const DEFAULT_THRESHOLD = 7;
export const inactiveCommand: CommandRegistry = { export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('inactive')
.setName('inactive') .addStringOption(addClanChoices)
.setDescription('Lists all inactive players') .addNumberOption((option) => {
.addStringOption(addClanChoices) const { translate } = translator(Locale.EnglishUS);
.addNumberOption((option) =>
option return option
.setName('threshold') .setName('threshold')
.setDescription( .setNameLocalizations(
`The number of days inactive (default: ${DEFAULT_THRESHOLD})`, localizationObject('bot.commands.inactive.options.threshold'),
)
.setDescription(
translate('bot.commands.inactive.options.threshold.description'),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.inactive.options.threshold.description',
),
)
.setMinValue(0);
}),
async handler(interaction) {
const { translate } = 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],
) )
.setMinValue(0), .filter(([, inactiveDays]) => inactiveDays >= threshold)
), .sort((a, b) => b[1] - a[1])
.map(
([name, days]) =>
[
name,
translate('bot.commands.inactive.body.listing', [
days.toFixed(0),
]),
] as [string, string],
);
const hasInactiveMembers = inactive.length > 0;
async handler(interaction) { return (
const { region, id } = await resolveClanFromCommand(interaction); <CommandWrapper>
const threshold = <TitleBar
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD; title={clanInfo.name}
const time = new Date().getTime() / 1000; image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
const clanInfo = await getClanInfo(region, id); description={`${translate('bot.commands.inactive.body.subtitle', [`${threshold}`])} • ${new Date().toLocaleDateString(interaction.locale)}`}
const accountInfo = await getAccountInfo(region, clanInfo.members_ids); />
const inactive = accountInfo
.map( {!hasInactiveMembers && (
(account) => <NoData type="players_in_period" locale={interaction.locale} />
[ )}
account.nickname, {hasInactiveMembers && <GenericStats stats={inactive} />}
(time - account.last_battle_time) / 60 / 60 / 24, </CommandWrapper>
] as [string, number],
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
.map(
([name, days]) => [name, `${days.toFixed(0)} days`] as [string, string],
); );
const hasInactiveMembers = inactive.length > 0; },
return ( autocomplete: autocompleteClan,
<Wrapper> });
<TitleBar });
name={clanInfo.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
/>
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
{hasInactiveMembers && <GenericStats stats={inactive} />}
</Wrapper>
);
},
autocomplete: autocompleteClan,
};
+158 -147
View File
@@ -1,209 +1,220 @@
import { teal } from '@radix-ui/colors'; import { teal } from '@radix-ui/colors';
import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js'; import { GuildMemberRoleManager } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' }; import discord from '../../discord.json' assert { type: 'json' };
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow'; import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
import Wrapper from '../components/Wrapper'; import CommandWrapper from '../components/CommandWrapper';
import { REGION_NAMES } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz'; import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import embedNegative from '../core/discord/embedNegative'; import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
export const verifyCommand: CommandRegistry = { export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('link').addStringOption((option) =>
.setName('link') addUsernameChoices(option).setRequired(true),
.setDescription('Links your Blitz and Discord account') ),
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
async handler(interaction) { async handler(interaction) {
const { id, region } = await resolvePlayerFromCommand(interaction); const { t, translate } = translator(interaction.locale);
const discordId = parseInt(interaction.user.id); const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id); const discordId = parseInt(interaction.user.id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
await linkBlitzAndDiscord(discordId, region, id); await linkBlitzAndDiscord(discordId, region, id);
if (interaction.guildId === discord.sklld_guild_id) { if (interaction.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
return embedNegative( return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`, translate(translate('bot.commands.link.body.no_role_permissions'), [
"I don't have the permission to change your manage roles.", markdownEscape(interaction.user.username),
]),
translate(
translate(
'bot.commands.link.body.no_role_permissions.description',
),
),
);
}
await (interaction.member!.roles as GuildMemberRoleManager).remove(
discord.sklld_verify_role,
);
await (interaction.member!.roles as GuildMemberRoleManager).add(
discord.sklld_peasant_role,
); );
} }
await (interaction.member!.roles as GuildMemberRoleManager).remove( return [
discord.sklld_verify_role, <CommandWrapper fat>
);
await (interaction.member!.roles as GuildMemberRoleManager).add(
discord.sklld_peasant_role,
);
}
return [
<Wrapper fat>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'space-between',
gap: 16,
}} }}
> >
<Glow color={teal.teal9} rotation={90} />
<div <div
style={{ style={{
display: 'flex', display: 'flex',
gap: 8, flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 16,
}} }}
> >
<img
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
/>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', gap: 8,
gap: 4, alignItems: 'center',
justifyContent: 'center',
}} }}
> >
<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
src="https://i.imgur.com/jIcRgog.png"
style={{
width: 16,
height: 16,
}}
/>
<span style={{ fontSize: 16 }}>Accounts linked</span>
</div>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img <img
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{ style={{
width: 64, width: 64,
height: 64, height: 64,
borderRadius: 32, borderRadius: 32,
objectFit: 'cover', 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',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', alignItems: 'center',
justifyContent: 'center',
gap: 4, gap: 4,
color: theme.colors.textLowContrast_teal,
}} }}
> >
<span <img
src="https://i.imgur.com/jIcRgog.png"
style={{ style={{
fontSize: 32, width: 16,
color: theme.colors.textHighContrast, height: 16,
fontWeight: 900, }}
maxWidth: 240, />
overflow: 'hidden', <span
textOverflow: 'ellipsis', style={{ fontSize: 16 }}
whiteSpace: 'nowrap', >{t`bot.commands.link.body.accounts_linked`}</span>
</div>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
}}
>
{clanAccountInfo?.clan ? (
<img
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,
}} }}
> >
{accountInfo.nickname} <span
</span> style={{
<span fontSize: 32,
style={{ color: theme.colors.textHighContrast,
fontSize: 16, fontWeight: 900,
color: theme.colors.textLowContrast, maxWidth: 240,
}} overflow: 'hidden',
> textOverflow: 'ellipsis',
{clanAccountInfo?.clan whiteSpace: 'nowrap',
? `[${clanAccountInfo?.clan?.tag}] - ` }}
: ''} >
{REGION_NAMES[region]} {accountInfo.nickname}
</span> </span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] • `
: ''}
{translate(`common.regions.normal.${region}`)}
</span>
</div>
</div> </div>
</div> </div>
<Glow color={teal.teal9} rotation={-90} />
</div> </div>
</CommandWrapper>,
<Glow color={teal.teal9} rotation={-90} /> embedInfo(
</div> t`bot.commands.link.embed.title`,
</Wrapper>, t`bot.commands.link.embed.description`,
),
];
},
embedInfo( autocomplete: autocompleteUsername,
'Incorrect account?', });
'Use the command again and select a username from the search results.', });
),
];
},
autocomplete: autocompleteUsername,
};
+104 -99
View File
@@ -1,9 +1,7 @@
import { SlashCommandBuilder } from 'discord.js'; import CommandWrapper from '../components/CommandWrapper';
import NoData, { NoDataType } from '../components/NoData'; import NoData from '../components/NoData';
import * as Tanks from '../components/Tanks'; import * as Tanks from '../components/Tanks';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats'; import getTankStats from '../core/blitz/getTankStats';
@@ -18,111 +16,118 @@ import { tankIcon } from '../core/blitzkrieg/tankIcon';
import addTierChoices from '../core/discord/addTierChoices'; import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const ownedTanksCommand: CommandRegistry = { export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('owned-tanks')
.setName('owned-tanks') .addStringOption(addTierChoices)
.setDescription("Shows a player's owned tanks") .addStringOption(addUsernameChoices),
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const tier = Number(interaction.options.getString('tier')); const { translate } = translator(interaction.locale);
const account = await resolvePlayerFromCommand(interaction); const tier = Number(interaction.options.getString('tier'));
const { id, region: server } = account; const account = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(server, id); const { id, region: server } = account;
const tankStats = await getTankStats(server, id); const accountInfo = await getAccountInfo(server, id);
const filteredTanks = ( const tankStats = await getTankStats(server, id, interaction.locale);
await Promise.all( const filteredTanks = (
tankStats.map(async (tankData) => ({ await Promise.all(
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!, tankStats.map(async (tankData) => ({
id: tankData.tank_id, tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
})), id: tankData.tank_id,
) })),
).filter((tank) => tank.tankDefinitions?.tier === tier); )
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']); ).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {}; const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
const nations: string[] = []; const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = [];
filteredTanks.forEach((tank) => { filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankDefinitions.nation] === undefined) { if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions]; groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation); nations.push(tank.tankDefinitions.nation);
} else { } else {
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions); groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
} }
}); });
nations.sort(); nations.sort();
return ( return (
<Wrapper> <CommandWrapper>
<TitleBar <TitleBar
name={accountInfo.nickname} title={accountInfo.nickname}
image={ image={
clanAccountInfo?.clan clanAccountInfo?.clan
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id) ? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
: undefined : undefined
} }
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`} description={translate('bot.commands.owned_tanks.body.subtitle', [
/> TIER_ROMAN_NUMERALS[tier as Tier],
])}
/>
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />} {filteredTanks.length === 0 && (
<NoData type="tanks_found" locale={interaction.locale} />
)}
{filteredTanks.length > 0 && {filteredTanks.length > 0 &&
(await Promise.all( (await Promise.all(
nations.map(async (nation) => { nations.map(async (nation) => {
const tanks = groupedTanks[nation]; const tanks = groupedTanks[nation];
const leftColumnSize = Math.ceil(tanks.length / 2); const leftColumnSize = Math.ceil(tanks.length / 2);
const leftColumn = tanks.slice(0, leftColumnSize); const leftColumn = tanks.slice(0, leftColumnSize);
const rightColumn = tanks.slice(leftColumnSize); const rightColumn = tanks.slice(leftColumnSize);
return ( return (
<Tanks.Root> <Tanks.Root key={nation}>
<Tanks.Title> <Tanks.Title>
{(await encyclopediaInfo).vehicle_nations[nation]} {translate(`common.nations.${nation}`)}
</Tanks.Title> </Tanks.Title>
<Tanks.Row> <Tanks.Row>
<Tanks.Column> <Tanks.Column>
{await Promise.all( {await Promise.all(
leftColumn.map(async (tank) => ( leftColumn.map(async (tank) => (
<Tanks.Item <Tanks.Item
key={tank.id} key={tank.id}
name={tank.name} name={tank.name}
tankType={tank.type} tankType={tank.class}
image={tankIcon(tank.id)} image={tankIcon(tank.id)}
treeType={tank.treeType} treeType={tank.treeType}
/> />
)), )),
)} )}
</Tanks.Column> </Tanks.Column>
<Tanks.Column> <Tanks.Column>
{await Promise.all( {await Promise.all(
rightColumn.map(async (tank) => ( rightColumn.map(async (tank) => (
<Tanks.Item <Tanks.Item
key={tank.id} key={tank.id}
name={tank.name} name={tank.name}
tankType={tank.type} tankType={tank.class}
image={tankIcon(tank.id)} image={tankIcon(tank.id)}
treeType={tank.treeType} treeType={tank.treeType}
/> />
)), )),
)} )}
</Tanks.Column> </Tanks.Column>
</Tanks.Row> </Tanks.Row>
</Tanks.Root> </Tanks.Root>
); );
}), }),
))} ))}
</Wrapper> </CommandWrapper>
); );
}, },
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
}; });
});
+39 -37
View File
@@ -1,42 +1,44 @@
import { SlashCommandBuilder } from 'discord.js'; import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const permissionsCommand: CommandRegistry = { export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('permissions'),
.setName('permissions')
.setDescription('Checks wether the bot has the required permissions'),
async handler(interaction) { async handler(interaction) {
// friday the 13th permissions easter egg const { t, translate } = translator(interaction.locale);
const isFridayThe13th = // friday the 13th permissions easter egg
new Date().getDay() === 5 && new Date().getDate() === 13; const isFridayThe13th =
const permissions = [ new Date().getDay() === 5 && new Date().getDate() === 13;
[ const permissions = [
interaction.appPermissions?.has('ViewChannel'), [
'View channels: needed for the refresh button to work', interaction.appPermissions?.has('ViewChannel'),
], t`bot.commands.permissions.body.view_channels`,
[ ],
interaction.appPermissions?.has('ReadMessageHistory'), [
'Read message history: also needed for the refresh button to work', interaction.appPermissions?.has('ReadMessageHistory'),
], t`bot.commands.permissions.body.read_message_history`,
[ ],
interaction.appPermissions?.has('AttachFiles'), [
'Attach files: needed for image-based commands to work', interaction.appPermissions?.has('AttachFiles'),
], t`bot.commands.permissions.body.attach_files`,
]; ],
const body = permissions ];
.map(([hasPermission, description]) => { const body = permissions
if (hasPermission) { .map(([hasPermission, description]) => {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`; if (hasPermission) {
} else { return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
return `${isFridayThe13th ? '💀' : '❌'} ${description}`; } else {
} return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
}) }
.join('\n'); })
.join('\n');
return `# Permissions\n${body}`; return translate('bot.commands.permissions.body.title', [body]);
}, },
}; });
});
+29 -26
View File
@@ -1,32 +1,35 @@
import { SlashCommandBuilder } from 'discord.js'; import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const pingCommand: CommandRegistry = { export const pingCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
handlesInteraction: true, inPublic: true,
handlesInteraction: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('ping', [
.setName('ping') { subcommand: 'blitzkrieg' },
.setDescription('Check if the bot is alive') { subcommand: 'wotb' },
.addSubcommand((option) => ]),
option.setName('blitzkrieg').setDescription('Ping Blitzkrieg'),
)
.addSubcommand((option) =>
option.setName('blitz').setDescription('Ping Blitz'),
),
async handler(interaction) { async handler(interaction) {
const subcommand = interaction.options.getSubcommand(); const { t, translate } = translator(interaction.locale);
const executionStart = Date.now(); const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === 'blitzkrieg') { if (subcommand === 'blitzkrieg') {
await interaction.editReply('Pong 🏓'); await interaction.editReply(t`bot.commands.ping.body.pong`);
} else { } else {
await fetch('https://api.wotblitz.com/'); await fetch('https://api.wotblitz.com/');
} }
const executionTime = Date.now() - executionStart; const executionTime = Date.now() - executionStart;
interaction.editReply(`Pong 🏓 - ${executionTime}ms`); interaction.editReply(
}, translate('bot.commands.ping.body.pong_with_time', [
}; `${Math.round(executionTime)}`,
]),
);
},
});
});
+73 -61
View File
@@ -1,68 +1,80 @@
import { SlashCommandBuilder } from 'discord.js'; // _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
import { startCase } from 'lodash'; // |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
import markdownEscape from 'markdown-escape'; // | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
import { getAccountAchievements } from '../core/blitz/getAccountAchievements'; // | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
import { getAccountInfo } from '../core/blitz/getAccountInfo'; // _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
import addUsernameChoices from '../core/discord/addUsernameChoices'; // |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo';
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate';
type SortBy = 'name' | 'count'; // import { SlashCommandBuilder } from 'discord.js';
// import { startCase } from 'lodash';
// import markdownEscape from 'markdown-escape';
// import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
// import addUsernameChoices from '../core/discord/addUsernameChoices';
// import autocompleteUsername from '../core/discord/autocompleteUsername';
// import embedInfo from '../core/discord/embedInfo';
// import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
// import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
// import { CommandRegistry } from '../events/interactionCreate';
export const playerAchievementsCommand: CommandRegistry = { // type SortBy = 'name' | 'count';
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() // export const playerAchievementsCommand = new Promise<CommandRegistry>(
.setName('player-achievements') // (resolve) => {
.setDescription("All the player's achievements") // resolve({
.addStringOption(addUsernameChoices) // inProduction: true,
.addStringOption((option) => // inPublic: true,
option
.setName('sort')
.setDescription('What to sort by (default: name)')
.addChoices(
{ name: 'By name', value: 'name' satisfies SortBy },
{ name: 'By count', value: 'count' satisfies SortBy },
),
),
async handler(interaction) { // command: new SlashCommandBuilder()
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy; // .setName('player-achievements')
const account = await resolvePlayerFromCommand(interaction); // .setDescription("All the player's achievements")
const { id, region: region } = account; // .addStringOption(addUsernameChoices)
const accountInfo = await getAccountInfo(region, id); // .addStringOption((option) =>
const accountsAchievements = await getAccountAchievements(region, id); // option
const accountAchievements = accountsAchievements; // .setName('sort')
const compound = { // .setDescription('What to sort by (default: name)')
...accountAchievements.achievements, // .addChoices(
...accountAchievements.max_series, // { name: 'By name', value: 'name' satisfies SortBy },
}; // { name: 'By count', value: 'count' satisfies SortBy },
// ),
// ),
return embedInfo( // async handler(interaction) {
`${markdownEscape(accountInfo.nickname)}'s information`, // const sortBy = (interaction.options.getString('sort') ??
// 'name') as SortBy;
// const account = await resolvePlayerFromCommand(interaction);
// const { id, region: region } = account;
// const accountInfo = await getAccountInfo(region, id);
// const accountsAchievements = await getAccountAchievements(region, id);
// const accountAchievements = accountsAchievements;
// const compound = {
// ...accountAchievements.achievements,
// ...accountAchievements.max_series,
// };
markdownTable( // return embedInfo(
Object.keys(compound) // `${markdownEscape(accountInfo.nickname)}'s information`,
.filter((achievement) => compound[achievement] > 0)
.sort(
sortBy === 'count'
? (a, b) => compound[b] - compound[a]
: undefined,
)
.map(
(achievement) =>
[
startCase(achievement),
compound[achievement],
] as TableInputEntry,
),
),
);
},
autocomplete: autocompleteUsername, // markdownTable(
}; // Object.keys(compound)
// .filter((achievement) => compound[achievement] > 0)
// .sort(
// sortBy === 'count'
// ? (a, b) => compound[b] - compound[a]
// : undefined,
// )
// .map(
// (achievement) =>
// [
// startCase(achievement),
// compound[achievement],
// ] as TableInputEntry,
// ),
// ),
// );
// },
// autocomplete: autocompleteUsername,
// });
// },
// );
+57 -40
View File
@@ -1,54 +1,71 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable'; import markdownTable from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const playerInfoCommand: CommandRegistry = { export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, resolve({
inPublic: true, inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() command:
.setName('player-info') createLocalizedCommand('player-info').addStringOption(addUsernameChoices),
.setDescription('Basic information about a player')
.addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction); const { t, translate } = translator(interaction.locale);
const { id, region: region } = account; const account = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id); const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
return embedInfo( return embedInfo(
`${markdownEscape(accountInfo.nickname)}'s information`, translate('bot.commands.player_info.body.title', [
markdownEscape(accountInfo.nickname),
]),
markdownTable([ markdownTable([
['Nickname', `${accountInfo.nickname}`], [
['Battles', `${accountInfo.statistics.all.battles}`], t`bot.commands.player_info.body.nickname`,
[ `${accountInfo.nickname}`,
'Winrate', ],
`${( [
100 * t`bot.commands.player_info.body.battles`,
(accountInfo.statistics.all.wins / `${accountInfo.statistics.all.battles}`,
accountInfo.statistics.all.battles) ],
).toFixed(2)}%`, [
], t`bot.commands.player_info.body.winrate`,
[], `${(
['Account ID', `${accountInfo.account_id}`], 100 *
[ (accountInfo.statistics.all.wins /
'Created', accountInfo.statistics.all.battles)
`${new Date(accountInfo.created_at * 1000).toDateString()}`, ).toFixed(2)}%`,
], ],
[ [],
'Last battle', [
`${new Date(accountInfo.last_battle_time * 1000).toDateString()}`, t`bot.commands.player_info.body.account_id`,
], `${accountInfo.account_id}`,
]), ],
); [
}, t`bot.commands.player_info.body.created`,
new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale,
),
],
[
t`bot.commands.player_info.body.last_battle`,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale,
),
],
]),
);
},
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
}; });
});
+353 -303
View File
@@ -1,17 +1,17 @@
import { import {
APIApplicationCommandOptionChoice, APIApplicationCommandOptionChoice,
SlashCommandBuilder, Locale,
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
} from 'discord.js'; } from 'discord.js';
import { range } from 'lodash'; import { range } from 'lodash';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import CommandWrapper from '../components/CommandWrapper';
import * as Leaderboard from '../components/Leaderboard'; import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { LEAGUES } from '../constants/leagues'; import { LEAGUES } from '../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings'; import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings';
import { REGION_NAMES_SHORT, Region } from '../constants/regions'; import { Region } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo';
@@ -25,9 +25,11 @@ import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchive
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative'; import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { UserError } from '../hooks/userError'; import { UserError } from '../hooks/userError';
export interface RatingsPlayer { export interface RatingsPlayer {
@@ -117,339 +119,387 @@ export interface RatingsNeighbors {
neighbors: RatingsPlayer[]; neighbors: RatingsPlayer[];
} }
const noOngoingSeason = embedNegative( interface SimplifiedPlayer {
'No ongoing season', id: number;
"Wargaming didn't provide any data for this season.", score: number;
); position: number;
clan: string | undefined;
nickname: string;
}
export const ratingsCommand = new Promise<CommandRegistryPromisable>( const DEFAULT_LIMIT = 10;
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingsSeason();
const seasonNumbers = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
function addSubcommands(option: SlashCommandSubcommandBuilder) { export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
return option const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
.addStringOption(addUsernameChoices) const onGoingSeason = await isOnGoingRatingsSeason();
.addIntegerOption((option) => const seasonNumbers = range(
option FIRST_ARCHIVED_RATINGS_SEASON,
.setName('limit') latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
.setDescription('How many neighbors to display (default: 10)') );
.setRequired(false)
.setMinValue(5) function addOptions(option: SlashCommandSubcommandBuilder) {
.setMaxValue(30), const { t, translate } = translator(Locale.EnglishUS);
)
.addStringOption((option) => return option
option .addIntegerOption((option) =>
.setName('season') option
.setDescription('Season number') .setName(t`bot.commands.ratings.options.limit`)
.setRequired(false) .setNameLocalizations(
.addChoices( localizationObject('bot.commands.ratings.options.limit'),
...seasonNumbers )
.map( .setDescription(
(number, index) => translate('bot.commands.ratings.options.limit.description', [
({ `${DEFAULT_LIMIT}`,
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`, ]),
value: `${number}`, )
}) satisfies APIApplicationCommandOptionChoice<string>, .setDescriptionLocalizations(
) localizationObject(
.reverse(), 'bot.commands.ratings.options.limit.description',
[`${DEFAULT_LIMIT}`],
), ),
); )
} .setRequired(false)
.setMinValue(5)
const command = new SlashCommandBuilder() .setMaxValue(30),
.setName('ratings')
.setDescription('Ratings battles statistics')
.addSubcommand((option) =>
addSubcommands(
option
.setName('neighbors')
.setDescription('Shows neighboring players on the leaderboard'),
),
) )
.addSubcommand((option) => .addStringOption((option) =>
addSubcommands( option
.setName(t`bot.commands.ratings.options.season`)
.setNameLocalizations(
localizationObject('bot.commands.ratings.options.season'),
)
.setDescription(
translate('bot.commands.ratings.options.season.description'),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.ratings.options.season.description',
),
)
.setRequired(false)
.addChoices(
...seasonNumbers
.map((number, index) => {
const current =
index === seasonNumbers.length - 1 && onGoingSeason;
return {
name: current
? translate(
'bot.commands.ratings.options.season.choices.current',
[`${number}`],
)
: `${number}`,
value: `${number}`,
name_localizations: current
? localizationObject(
'bot.commands.ratings.options.season.choices.current',
[`${number}`],
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse(),
),
);
}
resolve({
inProduction: true,
inPublic: true,
command: createLocalizedCommand('ratings', [
{
subcommand: 'neighbors',
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: 'league',
modify(option: SlashCommandSubcommandBuilder) {
option option
.setName('league') .addStringOption((option) => {
.setDescription('Shows top players from a league') const { t, translate } = translator(Locale.EnglishUS);
.addStringOption(addRegionChoices)
.addStringOption((option) => return option
option .setName(t`bot.commands.ratings.options.league`)
.setName('league') .setNameLocalizations(
.setDescription('The league to display') localizationObject('bot.commands.ratings.options.league'),
)
.setDescription(
translate('bot.commands.ratings.options.league.description'),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.ratings.options.league.description',
),
)
.addChoices( .addChoices(
...LEAGUES.map( ...LEAGUES.map(
({ name }, value) => ({ name }, value) =>
({ ({
name, name: translate(`common.leagues.${name}`),
value: `${value}`, value: `${value}`,
name_localizations: localizationObject(
`common.leagues.${name}`,
),
}) satisfies APIApplicationCommandOptionChoice<string>, }) satisfies APIApplicationCommandOptionChoice<string>,
), ),
) )
.setRequired(true), .setRequired(true);
), })
), .addStringOption(addRegionChoices);
); addOptions(option);
},
},
]),
resolve({ // BIG TODO: title seems to be in english
inProduction: true,
inPublic: true,
command, async handler(interaction) {
const { t, translate } = 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 midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
let playerId: number | undefined;
let regionRatingsInfo: RatingsInfo;
async handler(interaction) { if (subcommand === 'league') {
interface SimplifiedPlayer { const leagueIndex = parseInt(interaction.options.getString('league')!);
id: number; const region = interaction.options.getString('region') as Region;
score: number; regionRatingsInfo = await (isCurrentSeason
position: number; ? getRatingsInfo(region)
clan: string | undefined; : getArchivedRatingsInfo(region, season!));
nickname: string;
}
const subcommand = interaction.options.getSubcommand(true) as if (regionRatingsInfo.detail !== undefined)
| 'league' return t`bot.commands.ratings.body.no_ongoing_season`;
| 'neighbors';
const limit = interaction.options.getInteger('limit') ?? 10;
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 playersBefore: number;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
let playerId: number | undefined;
let regionRatingsInfo: RatingsInfo;
if (subcommand === 'league') { const result = isCurrentSeason
const leagueIndex = parseInt( ? await getRatingsLeague(region, leagueIndex).then(async (result) =>
interaction.options.getString('league')!, result.slice(0, limit).map(
); (player) =>
const region = interaction.options.getString('region') as Region; ({
regionRatingsInfo = await (isCurrentSeason id: player.spa_id,
? getRatingsInfo(region) score: player.score,
: getArchivedRatingsInfo(region, season!)); position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
);
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; 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 ??
translate('bot.commands.ratings.body.deleted_player', [
`${player.id}`,
]),
}) satisfies SimplifiedPlayer,
);
},
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
const result = isCurrentSeason players = result;
? await getRatingsLeague(region, leagueIndex).then(async (result) => playersAfter =
result.slice(0, limit).map( regionRatingsInfo.count - result[result.length - 1].position;
titleName = `${
leagueInfo
? translate(`common.leagues.${LEAGUES[leagueInfo.index].name}`)
: ''
} - ${translate(`common.regions.short.${region}`)}`;
titleImage = leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`;
titleDescription = translate('bot.commands.ratings.body.top_players', [
`${limit}`,
]);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo = isCurrentSeason
? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season!);
if (regionRatingsInfo.detail !== undefined)
return t`bot.commands.ratings.body.no_ongoing_season`;
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors = isCurrentSeason
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
players.neighbors.map(
(player) => (player) =>
({ ({
id: player.spa_id, id: player.spa_id,
score: player.score,
position: player.number, position: player.number,
score: player.score,
clan: player.clan_tag, clan: player.clan_tag,
nickname: player.nickname, nickname: player.nickname,
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer,
), ),
) )
: await getArchivedRatingsLeaderboard(region, season!).then( : await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => { async (players) => {
const firstPlayerIndex = const playerIndex = players.findIndex(
leagueIndex === 0 (player) => player.id === id,
? 0 );
: players.findIndex(
(player) => if (playerIndex === -1) {
player.score < LEAGUES[leagueIndex - 1].minScore, throw new UserError(
); translate('bot.commands.ratings.body.no_participation', [
const lastPlayerIndex = firstPlayerIndex + limit - 1; markdownEscape(accountInfo.nickname),
const trimmed = players.slice( `${season}`,
firstPlayerIndex, ]),
lastPlayerIndex + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
); );
}
return trimmed.map( const halfRange = Math.round(limit / 2);
(player, index) => const trimmed = players.slice(
({ playerIndex - halfRange,
id: player.id, playerIndex + halfRange + 1,
score: player.score, );
position: firstPlayerIndex + index + 1, const clanData = await getClanAccountInfo(
clan: clanData[player.id]?.clan?.tag, region,
nickname: trimmed.map((player) => player.id),
clanData[index]?.account_name ?? ['clan'],
`Deleted ${player.id}`, );
}) satisfies SimplifiedPlayer,
);
},
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
players = result; return trimmed.map((player, index) => ({
playersBefore = result ? result[0].position - 1 : 0; id: player.id,
playersAfter = score: player.score,
regionRatingsInfo.count - result[result.length - 1].position; position: playerIndex + index + 1,
titleName = `${ clan: clanData[index]?.clan?.tag,
leagueInfo ? leagueInfo.title : 'No Ongoing Season' nickname: clanData[index]
} - ${REGION_NAMES_SHORT[region]}`; ? clanData[index]!.account_name
titleImage = leagueInfo.big_icon.startsWith('http') : `Deleted ${player.id}`,
? leagueInfo.big_icon }));
: `https:${leagueInfo.big_icon}`; },
titleDescription = `Top ${limit} players`; );
} else { midnightLeaderboard = isCurrentSeason
const { region, id } = await resolvePlayerFromCommand(interaction); ? await getArchivedRatingsMidnightLeaderboard(
regionRatingsInfo = isCurrentSeason region,
? await getRatingsInfo(region) regionRatingsInfo.current_season,
: await getArchivedRatingsInfo(region, season!); )
: await getArchivedRatingsMidnightLeaderboard(region, season!);
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; 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 = t`bot.commands.ratings.body.neighbors`;
playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
playerId = id;
}
const accountInfo = await getAccountInfo(region, id); const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; detail: undefined;
const neighbors = isCurrentSeason } = regionRatingsInfo;
? await getRatingsNeighbors(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 getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) { const items = players.map((player) => {
throw new UserError( const reward = resolvedRegionRatingsInfo.rewards.find(
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`, (reward) =>
); player.position >= reward.from_position &&
} player.position <= reward.to_position,
);
const halfRange = Math.round(limit / 2); const midnightIndex = midnightLeaderboard?.findIndex(
const trimmed = players.slice( (item) => item.id === player.id,
playerIndex - halfRange, );
playerIndex + halfRange + 1, const midnightScore =
); midnightIndex === undefined
const clanData = await getClanAccountInfo( ? player.score
region, : midnightLeaderboard![midnightIndex].score;
trimmed.map((player) => player.id), const midnightPosition =
['clan'], midnightIndex === undefined ? player.position : midnightIndex + 1;
);
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}`,
}));
},
);
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(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
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbors';
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
playerId = id;
}
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
detail: undefined;
} = regionRatingsInfo;
const items = players.map((player) => {
const reward = resolvedRegionRatingsInfo.rewards.find(
(reward) =>
player.position >= reward.from_position &&
player.position <= reward.to_position,
);
const midnightIndex = midnightLeaderboard?.findIndex(
(item) => item.id === player.id,
);
const midnightScore =
midnightIndex === undefined
? player.score
: midnightLeaderboard![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}
reward={reward}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return ( return (
<Wrapper> <Leaderboard.Item
<TitleBar nickname={player.nickname}
name={titleName} score={player.score}
image={titleImage} position={player.position}
description={titleDescription} clan={player.clan}
/> reward={reward}
deltaScore={player.score - midnightScore}
{items && ( deltaPosition={midnightPosition - player.position}
<Leaderboard.Root> key={player.id}
{playersBefore > 0 && ( highlight={player.id === playerId}
<Leaderboard.Gap />
message={`+ ${playersBefore.toLocaleString()} more`}
/>
)}
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={`+ ${playersAfter.toLocaleString()} more`}
/>
)}
</Leaderboard.Root>
)}
</Wrapper>
); );
}, });
autocomplete: autocompleteUsername, return (
}); <CommandWrapper>
}, <TitleBar
); title={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={translate('bot.commands.ratings.body.more', [
`${playersAfter.toLocaleString(interaction.locale)}`,
])}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
);
},
autocomplete: autocompleteUsername,
});
});
+60 -34
View File
@@ -1,46 +1,72 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions'; import { Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchBlitz'; import fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { ClanList } from '../types/clanList'; import { ClanList } from '../types/clanList';
export const searchClansCommand: CommandRegistry = { const DEFAULT_LIMIT = 25;
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
.setName('search-clans') resolve({
.setDescription('Search clans') inProduction: true,
.addStringOption(addRegionChoices) inPublic: true,
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('The size of the search result (default: 25)')
.setMinValue(1)
.setMaxValue(100),
),
async handler(interaction) { command: createLocalizedCommand('search-clans')
const server = interaction.options.getString('region') as Region; .addStringOption(addRegionChoices)
const clan = interaction.options.getString('clan')!; .addStringOption((option) =>
const limit = interaction.options.getInteger('limit') ?? 25; addClanChoices(option).setAutocomplete(false),
const clanList = await fetchBlitz<ClanList>( )
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`, .addIntegerOption((option) => {
); const { t, translate } = translator(Locale.EnglishUS);
return embedInfo( return option
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`, .setName(t`bot.commands.search_clans.options.limit`)
clanList.length === 0 .setNameLocalizations(
? 'No clans found.' localizationObject('bot.commands.search_clans.options.limit'),
: `\`\`\`\n${clanList )
.map((clan) => `${clan.name} [${clan.tag}]`) .setDescription(
.join('\n')}\n\`\`\``, translate('bot.commands.search_clans.options.limit.description', [
); `${DEFAULT_LIMIT}`,
}, ]),
}; )
.setDescriptionLocalizations(
localizationObject(
'bot.commands.search_clans.options.limit.description',
[`${DEFAULT_LIMIT}`],
),
)
.setMinValue(1)
.setMaxValue(100);
}),
async handler(interaction) {
const { translate } = translator(interaction.locale);
const server = 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>(
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
);
return embedInfo(
translate('bot.commands.search_clans.body.title', [
markdownEscape(clan),
translate(`common.regions.normal.${server}`),
]),
clanList.length === 0
? translate('bot.commands.search_clans.body.no_results')
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join('\n')}\n\`\`\``,
);
},
});
});
+65 -41
View File
@@ -1,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions'; import { Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchBlitz'; import fetchBlitz from '../core/blitz/fetchBlitz';
import { import {
@@ -9,48 +9,72 @@ import {
} from '../core/blitz/searchPlayersAcrossRegions'; } from '../core/blitz/searchPlayersAcrossRegions';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const searchPlayersCommand: CommandRegistry = { const DEFAULT_LIMIT = 25;
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder() export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
.setName('search-players') const { t, translate } = translator(Locale.EnglishUS);
.setDescription('Search players in a Blitz server')
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('The size of the search result (default: 25)')
.setMinValue(1)
.setMaxValue(100),
),
async handler(interaction) { resolve({
const server = interaction.options.getString('region') as Region; inProduction: true,
const name = interaction.options.getString('username')!; inPublic: true,
const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
trimmedSearch,
)}&limit=${limit}`,
)
: [];
return embedInfo( command: createLocalizedCommand('search-players')
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`, .addStringOption(addRegionChoices)
`\`\`\`${ .addStringOption((option) =>
players.length === 0 addUsernameChoices(option).setAutocomplete(false).setRequired(true),
? 'No players found.' )
: players.map((player) => player.nickname).join('\n') .addIntegerOption((option) =>
}\`\`\``, option
); .setName(t`bot.commands.search_players.options.limit`)
}, .setNameLocalizations(
}; localizationObject('bot.commands.search_players.options.limit'),
)
.setDescription(
translate('bot.commands.search_players.options.limit.description', [
`${DEFAULT_LIMIT}`,
]),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.search_players.options.limit.description',
[`${DEFAULT_LIMIT}`],
),
)
.setMinValue(1)
.setMaxValue(100),
),
async handler(interaction) {
const { translate } = translator(interaction.locale);
const server = 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>(
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
trimmedSearch,
)}&limit=${limit}`,
)
: [];
return embedInfo(
translate('bot.commands.search_players.body.title', [
markdownEscape(trimmedSearch),
translate(`common.regions.normal.${server}`),
]),
`\`\`\`${
players.length === 0
? translate('bot.commands.search_players.body.no_results')
: players.map((player) => player.nickname).join('\n')
}\`\`\``,
);
},
});
});
+47 -36
View File
@@ -1,40 +1,51 @@
import { SlashCommandBuilder } from 'discord.js'; // _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
import { go } from 'fuzzysort'; // |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
import markdownEscape from 'markdown-escape'; // | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
import { tankNames } from '../core/blitzkrieg/tankDefinitions'; // | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
import addTankChoices from '../core/discord/addTankChoices'; // _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
import embedInfo from '../core/discord/embedInfo'; // |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
import { CommandRegistry } from '../events/interactionCreate';
export const searchTanksCommand: CommandRegistry = { // import { SlashCommandBuilder } from 'discord.js';
inProduction: true, // import { go } from 'fuzzysort';
inPublic: true, // import markdownEscape from 'markdown-escape';
// import { tankNames } from '../core/blitzkrieg/tankDefinitions';
// import addTankChoices from '../core/discord/addTankChoices';
// import embedInfo from '../core/discord/embedInfo';
// import { CommandRegistry } from '../events/interactionCreate';
command: new SlashCommandBuilder() // export const searchTanksCommand = new Promise<CommandRegistry>((resolve) => {
.setName('search-tanks') // resolve({
.setDescription('Search tanks') // inProduction: true,
.addStringOption((option) => addTankChoices(option).setAutocomplete(false)) // inPublic: true,
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('The size of the search result (default: 25)')
.setMinValue(1)
.setMaxValue(100),
),
async handler(interaction) { // command: new SlashCommandBuilder()
const tank = interaction.options.getString('tank')!; // .setName('search-tanks')
const limit = interaction.options.getInteger('limit') ?? 25; // .setDescription('Search tanks')
const results = go(tank, await tankNames, { // .addStringOption((option) =>
limit, // addTankChoices(option).setAutocomplete(false),
keys: ['combined'], // )
}).map((result) => result.obj.original); // .addIntegerOption((option) =>
// option
// .setName('limit')
// .setDescription('The size of the search result (default: 25)')
// .setMinValue(1)
// .setMaxValue(100),
// ),
return embedInfo( // async handler(interaction) {
`Tank search for "${markdownEscape(tank)}"`, // const tank = interaction.options.getString('tank')!;
results.length === 0 // const limit = interaction.options.getInteger('limit') ?? 25;
? 'No tanks found.' // const results = go(tank, await tankNames, {
: `\`\`\`\n${results.join('\n')}\n\`\`\``, // limit,
); // keys: ['combined'],
}, // }).map((result) => result.obj.original);
};
// return embedInfo(
// `Tank search for "${markdownEscape(tank)}"`,
// results.length === 0
// ? 'No tanks found.'
// : `\`\`\`\n${results.join('\n')}\n\`\`\``,
// );
// },
// });
// });
+58 -52
View File
@@ -1,8 +1,8 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import AllStatsOverview from '../components/AllStatsOverview'; import AllStatsOverview from '../components/AllStatsOverview';
import NoData, { NoDataType } from '../components/NoData'; import CommandWrapper from '../components/CommandWrapper';
import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
@@ -15,6 +15,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import { buttonRefresh } from '../core/discord/buttonRefresh'; import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL'; import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton'; import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand'; import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
@@ -27,80 +28,85 @@ import resolvePlayerFromCommand, {
ResolvedPlayer, ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
filters: StatFilters, filters: StatFilters,
locale: Locale,
) { ) {
const { nickname } = await getAccountInfo(region, id); const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined; const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
const diffedTankStats = await getStatsInPeriod(region, id, start, end); const diffedTankStats = await getStatsInPeriod(
region,
id,
start,
end,
locale,
);
const { stats, supplementary } = await filterStats(diffedTankStats, filters); const { stats, supplementary } = await filterStats(diffedTankStats, filters);
const filterDescriptions = await filtersToDescription(filters); const filterDescriptions = await filtersToDescription(filters, locale);
return ( return (
<Wrapper> <CommandWrapper>
<TitleBar <TitleBar
name={nickname} title={nickname}
image={clanImage} image={clanImage}
description={`${name} • ${filterDescriptions}`} description={`${name} • ${filterDescriptions}`}
/> />
{!stats.battles && <NoData type={NoDataType.BattlesInPeriod} />} {!stats.battles && <NoData type="battles_in_period" locale={locale} />}
{stats.battles > 0 && ( {stats.battles > 0 && (
<AllStatsOverview stats={stats} supplementaryStats={supplementary} /> <AllStatsOverview
locale={locale}
stats={stats}
supplementaryStats={supplementary}
/>
)} )}
</Wrapper> </CommandWrapper>
); );
} }
export const statsCommand = new Promise<CommandRegistryPromisable>( export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
async (resolve) => { resolve({
const command = await addPeriodicFilterOptions( inProduction: true,
new SlashCommandBuilder() inPublic: true,
.setName('stats')
.setDescription('Regular battles statistics'), command: await addPeriodicFilterOptions(
createLocalizedCommand('stats'),
(option) => option.addStringOption(addUsernameChoices), (option) => option.addStringOption(addUsernameChoices),
); ),
resolve({ async handler(interaction) {
inProduction: true, const player = await resolvePlayerFromCommand(interaction);
inPublic: true, const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
command, return Promise.all([
render(player, period, filters, interaction.locale),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
},
async handler(interaction) { autocomplete: (interaction) => {
const player = await resolvePlayerFromCommand(interaction); autocompleteUsername(interaction);
const period = resolvePeriodFromCommand(player.region, interaction); autocompleteTanks(interaction);
const filters = await getFiltersFromCommand(interaction); },
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
return Promise.all([ async button(interaction) {
render(player, period, filters), const player = await resolvePlayerFromButton(interaction);
buttonRefresh(interaction, path), const period = resolvePeriodFromButton(player.region, interaction);
getBlitzStarsLinkButton(player.region, player.id), const filters = getFiltersFromButton(interaction);
]);
},
autocomplete: (interaction) => { return await render(player, period, filters, interaction.locale);
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);
},
});
},
);
+44 -50
View File
@@ -1,12 +1,12 @@
import { SlashCommandBuilder } from 'discord.js'; import { NATIONS } from '../core/blitzkrieg/tankDefinitions';
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import { addFilterOptions } from '../core/discord/addFilterOptions'; import { addFilterOptions } from '../core/discord/addFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary'; import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL'; import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton'; import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand'; import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
@@ -14,58 +14,52 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown'; import { renderBreakdown } from './breakdown';
export const todayCommand = new Promise<CommandRegistryPromisable>( export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
async (resolve) => { const nations = await NATIONS;
const awaitedEncyclopediaInfo = await encyclopediaInfo; resolve({
const command = addFilterOptions( inProduction: true,
new SlashCommandBuilder() inPublic: true,
.setName('today')
.setDescription("Today's played tanks and statistics"),
awaitedEncyclopediaInfo,
).addStringOption(addUsernameChoices);
resolve({ command: addFilterOptions(
inProduction: true, createLocalizedCommand('today'),
inPublic: true, nations,
).addStringOption(addUsernameChoices),
command, async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(
player.region,
interaction,
'today',
);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction, true),
...filters,
});
async handler(interaction) { return [
const player = await resolvePlayerFromCommand(interaction); ...(await renderBreakdown(player, period, filters, interaction.locale)),
const period = resolvePeriodFromCommand( buttonRefresh(interaction, path),
player.region, await getBlitzStarsLinkButton(player.region, player.id),
interaction, ];
'today', },
);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction, true),
...filters,
});
return [ autocomplete: (interaction) => {
...(await renderBreakdown(player, period, filters)), autocompleteUsername(interaction);
buttonPrimary(path, 'Refresh'), autocompleteTanks(interaction);
await getBlitzStarsLinkButton(player.region, player.id), },
];
},
autocomplete: (interaction) => { async button(interaction) {
autocompleteUsername(interaction); const player = await resolvePlayerFromButton(interaction);
autocompleteTanks(interaction); const period = resolvePeriodFromButton(player.region, interaction);
}, const filters = getFiltersFromButton(interaction);
async button(interaction) { return await renderBreakdown(player, period, filters, interaction.locale);
const player = await resolvePlayerFromButton(interaction); },
const period = resolvePeriodFromButton(player.region, interaction); });
const filters = getFiltersFromButton(interaction); });
return await renderBreakdown(player, period, filters);
},
});
},
);
@@ -1,30 +1,21 @@
import { Locale } from 'discord.js';
import { Percentile } from '../../../../constants/percentiles'; import { Percentile } from '../../../../constants/percentiles';
import { translator } from '../../../../core/localization/translator';
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile'; import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
import { theme } from '../../../../stitches.config'; import { theme } from '../../../../stitches.config';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator'; import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
import { Glow } from './components/Glow'; import { Glow } from './components/Glow';
export interface WN8DisplayProps { export interface WN8DisplayProps {
WN8?: number; wn8?: number;
locale: Locale;
} }
export const PERCENTILE_NAMES = { export function WN8Display({ wn8, locale }: WN8DisplayProps) {
[Percentile.VeryBad]: 'Very Bad',
[Percentile.Bad]: 'Bad',
[Percentile.BelowAverage]: 'Below Average',
[Percentile.Average]: 'Average',
[Percentile.AboveAverage]: 'Above Average',
[Percentile.Good]: 'Good',
[Percentile.VeryGood]: 'Very Good',
[Percentile.Great]: 'Great',
[Percentile.Unicum]: 'Unicum',
[Percentile.SuperUnicum]: 'Super Unicum',
};
export function WN8Display({ WN8 }: WN8DisplayProps) {
const percentile = const percentile =
WN8 === undefined ? Percentile.VeryBad : getWN8Percentile(WN8); wn8 === undefined ? Percentile.VeryBad : getWN8Percentile(wn8);
const color = PERCENTILE_COLORS[percentile]; const color = PERCENTILE_COLORS[percentile];
const { translate } = translator(locale);
return ( return (
<div <div
@@ -59,7 +50,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
}} }}
> >
{WN8 === undefined ? '--' : WN8.toFixed(0)} {wn8 === undefined ? '--' : wn8.toFixed(0)}
</span> </span>
<span <span
style={{ style={{
@@ -67,7 +58,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
}} }}
> >
{PERCENTILE_NAMES[percentile]} {translate(`common.wn8_percentile.${percentile}`)}
</span> </span>
</div> </div>
@@ -34,7 +34,7 @@ export function WN8SurroundingStat({
style={{ style={{
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: 700,
}} }}
> >
{value} {value}
+15 -9
View File
@@ -1,4 +1,6 @@
import { Locale } from 'discord.js';
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo'; import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
import { translator } from '../../core/localization/translator';
import { WN8Display } from './components/WN8Display'; import { WN8Display } from './components/WN8Display';
import { import {
WN8SurroundingStat, WN8SurroundingStat,
@@ -8,12 +10,16 @@ import {
export interface AllStatsOverviewProps { export interface AllStatsOverviewProps {
stats: AllStats; stats: AllStats;
supplementaryStats: SupplementaryStats; supplementaryStats: SupplementaryStats;
locale: Locale;
} }
export default function AllStatsOverview({ export default function AllStatsOverview({
stats, stats,
locale,
supplementaryStats, supplementaryStats,
}: AllStatsOverviewProps) { }: AllStatsOverviewProps) {
const { t } = translator(locale);
return ( return (
<div <div
style={{ style={{
@@ -32,18 +38,18 @@ export default function AllStatsOverview({
}} }}
> >
<WN8SurroundingStat <WN8SurroundingStat
label="Battles" label={t`bot.commands.stats.body.battles`}
value={stats.battles.toLocaleString()} value={stats.battles.toLocaleString()}
align={WN8SurroundingStatAlign.Right} align={WN8SurroundingStatAlign.Right}
/> />
<WN8SurroundingStat <WN8SurroundingStat
label="Winrate" label={t`bot.commands.stats.body.winrate`}
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`} value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
align={WN8SurroundingStatAlign.Right} align={WN8SurroundingStatAlign.Right}
padded padded
/> />
<WN8SurroundingStat <WN8SurroundingStat
label="Damage" label={t`bot.commands.stats.body.damage`}
value={Math.round( value={Math.round(
stats.damage_dealt / stats.battles, stats.damage_dealt / stats.battles,
).toLocaleString()} ).toLocaleString()}
@@ -51,13 +57,13 @@ export default function AllStatsOverview({
padded padded
/> />
<WN8SurroundingStat <WN8SurroundingStat
label="Dmg. ratio" label={t`bot.commands.stats.body.damage_ratio`}
value={(stats.damage_dealt / stats.damage_received).toFixed(2)} value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
align={WN8SurroundingStatAlign.Right} align={WN8SurroundingStatAlign.Right}
/> />
</div> </div>
<WN8Display WN8={supplementaryStats.WN8} /> <WN8Display locale={locale} wn8={supplementaryStats.WN8} />
<div <div
style={{ style={{
@@ -68,25 +74,25 @@ export default function AllStatsOverview({
}} }}
> >
<WN8SurroundingStat <WN8SurroundingStat
label="Tier" label={t`bot.commands.stats.body.tier`}
value={ value={
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--' supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
} }
/> />
<WN8SurroundingStat <WN8SurroundingStat
label="Survival" label={t`bot.commands.stats.body.survival`}
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed( value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
2, 2,
)}%`} )}%`}
padded padded
/> />
<WN8SurroundingStat <WN8SurroundingStat
label="Accuracy" label={t`bot.commands.stats.body.accuracy`}
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`} value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
padded padded
/> />
<WN8SurroundingStat <WN8SurroundingStat
label="Kill ratio" label={t`bot.commands.stats.body.kill_ratio`}
value={( value={(
stats.frags / stats.frags /
(stats.battles - stats.survived_battles) (stats.battles - stats.survived_battles)
+2 -2
View File
@@ -5,7 +5,7 @@ import {
GrayColor, GrayColor,
PALETTES, PALETTES,
} from '../../../constants/radixColors'; } from '../../../constants/radixColors';
import { TREE_TYPE_ICONS, TankType, TreeType } from '../../Tanks'; import { TREE_TYPE_ICONS, TankClass, TreeType } from '../../Tanks';
import { RowStat } from './RowStat'; import { RowStat } from './RowStat';
export interface RowStatItem { export interface RowStatItem {
@@ -22,7 +22,7 @@ interface RowProps {
title: string; title: string;
minimized?: boolean; minimized?: boolean;
treeType?: TreeType; treeType?: TreeType;
tankType?: TankType; tankType?: TankClass;
color?: AccentColor | GrayColor; color?: AccentColor | GrayColor;
stats: (RowStatItem | undefined)[]; stats: (RowStatItem | undefined)[];
} }
@@ -63,7 +63,7 @@ export function RowStat({
<span <span
style={{ style={{
color: theme.textHighContrast, color: theme.textHighContrast,
fontWeight: 'bold', fontWeight: 700,
fontSize: 16, fontSize: 16,
textAlign: 'center', textAlign: 'center',
}} }}
@@ -1,26 +1,22 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
export interface WrapperProps { interface CommandWrapperProps {
children: ReactNode; children: ReactNode;
naked?: boolean;
fat?: boolean; fat?: boolean;
} }
export default function Wrapper({ children, naked, fat }: WrapperProps) { export default function CommandWrapper({ children, fat }: CommandWrapperProps) {
return ( return (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: `16px ${fat ? 0 : 16}px`, padding: `16px ${fat ? 0 : 16}px`,
gap: 32, gap: 16,
width: 480, width: 480,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
background: 'url(https://i.imgur.com/PhS06NJ.png)',
...(!naked && {
background: 'url(https://i.imgur.com/PhS06NJ.png)',
}),
}} }}
> >
{children} {children}
+23 -18
View File
@@ -1,4 +1,6 @@
import { Locale } from 'discord.js';
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo'; import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
import { translator } from '../../core/localization/translator';
import isNumber from '../../core/math/isNumber'; import isNumber from '../../core/math/isNumber';
import getWN8Percentile from '../../core/statistics/getWN8Percentile'; import getWN8Percentile from '../../core/statistics/getWN8Percentile';
import { Card } from './components/Card'; import { Card } from './components/Card';
@@ -8,12 +10,15 @@ import { Row } from './components/Row';
export interface GenericAllStatsProps { export interface GenericAllStatsProps {
stats: AllStats; stats: AllStats;
supplementaryStats?: SupplementaryStats; supplementaryStats?: SupplementaryStats;
locale: Locale;
} }
export default function GenericAllStats({ export default function GenericAllStats({
stats, stats,
supplementaryStats, supplementaryStats,
locale,
}: GenericAllStatsProps) { }: GenericAllStatsProps) {
const { t } = translator(locale);
const damageRatio = stats.damage_dealt / stats.damage_received; const damageRatio = stats.damage_dealt / stats.damage_received;
const killsToDeathRatio = const killsToDeathRatio =
stats.frags / (stats.battles - stats.survived_battles); stats.frags / (stats.battles - stats.survived_battles);
@@ -22,27 +27,27 @@ export default function GenericAllStats({
<Root> <Root>
<Row> <Row>
<Card <Card
title="Games" title={t`bot.commands.full_stats.body.cards.games`}
items={[ items={[
{ {
label: 'Winrate', label: t`bot.commands.full_stats.body.cards.games.winrate`,
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`, value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
}, },
{ {
label: 'Battles', label: t`bot.commands.full_stats.body.cards.games.battles`,
value: stats.battles, value: stats.battles,
}, },
{ {
label: 'Wins / Losses', label: t`bot.commands.full_stats.body.cards.games.wins_losses`,
value: `${stats.wins} / ${stats.losses}`, value: `${stats.wins} / ${stats.losses}`,
}, },
]} ]}
/> />
<Card <Card
title="Efficiency" title={t`bot.commands.full_stats.body.cards.efficiency`}
items={[ items={[
{ {
label: 'WN8', label: t`bot.commands.full_stats.body.cards.efficiency.wn8`,
value: value:
supplementaryStats && isNumber(supplementaryStats.WN8) supplementaryStats && isNumber(supplementaryStats.WN8)
? supplementaryStats.WN8!.toFixed(0) ? supplementaryStats.WN8!.toFixed(0)
@@ -52,14 +57,14 @@ export default function GenericAllStats({
: undefined, : undefined,
}, },
{ {
label: 'Survival', label: t`bot.commands.full_stats.body.cards.efficiency.survival`,
value: `${( value: `${(
100 * 100 *
(stats.survived_battles / stats.battles) (stats.survived_battles / stats.battles)
).toFixed(2)}%`, ).toFixed(2)}%`,
}, },
{ {
label: 'Accuracy', label: t`bot.commands.full_stats.body.cards.efficiency.accuracy`,
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`, value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
}, },
]} ]}
@@ -67,14 +72,14 @@ export default function GenericAllStats({
</Row> </Row>
<Row> <Row>
<Card <Card
title="Lethality" title={t`bot.commands.full_stats.body.cards.lethality`}
items={[ items={[
{ {
label: 'Average damage', label: t`bot.commands.full_stats.body.cards.lethality.average_damage`,
value: (stats.damage_dealt / stats.battles).toFixed(0), value: (stats.damage_dealt / stats.battles).toFixed(0),
}, },
{ {
label: 'Damage per tier', label: t`bot.commands.full_stats.body.cards.lethality.damage_per_tier`,
value: supplementaryStats?.tier value: supplementaryStats?.tier
? ( ? (
stats.damage_dealt / stats.damage_dealt /
@@ -84,32 +89,32 @@ export default function GenericAllStats({
: undefined, : undefined,
}, },
{ {
label: 'Damage ratio', label: t`bot.commands.full_stats.body.cards.lethality.damage_ratio`,
value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined, value: isFinite(damageRatio) ? damageRatio.toFixed(2) : undefined,
}, },
{ {
label: 'Average kills', label: t`bot.commands.full_stats.body.cards.lethality.average_kills`,
value: (stats.frags / stats.battles).toFixed(0), value: (stats.frags / stats.battles).toFixed(0),
}, },
]} ]}
/> />
<Card <Card
title="Miscellaneous" title={t`bot.commands.full_stats.body.cards.miscellaneous`}
items={[ items={[
{ {
label: 'Average tier', label: t`bot.commands.full_stats.body.cards.miscellaneous.average_tier`,
value: supplementaryStats?.tier?.toFixed(2), value: supplementaryStats?.tier?.toFixed(2),
}, },
{ {
label: 'Average spots', label: t`bot.commands.full_stats.body.cards.miscellaneous.average_spots`,
value: (stats.spotted / stats.battles).toFixed(2), value: (stats.spotted / stats.battles).toFixed(2),
}, },
{ {
label: 'Average XP', label: t`bot.commands.full_stats.body.cards.miscellaneous.average_xp`,
value: (stats.xp / stats.battles).toFixed(0), value: (stats.xp / stats.battles).toFixed(0),
}, },
{ {
label: 'Kills to death ratio', label: t`bot.commands.full_stats.body.cards.miscellaneous.kills_to_death_ratio`,
value: isFinite(killsToDeathRatio) value: isFinite(killsToDeathRatio)
? killsToDeathRatio.toFixed(2) ? killsToDeathRatio.toFixed(2)
: undefined, : undefined,
@@ -30,7 +30,7 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
<span <span
style={{ style={{
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold', fontWeight: 700,
fontSize: 16, fontSize: 16,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
-76
View File
@@ -1,76 +0,0 @@
import { MeshProps } from '@react-three/fiber';
import { forwardRef } from 'react';
import { DoubleSide, Mesh } from 'three';
interface InfiniteGridHelperProps extends MeshProps {
size1?: number;
size2?: number;
color?: THREE.Color;
distance?: number;
}
/**
* Thanks, Fyrestar, for [THREE.InfiniteGridHelper](https://github.com/Fyrestar/THREE.InfiniteGridHelper/)!
*/
const InfiniteGridHelper = forwardRef<Mesh, InfiniteGridHelperProps>(
({ size1, size2, color, distance, ...props }, ref) => {
return (
<mesh {...props} ref={ref} frustumCulled={false}>
<shaderMaterial
args={[
{
side: DoubleSide,
uniforms: {
uSize1: { value: size1 },
uSize2: { value: size2 },
uColor: { value: color },
uDistance: { value: distance },
},
extensions: { derivatives: true },
transparent: true,
vertexShader: `
varying vec3 worldPosition;
uniform float uDistance;
void main() {
vec3 pos = position.xzy * uDistance;
worldPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec3 worldPosition;
uniform float uSize1;
uniform float uSize2;
uniform vec3 uColor;
uniform float uDistance;
vec2 origin = vec2(0.0, 0.0);
float getGrid(float size) {
vec2 r = worldPosition.xz / size;
vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
float line = min(grid.x, grid.y);
return (1.0 - min(line, 1.0)) * max(0.0, min(1.0, cameraPosition.y / 5.0));
}
void main() {
float d = 1.0 - min(distance(origin, worldPosition.xz) / uDistance, 1.0);
float g1 = getGrid(uSize1);
float g2 = getGrid(uSize2);
gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 10.0));
gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
if ( gl_FragColor.a <= 0.0 ) discard;
}
`,
},
]}
/>
<planeGeometry />
</mesh>
);
},
);
export default InfiniteGridHelper;
+7 -8
View File
@@ -1,3 +1,5 @@
import { Locale } from 'discord.js';
import { translator } from '../core/localization/translator';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
export enum NoDataType { export enum NoDataType {
@@ -7,16 +9,13 @@ export enum NoDataType {
} }
export interface NoDataProps { export interface NoDataProps {
type: NoDataType; type: 'battles_in_period' | 'players_in_period' | 'tanks_found';
locale: Locale;
} }
const NO_DATA_MESSAGES: Record<NoDataType, string> = { export default function NoData({ type, locale }: NoDataProps) {
[NoDataType.BattlesInPeriod]: 'No battles played in this period', const { translate } = translator(locale);
[NoDataType.PlayersInPeriod]: 'No players in this period',
[NoDataType.TanksFound]: 'No tanks found',
};
export default function NoData({ type }: NoDataProps) {
return ( return (
<div <div
style={{ style={{
@@ -26,7 +25,7 @@ export default function NoData({ type }: NoDataProps) {
}} }}
> >
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}> <span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{NO_DATA_MESSAGES[type]} {translate(`bot.common.no_data.${type}`)}
</span> </span>
</div> </div>
); );
+9 -8
View File
@@ -5,18 +5,19 @@ import {
} from '../../../core/blitzkrieg/tankDefinitions'; } from '../../../core/blitzkrieg/tankDefinitions';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
export const TANK_TYPES = [ export const TANK_CLASSES = [
'light', 'lightTank',
'medium', 'mediumTank',
'heavy', 'heavyTank',
'tankDestroyer', 'AT-SPG',
] as const; ] as const;
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const; export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
export type TankType = (typeof TANK_TYPES)[number]; export type TankClass = (typeof TANK_CLASSES)[number];
export type TreeType = (typeof TREE_TYPES)[number]; export type TreeType = (typeof TREE_TYPES)[number];
// BIG TODO: REMOVE THIS AFTER LOCALIZATION
export const TREE_TYPE_NAMES: Record<TreeType, string> = { export const TREE_TYPE_NAMES: Record<TreeType, string> = {
researchable: 'Tech tree', researchable: 'Tech tree',
premium: 'Premium', premium: 'Premium',
@@ -31,7 +32,7 @@ export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
export interface ItemProps { export interface ItemProps {
image?: string; image?: string;
tankType?: TankType; tankType?: TankClass;
name: string; name: string;
treeType: TreeType; treeType: TreeType;
} }
@@ -42,7 +43,7 @@ const TREE_TYPE_COLOR = {
collector: '_blue', collector: '_blue',
} as const; } as const;
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankType, string>> = { export const TREE_TYPE_ICONS: Record<TreeType, Record<TankClass, string>> = {
researchable: TANK_ICONS, researchable: TANK_ICONS,
premium: TANK_ICONS_PREMIUM, premium: TANK_ICONS_PREMIUM,
collector: TANK_ICONS_COLLECTOR, collector: TANK_ICONS_COLLECTOR,
+25 -48
View File
@@ -1,75 +1,52 @@
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
export interface TitleBarProps { export interface TitleBarProps {
name: string; title: string;
image?: string; image?: string;
description?: string; description?: string;
} }
export default function TitleBar({ name, image, description }: TitleBarProps) { export default function TitleBar({ title, image, description }: TitleBarProps) {
return ( return (
<div <div
style={{ style={{
gap: 4,
display: 'flex', display: 'flex',
alignItems: 'center', flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', alignItems: 'center',
}} }}
> >
<div style={{ display: 'flex', gap: 8 }}> <div
style={{
gap: 4,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{image && ( {image && (
<img <img
style={{ width: 64, height: 64, objectFit: 'contain' }} style={{ width: 32, height: 32, objectFit: 'contain' }}
src={image} src={image}
/> />
)} )}
<div <span
style={{ style={{
display: 'flex', fontSize: 32,
flexDirection: 'column', fontWeight: 900,
justifyContent: 'center', background: `linear-gradient(180deg, ${theme.colors.textHighContrast} 0%, ${theme.colors.textLowContrast} 100%)`,
gap: 4, backgroundClip: 'text',
}} }}
> >
<div {title}
style={{ </span>
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
{name}
</span>
</div>
<div
style={{
display: 'flex',
justifyContent: image ? 'flex-start' : 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{description}
</span>
</div>
</div>
</div> </div>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{description}
</span>
</div> </div>
); );
} }
+5 -5
View File
@@ -1,7 +1,7 @@
export const LEAGUES = [ export const LEAGUES = [
{ name: 'Diamond', minScore: 5000 }, { name: 'diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 }, { name: 'platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 }, { name: 'gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 }, { name: 'silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 }, { name: 'bronze', minScore: 0 },
]; ];
+4 -14
View File
@@ -3,21 +3,11 @@ export const REGIONS = ['com', 'eu', 'asia'] as const;
export type Region = (typeof REGIONS)[number]; export type Region = (typeof REGIONS)[number];
export type RegionSubdomain = 'na' | 'eu' | 'asia'; export type RegionSubdomain = 'na' | 'eu' | 'asia';
export const REGION_NAMES: Record<Region, string> = { /**
* @deprecated
*/
export const UNLOCALIZED_REGION_NAMES: Record<Region, string> = {
com: 'North America', com: 'North America',
eu: 'Europe',
asia: 'Asia', asia: 'Asia',
};
// https://planetarynames.wr.usgs.gov/Abbreviations
export const REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA',
eu: 'EU',
asia: 'ASIA',
};
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
na: 'North America',
eu: 'Europe', eu: 'Europe',
asia: 'Asia',
}; };
+21 -22
View File
@@ -1,39 +1,38 @@
import { clamp } from 'lodash'; import { clamp } from 'lodash';
import { degToRad } from 'three/src/math/MathUtils';
import { PitchLimits, YawLimits } from '../blitzkrieg/modelDefinitions'; import { PitchLimits, YawLimits } from '../blitzkrieg/modelDefinitions';
import { normalizeAnglePI } from '../math/normalizeAngle180'; import { normalizeAngleRad } from '../math/normalizeAngleRad';
export function applyPitchYawLimits( export function applyPitchYawLimits(
pitch: number, // rad pitch: number, // rad
yaw: number, // rad yaw: number, // rad
pitchLimits: PitchLimits, // deg pitchLimits: PitchLimits, // deg
yawLimits?: YawLimits, // deg yawLimits?: YawLimits, // deg
verticalStabilizer = false,
) { ) {
let newPitch = pitch; let newPitch = pitch;
let newYaw = yaw; let newYaw = yaw;
if (yawLimits) { if (yawLimits) {
newYaw = normalizeAnglePI( newYaw = normalizeAngleRad(
clamp( clamp(newYaw, -degToRad(yawLimits.max), -degToRad(yawLimits.min)),
newYaw,
-yawLimits.max * (Math.PI / 180),
-yawLimits.min * (Math.PI / 180),
),
); );
} }
let lowerPitch = -pitchLimits.max * (Math.PI / 180); const pitchBuff = verticalStabilizer ? degToRad(3) : 0;
let upperPitch = -pitchLimits.min * (Math.PI / 180); let lowerPitch = -degToRad(pitchLimits.max) - pitchBuff;
const transition = (pitchLimits.transition ?? 10) * (Math.PI / 180); let upperPitch = -degToRad(pitchLimits.min) + pitchBuff;
const transition = degToRad(pitchLimits.transition ?? 10);
if (pitchLimits.back) { if (pitchLimits.back) {
const range = pitchLimits.back.range * (Math.PI / 180); const range = degToRad(pitchLimits.back.range);
const yawRotatedAbs = Math.abs(normalizeAnglePI(newYaw - Math.PI)); const yawRotatedAbs = Math.abs(normalizeAngleRad(newYaw - Math.PI));
if (yawRotatedAbs <= range + transition) { if (yawRotatedAbs <= range + transition) {
if (yawRotatedAbs <= range) { if (yawRotatedAbs <= range) {
// inside range // inside range
lowerPitch = -pitchLimits.back.max * (Math.PI / 180); lowerPitch = -degToRad(pitchLimits.back.max);
upperPitch = -pitchLimits.back.min * (Math.PI / 180); upperPitch = -degToRad(pitchLimits.back.min);
} else { } else {
// inside transition // inside transition
const transitionProgress = (yawRotatedAbs - range) / transition; const transitionProgress = (yawRotatedAbs - range) / transition;
@@ -44,21 +43,21 @@ export function applyPitchYawLimits(
(1 - transitionProgress) * pitchLimits.back.min + (1 - transitionProgress) * pitchLimits.back.min +
transitionProgress * pitchLimits.min; transitionProgress * pitchLimits.min;
lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); lowerPitch = -degToRad(maxInterpolatedPitch);
upperPitch = -minInterpolatedPitch * (Math.PI / 180); upperPitch = -degToRad(minInterpolatedPitch);
} }
} }
} }
if (pitchLimits.front) { if (pitchLimits.front) {
const range = pitchLimits.front.range * (Math.PI / 180); const range = degToRad(pitchLimits.front.range);
const yawAbs = Math.abs(normalizeAnglePI(newYaw)); const yawAbs = Math.abs(normalizeAngleRad(newYaw));
if (yawAbs <= range + transition) { if (yawAbs <= range + transition) {
if (yawAbs <= range) { if (yawAbs <= range) {
// inside range // inside range
lowerPitch = -pitchLimits.front.max * (Math.PI / 180); lowerPitch = -degToRad(pitchLimits.front.max);
upperPitch = -pitchLimits.front.min * (Math.PI / 180); upperPitch = -degToRad(pitchLimits.front.min);
} else { } else {
// inside transition // inside transition
const transitionProgress = (yawAbs - range) / transition; const transitionProgress = (yawAbs - range) / transition;
@@ -69,8 +68,8 @@ export function applyPitchYawLimits(
(1 - transitionProgress) * pitchLimits.front.min + (1 - transitionProgress) * pitchLimits.front.min +
transitionProgress * pitchLimits.min; transitionProgress * pitchLimits.min;
lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); lowerPitch = -degToRad(maxInterpolatedPitch);
upperPitch = -minInterpolatedPitch * (Math.PI / 180); upperPitch = -degToRad(minInterpolatedPitch);
} }
} }
} }
+9 -4
View File
@@ -1,18 +1,23 @@
import { Locale } from 'discord.js';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { UserError } from '../../hooks/userError'; import { UserError } from '../../hooks/userError';
import { TanksStats } from '../../types/tanksStats'; import { TanksStats } from '../../types/tanksStats';
import { translator } from '../localization/translator';
import fetchBlitz from './fetchBlitz'; import fetchBlitz from './fetchBlitz';
export default async function getTankStats(region: Region, id: number) { export default async function getTankStats(
region: Region,
id: number,
locale: Locale,
) {
const { t } = translator(locale);
const tankStats = await fetchBlitz<TanksStats>( const tankStats = await fetchBlitz<TanksStats>(
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
); );
if (tankStats[id] === null) { if (tankStats[id] === null) {
throw new UserError( throw new UserError(t`bot.common.errors.no_tank_stats`);
"# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
);
} }
return tankStats[id]; return tankStats[id];
+13 -5
View File
@@ -1,13 +1,19 @@
import { Locale } from 'discord.js';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import { UserError } from '../../hooks/userError';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { UserError } from '../../hooks/userError';
import { import {
tankDefinitions, tankDefinitions,
tanksDefinitionsArray, tanksDefinitionsArray,
} from '../blitzkrieg/tankDefinitions'; } from '../blitzkrieg/tankDefinitions';
import { translator } from '../localization/translator';
export default async function resolveTankId(tank: string | number) { export default async function resolveTankId(
const number = typeof tank === 'string' ? parseInt(tank) : tank; tank: string | number,
locale: Locale,
) {
const { translate } = translator(locale);
const number = typeof tank === 'string' ? Number(tank) : tank;
if (Number.isNaN(number)) { if (Number.isNaN(number)) {
const searchResult = go(`${tank}`, await tanksDefinitionsArray, { const searchResult = go(`${tank}`, await tanksDefinitionsArray, {
@@ -17,7 +23,9 @@ export default async function resolveTankId(tank: string | number) {
if (searchResult.length === 0) { if (searchResult.length === 0) {
throw new UserError( throw new UserError(
`# Tank not found\nCould not find tank by the name "${markdownEscape(`${tank}`)}".`, translate('bot.common.errors.tank_name_not_found', [
markdownEscape(`${tank}`),
]),
); );
} else { } else {
return searchResult[0].obj.id; return searchResult[0].obj.id;
@@ -27,7 +35,7 @@ export default async function resolveTankId(tank: string | number) {
return number; return number;
} else { } else {
throw new UserError( throw new UserError(
`# Tank not found\nCould not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`, translate('bot.common.errors.tank_id_not_found', [`${number}`]),
); );
} }
} }
+5 -1
View File
@@ -1,9 +1,11 @@
import { degToRad } from 'three/src/math/MathUtils';
import { import {
DuelMember, DuelMember,
genericDefaultEquipmentMatrix, genericDefaultEquipmentMatrix,
mutateDuel, mutateDuel,
} from '../../stores/duel'; } from '../../stores/duel';
import { availableProvisions } from './availableProvisions'; import { availableProvisions } from './availableProvisions';
import { modelDefinitions } from './modelDefinitions';
import { provisionDefinitions } from './provisionDefinitions'; import { provisionDefinitions } from './provisionDefinitions';
import { tankDefinitions } from './tankDefinitions'; import { tankDefinitions } from './tankDefinitions';
@@ -13,6 +15,8 @@ export async function assignDuelMember(
) { ) {
const awaitedTankDefinitions = await tankDefinitions; const awaitedTankDefinitions = await tankDefinitions;
const awaitedProvisionDefinitions = await provisionDefinitions; const awaitedProvisionDefinitions = await provisionDefinitions;
const awaitedModelDefinitions = await modelDefinitions;
const tankModelDefinition = awaitedModelDefinitions[id];
const tank = awaitedTankDefinitions[id]; const tank = awaitedTankDefinitions[id];
const turret = tank.turrets.at(-1)!; const turret = tank.turrets.at(-1)!;
const gun = turret.guns.at(-1)!; const gun = turret.guns.at(-1)!;
@@ -32,7 +36,7 @@ export async function assignDuelMember(
shell, shell,
track, track,
equipment: genericDefaultEquipmentMatrix, equipment: genericDefaultEquipmentMatrix,
pitch: 0, pitch: degToRad(tankModelDefinition.turretRotation?.pitch ?? 0),
yaw: 0, yaw: 0,
camouflage: true, camouflage: true,
consumables: [], consumables: [],
+19 -15
View File
@@ -1,24 +1,28 @@
import { TREE_TYPE_NAMES } from '../../components/Tanks'; import { Locale } from 'discord.js';
import { encyclopediaInfo } from '../blitz/encyclopediaInfo'; import { translator } from '../localization/translator';
import { StatFilters } from '../statistics/filterStats'; import { StatFilters } from '../statistics/filterStats';
import { TIER_ROMAN_NUMERALS, Tier, tankDefinitions } from './tankDefinitions'; import { TIER_ROMAN_NUMERALS, Tier, tankDefinitions } from './tankDefinitions';
export async function filtersToDescription({ export async function filtersToDescription(
nation, { nation, tier, tankType, treeType, tank }: StatFilters,
tier, locale: Locale,
tankType, ) {
treeType, const { translate } = translator(locale);
tank,
}: StatFilters) {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
const awaitedTankDefinitions = await tankDefinitions; const awaitedTankDefinitions = await tankDefinitions;
const info: string[] = []; const info: string[] = [];
// TODO: somehow localize tank names?
if (tank) return awaitedTankDefinitions[tank].name; if (tank) return awaitedTankDefinitions[tank].name;
if (nation) info.push(awaitedEncyclopediaInfo.vehicle_nations[nation]); if (nation) info.push(translate(`common.nations.${nation}`));
if (tier) info.push(`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]}`); if (tier) {
if (tankType) info.push(awaitedEncyclopediaInfo.vehicle_types[tankType]); info.push(
if (treeType) info.push(TREE_TYPE_NAMES[treeType]); translate('bot.common.filters.tier', [TIER_ROMAN_NUMERALS[tier as Tier]]),
);
}
if (tankType) info.push(translate(`common.tank_class_short.${tankType}`));
if (treeType) info.push(translate(`common.tree_type.${treeType}`));
return info.length === 0 ? 'No filters' : info.join(', '); return info.length === 0
? translate('bot.common.filters.none')
: info.join(', ');
} }
+3 -1
View File
@@ -1,3 +1,4 @@
import { Locale } from 'discord.js';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { import {
GetHistoriesOptions, GetHistoriesOptions,
@@ -34,6 +35,7 @@ export const getTankHistoriesDefaultOptions: GetTankHistoriesOptions = {
export default async function getTankHistories( export default async function getTankHistories(
server: Region, server: Region,
id: number, id: number,
locale: Locale,
options?: Partial<GetTankHistoriesOptions>, options?: Partial<GetTankHistoriesOptions>,
) { ) {
const mergedOptions = { ...getTankHistoriesDefaultOptions, ...options }; const mergedOptions = { ...getTankHistoriesDefaultOptions, ...options };
@@ -76,7 +78,7 @@ export default async function getTankHistories(
const latestNodes: TankHistories = []; const latestNodes: TankHistories = [];
if (mergedOptions.includeLatestHistories) { if (mergedOptions.includeLatestHistories) {
(await getTankStats(server, id)).forEach((tankHistory) => { (await getTankStats(server, id, locale)).forEach((tankHistory) => {
if ( if (
tankHistory.all.battles > (lastBattles[tankHistory.tank_id] ?? 0) && tankHistory.all.battles > (lastBattles[tankHistory.tank_id] ?? 0) &&
(mergedOptions.tankId === undefined || (mergedOptions.tankId === undefined ||
-26
View File
@@ -1,26 +0,0 @@
import { readFile } from 'fs/promises';
import satori from 'satori';
const FONT_NAME = 'Roboto';
const FONT_FILES = [
{ weight: 400 as const, path: 'src/assets/fonts/Roboto.ttf' },
{ weight: 700 as const, path: 'src/assets/fonts/Roboto-Bold.ttf' },
{ weight: 900 as const, path: 'src/assets/fonts/Roboto-Black.ttf' },
];
const fonts = Promise.all(
FONT_FILES.map(({ path, weight }) =>
readFile(path).then((data) => ({
data: Buffer.from(data.buffer),
name: FONT_NAME,
weight,
})),
),
);
export default async function jsxToSvg(jsx: JSX.Element) {
return await satori(jsx, {
width: 480,
fonts: await fonts,
});
}
File renamed without changes.
+28
View File
@@ -0,0 +1,28 @@
import { readFileSync } from 'fs';
import satori from 'satori';
const fonts = [
{
file: require('./Roboto.ttf') as string,
weight: 400 as const,
},
{
file: require('./Roboto-Bold.ttf') as string,
weight: 700 as const,
},
{
file: require('./Roboto-Black.ttf') as string,
weight: 900 as const,
},
];
export default async function jsxToSvg(jsx: JSX.Element) {
return await satori(jsx, {
width: 480,
fonts: fonts.map(({ file, weight }) => ({
weight,
name: 'Roboto',
data: readFileSync(`${__dirname}/${file}`),
})),
});
}
+3 -3
View File
@@ -5,7 +5,7 @@ export function resolveDpm(
shell: ShellDefinition, shell: ShellDefinition,
damageCoefficient = 1, damageCoefficient = 1,
reloadCoefficient = 1, reloadCoefficient = 1,
interClipCoefficient = 1, intraClipCoefficient = 1,
) { ) {
const alpha = shell.damage.armor * damageCoefficient; const alpha = shell.damage.armor * damageCoefficient;
@@ -14,14 +14,14 @@ export function resolveDpm(
} else if (gun.type === 'autoLoader') { } else if (gun.type === 'autoLoader') {
return ( return (
((alpha * gun.count) / ((alpha * gun.count) /
(gun.reload + (gun.count - 1) * gun.interClip * interClipCoefficient)) * (gun.reload + (gun.count - 1) * gun.intraClip * intraClipCoefficient)) *
60 60
); );
} else { } else {
return ( return (
((alpha * gun.count) / ((alpha * gun.count) /
(gun.reload.reduce((a, b) => a + b, 0) + (gun.reload.reduce((a, b) => a + b, 0) +
(gun.count - 1) * gun.interClip)) * (gun.count - 1) * gun.intraClip)) *
60 60
); );
} }
+2 -2
View File
@@ -4,10 +4,10 @@ export function resolveReload(gun: GunDefinition) {
if (gun.type === 'regular') { if (gun.type === 'regular') {
return gun.reload; return gun.reload;
} else if (gun.type === 'autoLoader') { } else if (gun.type === 'autoLoader') {
return gun.reload + (gun.count - 1) * gun.interClip; return gun.reload + (gun.count - 1) * gun.intraClip;
} else { } else {
return ( return (
gun.reload.reduce((a, b) => a + b, 0) + (gun.count - 1) * gun.interClip gun.reload.reduce((a, b) => a + b, 0) + (gun.count - 1) * gun.intraClip
); );
} }
} }
+1
View File
@@ -6,6 +6,7 @@ export const SECRET_KEYS = [
'DISCORD_TOKEN', 'DISCORD_TOKEN',
'GH_TOKEN', 'GH_TOKEN',
'DISCORD_BLITZ_DB', 'DISCORD_BLITZ_DB',
'WOTB_DLC_CDN',
] as const; ] as const;
export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[] export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[]
+19 -19
View File
@@ -1,6 +1,6 @@
import { deburr } from 'lodash'; import { deburr } from 'lodash';
import { Vector2Tuple } from 'three'; import { Vector2Tuple } from 'three';
import { TankType, TreeType } from '../../components/Tanks'; import { TankClass, TreeType } from '../../components/Tanks';
import { asset } from './asset'; import { asset } from './asset';
import { fetchCdonLz4 } from './fetchCdonLz4'; import { fetchCdonLz4 } from './fetchCdonLz4';
@@ -44,7 +44,7 @@ export interface TankDefinition {
consumables: number; consumables: number;
provisions: number; provisions: number;
tier: Tier; tier: Tier;
type: TankType; class: TankClass;
testing?: boolean; testing?: boolean;
turrets: TurretDefinition[]; turrets: TurretDefinition[];
engines: EngineDefinition[]; engines: EngineDefinition[];
@@ -125,13 +125,13 @@ interface GunDefinitionRegular extends GunDefinitionBase {
interface GunDefinitionAutoLoader extends GunDefinitionBase { interface GunDefinitionAutoLoader extends GunDefinitionBase {
type: 'autoLoader'; type: 'autoLoader';
reload: number; reload: number;
interClip: number; intraClip: number;
count: number; count: number;
} }
interface GunDefinitionAutoReloader extends GunDefinitionBase { interface GunDefinitionAutoReloader extends GunDefinitionBase {
type: 'autoReloader'; type: 'autoReloader';
reload: number[]; reload: number[];
interClip: number; intraClip: number;
count: number; count: number;
} }
export type ShellDefinition = { export type ShellDefinition = {
@@ -180,25 +180,25 @@ export const tankNames = tanksDefinitionsArray.then((tanks) =>
), ),
); );
export const TANK_ICONS: Record<TankType, string> = { export const TANK_ICONS: Record<TankClass, string> = {
tankDestroyer: 'https://i.imgur.com/BIHSEH0.png', 'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
light: 'https://i.imgur.com/CSNha5V.png', lightTank: 'https://i.imgur.com/CSNha5V.png',
medium: 'https://i.imgur.com/wvf3ltm.png', mediumTank: 'https://i.imgur.com/wvf3ltm.png',
heavy: 'https://i.imgur.com/ECeqlZa.png', heavyTank: 'https://i.imgur.com/ECeqlZa.png',
}; };
export const TANK_ICONS_PREMIUM: Record<TankType, string> = { export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
tankDestroyer: 'https://i.imgur.com/TCu3EdR.png', 'AT-SPG': 'https://i.imgur.com/TCu3EdR.png',
light: 'https://i.imgur.com/zdkpTRb.png', lightTank: 'https://i.imgur.com/zdkpTRb.png',
medium: 'https://i.imgur.com/3z7eHX6.png', mediumTank: 'https://i.imgur.com/3z7eHX6.png',
heavy: 'https://i.imgur.com/P3vbmyA.png', heavyTank: 'https://i.imgur.com/P3vbmyA.png',
}; };
export const TANK_ICONS_COLLECTOR: Record<TankType, string> = { export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
tankDestroyer: 'https://i.imgur.com/WTjeirB.png', 'AT-SPG': 'https://i.imgur.com/WTjeirB.png',
light: 'https://i.imgur.com/EwhtKkU.png', lightTank: 'https://i.imgur.com/EwhtKkU.png',
medium: 'https://i.imgur.com/u8YDMBh.png', mediumTank: 'https://i.imgur.com/u8YDMBh.png',
heavy: 'https://i.imgur.com/8xRf3nc.png', heavyTank: 'https://i.imgur.com/8xRf3nc.png',
}; };
export const TIERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const; export const TIERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const;
+3 -1
View File
@@ -1,3 +1,4 @@
import { Locale } from 'discord.js';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { AllStats } from '../blitz/getAccountInfo'; import { AllStats } from '../blitz/getAccountInfo';
import getTankHistories, { import getTankHistories, {
@@ -45,8 +46,9 @@ export default async function getStatsInPeriod(
id: number, id: number,
start: number, start: number,
end: number, end: number,
locale: Locale,
) { ) {
const history = await getTankHistories(server, id, { const history = await getTankHistories(server, id, locale, {
includeLatestHistories: true, includeLatestHistories: true,
}); });
+10 -2
View File
@@ -1,9 +1,17 @@
import { SlashCommandStringOption } from 'discord.js'; import { Locale, SlashCommandStringOption } from 'discord.js';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
export default function addClanChoices(option: SlashCommandStringOption) { export default function addClanChoices(option: SlashCommandStringOption) {
const { translate } = translator(Locale.EnglishUS);
return option return option
.setName('clan') .setName('clan')
.setDescription('The clan name or tag you are checking') .setNameLocalizations(localizationObject('bot.common.options.clan'))
.setDescription(translate('bot.common.options.clan.description'))
.setDescriptionLocalizations(
localizationObject('bot.common.options.clan.description'),
)
.setAutocomplete(true) .setAutocomplete(true)
.setMinLength(2) .setMinLength(2)
.setRequired(true); .setRequired(true);
+102 -54
View File
@@ -1,60 +1,108 @@
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js'; import {
import { TREE_TYPE_NAMES, TreeType } from '../../components/Tanks'; APIApplicationCommandOptionChoice,
import { EncyclopediaInfo } from '../blitz/encyclopediaInfo'; Locale,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
} from 'discord.js';
import { TANK_CLASSES, TREE_TYPES } from '../../components/Tanks';
import { translator } from '../localization/translator';
import addTankChoices from './addTankChoices'; import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices'; import addTierChoices from './addTierChoices';
import { localizationObject } from './localizationObject';
export function addFilterOptions< export function addFilterOptions<
Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder, Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder,
>(option: Option, encyclopediaInfo: EncyclopediaInfo) { >(option: Option, nations: string[]) {
return option const { translate } = translator(Locale.EnglishUS);
.addStringOption((option) =>
option return (
.setName('nation') option
.setDescription('Nation') .addStringOption((option) =>
.addChoices( option
...Object.entries(encyclopediaInfo.vehicle_nations).map( .setName(translate('bot.common.options.filters.nation'))
([type, name]) => ({ name, value: type }), .setNameLocalizations(
), localizationObject('bot.common.options.filters.nation'),
) )
.setRequired(false), .setDescription(
) translate('bot.common.options.filters.nation.description'),
.addStringOption((option) => addTierChoices(option).setRequired(false)) )
.addStringOption((option) => .setDescriptionLocalizations(
option localizationObject('bot.common.options.filters.nation.description'),
.setName('tank-type') )
.setDescription('Tank type') .addChoices(
.addChoices( ...nations.map(
...Object.entries(encyclopediaInfo.vehicle_types).map( (nation) =>
([type, name]) => ({ ({
name: name.replace(' Tank', ''), value: nation,
value: type, name: translate(`common.nations.${nation}`),
}), name_localizations: localizationObject(
), `common.nations.${nation}`,
) ),
.setRequired(false), }) satisfies APIApplicationCommandOptionChoice,
) ),
.addStringOption((option) => )
option .setRequired(false),
.setName('tree-type') )
.setDescription('Tech tree type') .addStringOption((option) => addTierChoices(option).setRequired(false))
.addChoices( .addStringOption((option) =>
{ option
name: TREE_TYPE_NAMES['researchable'], .setName(translate('bot.common.options.filters.tank_class'))
value: 'researchable' satisfies TreeType, .setNameLocalizations(
}, localizationObject('bot.common.options.filters.tank_class'),
{ )
name: TREE_TYPE_NAMES.premium, .setDescription(
value: 'premium' satisfies TreeType, translate('bot.common.options.filters.tank_class.description'),
}, )
{ .setDescriptionLocalizations(
name: TREE_TYPE_NAMES.collector, localizationObject(
value: 'collector' satisfies TreeType, 'bot.common.options.filters.tank_class.description',
}, ),
) )
.setRequired(false), .addChoices(
) ...TANK_CLASSES.map(
.addStringOption((option) => (tankType) =>
addTankChoices(option).setRequired(false), ({
) as Option; value: tankType,
name: translate(`common.tank_class_short.${tankType}`),
name_localizations: localizationObject(
`common.tank_class_short.${tankType}`,
),
}) satisfies APIApplicationCommandOptionChoice,
),
)
.setRequired(false),
)
.addStringOption((option) =>
option
.setName(translate('bot.common.options.filters.tree_type'))
.setNameLocalizations(
localizationObject('bot.common.options.filters.tree_type'),
)
.setDescription(
translate('bot.common.options.filters.tree_type.description'),
)
.setDescriptionLocalizations(
localizationObject(
'bot.common.options.filters.tree_type.description',
),
)
.addChoices(
...TREE_TYPES.map(
(treeType) =>
({
value: treeType,
name: translate(`common.tree_type.${treeType}`),
name_localizations: localizationObject(
`common.tree_type.${treeType}`,
),
}) satisfies APIApplicationCommandOptionChoice,
),
)
.setRequired(false),
)
// TODO: somehow localize this?
.addStringOption((option) =>
addTankChoices(option).setRequired(false),
) as Option
);
} }
+91 -20
View File
@@ -1,49 +1,120 @@
import { import {
Locale,
SlashCommandBuilder, SlashCommandBuilder,
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder, SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
export type PeriodType = 'today' | '30' | '60' | '90' | 'career' | 'custom'; export type PeriodType = 'today' | '30' | '60' | '90' | 'career' | 'custom';
export type PeriodSize = 'today' | `${number}` | 'career'; export type PeriodSize = 'today' | `${number}` | 'career';
export default function addPeriodSubCommands( export function addPeriodSubCommands(
option: SlashCommandBuilder | SlashCommandSubcommandGroupBuilder, option: SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
extra: ( extra: (
option: SlashCommandSubcommandBuilder, option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option, ) => SlashCommandSubcommandBuilder = (option) => option,
) { ) {
const { translate } = translator(Locale.EnglishUS);
const subcommandNameLocalizations = {
today: localizationObject('bot.common.subcommands.today'),
30: localizationObject('bot.common.subcommands.30'),
60: localizationObject('bot.common.subcommands.60'),
90: localizationObject('bot.common.subcommands.90'),
career: localizationObject('bot.common.subcommands.career'),
custom: localizationObject('bot.common.subcommands.custom'),
};
const subcommandDescriptionLocalizations = {
today: localizationObject('bot.common.subcommands.today.description'),
30: localizationObject('bot.common.subcommands.30.description'),
60: localizationObject('bot.common.subcommands.60.description'),
90: localizationObject('bot.common.subcommands.90.description'),
career: localizationObject('bot.common.subcommands.career.description'),
custom: localizationObject('bot.common.subcommands.custom.description'),
};
option option
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('today').setDescription('Today')), extra(
) option
.addSubcommand((option) => .setName(translate('bot.common.subcommands.today'))
extra(option.setName('30').setDescription('30 days')), .setNameLocalizations(subcommandNameLocalizations.today)
) .setDescription(translate('bot.common.subcommands.today.description'))
.addSubcommand((option) => .setDescriptionLocalizations(
extra(option.setName('60').setDescription('60 days')), subcommandDescriptionLocalizations.today,
) ),
.addSubcommand((option) => ),
extra(option.setName('90').setDescription('90 days')),
)
.addSubcommand((option) =>
extra(option.setName('career').setDescription('Career')),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra( extra(
option option
.setName('custom') .setName(translate('bot.common.subcommands.30'))
.setDescription('Custom range') .setNameLocalizations(subcommandNameLocalizations[30])
.setDescription(translate('bot.common.subcommands.30.description'))
.setDescriptionLocalizations(subcommandDescriptionLocalizations[30]),
),
)
.addSubcommand((option) =>
extra(
option
.setName(translate('bot.common.subcommands.60'))
.setNameLocalizations(subcommandNameLocalizations[60])
.setDescription(translate('bot.common.subcommands.60.description'))
.setDescriptionLocalizations(subcommandDescriptionLocalizations[60]),
),
)
.addSubcommand((option) =>
extra(
option
.setName(translate('bot.common.subcommands.90'))
.setNameLocalizations(subcommandNameLocalizations[90])
.setDescription(translate('bot.common.subcommands.90.description'))
.setDescriptionLocalizations(subcommandDescriptionLocalizations[90]),
),
)
.addSubcommand((option) =>
extra(
option
.setName(translate('bot.common.subcommands.career'))
.setNameLocalizations(subcommandNameLocalizations.career)
.setDescription(
translate('bot.common.subcommands.career.description'),
)
.setDescriptionLocalizations(
subcommandDescriptionLocalizations.career,
),
),
)
.addSubcommand((option) =>
extra(
option
.setName(translate('bot.common.subcommands.custom'))
.setNameLocalizations(subcommandNameLocalizations.custom)
.setDescription(
translate('bot.common.subcommands.custom.description'),
)
.setDescriptionLocalizations(
subcommandDescriptionLocalizations.custom,
)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('start') .setName(translate('bot.common.subcommands.custom.options.start'))
.setDescription('Start in number of days ago from today') .setDescription(
translate(
'bot.common.subcommands.custom.options.start.description',
),
)
.setRequired(true), .setRequired(true),
) )
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('end') .setName(translate('bot.common.subcommands.custom.options.end'))
.setDescription('End in number of days') .setDescription(
translate(
'bot.common.subcommands.custom.options.end.description',
),
)
.setRequired(true), .setRequired(true),
), ),
), ),
+6 -7
View File
@@ -3,9 +3,9 @@ import {
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder, SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { encyclopediaInfo } from '../blitz/encyclopediaInfo'; import { NATIONS } from '../blitzkrieg/tankDefinitions';
import { addFilterOptions } from './addFilterOptions'; import { addFilterOptions } from './addFilterOptions';
import addPeriodSubCommands from './addPeriodSubCommands'; import { addPeriodSubCommands } from './addPeriodSubCommands';
export default async function addPeriodicFilterOptions< export default async function addPeriodicFilterOptions<
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder, OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
@@ -15,11 +15,10 @@ export default async function addPeriodicFilterOptions<
option: SlashCommandSubcommandBuilder, option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option, ) => SlashCommandSubcommandBuilder = (option) => option,
): Promise<OptionType> { ): Promise<OptionType> {
const awaitedEncyclopediaInfo = await encyclopediaInfo; const nations = await NATIONS;
addPeriodSubCommands(option, (option) =>
addPeriodSubCommands(option, (option) => { addFilterOptions(extra(option), nations),
return addFilterOptions(extra(option), awaitedEncyclopediaInfo); );
});
return option; return option;
} }
+26 -7
View File
@@ -1,14 +1,33 @@
import { SlashCommandStringOption } from 'discord.js'; import {
import { REGION_NAMES } from '../../constants/regions'; APIApplicationCommandOptionChoice,
Locale,
SlashCommandStringOption,
} from 'discord.js';
import { REGIONS } from '../../constants/regions';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
export default function addRegionChoices(option: SlashCommandStringOption) { export default function addRegionChoices(option: SlashCommandStringOption) {
const { t, translate } = translator(Locale.EnglishUS);
return option return option
.setName('region') .setName(t`bot.common.options.region`)
.setDescription('The Blitz region you are in') .setNameLocalizations(localizationObject('bot.common.options.region'))
.setDescription(t`bot.common.options.region.description`)
.setDescriptionLocalizations(
localizationObject('bot.common.options.region.description'),
)
.addChoices( .addChoices(
{ name: REGION_NAMES.com, value: 'com' }, ...REGIONS.map(
{ name: REGION_NAMES.eu, value: 'eu' }, (region) =>
{ name: REGION_NAMES.asia, value: 'asia' }, ({
value: region,
name: translate(`common.regions.normal.${region}`),
name_localizations: localizationObject(
`common.regions.normal.${region}`,
),
}) satisfies APIApplicationCommandOptionChoice,
),
) )
.setRequired(true); .setRequired(true);
} }
+11 -3
View File
@@ -1,9 +1,17 @@
import { SlashCommandStringOption } from 'discord.js'; import { Locale, SlashCommandStringOption } from 'discord.js';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
export default function addTankChoices(option: SlashCommandStringOption) { export default function addTankChoices(option: SlashCommandStringOption) {
const { translate } = translator(Locale.EnglishUS);
return option return option
.setName('tank') .setName(translate('bot.common.options.tank'))
.setDescription('The tank to get stats for') .setNameLocalizations(localizationObject('bot.common.options.tank'))
.setDescription(translate('bot.common.options.tank.description'))
.setDescriptionLocalizations(
localizationObject('bot.common.options.tank.description'),
)
.setAutocomplete(true) .setAutocomplete(true)
.setRequired(true); .setRequired(true);
} }
+29 -7
View File
@@ -1,16 +1,38 @@
import { SlashCommandStringOption } from 'discord.js'; import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandStringOption,
} from 'discord.js';
import { range } from 'lodash'; import { range } from 'lodash';
import { translator } from '../localization/translator';
import numberToRomanNumeral from '../math/numberToRomanNumeral'; import numberToRomanNumeral from '../math/numberToRomanNumeral';
import { localizationObject } from './localizationObject';
export default function addTierChoices(option: SlashCommandStringOption) { export default function addTierChoices(option: SlashCommandStringOption) {
const { t, translate } = translator(Locale.EnglishUS);
return option return option
.setName('tier') .setName(t`bot.common.options.tier`)
.setDescription('The tier you want to see') .setNameLocalizations(localizationObject('bot.common.options.tier'))
.setDescription(t`bot.common.options.tier.description`)
.setDescriptionLocalizations(
localizationObject('bot.common.options.tier.description'),
)
.setChoices( .setChoices(
...range(10, 0).map((tier) => ({ ...range(10, 0).map(
name: `Tier ${tier} - ${numberToRomanNumeral(tier)}`, (tier) =>
value: `${tier}`, ({
})), name: translate('bot.common.options.tier.choices.tier', [
`${tier}`,
numberToRomanNumeral(tier),
]),
value: `${tier}`,
name_localizations: localizationObject(
'bot.common.options.tier.choices.tier',
[`${tier}`, numberToRomanNumeral(tier)],
),
}) satisfies APIApplicationCommandOptionChoice,
),
) )
.setRequired(true); .setRequired(true);
} }
+11 -3
View File
@@ -1,8 +1,16 @@
import { SlashCommandStringOption } from 'discord.js'; import { Locale, SlashCommandStringOption } from 'discord.js';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
export default function addUsernameChoices(option: SlashCommandStringOption) { export default function addUsernameChoices(option: SlashCommandStringOption) {
const { t } = translator(Locale.EnglishUS);
return option return option
.setName('username') .setName(t`bot.common.options.username`)
.setDescription('The username you use in Blitz') .setNameLocalizations(localizationObject('bot.common.options.username'))
.setDescription(t`bot.common.options.username.description`)
.setDescriptionLocalizations(
localizationObject('bot.common.options.username.description'),
)
.setAutocomplete(true); .setAutocomplete(true);
} }
+8 -3
View File
@@ -3,8 +3,8 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_NAMES_SHORT } from '../../constants/regions';
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
import { translator } from '../localization/translator';
export const DISCORD_CHOICES_MAX_NAME_SIZE = 25; export const DISCORD_CHOICES_MAX_NAME_SIZE = 25;
export const OVERFLOW_SUFFIX = '…'; export const OVERFLOW_SUFFIX = '…';
@@ -12,6 +12,7 @@ export const OVERFLOW_SUFFIX = '…';
export default async function autocompleteClan( export default async function autocompleteClan(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
) { ) {
const { translate } = translator(interaction.locale);
const focusedOption = interaction.options.getFocused(true); const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'clan') return; if (focusedOption.name !== 'clan') return;
if (focusedOption.value.length < 2) return interaction.respond([]); if (focusedOption.value.length < 2) return interaction.respond([]);
@@ -20,7 +21,9 @@ export default async function autocompleteClan(
await interaction.respond( await interaction.respond(
clans clans
? clans.map((clan) => { ? clans.map((clan) => {
let name = `${clan.name} (${REGION_NAMES_SHORT[clan.region]})`; let name = `${clan.name} (${translate(
`common.regions.short.${clan.region}`,
)})`;
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE;
@@ -28,7 +31,9 @@ export default async function autocompleteClan(
name = `${clan.name.slice( name = `${clan.name.slice(
0, 0,
clan.name.length - overSize - OVERFLOW_SUFFIX.length, clan.name.length - overSize - OVERFLOW_SUFFIX.length,
)}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[clan.region]})`; )}${OVERFLOW_SUFFIX} (${translate(
`common.regions.short.${clan.region}`,
)})`;
} }
return { return {
+8 -5
View File
@@ -3,8 +3,8 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_NAMES_SHORT } from '../../constants/regions';
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
import { translator } from '../localization/translator';
import { import {
DISCORD_CHOICES_MAX_NAME_SIZE, DISCORD_CHOICES_MAX_NAME_SIZE,
OVERFLOW_SUFFIX, OVERFLOW_SUFFIX,
@@ -13,6 +13,7 @@ import {
export default async function autocompleteUsername( export default async function autocompleteUsername(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
) { ) {
const { translate } = translator(interaction.locale);
const focusedOption = interaction.options.getFocused(true); const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'username') return; if (focusedOption.name !== 'username') return;
const players = await searchPlayersAcrossRegions(focusedOption.value); const players = await searchPlayersAcrossRegions(focusedOption.value);
@@ -21,9 +22,9 @@ export default async function autocompleteUsername(
await interaction.respond( await interaction.respond(
players players
? players.map((player) => { ? players.map((player) => {
let name = `${player.nickname} (${ let name = `${player.nickname} (${translate(
REGION_NAMES_SHORT[player.region] `common.regions.short.${player.region}`,
})`; )})`;
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE;
@@ -31,7 +32,9 @@ export default async function autocompleteUsername(
name = `${player.nickname.slice( name = `${player.nickname.slice(
0, 0,
player.nickname.length - overSize - OVERFLOW_SUFFIX.length, player.nickname.length - overSize - OVERFLOW_SUFFIX.length,
)}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[player.region]})`; )}${OVERFLOW_SUFFIX} (${translate(
`common.regions.short.${player.region}`,
)})`;
} }
return { return {
+5 -2
View File
@@ -1,15 +1,18 @@
import { ChatInputCommandInteraction } from 'discord.js'; import { ChatInputCommandInteraction } from 'discord.js';
import { translator } from '../localization/translator';
import buttonPrimary from './buttonPrimary'; import buttonPrimary from './buttonPrimary';
export function buttonRefresh( export function buttonRefresh(
interaction: ChatInputCommandInteraction, interaction: ChatInputCommandInteraction,
path: string, path: string,
) { ) {
const { t } = translator(interaction.locale);
if ( if (
interaction.appPermissions?.has('AttachFiles') && interaction.appPermissions?.has('ReadMessageHistory') &&
interaction.appPermissions?.has('ViewChannel') interaction.appPermissions?.has('ViewChannel')
) { ) {
return buttonPrimary(path, 'Refresh'); return buttonPrimary(path, t`bot.common.buttons.refresh`);
} }
return null; return null;
@@ -0,0 +1,85 @@
import {
Locale,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
interface CreateLocalizedCommandSubcommand {
subcommand: string;
modify?: (option: SlashCommandSubcommandBuilder) => void;
}
interface CreateLocalizedCommandSubcommandGroup {
group: string;
modify?: (option: SlashCommandSubcommandGroupBuilder) => void;
}
export function createLocalizedCommand(
command: string,
sub?:
| CreateLocalizedCommandSubcommand[]
| CreateLocalizedCommandSubcommandGroup[],
) {
const commandPathItem = command.replaceAll('-', '_');
const { translate } = translator(Locale.EnglishUS);
const slashCommand = new SlashCommandBuilder()
.setName(command)
.setNameLocalizations(localizationObject(`bot.commands.${commandPathItem}`))
.setDescription(translate(`bot.commands.${commandPathItem}.description`))
.setDescriptionLocalizations(
localizationObject(`bot.commands.${commandPathItem}.description`),
);
if (sub) {
sub.map((subItem) => {
if ('subcommand' in subItem) {
const subcommandNameLocalizations = localizationObject(
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}`,
);
const subcommandDescriptionLocalizations = localizationObject(
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}.description`,
);
slashCommand.addSubcommand((option) => {
if (subItem.modify) subItem.modify(option);
return option
.setName(subItem.subcommand)
.setNameLocalizations(subcommandNameLocalizations)
.setDescription(
translate(
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}.description`,
),
)
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
});
} else {
const subcommandNameLocalizations = localizationObject(
`bot.commands.${commandPathItem}.groups.${subItem.group}`,
);
const subcommandDescriptionLocalizations = localizationObject(
`bot.commands.${commandPathItem}.groups.${subItem.group}.description`,
);
slashCommand.addSubcommandGroup((option) => {
if (subItem.modify) subItem.modify(option);
return option
.setName(subItem.group)
.setNameLocalizations(subcommandNameLocalizations)
.setDescription(
translate(
`bot.commands.${commandPathItem}.groups.${subItem.group}.description`,
),
)
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
});
}
});
}
return slashCommand;
}
+1 -1
View File
@@ -8,7 +8,7 @@ export function getFiltersFromButton(interaction: ButtonInteraction) {
return { return {
nation: url.searchParams.get('nation') ?? undefined, nation: url.searchParams.get('nation') ?? undefined,
tank: parseInt(url.searchParams.get('tank') ?? '0') || undefined, tank: parseInt(url.searchParams.get('tank') ?? '0') || undefined,
tankType: url.searchParams.get('tank-type') ?? undefined, tankType: url.searchParams.get('tank-class') ?? undefined,
treeType: treeType:
(url.searchParams.get('tree-type') as TreeType | undefined) ?? undefined, (url.searchParams.get('tree-type') as TreeType | undefined) ?? undefined,
tier: parseInt(url.searchParams.get('tier') ?? '0') || undefined, tier: parseInt(url.searchParams.get('tier') ?? '0') || undefined,
+5 -2
View File
@@ -11,10 +11,13 @@ export async function getFiltersFromCommand(
return { return {
nation: interaction.options.getString('nation') ?? undefined, nation: interaction.options.getString('nation') ?? undefined,
tier: parseInt(interaction.options.getString('tier') ?? '0') || undefined, tier: parseInt(interaction.options.getString('tier') ?? '0') || undefined,
tankType: interaction.options.getString('tank-type') ?? undefined, tankType: interaction.options.getString('tank-class') ?? undefined,
treeType: treeType:
(interaction.options.getString('tree-type') as TreeType | undefined) ?? (interaction.options.getString('tree-type') as TreeType | undefined) ??
undefined, undefined,
tank: tankRaw === null ? undefined : await resolveTankId(tankRaw), tank:
tankRaw === null
? undefined
: await resolveTankId(tankRaw, interaction.locale),
} satisfies StatFilters; } satisfies StatFilters;
} }
+16
View File
@@ -0,0 +1,16 @@
import { Locale } from 'discord.js';
import { SUPPORTED_LOCALES } from '../localization/strings';
import { translator } from '../localization/translator';
export function localizationObject(path: string, literals?: string[]) {
return SUPPORTED_LOCALES.reduce<Partial<Record<Locale, string>>>(
(localizations, locale) => {
const { translate } = translator(locale);
return {
...localizations,
[locale]: translate(path, literals),
};
},
{},
);
}
+3 -1
View File
@@ -3,11 +3,13 @@ import markdownEscape from 'markdown-escape';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { UserError } from '../../hooks/userError'; import { UserError } from '../../hooks/userError';
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
import { translator } from '../localization/translator';
import { serverAndIdPattern } from './resolvePlayerFromCommand'; import { serverAndIdPattern } from './resolvePlayerFromCommand';
export default async function resolveClanFromCommand( export default async function resolveClanFromCommand(
interaction: ChatInputCommandInteraction, interaction: ChatInputCommandInteraction,
) { ) {
const { translate } = translator(interaction.locale);
const clan = interaction.options.getString('clan', true); const clan = interaction.options.getString('clan', true);
if (serverAndIdPattern.test(clan)) { if (serverAndIdPattern.test(clan)) {
@@ -20,7 +22,7 @@ export default async function resolveClanFromCommand(
return { region: accounts[0].region, id: accounts[0].clan_id }; return { region: accounts[0].region, id: accounts[0].clan_id };
} else { } else {
throw new UserError( throw new UserError(
`# Could not find clan\nCouldn't find "${markdownEscape(clan)}". Try selecting a clan from the search result.`, translate('bot.common.errors.clan_not_found', [markdownEscape(clan)]),
); );
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@ export default function resolvePeriodFromButton(
start = getTimeDaysAgo(region, endDaysAgoMax); start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin); end = getTimeDaysAgo(region, startDaysAgoMin);
} else { } else {
name = getPeriodOptionName(periodSubcommand); name = getPeriodOptionName(periodSubcommand, interaction.locale);
start = getPeriodStart(region, periodSubcommand); start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow(); end = getPeriodNow();
} }
+13 -7
View File
@@ -1,14 +1,16 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction, Locale } from 'discord.js';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzkrieg/getPeriodNow'; import getPeriodNow from '../blitzkrieg/getPeriodNow';
import getPeriodStart from '../blitzkrieg/getPeriodStart'; import getPeriodStart from '../blitzkrieg/getPeriodStart';
import getTimeDaysAgo from '../blitzkrieg/getTimeDaysAgo'; import getTimeDaysAgo from '../blitzkrieg/getTimeDaysAgo';
import { translator } from '../localization/translator';
import { PeriodSize, PeriodType } from './addPeriodSubCommands'; import { PeriodSize, PeriodType } from './addPeriodSubCommands';
export function getPeriodOptionName(period: PeriodSize) { export function getPeriodOptionName(period: PeriodSize, locale: Locale) {
if (period === 'career') return 'Career'; const { translate } = translator(locale);
if (period === 'today') return 'Today'; if (period === 'career') return translate('bot.common.periods.career');
return `${period} days`; if (period === 'today') return translate('bot.common.periods.today');
return translate('bot.common.periods.days', [period]);
} }
export interface ResolvedPeriod { export interface ResolvedPeriod {
@@ -22,6 +24,7 @@ export default function resolvePeriodFromCommand(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
forcedPeriod?: PeriodType, forcedPeriod?: PeriodType,
) { ) {
const { translate } = translator(interaction.locale);
let name: string; let name: string;
let start: number; let start: number;
let end: number; let end: number;
@@ -35,11 +38,14 @@ export default function resolvePeriodFromCommand(
const startDaysAgoMin = Math.min(startOption, endOption); const startDaysAgoMin = Math.min(startOption, endOption);
const endDaysAgoMax = Math.max(startOption, endOption); const endDaysAgoMax = Math.max(startOption, endOption);
name = `${startDaysAgoMin} - ${endDaysAgoMax} days`; name = translate('bot.common.periods.custom', [
`${startDaysAgoMin}`,
`${endDaysAgoMax}`,
]);
start = getTimeDaysAgo(region, endDaysAgoMax); start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin); end = getTimeDaysAgo(region, startDaysAgoMin);
} else { } else {
name = getPeriodOptionName(periodSubcommand); name = getPeriodOptionName(periodSubcommand, interaction.locale);
start = getPeriodStart(region, periodSubcommand); start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow(); end = getPeriodNow();
} }
+6 -6
View File
@@ -4,6 +4,7 @@ import { Region } from '../../constants/regions';
import { UserError } from '../../hooks/userError'; import { UserError } from '../../hooks/userError';
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
import { getBlitzFromDiscord } from '../blitzkrieg/discordBlitz'; import { getBlitzFromDiscord } from '../blitzkrieg/discordBlitz';
import { translator } from '../localization/translator';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
@@ -15,6 +16,7 @@ export interface ResolvedPlayer {
export default async function resolvePlayerFromCommand( export default async function resolvePlayerFromCommand(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) { ) {
const { t, translate } = translator(interaction.locale);
const commandUsername = interaction.options.getString('username'); const commandUsername = interaction.options.getString('username');
if (commandUsername) { if (commandUsername) {
@@ -35,9 +37,9 @@ export default async function resolvePlayerFromCommand(
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
throw new UserError( throw new UserError(
`# Could not find user\nI couldn't find user "${markdownEscape( translate('bot.common.errors.player_not_found', [
commandUsername, markdownEscape(commandUsername),
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`, ]),
); );
} }
} }
@@ -47,9 +49,7 @@ export default async function resolvePlayerFromCommand(
if (account) { if (account) {
return { region: account.region, id: account.blitz }; return { region: account.region, id: account.blitz };
} else { } else {
throw new UserError( throw new UserError(t`bot.common.errors.player_not_linked`);
"# I don't know who you are\nUse the `/link` command to get started.",
);
} }
} }
} }
+21
View File
@@ -0,0 +1,21 @@
import { Locale } from 'discord.js';
export const SUPPORTED_LOCALES: Locale[] = [Locale.EnglishUS, Locale.SpanishES];
export type TranslationFragmentTree = {
[key: string]: TranslationFragment;
} & {
$?: string;
};
export type TranslationFragment = string | TranslationFragmentTree;
export const translations = SUPPORTED_LOCALES.map((locale) => ({
locale,
translations: require(`../../lang/${locale}.json`),
})).reduce(
(table, { locale, translations }) => ({
...table,
[locale]: translations,
}),
{},
) as Record<Locale, TranslationFragment>;
+74
View File
@@ -0,0 +1,74 @@
import { Locale } from 'discord.js';
import { SUPPORTED_LOCALES, translations } from './strings';
export function translator(localeRaw: Locale) {
const locale = SUPPORTED_LOCALES.includes(localeRaw)
? localeRaw
: Locale.EnglishUS;
const strings = translations[locale];
function translate(path: string, literals?: string[]): string {
const pathArray = path.split('.');
let fragment = strings;
let resolvedString: string;
for (const pathItem of pathArray) {
if (typeof fragment === 'string') {
throw new SyntaxError(
`Attempted to access string with key "${pathItem}" in "${path}" from within a string instead of an object in locale "${locale}"`,
);
} else {
fragment = fragment[pathItem];
if (typeof fragment === 'undefined') {
if (locale === Locale.EnglishUS) {
throw new Error(
`Undefined translation at "${pathItem}" in "${path}" for locale "${locale}"`,
);
} else {
console.warn(
`Undefined translation at "${pathItem}" in "${path}" for locale "${locale}"; falling back to en-US`,
);
return translator(Locale.EnglishUS).translate(path, literals);
}
}
}
}
if (typeof fragment === 'string') {
resolvedString = fragment;
} else if (fragment.$) {
resolvedString = fragment.$!;
} else {
throw new Error(
`Unresolved tree ending for "${path}" in locale "${locale}"`,
);
}
if (literals) {
const chunks = resolvedString.split('%s');
let embeddedString = '';
chunks.forEach((chunk, index) => {
if (index !== 0) embeddedString += literals[index - 1];
embeddedString += chunk;
});
return embeddedString;
}
return resolvedString;
}
function t(paths: TemplateStringsArray, ...embeds: string[]) {
return paths
.map((path, index) =>
path.length === 0
? ''
: `${translate(path)}${index === embeds.length ? '' : embeds[index]}`,
)
.join('');
}
return { locale, translate, t };
}
@@ -1,4 +1,4 @@
export function normalizeAnglePI(angle: number) { export function normalizeAngleRad(angle: number) {
const newAngle = angle % (2 * Math.PI); const newAngle = angle % (2 * Math.PI);
if (newAngle > Math.PI) return newAngle - 2 * Math.PI; if (newAngle > Math.PI) return newAngle - 2 * Math.PI;
if (newAngle < -Math.PI) return newAngle + 2 * Math.PI; if (newAngle < -Math.PI) return newAngle + 2 * Math.PI;
+1 -1
View File
@@ -29,7 +29,7 @@ export async function filterStats(
entry && entry &&
(filters.nation === undefined || entry.nation === filters.nation) && (filters.nation === undefined || entry.nation === filters.nation) &&
(filters.tier === undefined || entry.tier === filters.tier) && (filters.tier === undefined || entry.tier === filters.tier) &&
(filters.tankType === undefined || entry.type === filters.tankType) && (filters.tankType === undefined || entry.class === filters.tankType) &&
(filters.treeType === undefined || (filters.treeType === undefined ||
(filters.treeType === 'collector' && entry.treeType === 'collector') || (filters.treeType === 'collector' && entry.treeType === 'collector') ||
(filters.treeType === 'premium' && entry.treeType === 'premium') || (filters.treeType === 'premium' && entry.treeType === 'premium') ||
@@ -8,11 +8,11 @@ import {
InteractionReplyOptions, InteractionReplyOptions,
} from 'discord.js'; } from 'discord.js';
import { commands } from '..'; import { commands } from '..';
import discord from '../../../../discord.json' assert { type: 'json' };
import buttonLink from '../../../core/discord/buttonLink'; import buttonLink from '../../../core/discord/buttonLink';
import embedWarning from '../../../core/discord/embedWarning'; import embedWarning from '../../../core/discord/embedWarning';
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
import { psa } from '../../../core/discord/psa'; import { psa } from '../../../core/discord/psa';
import { translator } from '../../../core/localization/translator';
import { UserError } from '../../../hooks/userError'; import { UserError } from '../../../hooks/userError';
export default async function handleChatInputCommand( export default async function handleChatInputCommand(
@@ -23,19 +23,6 @@ export default async function handleChatInputCommand(
await interaction.deferReply(); await interaction.deferReply();
if (registry.inPreview && interaction.guildId !== discord.tres_guild_id) {
interaction.editReply({
embeds: [
embedWarning(
`\`/${registry.command.name}\` is in Public Preview`,
'[Join the official Discord server](https://discord.gg/nDt7AjGJQH) to gain early access to this command before it is released to the public.',
),
],
});
return;
}
try { try {
const returnable = await registry.handler(interaction); const returnable = await registry.handler(interaction);
@@ -107,10 +94,11 @@ export default async function handleChatInputCommand(
interaction.followUp(followUp); interaction.followUp(followUp);
} }
} catch (error) { } catch (error) {
const { t } = translator(interaction.locale);
const components = [ const components = [
new ActionRowBuilder<ButtonBuilder>().addComponents( new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setLabel('Get Help on Discord Server') .setLabel(t`bot.common.errors.get_help`)
.setURL('https://discord.gg/nDt7AjGJQH') .setURL('https://discord.gg/nDt7AjGJQH')
.setStyle(ButtonStyle.Link), .setStyle(ButtonStyle.Link),
), ),
@@ -125,8 +113,7 @@ export default async function handleChatInputCommand(
console.error(interaction.commandName, error); console.error(interaction.commandName, error);
interaction.editReply({ interaction.editReply({
content: content: t`bot.common.errors.uncaught_error`,
"# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
components, components,
}); });
} }
+26 -39
View File
@@ -17,7 +17,6 @@ import discord from '../../../discord.json' assert { type: 'json' };
import { aboutCommand } from '../../commands/about'; import { aboutCommand } from '../../commands/about';
import { breakdownCommand } from '../../commands/breakdown'; import { breakdownCommand } from '../../commands/breakdown';
import { debugCommand } from '../../commands/debug'; import { debugCommand } from '../../commands/debug';
import { eligibleCommand } from '../../commands/eligible';
import { evolutionCommand } from '../../commands/evolution'; import { evolutionCommand } from '../../commands/evolution';
import { fullStatsCommand } from '../../commands/fullStats'; import { fullStatsCommand } from '../../commands/fullStats';
import { inactiveCommand } from '../../commands/inactive'; import { inactiveCommand } from '../../commands/inactive';
@@ -25,12 +24,10 @@ import { verifyCommand } from '../../commands/link';
import { ownedTanksCommand } from '../../commands/ownedTanks'; import { ownedTanksCommand } from '../../commands/ownedTanks';
import { permissionsCommand } from '../../commands/permissions'; import { permissionsCommand } from '../../commands/permissions';
import { pingCommand } from '../../commands/ping'; import { pingCommand } from '../../commands/ping';
import { playerAchievementsCommand } from '../../commands/playerAchievements';
import { playerInfoCommand } from '../../commands/playerInfo'; import { playerInfoCommand } from '../../commands/playerInfo';
import { ratingsCommand } from '../../commands/ratings'; import { ratingsCommand } from '../../commands/ratings';
import { searchClansCommand } from '../../commands/searchClans'; import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers'; import { searchPlayersCommand } from '../../commands/searchPlayers';
import { searchTanksCommand } from '../../commands/searchTanks';
import { statsCommand } from '../../commands/stats'; import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today'; import { todayCommand } from '../../commands/today';
import getClientId from '../../core/blitzkrieg/getClientId'; import getClientId from '../../core/blitzkrieg/getClientId';
@@ -54,54 +51,39 @@ export type InteractionReturnable =
| InteractionIterableReturnable | InteractionIterableReturnable
| Promise<InteractionIterableReturnable>; | Promise<InteractionIterableReturnable>;
type CommandRegistryBase = { interface CommandRegistryBase {
inProduction: boolean;
inPublic: boolean;
command: command:
| SlashCommandBuilder | SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>; | Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
};
type CommandRegistryDefinitionBase = {
inProduction: boolean;
inPublic: boolean;
inPreview?: boolean;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void; autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable; button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
}; }
type CommandRegistryDefinitionHandlesInteraction = { interface CommandRegistryImplicit extends CommandRegistryBase {
handlesInteraction: true; handlesInteraction: true;
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void; handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
}; }
type CommandRegistryDefinitionDoesHandleInteraction = { interface CommandRegistryExplicit extends CommandRegistryBase {
handlesInteraction?: false; handlesInteraction?: false;
handler: ( handler: (
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable; ) => InteractionReturnable;
}; }
export type CommandRegistryPromisable = CommandRegistryBase & export type CommandRegistry = CommandRegistryImplicit | CommandRegistryExplicit;
(CommandRegistryDefinitionBase &
(
| CommandRegistryDefinitionHandlesInteraction
| CommandRegistryDefinitionDoesHandleInteraction
));
export type CommandRegistry =
| CommandRegistryPromisable
| Promise<CommandRegistryPromisable>;
const rest = new REST().setToken(secrets.DISCORD_TOKEN); export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const COMMANDS_RAW: CommandRegistry[] = [
permissionsCommand, permissionsCommand,
debugCommand, debugCommand,
eligibleCommand,
aboutCommand, aboutCommand,
inactiveCommand, inactiveCommand,
ownedTanksCommand, ownedTanksCommand,
playerAchievementsCommand,
playerInfoCommand, playerInfoCommand,
searchClansCommand, searchClansCommand,
searchPlayersCommand, searchPlayersCommand,
searchTanksCommand,
fullStatsCommand, fullStatsCommand,
breakdownCommand, breakdownCommand,
verifyCommand, verifyCommand,
@@ -112,20 +94,25 @@ export const COMMANDS_RAW: CommandRegistry[] = [
todayCommand, todayCommand,
]; ];
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) => export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
rawCommands.reduce<Record<string, CommandRegistryPromisable>>( return rawCommands.reduce<Record<string, CommandRegistry>>(
(accumulator, registry) => { (commands, registry, index) => {
if (isDev()) registry.command.setDefaultMemberPermissions(0); if (registry.status === 'rejected') {
console.warn(
`Command ${index} failed to load; skipping...`,
registry.reason,
);
return { return commands;
...accumulator, }
[registry.command.name]: registry, if (isDev()) registry.value.command.setDefaultMemberPermissions(0);
}; return { ...commands, [registry.value.command.name]: registry.value };
}, },
{}, {},
), );
); });
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
[]; [];
export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more