Compare commits

..
167 Commits
Author SHA1 Message Date
TrèsAbhi 990ab1520c Merge pull request #100 from tresabhi/dev 2024-03-16 15:12:54 -05:00
TrèsAbhi 81cb1afeff fix: types 2024-03-16 15:12:47 -05:00
TrèsAbhi dba5a0a2cd feat: logs 2024-03-16 15:10:55 -05:00
TrèsAbhi 04cd205fee feat: preview servers 2024-03-16 15:07:05 -05:00
TrèsAbhi ec93d8e9ab feat: better words 2024-03-16 10:41:25 -05:00
TrèsAbhi 56321d9e33 feat: simplified owned tanks command 2024-03-16 00:06:44 -05:00
TrèsAbhi d39ab43e74 feat: error handling for /research 2024-03-15 21:50:19 -05:00
TrèsAbhi 787d3ae6ea feat: functionality and icons 2024-03-15 12:28:29 -05:00
TrèsAbhi 035f751bf1 feat: remove protobuf 2024-03-15 10:47:05 -05:00
TrèsAbhi 43442ac829 feat: start of protobuf 2024-03-15 00:21:58 -05:00
TrèsAbhi da02a089b9 feat: xp definitions 2024-03-14 23:31:50 -05:00
TrèsAbhi 476d8e804a feat: some ui 2024-03-14 17:31:11 -05:00
TrèsAbhi 43e1d53d39 feat: resolving lines 2024-03-14 16:43:28 -05:00
TrèsAbhi 5372068830 feat: added trees 2024-03-13 23:23:06 -05:00
TrèsAbhi 025cec3687 Merge pull request #98 from tresabhi/main 2024-03-13 20:30:50 -05:00
TrèsAbhi c47c629a83 Merge pull request #95 from tresabhi/l10n 2024-03-13 20:30:09 -05:00
TrèsAbhi 2b7907f8f7 fix: use main 2024-03-13 20:29:28 -05:00
TrèsAbhi 903d4e4d95 fix: multiple equipments 2024-03-13 18:50:08 -05:00
TrèsAbhi 8857b2baae feat: production ready 2024-03-13 18:36:48 -05:00
TrèsAbhi f7bbb17f66 Merge pull request #96 from tresabhi/1.16.4 2024-03-13 18:28:59 -05:00
TrèsAbhi f407ffe4bb feat: ignore script types 2024-03-13 18:25:13 -05:00
TrèsAbhi 791a09daae feat: simplified imports 2024-03-13 18:15:13 -05:00
TrèsAbhi 9592c0b55f feat: logs 2024-03-13 18:00:25 -05:00
TrèsAbhi d9a34dc60e fix: use no-store 2024-03-13 17:52:06 -05:00
TrèsAbhi 30e4e4e9b3 fix: autoreloader dpm by crew stats 2024-03-13 17:46:48 -05:00
TrèsAbhi bd3ea9a8ec fix: crew stats for auto reloader dpm 2024-03-13 17:42:19 -05:00
TrèsAbhi 8131b69422 fix: use WOTB_DLC_CDN 2024-03-13 17:35:40 -05:00
TrèsAbhi 340d9c2666 feat: nicer layer penetration indices 2024-03-13 17:34:29 -05:00
TrèsAbhi 2a169fac34 fix: more accurate splash mechanics 2024-03-13 17:15:22 -05:00
TrèsAbhi 4819100ec6 feat: sharp-based texture manipulation 2024-03-13 17:00:40 -05:00
TrèsAbhi f39a94cbe0 fix: us depot 2024-03-13 16:52:52 -05:00
TrèsAbhi e61598f768 Update cd-assets.yml 2024-03-13 14:14:14 -05:00
TrèsAbhi 04ee3a0a44 fix: use correct gh token 2024-03-13 13:30:08 -05:00
TrèsAbhi 8fa613f1e2 fix: use github token 2024-03-13 13:29:14 -05:00
TrèsAbhi bdc240df08 feat: push on any 2024-03-13 13:27:15 -05:00
TrèsAbhi 7169e0d91a New translations en-us.json (Spanish) 2024-03-13 05:37:55 -05:00
TrèsAbhi 889ed7e191 feat: verbose 2024-03-13 00:22:54 -05:00
TrèsAbhi 1fb8dac805 feat: smarter host 2024-03-13 00:19:33 -05:00
TrèsAbhi 9f71ac68b9 feat: patches 2024-03-13 00:17:30 -05:00
TrèsAbhi c1f3517a79 Merge pull request #97 from tresabhi/main 2024-03-12 23:39:24 -05:00
TrèsAbhi adb0af0f18 Delete docs/changelogs/1.16.2 copy.md 2024-03-12 23:39:06 -05:00
TrèsAbhi f79f1ffdc3 feat: organize files 2024-03-12 23:36:56 -05:00
TrèsAbhi 1c9b7463fc New translations en-us.json (Croatian (disambiguation)) 2024-03-12 23:04:08 -05:00
TrèsAbhi 80741d66fc New translations en-us.json (Spanish) 2024-03-12 23:04:07 -05:00
TrèsAbhi f02fb00e8c fix: space after plus 2024-03-12 22:50:08 -05:00
TrèsAbhi 3d353cc3cf feat: updated workflow action versions 2024-03-12 22:48:41 -05:00
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
201 changed files with 5723 additions and 2800 deletions

No files matched your search

