Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3363c9ab58 | ||
|
|
10f64902aa | ||
|
|
25e1f4caa4 | ||
|
|
222fa89422 | ||
|
|
750e145f03 | ||
|
|
3bbbbfa402 | ||
|
|
f728751fa9 | ||
|
|
f5425d0e54 | ||
|
|
debaaccf4c | ||
|
|
a3e20501af | ||
|
|
c87a75f3fd | ||
|
|
55729c5eda | ||
|
|
06cbcc2339 | ||
|
|
1afe860e76 | ||
|
|
c0ac935652 | ||
|
|
7c76b0f5a5 | ||
|
|
42091c54b7 | ||
|
|
396b48d017 | ||
|
|
bb1c0c7625 | ||
|
|
1170676141 | ||
|
|
7baaaa9db9 | ||
|
|
c1ecef0179 | ||
|
|
032d98e6bd | ||
|
|
f4a90cd531 | ||
|
|
82f03583fe | ||
|
|
81c1a50052 | ||
|
|
c7207799e2 | ||
|
|
e0a719ee93 | ||
|
|
ee6d2e7d82 | ||
|
|
14710b5218 | ||
|
|
4b5e0d1c1c | ||
|
|
9968e8346c | ||
|
|
bc4da17f10 | ||
|
|
da56c563fc | ||
|
|
95f4dbf6a0 | ||
|
|
6c36f343ea | ||
|
|
9257ebdd1e | ||
|
|
8f1017195f | ||
|
|
20da294b36 | ||
|
|
a63415461a | ||
|
|
2bb4c78551 | ||
|
|
2909e61721 | ||
|
|
171da2a194 | ||
|
|
933c468e2d | ||
|
|
0d1eb9b12e | ||
|
|
02a03b4d68 | ||
|
|
ef9f3bd361 | ||
|
|
f18b6ee754 | ||
|
|
deb9e463e1 | ||
|
|
c799534009 | ||
|
|
aea01072fb | ||
|
|
ca8dd5a702 | ||
|
|
e79c4a1e3f | ||
|
|
e19d0d948a | ||
|
|
a160ccd095 | ||
|
|
d2d0477186 | ||
|
|
c742c0725d | ||
|
|
9e6343a111 | ||
|
|
faf33b5f2d | ||
|
|
8a78ac7d82 | ||
|
|
d7c7814b3a | ||
|
|
15bf02f829 | ||
|
|
4f62a594b3 | ||
|
|
64253d0120 | ||
|
|
286936131d | ||
|
|
95588ef6b6 | ||
|
|
daef04ccd8 | ||
|
|
86e385c548 | ||
|
|
cc193b99d6 | ||
|
|
c026007937 | ||
|
|
dbc117a9d4 | ||
|
|
34665bd97a | ||
|
|
6d05e607b3 | ||
|
|
14bfc3018b | ||
|
|
7e4f195f94 | ||
|
|
4b6de934a5 | ||
|
|
14fbb595fd | ||
|
|
18a5fb80d0 | ||
|
|
551ef79f03 | ||
|
|
81aec6bad2 | ||
|
|
d08949c597 | ||
|
|
212aee0373 | ||
|
|
059b78c9fb | ||
|
|
11ba5fc28e | ||
|
|
fbde9ec64f | ||
|
|
6600bba58c | ||
|
|
9e340d2e16 | ||
|
|
6f784e9e94 | ||
|
|
62f6aabf05 | ||
|
|
46457a8bdc | ||
|
|
1081e836f5 | ||
|
|
dfe1e37592 | ||
|
|
0b9db21c10 | ||
|
|
57527115d4 | ||
|
|
8d2679c458 | ||
|
|
ca00f18a6a | ||
|
|
466f68701f | ||
|
|
1bb83da253 | ||
|
|
bd1e17bac9 | ||
|
|
322f95e42e | ||
|
|
3ea77f5899 | ||
|
|
882e61ff44 | ||
|
|
d991be4934 | ||
|
|
dfba8bd15f | ||
|
|
87e23673fb | ||
|
|
8fbd1acf4a | ||
|
|
d7dfbaa799 | ||
|
|
04ce20857e | ||
|
|
4550246d63 | ||
|
|
dd673f44ae | ||
|
|
3245284ecc | ||
|
|
3d9872a671 | ||
|
|
f02db2a2d8 | ||
|
|
d1ebb79432 | ||
|
|
44f03a1fce | ||
|
|
6b6ee94d9b | ||
|
|
81c2ca80bc | ||
|
|
c83cec8ed7 | ||
|
|
93ffe5c507 | ||
|
|
8cd1a70333 | ||
|
|
06ae024c2c | ||
|
|
ce8fd0f22f | ||
|
|
7dc1aff1dc | ||
|
|
a60003127f | ||
|
|
7d1ba58806 | ||
|
|
963d71e221 | ||
|
|
2a804fda1c | ||
|
|
597a1a04d2 | ||
|
|
aec6f49ea1 | ||
|
|
e1ad710de5 | ||
|
|
edc8447617 | ||
|
|
f726d4f8e8 | ||
|
|
1f333f6b88 | ||
|
|
9b855fca47 | ||
|
|
c5f433fdf4 | ||
|
|
fd61dae1c1 | ||
|
|
2c45f9759f | ||
|
|
0d3261f748 | ||
|
|
d38e873800 | ||
|
|
ca38914095 | ||
|
|
ee5beeea45 | ||
|
|
a9d40d9f49 | ||
|
|
b8be916c18 | ||
|
|
24d0313fd3 | ||
|
|
c3d2a81327 | ||
|
|
dfeb117663 | ||
|
|
495a830506 | ||
|
|
a730f67ca5 | ||
|
|
9a3867770d | ||
|
|
f8a66bb5e7 | ||
|
|
4b00e1342e | ||
|
|
f9e0584f00 | ||
|
|
900c290e75 | ||
|
|
69e1577d18 | ||
|
|
e3864b86a4 |
No files matched your search
@@ -1,34 +0,0 @@
|
||||
name: CD Asia Ratings Latest
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
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: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --latest
|
||||
@@ -1,34 +0,0 @@
|
||||
name: CD Asia Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 17 * * *"
|
||||
|
||||
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: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia
|
||||
@@ -2,7 +2,7 @@ name: CD Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -29,4 +29,4 @@ jobs:
|
||||
unzip bot.zip
|
||||
|
||||
- name: Run bot
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps index.cjs
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
|
||||
@@ -1,8 +1,12 @@
|
||||
name: CD Europe Ratings Latest
|
||||
name: CD Asia Ratings
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# asia midnight: '0 17 * * *'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 17 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +20,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,11 +28,11 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --latest
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
@@ -1,8 +1,12 @@
|
||||
name: CD North America Ratings Latest
|
||||
name: CD Europe Ratings
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# eu midnight: '0 23 * * *'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 23 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -31,4 +35,4 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --latest
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
@@ -1,8 +1,12 @@
|
||||
name: CD Europe Ratings Leaderboards
|
||||
name: CD North America Ratings
|
||||
|
||||
# latest: '0 0 * * *'
|
||||
# na midnight: '0 5 * * *'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 23 * * *"
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +20,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,11 +28,11 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }}
|
||||
@@ -1,6 +1,6 @@
|
||||
name: CI Build Bot
|
||||
|
||||
on: [push, pull_request]
|
||||
on: push
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: CI Build Docs
|
||||
|
||||
on: [push, pull_request]
|
||||
on: push
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: CI Build Website
|
||||
|
||||
on: [push, pull_request]
|
||||
on: push
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: CD North America Ratings Leaderboards
|
||||
name: CI Version
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -31,4 +29,4 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na
|
||||
run: yarn lint:version
|
||||
@@ -13,4 +13,5 @@ test.*
|
||||
/docs/.vitepress/cache
|
||||
servers.csv
|
||||
.next/
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
temp/
|
||||
@@ -6,15 +6,20 @@
|
||||
"blitzstars",
|
||||
"cleandist",
|
||||
"cockroackdb",
|
||||
"corsproxy",
|
||||
"customperiod",
|
||||
"deburr",
|
||||
"diacriticless",
|
||||
"Everytime",
|
||||
"fullstats",
|
||||
"fuzzysort",
|
||||
"immer",
|
||||
"kranvagn",
|
||||
"krieg",
|
||||
"Leaderboard",
|
||||
"leaderboards",
|
||||
"MURICA",
|
||||
"Oopsy",
|
||||
"outdir",
|
||||
"outfile",
|
||||
"ownedtanks",
|
||||
@@ -35,6 +40,7 @@
|
||||
"subnation",
|
||||
"tankaverages",
|
||||
"tankopedia",
|
||||
"tankopedias",
|
||||
"tankstats",
|
||||
"tresabhi",
|
||||
"Unicum",
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Guide', link: '/guide/about' },
|
||||
{ text: 'Guide', link: '/guide/introduction' },
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Blitzkrieg 1.14.1
|
||||
|
||||
Oopsy daisy, a memory leak!
|
||||
|
||||
## Changes
|
||||
|
||||
- Simplified message for users to use `/link`
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed a memory leak in the render thread manager
|
||||
|
||||
## Technical changes
|
||||
|
||||
- Added `UserError` extending `Error`
|
||||
- This lets the bot avoid spamming the debug console for mistakes users make
|
||||
- Reduced render threads to 2
|
||||
@@ -0,0 +1,8 @@
|
||||
# Blitzkrieg 1.14.2
|
||||
|
||||
A late night hot fix for my homies
|
||||
|
||||
## Fixes
|
||||
|
||||
- Clan info is now fetched using `clan_id` instead of `account_id`
|
||||
- Generic stats row names are now bounded by a max width
|
||||
@@ -0,0 +1,24 @@
|
||||
# Blitzkrieg 1.14.3
|
||||
|
||||
Pull your brush out and start polishing.
|
||||
|
||||
## Changes
|
||||
|
||||

|
||||
|
||||
- Rows in `/breakdown` are now chunked into `8` per image
|
||||
- This is true unless the number of pages exceeds `10` in which case the rows are even distributed across all `10` pages
|
||||
- `10` is the maximum number of images that Discord allows
|
||||
- Short region names are used in username autocomplete instead of their full names
|
||||
- Example: "North America" is "NA" now
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed reference to username inference in the username option description
|
||||
- Removed an unnecessary type `asd` that did absolutely nothing used nowhere
|
||||
|
||||
## Technical changes
|
||||
|
||||
- Image orders are now preserved even if processed in parallel and all completed out of order
|
||||
- `fetchBlitz` is now throttled to `10` requests per second
|
||||
- Request overflows are managed by queueing
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.14.4
|
||||
|
||||
Streamers get some knowledge today
|
||||
|
||||
## Changes
|
||||
|
||||
- Added a video in the embed button prompt
|
||||
@@ -0,0 +1,72 @@
|
||||
# Blitzkrieg 1.15.0
|
||||
|
||||
Ratings will never be the same.
|
||||
|
||||
## Features
|
||||
|
||||