+14 -3
View File
@@ -2,10 +2,9 @@ name: CD Build Assets
on:
push:
branches: dev
workflow_dispatch:
# schedule:
# - cron: '0 0 * * *'
schedule:
- cron: '0 0 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -18,6 +17,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Use node
uses: actions/setup-node@v4
@@ -46,8 +48,17 @@ jobs:
- name: Run DepotDownloader
run: ./temp/depotDownloader/DepotDownloader.exe -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
- name: Apply patches
run: |
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
$env:DISCORD_BLITZ_DB = "${{ secrets.DISCORD_BLITZ_DB }}"
$env:WOTB_DLC_CDN = "${{ secrets.WOTB_DLC_CDN }}"
yarn setup:patches --depot
- name: Build assets
run: |
$env:GH_TOKEN = "${{ secrets.GH_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' || '' }}
+3 -3
View File
@@ -18,10 +18,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -54,7 +54,7 @@ jobs:
uses: actions/deploy-pages@v1
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: dist
path: docs/.vitepress/dist
+3 -1
View File
@@ -6,6 +6,8 @@ on:
schedule:
- cron: '0 */5 * * *'
workflow_dispatch:
release:
types: [created, published, released, edited]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -17,7 +19,7 @@ jobs:
steps:
- name: Use node
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18
+3 -3
View File
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -35,4 +35,4 @@ jobs:
run: yarn
- 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' }}
+3 -3
View File
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -35,4 +35,4 @@ jobs:
run: yarn
- 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' }}
+3 -3
View File
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -35,4 +35,4 @@ jobs:
run: yarn
- 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' }}
+3 -3
View File
@@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -37,7 +37,7 @@ jobs:
run: yarn build:bot
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: bot
path: dist/bot
+3 -3
View File
@@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -37,7 +37,7 @@ jobs:
run: yarn build:docs
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: docs
path: dist/docs
+3 -3
View File
@@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
@@ -37,7 +37,7 @@ jobs:
run: yarn build:website
- name: Archive build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: website
path: dist/website
-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
+2 -1
View File
@@ -17,4 +17,5 @@ tsconfig.tsbuildinfo
temp/
/.yarn
/.VSCodeCounter
/.yarn
/.yarn
depots
+3
View File
@@ -0,0 +1,3 @@
[submodule "submodules/closedkrieg"]
path = submodules/closedkrieg
url = https://github.com/tresabhi/closedkrieg
+8
View File
@@ -5,7 +5,9 @@
"accountinfo",
"aeson",
"alphatest",
"APAC",
"apcr",
"applypatches",
"artefacts",
"atgm",
"AYUV",
@@ -20,6 +22,7 @@
"CDON",
"classname",
"cleandist",
"closedkrieg",
"cockroackdb",
"Coef",
"cooldown",
@@ -40,8 +43,10 @@
"donotremove",
"drei",
"Droodles",
"dvpd",
"dvpl",
"DVPLCRC",
"dvpm",
"dxgi",
"Everytime",
"FASTNAME",
@@ -70,6 +75,7 @@
"lods",
"metallicness",
"minitelrose",
"Minotauro",
"MIPMAPCOUNT",
"modeltransform",
"MURICA",
@@ -129,6 +135,7 @@
"TYPELESS",
"ubjson",
"Unicum",
"UNLOCALIZED",
"UNORM",
"Unregistering",
"vickers",
@@ -138,6 +145,7 @@
"webp",
"winrate",
"wireframe",
"WOTB",
"wotblitz",
"xffn",
"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
+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
+19
View File
@@ -0,0 +1,19 @@
# Blitzkrieg 1.16.4
A few issues here and there addressed for good measure.
## Changes
- Armor layer penetration indices are now much more compact.
## Fixes
- Reverted splash mechanics changes from `1.16.1` as it made them less accurate.
- Auto-reloaders now report their DPM stats correctly with crew training.
## Technical Changes
- All files in the project were reviewed and constant exports were isolated.
- The downloaded game in the workflow now also includes the latest patches.
- Asset building now uses sharp under the hood which should in theory be faster.
- Fetching CDON LZ4 files now uses `no-store` instead of `no-cache`
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.17.0
A preview command and a touchup.
## Features
- Added `/research` in preview only accessible in the official Discord server.
- More details will be posted when the command is made public.
## Changes
- `/owned-tanks` now returns a simple text message listing all the tanks instead of an image.
- This is meant to improve the readability and ease of copying.
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkrieg",
"version": "1.16.0",
"version": "1.17.0",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
@@ -19,13 +19,15 @@
"start:website": "next start",
"host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
"setup:patches": "vite-node --script scripts/applyPatches",
"lint": "npx tsc",
"lint:website": "next lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"lint:version": "vite-node --script scripts/checkVersionMissMatch",
"knip": "knip",
"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",
"dependencies": {
@@ -33,7 +35,7 @@
"@octokit/plugin-throttling": "^8.1.3",
"@octokit/rest": "^20.0.1",
"@prisma/client": "^5.4.1",
"@radix-ui/colors": "^3.0.0-rc.4",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^2.0.1",
"@react-three/drei": "^9.93.0",
+48
View File
@@ -0,0 +1,48 @@
import { mkdir, writeFile } from 'fs/promises';
import { parse as parsePath } from 'path';
import { parse as parseYaml } from 'yaml';
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
import { writeDVPL } from '../src/core/blitz/writeDVPL';
import { secrets } from '../src/core/blitzkrieg/secrets';
import { dvp } from '../submodules/closedkrieg/src/dvp';
import { DATA } from './buildAssets/constants';
const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`);
const currentVersion = versionTextFile
.split(' ')[0]
.split('.')
.slice(0, 3)
.join('.');
console.log(`Installing patches for ${currentVersion}...`);
let patchIndex = 1;
while (true) {
const response = await fetch(
`${secrets.WOTB_DLC_CDN}/dlc/s${currentVersion}_${patchIndex}.yaml`,
);
if (response.status === 200) {
console.log(`Applying patch ${patchIndex}...`);
const data = parseYaml(await response.text());
const dvpm = await fetch(`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11}`).then(
(response) => response.arrayBuffer(),
);
const dvpd = await fetch(
`${secrets.WOTB_DLC_CDN}/dlc/${data.dx11.replace('.dvpm', '.dvpd')}`,
).then((response) => response.arrayBuffer());
const files = await dvp(dvpm, dvpd);
await Promise.all(
files.map(async ({ path, data }) => {
const { dir } = parsePath(path);
await mkdir(`${DATA}/${dir}`, { recursive: true });
await writeFile(`${DATA}/${path}.dvpl`, writeDVPL(Buffer.from(data)));
}),
);
patchIndex++;
} else break;
}
+4 -3
View File
@@ -2,10 +2,11 @@ import { readdir } from 'fs/promises';
import { argv } from 'process';
const isDepot = argv.includes('--depot');
export const WOTB_WIN32_DEPOT = 444202;
export const DATA = isDepot
? await (async () => {
const [installationVersion] = await readdir('depots/444202');
return `depots/444202/${installationVersion}/Data`;
const [installationVersion] = await readdir(`depots/${WOTB_WIN32_DEPOT}`);
return `depots/${WOTB_WIN32_DEPOT}/${installationVersion}/Data`;
})()
: 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data';
@@ -23,7 +24,7 @@ export const POI = {
collisionMeshes: '3d/Tanks/CollisionMeshes',
optionalDevices: 'XML/item_defs/vehicles/common/optional_devices.xml',
optionalDeviceSlots:
'XML/item_defs/vehicles/common/optional_device_slots.xml',
'XML/item_defs/vehicles/common/optional_device_slots.xml',
optionalDeviceImageMappings:
'UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml',
optionalDevicesImage:
+39
View File
@@ -0,0 +1,39 @@
import sharp from 'sharp';
import { readDVPLFile } from '../../src/core/blitz/readDVPLFile';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
import { DATA } from './constants';
const ICONS = [
['currency_silver_m.packed.webp', 'silver'],
['currency_premium_xl.packed.webp', 'premium'],
['currency_gold_m.packed.webp', 'gold'],
['currency_free-xp_xl.packed.webp', 'free-xp'],
['currency_elite-xp_xl.packed.webp', 'elite-xp'],
['currency_crew-xp_xl.packed.webp', 'crew-xp'],
['currency_battle-xp_xl.packed.webp', 'xp'],
];
export async function currencies(production: boolean) {
console.log('Building currency icons...');
const changes = await Promise.all(
ICONS.map(async ([file, name]) => {
const content = (
await sharp(
await readDVPLFile(`${DATA}/Gfx/Lobby/currency/${file}.dvpl`),
)
.trim()
.toBuffer()
).toString('base64');
return {
content,
encoding: 'base64',
path: `icons/currencies/${name}.webp`,
} satisfies FileChange;
}),
);
await commitAssets('currency icons', changes, production);
}
+165 -60
View File
@@ -1,8 +1,8 @@
import { readdir } from 'fs/promises';
import { readdir, writeFile } from 'fs/promises';
import { parse as parsePath } from 'path';
import { Vector3Tuple } from 'three';
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 { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
@@ -34,7 +34,6 @@ import {
import { DATA, POI } from './constants';
import { TankParameters } from './tankIcons';
type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
interface Strings {
[key: string]: string;
}
@@ -80,6 +79,16 @@ type VehicleDefinitionArmor = Record<
string,
number | { vehicleDamageFactor: 0; '#text': number }
>;
interface UnlocksInner {
cost: number;
'#text': number;
}
type Unlocks = {
[key in 'vehicle' | 'engine' | 'chassis' | 'turret' | 'gun']:
| UnlocksInner
| UnlocksInner[];
};
type UnlocksListing = Unlocks | undefined;
interface VehicleDefinitions {
invisibility: {
moving: number;
@@ -93,7 +102,7 @@ interface VehicleDefinitions {
};
consumableSlots: number;
provisionSlots: number;
optDevicePreset: string;
optDevicePreset: string | string[];
hull: {
armor: VehicleDefinitionArmor;
turretPositions: { turret: string };
@@ -103,6 +112,7 @@ interface VehicleDefinitions {
};
chassis: {
[key: string]: {
unlocks: UnlocksListing;
weight: number;
terrainResistance: string;
rotationSpeed: number;
@@ -131,10 +141,13 @@ interface VehicleDefinitions {
};
};
engines: {
[key: string]: unknown;
[key: string]: {
unlocks: UnlocksListing;
};
};
turrets0: {
[key: string]: {
unlocks: UnlocksListing;
rotationSpeed: number;
weight: number;
circularVisionRadius: number;
@@ -150,6 +163,7 @@ interface VehicleDefinitions {
};
guns: {
[key: string]: {
unlocks: UnlocksListing;
armor: VehicleDefinitionArmor & {
gun?:
| number
@@ -306,19 +320,6 @@ type ConsumablesVehicleFilter =
| { name: 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> = {
ARMOR_PIERCING: 'ap',
ARMOR_PIERCING_CR: 'ap_cr',
@@ -367,6 +368,8 @@ export async function definitions(production: boolean) {
`${DATA}/${POI.provisionsCommon}.dvpl`,
);
const tankXps = new Map<number, number>();
await Promise.all(
nations.map(async (nation) => {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
@@ -399,6 +402,10 @@ export async function definitions(production: boolean) {
);
for (const tankKey in tankList.root) {
const gunXps = new Map<number, number>();
const turretXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
const tank = tankList.root[tankKey];
const tankPrice: TankDefinitionPrice =
typeof tank.price === 'number'
@@ -441,6 +448,7 @@ export async function definitions(production: boolean) {
});
const crew: Crew[] = [];
const fixedCamouflage = tankTags.includes('eventCamouflage_user');
const totalUnlocks: UnlocksListing[] = [];
Object.entries(tankDefinition.root.crew).forEach(([key, value]) => {
let entry: Crew;
@@ -487,7 +495,8 @@ export async function definitions(production: boolean) {
forwards: tankDefinition.root.speedLimits.forward,
backwards: tankDefinition.root.speedLimits.backward,
},
equipment,
equipment:
typeof equipment === 'string' ? equipment : equipment.at(-1)!,
consumables: tankDefinition.root.consumableSlots,
provisions: tankDefinition.root.provisionSlots,
name:
@@ -502,7 +511,7 @@ export async function definitions(production: boolean) {
? 'premium'
: 'researchable',
tier: tank.level as Tier,
type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType],
class: tankTags[0] as TankClass,
testing: tankTags.includes('testTank') ? true : undefined,
price: tankPrice,
camouflage: {
@@ -545,6 +554,7 @@ export async function definitions(production: boolean) {
.split(' ')
.map(Number) as Vector3Tuple;
totalUnlocks.push(track.unlocks);
tankDefinitions[tankId].tracks.push({
id: trackId,
weight: track.weight,
@@ -571,16 +581,18 @@ export async function definitions(production: boolean) {
});
Object.keys(tankDefinition.root.engines).forEach((engineKey) => {
const engine = enginesList.root.shared[engineKey];
const engine = tankDefinition.root.engines[engineKey];
const engineListEntry = enginesList.root.shared[engineKey];
const engineId = toUniqueId(nation, enginesList.root.ids[engineKey]);
totalUnlocks.push(engine.unlocks);
tankDefinitions[tankId].engines.push({
id: engineId,
name: strings[engine.userString],
fireChance: engine.fireStartingChance,
tier: engine.level as Tier,
weight: engine.weight,
power: engine.power,
name: strings[engineListEntry.userString],
fireChance: engineListEntry.fireStartingChance,
tier: engineListEntry.level as Tier,
weight: engineListEntry.weight,
power: engineListEntry.power,
});
});
@@ -607,6 +619,7 @@ export async function definitions(production: boolean) {
.map(Number) as Vector3Tuple;
const turretArmor: ModelArmor = { thickness: {} };
totalUnlocks.push(turret.unlocks);
Object.keys(turret.armor)
.filter((name) => name.startsWith('armor_'))
.forEach((name) => {
@@ -659,11 +672,11 @@ export async function definitions(production: boolean) {
};
Object.keys(turret.guns).forEach((gunKey, gunIndex) => {
const turretGunEntry = turret.guns[gunKey];
const gun = turret.guns[gunKey];
const gunId = toUniqueId(nation, gunList.root.ids[gunKey]);
const gunListEntry = gunList.root.shared[gunKey];
const pitchLimitsRaw =
turretGunEntry.pitchLimits ?? gunListEntry.pitchLimits;
gun.pitchLimits ?? gunListEntry.pitchLimits;
const gunPitch = (
typeof pitchLimitsRaw === 'string'
? pitchLimitsRaw
@@ -672,43 +685,41 @@ export async function definitions(production: boolean) {
.split(' ')
.map(Number) as [number, number];
const gunModel = Number(
parsePath(turretGunEntry.models.undamaged).name.split('_')[1],
parsePath(gun.models.undamaged).name.split('_')[1],
);
const gunName =
strings[gunListEntry.userString] ?? gunKey.replaceAll('_', ' ');
const gunType =
'clip' in turretGunEntry
? turretGunEntry.pumpGunMode
'clip' in gun
? gun.pumpGunMode
? 'autoReloader'
: 'autoLoader'
: 'regular';
const gunReload =
gunType === 'autoReloader'
? turretGunEntry.pumpGunReloadTimes!.split(' ').map(Number)
: turretGunEntry.reloadTime;
? gun.pumpGunReloadTimes!.split(' ').map(Number)
: gun.reloadTime;
const gunClipCount =
gunType === 'regular' ? undefined : turretGunEntry.clip!.count;
const gunInterClip =
gunType === 'regular'
? undefined
: 60 / turretGunEntry.clip!.rate;
const front = turretGunEntry.extraPitchLimits?.front
? turretGunEntry.extraPitchLimits.front.split(' ').map(Number)
gunType === 'regular' ? undefined : gun.clip!.count;
const gunIntraClip =
gunType === 'regular' ? undefined : 60 / gun.clip!.rate;
const front = gun.extraPitchLimits?.front
? gun.extraPitchLimits.front.split(' ').map(Number)
: undefined;
const back = turretGunEntry.extraPitchLimits?.back
? turretGunEntry.extraPitchLimits.back.split(' ').map(Number)
const back = gun.extraPitchLimits?.back
? gun.extraPitchLimits.back.split(' ').map(Number)
: undefined;
const transition = turretGunEntry.extraPitchLimits?.transition
? typeof turretGunEntry.extraPitchLimits.transition === 'number'
? turretGunEntry.extraPitchLimits.transition
: turretGunEntry.extraPitchLimits.transition.at(-1)!
const transition = gun.extraPitchLimits?.transition
? typeof gun.extraPitchLimits.transition === 'number'
? gun.extraPitchLimits.transition
: gun.extraPitchLimits.transition.at(-1)!
: undefined;
const gunArmor: ModelArmor = { thickness: {} };
const shotDispersionFactors =
turretGunEntry.shotDispersionFactors ??
gunListEntry.shotDispersionFactors;
gun.shotDispersionFactors ?? gunListEntry.shotDispersionFactors;
Object.keys(turretGunEntry.armor)
totalUnlocks.push(gun.unlocks);
Object.keys(gun.armor)
.filter((name) => name.startsWith('armor_'))
.forEach((name) => {
const armorIdString = name.match(/armor_(\d+)/)?.[1];
@@ -716,7 +727,7 @@ export async function definitions(production: boolean) {
throw new SyntaxError(`Invalid armor id: ${name}`);
}
const armorId = parseInt(armorIdString);
const armorRaw = turretGunEntry.armor[name];
const armorRaw = gun.armor[name];
if (typeof armorRaw === 'number') {
gunArmor.thickness[armorId] = armorRaw;
} else {
@@ -736,15 +747,15 @@ export async function definitions(production: boolean) {
type: gunType,
reload: gunReload,
count: gunClipCount,
interClip: gunInterClip,
intraClip: gunIntraClip,
camouflageLoss:
typeof turretGunEntry.invisibilityFactorAtShot === 'number'
? turretGunEntry.invisibilityFactorAtShot
: turretGunEntry.invisibilityFactorAtShot.at(-1)!,
aimTime: turretGunEntry.aimingTime ?? gunListEntry.aimingTime,
typeof gun.invisibilityFactorAtShot === 'number'
? gun.invisibilityFactorAtShot
: gun.invisibilityFactorAtShot.at(-1)!,
aimTime: gun.aimingTime ?? gunListEntry.aimingTime,
dispersion: {
base:
turretGunEntry.shotDispersionRadius ??
gun.shotDispersionRadius ??
gunListEntry.shotDispersionRadius,
damaged: shotDispersionFactors.whileGunDamaged,
shot: shotDispersionFactors.afterShot,
@@ -756,11 +767,11 @@ export async function definitions(production: boolean) {
armor: gunArmor,
model: gunModel,
thickness:
turretGunEntry.armor.gun === undefined
gun.armor.gun === undefined
? 0
: typeof turretGunEntry.armor.gun === 'number'
? turretGunEntry.armor.gun
: turretGunEntry.armor.gun['#text'],
: typeof gun.armor.gun === 'number'
? gun.armor.gun
: gun.armor.gun['#text'],
pitch: {
min: gunPitch[0],
max: gunPitch[1],
@@ -822,10 +833,100 @@ export async function definitions(production: boolean) {
});
},
);
totalUnlocks.forEach((unlocks) => {
if (unlocks === undefined) return;
Object.entries(unlocks).forEach(([key, value]) => {
(Array.isArray(value) ? value : [value]).forEach((vehicle) => {
switch (key as keyof Unlocks) {
case 'vehicle': {
const tankListEntry = tankList.root[vehicle['#text']];
const currentTank = tankDefinitions[tankId];
const successorId = toUniqueId(nation, tankListEntry.id);
tankXps.set(successorId, vehicle.cost);
if (currentTank.successors === undefined) {
currentTank.successors = [];
}
if (!currentTank.successors!.includes(successorId)) {
currentTank.successors!.push(successorId);
}
break;
}
case 'gun': {
gunXps.set(
toUniqueId(nation, gunList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
case 'turret': {
turretXps.set(
toUniqueId(nation, turretList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
case 'engine': {
engineXps.set(
toUniqueId(nation, enginesList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
case 'chassis': {
trackXps.set(
toUniqueId(nation, chassisList.root.ids[vehicle['#text']]),
vehicle.cost,
);
break;
}
}
});
});
});
Object.values(tankDefinitions[tankId].turrets).forEach((turret) => {
turret.xp = turretXps.get(turret.id);
Object.values(turret.guns).forEach((gun) => {
gun.xp = gunXps.get(gun.id);
});
});
Object.values(tankDefinitions[tankId].engines).forEach((engine) => {
engine.xp = engineXps.get(engine.id);
});
Object.values(tankDefinitions[tankId].tracks).forEach((track) => {
track.xp = trackXps.get(track.id);
});
}
}),
);
Object.values(tankDefinitions).forEach((tank) => {
tank.xp = tankXps.get(tank.id);
});
Object.values(tankDefinitions).forEach((tank) => {
tank.successors?.forEach((predecessorId) => {
if (tankDefinitions[predecessorId].ancestors === undefined) {
tankDefinitions[predecessorId].ancestors = [];
}
if (!tankDefinitions[predecessorId].ancestors?.includes(tank.id)) {
tankDefinitions[predecessorId].ancestors?.push(tank.id);
}
});
});
Object.entries(optionalDevices.root).forEach(
([optionalDeviceKey, optionalDeviceEntry]) => {
if (optionalDeviceKey === 'nextAvailableId') return;
@@ -983,6 +1084,10 @@ export async function definitions(production: boolean) {
}
});
writeFile(
'test.tanks.cdon.lz4',
Buffer.from(superCompress(tankDefinitions), 'base64'),
);
await commitAssets(
'definitions',
[
+2
View File
@@ -14,6 +14,7 @@ import { boosterIcons } from './boosterIcons';
import { camouflageIcons } from './camouflageIcons';
import { circleFlags } from './circleFlags';
import { consumableProvisionIcons } from './consumableProvisionIcons';
import { currencies } from './currencies';
import { definitions } from './definitions';
import { equipmentIcons } from './equipmentIcons';
import { moduleIcons } from './moduleIcons';
@@ -38,6 +39,7 @@ const methods = [
boosterIcons,
tankArmors,
tankModels,
currencies,
];
for (const method of methods) {
-19
View File
@@ -1,21 +1,15 @@
import { NodeIO } from '@gltf-transform/core';
import { readdir } from 'fs/promises';
import { Vector3Tuple } from 'three';
import { extractModel } from '../../src/core/blitz/extractModel';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
// import { AssetCommit } from '../../src/core/github/assetCommit';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
import { DATA, POI } from './constants';
import { VehicleDefinitionList } from './definitions';
import { TankParameters } from './tankIcons';
interface VehicleCustomization {
armorColor: string;
}
export async function tankModels(production: boolean) {
console.log('Building tank models...');
@@ -27,18 +21,9 @@ export async function tankModels(production: boolean) {
for (const nationIndex in nations) {
const nation = nations[nationIndex];
const changes: FileChange[] = [];
// const assetCommit = new AssetCommit(`tank models ${nation}`, production);
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
);
const customization = await readXMLDVPL<{ root: VehicleCustomization }>(
`${DATA}/${POI.vehicleDefinitions}/${nation}/customization.xml.dvpl`,
);
const baseColor = customization.root.armorColor
.split(' ')
.slice(0, 3)
.map(Number)
.map((channel) => channel / 255) as Vector3Tuple;
console.log(`Building models for ${nation}`);
@@ -56,11 +41,8 @@ export async function tankModels(production: boolean) {
const model = await extractModel(
DATA,
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
baseColor,
);
// writeFile(`test.${tankKey}.glb`, await nodeIO.writeBinary(model));
changes.push({
path: `3d/tanks/models/${id}.glb`,
content: Buffer.from(await nodeIO.writeBinary(model)).toString(
@@ -71,7 +53,6 @@ export async function tankModels(production: boolean) {
}),
);
// assetCommit.push();
await commitAssets(`tank models ${nation}`, changes, production);
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"resolveJsonModule": true,
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"incremental": true,
"isolatedModules": true
}
}
+3 -5
View File
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { resetSession } from '../../../core/blitzkrieg/resetSession';
import {
TankDefinition,
tankDefinitions,
@@ -13,10 +14,7 @@ import { tankAverages } from '../../../core/blitzstars/tankAverages';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import mutateSession, {
resetSession,
useSession,
} from '../../../stores/session';
import mutateSession, { useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
@@ -311,7 +309,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) {
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
}
type="tank"
tankType={tankDefinitions?.type}
tankType={tankDefinitions?.class}
treeType={tankDefinitions?.treeType}
stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display];
+6 -2
View File
@@ -20,7 +20,11 @@ import PageWrapper from '../../../components/PageWrapper';
import { Skeleton } from '../../../components/Skeleton';
import { LEAGUES } from '../../../constants/leagues';
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 fetchBlitz from '../../../core/blitz/fetchBlitz';
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
@@ -556,7 +560,7 @@ export default function Page() {
<Select.Content>
{REGIONS.map((region) => (
<Select.Item key={region} value={region}>
{REGION_NAMES[region]}
{UNLOCALIZED_REGION_NAMES[region]}
</Select.Item>
))}
</Select.Content>
+5 -3
View File
@@ -18,11 +18,13 @@ import {
import { debounce } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { REGION_NAMES } from '../../../constants/regions';
import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
import searchPlayersAcrossRegions, {
AccountListWithServer,
} from '../../../core/blitz/searchPlayersAcrossRegions';
import { resetSession, setSession, useSession } from '../../../stores/session';
import { resetSession } from '../../../core/blitzkrieg/resetSession';
import { setSession } from '../../../core/blitzkrieg/setSession';
import { useSession } from '../../../stores/session';
import SessionPage from '../../embeds/session/page';
import { Menu } from './components/Menu';
import * as styles from './page.css';
@@ -107,7 +109,7 @@ export default function Page() {
setShowSearchResults(false);
setSession(region, id, nickname);
}}
shortcut={REGION_NAMES[region]}
shortcut={UNLOCALIZED_REGION_NAMES[region]}
>
{nickname}
</DropdownMenu.Item>
@@ -19,7 +19,7 @@ import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopedi
import {
SHELL_NAMES,
TIER_ROMAN_NUMERALS,
} from '../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
import { mutateDuel, useDuel } from '../../../../../stores/duel';
import {
mutateTankopediaTemporary,
@@ -1,6 +1,6 @@
import { Flex, Heading } from '@radix-ui/themes';
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
@@ -4,6 +4,7 @@ import { debounce } from 'lodash';
import { use, useEffect, useRef, useState } from 'react';
import { lerp } from 'three/src/math/MathUtils';
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { coefficient } from '../../../../../../core/blitzkrieg/coefficient';
@@ -17,7 +18,7 @@ import {
CREW_MEMBER_NAMES,
GUN_TYPE_NAMES,
SHELL_NAMES,
} from '../../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
import { useConsumable } from '../../../../../../hooks/useConsumable';
import { useEquipment } from '../../../../../../hooks/useEquipment';
@@ -56,6 +57,7 @@ export function Characteristics() {
turretModelDefinition.boundingBox,
),
);
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
const weight =
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
const weightTons = weight / 1000;
@@ -113,7 +115,7 @@ export function Characteristics() {
// TODO: add max rolls
[hasTungsten, 0.15],
);
const interClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const shellVelocityCoefficient = coefficient(
[hasSupercharger, 0.3],
[hasImprovedGunPowder, 0.3],
@@ -136,12 +138,13 @@ export function Characteristics() {
]);
const enginePowerCoefficient = coefficient(
[
hasEngineAccelerator && (tank.type === 'light' || tank.type === 'medium'),
hasEngineAccelerator &&
(tank.class === 'lightTank' || tank.class === 'mediumTank'),
0.05,
],
[
hasEngineAccelerator &&
(tank.type === 'heavy' || tank.type === 'tankDestroyer'),
(tank.class === 'heavyTank' || tank.class === 'AT-SPG'),
0.07,
],
[hasEnginePowerBoost, 0.2],
@@ -163,7 +166,7 @@ export function Characteristics() {
const viewRangeCoefficient =
coefficient([
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)]);
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
@@ -180,38 +183,26 @@ export function Characteristics() {
const camouflageSumMoving = sum(
[
hasCamouflageNet,
tank.type === 'heavy'
? 0.03
: tank.type === 'tankDestroyer'
? 0.07
: 0.05,
tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05,
],
[
camouflage,
tank.type === 'tankDestroyer'
? 0.04
: tank.type === 'heavy'
? 0.03
: 0.02,
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
],
);
const camouflageSumStill = sum(
[
hasCamouflageNet,
2 *
(tank.type === 'heavy'
(tank.class === 'heavyTank'
? 0.03
: tank.type === 'tankDestroyer'
: tank.class === 'AT-SPG'
? 0.07
: 0.05),
],
[
camouflage,
tank.type === 'tankDestroyer'
? 0.04
: tank.type === 'heavy'
? 0.03
: 0.02,
tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02,
],
);
@@ -221,7 +212,7 @@ export function Characteristics() {
shell,
damageCoefficient,
reloadCoefficient,
interClipCoefficient,
intraClipCoefficient,
);
useEffect(() => {
@@ -229,6 +220,17 @@ export function Characteristics() {
penetrationDistanceInput.current.value = `${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 (
<Flex direction="column" gap="8" style={{ width: '100%' }}>
@@ -266,9 +268,13 @@ export function Characteristics() {
<>
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
{gun.reload.at(-1)! < gun.reload.at(-2)!
? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) *
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
60
: (shell.damage.armor / gun.reload[0]) * 60}
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient)) *
60}
</InfoWithDelta>
<InfoWithDelta
decimals={0}
@@ -277,13 +283,26 @@ export function Characteristics() {
unit="hp / min"
>
{gun.reload.at(-1)! < gun.reload.at(-2)!
? (shell.damage.armor / (gun.reload.at(-1)! + gun.interClip)) *
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(0, -1).length - 1) * gun.interClip) +
shell.damage.armor * gun.reload.slice(0, -1).length
: (shell.damage.armor / (gun.reload[0] + gun.interClip)) *
(60 - (gun.reload.slice(1).length - 1) * gun.interClip) +
shell.damage.armor * gun.reload.slice(1).length}
(gun.reload.slice(0, -1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient *
shell.damage.armor *
gun.reload.slice(0, -1).length
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
(60 -
(gun.reload.slice(1).length - 1) *
gun.intraClip *
intraClipCoefficient) +
damageCoefficient *
shell.damage.armor *
gun.reload.slice(1).length}
</InfoWithDelta>
<Info
indent
@@ -296,14 +315,15 @@ export function Characteristics() {
</a>
}
/>
<Info indent name="Optimal shell index">
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1}
</Info>
</>
)}
{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) => (
<InfoWithDelta
key={index}
@@ -312,7 +332,7 @@ export function Characteristics() {
decimals={2}
deltaType="lowerIsBetter"
>
{reload}
{reload * reloadCoefficient}
</InfoWithDelta>
))}
</>
@@ -326,17 +346,18 @@ export function Characteristics() {
{gun.reload * reloadCoefficient}
</InfoWithDelta>
)}
{gun.type === 'autoLoader' && (
<InfoWithDelta
indent
decimals={2}
name="Inter-clip reload"
unit="s"
deltaType="lowerIsBetter"
>
{gun.interClip * interClipCoefficient}
</InfoWithDelta>
)}
{gun.type === 'autoLoader' ||
(gun.type === 'autoReloader' && (
<InfoWithDelta
indent
decimals={2}
name="Intra-clip"
unit="s"
deltaType="lowerIsBetter"
>
{gun.intraClip * intraClipCoefficient}
</InfoWithDelta>
))}
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
{shell.caliber}
</InfoWithDelta>
@@ -468,10 +489,12 @@ export function Characteristics() {
<Info name="Gun flexibility" unit="°" />
<InfoWithDelta decimals={1} indent name="Depression">
{gunModelDefinition.pitch.max +
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)}
</InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Elevation">
{-gunModelDefinition.pitch.min +
{-gunModelDefinition.pitch.min -
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)}
</InfoWithDelta>
{gunModelDefinition.pitch.front && (
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react';
import { Vector3 } from 'three';
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../core/blitzkrieg/hasEquipment';
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose';
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) {
case Pose.HullDown: {
const [pitch] = applyPitchYawLimits(
@@ -75,6 +78,7 @@ export function Controls() {
0,
protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
camera.position
@@ -106,6 +110,7 @@ export function Controls() {
0,
protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
camera.position
@@ -11,6 +11,7 @@ import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinit
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
import mutateTankopediaPersistent, {
@@ -29,6 +30,7 @@ import { TankModel } from './components/TankModel';
export function TankSandbox() {
const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122);
const awaitedModelDefinitions = use(modelDefinitions);
const canvasWrapper = useRef<HTMLDivElement>(null);
const [isFullScreen, setIsFullScreen] = useState(false);
@@ -70,6 +72,7 @@ export function TankSandbox() {
0,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
@@ -87,6 +90,7 @@ export function TankSandbox() {
0,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
@@ -104,6 +108,7 @@ export function TankSandbox() {
0,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
@@ -131,6 +136,7 @@ export function TankSandbox() {
draft.protagonist!.yaw,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
});
}, [protagonist.gun, protagonist.turret]);
@@ -4,13 +4,14 @@ import { Euler, Group, Vector3 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../../../core/blitzkrieg/hasEquipment';
import { jsxTree } from '../../../../../../../core/blitzkrieg/jsxTree';
import { modelDefinitions } from '../../../../../../../core/blitzkrieg/modelDefinitions';
import {
ModelTransformEventData,
modelTransformEvent,
} from '../../../../../../../core/blitzkrieg/modelTransform';
import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180';
import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngleRad';
import { useAwait } from '../../../../../../../hooks/useAwait';
import { useModel } from '../../../../../../../hooks/useModel';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
@@ -106,7 +107,6 @@ export const TankModel = memo(() => {
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const { gltf } = useModel(protagonist.tank.id);
const nodes = Object.values(gltf.nodes);
@@ -161,19 +161,21 @@ export const TankModel = memo(() => {
window.addEventListener('pointerup', handlePointerUp);
}
}
function handlePointerMove(event: PointerEvent) {
async function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
[pitch, yaw] = applyPitchYawLimits(
pitch,
yaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
}
function handlePointerUp() {
mutateDuel((draft) => {
draft.protagonist!.pitch = normalizeAnglePI(pitch);
draft.protagonist!.yaw = normalizeAnglePI(yaw);
draft.protagonist!.pitch = normalizeAngleRad(pitch);
draft.protagonist!.yaw = normalizeAngleRad(yaw);
});
mutateTankopediaPersistent((draft) => {
draft.model.visual.controlsEnabled = true;
@@ -220,12 +222,14 @@ export const TankModel = memo(() => {
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
async function handlePointerMove(event: PointerEvent) {
const hasImprovedVerticalStabilizer = await hasEquipment(122);
[pitch, yaw] = applyPitchYawLimits(
pitch - event.movementY * (Math.PI / canvas.height),
yaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
}
@@ -234,8 +238,8 @@ export const TankModel = memo(() => {
draft.model.visual.controlsEnabled = true;
});
mutateDuel((draft) => {
draft.protagonist!.pitch = normalizeAnglePI(pitch);
draft.protagonist!.yaw = normalizeAnglePI(yaw);
draft.protagonist!.pitch = normalizeAngleRad(pitch);
draft.protagonist!.yaw = normalizeAngleRad(yaw);
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
@@ -1,5 +1,6 @@
import { Flex, TextField } from '@radix-ui/themes';
import { use, useEffect, useRef } from 'react';
import { degToRad, radToDeg } from 'three/src/math/MathUtils';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
@@ -20,22 +21,21 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
const mode = useTankopediaPersistent((state) => state.mode);
const hasCalibratedShells = useEquipment(103, true);
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 turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.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 (
<Flex
@@ -68,22 +68,22 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
Yaw
</TextField.Slot>
<TextField.Input
defaultValue={Math.round(protagonist.yaw * (180 / Math.PI))}
onBlur={() => {
const [pitch, yaw] = applyPitchYawLimits(
protagonist.pitch,
-Number(turretYawInput.current?.value) * (Math.PI / 180),
degToRad(Number(turretYawInput.current!.value)),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
mutateDuel((state) => {
state.protagonist!.pitch = pitch;
state.protagonist!.yaw = yaw;
});
turretYawInput.current!.value = `${Math.round(
yaw * (180 / Math.PI),
)}`;
turretYawInput.current!.value = radToDeg(protagonist.yaw).toFixed(
1,
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
@@ -116,22 +116,24 @@ export function RotationInputs({ isFullScreen }: RotationInputsProps) {
Pitch
</TextField.Slot>
<TextField.Input
defaultValue={-Math.round(protagonist.pitch * (180 / Math.PI))}
onBlur={() => {
const [pitch, yaw] = applyPitchYawLimits(
-Number(gunPitchInput.current?.value) * (Math.PI / 180),
degToRad(
-Number(gunPitchInput.current!.value) + initialGunPitch,
),
protagonist.yaw,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
mutateDuel((state) => {
state.protagonist!.pitch = pitch;
state.protagonist!.yaw = yaw;
});
gunPitchInput.current!.value = `${Math.round(
pitch * (180 / Math.PI),
)}`;
gunPitchInput.current!.value = (
-radToDeg(protagonist.pitch) + initialGunPitch
).toFixed(1);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
@@ -1,32 +1,30 @@
import { Color } from 'three';
import InfiniteGridHelper from '../../../../../components/InfiniteGridHelper';
import { useFrame, useLoader } from '@react-three/fiber';
import { clamp } from 'lodash';
import { useRef } from 'react';
import { MeshStandardMaterial, TextureLoader } from 'three';
import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
export function SceneProps() {
const showGrid = useTankopediaPersistent(
(state) => state.model.visual.showGrid,
);
const showEnvironment = useTankopediaPersistent(
(state) => state.model.visual.showEnvironment,
const show = useTankopediaPersistent(
(state) =>
state.model.visual.showGrid && !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 (
<>
<InfiniteGridHelper
size1={1 / 5}
size2={1}
distance={20}
color={new Color('#ffffff')}
/>
<InfiniteGridHelper
position={[0, 1e-4, 0]}
size1={0}
size2={100}
distance={25}
color={new Color('red')}
/>
</>
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial ref={material} map={texture} transparent />
</mesh>
);
}
@@ -1,7 +1,7 @@
import { UpdateIcon } from '@radix-ui/react-icons';
import { Button, Dialog, Flex, Heading } from '@radix-ui/themes';
import { useState } from 'react';
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks';
import { TREE_TYPE_ICONS } from '../../../../../components/Tanks/components/Item/constants';
import { assignDuelMember } from '../../../../../core/blitzkrieg/assignDuelMember';
import { pushTankopediaPath } from '../../../../../core/blitzkrieg/pushTankopediaPath';
import { useDuel } from '../../../../../stores/duel';
@@ -30,7 +30,7 @@ export function Title() {
<img
src={
TREE_TYPE_ICONS[protagonist.tank.treeType][
protagonist.tank.type
protagonist.tank.class
]
}
style={{
@@ -1,9 +1,7 @@
import { Button, Flex, Text } from '@radix-ui/themes';
import { SmallTankIcon } from '../../../../../../components/SmallTankIcon';
import {
TIER_ROMAN_NUMERALS,
TankDefinition,
} from '../../../../../../core/blitzkrieg/tankDefinitions';
import { TankDefinition } from '../../../../../../core/blitzkrieg/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
interface CompactSearchResultRowProps {
tanks: TankDefinition[];
@@ -4,17 +4,18 @@ import { CaretDownIcon } from '@radix-ui/react-icons';
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
import { use } from 'react';
import { Flag } from '../../../../../../../components/Flag';
import {} from '../../../../../../../components/Tanks';
import {
TANK_TYPES,
TANK_CLASSES,
TREE_TYPES,
TREE_TYPE_ICONS,
TREE_TYPE_IMAGES,
} from '../../../../../../../components/Tanks';
} from '../../../../../../../components/Tanks/components/Item/constants';
import { NATIONS } from '../../../../../../../core/blitzkrieg/tankDefinitions';
import {
NATIONS,
TIERS,
TIER_ROMAN_NUMERALS,
} from '../../../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../../../core/blitzkrieg/tankDefinitions/constants';
import mutateTankopediaPersistent, {
TankopediaTestTankDisplay,
useTankopediaPersistent,
@@ -121,7 +122,7 @@ export function Options() {
))}
</Flex>
<Flex>
{[...TANK_TYPES].map((type, index) => (
{[...TANK_CLASSES].map((type, index) => (
<Button
key={type}
color={filters.types.includes(type) ? undefined : 'gray'}
@@ -132,9 +133,9 @@ export function Options() {
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius:
index === TANK_TYPES.length - 1 ? undefined : 0,
index === TANK_CLASSES.length - 1 ? undefined : 0,
borderBottomRightRadius:
index === TANK_TYPES.length - 1 ? undefined : 0,
index === TANK_CLASSES.length - 1 ? undefined : 0,
}}
onClick={() =>
mutateTankopediaPersistent((draft) => {
@@ -15,7 +15,8 @@ import {
} from '@radix-ui/themes';
import { go } from 'fuzzysort';
import { use, useEffect, useMemo, useRef, useState } from 'react';
import { TankType, TreeType } from '../../../../../components/Tanks';
import { TreeType } from '../../../../../components/Tanks';
import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants';
import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration';
import { asset } from '../../../../../core/blitzkrieg/asset';
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
@@ -24,13 +25,15 @@ import { resolveDpm } from '../../../../../core/blitzkrieg/resolveDpm';
import { resolveReload } from '../../../../../core/blitzkrieg/resolveReload';
import {
NATIONS,
TankDefinition,
tanksDefinitionsArray,
} from '../../../../../core/blitzkrieg/tankDefinitions';
import {
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
TIER_ROMAN_NUMERALS,
TankDefinition,
tanksDefinitionsArray,
} from '../../../../../core/blitzkrieg/tankDefinitions';
} from '../../../../../core/blitzkrieg/tankDefinitions/constants';
import { tankIcon } from '../../../../../core/blitzkrieg/tankIcon';
import { unionBoundingBox } from '../../../../../core/blitzkrieg/unionBoundingBox';
import { theme } from '../../../../../stitches.config';
@@ -49,7 +52,6 @@ interface TankSearchProps {
onSelect?: (tank: TankDefinition) => void;
}
const tankTypeOrder: TankType[] = ['light', 'medium', 'heavy', 'tankDestroyer'];
const treeTypeOrder: TreeType[] = ['researchable', 'premium', 'collector'];
export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
@@ -70,7 +72,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
)
.sort(
(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(
@@ -89,7 +91,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
: filters.tiers.includes(tank.tier)) &&
(filters.types.length === 0
? true
: filters.types.includes(tank.type)) &&
: filters.types.includes(tank.class)) &&
(filters.treeTypes.length === 0
? true
: filters.treeTypes.includes(tank.treeType)) &&
@@ -844,7 +846,7 @@ export function TankSearch({ compact, onSelect = () => {} }: TankSearchProps) {
? TANK_ICONS_COLLECTOR
: tank.treeType === 'premium'
? TANK_ICONS_PREMIUM
: TANK_ICONS)[tank.type]
: TANK_ICONS)[tank.class]
}
style={{ width: '1em', height: '1em' }}
/>
+54 -52
View File
@@ -1,67 +1,69 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate';
const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`;
const DOCS = {
const subcommands = [
'embeds',
'introduction',
'invite',
'timezones',
'creators',
] as const;
type Subcommand = (typeof subcommands)[number];
const DOCS: Record<Subcommand, string> = {
embeds: 'guide/embeds',
introduction: 'guide/introduction',
invite: 'guide/invite',
timezones: 'guide/timezones',
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) {
Object.entries(DOC_DESCRIPTIONS).forEach(([key, value]) => {
option.addSubcommand((sub) => sub.setName(key).setDescription(value));
});
export const aboutCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
inProduction: true,
inPublic: true,
return option;
}
command: createLocalizedCommand('about', [
{ subcommand: 'commands' },
...subcommands.map((subcommand) => ({ subcommand })),
]),
export const aboutCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
const { t } = translator(interaction.locale);
command: addDocsSubcommands(
new SlashCommandBuilder()
.setName('about')
.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}`,
if (subcommand === 'commands') {
return t`bot.commands.about.subcommands.commands.body${(
await Promise.all(COMMANDS_RAW)
)
.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 content = fetch(url).then((response) => response.text());
const url = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/${DOCS[subcommand as Subcommand]}.md`;
const content = fetch(url).then((response) => response.text());
return content;
},
};
return content;
},
});
});
+71
View File
@@ -0,0 +1,71 @@
import { getBlitzStarsLinkButton } from '../../core/blitzstars/getBlitzStarsLinkButton';
import addPeriodicFilterOptions from '../../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../../core/discord/addUsernameChoices';
import autocompleteTanks from '../../core/discord/autocompleteTanks';
import autocompleteUsername from '../../core/discord/autocompleteUsername';
import { buttonRefresh } from '../../core/discord/buttonRefresh';
import commandToURL from '../../core/discord/commandToURL';
import { createLocalizedCommand } from '../../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../../events/interactionCreate';
import { renderBreakdown } from './render';
export const breakdownCommand = new Promise<CommandRegistry>(
async (resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: await addPeriodicFilterOptions(
createLocalizedCommand('breakdown'),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
return [
...(await renderBreakdown(
player,
period,
filters,
interaction.locale,
)),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(
player,
period,
filters,
interaction.locale,
);
},
});
},
);
@@ -1,41 +1,25 @@
import { SlashCommandBuilder } from 'discord.js';
import { Locale } from 'discord.js';
import { chunk } from 'lodash';
import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import * as Breakdown from '../../components/Breakdown';
import CommandWrapper from '../../components/CommandWrapper';
import NoData from '../../components/NoData';
import TitleBar from '../../components/TitleBar';
import { AllStats, getAccountInfo } from '../../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../../core/blitz/getClanAccountInfo';
import getTankStats from '../../core/blitz/getTankStats';
import { filtersToDescription } from '../../core/blitzkrieg/filtersToDescription';
import {
TankDefinition,
tankDefinitions,
} from '../core/blitzkrieg/tankDefinitions';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../core/blitzstars/tankAverages';
import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary';
import commandToURL from '../core/discord/commandToURL';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand, {
ResolvedPeriod,
} from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import calculateWN8 from '../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryPromisable } from '../events/interactionCreate';
} from '../../core/blitzkrieg/tankDefinitions';
import getStatsInPeriod from '../../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand';
import { translator } from '../../core/localization/translator';
import calculateWN8 from '../../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../../core/statistics/filterStats';
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
@@ -44,15 +28,17 @@ export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { t, translate } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
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 accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ['clan']);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters);
const tankStats = await getTankStats(region, id, locale);
const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: AllStats[] = [];
const orderedCareerStats: AllStats[] = [];
const orderedCurrentWN8: (number | undefined)[] = [];
@@ -144,21 +130,21 @@ export async function renderBreakdown(
<Breakdown.Row
key={id}
type="summary"
title="Total"
title={t`bot.commands.breakdown.body.total`}
stats={[
{
title: 'Battles',
title: t`bot.commands.breakdown.body.battles`,
current: currentBattles,
career: careerBattles,
},
{
title: 'Winrate',
title: t`bot.commands.breakdown.body.winrate`,
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
},
{
title: 'WN8',
title: t`bot.commands.breakdown.body.wn8`,
current:
currentWN8 === undefined
? undefined
@@ -173,7 +159,7 @@ export async function renderBreakdown(
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
title: t`bot.commands.breakdown.body.damage`,
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
@@ -195,17 +181,20 @@ export async function renderBreakdown(
<Breakdown.Row
key={id}
type="tank"
tankType={tankDefinition?.type}
tankType={tankDefinition?.class}
treeType={tankDefinition?.treeType}
title={tankDefinition?.name ?? `Unknown tank ${id}`}
title={
tankDefinition?.name ??
translate('bot.commands.breakdown.body.unknown_tank', [`${id}`])
}
stats={[
{
title: 'Battles',
title: t`bot.commands.breakdown.body.battles`,
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: 'Winrate',
title: t`bot.commands.breakdown.body.winrate`,
current: `${(100 * (current.wins / current.battles)).toFixed(
2,
)}%`,
@@ -214,7 +203,7 @@ export async function renderBreakdown(
current.wins / current.battles - career.wins / career.battles,
},
{
title: 'WN8',
title: t`bot.commands.breakdown.body.wn8`,
current:
currentWN8 === undefined
? undefined
@@ -229,7 +218,7 @@ export async function renderBreakdown(
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
title: t`bot.commands.breakdown.body.damage`,
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
@@ -254,10 +243,10 @@ export async function renderBreakdown(
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<Wrapper>
<CommandWrapper>
{index === 0 && (
<TitleBar
name={accountInfo.nickname}
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
@@ -268,14 +257,14 @@ export async function renderBreakdown(
)}
<Breakdown.Root>{page}</Breakdown.Root>
</Wrapper>
</CommandWrapper>
))
.reverse();
} else {
return [
<Wrapper>
<CommandWrapper>
<TitleBar
name={accountInfo.nickname}
title={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
@@ -284,56 +273,8 @@ export async function renderBreakdown(
description={`${name} • ${filterDescriptions}`}
/>
<NoData type={NoDataType.BattlesInPeriod} />
</Wrapper>,
<NoData type="battles_in_period" locale={locale} />
</CommandWrapper>,
];
}
}
export const breakdownCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('breakdown')
.setDescription("A period's breakdown by tanks played"),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
inProduction: true,
inPublic: true,
command,
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
return [
...(await renderBreakdown(player, period, filters)),
buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters);
},
});
},
);
+32 -28
View File
@@ -1,36 +1,40 @@
import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' };
import { client } from '../core/discord/client';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
const executionStart = new Date().getTime();
export const debugCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
export const debugCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('debug')
.setDescription('Debug information about the bot'),
command: createLocalizedCommand('debug'),
async handler() {
const currentTime = new Date().getTime();
const uptime = currentTime - executionStart;
const list = [
['Version', packageJSON.version],
['Shard', client.shard?.ids[0] ?? 'default'],
[
'Uptime',
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
(uptime / 1000 / 60) % 60,
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
uptime % 1000,
)}ms`,
],
]
.map(([key, value]) => `- ${key}: ${value}`)
.join('\n');
return `# Debug\n\n${list}`;
},
};
async handler(interaction) {
const { t } = translator(interaction.locale);
const currentTime = new Date().getTime();
const uptime = currentTime - executionStart;
const list = [
[t`bot.commands.debug.body.version`, packageJSON.version],
[
t`bot.commands.debug.body.shard`,
client.shard?.ids[0] ?? t`bot.commands.debug.body.shard.default`,
],
[
t`bot.commands.debug.body.uptime`,
`${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
(uptime / 1000 / 60) % 60,
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
uptime % 1000,
)}ms`,
],
];
return 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> = {
SKLLD: 'Skilled',
SMRI: 'Samurai',
};
// export type SkilledClan = 'SKLLD' | 'SMRI';
export const eligibleCommand: CommandRegistry = {
inProduction: true,
inPublic: false,
// export const SKILLED_CLANS: Record<SkilledClan, string> = {
// SKLLD: 'Skilled',
// SMRI: 'Samurai',
// };
command: new SlashCommandBuilder()
.setName('eligible')
.setDescription('Checks eligibility for any Skilled clan')
.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),
// export const eligibleCommand: CommandRegistry = {
// inProduction: true,
// inPublic: false,
async handler(interaction) {
const clan = interaction.options.getString('clan') as SkilledClan;
const { id, region: region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const problems: TableInput = [];
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
let isEligible = false;
let body = '';
// command: new SlashCommandBuilder()
// .setName('eligible')
// .setDescription('Checks eligibility for any Skilled clan')
// .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),
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];
// async handler(interaction) {
// const clan = interaction.options.getString('clan') as SkilledClan;
// const { id, region: region } = await resolvePlayerFromCommand(interaction);
// const accountInfo = await getAccountInfo(region, id);
// const problems: TableInput = [];
// const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
// let isEligible = false;
// let body = '';
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;
},
0,
)) /
(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;
// },
// 0,
// )) /
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
// const tankDefinition = (await tankDefinitions)[tankId];
return stats.battles;
},
0,
));
const WN8 =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
// if (!tankDefinition) return accumulator;
// edge case where new tanks don't have averages
if ((await tankAverages)[tankId]) {
const tankWN8 = calculateWN8(
(await tankAverages)[tankId].all,
stats,
);
// return stats.battles;
// },
// 0,
// ));
// const WN8 =
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
if (isNaN(tankWN8)) return accumulator;
return (await accumulator) + tankWN8 * stats.battles;
} else return accumulator;
},
0,
)) / stats.battles;
const tier10Damage =
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
// // edge case where new tanks don't have averages
// if ((await tankAverages)[tankId]) {
// const tankWN8 = calculateWN8(
// (await tankAverages)[tankId].all,
// stats,
// );
if ((await tankDefinitions)[tankId]?.tier === 10) {
const tankStats = diffed[tankId];
return (await accumulator) + tankStats.damage_dealt;
} else return accumulator;
},
0,
)) /
(await entries.reduce<PossiblyPromise<number>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
// if (isNaN(tankWN8)) return accumulator;
// return (await accumulator) + tankWN8 * stats.battles;
// } else return accumulator;
// },
// 0,
// )) / stats.battles;
// const tier10Damage =
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
if ((await tankDefinitions)[tankId]?.tier === 10) {
const tankStats = diffed[tankId];
return (await accumulator) + tankStats.battles;
} else return accumulator;
},
0,
));
// if ((await tankDefinitions)[tankId]?.tier === 10) {
// const tankStats = diffed[tankId];
// return (await accumulator) + tankStats.damage_dealt;
// } else return accumulator;
// },
// 0,
// )) /
// (await entries.reduce<PossiblyPromise<number>>(
// async (accumulator, [tankIdString, stats]) => {
// const tankId = parseInt(tankIdString);
if (stats.battles < 126) {
problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]);
}
if (averageTier < 8) {
problems.push([
'Average tier',
`${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})`,
]);
}
// if ((await tankDefinitions)[tankId]?.tier === 10) {
// const tankStats = diffed[tankId];
// return (await accumulator) + tankStats.battles;
// } else return accumulator;
// },
// 0,
// ));
isEligible = problems.length === 0;
body = isEligible
? 'Player is fully eligible.'
: `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable(
problems,
)}`;
} else if (clan === 'SMRI') {
const stats = accountInfo.statistics.all;
// if (stats.battles < 126) {
// problems.push(['Battles', `${stats.battles} (${stats.battles - 126})`]);
// }
// if (averageTier < 8) {
// problems.push([
// 'Average tier',
// `${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})`,
// ]);
// }
if (stats.battles < 1500) {
problems.push([
'Battles',
`${stats.battles} (${stats.battles - 1500})`,
]);
}
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)}%)`,
]);
}
// isEligible = problems.length === 0;
// body = isEligible
// ? 'Player is fully eligible.'
// : `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable(
// problems,
// )}`;
// } else if (clan === 'SMRI') {
// const stats = accountInfo.statistics.all;
isEligible = problems.length === 0;
body = isEligible
? 'Player is fully eligible.'
: `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable(
problems,
)}`;
}
// if (stats.battles < 1500) {
// problems.push([
// 'Battles',
// `${stats.battles} (${stats.battles - 1500})`,
// ]);
// }
// 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);
// },
// };
+50 -53
View File
@@ -1,11 +1,9 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { Locale, SlashCommandSubcommandGroupBuilder } from 'discord.js';
import CommandWrapper from '../components/CommandWrapper';
import * as Graph from '../components/Graph';
import NoData, { NoDataType } from '../components/NoData';
import { LineColor } from '../components/Graph/components/Line/constants';
import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import resolveTankId from '../core/blitz/resolveTankId';
@@ -13,13 +11,14 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import getTankHistories from '../core/blitzkrieg/getTankHistories';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
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 addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
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 { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand, {
@@ -29,7 +28,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate';
import { CommandRegistry } from '../events/interactionCreate';
type EvolutionStatType = 'player' | 'tank';
@@ -38,6 +37,7 @@ async function render(
{ start, end, name }: ResolvedPeriod,
type: 'player' | 'tank',
tankId: number | null,
locale: Locale,
) {
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
@@ -49,7 +49,7 @@ async function render(
includeLatestHistories: true,
includePreviousHistories: true,
})
: getTankHistories(region, id, {
: getTankHistories(region, id, locale, {
tankId: tankId!,
start,
end,
@@ -74,8 +74,8 @@ async function render(
const minX = Math.min(...xs);
return (
<Wrapper>
<TitleBar name={accountInfo.nickname} image={logo} description={name} />
<CommandWrapper>
<TitleBar title={accountInfo.nickname} image={logo} description={name} />
{plot.length > 0 && (
<Graph.Root
@@ -90,11 +90,11 @@ async function render(
max: maxX,
precision: 0,
}}
xMinLabel={new Date(minTime * 1000).toDateString()}
xMaxLabel={new Date(maxTime * 1000).toDateString()}
xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
>
{Graph.Line({
color: Graph.LineColor.Red,
color: LineColor.Red,
plot: plot,
minY,
maxY,
@@ -102,47 +102,37 @@ async function render(
</Graph.Root>
)}
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
</Wrapper>
{plot.length === 0 && <NoData type="battles_in_period" locale={locale} />}
</CommandWrapper>
);
}
export const evolutionCommand = new Promise<CommandRegistryPromisable>(
export const evolutionCommand = new Promise<CommandRegistry>(
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({
inProduction: 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) {
const commandGroup =
@@ -151,18 +141,24 @@ export const evolutionCommand = new Promise<CommandRegistryPromisable>(
const period = resolvePeriodFromCommand(player.region, interaction);
const tankIdRaw = interaction.options.getString('tank')!;
const tankId =
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
commandGroup === 'tank'
? await resolveTankId(tankIdRaw, interaction.locale)
: null;
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
tankId,
});
getBlitzStarsLinkButton;
return [
await render(player, period, commandGroup, tankId),
buttonPrimary(path, 'Refresh'),
await render(
player,
period,
commandGroup,
tankId,
interaction.locale,
),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(
player.region,
player.id,
@@ -188,6 +184,7 @@ export const evolutionCommand = new Promise<CommandRegistryPromisable>(
period,
commandGroup,
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 NoData, { NoDataType } from '../components/NoData';
import NoData from '../components/NoData';
import TierWeights from '../components/TierWeights';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
@@ -16,6 +16,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
@@ -29,55 +30,64 @@ import resolvePlayerFromCommand, {
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { getTierWeights } from '../core/statistics/getTierWeights';
import { CommandRegistryPromisable } from '../events/interactionCreate';
import { CommandRegistry } from '../events/interactionCreate';
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? 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(
diffedTankStats,
filters,
);
const filterDescriptions = await filtersToDescription(filters);
const filterDescriptions = await filtersToDescription(filters, locale);
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
return (
<Wrapper>
<CommandWrapper>
<TitleBar
name={nickname}
title={nickname}
image={clanImage}
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 && (
<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) => {
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('full-stats')
.setDescription('Full in-game statistics'),
(option) => option.addStringOption(addUsernameChoices),
);
resolve({
inProduction: true,
inPublic: true,
command,
command: await addPeriodicFilterOptions(
createLocalizedCommand('full-stats'),
(option) => option.addStringOption(addUsernameChoices),
),
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
@@ -90,7 +100,7 @@ export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
});
return Promise.all([
render(player, period, filters),
render(player, period, filters, interaction.locale),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
@@ -106,7 +116,7 @@ export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
const period = resolvePeriodFromButton(player.region, 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 NoData, { NoDataType } from '../components/NoData';
import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanInfo } from '../core/blitz/getClanInfo';
import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_THRESHOLD = 7;
export const inactiveCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('inactive')
.setDescription('Lists all inactive players')
.addStringOption(addClanChoices)
.addNumberOption((option) =>
option
.setName('threshold')
.setDescription(
`The number of days inactive (default: ${DEFAULT_THRESHOLD})`,
command: createLocalizedCommand('inactive')
.addStringOption(addClanChoices)
.addNumberOption((option) => {
const { translate } = translator(Locale.EnglishUS);
return option
.setName('threshold')
.setNameLocalizations(
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) {
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
const inactive = accountInfo
.map(
(account) =>
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number],
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
.map(
([name, days]) => [name, `${days.toFixed(0)} days`] as [string, string],
return (
<CommandWrapper>
<TitleBar
title={clanInfo.name}
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${translate('bot.commands.inactive.body.subtitle', [`${threshold}`])} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
{!hasInactiveMembers && (
<NoData type="players_in_period" locale={interaction.locale} />
)}
{hasInactiveMembers && <GenericStats stats={inactive} />}
</CommandWrapper>
);
const hasInactiveMembers = inactive.length > 0;
},
return (
<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,
};
autocomplete: autocompleteClan,
});
});
+158 -147
View File
@@ -1,209 +1,220 @@
import { teal } from '@radix-ui/colors';
import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js';
import { GuildMemberRoleManager } from 'discord.js';
import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' };
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
import Wrapper from '../components/Wrapper';
import { REGION_NAMES } from '../constants/regions';
import CommandWrapper from '../components/CommandWrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo';
import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
export const verifyCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('link')
.setDescription('Links your Blitz and Discord account')
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
command: createLocalizedCommand('link').addStringOption((option) =>
addUsernameChoices(option).setRequired(true),
),
async handler(interaction) {
const { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = parseInt(interaction.user.id);
const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
async handler(interaction) {
const { t, translate } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = parseInt(interaction.user.id);
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.guild?.members.me?.permissions.has('ManageRoles')) {
return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`,
"I don't have the permission to change your manage roles.",
if (interaction.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
return embedNegative(
translate(translate('bot.commands.link.body.no_role_permissions'), [
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(
discord.sklld_verify_role,
);
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} />
return [
<CommandWrapper fat>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
justifyContent: 'space-between',
}}
>
<Glow color={teal.teal9} rotation={90} />
<div
style={{
display: 'flex',
gap: 8,
flexDirection: 'column',
alignItems: '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
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
gap: 8,
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
src={
interaction.user.avatarURL({ extension: 'png' }) ??
'https://i.stack.imgur.com/l60Hf.png'
}
style={{
width: 64,
height: 64,
borderRadius: 32,
objectFit: 'cover',
}}
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
/>
) : null}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{interaction.user.displayName}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
@{interaction.user.username}
</span>
</div>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
color: theme.colors.textLowContrast_teal,
}}
>
<span
<img
src="https://i.imgur.com/jIcRgog.png"
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: 16,
height: 16,
}}
/>
<span
style={{ fontSize: 16 }}
>{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={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] - `
: ''}
{REGION_NAMES[region]}
</span>
<span
style={{
fontSize: 32,
color: theme.colors.textHighContrast,
fontWeight: 900,
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{accountInfo.nickname}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
{clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] • `
: ''}
{translate(`common.regions.normal.${region}`)}
</span>
</div>
</div>
</div>
<Glow color={teal.teal9} rotation={-90} />
</div>
</CommandWrapper>,
<Glow color={teal.teal9} rotation={-90} />
</div>
</Wrapper>,
embedInfo(
t`bot.commands.link.embed.title`,
t`bot.commands.link.embed.description`,
),
];
},
embedInfo(
'Incorrect account?',
'Use the command again and select a username from the search results.',
),
];
},
autocomplete: autocompleteUsername,
};
autocomplete: autocompleteUsername,
});
});
+66 -108
View File
@@ -1,128 +1,86 @@
import { SlashCommandBuilder } from 'discord.js';
import NoData, { NoDataType } from '../components/NoData';
import * as Tanks from '../components/Tanks';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
import { chunk } from 'lodash';
import markdownEscape from 'markdown-escape';
import { TANK_CLASSES } from '../components/Tanks/components/Item/constants';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import {
TIER_ROMAN_NUMERALS,
NATIONS,
TankDefinition,
Tier,
flags,
tankDefinitions,
} from '../core/blitzkrieg/tankDefinitions';
import { tankIcon } from '../core/blitzkrieg/tankIcon';
import { TIER_ROMAN_NUMERALS } from '../core/blitzkrieg/tankDefinitions/constants';
import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const ownedTanksCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
const TANKS_PER_MESSAGE = 64;
command: new SlashCommandBuilder()
.setName('owned-tanks')
.setDescription("Shows a player's owned tanks")
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
async handler(interaction) {
const tier = Number(interaction.options.getString('tier'));
const account = await resolvePlayerFromCommand(interaction);
const { id, region: server } = account;
const accountInfo = await getAccountInfo(server, id);
const tankStats = await getTankStats(server, id);
const filteredTanks = (
await Promise.all(
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
id: tankData.tank_id,
})),
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = [];
command: createLocalizedCommand('owned-tanks')
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation);
} else {
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
}
});
async handler(interaction) {
const { translate } = translator(interaction.locale);
const tier = Number(interaction.options.getString('tier'));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id, interaction.locale);
const filteredTanks = (
await Promise.all(
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions)[tankData.tank_id]!,
id: tankData.tank_id,
})),
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {};
const nations: string[] = [];
nations.sort();
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankDefinitions.nation] === undefined) {
groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions];
nations.push(tank.tankDefinitions.nation);
} else {
groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions);
}
});
return (
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanAccountInfo?.clan
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
: undefined
}
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`}
/>
const awaitedTankDefinitions = await tankDefinitions;
const awaitedNations = await NATIONS;
const lines = tankStats
.map(({ tank_id }) => awaitedTankDefinitions[tank_id])
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
)
.sort(
(a, b) =>
awaitedNations.indexOf(a.nation) - awaitedNations.indexOf(b.nation),
)
.map((tank) => `${flags[tank.nation]} ${markdownEscape(tank.name)}`);
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
return chunk(lines, TANKS_PER_MESSAGE).map((lines, index) =>
index === 0
? `${translate('bot.commands.owned_tanks.body.title', [
markdownEscape(accountInfo.nickname),
TIER_ROMAN_NUMERALS[tier as Tier],
])}${lines.join('\n')}`
: lines.join('\n'),
);
},
{filteredTanks.length > 0 &&
(await Promise.all(
nations.map(async (nation) => {
const tanks = groupedTanks[nation];
const leftColumnSize = Math.ceil(tanks.length / 2);
const leftColumn = tanks.slice(0, leftColumnSize);
const rightColumn = tanks.slice(leftColumnSize);
return (
<Tanks.Root>
<Tanks.Title>
{(await encyclopediaInfo).vehicle_nations[nation]}
</Tanks.Title>
<Tanks.Row>
<Tanks.Column>
{await Promise.all(
leftColumn.map(async (tank) => (
<Tanks.Item
key={tank.id}
name={tank.name}
tankType={tank.type}
image={tankIcon(tank.id)}
treeType={tank.treeType}
/>
)),
)}
</Tanks.Column>
<Tanks.Column>
{await Promise.all(
rightColumn.map(async (tank) => (
<Tanks.Item
key={tank.id}
name={tank.name}
tankType={tank.type}
image={tankIcon(tank.id)}
treeType={tank.treeType}
/>
)),
)}
</Tanks.Column>
</Tanks.Row>
</Tanks.Root>
);
}),
))}
</Wrapper>
);
},
autocomplete: autocompleteUsername,
};
autocomplete: autocompleteUsername,
});
});
+43 -37
View File
@@ -1,42 +1,48 @@
import { SlashCommandBuilder } from 'discord.js';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const permissionsCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('permissions')
.setDescription('Checks wether the bot has the required permissions'),
command: createLocalizedCommand('permissions'),
async handler(interaction) {
// friday the 13th permissions easter egg
const isFridayThe13th =
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('ReadMessageHistory'),
'Read message history: also needed for the refresh button to work',
],
[
interaction.appPermissions?.has('AttachFiles'),
'Attach files: needed for image-based commands to work',
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
} else {
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
}
})
.join('\n');
async handler(interaction) {
const { t, translate } = translator(interaction.locale);
// friday the 13th permissions easter egg
const isFridayThe13th =
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has('ViewChannel'),
t`bot.commands.permissions.body.view_channels`,
],
[
interaction.appPermissions?.has('ReadMessageHistory'),
t`bot.commands.permissions.body.read_message_history`,
],
[
interaction.appPermissions?.has('AttachFiles'),
t`bot.commands.permissions.body.attach_files`,
],
[
interaction.appPermissions?.has('UseExternalEmojis'),
t`bot.commands.permissions.body.external_emojis`,
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
} else {
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
}
})
.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';
export const pingCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
handlesInteraction: true,
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
handlesInteraction: true,
command: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check if the bot is alive')
.addSubcommand((option) =>
option.setName('blitzkrieg').setDescription('Ping Blitzkrieg'),
)
.addSubcommand((option) =>
option.setName('blitz').setDescription('Ping Blitz'),
),
command: createLocalizedCommand('ping', [
{ subcommand: 'blitzkrieg' },
{ subcommand: 'wotb' },
]),
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
async handler(interaction) {
const { t, translate } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === 'blitzkrieg') {
await interaction.editReply('Pong 🏓');
} else {
await fetch('https://api.wotblitz.com/');
}
if (subcommand === 'blitzkrieg') {
await interaction.editReply(t`bot.commands.ping.body.pong`);
} else {
await fetch('https://api.wotblitz.com/');
}
const executionTime = Date.now() - executionStart;
interaction.editReply(`Pong 🏓 - ${executionTime}ms`);
},
};
const executionTime = Date.now() - executionStart;
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 = {
inProduction: true,
inPublic: true,
// type SortBy = 'name' | 'count';
command: new SlashCommandBuilder()
.setName('player-achievements')
.setDescription("All the player's achievements")
.addStringOption(addUsernameChoices)
.addStringOption((option) =>
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 },
),
),
// export const playerAchievementsCommand = new Promise<CommandRegistry>(
// (resolve) => {
// resolve({
// inProduction: true,
// inPublic: true,
async handler(interaction) {
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,
};
// command: new SlashCommandBuilder()
// .setName('player-achievements')
// .setDescription("All the player's achievements")
// .addStringOption(addUsernameChoices)
// .addStringOption((option) =>
// 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 },
// ),
// ),
return embedInfo(
`${markdownEscape(accountInfo.nickname)}'s information`,
// async handler(interaction) {
// 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(
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,
),
),
);
},
// return embedInfo(
// `${markdownEscape(accountInfo.nickname)}'s information`,
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 { getAccountInfo } from '../core/blitz/getAccountInfo';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const playerInfoCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('player-info')
.setDescription('Basic information about a player')
.addStringOption(addUsernameChoices),
command:
createLocalizedCommand('player-info').addStringOption(addUsernameChoices),
async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction);
const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
async handler(interaction) {
const { t, translate } = translator(interaction.locale);
const account = await resolvePlayerFromCommand(interaction);
const { id, region: region } = account;
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
`${markdownEscape(accountInfo.nickname)}'s information`,
return embedInfo(
translate('bot.commands.player_info.body.title', [
markdownEscape(accountInfo.nickname),
]),
markdownTable([
['Nickname', `${accountInfo.nickname}`],
['Battles', `${accountInfo.statistics.all.battles}`],
[
'Winrate',
`${(
100 *
(accountInfo.statistics.all.wins /
accountInfo.statistics.all.battles)
).toFixed(2)}%`,
],
[],
['Account ID', `${accountInfo.account_id}`],
[
'Created',
`${new Date(accountInfo.created_at * 1000).toDateString()}`,
],
[
'Last battle',
`${new Date(accountInfo.last_battle_time * 1000).toDateString()}`,
],
]),
);
},
markdownTable([
[
t`bot.commands.player_info.body.nickname`,
`${accountInfo.nickname}`,
],
[
t`bot.commands.player_info.body.battles`,
`${accountInfo.statistics.all.battles}`,
],
[
t`bot.commands.player_info.body.winrate`,
`${(
100 *
(accountInfo.statistics.all.wins /
accountInfo.statistics.all.battles)
).toFixed(2)}%`,
],
[],
[
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 {
APIApplicationCommandOptionChoice,
SlashCommandBuilder,
Locale,
SlashCommandSubcommandBuilder,
} from 'discord.js';
import { range } from 'lodash';
import markdownEscape from 'markdown-escape';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import CommandWrapper from '../components/CommandWrapper';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { LEAGUES } from '../constants/leagues';
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 { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo';
@@ -25,9 +25,11 @@ import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchive
import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
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 { CommandRegistryPromisable } from '../events/interactionCreate';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { UserError } from '../hooks/userError';
export interface RatingsPlayer {
@@ -117,339 +119,387 @@ export interface RatingsNeighbors {
neighbors: RatingsPlayer[];
}
const noOngoingSeason = embedNegative(
'No ongoing season',
"Wargaming didn't provide any data for this season.",
);
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
export const ratingsCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingsSeason();
const seasonNumbers = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
const DEFAULT_LIMIT = 10;
function addSubcommands(option: SlashCommandSubcommandBuilder) {
return option
.addStringOption(addUsernameChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many neighbors to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
)
.addStringOption((option) =>
option
.setName('season')
.setDescription('Season number')
.setRequired(false)
.addChoices(
...seasonNumbers
.map(
(number, index) =>
({
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
value: `${number}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
)
.reverse(),
export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingsSeason();
const seasonNumbers = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
function addOptions(option: SlashCommandSubcommandBuilder) {
const { t, translate } = translator(Locale.EnglishUS);
return option
.addIntegerOption((option) =>
option
.setName(t`bot.commands.ratings.options.limit`)
.setNameLocalizations(
localizationObject('bot.commands.ratings.options.limit'),
)
.setDescription(
translate('bot.commands.ratings.options.limit.description', [
`${DEFAULT_LIMIT}`,
]),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.ratings.options.limit.description',
[`${DEFAULT_LIMIT}`],
),
);
}
const command = new SlashCommandBuilder()
.setName('ratings')
.setDescription('Ratings battles statistics')
.addSubcommand((option) =>
addSubcommands(
option
.setName('neighbors')
.setDescription('Shows neighboring players on the leaderboard'),
),
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
)
.addSubcommand((option) =>
addSubcommands(
.addStringOption((option) =>
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
.setName('league')
.setDescription('Shows top players from a league')
.addStringOption(addRegionChoices)
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addStringOption((option) => {
const { t, translate } = translator(Locale.EnglishUS);
return option
.setName(t`bot.commands.ratings.options.league`)
.setNameLocalizations(
localizationObject('bot.commands.ratings.options.league'),
)
.setDescription(
translate('bot.commands.ratings.options.league.description'),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.ratings.options.league.description',
),
)
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
name: translate(`common.leagues.${name}`),
value: `${value}`,
name_localizations: localizationObject(
`common.leagues.${name}`,
),
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
),
),
);
.setRequired(true);
})
.addStringOption(addRegionChoices);
addOptions(option);
},
},
]),
resolve({
inProduction: true,
inPublic: true,
// BIG TODO: title seems to be in english
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) {
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
if (subcommand === 'league') {
const leagueIndex = parseInt(interaction.options.getString('league')!);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (isCurrentSeason
? getRatingsInfo(region)
: getArchivedRatingsInfo(region, season!));
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| '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 (regionRatingsInfo.detail !== undefined)
return t`bot.commands.ratings.body.no_ongoing_season`;
if (subcommand === 'league') {
const leagueIndex = parseInt(
interaction.options.getString('league')!,
);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (isCurrentSeason
? getRatingsInfo(region)
: getArchivedRatingsInfo(region, season!));
const result = isCurrentSeason
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: await 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
? await getRatingsLeague(region, leagueIndex).then(async (result) =>
result.slice(0, limit).map(
players = result;
playersAfter =
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) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
score: player.score,
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'],
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw new UserError(
translate('bot.commands.ratings.body.no_participation', [
markdownEscape(accountInfo.nickname),
`${season}`,
]),
);
}
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 ??
`Deleted ${player.id}`,
}) satisfies SimplifiedPlayer,
);
},
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
const halfRange = Math.round(limit / 2);
const trimmed = players.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
['clan'],
);
players = result;
playersBefore = result ? result[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.count - result[result.length - 1].position;
titleName = `${
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
} - ${REGION_NAMES_SHORT[region]}`;
titleImage = leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`;
titleDescription = `Top ${limit} players`;
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo = isCurrentSeason
? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season!);
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!);
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 clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors = isCurrentSeason
? 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,
);
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
detail: undefined;
} = regionRatingsInfo;
if (playerIndex === -1) {
throw new UserError(
`# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`,
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
['clan'],
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[index]?.clan?.tag,
nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
},
);
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}
/>
);
});
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 (
<Wrapper>
<TitleBar
name={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{playersBefore > 0 && (
<Leaderboard.Gap
message={`+ ${playersBefore.toLocaleString()} more`}
/>
)}
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={`+ ${playersAfter.toLocaleString()} more`}
/>
)}
</Leaderboard.Root>
)}
</Wrapper>
<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}
/>
);
},
});
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,
});
});
+546
View File
@@ -0,0 +1,546 @@
import { Locale, escapeMarkdown } from 'discord.js';
import CommandWrapper from '../components/CommandWrapper';
import TitleBar from '../components/TitleBar';
import { equipmentPriceMatrix } from '../constants/equipmentPrice';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getTankStats from '../core/blitz/getTankStats';
import resolveTankId from '../core/blitz/resolveTankId';
import { asset } from '../core/blitzkrieg/asset';
import { buildTechTreeLine } from '../core/blitzkrieg/buildTechTreeLine';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { iconPng } from '../core/blitzkrieg/iconPng';
import { resolveAncestry } from '../core/blitzkrieg/resolveAncestry';
import { tankDefinitions } from '../core/blitzkrieg/tankDefinitions';
import {
TANK_ICONS,
TIER_ROMAN_NUMERALS,
} from '../core/blitzkrieg/tankDefinitions/constants';
import { tankIconPng } from '../core/blitzkrieg/tankIconPng';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { t } = translator(Locale.EnglishUS);
resolve({
inProduction: true,
inPublic: true,
inPreview: true,
command: createLocalizedCommand('research')
.addStringOption((option) =>
option
.setName(t`bot.commands.research.options.target_tank`)
.setNameLocalizations(
localizationObject('bot.commands.research.options.target_tank'),
)
.setDescription(
t`bot.commands.research.options.target_tank.description`,
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.research.options.target_tank.description',
),
)
.setAutocomplete(true)
.setRequired(true),
)
.addStringOption((option) =>
option
.setName(t`bot.commands.research.options.starting_tank`)
.setNameLocalizations(
localizationObject('bot.commands.research.options.starting_tank'),
)
.setDescription(
t`bot.commands.research.options.starting_tank.description`,
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.research.options.starting_tank.description',
),
)
.setAutocomplete(true)
.setRequired(false),
),
async handler(interaction) {
const { t, translate } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId(
interaction.options.getString('target-tank', true),
interaction.locale,
true,
);
const startingTankRaw = interaction.options.getString('starting-tank');
const targetTank = awaitedTankDefinitions[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
const targetTankAncestry = await resolveAncestry(targetTankId);
if (startingTankRaw) {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true,
);
if (targetTankId === startingTankId) {
return translate('bot.commands.research.errors.start_end_equal', [
escapeMarkdown(targetTank.name),
]);
}
const startingTank = awaitedTankDefinitions[startingTankId];
if (startingTank.tier > targetTank.tier) {
return translate('bot.commands.research.errors.unordered', [
escapeMarkdown(startingTank.name),
escapeMarkdown(targetTank.name),
escapeMarkdown(targetTank.name),
]);
}
if (!targetTankAncestry.includes(startingTankId)) {
return translate('bot.commands.research.errors.tanks_not_on_line', [
escapeMarkdown(targetTank.name),
escapeMarkdown(startingTank.name),
]);
}
} else {
if (targetTank.treeType !== 'researchable') {
return translate('bot.commands.research.errors.non_tech_tree', [
escapeMarkdown(targetTank.name),
]);
}
if (targetTank.tier === 1) {
return translate('bot.commands.research.errors.tier_1', [
escapeMarkdown(targetTank.name),
]);
}
const tankStats = await getTankStats(region, id, interaction.locale);
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return translate('bot.commands.research.errors.already_researched', [
escapeMarkdown(targetTank.name),
]);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
awaitedTankDefinitions[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id),
);
if (foundAncestor === undefined) {
throw new Error('No first owned tech tree found.');
}
startingTankId = foundAncestor;
}
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId,
);
const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
const costs = await Promise.all(
line.map(async (id) => {
const tank = awaitedTankDefinitions[id];
const turretXps = new Map<number, number>();
const gunXps = new Map<number, number>();
const engineXps = new Map<number, number>();
const trackXps = new Map<number, number>();
tank.turrets.forEach((turret) => {
if (turret.xp !== undefined) {
turretXps.set(turret.id, turret.xp);
}
turret.guns.forEach((gun) => {
if (gun.xp !== undefined) {
gunXps.set(gun.id, gun.xp);
}
});
});
tank.engines.forEach((engine) => {
if (engine.xp !== undefined) {
engineXps.set(engine.id, engine.xp);
}
});
tank.tracks.forEach((track) => {
if (track.xp !== undefined) {
trackXps.set(track.id, track.xp);
}
});
let moduleXp = 0;
turretXps.forEach((value) => {
moduleXp += value;
});
gunXps.forEach((value) => {
moduleXp += value;
});
engineXps.forEach((value) => {
moduleXp += value;
});
trackXps.forEach((value) => {
moduleXp += value;
});
return {
research: tank.xp,
upgrades: moduleXp,
purchase: tank.price.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0,
),
};
}),
);
const image = (
<CommandWrapper>
<TitleBar
title={nickname}
image={clanImage}
description={`${t`bot.commands.research.body.subtitle`} • ${targetTank.name}`}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<div
style={{
display: 'flex',
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div style={{ width: 48 }}></div>
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
right: 24,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{t`bot.commands.research.body.total`}
</span>
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
padding: '16px 0',
}}
>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{costs
.reduce((a, b) => a + b.purchase + b.equipment, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
</span>
</div>
</div>
</div>
{await Promise.all(
line.map(async (id, index) => {
const tank = awaitedTankDefinitions[id];
return (
<div
key={id}
style={{
display: 'flex',
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
}}
>
<div
style={{
width: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
</div>
<div
style={{
flex: 1,
display: 'flex',
position: 'relative',
right: '5%',
}}
>
<div
style={{
height: 120,
width: 240,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
left: '25%',
top: '5%',
}}
>
<img
src={await tankIconPng(id)}
style={{
height: '100%',
position: 'absolute',
objectFit: 'contain',
}}
/>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 8,
left: '50%',
transform: 'translateX(-50%)',
}}
>
<img
src={TANK_ICONS[tank.class]}
width={16}
height={16}
/>
<span
style={{
color: theme.colors.textHighContrast,
}}
>
{tank.name}
</span>
</div>
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
padding: '16px 0',
}}
>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.research', [
`${costs[index].research?.toLocaleString(
interaction.locale,
)}`,
])}
</span>
</div>
{costs[index].upgrades > 0 && (
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(
asset('icons/currencies/free-xp.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.upgrades', [
costs[index].upgrades.toLocaleString(),
])}
</span>
</div>
)}
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.purchase', [
costs[index].purchase.toLocaleString(
interaction.locale,
),
])}
</span>
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<img
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
/>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{translate('bot.commands.research.body.equipment', [
costs[index].equipment.toLocaleString(
interaction.locale,
),
])}
</span>
</div>
</div>
</div>
);
}),
)}
</div>
</CommandWrapper>
);
return startingTankRaw
? image
: [t`bot.commands.research.body.estimation`, image];
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
},
});
});
+60 -34
View File
@@ -1,46 +1,72 @@
import { SlashCommandBuilder } from 'discord.js';
import { Locale } from 'discord.js';
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 fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { ClanList } from '../types/clanList';
export const searchClansCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
const DEFAULT_LIMIT = 25;
command: new SlashCommandBuilder()
.setName('search-clans')
.setDescription('Search clans')
.addStringOption(addRegionChoices)
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('The size of the search result (default: 25)')
.setMinValue(1)
.setMaxValue(100),
),
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
async handler(interaction) {
const server = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const clanList = await fetchBlitz<ClanList>(
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
);
command: createLocalizedCommand('search-clans')
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addClanChoices(option).setAutocomplete(false),
)
.addIntegerOption((option) => {
const { t, translate } = translator(Locale.EnglishUS);
return embedInfo(
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
clanList.length === 0
? 'No clans found.'
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join('\n')}\n\`\`\``,
);
},
};
return option
.setName(t`bot.commands.search_clans.options.limit`)
.setNameLocalizations(
localizationObject('bot.commands.search_clans.options.limit'),
)
.setDescription(
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\`\`\``,
);
},
});
});
+67 -45
View File
@@ -1,56 +1,78 @@
import { SlashCommandBuilder } from 'discord.js';
import { Locale } from 'discord.js';
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 fetchBlitz from '../core/blitz/fetchBlitz';
import {
AccountList,
usernamePattern,
} from '../core/blitz/searchPlayersAcrossRegions';
import { AccountList } from '../core/blitz/searchPlayersAcrossRegions';
import { usernamePattern } from '../core/blitz/searchPlayersAcrossRegions/constants';
import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const searchPlayersCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
const DEFAULT_LIMIT = 25;
command: new SlashCommandBuilder()
.setName('search-players')
.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),
),
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { t, translate } = translator(Locale.EnglishUS);
async handler(interaction) {
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}`,
)
: [];
resolve({
inProduction: true,
inPublic: true,
return embedInfo(
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
`\`\`\`${
players.length === 0
? 'No players found.'
: players.map((player) => player.nickname).join('\n')
}\`\`\``,
);
},
};
command: createLocalizedCommand('search-players')
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
)
.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 = {
inProduction: true,
inPublic: true,
// 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';
command: new SlashCommandBuilder()
.setName('search-tanks')
.setDescription('Search tanks')
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('The size of the search result (default: 25)')
.setMinValue(1)
.setMaxValue(100),
),
// export const searchTanksCommand = new Promise<CommandRegistry>((resolve) => {
// resolve({
// inProduction: true,
// inPublic: true,
async handler(interaction) {
const tank = interaction.options.getString('tank')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const results = go(tank, await tankNames, {
limit,
keys: ['combined'],
}).map((result) => result.obj.original);
// command: new SlashCommandBuilder()
// .setName('search-tanks')
// .setDescription('Search tanks')
// .addStringOption((option) =>
// addTankChoices(option).setAutocomplete(false),
// )
// .addIntegerOption((option) =>
// option
// .setName('limit')
// .setDescription('The size of the search result (default: 25)')
// .setMinValue(1)
// .setMaxValue(100),
// ),
return embedInfo(
`Tank search for "${markdownEscape(tank)}"`,
results.length === 0
? 'No tanks found.'
: `\`\`\`\n${results.join('\n')}\n\`\`\``,
);
},
};
// async handler(interaction) {
// const tank = interaction.options.getString('tank')!;
// const limit = interaction.options.getInteger('limit') ?? 25;
// const results = go(tank, await tankNames, {
// 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 NoData, { NoDataType } from '../components/NoData';
import CommandWrapper from '../components/CommandWrapper';
import NoData from '../components/NoData';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
@@ -15,6 +15,7 @@ import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
@@ -27,80 +28,85 @@ import resolvePlayerFromCommand, {
ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { CommandRegistryPromisable } from '../events/interactionCreate';
import { CommandRegistry } from '../events/interactionCreate';
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale,
) {
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? 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 filterDescriptions = await filtersToDescription(filters);
const filterDescriptions = await filtersToDescription(filters, locale);
return (
<Wrapper>
<CommandWrapper>
<TitleBar
name={nickname}
title={nickname}
image={clanImage}
description={`${name} • ${filterDescriptions}`}
/>
{!stats.battles && <NoData type={NoDataType.BattlesInPeriod} />}
{!stats.battles && <NoData type="battles_in_period" locale={locale} />}
{stats.battles > 0 && (
<AllStatsOverview stats={stats} supplementaryStats={supplementary} />
<AllStatsOverview
locale={locale}
stats={stats}
supplementaryStats={supplementary}
/>
)}
</Wrapper>
</CommandWrapper>
);
}
export const statsCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder()
.setName('stats')
.setDescription('Regular battles statistics'),
export const statsCommand = new Promise<CommandRegistry>(async (resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: await addPeriodicFilterOptions(
createLocalizedCommand('stats'),
(option) => option.addStringOption(addUsernameChoices),
);
),
resolve({
inProduction: true,
inPublic: true,
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
command,
return Promise.all([
render(player, period, filters, interaction.locale),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
},
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.region, interaction);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction),
...filters,
});
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
return Promise.all([
render(player, period, filters),
buttonRefresh(interaction, path),
getBlitzStarsLinkButton(player.region, player.id),
]);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await render(player, period, filters);
},
});
},
);
return await render(player, period, filters, interaction.locale);
},
});
});
+45 -51
View File
@@ -1,12 +1,12 @@
import { SlashCommandBuilder } from 'discord.js';
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
import { NATIONS } from '../core/blitzkrieg/tankDefinitions';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import { addFilterOptions } from '../core/discord/addFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary';
import { buttonRefresh } from '../core/discord/buttonRefresh';
import commandToURL from '../core/discord/commandToURL';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
@@ -14,58 +14,52 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown';
import { CommandRegistry } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown/render';
export const todayCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
const command = addFilterOptions(
new SlashCommandBuilder()
.setName('today')
.setDescription("Today's played tanks and statistics"),
awaitedEncyclopediaInfo,
).addStringOption(addUsernameChoices);
export const todayCommand = new Promise<CommandRegistry>(async (resolve) => {
const nations = await NATIONS;
resolve({
inProduction: true,
inPublic: true,
resolve({
inProduction: true,
inPublic: true,
command: addFilterOptions(
createLocalizedCommand('today'),
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) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(
player.region,
interaction,
'today',
);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction, true),
...filters,
});
return [
...(await renderBreakdown(player, period, filters, interaction.locale)),
buttonRefresh(interaction, path),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
return [
...(await renderBreakdown(player, period, filters)),
buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters);
},
});
},
);
return await renderBreakdown(player, period, filters, interaction.locale);
},
});
});
@@ -1,30 +1,21 @@
import { Locale } from 'discord.js';
import { Percentile } from '../../../../constants/percentiles';
import { translator } from '../../../../core/localization/translator';
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
import { theme } from '../../../../stitches.config';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator/constants';
import { Glow } from './components/Glow';
export interface WN8DisplayProps {
WN8?: number;
wn8?: number;
locale: Locale;
}
export const PERCENTILE_NAMES = {
[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) {
export function WN8Display({ wn8, locale }: WN8DisplayProps) {
const percentile =
WN8 === undefined ? Percentile.VeryBad : getWN8Percentile(WN8);
wn8 === undefined ? Percentile.VeryBad : getWN8Percentile(wn8);
const color = PERCENTILE_COLORS[percentile];
const { translate } = translator(locale);
return (
<div
@@ -59,7 +50,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
color: theme.colors.textHighContrast,
}}
>
{WN8 === undefined ? '--' : WN8.toFixed(0)}
{wn8 === undefined ? '--' : wn8.toFixed(0)}
</span>
<span
style={{
@@ -67,7 +58,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
color: theme.colors.textHighContrast,
}}
>
{PERCENTILE_NAMES[percentile]}
{translate(`common.wn8_percentile.${percentile}`)}
</span>
</div>
@@ -34,7 +34,7 @@ export function WN8SurroundingStat({
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
fontWeight: 'bold',
fontWeight: 700,
}}
>
{value}
+15 -9
View File
@@ -1,4 +1,6 @@
import { Locale } from 'discord.js';
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
import { translator } from '../../core/localization/translator';
import { WN8Display } from './components/WN8Display';
import {
WN8SurroundingStat,
@@ -8,12 +10,16 @@ import {
export interface AllStatsOverviewProps {
stats: AllStats;
supplementaryStats: SupplementaryStats;
locale: Locale;
}
export default function AllStatsOverview({
stats,
locale,
supplementaryStats,
}: AllStatsOverviewProps) {
const { t } = translator(locale);
return (
<div
style={{
@@ -32,18 +38,18 @@ export default function AllStatsOverview({
}}
>
<WN8SurroundingStat
label="Battles"
label={t`bot.commands.stats.body.battles`}
value={stats.battles.toLocaleString()}
align={WN8SurroundingStatAlign.Right}
/>
<WN8SurroundingStat
label="Winrate"
label={t`bot.commands.stats.body.winrate`}
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label="Damage"
label={t`bot.commands.stats.body.damage`}
value={Math.round(
stats.damage_dealt / stats.battles,
).toLocaleString()}
@@ -51,13 +57,13 @@ export default function AllStatsOverview({
padded
/>
<WN8SurroundingStat
label="Dmg. ratio"
label={t`bot.commands.stats.body.damage_ratio`}
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
align={WN8SurroundingStatAlign.Right}
/>
</div>
<WN8Display WN8={supplementaryStats.WN8} />
<WN8Display locale={locale} wn8={supplementaryStats.WN8} />
<div
style={{
@@ -68,25 +74,25 @@ export default function AllStatsOverview({
}}
>
<WN8SurroundingStat
label="Tier"
label={t`bot.commands.stats.body.tier`}
value={
supplementaryStats.tier ? supplementaryStats.tier.toFixed(2) : '--'
}
/>
<WN8SurroundingStat
label="Survival"
label={t`bot.commands.stats.body.survival`}
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
2,
)}%`}
padded
/>
<WN8SurroundingStat
label="Accuracy"
label={t`bot.commands.stats.body.accuracy`}
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
padded
/>
<WN8SurroundingStat
label="Kill ratio"
label={t`bot.commands.stats.body.kill_ratio`}
value={(
stats.frags /
(stats.battles - stats.survived_battles)
@@ -1,12 +1,6 @@
import { useFrame } from '@react-three/fiber';
import { useEffect } from 'react';
import {
MeshBasicMaterial,
Object3D,
ShaderMaterial,
Vector2,
WebGLRenderTarget,
} from 'three';
import { MeshBasicMaterial, Object3D, ShaderMaterial, Vector2 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { canSplash } from '../../../../core/blitz/canSplash';
import { isExplosive } from '../../../../core/blitz/isExplosive';
@@ -21,8 +15,7 @@ import {
} from '../../../../stores/tankopedia';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
export const spacedArmorRenderTarget = new WebGLRenderTarget();
import { spacedArmorRenderTarget } from './target';
interface CoreArmorSceneComponentProps {
thickness: number;
@@ -53,7 +53,7 @@ void main() {
float spacedArmorThickness = spacedArmorBufferFragment.r * penetration;
remainingPenetration -= spacedArmorThickness;
if (isExplosive && !canSplash && remainingPenetration > 0.0) {
if (isExplosive && remainingPenetration > 0.0) {
// there is a 50% penetration loss per meter for HEAT shells
remainingPenetration -= 0.5 * remainingPenetration * distanceFromSpacedArmor;
}
@@ -0,0 +1,3 @@
import { WebGLRenderTarget } from 'three';
export const spacedArmorRenderTarget = new WebGLRenderTarget();
+26 -21
View File
@@ -1,4 +1,4 @@
import { Card, Flex, Inset, Text } from '@radix-ui/themes';
import { Card, Flex, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
@@ -182,33 +182,38 @@ export function ShotDisplay() {
pointerEvents: 'none',
}}
>
<Card
<div
style={{
backgroundColor:
trueStatus === 'penetration'
? '#00ff00c0'
? '#00ff0080'
: trueStatus === 'blocked'
? '#ff0000c0'
: '#ffff00c0',
? '#ff000080'
: '#ffff0080',
borderRadius: 8,
border: 'none',
padding: 4,
width: hasMultipleLayers ? undefined : 8,
height: hasMultipleLayers ? undefined : 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Inset>
{hasMultipleLayers && (
<Text
style={{
width: '100%',
display: 'block',
textAlign: 'center',
color:
trueStatus === 'ricochet' ? 'black' : undefined,
}}
>
<b>{layer.index + 1}</b>
</Text>
)}
</Inset>
</Card>
{hasMultipleLayers && (
<Text
size="2"
style={{
width: '100%',
display: 'block',
textAlign: 'center',
color: trueStatus === 'ricochet' ? 'black' : undefined,
}}
>
<b>{layer.index + 1}</b>
</Text>
)}
</div>
</Html>
</group>
</>
@@ -170,7 +170,7 @@ export function SpacedArmorSceneComponent({
const distance = (
shot.layers.at(-1) as ShotLayerBase
).point.distanceTo(intersections[0].point);
if (explosive && !splashing)
if (explosive)
remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration < 0;
@@ -196,7 +196,7 @@ export function SpacedArmorSceneComponent({
const previousIntersection = intersections[layerIndex - 1];
const distance =
intersection.distance - previousIntersection.distance;
if (explosive && !splashing)
if (explosive)
remainingPenetration -=
0.5 * remainingPenetration * distance;
const wasted = remainingPenetration < 0;
+1 -1
View File
@@ -2,7 +2,7 @@ import { createPortal, useFrame } from '@react-three/fiber';
import { memo, useState } from 'react';
import { DepthTexture, Scene, Vector2 } from 'three';
import { CoreArmorScene } from './components/CoreArmorScene';
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent/target';
import { SpacedArmorScene } from './components/SpacedArmorScene';
export const Armor = memo(() => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface RootProps {
interface RootProps {
children: ReactNode;
}
+3 -2
View File
@@ -5,7 +5,8 @@ import {
GrayColor,
PALETTES,
} from '../../../constants/radixColors';
import { TREE_TYPE_ICONS, TankType, TreeType } from '../../Tanks';
import { TankClass, TreeType } from '../../Tanks';
import { TREE_TYPE_ICONS } from '../../Tanks/components/Item/constants';
import { RowStat } from './RowStat';
export interface RowStatItem {
@@ -22,7 +23,7 @@ interface RowProps {
title: string;
minimized?: boolean;
treeType?: TreeType;
tankType?: TankType;
tankType?: TankClass;
color?: AccentColor | GrayColor;
stats: (RowStatItem | undefined)[];
}
@@ -7,7 +7,7 @@ import {
} from '../../../constants/radixColors';
import PercentileIndicator from '../../PercentileIndicator';
export interface RowStatProps {
interface RowStatProps {
name: string;
value?: string | number;
delta?: number;
@@ -63,7 +63,7 @@ export function RowStat({
<span
style={{
color: theme.textHighContrast,
fontWeight: 'bold',
fontWeight: 700,
fontSize: 16,
textAlign: 'center',
}}
@@ -1,26 +1,22 @@
import { ReactNode } from 'react';
import { theme } from '../stitches.config';
export interface WrapperProps {
interface CommandWrapperProps {
children: ReactNode;
naked?: boolean;
fat?: boolean;
}
export default function Wrapper({ children, naked, fat }: WrapperProps) {
export default function CommandWrapper({ children, fat }: CommandWrapperProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: `16px ${fat ? 0 : 16}px`,
gap: 32,
gap: 16,
width: 480,
color: theme.colors.textHighContrast,
...(!naked && {
background: 'url(https://i.imgur.com/PhS06NJ.png)',
}),
background: theme.colors.appBackground1,
}}
>
{children}
@@ -2,13 +2,13 @@ import { Percentile } from '../../../constants/percentiles';
import { theme } from '../../../stitches.config';
import PercentileIndicator from '../../PercentileIndicator';
export interface CardItem {
interface CardItem {
label: string;
value: string | number | undefined;
percentile?: Percentile;
}
export interface CardProps {
interface CardProps {
items: CardItem[];
title: string;
}
@@ -1,4 +1,4 @@
export interface RootProps {
interface RootProps {
children: React.ReactNode;
}
@@ -1,4 +1,4 @@
export interface RowProps {
interface RowProps {
children: React.ReactNode;
}
+24 -19
View File
@@ -1,19 +1,24 @@
import { Locale } from 'discord.js';
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
import { translator } from '../../core/localization/translator';
import isNumber from '../../core/math/isNumber';
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
import { Card } from './components/Card';
import { Root } from './components/Root';
import { Row } from './components/Row';
export interface GenericAllStatsProps {
interface GenericAllStatsProps {
stats: AllStats;
supplementaryStats?: SupplementaryStats;
locale: Locale;
}
export default function GenericAllStats({
stats,
supplementaryStats,
locale,
}: GenericAllStatsProps) {
const { t } = translator(locale);
const damageRatio = stats.damage_dealt / stats.damage_received;
const killsToDeathRatio =
stats.frags / (stats.battles - stats.survived_battles);
@@ -22,27 +27,27 @@ export default function GenericAllStats({
<Root>
<Row>
<Card
title="Games"
title={t`bot.commands.full_stats.body.cards.games`}
items={[
{
label: 'Winrate',
label: t`bot.commands.full_stats.body.cards.games.winrate`,
value: `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
},
{
label: 'Battles',
label: t`bot.commands.full_stats.body.cards.games.battles`,
value: stats.battles,
},
{
label: 'Wins / Losses',
label: t`bot.commands.full_stats.body.cards.games.wins_losses`,
value: `${stats.wins} / ${stats.losses}`,
},
]}
/>
<Card
title="Efficiency"
title={t`bot.commands.full_stats.body.cards.efficiency`}
items={[
{
label: 'WN8',
label: t`bot.commands.full_stats.body.cards.efficiency.wn8`,
value:
supplementaryStats && isNumber(supplementaryStats.WN8)
? supplementaryStats.WN8!.toFixed(0)
@@ -52,14 +57,14 @@ export default function GenericAllStats({
: undefined,
},
{
label: 'Survival',
label: t`bot.commands.full_stats.body.cards.efficiency.survival`,
value: `${(
100 *
(stats.survived_battles / stats.battles)
).toFixed(2)}%`,
},
{
label: 'Accuracy',
label: t`bot.commands.full_stats.body.cards.efficiency.accuracy`,
value: `${((stats.hits / stats.shots) * 100).toFixed(2)}%`,
},
]}
@@ -67,14 +72,14 @@ export default function GenericAllStats({
</Row>
<Row>
<Card
title="Lethality"
title={t`bot.commands.full_stats.body.cards.lethality`}
items={[
{
label: 'Average damage',
label: t`bot.commands.full_stats.body.cards.lethality.average_damage`,
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
? (
stats.damage_dealt /
@@ -84,32 +89,32 @@ export default function GenericAllStats({
: undefined,
},
{
label: 'Damage ratio',
label: t`bot.commands.full_stats.body.cards.lethality.damage_ratio`,
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),
},
]}
/>
<Card
title="Miscellaneous"
title={t`bot.commands.full_stats.body.cards.miscellaneous`}
items={[
{
label: 'Average tier',
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_tier`,
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),
},
{
label: 'Average XP',
label: t`bot.commands.full_stats.body.cards.miscellaneous.average_xp`,
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)
? killsToDeathRatio.toFixed(2)
: undefined,
@@ -1,8 +1,8 @@
import { Stat } from '..';
import { GenericStat } from '..';
import GenericStatsRow from './GenericStatsRow';
export interface GenericStatsColumnProps {
stats: Stat[];
interface GenericStatsColumnProps {
stats: GenericStat[];
}
export default function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
@@ -1,9 +1,9 @@
import { Stat } from '..';
import { GenericStat } from '..';
import { theme } from '../../../stitches.config';
import PercentileIndicator from '../../PercentileIndicator';
export interface GenericStatsRowProps {
stat: Stat;
interface GenericStatsRowProps {
stat: GenericStat;
}
export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
@@ -30,7 +30,7 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
<span
style={{
color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontWeight: 700,
fontSize: 16,
display: 'flex',
alignItems: 'center',
+3 -3
View File
@@ -1,12 +1,12 @@
import { Percentile } from '../../constants/percentiles';
import GenericStatsColumn from './components/GenericStatsColumn';
export type Stat =
export type GenericStat =
| [string, string | number | undefined]
| [string, string | number | undefined, Percentile | undefined];
export interface GenericStatsProps {
stats: Stat[];
stats: GenericStat[];
columnCount?: number;
}
@@ -16,7 +16,7 @@ export default function GenericStats({
}: GenericStatsProps) {
const filteredStats = stats.filter(([, value]) => value !== undefined);
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
const columns: Stat[][] = [];
const columns: GenericStat[][] = [];
filteredStats.forEach((stat, index) => {
const column = Math.floor(index / itemsPerRow);
@@ -0,0 +1,11 @@
export enum LineColor {
Red,
Green,
Blue,
White,
}
export enum LineStyle {
Solid,
Dashes,
DashesAndDots,
}
@@ -1,12 +1,6 @@
import { blue, green, red, slateDark } from '@radix-ui/colors';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './Root';
export enum LineColor {
Red,
Green,
Blue,
White,
}
import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants';
import { LineColor, LineStyle } from './constants';
const LINE_COLORS = {
[LineColor.Red]: red.red9,
@@ -15,12 +9,6 @@ const LINE_COLORS = {
[LineColor.White]: slateDark.slate11,
};
export enum LineStyle {
Solid,
Dashes,
DashesAndDots,
}
const LINE_STYLES = {
[LineStyle.Solid]: undefined,
[LineStyle.Dashes]: '8, 8',
@@ -28,9 +16,9 @@ const LINE_STYLES = {
};
export type PlotItem = [number, number];
export type Plot = PlotItem[];
type Plot = PlotItem[];
export interface LineProps {
interface LineProps {
plot: Plot;
minY?: number;
maxY?: number;
@@ -0,0 +1,4 @@
export enum MarginOrientation {
Vertical,
Horizontal,
}
@@ -1,13 +1,9 @@
import { times } from 'lodash';
import { theme } from '../../../stitches.config';
import { MarginInputProps } from './Root';
import { theme } from '../../../../stitches.config';
import { MarginInputProps } from '../Root';
import { MarginOrientation } from './constants';
export enum MarginOrientation {
Vertical,
Horizontal,
}
export interface MarginProps extends MarginInputProps {
interface MarginProps extends MarginInputProps {
verticalSeparations: number;
orientation?: MarginOrientation;
}
@@ -0,0 +1,2 @@
export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320;
@@ -1,10 +1,9 @@
import { times } from 'lodash';
import { ReactNode } from 'react';
import { theme } from '../../../stitches.config';
import { Margin, MarginOrientation } from './Margin';
export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320;
import { theme } from '../../../../stitches.config';
import { Margin } from '../Margin';
import { MarginOrientation } from '../Margin/constants';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './constants';
export interface MarginInputProps {
min: number;
@@ -13,7 +12,7 @@ export interface MarginInputProps {
precision?: number;
}
export interface RootProps {
interface RootProps {
children: ReactNode;
verticalMargin?: MarginInputProps;
horizontalMargin?: MarginInputProps;
-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;
@@ -1,6 +1,6 @@
import { theme } from '../../../stitches.config';
export interface GapProps {
interface GapProps {
message: string;
}
@@ -2,7 +2,7 @@ import { ComponentProps } from 'react';
import { RatingsReward } from '../../../commands/ratings';
import { theme } from '../../../stitches.config';
export interface ItemProps extends ComponentProps<'div'> {
interface ItemProps extends ComponentProps<'div'> {
position: number;
deltaPosition?: number;
score?: number;
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface RootProps {
interface RootProps {
children: ReactNode;
}
+1 -1
View File
@@ -1,8 +1,8 @@
import { DiscordLogoIcon } from '@radix-ui/react-icons';
import { Flex, Text, Theme } from '@radix-ui/themes';
import Link from 'next/link';
import { theme } from '../../stitches.config';
import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide';
import { theme } from '../../stitches.config';
export default function Navbar() {
return (
-33
View File
@@ -1,33 +0,0 @@
import { theme } from '../stitches.config';
export enum NoDataType {
BattlesInPeriod,
PlayersInPeriod,
TanksFound,
}
export interface NoDataProps {
type: NoDataType;
}
const NO_DATA_MESSAGES: Record<NoDataType, string> = {
[NoDataType.BattlesInPeriod]: 'No battles played in this period',
[NoDataType.PlayersInPeriod]: 'No players in this period',
[NoDataType.TanksFound]: 'No tanks found',
};
export default function NoData({ type }: NoDataProps) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{NO_DATA_MESSAGES[type]}
</span>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
export enum NoDataType {
BattlesInPeriod,
PlayersInPeriod,
TanksFound,
}
+26
View File
@@ -0,0 +1,26 @@
import { Locale } from 'discord.js';
import { translator } from '../../core/localization/translator';
import { theme } from '../../stitches.config';
interface NoDataProps {
type: 'battles_in_period' | 'players_in_period' | 'tanks_found';
locale: Locale;
}
export default function NoData({ type, locale }: NoDataProps) {
const { translate } = translator(locale);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{translate(`bot.common.no_data.${type}`)}
</span>
</div>
);
}
@@ -10,11 +10,7 @@ import {
slate,
teal,
} from '@radix-ui/colors';
import { Percentile } from '../constants/percentiles';
export interface PercentileIndicatorProps {
percentile: Percentile;
}
import { Percentile } from '../../constants/percentiles';
export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9,
@@ -28,18 +24,3 @@ export const PERCENTILE_COLORS = {
[Percentile.Unicum]: pink.pink9,
[Percentile.SuperUnicum]: purple.purple9,
};
export default function PercentileIndicator({
percentile,
}: PercentileIndicatorProps) {
return (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: PERCENTILE_COLORS[percentile],
}}
/>
);
}
@@ -0,0 +1,21 @@
import { Percentile } from '../../constants/percentiles';
import { PERCENTILE_COLORS } from './constants';
interface PercentileIndicatorProps {
percentile: Percentile;
}
export default function PercentileIndicator({
percentile,
}: PercentileIndicatorProps) {
return (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: PERCENTILE_COLORS[percentile],
}}
/>
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
export interface ColumnProps {
interface ColumnProps {
children: ReactNode;
}
@@ -0,0 +1,31 @@
import { TankClass, TreeType } from '.';
import {
TANK_ICONS,
TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM,
} from '../../../../core/blitzkrieg/tankDefinitions/constants';
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const; // BIG TODO: REMOVE THIS AFTER LOCALIZATION
export const TANK_CLASSES = [
'lightTank',
'mediumTank',
'heavyTank',
'AT-SPG',
] as const;
export const TREE_TYPE_NAMES: Record<TreeType, string> = {
researchable: 'Tech tree',
premium: 'Premium',
collector: 'Collector',
};
export const TREE_TYPE_IMAGES: Record<TreeType, string> = {
researchable: 'https://i.imgur.com/pJxO2XY.png',
premium: 'https://i.imgur.com/mZzSwOU.png',
collector: 'https://i.imgur.com/7A0RsG5.png',
};
export const TREE_TYPE_ICONS: Record<TreeType, Record<TankClass, string>> = {
researchable: TANK_ICONS,
premium: TANK_ICONS_PREMIUM,
collector: TANK_ICONS_COLLECTOR,
};
Loaded 100 of 201 files, more files were not shown because too many files have changed in this diff. Show more