|
||||
|
||||
- Blitzkrieg ratings leaderboard
|
||||
- Available at https://blitz-krieg.vercel.app/tools/ratings
|
||||
- **Highly experimental; [please report bugs on Discord server](https://discord.gg/nDt7AjGJQH)**
|
||||
- Current season leaderboards are live meaning that there may be scores and positions that are out of order
|
||||
- Options
|
||||
- Regions can be swapped between NA, EU, and Asia
|
||||
- The current season and all archived season (from 49 and onwards) are available
|
||||
- Jump: used to jump to certain players
|
||||
- Score: finds the first player with at least your desired score
|
||||
- Position: finds the player at that position
|
||||
- League: finds the top-most player in that league
|
||||
- Player: finds the player
|
||||
- **Jumping to scores and positions for the current season is a compounding function (hence the loading screen) and is limited to the speed of Wargaming's APIs which is pretty darn slow**
|
||||
- Page controls
|
||||
- Flip between pages left and right
|
||||
- 33 players are loaded at once
|
||||
- Left control is disabled on the first page and vice versa
|
||||
- Page turning controls are also disabled if the next/previous page hasn't loaded yet
|
||||
- Page numbers can be manually typed in the text field too
|
||||
- This is available both at the top and the bottom of the screen
|
||||
- Players
|
||||
- Shows the position of the player
|
||||
- Shows the player nickname and clan name
|
||||
- "Deleted player ID" us shown when loading a deleted player
|
||||
- A skeleton is shown when the player and clan is loading
|
||||
- Shows the player percentile
|
||||
- Shows reward for the position
|
||||
- This is blank when there is no reward
|
||||
- This column isn't available for archived seasons
|
||||
- Shows the score
|
||||
- A skeleton is shown when loading
|
||||
- Click on a row to highlight it
|
||||
- Using a jump button also highlights a row
|
||||
- `/credits`: thanks who helped made this project
|
||||
|
||||
## Changes
|
||||
|
||||
- Error messages that user face are now even more simplified
|
||||
- Increased username and clan autocomplete size limits to `25`
|
||||
- Added capability for per-page accent theming
|
||||
- Example: the ratings page is red while the session tracker is blue
|
||||
- Simplified welcome message for SKLLD clan server
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Isolated tankopedia info
|
||||
- Implemented `corsproxy.io` for non-timeout tolerant endpoints
|
||||
- Latest archived season number is now data driven
|
||||
- Ratings archives are now public
|
||||
- This eliminates the need of `octokit`
|
||||
- Ratings leaderboard archiver is now error tolerant
|
||||
- Fetch attempts are made every `1000ms`
|
||||
- Blitz errors are now managed better freeing up the queue faster
|
||||
- Isolated first archived season number
|
||||
- Added more advanced PSAs
|
||||
- These can now feature images or embeds
|
||||
- These can now modify message contents
|
||||
- These can also now be secret
|
||||
|
||||
## Experimental Changes
|
||||
|
||||
- Added tankopedia extractor
|
||||
- Retrieves tankopedia data from the game files
|
||||
- This is not utilized as of right now
|
||||
@@ -0,0 +1,24 @@
|
||||
# Blitzkrieg 1.15.1
|
||||
|
||||
'MURICA!
|
||||
|
||||
## Changes
|
||||
|
||||
- "Neighbor" is now spelled in the American way
|
||||
- Updated documentation website with the latest information
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed the guide link on the documentation website
|
||||
|
||||
## Technical features
|
||||
|
||||
- Added capability to fetch patiently while also parsing the JSON
|
||||
|
||||
## Technical changes
|
||||
|
||||
- Removed neighbor relative position caching
|
||||
|
||||
## Technical fixes
|
||||
|
||||
- `/ratings` now uses the non-caching APIs
|
||||
@@ -0,0 +1,9 @@
|
||||
# Blitzkrieg 1.15.2
|
||||
|
||||
Infinity reduced to a finite value.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Added a maximum value to the jump to position button
|
||||
- Removed neighbor checking for the seeding player
|
||||
- This fixes the jump to position button not loading at all
|
||||
@@ -0,0 +1,30 @@
|
||||
# Blitzkrieg 1.15.3
|
||||
|
||||
The bot now gets more expressive!
|
||||
|
||||
## Features
|
||||
|
||||
- The bot can now have presence messages; examples:
|
||||
- "Living in {number} servers"
|
||||
- "Server {number} users"
|
||||
- "Observing {number} channels"
|
||||
- More will be added in the future! Give me some ideas
|
||||
|
||||
## Changes
|
||||
|
||||
- Moved `/credits` to `/about creators`
|
||||
- `/debug` command now ditches embeds and has better information to serve
|
||||
|
||||
## Technical Features
|
||||
|
||||
- Added a workflow and lint for package and changelog version miss match
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Removed manual trigger for Asian latest ratings leaderboard archiver
|
||||
- Leaderboard archiver now accepts an explicit target like `midnight` or `latest`
|
||||
- Combined midnight and latest leaderboard archival workflow into one file
|
||||
|
||||
## Technical Fixed
|
||||
|
||||
- Reviewed all workflow triggers and updated them
|
||||
@@ -0,0 +1,6 @@
|
||||
# Creators
|
||||
|
||||
- TrèsAbhi `@tresabhi`
|
||||
- Creator & main programmer; _I do it all!_
|
||||
- Prince_NA `@prince_na`
|
||||
- Advisor & tester
|
||||
@@ -1,17 +0,0 @@
|
||||
# Installing Blitzkrieg
|
||||
|
||||
It's easy as 1, 2, 3!
|
||||
|
||||
## Step 1
|
||||
|
||||
Join a server that already has Blitzkrieg or [join our official Discord server](https://discord.gg/nDt7AjGJQH).
|
||||
|
||||
## Step 2
|
||||
|
||||
View the bot's profile and click "Add to Server".
|
||||
|
||||
<video src="https://i.imgur.com/Zq0P3m2.mp4" autoplay loop muted width=320 title="how to install"></video>
|
||||
|
||||
## Step 3
|
||||
|
||||
Review the permissions the bot is asking for and add it to your server!
|
||||
@@ -1,6 +1,10 @@
|
||||
# Introduction
|
||||
|
||||
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
|
||||
🎉 Services for everything and anything World of Tanks Blitz
|
||||
|
||||
## Bot
|
||||
|
||||
Blitzkrieg is available as a Discord bot and can be [installed very easily](/guide/invite).
|
||||
|
||||
- Track statistics
|
||||
- Find inactive players in a clan
|
||||
@@ -8,3 +12,11 @@ Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blit
|
||||
- Create embeds for streams
|
||||
- Track a player/tanks statistical evolution
|
||||
- ...and a variety of other similar things
|
||||
|
||||
## Website
|
||||
|
||||
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
|
||||
|
||||
- Session tracker
|
||||
- Ratings leaderboard
|
||||
- Way more to come
|
||||
@@ -12,10 +12,10 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Add to Discord Server
|
||||
link: guide/install
|
||||
link: /guide/invite
|
||||
- theme: alt
|
||||
text: Guide
|
||||
link: /guide/about
|
||||
link: /guide/introduction
|
||||
|
||||
features:
|
||||
- title: Clean and Efficient UI
|
||||
|
||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 183 KiB |
@@ -7,6 +7,7 @@ const nextConfig = {
|
||||
...withVanillaExtract(),
|
||||
|
||||
distDir: 'dist/website',
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.14.0",
|
||||
"version": "1.15.3",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
@@ -9,6 +9,7 @@
|
||||
"build:website": "next build",
|
||||
"build:docs": "vitepress build docs",
|
||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||
"build:tankopedia": "vite-node --script scripts/buildTankopedia",
|
||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||
"dev:docs": "vitepress dev docs",
|
||||
"dev:website": "next dev",
|
||||
@@ -20,6 +21,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -33,12 +35,14 @@
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"bepaint": "^0.1.0-beta.3",
|
||||
"decompress": "^4.2.1",
|
||||
"discord.js": "^14.13.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"esbuild": "^0.19.3",
|
||||
"express": "^4.18.2",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"immer": "^10.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-escape": "^2.0.0",
|
||||
"next": "^13.5.2",
|
||||
@@ -49,12 +53,15 @@
|
||||
"vite-node": "^0.34.5",
|
||||
"vitepress": "^1.0.0-rc.20",
|
||||
"vitepress-plugin-auto-sidebar": "^1.1.0",
|
||||
"xml-js": "^1.6.11",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@total-typescript/ts-reset": "^0.5.1",
|
||||
"@types/decompress": "^4.2.6",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/js-yaml": "^4.0.8",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/markdown-escape": "^1.1.0",
|
||||
"@types/node": "^20.6.5",
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 8.2 MiB After Width: | Height: | Size: 5.4 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -1,36 +1,44 @@
|
||||
import { build } from 'esbuild';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { cp, readdir } from 'fs/promises';
|
||||
|
||||
const FILES = [
|
||||
'bot',
|
||||
'index',
|
||||
readdir('src/workers').then((workers) => {
|
||||
const files = [
|
||||
'bot',
|
||||
'index',
|
||||
|
||||
(await readdir('src/workers')).map(
|
||||
(file) => `workers/${file.replace('.ts', '')}`,
|
||||
),
|
||||
];
|
||||
...workers.map((file) => `workers/${file.replace('.ts', '')}`),
|
||||
];
|
||||
|
||||
await build({
|
||||
entryPoints: FILES.map((file) => `src/${file}.ts`),
|
||||
outdir: 'dist/bot',
|
||||
build({
|
||||
entryPoints: files.map((file) => `src/${file}.ts`),
|
||||
outdir: 'dist/bot',
|
||||
|
||||
platform: 'node',
|
||||
tsconfig: 'tsconfig.esbuild.json',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
format: 'cjs',
|
||||
jsx: 'transform',
|
||||
outExtension: {
|
||||
'.js': '.cjs',
|
||||
},
|
||||
platform: 'node',
|
||||
tsconfig: 'tsconfig.esbuild.json',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
format: 'cjs',
|
||||
jsx: 'transform',
|
||||
outExtension: {
|
||||
'.js': '.cjs',
|
||||
},
|
||||
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // cause goofy errors
|
||||
minifySyntax: true,
|
||||
minifyWhitespace: true,
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // causes goofy errors
|
||||
minifySyntax: true,
|
||||
minifyWhitespace: true,
|
||||
|
||||
logLevel: 'info',
|
||||
logLevel: 'info',
|
||||
});
|
||||
});
|
||||
|
||||
// copy schema.prisma
|
||||
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||
readdir('node_modules/prisma').then((files) =>
|
||||
files
|
||||
.filter((file) => file.endsWith('.node'))
|
||||
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
||||
);
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
RatingsNeighbors,
|
||||
RatingsPlayer,
|
||||
} from '../src/commands/ratings';
|
||||
import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
|
||||
import commitMultipleFiles from './commitMultipleFiles.js';
|
||||
|
||||
/*
|
||||
@@ -13,25 +14,27 @@ import commitMultipleFiles from './commitMultipleFiles.js';
|
||||
* Central European Time (UTC+1): use 0 23 * * *.
|
||||
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
||||
*
|
||||
* na/ratings/49/info.json: season info like rewards, start & end time, etc.
|
||||
* na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||
* na/ratings/49/midnight.json: last midnight's leaderboard relative to the region
|
||||
* [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc.
|
||||
* [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||
* [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region
|
||||
*/
|
||||
|
||||
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
|
||||
export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number };
|
||||
export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[];
|
||||
|
||||
const publish = argv.includes('--publish');
|
||||
const latest = argv.includes('--latest');
|
||||
const target = argv
|
||||
.find((arg) => arg.startsWith('--target='))
|
||||
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
||||
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
||||
|
||||
if (!target) throw new Error('Target parameter not specified');
|
||||
if (!server) throw new Error('Region parameter not specified');
|
||||
|
||||
const NEIGHBORS = 1000;
|
||||
const PLAYERS = await fetch(
|
||||
const NEIGHBORS = 2 ** 8;
|
||||
const PLAYERS = await patientFetchJSON<RatingsInfo>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
)
|
||||
.then((response) => response.json() as Promise<RatingsInfo>)
|
||||
.then((data) => (data.detail === undefined ? data.count : undefined));
|
||||
).then((data) => (data.detail === undefined ? data.count : undefined));
|
||||
|
||||
if (PLAYERS === undefined) {
|
||||
throw new Error('No current ratings season running');
|
||||
@@ -42,9 +45,9 @@ const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
||||
let registered = 0;
|
||||
|
||||
async function branchFromPlayer(id: number) {
|
||||
const { neighbors } = await fetch(
|
||||
const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||
);
|
||||
|
||||
const firstPlayer = neighbors[0];
|
||||
const lastPlayer = neighbors[neighbors.length - 1];
|
||||
@@ -73,9 +76,9 @@ async function branchFromPlayer(id: number) {
|
||||
}
|
||||
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await fetch(
|
||||
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
|
||||
);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result[result.length - 1];
|
||||
@@ -111,18 +114,13 @@ const leaderboardJSON = JSON.stringify(leaderboard);
|
||||
if (publish) {
|
||||
const octokit = new Octokit({ auth: env.GH_TOKEN });
|
||||
|
||||
const info = await fetch(
|
||||
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).then(
|
||||
(response) =>
|
||||
response.json() as Promise<RatingsInfo & { detail: undefined }>,
|
||||
);
|
||||
const normalizedServer = server === 'na' ? 'com' : server;
|
||||
|
||||
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
|
||||
latest ? 'latest' : 'midnight'
|
||||
}.json`;
|
||||
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${target}.json`;
|
||||
const infoJSON = JSON.stringify(info);
|
||||
|
||||
console.log(`Publishing to season ${info.current_season}...`);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import decompress from 'decompress';
|
||||
import { readFile, readdir, rm, writeFile } from 'fs/promises';
|
||||
import { load } from 'js-yaml';
|
||||
import { ElementCompact, xml2js } from 'xml-js';
|
||||
import { NATION_IDS } from '../src/constants/nations';
|
||||
|
||||
const REWRITE = false;
|
||||
const VEHICLE_DEFS = 'assets/Data/XML/item_defs/vehicles';
|
||||
const APK_FILE = 'temp/blitz.apk';
|
||||
const STRINGS = 'assets/Data/Strings';
|
||||
const LANGUAGE = 'en';
|
||||
const DIRECTORIES_OF_INTEREST = [VEHICLE_DEFS, STRINGS];
|
||||
|
||||
if (REWRITE) {
|
||||
await rm('temp/blitz', { recursive: true, force: true });
|
||||
decompress(APK_FILE, 'temp/blitz', {
|
||||
filter: (file) =>
|
||||
DIRECTORIES_OF_INTEREST.some((dir) => file.path.startsWith(dir)),
|
||||
});
|
||||
}
|
||||
|
||||
interface BlitzkriegTankopedia {
|
||||
id: number;
|
||||
nation: string;
|
||||
name: string;
|
||||
name_short?: string;
|
||||
is_premium: boolean;
|
||||
is_collector: boolean;
|
||||
tier: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const strings = await readFile(
|
||||
`temp/blitz/${STRINGS}/${LANGUAGE}.yaml`,
|
||||
'utf-8',
|
||||
).then((data) => load(data) as Record<string, string>);
|
||||
const tankopedia: Record<number, BlitzkriegTankopedia> = {};
|
||||
const nations = await readdir(`temp/blitz/${VEHICLE_DEFS}`);
|
||||
|
||||
await Promise.all(
|
||||
nations
|
||||
.filter((nation) => nation !== 'common')
|
||||
.map(async (nation) => {
|
||||
const listXML = await readFile(
|
||||
`temp/blitz/${VEHICLE_DEFS}/${nation}/list.xml`,
|
||||
'utf-8',
|
||||
);
|
||||
const list = xml2js(listXML, { compact: true }) as ElementCompact;
|
||||
|
||||
await Promise.all(
|
||||
Object.entries<ElementCompact>(list.root).map(
|
||||
async ([vehicle, vehicleData]) => {
|
||||
// const vehicleXML = await readFile(
|
||||
// `temp/blitz/${VEHICLE_DEFS}/${nation}/${vehicle}.xml`,
|
||||
// 'utf-8',
|
||||
// ).then((xml) => xml2js(xml, { compact: true }) as ElementCompact);
|
||||
const nationVehicleId = parseInt(list.root[vehicle].id._text);
|
||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
// TODO: use userString property instead
|
||||
const name = strings[`#${nation}_vehicles:${vehicle}`];
|
||||
const name_short = strings[`#${nation}_vehicles:${vehicle}_short`];
|
||||
const is_premium = 'gold' in vehicleData.price;
|
||||
const is_collector = vehicleData.sellPrice
|
||||
? 'gold' in vehicleData.sellPrice
|
||||
: false;
|
||||
const tier = parseInt(vehicleData.level._text);
|
||||
const tags = vehicleData.tags._text.split(' ');
|
||||
const type = tags[0];
|
||||
|
||||
tankopedia[id] = {
|
||||
id,
|
||||
name,
|
||||
name_short,
|
||||
nation,
|
||||
is_premium,
|
||||
is_collector,
|
||||
tier,
|
||||
type,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
writeFile('dist/tankopedia.json', JSON.stringify(tankopedia, null, 2));
|
||||
@@ -0,0 +1,12 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import packageJSON from '../package.json';
|
||||
|
||||
const { version } = packageJSON;
|
||||
const changelogs = await readdir('docs/changelogs');
|
||||
const hasVersion = changelogs.includes(`${version}.md`);
|
||||
|
||||
if (hasVersion) {
|
||||
console.log(`Version ${version} found in changelogs`);
|
||||
} else {
|
||||
throw new Error(`Version ${version} not found in changelogs`);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../constants/regions';
|
||||
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region } },
|
||||
) {
|
||||
return NextResponse.json(await getRatingsInfo(params.region));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../../constants/regions';
|
||||
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region; league: number } },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
await getRatingsLeague(params.region, params.league, true),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Region } from '../../../../../../../constants/regions';
|
||||
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { region: Region; username: string } },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
await searchCurrentRatingsPlayers(params.region, params.username),
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
|
||||
import * as Breakdown from '../../../components/Breakdown';
|
||||
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../../../core/blitz/fetchWargaming';
|
||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||
import {
|
||||
TankopediaEntry,
|
||||
|
||||
@@ -41,12 +41,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||
}}
|
||||
>
|
||||
<Theme
|
||||
appearance="dark"
|
||||
panelBackground="translucent"
|
||||
radius="full"
|
||||
accentColor="blue"
|
||||
>
|
||||
<Theme appearance="dark" panelBackground="translucent" radius="full">
|
||||
{!isEmbed && <Navbar />}
|
||||
|
||||
<Flex direction="column">{children}</Flex>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
const spinKeyframes = keyframes({
|
||||
from: { transform: 'rotate(0deg)' },
|
||||
to: { transform: 'rotate(360deg)' },
|
||||
});
|
||||
|
||||
export const infiniteSpin = style({
|
||||
animationName: spinKeyframes,
|
||||
animationIterationCount: 'infinite',
|
||||
animationTimingFunction: 'linear',
|
||||
animationDuration: '1s',
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
CaretLeftIcon,
|
||||
CaretRightIcon,
|
||||
SymbolIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Button, Flex, TextField } from '@radix-ui/themes';
|
||||
import { range } from 'lodash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { infiniteSpin } from './index.css';
|
||||
|
||||
interface PageTurnerProps {
|
||||
leaderboard: Record<number, number>;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
pages: number;
|
||||
onPageChange: (page: number, isButtonClick: boolean) => void;
|
||||
totalPlayers: number;
|
||||
}
|
||||
|
||||
export function PageTurner({
|
||||
page,
|
||||
rowsPerPage,
|
||||
onPageChange,
|
||||
pages,
|
||||
leaderboard,
|
||||
totalPlayers,
|
||||
}: PageTurnerProps) {
|
||||
const pageInput = useRef<HTMLInputElement>(null);
|
||||
const rightLoading = range(
|
||||
page * rowsPerPage,
|
||||
Math.min(totalPlayers - 1, page * rowsPerPage + rowsPerPage + 1),
|
||||
).some((index) => !(index in leaderboard));
|
||||
const leftLoading = range(
|
||||
Math.max(0, page * rowsPerPage - 1),
|
||||
Math.max(0, page * rowsPerPage - rowsPerPage - 1) + 1,
|
||||
).some((index) => !(index in leaderboard));
|
||||
|
||||
useEffect(() => {
|
||||
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<Flex justify="center" wrap="wrap" gap="2">
|
||||
<Flex gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => onPageChange(Math.max(page - 1, 0), true)}
|
||||
disabled={page === 0 || leftLoading}
|
||||
>
|
||||
{leftLoading ? (
|
||||
<SymbolIcon className={infiniteSpin} />
|
||||
) : (
|
||||
<CaretLeftIcon />
|
||||
)}
|
||||
</Button>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>Page</TextField.Slot>
|
||||
<TextField.Input
|
||||
type="number"
|
||||
ref={pageInput}
|
||||
style={{ width: 64, textAlign: 'center' }}
|
||||
onBlur={(event) => {
|
||||
onPageChange(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(pages - 1, event.target.valueAsNumber - 1),
|
||||
),
|
||||
false,
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField.Slot>out of {pages}</TextField.Slot>
|
||||
</TextField.Root>
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => {
|
||||
onPageChange(Math.min(page + 1, pages - 1), true);
|
||||
}}
|
||||
disabled={page === pages - 1 || rightLoading}
|
||||
>
|
||||
{rightLoading ? (
|
||||
<SymbolIcon className={infiniteSpin} />
|
||||
) : (
|
||||
<CaretRightIcon />
|
||||
)}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export default function Page() {
|
||||
}, [session]);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageWrapper color="blue">
|
||||
<div className={styles.toolBar}>
|
||||
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
||||
<TextField.Root>
|
||||
@@ -124,7 +124,7 @@ export default function Page() {
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={(event) => {
|
||||
if (!session.promptBeforeReset) {
|
||||
if (!session.resetPrompt) {
|
||||
event.preventDefault();
|
||||
resetSession();
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export default function Page() {
|
||||
mr="1"
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({
|
||||
promptBeforeReset: !checked,
|
||||
resetPrompt: !checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -183,7 +183,7 @@ export default function Page() {
|
||||
variant="soft"
|
||||
className={styles.toolbarButton}
|
||||
onClick={(event) => {
|
||||
if (!session.showEmbedPrompt) {
|
||||
if (!session.embedPrompt) {
|
||||
event.preventDefault();
|
||||
resetSession();
|
||||
}
|
||||
@@ -202,14 +202,17 @@ export default function Page() {
|
||||
Copied link to your clipboard
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Keep in mind:
|
||||
<ul>
|
||||
<li>Paste this link into your browser to view the embed</li>
|
||||
<li>Embeds abide your custom options</li>
|
||||
<li>
|
||||
Options do not transfer across websites/streaming softwares
|
||||
</li>
|
||||
</ul>
|
||||
Instructions for streamers:
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16 / 9',
|
||||
border: 'none',
|
||||
}}
|
||||
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
</AlertDialog.Description>
|
||||
|
||||
<Text size="2">
|
||||
@@ -217,7 +220,7 @@ export default function Page() {
|
||||
mr="1"
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({
|
||||
showEmbedPrompt: !checked,
|
||||
embedPrompt: !checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ const DOCS = {
|
||||
introduction: 'guide/introduction',
|
||||
invite: 'guide/invite',
|
||||
timezones: 'guide/timezones',
|
||||
creators: 'guide/creators',
|
||||
};
|
||||
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||
permissions: 'The permissions needed for the bot to function',
|
||||
@@ -18,6 +19,7 @@ const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||
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) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { chunk } from 'lodash';
|
||||
import * as Breakdown from '../components/Breakdown';
|
||||
import NoData, { NoDataType } from '../components/NoData';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
@@ -35,6 +36,9 @@ import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
|
||||
const ROWS_PER_PAGE = 8;
|
||||
const MAX_PAGES = 10;
|
||||
|
||||
async function render(
|
||||
{ region, id }: ResolvedPlayer,
|
||||
{ start, end, name }: ResolvedPeriod,
|
||||
@@ -132,38 +136,80 @@ async function render(
|
||||
0,
|
||||
) / careerBattles;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={accountInfo.nickname}
|
||||
image={
|
||||
clanData?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`${name} • ${filterDescriptions}`}
|
||||
/>
|
||||
const children: JSX.Element[] = [];
|
||||
|
||||
{filteredOrder.length === 0 && (
|
||||
<NoData type={NoDataType.BattlesInPeriod} />
|
||||
)}
|
||||
{filteredOrder.length > 0 && (
|
||||
<Breakdown.Root>
|
||||
if (filteredOrder.length > 0) {
|
||||
children.push(
|
||||
<Breakdown.Row
|
||||
key={id}
|
||||
type="summary"
|
||||
title="Total"
|
||||
stats={[
|
||||
{
|
||||
title: 'Battles',
|
||||
current: currentBattles,
|
||||
career: careerBattles,
|
||||
},
|
||||
{
|
||||
title: 'Winrate',
|
||||
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
||||
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||
delta: currentWinrate - careerWinrate,
|
||||
},
|
||||
{
|
||||
title: 'WN8',
|
||||
current:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(currentWN8).toLocaleString(),
|
||||
career:
|
||||
careerWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(careerWN8).toLocaleString(),
|
||||
percentile:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: getWN8Percentile(currentWN8),
|
||||
},
|
||||
{
|
||||
title: 'Damage',
|
||||
current: Math.round(currentDamage).toLocaleString(),
|
||||
career: Math.round(careerDamage).toLocaleString(),
|
||||
delta: currentDamage - careerDamage,
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
filteredOrder.map(async (id, index) => {
|
||||
const tankopediaEntry = awaitedTankopedia[id];
|
||||
const current = orderedCurrentStats[index];
|
||||
const career = orderedCareerStats[index];
|
||||
const currentWN8 = orderedCurrentWN8[index];
|
||||
const careerWN8 = orderedCareerWN8[index];
|
||||
|
||||
children.push(
|
||||
<Breakdown.Row
|
||||
key={id}
|
||||
type="summary"
|
||||
title="Total"
|
||||
type="tank"
|
||||
tankType={tankopediaEntry?.type}
|
||||
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||
title={await resolveTankName(id)}
|
||||
stats={[
|
||||
{
|
||||
title: 'Battles',
|
||||
current: currentBattles,
|
||||
career: careerBattles,
|
||||
current: current.battles.toLocaleString(),
|
||||
career: career.battles.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'Winrate',
|
||||
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
||||
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||
delta: currentWinrate - careerWinrate,
|
||||
current: `${(100 * (current.wins / current.battles)).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
|
||||
delta:
|
||||
current.wins / current.battles - career.wins / career.battles,
|
||||
},
|
||||
{
|
||||
title: 'WN8',
|
||||
@@ -182,83 +228,67 @@ async function render(
|
||||
},
|
||||
{
|
||||
title: 'Damage',
|
||||
current: Math.round(currentDamage).toLocaleString(),
|
||||
career: Math.round(careerDamage).toLocaleString(),
|
||||
delta: currentDamage - careerDamage,
|
||||
current: Math.round(
|
||||
current.damage_dealt / current.battles,
|
||||
).toLocaleString(),
|
||||
career: Math.round(
|
||||
career.damage_dealt / career.battles,
|
||||
).toLocaleString(),
|
||||
delta:
|
||||
current.damage_dealt / current.battles -
|
||||
career.damage_dealt / career.battles,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{await Promise.all(
|
||||
filteredOrder.map(async (id, index) => {
|
||||
const tankopediaEntry = awaitedTankopedia[id];
|
||||
const current = orderedCurrentStats[index];
|
||||
const career = orderedCareerStats[index];
|
||||
const currentWN8 = orderedCurrentWN8[index];
|
||||
const careerWN8 = orderedCareerWN8[index];
|
||||
|
||||
return (
|
||||
<Breakdown.Row
|
||||
key={id}
|
||||
type="tank"
|
||||
tankType={tankopediaEntry?.type}
|
||||
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||
title={await resolveTankName(id)}
|
||||
stats={[
|
||||
{
|
||||
title: 'Battles',
|
||||
current: current.battles.toLocaleString(),
|
||||
career: career.battles.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'Winrate',
|
||||
current: `${(
|
||||
100 *
|
||||
(current.wins / current.battles)
|
||||
).toFixed(2)}%`,
|
||||
career: `${(100 * (career.wins / career.battles)).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
delta:
|
||||
current.wins / current.battles -
|
||||
career.wins / career.battles,
|
||||
},
|
||||
{
|
||||
title: 'WN8',
|
||||
current:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(currentWN8).toLocaleString(),
|
||||
career:
|
||||
careerWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(careerWN8).toLocaleString(),
|
||||
percentile:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: getWN8Percentile(currentWN8),
|
||||
},
|
||||
{
|
||||
title: 'Damage',
|
||||
current: Math.round(
|
||||
current.damage_dealt / current.battles,
|
||||
).toLocaleString(),
|
||||
career: Math.round(
|
||||
career.damage_dealt / career.battles,
|
||||
).toLocaleString(),
|
||||
delta:
|
||||
current.damage_dealt / current.battles -
|
||||
career.damage_dealt / career.battles,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</Breakdown.Root>
|
||||
)}
|
||||
</Wrapper>
|
||||
const pages = chunk(
|
||||
children,
|
||||
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
|
||||
);
|
||||
|
||||
if (filteredOrder.length > 0) {
|
||||
return pages
|
||||
.map((page, index) => (
|
||||
<Wrapper>
|
||||
{index === 0 && (
|
||||
<TitleBar
|
||||
name={accountInfo.nickname}
|
||||
image={
|
||||
clanData?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`${name} • ${filterDescriptions}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Breakdown.Root>{page}</Breakdown.Root>
|
||||
</Wrapper>
|
||||
))
|
||||
.reverse();
|
||||
} else {
|
||||
return [
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={accountInfo.nickname}
|
||||
image={
|
||||
clanData?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`${name} • ${filterDescriptions}`}
|
||||
/>
|
||||
|
||||
{filteredOrder.length === 0 && (
|
||||
<NoData type={NoDataType.BattlesInPeriod} />
|
||||
)}
|
||||
</Wrapper>,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
||||
@@ -286,11 +316,11 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
||||
...filters,
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
render(player, period, filters),
|
||||
return [
|
||||
...(await render(player, period, filters)),
|
||||
buttonPrimary(path, 'Refresh'),
|
||||
getBlitzStarsLinkButton(player.region, player.id),
|
||||
]);
|
||||
await getBlitzStarsLinkButton(player.region, player.id),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||
import getClientId from '../core/blitzkrieg/getClientId';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||
import { client } from '../core/discord/client';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable from '../core/discord/markdownTable';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
const executionStart = new Date().getTime();
|
||||
@@ -21,24 +16,21 @@ export const debugCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
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 embedInfo(
|
||||
`${client.user?.username} debug information`,
|
||||
markdownTable([
|
||||
['Version', packageJSON.version],
|
||||
['Client ID', getClientId()],
|
||||
['Tag', interaction.client.user.tag],
|
||||
[
|
||||
'Uptime',
|
||||
`${Math.floor(uptime / 1000 / 60 / 60 / 24)}d ${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`,
|
||||
],
|
||||
['Tankopedia cached', `${(await tankopedia) !== undefined}`],
|
||||
['Tank averages cached', `${(await tankAverages) !== undefined}`],
|
||||
]),
|
||||
);
|
||||
return `# Debug\n\n${list}`;
|
||||
},
|
||||
};
|
||||
@@ -8,13 +8,13 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||
import getTankStats from '../core/blitz/getTankStats';
|
||||
import getTreeType from '../core/blitz/getTreeType';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
import { tankopediaInfo } from '../core/blitz/tankopediaInfo';
|
||||
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||
import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
TankopediaEntry,
|
||||
Tier,
|
||||
tankopedia,
|
||||
tankopediaInfo,
|
||||
} from '../core/blitzstars/tankopedia';
|
||||
import addTierChoices from '../core/discord/addTierChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
|
||||
@@ -4,21 +4,25 @@ import {
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
} from 'discord.js';
|
||||
import { range } from 'lodash';
|
||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||
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 { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
|
||||
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
|
||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
|
||||
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
||||
import getMidnightLeaderboard, {
|
||||
DATABASE_REPO,
|
||||
} from '../core/blitzkrieg/getMidnightLeaderboard';
|
||||
import { octokit } from '../core/blitzkrieg/octokit';
|
||||
import throwError from '../core/blitzkrieg/throwError';
|
||||
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
|
||||
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
|
||||
import { UserError } from '../core/blitzkrieg/userError';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
@@ -113,14 +117,6 @@ export interface RatingsNeighbors {
|
||||
neighbors: RatingsPlayer[];
|
||||
}
|
||||
|
||||
const LEAGUES = [
|
||||
{ name: 'Diamond', minScore: 5000 },
|
||||
{ name: 'Platinum', minScore: 4000 },
|
||||
{ name: 'Gold', minScore: 3000 },
|
||||
{ name: 'Silver', minScore: 2000 },
|
||||
{ name: 'Bronze', minScore: 0 },
|
||||
];
|
||||
|
||||
function addSubcommands(
|
||||
option: SlashCommandSubcommandGroupBuilder,
|
||||
addOptions = (option: SlashCommandSubcommandBuilder) => option,
|
||||
@@ -129,14 +125,14 @@ function addSubcommands(
|
||||
.addSubcommand((option) =>
|
||||
addOptions(
|
||||
option
|
||||
.setName('neighbours')
|
||||
.setDescription('Your position and neighbours'),
|
||||
.setName('neighbors')
|
||||
.setDescription('Your position and neighbors'),
|
||||
)
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setDescription('How many neighbours to display (default: 10)')
|
||||
.setDescription('How many neighbors to display (default: 10)')
|
||||
.setRequired(false)
|
||||
.setMinValue(5)
|
||||
.setMaxValue(30),
|
||||
@@ -178,23 +174,17 @@ const noOngoingSeason = embedNegative(
|
||||
|
||||
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const seasonChoices = await octokit.repos
|
||||
.getContent({
|
||||
...DATABASE_REPO,
|
||||
path: 'com/ratings',
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (!Array.isArray(data)) {
|
||||
throw throwError('Archived ratings data is malformed');
|
||||
}
|
||||
return data.map(
|
||||
(folder) =>
|
||||
({
|
||||
name: `Season ${folder.name}`,
|
||||
value: folder.name,
|
||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||
);
|
||||
});
|
||||
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
|
||||
const seasonChoices = range(
|
||||
FIRST_ARCHIVED_RATINGS_SEASON,
|
||||
latestArchivedSeasonNumber + 1,
|
||||
).map(
|
||||
(number) =>
|
||||
({
|
||||
name: `Season ${number}`,
|
||||
value: `${number}`,
|
||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||
);
|
||||
|
||||
const command = new SlashCommandBuilder()
|
||||
.setName('ratings')
|
||||
@@ -240,7 +230,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
| 'current';
|
||||
const subcommand = interaction.options.getSubcommand(true) as
|
||||
| 'league'
|
||||
| 'neighbours';
|
||||
| 'neighbors';
|
||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||
const seasonOption = interaction.options.getString('season');
|
||||
const season = seasonOption ? parseInt(seasonOption) : undefined;
|
||||
@@ -262,18 +252,15 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
const region = interaction.options.getString('region') as Region;
|
||||
regionRatingsInfo = await (subcommandGroup === 'current'
|
||||
? getRatingsInfo(region)
|
||||
: getArchivedRatingsInfo(season!, region));
|
||||
: getArchivedRatingsInfo(region, season!));
|
||||
|
||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||
|
||||
const regionSubdomain = regionToRegionSubdomain(region);
|
||||
const result =
|
||||
subcommandGroup === 'current'
|
||||
? await fetch(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
||||
)
|
||||
.then((response) => response.json() as Promise<LeagueTop>)
|
||||
.then(async ({ result }) =>
|
||||
? await getRatingsLeague(region, leagueIndex).then(
|
||||
async (result) =>
|
||||
result.slice(0, limit).map(
|
||||
(player) =>
|
||||
({
|
||||
@@ -284,20 +271,9 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
nickname: player.nickname,
|
||||
}) satisfies SimplifiedPlayer,
|
||||
),
|
||||
)
|
||||
: await octokit.repos
|
||||
.getContent({
|
||||
...DATABASE_REPO,
|
||||
path: `${region}/ratings/${season}/latest.json`,
|
||||
})
|
||||
.then(async ({ data }) => {
|
||||
if (Array.isArray(data) || data.type !== 'file') {
|
||||
throw throwError('Archived ratings data is malformed');
|
||||
}
|
||||
|
||||
const players = (await fetch(data.download_url!).then(
|
||||
(response) => response.json(),
|
||||
)) as BlitzkriegRatingsLeaderboard;
|
||||
)
|
||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
||||
async (players) => {
|
||||
const firstPlayerIndex =
|
||||
leagueIndex === 0
|
||||
? 0
|
||||
@@ -328,15 +304,16 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
`Unknown Player ${player.id}`,
|
||||
}) satisfies SimplifiedPlayer,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||
midnightLeaderboard =
|
||||
subcommandGroup === 'current'
|
||||
? await getMidnightLeaderboard(
|
||||
? await getArchivedRatingsMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
)
|
||||
: await getMidnightLeaderboard(region, season!);
|
||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||
|
||||
players = result;
|
||||
playersBefore = result ? result[0].position - 1 : 0;
|
||||
@@ -354,73 +331,43 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
regionRatingsInfo =
|
||||
subcommandGroup === 'current'
|
||||
? await getRatingsInfo(region)
|
||||
: await octokit.repos
|
||||
.getContent({
|
||||
...DATABASE_REPO,
|
||||
path: `${region}/ratings/${season}/info.json`,
|
||||
})
|
||||
.then(async ({ data }) => {
|
||||
if (Array.isArray(data) || data.type !== 'file') {
|
||||
throw throwError('Archived ratings info is malformed');
|
||||
}
|
||||
|
||||
const response = fetch(data.download_url!);
|
||||
return (await response.then((response) =>
|
||||
response.json(),
|
||||
)) as RatingsInfo;
|
||||
});
|
||||
: await getArchivedRatingsInfo(region, season!);
|
||||
|
||||
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||
|
||||
const accountInfo = await getAccountInfo(region, id);
|
||||
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||
const regionSubdomain = regionToRegionSubdomain(region);
|
||||
const neighbors =
|
||||
subcommandGroup === 'current'
|
||||
? await fetch(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||
limit / 2,
|
||||
)}`,
|
||||
? 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,
|
||||
),
|
||||
)
|
||||
.then(
|
||||
(response) => response.json() as Promise<RatingsNeighbors>,
|
||||
)
|
||||
.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 octokit.repos
|
||||
.getContent({
|
||||
...DATABASE_REPO,
|
||||
path: `${region}/ratings/${season}/latest.json`,
|
||||
})
|
||||
.then(async ({ data }) => {
|
||||
if (Array.isArray(data) || data.type !== 'file') {
|
||||
throw throwError(
|
||||
'Archived ratings latest data is malformed',
|
||||
);
|
||||
}
|
||||
|
||||
const response = fetch(data.download_url!);
|
||||
const players = (await response.then((response) =>
|
||||
response.json(),
|
||||
)) as BlitzkriegRatingsLeaderboard;
|
||||
: await getArchivedRatingsLeaderboard(region, season!).then(
|
||||
async (players) => {
|
||||
const playerIndex = players.findIndex(
|
||||
(player) => player.id === id,
|
||||
);
|
||||
|
||||
if (playerIndex === -1) {
|
||||
throw throwError(
|
||||
throw new UserError(
|
||||
`${accountInfo.nickname} didn't player ratings in season ${season}`,
|
||||
'This player did not participate in this season or did not get past calibration.',
|
||||
{
|
||||
cause:
|
||||
'This player did not participate in this season or did not get past calibration.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -442,14 +389,15 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
clan: clanData[player.id]!.clan?.tag,
|
||||
nickname: clanData[player.id]!.account_name,
|
||||
}));
|
||||
});
|
||||
},
|
||||
);
|
||||
midnightLeaderboard =
|
||||
subcommandGroup === 'current'
|
||||
? await getMidnightLeaderboard(
|
||||
? await getArchivedRatingsMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
)
|
||||
: await getMidnightLeaderboard(region, season!);
|
||||
: await getArchivedRatingsMidnightLeaderboard(region, season!);
|
||||
|
||||
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
||||
|
||||
@@ -460,7 +408,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
titleName = neighbors
|
||||
? accountInfo.nickname
|
||||
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
||||
titleDescription = 'Ratings neighbours';
|
||||
titleDescription = 'Ratings neighbors';
|
||||
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
||||
playersAfter =
|
||||
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
||||
@@ -513,10 +461,12 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
{items && (
|
||||
<Leaderboard.Root>
|
||||
{playersBefore > 0 && (
|
||||
<Leaderboard.Gap number={playersBefore} />
|
||||
<Leaderboard.Gap message={`+ ${playersBefore} more`} />
|
||||
)}
|
||||
{items}
|
||||
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
||||
{playersAfter > 0 && (
|
||||
<Leaderboard.Gap message={`+ ${playersAfter} more`} />
|
||||
)}
|
||||
</Leaderboard.Root>
|
||||
)}
|
||||
</Wrapper>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../core/blitz/fetchWargaming';
|
||||
import fetchBlitz from '../core/blitz/fetchBlitz';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../core/blitz/fetchWargaming';
|
||||
import fetchBlitz from '../core/blitz/fetchBlitz';
|
||||
import {
|
||||
AccountList,
|
||||
usernamePattern,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
/**
|
||||
* TODO: @deprecated
|
||||
*/
|
||||
export const todayCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('today')
|
||||
.setDescription('This command has been renamed to `/breakdown today`'),
|
||||
|
||||
handler() {
|
||||
return '# This command has been renamed to `/breakdown today`\n`/today` now comes with filters, periods and more! Use `/breakdown today` for same old functionality but also do checkout the other options.';
|
||||
},
|
||||
};
|
||||
@@ -15,9 +15,18 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 128,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{stat[0]}
|
||||
</span>
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface GapProps {
|
||||
number: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function Gap({ number }: GapProps) {
|
||||
export function Gap({ message: number }: GapProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -22,7 +22,7 @@ export function Gap({ number }: GapProps) {
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
+ {number.toLocaleString()} more
|
||||
{number.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import { RatingsReward } from '../../../commands/ratings';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface ItemProps {
|
||||
export interface ItemProps extends ComponentProps<'div'> {
|
||||
position: number;
|
||||
deltaPosition?: number;
|
||||
score: number;
|
||||
score?: number;
|
||||
deltaScore?: number;
|
||||
reward?: RatingsReward;
|
||||
nickname: string;
|
||||
@@ -25,10 +26,13 @@ export function Item({
|
||||
nickname,
|
||||
clan,
|
||||
highlight,
|
||||
...props
|
||||
}: ItemProps) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: highlight
|
||||
@@ -185,16 +189,18 @@ export function Item({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{score.toLocaleString()}
|
||||
</span>
|
||||
{score !== undefined && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{score.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import { Flex, Theme } from '@radix-ui/themes';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
interface PageWrapperProps extends ComponentProps<typeof Flex> {
|
||||
color?: ComponentProps<typeof Theme>['accentColor'];
|
||||
}
|
||||
|
||||
export default function PageWrapper({
|
||||
style,
|
||||
color,
|
||||
...props
|
||||
}: ComponentProps<'div'>) {
|
||||
}: PageWrapperProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 800,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
padding: 16,
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
boxSizing: 'border-box',
|
||||
<Theme accentColor={color}>
|
||||
<Flex
|
||||
{...props}
|
||||
direction="column"
|
||||
gap="4"
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 800,
|
||||
margin: 'auto',
|
||||
padding: 16,
|
||||
boxSizing: 'border-box',
|
||||
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { slateDarkA } from '@radix-ui/colors';
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
const animation = keyframes({
|
||||
'0%': { backgroundColor: slateDarkA.slateA2 },
|
||||
'50%': { backgroundColor: slateDarkA.slateA3 },
|
||||
'100%': { backgroundColor: slateDarkA.slateA2 },
|
||||
});
|
||||
|
||||
export const skeleton = style({
|
||||
borderRadius: 8,
|
||||
animation: `${animation} 2s ease-in-out infinite`,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import { skeleton } from './index.css';
|
||||
|
||||
export function Skeleton({ className, ...props }: ComponentProps<'div'>) {
|
||||
return <div {...props} className={`${className} ${skeleton}`} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const LEAGUES = [
|
||||
{ name: 'Diamond', minScore: 5000 },
|
||||
{ name: 'Platinum', minScore: 4000 },
|
||||
{ name: 'Gold', minScore: 3000 },
|
||||
{ name: 'Silver', minScore: 2000 },
|
||||
{ name: 'Bronze', minScore: 0 },
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
export const NATION_IDS: Record<string, number> = {
|
||||
ussr: 0,
|
||||
germany: 1,
|
||||
usa: 2,
|
||||
china: 3,
|
||||
france: 4,
|
||||
uk: 5,
|
||||
japan: 6,
|
||||
other: 7,
|
||||
european: 8,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const FIRST_ARCHIVED_RATINGS_SEASON = 49;
|
||||
@@ -1,4 +1,6 @@
|
||||
export type Region = 'com' | 'eu' | 'asia';
|
||||
export const REGIONS = ['com', 'eu', 'asia'] as const;
|
||||
|
||||
export type Region = (typeof REGIONS)[number];
|
||||
export type RegionSubdomain = 'na' | 'eu' | 'asia';
|
||||
|
||||
export const REGION_NAMES: Record<Region, string> = {
|
||||
@@ -11,7 +13,7 @@ export const REGION_NAMES: Record<Region, string> = {
|
||||
export const REGION_NAMES_SHORT: Record<Region, string> = {
|
||||
com: 'NA',
|
||||
eu: 'EU',
|
||||
asia: 'AS',
|
||||
asia: 'ASIA',
|
||||
};
|
||||
|
||||
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
||||
|
||||
@@ -9,13 +9,12 @@ export const TOOLS: Tool[] = [
|
||||
{
|
||||
id: 'session',
|
||||
title: 'Session tracker',
|
||||
description: 'Track your performance in a session',
|
||||
description: 'Live session performance tracker',
|
||||
},
|
||||
{
|
||||
id: 'ratings',
|
||||
title: 'Ratings',
|
||||
description: 'Current and old season leader boards',
|
||||
disabled: true,
|
||||
description: 'Current and archived ratings leaderboards',
|
||||
},
|
||||
{
|
||||
id: 'tankopedia',
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
export type BlitzResponse<Data extends object> =
|
||||
| {
|
||||
status: 'error';
|
||||
error: { field: string; message: string; code: 402; value: null };
|
||||
}
|
||||
| {
|
||||
status: 'ok';
|
||||
data: Data;
|
||||
};
|
||||
|
||||
const MAX_CALLS_PER_SECOND = 10;
|
||||
|
||||
const queue: { url: string; resolve: (data: object) => void }[] = [];
|
||||
let inProgress = 0;
|
||||
|
||||
async function manageQueue() {
|
||||
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
|
||||
inProgress++;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
inProgress--;
|
||||
manageQueue();
|
||||
}, 1000);
|
||||
|
||||
const { url, resolve } = queue.shift()!;
|
||||
const data = (await fetch(url)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => ({ status: 'error', error }))) as BlitzResponse<
|
||||
typeof resolve
|
||||
>;
|
||||
|
||||
if (data.status === 'ok') {
|
||||
resolve(data.data);
|
||||
} else {
|
||||
clearTimeout(timeout);
|
||||
inProgress--;
|
||||
manageQueue();
|
||||
|
||||
throw new Error(`Wargaming response error status: "${data.status}"`, {
|
||||
cause: `Message: "${data.error.message}"\nURL: "${url}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function fetchBlitz<Data extends object>(url: string) {
|
||||
return new Promise<Data>((resolve) => {
|
||||
queue.push({
|
||||
url,
|
||||
resolve(data) {
|
||||
resolve(data as Data);
|
||||
},
|
||||
});
|
||||
manageQueue();
|
||||
});
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import throwError from '../blitzkrieg/throwError';
|
||||
|
||||
export type BlitzResponse<Data extends object> =
|
||||
| {
|
||||
status: 'error';
|
||||
error: { field: string; message: string; code: 402; value: null };
|
||||
}
|
||||
| {
|
||||
status: 'ok';
|
||||
data: Data;
|
||||
};
|
||||
|
||||
export default async function fetchBlitz<Data extends object>(url: string) {
|
||||
const data = (await fetch(url).then((response) =>
|
||||
response.json(),
|
||||
)) as BlitzResponse<Data>;
|
||||
|
||||
if (data.status === 'ok') {
|
||||
return data.data;
|
||||
} else {
|
||||
throw throwError(
|
||||
`Wargaming response error status:"${data.status}"`,
|
||||
`Message: "${data.error.message}"\nURL: "${url}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from './fetchWargaming';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
|
||||
interface IndividualAccountAchievements {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from './fetchWargaming';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
|
||||
export interface AllStats {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from './fetchWargaming';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
import { normalizeExtra } from './normalizeExtra';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from './fetchWargaming';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
import { normalizeIds } from './normalizeIds';
|
||||
|
||||
export interface IndividualClanInfo {
|
||||
@@ -41,7 +41,7 @@ export async function getClanInfo<
|
||||
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
|
||||
>(region: Region, ids: Ids) {
|
||||
const object = await fetchBlitz<ClanInfo>(
|
||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
|
||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${normalizeIds(
|
||||
ids,
|
||||
)}`,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { RatingsInfo } from '../../commands/ratings';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { context } from '../blitzkrieg/context';
|
||||
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||
|
||||
export default async function getRatingsInfo(region: Region) {
|
||||
const response = await fetch(
|
||||
`https://${regionToRegionSubdomain(
|
||||
region,
|
||||
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
const response = await patientFetch(
|
||||
context === 'website'
|
||||
? `/api/${region}/ratings/current/info`
|
||||
: `https://${regionToRegionSubdomain(
|
||||
region,
|
||||
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
undefined,
|
||||
{ cache: 'no-store' },
|
||||
);
|
||||
|
||||
return (await response.json()) as Promise<RatingsInfo>;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { LeagueTop, RatingsPlayer } from '../../commands/ratings';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { context } from '../blitzkrieg/context';
|
||||
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||
|
||||
export async function getRatingsLeague(
|
||||
region: Region,
|
||||
league: number,
|
||||
usePatientFetcher = false,
|
||||
) {
|
||||
const fetcher = (url: string) =>
|
||||
usePatientFetcher
|
||||
? patientFetch(url, undefined, { cache: 'no-store' })
|
||||
: fetch(url, { cache: 'no-store' });
|
||||
const response = await fetcher(
|
||||
context === 'website'
|
||||
? `/api/${region}/ratings/current/league/${league}`
|
||||
: `https://${regionToRegionSubdomain(
|
||||
region,
|
||||
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
return context === 'website'
|
||||
? (json as RatingsPlayer[])
|
||||
: (json as LeagueTop).result;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { RatingsNeighbors } from '../../commands/ratings';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { patientFetch } from '../blitzkrieg/patientFetch';
|
||||
import { withCORSProxy } from '../blitzkrieg/withCORSProxy';
|
||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||
|
||||
export async function getRatingsNeighbors(
|
||||
region: Region,
|
||||
id: number,
|
||||
count: number,
|
||||
usePatientFetcher = false,
|
||||
) {
|
||||
const fetcher = (url: string) =>
|
||||
usePatientFetcher
|
||||
? patientFetch(url, undefined, { cache: 'no-store' })
|
||||
: fetch(url, { cache: 'no-store' });
|
||||
const response = await fetcher(
|
||||
withCORSProxy(
|
||||
`https://${regionToRegionSubdomain(
|
||||
region,
|
||||
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`,
|
||||
),
|
||||
);
|
||||
const json = (await response.json()) as RatingsNeighbors;
|
||||
|
||||
return json;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import { TanksStats } from '../../types/tanksStats';
|
||||
import throwError from '../blitzkrieg/throwError';
|
||||
import fetchBlitz from './fetchWargaming';
|
||||
import { UserError } from '../blitzkrieg/userError';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
|
||||
export default async function getTankStats(region: Region, id: number) {
|
||||
const tankStats = await fetchBlitz<TanksStats>(
|
||||
@@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) {
|
||||
);
|
||||
|
||||
if (tankStats[id] === null) {
|
||||
throw throwError(
|
||||
'No tank stats available',
|
||||
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
|
||||
);
|
||||
throw new UserError('No tank stats available', {
|
||||
cause:
|
||||
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
|
||||
});
|
||||
}
|
||||
|
||||
return tankStats[id];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { go } from 'fuzzysort';
|
||||
import throwError from '../blitzkrieg/throwError';
|
||||
import { UserError } from '../blitzkrieg/userError';
|
||||
import { TANKS, tankopedia } from '../blitzstars/tankopedia';
|
||||
|
||||
export default async function resolveTankId(tank: string | number) {
|
||||
@@ -12,10 +12,9 @@ export default async function resolveTankId(tank: string | number) {
|
||||
});
|
||||
|
||||
if (searchResult.length === 0) {
|
||||
throw throwError(
|
||||
'Tank not found',
|
||||
`Could not find tank by the name "${tank}".`,
|
||||
);
|
||||
throw new UserError('Tank not found', {
|
||||
cause: `Could not find tank by the name "${tank}".`,
|
||||
});
|
||||
} else {
|
||||
return searchResult[0].obj.tank_id;
|
||||
}
|
||||
@@ -23,10 +22,9 @@ export default async function resolveTankId(tank: string | number) {
|
||||
if ((await tankopedia)[number]) {
|
||||
return number;
|
||||
} else {
|
||||
throw throwError(
|
||||
'Tank not found',
|
||||
`Could not find tank by the ID "${number}".`,
|
||||
);
|
||||
throw new UserError('Tank not found', {
|
||||
cause: `Could not find tank by the ID "${number}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import { Clan, ClanList } from '../../types/clanList';
|
||||
import fetchBlitz from './fetchWargaming';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
|
||||
export type ClanListWithRegion = (Clan & {
|
||||
region: 'com' | 'eu' | 'asia';
|
||||
@@ -9,43 +9,45 @@ export type ClanListWithRegion = (Clan & {
|
||||
|
||||
export default async function searchClansAcrossRegions(
|
||||
search: string,
|
||||
limit = 9,
|
||||
limit = 25,
|
||||
) {
|
||||
const trimmed = search.trim();
|
||||
if (trimmed.length < 3 && trimmed.length > 100) return [];
|
||||
const normalizedLimit = Math.round(limit / 3);
|
||||
const normalizedLimit = Math.floor(limit / 3);
|
||||
|
||||
// TODO: remove splice once wargaming fixes the functionality of the limit felid
|
||||
|
||||
return (
|
||||
await Promise.all([
|
||||
fetchBlitz<ClanList>(
|
||||
`https://api.wotblitz.com/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
).then((value) =>
|
||||
value
|
||||
.map((account) => ({
|
||||
...account,
|
||||
region: 'com' as Region,
|
||||
})),
|
||||
}))
|
||||
.splice(0, normalizedLimit),
|
||||
),
|
||||
fetchBlitz<ClanList>(
|
||||
`https://api.wotblitz.eu/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
).then((value) =>
|
||||
value
|
||||
.map((account) => ({
|
||||
...account,
|
||||
region: 'eu' as Region,
|
||||
})),
|
||||
}))
|
||||
.splice(0, normalizedLimit),
|
||||
),
|
||||
fetchBlitz<ClanList>(
|
||||
`https://api.wotblitz.asia/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
).then((value) =>
|
||||
value
|
||||
.map((account) => ({
|
||||
...account,
|
||||
region: 'asia' as Region,
|
||||
})),
|
||||
}))
|
||||
.splice(0, normalizedLimit),
|
||||
),
|
||||
])
|
||||
)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { context } from '../blitzkrieg/context';
|
||||
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||
|
||||
type CurrentRatingsPlayerSearch = Record<
|
||||
number,
|
||||
{
|
||||
spa_id: number;
|
||||
nickname: string;
|
||||
clan_tag: string;
|
||||
} & (
|
||||
| {
|
||||
skip: true;
|
||||
}
|
||||
| {
|
||||
skip: false;
|
||||
mmr: number;
|
||||
season_number: number;
|
||||
calibrationBattlesLeft: number;
|
||||
number: number;
|
||||
updated_at: string;
|
||||
neighbors: [];
|
||||
}
|
||||
)
|
||||
>;
|
||||
|
||||
export async function searchCurrentRatingsPlayers(
|
||||
region: Region,
|
||||
name: string,
|
||||
) {
|
||||
const encodedSearch = encodeURIComponent(name);
|
||||
const response = await fetch(
|
||||
context === 'website'
|
||||
? `/api/${region}/ratings/current/search/${encodedSearch}`
|
||||
: `https://${regionToRegionSubdomain(
|
||||
region,
|
||||
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
|
||||
{ cache: 'no-store' },
|
||||
);
|
||||
const accountList = (await response.json()) as CurrentRatingsPlayerSearch;
|
||||
|
||||
return accountList;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from './fetchWargaming';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
|
||||
interface Account {
|
||||
export interface AccountListItem {
|
||||
nickname: string;
|
||||
account_id: number;
|
||||
}
|
||||
export type AccountList = Account[];
|
||||
export type AccountListWithServer = (Account & {
|
||||
export type AccountList = AccountListItem[];
|
||||
export type AccountListWithServer = (AccountListItem & {
|
||||
region: Region;
|
||||
})[];
|
||||
|
||||
@@ -16,10 +16,10 @@ export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
|
||||
|
||||
export default async function searchPlayersAcrossRegions(
|
||||
search: string,
|
||||
limit = 9,
|
||||
limit = 25,
|
||||
) {
|
||||
const trimmedSearch = search.trim();
|
||||
const normalizedLimit = Math.round(limit / 3);
|
||||
const normalizedLimit = Math.floor(limit / 3);
|
||||
const encodedSearch = encodeURIComponent(trimmedSearch);
|
||||
|
||||
if (usernamePattern.test(trimmedSearch)) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from './fetchBlitz';
|
||||
|
||||
export interface TankopediaInfo {
|
||||
achievement_sections: Record<string, { name: string; order: number }>;
|
||||
tanks_updated_at: number;
|
||||
languages: Record<string, string>;
|
||||
vehicle_types: Record<string, string>;
|
||||
vehicle_nations: Record<string, string>;
|
||||
game_version: string;
|
||||
}
|
||||
|
||||
export const tankopediaInfo = fetchBlitz<TankopediaInfo>(
|
||||
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
type Context = 'website' | 'bot';
|
||||
type Context = 'website' | 'server';
|
||||
|
||||
export const context: Context =
|
||||
typeof window === 'undefined' ? 'bot' : 'website';
|
||||
typeof window === 'undefined' ? 'server' : 'website';
|
||||
@@ -1,10 +1,7 @@
|
||||
import { TREE_TYPE_NAMES } from '../../components/Tanks';
|
||||
import resolveTankName from '../blitz/resolveTankName';
|
||||
import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
Tier,
|
||||
tankopediaInfo,
|
||||
} from '../blitzstars/tankopedia';
|
||||
import { tankopediaInfo } from '../blitz/tankopediaInfo';
|
||||
import { TIER_ROMAN_NUMERALS, Tier } from '../blitzstars/tankopedia';
|
||||
import { StatFilters } from '../statistics/filterStats';
|
||||
|
||||
export async function filtersToDescription({
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import getRatingsInfo from '../blitz/getRatingsInfo';
|
||||
|
||||
let cachedLatestArchivedSeasonNumber: number | null = null;
|
||||
|
||||
export async function getArchivedLatestSeasonNumber() {
|
||||
const ratingsInfo = await getRatingsInfo('com');
|
||||
|
||||
if (ratingsInfo.detail) {
|
||||
const comURL = (
|
||||
(await fetch(
|
||||
'https://api.github.com/repos/tresabhi/blitzkrieg-db/git/trees/main',
|
||||
).then((response) => response.json())) as {
|
||||
tree: { path: string; url: string }[];
|
||||
}
|
||||
).tree.find(({ path }) => path === 'com')!.url;
|
||||
const ratingsURL = (
|
||||
(await fetch(comURL).then((response) => response.json())) as {
|
||||
tree: [{ url: string }];
|
||||
}
|
||||
).tree[0].url;
|
||||
const latestSeason = Math.max(
|
||||
...(
|
||||
(await fetch(ratingsURL).then((response) => {
|
||||
response.json();
|
||||
})) as { tree: { path: string }[] }
|
||||
).tree.map(({ path }) => parseInt(path)),
|
||||
);
|
||||
|
||||
return latestSeason;
|
||||
} else {
|
||||
return ratingsInfo.current_season - 1;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import { RatingsInfo } from '../../commands/ratings';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { DATABASE_REPO } from './getMidnightLeaderboard';
|
||||
import { octokit } from './octokit';
|
||||
import throwError from './throwError';
|
||||
|
||||
const ARCHIVED_RATINGS_CACHE: Record<
|
||||
const INFO_CACHE: Record<
|
||||
Region,
|
||||
Record<
|
||||
number,
|
||||
@@ -19,27 +16,21 @@ const ARCHIVED_RATINGS_CACHE: Record<
|
||||
};
|
||||
|
||||
export default async function getArchivedRatingsInfo(
|
||||
season: number,
|
||||
region: Region,
|
||||
season: number,
|
||||
) {
|
||||
if (ARCHIVED_RATINGS_CACHE[region][season]) {
|
||||
return ARCHIVED_RATINGS_CACHE[region][season];
|
||||
if (INFO_CACHE[region][season]) {
|
||||
return INFO_CACHE[region][season];
|
||||
}
|
||||
|
||||
const { data } = await octokit.repos.getContent({
|
||||
...DATABASE_REPO,
|
||||
path: `${region}/ratings/${season}/info.json`,
|
||||
});
|
||||
|
||||
if (Array.isArray(data) || data.type !== 'file')
|
||||
throw throwError('Archived ratings info is malformed');
|
||||
|
||||
const content = Buffer.from(data.content, 'base64').toString();
|
||||
const jsonContent = JSON.parse(content) as RatingsInfo & {
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/tresabhi/blitzkrieg-db/main/${region}/ratings/${season}/info.json`,
|
||||
);
|
||||
const jsonContent = (await response.json()) as RatingsInfo & {
|
||||
detail: undefined;
|
||||
};
|
||||
|
||||
ARCHIVED_RATINGS_CACHE[region][season] = jsonContent;
|
||||
INFO_CACHE[region][season] = jsonContent;
|
||||
|
||||
return jsonContent;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
||||
import { Region } from '../../constants/regions';
|
||||
|
||||
const LEADERBOARD_CACHE: Record<
|
||||
Region,
|
||||
Record<number, BlitzkriegRatingsLeaderboard>
|
||||
> = {
|
||||
asia: {},
|
||||
eu: {},
|
||||
com: {},
|
||||
};
|
||||
|
||||
export async function getArchivedRatingsLeaderboard(
|
||||
region: Region,
|
||||
season: number,
|
||||
) {
|
||||
if (LEADERBOARD_CACHE[region][season]) {
|
||||
return LEADERBOARD_CACHE[region][season];
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/tresabhi/blitzkrieg-db/main/${region}/ratings/${season}/latest.json`,
|
||||
);
|
||||
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
|
||||
LEADERBOARD_CACHE[region][season] = json;
|
||||
|
||||
return json;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { octokit } from './octokit';
|
||||
|
||||
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
|
||||
|
||||
@@ -13,7 +12,7 @@ const MIDNIGHT_LEADERBOARD_CACHE: Record<
|
||||
asia: {},
|
||||
};
|
||||
|
||||
export default async function getMidnightLeaderboard(
|
||||
export default async function getArchivedRatingsMidnightLeaderboard(
|
||||
region: Region,
|
||||
season: number,
|
||||
) {
|
||||
@@ -22,19 +21,13 @@ export default async function getMidnightLeaderboard(
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await octokit.repos.getContent({
|
||||
...DATABASE_REPO,
|
||||
path: `${region}/ratings/${season}/midnight.json`,
|
||||
});
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/tresabhi/blitzkrieg-db/main/${region}/ratings/${season}/midnight.json`,
|
||||
);
|
||||
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
|
||||
MIDNIGHT_LEADERBOARD_CACHE[region][season] = json;
|
||||
|
||||
if (!Array.isArray(data) && data.type === 'file') {
|
||||
const response = await fetch(data.download_url!);
|
||||
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
|
||||
|
||||
MIDNIGHT_LEADERBOARD_CACHE[region][season] = json;
|
||||
|
||||
return json;
|
||||
}
|
||||
return json;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`No midnight leaderboard found for season ${season}, region ${region}`,
|
||||
@@ -0,0 +1,21 @@
|
||||
export async function patientFetch(
|
||||
url: string,
|
||||
timeout = 1000,
|
||||
options?: Partial<{ cache: 'no-store' }>,
|
||||
) {
|
||||
let patientResponse: Response;
|
||||
|
||||
while (patientResponse! === undefined) {
|
||||
try {
|
||||
patientResponse = await fetch(url, options);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to fetch ${url}; retrying in ${timeout}ms...`,
|
||||
error,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
}
|
||||
|
||||
return patientResponse;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export async function patientFetchJSON<Type extends object>(
|
||||
url: string,
|
||||
timeout = 1000,
|
||||
options?: Partial<{ cache: 'no-store' }>,
|
||||
) {
|
||||
let patientJSON: Type;
|
||||
|
||||
while (patientJSON! === undefined) {
|
||||
try {
|
||||
patientJSON = (await fetch(url, options).then((response) =>
|
||||
response.json(),
|
||||
)) as Type;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to fetch ${url} as JSON; retrying in ${timeout}ms...`,
|
||||
error,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
}
|
||||
|
||||
return patientJSON;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Worker } from 'worker_threads';
|
||||
|
||||
const MAX_WORKERS = 3;
|
||||
const MAX_WORKERS = 2;
|
||||
|
||||
const workers = [...Array(MAX_WORKERS)].map(() => {
|
||||
const worker = new Worker(`${__dirname}/workers/render.cjs`);
|
||||
@@ -19,18 +19,22 @@ function manageQueue() {
|
||||
|
||||
function cleanup() {
|
||||
workers.push(worker);
|
||||
worker.off('message', handleMessage);
|
||||
worker.off('error', handleError);
|
||||
manageQueue();
|
||||
}
|
||||
function handleMessage(png: Uint8Array) {
|
||||
cleanup();
|
||||
resolve(Buffer.from(png));
|
||||
}
|
||||
function handleError(error: Error) {
|
||||
cleanup();
|
||||
throw error;
|
||||
}
|
||||
|
||||
worker.postMessage(svg);
|
||||
worker.on('message', (png: Uint8Array) => {
|
||||
cleanup();
|
||||
resolve(Buffer.from(png));
|
||||
});
|
||||
worker.on('error', (error) => {
|
||||
cleanup();
|
||||
throw error;
|
||||
});
|
||||
worker.on('message', handleMessage);
|
||||
worker.on('error', handleError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function throwError(message: string, cause?: string) {
|
||||
return new Error(message, { cause });
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Discriminator for user caused errors to not report in logs
|
||||
*/
|
||||
export class UserError extends Error {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { context } from './context';
|
||||
|
||||
export function withCORSProxy(url: string) {
|
||||
return context === 'website'
|
||||
? `https://corsproxy.io/?${encodeURIComponent(url)}`
|
||||
: url;
|
||||
}
|
||||
@@ -85,13 +85,13 @@ tankopedia;
|
||||
export type TankAverages = Record<number, IndividualTankAverage>;
|
||||
|
||||
export const tankAverages = fetch(
|
||||
context === 'bot'
|
||||
? 'https://www.blitzstars.com/api/tankaverages.json'
|
||||
: '/api/tank-averages',
|
||||
context === 'website'
|
||||
? '/api/tank-averages'
|
||||
: 'https://www.blitzstars.com/api/tankaverages.json',
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
if (context === 'bot') {
|
||||
if (context === 'server') {
|
||||
const partialTankAverages: TankAverages = {};
|
||||
|
||||
(json as IndividualTankAverage[]).forEach((individualTankAverage) => {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { deburr } from 'lodash';
|
||||
import { TreeTypeString } from '../../components/Tanks';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../blitz/fetchWargaming';
|
||||
import { context } from '../blitzkrieg/context';
|
||||
|
||||
export interface TankopediaEntry {
|
||||
@@ -16,19 +13,18 @@ export interface TankopediaEntry {
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
type asd = TreeTypeString;
|
||||
export interface Tankopedia {
|
||||
[id: number]: TankopediaEntry | undefined;
|
||||
}
|
||||
|
||||
export const tankopedia = fetch(
|
||||
context === 'bot'
|
||||
? 'https://www.blitzstars.com/bs-tankopedia.json'
|
||||
: '/api/tankopedia',
|
||||
context === 'website'
|
||||
? '/api/tankopedia'
|
||||
: 'https://www.blitzstars.com/bs-tankopedia.json',
|
||||
)
|
||||
.then(async (response) => response.json())
|
||||
.then((wrapperTankopedia) =>
|
||||
context === 'bot'
|
||||
context === 'server'
|
||||
? (wrapperTankopedia as { data: Tankopedia }).data
|
||||
: (wrapperTankopedia as Tankopedia),
|
||||
);
|
||||
@@ -58,20 +54,6 @@ export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) =>
|
||||
),
|
||||
);
|
||||
|
||||
export interface TankopediaInfo {
|
||||
achievement_sections: Record<string, { name: string; order: number }>;
|
||||
tanks_updated_at: number;
|
||||
languages: Record<string, string>;
|
||||
vehicle_types: Record<string, string>;
|
||||
vehicle_nations: Record<string, string>;
|
||||
game_version: string;
|
||||
}
|
||||
|
||||
// this is blocking because info is needed for command creation
|
||||
export const tankopediaInfo = fetchBlitz<TankopediaInfo>(
|
||||
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
|
||||
);
|
||||
|
||||
export const TANK_ICONS: Record<string, string> = {
|
||||
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
|
||||
lightTank: 'https://i.imgur.com/CSNha5V.png',
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
} from 'discord.js';
|
||||
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
|
||||
import { tankopediaInfo } from '../blitzstars/tankopedia';
|
||||
import { tankopediaInfo } from '../blitz/tankopediaInfo';
|
||||
import addPeriodSubCommands from './addPeriodSubCommands';
|
||||
import addTankChoices from './addTankChoices';
|
||||
import addTierChoices from './addTierChoices';
|
||||
|
||||
@@ -3,8 +3,6 @@ import { SlashCommandStringOption } from 'discord.js';
|
||||
export default function addUsernameChoices(option: SlashCommandStringOption) {
|
||||
return option
|
||||
.setName('username')
|
||||
.setDescription(
|
||||
'The username you use in Blitz (inferred from Discord nickname by default)',
|
||||
)
|
||||
.setDescription('The username you use in Blitz')
|
||||
.setAutocomplete(true);
|
||||
}
|
||||
@@ -3,26 +3,39 @@ import {
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { REGION_NAMES } from '../../constants/regions';
|
||||
import { REGION_NAMES_SHORT } from '../../constants/regions';
|
||||
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
|
||||
|
||||
export const DISCORD_CHOICES_MAX_NAME_SIZE = 25;
|
||||
export const OVERFLOW_SUFFIX = '…';
|
||||
|
||||
export default async function autocompleteClan(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
) {
|
||||
const focusedOption = interaction.options.getFocused(true);
|
||||
if (focusedOption.name !== 'clan') return;
|
||||
if (focusedOption.value.length < 2) return interaction.respond([]);
|
||||
const players = await searchClansAcrossRegions(focusedOption.value);
|
||||
const clans = await searchClansAcrossRegions(focusedOption.value);
|
||||
|
||||
await interaction.respond(
|
||||
players
|
||||
? players.map(
|
||||
(player) =>
|
||||
({
|
||||
name: `${player.name} (${REGION_NAMES[player.region]})`,
|
||||
value: `${player.region}/${player.clan_id}`,
|
||||
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
||||
)
|
||||
clans
|
||||
? clans.map((clan) => {
|
||||
let name = `${clan.name} (${REGION_NAMES_SHORT[clan.region]})`;
|
||||
|
||||
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
|
||||
const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE;
|
||||
|
||||
name = `${clan.name.slice(
|
||||
0,
|
||||
clan.name.length - overSize - OVERFLOW_SUFFIX.length,
|
||||
)}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[clan.region]})`;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value: `${clan.region}/${clan.clan_id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>;
|
||||
})
|
||||
: [],
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
} from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import { TANK_NAMES_DIACRITICS } from '../blitzstars/tankopedia';
|
||||
import {
|
||||
DISCORD_CHOICES_MAX_NAME_SIZE,
|
||||
OVERFLOW_SUFFIX,
|
||||
} from './autocompleteClan';
|
||||
|
||||
export default async function autocompleteTanks(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
@@ -18,13 +22,23 @@ export default async function autocompleteTanks(
|
||||
go(focusedOption.value, await TANK_NAMES_DIACRITICS, {
|
||||
keys: ['combined'],
|
||||
limit: 10,
|
||||
}).map(
|
||||
async (item) =>
|
||||
({
|
||||
name: item.obj.original,
|
||||
value: `${item.obj.id}`,
|
||||
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
||||
),
|
||||
}).map(async (item) => {
|
||||
let name = item.obj.original;
|
||||
|
||||
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
|
||||
const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE;
|
||||
|
||||
name = `${item.obj.original.slice(
|
||||
0,
|
||||
item.obj.original.length - overSize - OVERFLOW_SUFFIX.length,
|
||||
)}${OVERFLOW_SUFFIX}`;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value: `${item.obj.id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>;
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
|
||||