Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92e35bd0ac | ||
|
|
cb9d283f62 | ||
|
|
8aba19e05b | ||
|
|
b7b4035492 | ||
|
|
3667fd82b7 | ||
|
|
691d5cde27 | ||
|
|
7f431b07e1 | ||
|
|
8264f19ad4 | ||
|
|
e947cfba45 | ||
|
|
e69590c035 | ||
|
|
8b19393d7c | ||
|
|
9480603572 | ||
|
|
90c6535a4c | ||
|
|
258588921a | ||
|
|
525b309037 | ||
|
|
2f941b9224 | ||
|
|
259c4240b5 | ||
|
|
9eb7157fce | ||
|
|
1138e1a826 | ||
|
|
e5057b8c1d | ||
|
|
7c04243b3d | ||
|
|
c6b54454be | ||
|
|
876fb0fc5e | ||
|
|
38f912dc0a | ||
|
|
eb24f49766 | ||
|
|
168ab1e1a1 | ||
|
|
49f601d200 | ||
|
|
15f32a0044 | ||
|
|
7bb3c60a61 | ||
|
|
4d1dfc1ae5 | ||
|
|
ec5dfd7727 | ||
|
|
8e5a588e87 | ||
|
|
b9c3cfaee4 | ||
|
|
01bd3f8d68 | ||
|
|
b39f545669 | ||
|
|
e7f517e157 | ||
|
|
1d7de715e7 | ||
|
|
76501c1aa0 | ||
|
|
a535a07810 | ||
|
|
6add54e86d | ||
|
|
a74eba9e97 | ||
|
|
34f782e747 | ||
|
|
1990322d6d | ||
|
|
0339979f9f | ||
|
|
823465fde9 | ||
|
|
1feae49490 | ||
|
|
1a1e2169a8 | ||
|
|
f1b1e25d9a | ||
|
|
ce6472be8d | ||
|
|
79232cecb9 | ||
|
|
1a802da0ce | ||
|
|
b33a845c08 | ||
|
|
6643b51a9b | ||
|
|
1cce00a09a | ||
|
|
1ef9a0289f | ||
|
|
498a359f01 | ||
|
|
ebbcdc2089 | ||
|
|
cced57b22e | ||
|
|
96d47a82bc | ||
|
|
b4cab18ecd | ||
|
|
d801447e5d | ||
|
|
8c041d4eef | ||
|
|
6ff2a23ff7 | ||
|
|
925ac96264 | ||
|
|
9e89198cd5 | ||
|
|
a1e6643dc0 | ||
|
|
63257a13f5 | ||
|
|
e37e0ccf9f | ||
|
|
018783bb47 | ||
|
|
f883670cbc | ||
|
|
57f6e928b7 | ||
|
|
cbeb83d8d0 | ||
|
|
33b4ea04b4 | ||
|
|
350434cc5a | ||
|
|
c0eca0c851 | ||
|
|
cdac43fa5f | ||
|
|
30aeabba88 | ||
|
|
02a07f3a8a | ||
|
|
986a32b3ce | ||
|
|
8c4afa73f8 | ||
|
|
6db3ccdb40 | ||
|
|
cf0fe5cbba | ||
|
|
621398f95b | ||
|
|
186a972db7 | ||
|
|
7473a17a5e | ||
|
|
66e08afb0d | ||
|
|
851da1d020 | ||
|
|
c44840978c | ||
|
|
90e012319a | ||
|
|
18de9cbaab | ||
|
|
42a76cbd38 | ||
|
|
eb95634801 | ||
|
|
de8bfff47f | ||
|
|
07c18911df | ||
|
|
cc769cc330 | ||
|
|
9794f58936 | ||
|
|
a44fcff3b1 | ||
|
|
7e4a3e88b9 | ||
|
|
8fd45e2205 | ||
|
|
7d235057e8 | ||
|
|
63a8a67771 | ||
|
|
bac39ca176 | ||
|
|
1979047bbc | ||
|
|
8860c1b0f8 | ||
|
|
626b1e8601 | ||
|
|
04086f31b9 | ||
|
|
7037aa9c3b | ||
|
|
f5bab8a698 | ||
|
|
ee44b1b1d8 | ||
|
|
d0f91df95b | ||
|
|
dc1c49da6a | ||
|
|
331040bbdf | ||
|
|
70e1d4b30d | ||
|
|
03dbd797fb | ||
|
|
eb4cb1fbfe | ||
|
|
28172f7f8f | ||
|
|
cb5347cbb8 | ||
|
|
1ce0781d2d | ||
|
|
1257aa76d7 | ||
|
|
6ec3835896 | ||
|
|
506fe7d9bd | ||
|
|
14ef9f6295 | ||
|
|
0d76e1a29e | ||
|
|
9e42d7e8f5 | ||
|
|
364f97332b | ||
|
|
caa7bb8a65 | ||
|
|
3e956b34c1 | ||
|
|
038374e5aa | ||
|
|
dafd54f830 | ||
|
|
cb65e6b0af | ||
|
|
4ca8590c83 | ||
|
|
c5ef02d7e6 | ||
|
|
8175aa8715 | ||
|
|
b426634659 | ||
|
|
e92da1b700 | ||
|
|
7e0a2e88ee | ||
|
|
16afa8c2d4 | ||
|
|
907288455a | ||
|
|
64689e4e3b | ||
|
|
294377846a | ||
|
|
70e4cbaba2 | ||
|
|
9e2f9526fb | ||
|
|
84bec20fa5 | ||
|
|
e9a492fde6 | ||
|
|
76a69f08b4 | ||
|
|
893110d3ef | ||
|
|
70575fa9cf | ||
|
|
bb2450eb3c | ||
|
|
ab56689326 | ||
|
|
a9cf07480a | ||
|
|
c81de2d758 | ||
|
|
aa1c7639f2 | ||
|
|
d448101b32 | ||
|
|
e806f0ede2 | ||
|
|
8253c27333 | ||
|
|
254392370f | ||
|
|
d2319688a5 | ||
|
|
ffebe6c696 | ||
|
|
9e4094f3a9 | ||
|
|
f7eadb08a9 | ||
|
|
d511cfa4cc | ||
|
|
270a3051b7 | ||
|
|
81bc9f9ba6 | ||
|
|
338cec4f18 | ||
|
|
b455e3380f | ||
|
|
92761f2c55 | ||
|
|
7823b8ad2e | ||
|
|
cac82f3124 | ||
|
|
4b892d06e4 | ||
|
|
92149b0f00 | ||
|
|
399aa4c03c | ||
|
|
d5de715032 | ||
|
|
e6fb179184 | ||
|
|
af54f12ec3 | ||
|
|
909cabd517 | ||
|
|
a734a71d2f | ||
|
|
91dc45c880 | ||
|
|
91f364b76a | ||
|
|
23dd088466 | ||
|
|
2cabcc57f4 | ||
|
|
dd6092a5e7 | ||
|
|
3c2861f956 | ||
|
|
894b841769 | ||
|
|
62627c96a3 | ||
|
|
c173ece882 | ||
|
|
eca7cbbbeb | ||
|
|
c72bfa9f71 | ||
|
|
b4ae39a509 | ||
|
|
631c051895 | ||
|
|
9a2e11a2a4 | ||
|
|
aa6011b18d | ||
|
|
9419aec116 | ||
|
|
638402589f | ||
|
|
6d6a6bf738 | ||
|
|
31f6123a55 | ||
|
|
18a3e3092c | ||
|
|
5ded0f8744 | ||
|
|
4e0f0536fa | ||
|
|
3f081a5577 | ||
|
|
2447d663fa |
No files matched your search
@@ -0,0 +1,34 @@
|
||||
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
|
||||
@@ -0,0 +1,34 @@
|
||||
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
|
||||
@@ -16,18 +16,14 @@ jobs:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Get code
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
@@ -51,7 +47,7 @@ jobs:
|
||||
- name: Upload pages artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
path: dist/docs
|
||||
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD Europe 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=eu --latest
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD Europe Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 23 * * *"
|
||||
|
||||
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=eu
|
||||
@@ -12,24 +12,21 @@ concurrency:
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 18
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
||||
| grep "browser_download_url.*dist.zip" \
|
||||
| grep "browser_download_url.*bot.zip" \
|
||||
| cut -d : -f 2,3 \
|
||||
| tr -d \")
|
||||
curl -L -o dist.zip $DOWNLOAD_URL
|
||||
unzip dist.zip
|
||||
curl -L -o bot.zip $DOWNLOAD_URL
|
||||
unzip bot.zip
|
||||
|
||||
- name: Run bot
|
||||
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps --trace-warnings index.cjs
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD North America 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=na --latest
|
||||
@@ -0,0 +1,34 @@
|
||||
name: CD North America Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
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=na
|
||||
@@ -1,21 +0,0 @@
|
||||
name: CD Ratings Leaderboard North America
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Get code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Generate leaderboard
|
||||
run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI Build
|
||||
name: CI Build Bot
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
@@ -11,10 +11,6 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -22,7 +18,7 @@ jobs:
|
||||
- name: Use node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Yarn
|
||||
@@ -38,10 +34,10 @@ jobs:
|
||||
run: yarn
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
run: yarn build:bot
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
name: bot
|
||||
path: dist/bot
|
||||
@@ -0,0 +1,43 @@
|
||||
name: CI Build Docs
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
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: Build
|
||||
run: yarn build:docs
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: dist/docs
|
||||
@@ -0,0 +1,43 @@
|
||||
name: CI Build Website
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
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: Build
|
||||
run: yarn build:website
|
||||
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: website
|
||||
path: dist/website
|
||||
@@ -7,8 +7,10 @@ package-lock.json
|
||||
dist/
|
||||
dist.*
|
||||
yarn-error.log
|
||||
commands.txt
|
||||
/test.*
|
||||
test.*
|
||||
/docs/.vitepress/dist
|
||||
/docs/.vitepress/cache
|
||||
servers.csv
|
||||
servers.csv
|
||||
.next/
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -1,3 +0,0 @@
|
||||
tasks:
|
||||
- init: yarn
|
||||
- command: yarn run pw
|
||||
@@ -6,6 +6,8 @@
|
||||
"blitzstars",
|
||||
"cleandist",
|
||||
"customperiod",
|
||||
"deburr",
|
||||
"diacriticless",
|
||||
"Everytime",
|
||||
"fullstats",
|
||||
"fuzzysort",
|
||||
@@ -28,6 +30,7 @@
|
||||
"statsfull",
|
||||
"Stringifying",
|
||||
"strvk",
|
||||
"subnation",
|
||||
"tankaverages",
|
||||
"tankopedia",
|
||||
"tankstats",
|
||||
@@ -37,7 +40,9 @@
|
||||
"vickers",
|
||||
"vitepress",
|
||||
"wargaming",
|
||||
"winrate"
|
||||
"webp",
|
||||
"winrate",
|
||||
"zustand"
|
||||
],
|
||||
"deno.enable": false,
|
||||
"deno.unstable": false
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
# blitzkrieg
|
||||
# Blitzkrieg
|
||||
|
||||
A Discord bot for anything and everything related to World of Tanks Blitz.
|
||||
[](https://codeium.com)
|
||||
|
||||
## Invite the Bot
|
||||
Tools for everything World of Tanks Blitz
|
||||
|
||||
Join the [Discord server](https://discord.gg/nDt7AjGJQH), click on the bot's profile, and click "Add to Server".
|
||||
## Invite the bot
|
||||
|
||||
## Using
|
||||
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||
|
||||
### Installing dependencies
|
||||

|
||||
|
||||
## Visit the website
|
||||
|
||||
### https://blitz-krieg.vercel.app/
|
||||
|
||||

|
||||
|
||||
## Visit the documentation website
|
||||
|
||||
### https://tresabhi.github.io/blitzkrieg/
|
||||
|
||||

|
||||
|
||||
## Run the bot
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
### Running
|
||||
### Create `.env`
|
||||
|
||||
```env
|
||||
GH_TOKEN={YOUR_GITHUB_TOKEN_HERE}
|
||||
DISCORD_TOKEN={YOUR_DISCORD_TOKE_HERE}
|
||||
```
|
||||
|
||||
### Run the command
|
||||
|
||||
```bash
|
||||
yarn start discord-token=<token> wargaming-application-id=<id>
|
||||
yarn dev:bot
|
||||
```
|
||||
@@ -1,25 +1,39 @@
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { readdirSync } from 'fs';
|
||||
import { parse } from 'path';
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
||||
|
||||
export default defineConfig({
|
||||
title: 'blitzkrieg',
|
||||
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
||||
base: '/blitzkrieg/',
|
||||
outDir: '../dist/docs',
|
||||
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Examples', link: '/markdown-examples' },
|
||||
{ text: 'Guide', link: '/guide/about' },
|
||||
],
|
||||
|
||||
sidebar: getSidebar({
|
||||
collapsed: false,
|
||||
contentDirs: ['guide', 'legal', 'changelogs'],
|
||||
contentRoot: '/docs/',
|
||||
}) as DefaultTheme.Sidebar,
|
||||
sidebar: [
|
||||
...getSidebar({
|
||||
collapsed: false,
|
||||
contentDirs: ['guide', 'legal'],
|
||||
contentRoot: '/docs/',
|
||||
}),
|
||||
|
||||
{
|
||||
text: 'Changelogs',
|
||||
items: readdirSync('./docs/changelogs')
|
||||
.map((file) => parse(file).name)
|
||||
.sort(compareVersions)
|
||||
.map((name) => ({ text: name, link: `/changelogs/${name}.html` })),
|
||||
},
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
|
||||
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
# Blitzkrieg 1.10.2
|
||||
|
||||
The experiment in Blitzkrieg `1.10.1` was a bit of a failure so here are a few changes.
|
||||
|
||||
## Fixes
|
||||
|
||||
- North American leaderboards are now saved in the `na` directory instead of `asia`
|
||||
- Dependencies are now installed before the script is ran
|
||||
- Added leaderboards for all regions
|
||||
- All regions now have their own separate cron timings
|
||||
@@ -0,0 +1,22 @@
|
||||
# Blitzkrieg 1.11.0
|
||||
|
||||
I am going on vacation so I thought I'd publish this a bit prematurely without all the planned features.
|
||||
|
||||
## Changes
|
||||
|
||||
- Added `/ratings`
|
||||
- This command currently only has the `leagues` subcommand group
|
||||
- Displays the top players in a league
|
||||
- Also shows the changes in their positions and scores since last region midnight
|
||||
- Use `/help timezones` to know when Blitzkrieg records the leaderboard
|
||||
- Blitzkrieg now internally generates and keeps a permanent record of all leaderboards updated every `UTC+0` midnight
|
||||
- This approach will allow us to capture the absolute latest state of the leaderboard before it is wiped for the next season
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed region "Europe" in `/verify` example
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Add scripts were upgraded to TypeScript
|
||||
- Secrets now only throw errors if they're used when not provided
|
||||
@@ -0,0 +1,54 @@
|
||||
# Blitzkrieg 1.12.0
|
||||
|
||||
_Houston, permission to return to Earth? A new update dropped._
|
||||
|
||||
## Features
|
||||
|
||||
- Added `multi-tank` filters to `/stats` and `/full-stats`
|
||||
- This lets you pull up the stats by combining filters of `nation`, `tank-type`, `tier`, and `tree-type`
|
||||
- Added `/ratings neighbours`
|
||||
- Lets you see your rank and the neighbours around you
|
||||
- Added a bunch more `/today` parameters (thank you for the suggestions, HisRoyalFatness!)
|
||||
- `cutoff`: the max number of rows to show (excluding the total row)
|
||||
- `maximized`: the max number of rows that show the full non-collapsed stats (default: `4`)
|
||||
- `show-total`: show the total row or not (default: `true`)
|
||||
- Tank search is now accent-tolerant
|
||||
|
||||
## Changes
|
||||
|
||||
- `/ratings` has been moved out of preview and is now accessible to all
|
||||
- `/today` now has a more appealing look with more efficient space management
|
||||
- `/stats`, `/full-stats`, and `/evolution` now have less convoluted commands
|
||||
- All periods now fall under `period` and `custom`
|
||||
- `period` has preset periods like `today`, `career`, `30`, `60`, `90`
|
||||
- `custom` lets you set a start and end day
|
||||
- Added dashes to command names, subcommands, options, etc. to make them more readable
|
||||
- Titlebar
|
||||
- Removed the Blitzkrieg logo
|
||||
- Removed region
|
||||
- Center aligned items
|
||||
- Footers are now center alinged when they do no have footer text on the left
|
||||
- Moved several title discriminators into the description
|
||||
- `/ratings` is now data-driven
|
||||
- Embeds now auto refresh every `30` seconds instead of `60`
|
||||
- Added numbers besides roman numerals in tier choice
|
||||
- I forgot to implement this in places other than `/owned-tanks`... oh well!
|
||||
- Increased precision of winrate in `/evolution` to `2` decimals
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed duplicate "Efficiency" cards in `/full-stats`
|
||||
- Titlebar gap is now `4px` just like Figma
|
||||
- Renamed `/statsfull` to `/full-stats` in the `/stats` footer
|
||||
- Rewards in `/ratings` are now properly aligned
|
||||
- `/ratings` now uses the correct source of information per region instead of getting it from a cached North American source
|
||||
- Search parameters are now passed onto the URL parser correctly
|
||||
- Custom date ranges now work correctly once again as the start is now the maximum days ago from today and the end is the minimum
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- All generated images are now `480px` wide instead of `640px` to combat the unnecessary amounts of negative space
|
||||
- Changed all relevant typings to be sourced from `stats` instead of `full-stats`
|
||||
- Added minimum character limit to clan search
|
||||
- Username auto-completing now only takes in regex-accepted parts of the input
|
||||
- Search parameters are now longer automatically generated by inferring command interactions
|
||||
@@ -0,0 +1,8 @@
|
||||
# Blitzkrieg 1.12.1
|
||||
|
||||
A duo of nasty bugs fixed.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed a tank index and id mismatch
|
||||
- Reverted username pattern infrence in command username inputs
|
||||
@@ -0,0 +1,29 @@
|
||||
# Blitzkrieg 1.12.2
|
||||
|
||||
A much needed room service is here!
|
||||
|
||||
## Changes
|
||||
|
||||
- Tier selection is now easier
|
||||
- Tier options across all commands have been unified to a single style
|
||||
- Tiers are now sorted from `10` to `1`
|
||||
- Tiers now show up in the format `Tier {tier_number} - {tier_roman_numeral}` (example: `Tier 8 - VIII`)
|
||||
- Removed powered by footer as links are provided regardless
|
||||
- Changed invite instructions to instead use application directory
|
||||
|
||||
## Fixes
|
||||
|
||||
- Guide link on the website is now fixed
|
||||
- Versions now show up in the correct order in the navigation bar
|
||||
- Added Blitzkrieg's GitHub link instead of the default Vitepress
|
||||
- Changed examples to guide in the navigation bar
|
||||
- Clan name and icon now show up with multi-tank filters
|
||||
- Removed a random `console.log`
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Added `knip`
|
||||
- Removed a lot of unused files and dependencies
|
||||
- Updated all dependencies to their latest
|
||||
- Code split `bot.ts`
|
||||
- `client` and `octokit` now reside separately
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.12.3
|
||||
|
||||
3-day downtime fixed!
|
||||
|
||||
## Changes
|
||||
|
||||
- Split `/ratings` into `/ratings current` and `/ratings archive`
|
||||
- `current` now properly handles when there is no ongoing ratings season
|
||||
- `archive` displays older archived seasons
|
||||
|
||||
## Fixes
|
||||
|
||||
- The bot is now no ratings seasons tolerant
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.12.4
|
||||
|
||||
Added very badly-coded temporary `/clan-event` command (without linting)
|
||||
|
||||
## Changes
|
||||
|
||||
- Added `/clan-event` command
|
||||
@@ -0,0 +1,12 @@
|
||||
# Blitzkrieg 1.12.5
|
||||
|
||||
Some patch up work
|
||||
|
||||
## Changes
|
||||
|
||||
- `/clan-event` now is more legible and comes with an approximation warning
|
||||
|
||||
## Fixes
|
||||
|
||||
- `/clan-event` now uses a more optimized way of fetching large data
|
||||
- This should fix the issue where too many people using the command broke it
|
||||
@@ -0,0 +1,12 @@
|
||||
# Blitzkrieg 1.12.6
|
||||
|
||||
Stops making the bot lie!
|
||||
|
||||
## Changes
|
||||
|
||||
- The approximation warning for `/clan-event` has been made more concise
|
||||
- Players with `0` wins are now omitted
|
||||
|
||||
## Fixes
|
||||
|
||||
- Players who haven't player within the current day are now omitted
|
||||
@@ -0,0 +1,13 @@
|
||||
# Blitzkrieg 1.12.7
|
||||
|
||||
A few fixes and clarifications
|
||||
|
||||
## Changes
|
||||
|
||||
- `/clan-event`
|
||||
- Made statement about the limited publicly available data
|
||||
- Added a statement about reset times
|
||||
|
||||
## Fixes
|
||||
|
||||
- Commands with images now once again work
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.12.8
|
||||
|
||||
Blitzkrieg is a completely free service and I earn no revenue from it. To keep it sustainable, I have to rely on free services. Unfortunately for embeds, seems like I have exceeded my limits. Before this service starts charging me, I have shut down embeds. They'll hopefully be back in 1.13 alongside the website! Only the future will tell...
|
||||
|
||||
## Changes
|
||||
|
||||
- Removed all embed buttons
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.12.9
|
||||
|
||||
Now that the clan event is over, we don't need `/clan-event`. This command may be reintroduced in the future as a part of a different command/feature and will definitely be re-enabled during the next clan event.
|
||||
|
||||
## Changes
|
||||
|
||||
- Disabled the `/clan-event` command
|
||||
@@ -0,0 +1,32 @@
|
||||
# Blitzkrieg 1.13.0
|
||||
|
||||
One of the biggest updates ever! This changelog's size does not truly represent the work (and brain cells) spent.
|
||||
|
||||
## Changes
|
||||
|
||||
- Added Blitzkrieg homepage
|
||||
- Added session tracker
|
||||
- Search players up and select one
|
||||
- This starts tracking your games from that point
|
||||
- This is displayed in a similar fashion to the `/today` command
|
||||
- Updated the permissions doc
|
||||
- This now includes all 3 of the needed permissions for full operation
|
||||
- Also renamed the permissions to better match Discord
|
||||
- Renamed `/help` to `/about`
|
||||
- Renamed `/help about` to `/about bot`
|
||||
|
||||
## Fixes
|
||||
|
||||
- Added `/ratings` error handling for `null` clans, players, and rewards
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Removed all server assets
|
||||
- Simplified build script to only compile the bot
|
||||
- Upgraded the build script to TypeScript
|
||||
- Moved docs build to `dist/docs`
|
||||
- Split `tsconfig.json` for Next.js and esbuild
|
||||
- Removed Gitpod config
|
||||
- Switched to `.env`
|
||||
- Made Wargaming application ID public
|
||||
- Started migration to Radix UI
|
||||
@@ -0,0 +1,17 @@
|
||||
# Blitzkrieg 1.13.1
|
||||
|
||||
The quickest minor update in town 🤠
|
||||
|
||||
## Changes
|
||||
|
||||
- Added a title, description, and icon to the website
|
||||
- Added display options to the session tracker
|
||||
|
||||
## Fixes
|
||||
|
||||
- Center aligned stats in breakdown to work better on low width devices
|
||||
- Center aligned the Blitzkrieg logo in the navbar
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Added up-to-date information to `README.md` and embed docs
|
||||
@@ -0,0 +1,7 @@
|
||||
# Blitzkrieg 1.13.2
|
||||
|
||||
One lonely albeit important fix.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Gradients for `/stats` now use Radix's new hex values
|
||||
@@ -1,23 +1,23 @@
|
||||
# Embeds
|
||||
|
||||
Embeds are generated by a few commands as a URL that can be loaded in a browser/streaming software.
|
||||
Embeds are URLs generated by commands or tools that represent the "naked" version of the respective command or tool
|
||||
|
||||
## Creating Embeds
|
||||
|
||||
1. Use a command of your liking (for example, `/stats`).
|
||||
2. Customize the command however you like and the URL generated for the embed will match your needs.
|
||||
3. Send the command and when the bot is "done thinking," an "Embed" button will appear below the bot's reply.
|
||||
1. Use a command or tool and click on "Embed" if it's available
|
||||
2. Make sure to tweak the settings of the command or tool because embeds replicate everything
|
||||
|
||||
## Using Embeds in Streaming Softwares
|
||||
|
||||
1. Click on the "Embed" button.
|
||||
2. Copy the URL from your browser.
|
||||
3. Paste the URL into a Browser Source.
|
||||
4. Set the refresh rate/FPS to something very low like `0.0167` (that's 1 frame every 60 seconds).
|
||||
1. Click on the "Embed" button
|
||||
- If this was generated by a command, copy the URL from the browser
|
||||
- If this was generated by a tool, the URL has been copied to your clipboard already
|
||||
2. Paste this URL into your streaming service as a "Browser Source"
|
||||
- Tool embeds use local storage so data won't be transferred from you browser to your streaming software
|
||||
- Make sure to visit the tool's page in your streaming software (using the "Interact" feature) and then open the embed
|
||||
- Set your browser source refresh rate to `1Hz` because that's how fast embeds refresh
|
||||
|
||||
## How Embeds Behave
|
||||
|
||||
- They load like any normal website albeit with a significant delay.
|
||||
- They auto refresh every `60s`.
|
||||
- All embeds and images generated by Blitzkrieg have a width of `640px` but the height varies and must be found manually through trial and error.
|
||||
- It's okay to have a Browser Source that has a height larger than needed as Blitzkrieg embeds are transparent.
|
||||
- They load like any normal website
|
||||
- They auto refresh every `1s`
|
||||
@@ -1,4 +1,4 @@
|
||||
# About
|
||||
# Introduction
|
||||
|
||||
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
# Inviting the Bot
|
||||
|
||||
It's quite simple really. Just follow the steps below:
|
||||
|
||||
1. Join a Discord server that already has Blitzkrieg installed or [the official Discord Server](https://discord.gg/nDt7AjGJQH).
|
||||
2. Click on the bot's profile.
|
||||
3. Click the "Add to Server" button under the bot's name and ID.
|
||||
4. Select what server you want it in and see what permissions you seem fit for the bot.
|
||||
|
||||
Voila! You're ready to go.
|
||||
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
The following Discord permissions are required by the bot to ensure full functionality:
|
||||
|
||||
- Manage nicknames
|
||||
- Read messages / view channels
|
||||
- Attach files
|
||||
- View channels
|
||||
@@ -15,7 +15,7 @@ hero:
|
||||
link: guide/install
|
||||
- theme: alt
|
||||
text: Guide
|
||||
link: /guide
|
||||
link: /guide/about
|
||||
|
||||
features:
|
||||
- title: Clean and Efficient UI
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
|
||||
|
||||
const withVanillaExtract = createVanillaExtractPlugin();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
...withVanillaExtract(),
|
||||
|
||||
distDir: 'dist/website',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"main": "server.cjs",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
||||
}
|
||||
|
||||
@@ -1,53 +1,67 @@
|
||||
{
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.10.0",
|
||||
"description": "",
|
||||
"version": "1.13.2",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:bot": "node scripts/build --build=bot",
|
||||
"build:server": "node scripts/build --build=server",
|
||||
"build:bot": "vite-node --script scripts/build",
|
||||
"build:website": "next build",
|
||||
"build:docs": "vitepress build docs",
|
||||
"build:ratingsleaderboard": "node scripts/generateRatingsLeaderboard",
|
||||
"build": "node scripts/build --build=all",
|
||||
"dev:bot": "yarn build:bot --dev && yarn start:bot",
|
||||
"dev:server": "yarn build:server --dev && yarn start:server",
|
||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||
"dev:docs": "vitepress dev docs",
|
||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
|
||||
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
|
||||
"dev:website": "next dev",
|
||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs",
|
||||
"start:website": "next start",
|
||||
"host:docs": "vitepress preview docs",
|
||||
"publish:ratingsleaderboard": "node scripts/generateRatingsLeaderboard --publish",
|
||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||
"zip": "zip -r dist.zip dist",
|
||||
"lint": "npx tsc",
|
||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\""
|
||||
"lint:website": "next lint",
|
||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
||||
"knip": "knip",
|
||||
"update": "yarn upgrade --latest"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@octokit/rest": "^20.0.1",
|
||||
"@radix-ui/colors": "^3.0.0-rc.4",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/themes": "^1.1.2",
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"bepaint": "^0.1.0-beta.3",
|
||||
"discord.js": "^14.9.0",
|
||||
"esbuild": "^0.18.10",
|
||||
"escape-html": "^1.0.3",
|
||||
"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",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-escape": "^2.0.0",
|
||||
"next": "^13.5.2",
|
||||
"react": "^18.2.0",
|
||||
"satori": "^0.9.1",
|
||||
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fuzzysort": "^1.0.1",
|
||||
"satori": "^0.10.8",
|
||||
"vite-node": "^0.34.5",
|
||||
"vitepress": "^1.0.0-rc.20",
|
||||
"vitepress-plugin-auto-sidebar": "^1.1.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^20.0.1",
|
||||
"@total-typescript/ts-reset": "^0.4.2",
|
||||
"@total-typescript/ts-reset": "^0.5.1",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/markdown-escape": "^1.1.0",
|
||||
"@types/node": "^20.2.1",
|
||||
"@types/react": "^18.2.7",
|
||||
"prettier": "^3.0.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vitepress": "^1.0.0-beta.3"
|
||||
"@types/node": "^20.6.5",
|
||||
"@types/react": "^18.2.22",
|
||||
"@vanilla-extract/next-plugin": "^2.3.1",
|
||||
"compare-versions": "^6.1.0",
|
||||
"knip": "^2.27.1",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 8.2 MiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 177 KiB |
@@ -1,64 +0,0 @@
|
||||
import { build } from 'esbuild';
|
||||
import { copyFile, mkdir, rm } from 'fs/promises';
|
||||
import { argv } from 'process';
|
||||
|
||||
const isProd = !argv.includes('--dev');
|
||||
const buildAll = argv.includes('--build=all');
|
||||
const buildBot = argv.includes('--build=bot') || buildAll;
|
||||
const buildServer = argv.includes('--build=server') || buildAll;
|
||||
|
||||
if (isProd) {
|
||||
// only remove in production to ensure no time is wasted in dev
|
||||
console.log('Removing dist...');
|
||||
await rm('dist', { recursive: true, force: true });
|
||||
console.log('Dist removed');
|
||||
|
||||
console.log('Creating dist...');
|
||||
await mkdir('dist');
|
||||
console.log('Dist created');
|
||||
|
||||
console.log('Copying package.json...');
|
||||
copyFile('package.dist.json', 'dist/package.json').then(() =>
|
||||
console.log('package.json copied'),
|
||||
);
|
||||
}
|
||||
|
||||
const commonOptions = {
|
||||
platform: 'node',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // causes errors
|
||||
minifySyntax: isProd,
|
||||
minifyWhitespace: isProd,
|
||||
|
||||
logLevel: isProd ? 'info' : 'silent',
|
||||
};
|
||||
|
||||
if (buildBot) {
|
||||
console.log('Building bot...');
|
||||
build({
|
||||
...commonOptions,
|
||||
|
||||
entryPoints: ['src/bot.ts'],
|
||||
outfile: 'dist/bot.cjs',
|
||||
}).then(() => {
|
||||
console.log('Bot built');
|
||||
});
|
||||
}
|
||||
|
||||
if (buildServer) {
|
||||
console.log('Building server...');
|
||||
build({
|
||||
...commonOptions,
|
||||
|
||||
entryPoints: ['src/server.ts'],
|
||||
outfile: 'dist/server.cjs',
|
||||
}).then(() => {
|
||||
console.log('Server built');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { build } from 'esbuild';
|
||||
|
||||
console.log('Building bot...');
|
||||
|
||||
await build({
|
||||
entryPoints: ['src/bot.ts'],
|
||||
outfile: 'dist/bot/index.cjs',
|
||||
|
||||
platform: 'node',
|
||||
tsconfig: 'tsconfig.esbuild.json',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
format: 'cjs',
|
||||
jsx: 'transform',
|
||||
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minifyIdentifiers: false, // cause goofy errors
|
||||
minifySyntax: true,
|
||||
minifyWhitespace: true,
|
||||
|
||||
logLevel: 'info',
|
||||
});
|
||||
|
||||
console.log('Built bot');
|
||||
@@ -1,15 +1,27 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { rm, writeFile } from 'fs/promises';
|
||||
import { argv, env } from 'process';
|
||||
import {
|
||||
RatingsInfo,
|
||||
RatingsNeighbors,
|
||||
RatingsPlayer,
|
||||
} from '../src/commands/ratings';
|
||||
import commitMultipleFiles from './commitMultipleFiles.js';
|
||||
|
||||
/*
|
||||
For Central North American Time (UTC-5), use 0 5 * * *.
|
||||
For Central European Time (UTC+1), use 0 23 * * *.
|
||||
For Central Asia Standard Time (UTC+7), use 0 17 * * *.
|
||||
*/
|
||||
* Central North American Time (UTC-5): use 0 5 * * *.
|
||||
* 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
|
||||
*/
|
||||
|
||||
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
|
||||
|
||||
const publish = argv.includes('--publish');
|
||||
const latest = argv.includes('--latest');
|
||||
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
||||
|
||||
if (!server) throw new Error('Region parameter not specified');
|
||||
@@ -18,22 +30,26 @@ const NEIGHBORS = 1000;
|
||||
const PLAYERS = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => data.count);
|
||||
.then((response) => response.json() as Promise<RatingsInfo>)
|
||||
.then((data) => (data.detail === undefined ? data.count : undefined));
|
||||
|
||||
const players = {};
|
||||
const leaderboard = [];
|
||||
if (PLAYERS === undefined) {
|
||||
throw new Error('No current ratings season running');
|
||||
}
|
||||
|
||||
const players: Record<number, RatingsPlayer> = {};
|
||||
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
||||
let registered = 0;
|
||||
|
||||
async function branchFromPlayer(id) {
|
||||
async function branchFromPlayer(id: number) {
|
||||
const { neighbors } = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||
).then((response) => response.json());
|
||||
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||
|
||||
const firstPlayer = neighbors[0];
|
||||
const lastPlayer = neighbors[neighbors.length - 1];
|
||||
|
||||
const branches = [];
|
||||
const branches: Promise<void>[] = [];
|
||||
if (!players[firstPlayer.number])
|
||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||
if (!players[lastPlayer.number])
|
||||
@@ -48,7 +64,7 @@ async function branchFromPlayer(id) {
|
||||
|
||||
console.log(
|
||||
`Registered ${registered} / ${PLAYERS} (${(
|
||||
(registered / PLAYERS) *
|
||||
(registered / PLAYERS!) *
|
||||
100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
@@ -56,10 +72,10 @@ async function branchFromPlayer(id) {
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
async function branchFromLeague(id) {
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
).then((response) => response.json());
|
||||
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result[result.length - 1];
|
||||
@@ -70,7 +86,7 @@ async function branchFromLeague(id) {
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Branching form leagues...');
|
||||
console.log('Branching from leagues...');
|
||||
await Promise.all([
|
||||
branchFromLeague(0),
|
||||
branchFromLeague(1),
|
||||
@@ -81,8 +97,13 @@ await Promise.all([
|
||||
|
||||
console.log('Converting to an array...');
|
||||
for (let index = 0; index < PLAYERS; index++) {
|
||||
leaderboard[index] = players[index + 1];
|
||||
if (!leaderboard[index]) console.warn(`${index} had no player`);
|
||||
const listing = players[index + 1];
|
||||
if (!listing) console.warn(`Position ${index + 1} had no player`);
|
||||
|
||||
leaderboard[index] = {
|
||||
id: listing.spa_id,
|
||||
score: listing.score,
|
||||
};
|
||||
}
|
||||
|
||||
const leaderboardJSON = JSON.stringify(leaderboard);
|
||||
@@ -92,12 +113,16 @@ if (publish) {
|
||||
|
||||
const info = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).then((response) => response.json());
|
||||
const time = Math.round(Date.now() / 1000);
|
||||
).then(
|
||||
(response) =>
|
||||
response.json() as Promise<RatingsInfo & { detail: undefined }>,
|
||||
);
|
||||
const normalizedServer = server === 'na' ? 'com' : server;
|
||||
|
||||
const infoPath = `${normalizedServer}/ratings/seasons/${info.current_season}.json`;
|
||||
const leaderboardPath = `${normalizedServer}/ratings/leaderboards/${info.current_season}/${time}.json`;
|
||||
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
|
||||
latest ? 'latest' : 'midnight'
|
||||
}.json`;
|
||||
const infoJSON = JSON.stringify(info);
|
||||
|
||||
console.log(`Publishing to season ${info.current_season}...`);
|
||||
@@ -106,7 +131,7 @@ if (publish) {
|
||||
'tresabhi',
|
||||
'blitzkrieg-db',
|
||||
'main',
|
||||
`${info.current_season}/${time}`,
|
||||
new Date().toString(),
|
||||
[
|
||||
{ path: infoPath, content: infoJSON },
|
||||
{ path: leaderboardPath, content: leaderboardJSON },
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
export default async function commitMultipleFiles(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
message,
|
||||
changes,
|
||||
octokit: Octokit,
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
message: string,
|
||||
changes: { path: string; content: string }[],
|
||||
) {
|
||||
// Get the latest commit SHA on the specified branch
|
||||
const { data: refData } = await octokit.git.getRef({
|
||||
@@ -31,12 +33,13 @@ export default async function commitMultipleFiles(
|
||||
content: change.content,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
return {
|
||||
sha: blobData.sha,
|
||||
path: change.path,
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
};
|
||||
} as const;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(await tankAverages);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { tankopedia } from '../../../core/blitz/tankopedia';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(await tankopedia);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as Breakdown from '../../../components/Breakdown';
|
||||
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import calculateWN8 from '../../../core/blitz/calculateWN8';
|
||||
import { diffNormalizedTankStats } from '../../../core/blitz/diffNormalizedTankStats';
|
||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
||||
import getWargamingResponse from '../../../core/blitz/getWargamingResponse';
|
||||
import { TankopediaEntry, tankopedia } from '../../../core/blitz/tankopedia';
|
||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||
import { useSession } from '../../../stores/session';
|
||||
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
||||
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
||||
|
||||
const REFRESH_RATE = 1 / 5;
|
||||
|
||||
export default function SessionPage() {
|
||||
const session = useSession();
|
||||
const since = session.isTracking ? new Date(session.time) : undefined;
|
||||
const [diff, setDiff] = useState<
|
||||
| {
|
||||
careerWN8: number;
|
||||
|
||||
list: {
|
||||
stats: IndividualTankStats;
|
||||
tankopedia?: TankopediaEntry;
|
||||
career: IndividualTankStats;
|
||||
currentWN8?: number;
|
||||
careerWN8?: number;
|
||||
}[];
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
const sessionBattles = diff?.list.reduce(
|
||||
(accumulator, { stats }) => accumulator + stats.all.battles,
|
||||
0,
|
||||
);
|
||||
const tankStatsArray = session.isTracking
|
||||
? (Object.values(session.tankStats) as IndividualTankStats[])
|
||||
: [];
|
||||
const sessionWinrate = diff
|
||||
? diff.list.reduce(
|
||||
(accumulator, { stats }) => accumulator + stats.all.wins,
|
||||
0,
|
||||
) / sessionBattles!
|
||||
: 0;
|
||||
const careerBattles = tankStatsArray.reduce(
|
||||
(accumulator, stats) => accumulator + stats.all.battles,
|
||||
0,
|
||||
);
|
||||
const careerWinrate =
|
||||
tankStatsArray.reduce(
|
||||
(accumulator, stats) => accumulator + stats.all.wins,
|
||||
0,
|
||||
) / careerBattles;
|
||||
const currentDamage = diff
|
||||
? diff.list.reduce(
|
||||
(accumulator, { stats }) => accumulator + stats.all.damage_dealt,
|
||||
0,
|
||||
) / sessionBattles!
|
||||
: 0;
|
||||
const careerDamage =
|
||||
tankStatsArray.reduce(
|
||||
(accumulator, stats) => accumulator + stats.all.damage_dealt,
|
||||
0,
|
||||
) / careerBattles;
|
||||
|
||||
useEffect(() => {
|
||||
async function recalculateDiff() {
|
||||
if (session.isTracking) {
|
||||
const { id, region } = session;
|
||||
const careerRaw = await getWargamingResponse<TanksStats>(
|
||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const career = careerRaw[id].reduce<
|
||||
Record<number, IndividualTankStats>
|
||||
>(
|
||||
(accumulator, curr) => ({
|
||||
...accumulator,
|
||||
[curr.tank_id]: curr,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const awaitedTankopedia = await tankopedia;
|
||||
const awaitedTankAverages = await tankAverages;
|
||||
const careerWN8 =
|
||||
careerRaw[id].reduce(
|
||||
(accumulator, { tank_id, all }) =>
|
||||
accumulator +
|
||||
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||
? all.battles *
|
||||
calculateWN8(awaitedTankAverages[tank_id].all, all)
|
||||
: 0),
|
||||
0,
|
||||
) /
|
||||
careerRaw[id].reduce(
|
||||
(accumulator, { tank_id, all }) =>
|
||||
accumulator +
|
||||
(awaitedTankAverages[tank_id] && all.battles > 0
|
||||
? all.battles
|
||||
: 0),
|
||||
0,
|
||||
);
|
||||
|
||||
setDiff({
|
||||
careerWN8,
|
||||
|
||||
list: (
|
||||
Object.values(
|
||||
diffNormalizedTankStats(session.tankStats, career),
|
||||
) as IndividualTankStats[]
|
||||
).map((stats) => {
|
||||
return {
|
||||
stats,
|
||||
tankopedia: awaitedTankopedia[stats.tank_id],
|
||||
career: career[stats.tank_id],
|
||||
currentWN8: awaitedTankAverages[stats.tank_id]
|
||||
? calculateWN8(
|
||||
awaitedTankAverages[stats.tank_id].all,
|
||||
stats.all,
|
||||
)
|
||||
: undefined,
|
||||
careerWN8: awaitedTankAverages[stats.tank_id]
|
||||
? calculateWN8(
|
||||
awaitedTankAverages[stats.tank_id].all,
|
||||
career[stats.tank_id].all,
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
recalculateDiff();
|
||||
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [session]);
|
||||
|
||||
const totalRowStats: Record<
|
||||
CustomColumnDisplay,
|
||||
Breakdown.RowStatItem | undefined
|
||||
> = {
|
||||
battles: {
|
||||
title: 'Battles',
|
||||
current: sessionBattles?.toLocaleString(),
|
||||
career: tankStatsArray
|
||||
.reduce((accumulator, stats) => accumulator + stats.all.battles, 0)
|
||||
.toLocaleString(),
|
||||
},
|
||||
winrate: {
|
||||
title: 'Winrate',
|
||||
current: sessionBattles ? `${(100 * sessionWinrate).toFixed(2)}%` : '--',
|
||||
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
|
||||
},
|
||||
wn8: (() => {
|
||||
if (!diff) return undefined;
|
||||
|
||||
const battlesWithWN8 = diff.list.reduce(
|
||||
(accumulator, { currentWN8, stats }) =>
|
||||
(currentWN8 === undefined ? 0 : stats.all.battles) + accumulator,
|
||||
0,
|
||||
);
|
||||
const currentWN8 =
|
||||
diff.list.reduce(
|
||||
(accumulator, { currentWN8, stats }) =>
|
||||
stats.all.battles * (currentWN8 ?? 0) + accumulator,
|
||||
0,
|
||||
) / battlesWithWN8!;
|
||||
|
||||
return {
|
||||
title: 'WN8',
|
||||
current: battlesWithWN8
|
||||
? Math.round(currentWN8!).toLocaleString()
|
||||
: '--',
|
||||
percentile: battlesWithWN8 ? getWN8Percentile(currentWN8!) : undefined,
|
||||
career: Math.round(diff.careerWN8).toLocaleString(),
|
||||
};
|
||||
})(),
|
||||
damage: {
|
||||
title: 'Damage',
|
||||
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
|
||||
current: sessionBattles
|
||||
? Math.round(currentDamage).toLocaleString()
|
||||
: '--',
|
||||
career: Math.round(careerDamage).toLocaleString(),
|
||||
},
|
||||
none: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Breakdown.Root>
|
||||
{session.showTotal &&
|
||||
session.isTracking &&
|
||||
since !== undefined &&
|
||||
diff !== undefined && (
|
||||
<Breakdown.Row
|
||||
minimized={!session.showCareer}
|
||||
title={`Since ${since.toLocaleString(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
day: 'numeric',
|
||||
})}`}
|
||||
stats={session.customColumns.map((customColumn) => {
|
||||
const rowStat = totalRowStats[customColumn.display];
|
||||
|
||||
if (!rowStat) return undefined;
|
||||
|
||||
return {
|
||||
...rowStat,
|
||||
delta:
|
||||
customColumn.showDelta && rowStat.percentile === undefined
|
||||
? rowStat.delta
|
||||
: undefined,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
{diff?.list
|
||||
.sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time)
|
||||
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
|
||||
const rowStats: Record<
|
||||
CustomColumnDisplay,
|
||||
Breakdown.RowStatItem | undefined
|
||||
> = {
|
||||
battles: {
|
||||
title: 'Battles',
|
||||
current: stats.all.battles.toLocaleString(),
|
||||
career: career.all.battles.toLocaleString(),
|
||||
},
|
||||
winrate: {
|
||||
title: 'Winrate',
|
||||
current: `${(100 * (stats.all.wins / stats.all.battles)).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
career: `${(100 * (career.all.wins / career.all.battles)).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
delta:
|
||||
stats.all.wins / stats.all.battles -
|
||||
career.all.wins / career.all.battles,
|
||||
},
|
||||
wn8: {
|
||||
title: 'WN8',
|
||||
current:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(currentWN8).toLocaleString(),
|
||||
career:
|
||||
careerWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(careerWN8).toLocaleString(),
|
||||
percentile:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: getWN8Percentile(currentWN8),
|
||||
},
|
||||
damage: {
|
||||
title: 'Damage',
|
||||
current: Math.round(
|
||||
stats.all.damage_dealt / stats.all.battles,
|
||||
).toLocaleString(),
|
||||
career: Math.round(
|
||||
career.all.damage_dealt / career.all.battles,
|
||||
).toLocaleString(),
|
||||
delta:
|
||||
stats.all.damage_dealt / stats.all.battles -
|
||||
career.all.damage_dealt / career.all.battles,
|
||||
},
|
||||
none: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Breakdown.Row
|
||||
key={stats.tank_id}
|
||||
minimized={!session.showCareer}
|
||||
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
|
||||
type="tank"
|
||||
tankType={tankopedia?.type}
|
||||
treeType={(() => {
|
||||
if (tankopedia?.is_collectible) return TreeTypeEnum.Collector;
|
||||
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
|
||||
})()}
|
||||
stats={session.customColumns.map((customColumn) => {
|
||||
const rowStat = rowStats[customColumn.display];
|
||||
|
||||
if (!rowStat) return undefined;
|
||||
|
||||
return {
|
||||
...rowStat,
|
||||
delta:
|
||||
customColumn.showDelta && rowStat.percentile === undefined
|
||||
? rowStat.delta
|
||||
: undefined,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Breakdown.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { Flex, Theme } from '@radix-ui/themes';
|
||||
import '@radix-ui/themes/styles.css';
|
||||
import { config } from 'dotenv';
|
||||
import { Roboto_Flex } from 'next/font/google';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import Navbar from '../components/Navbar';
|
||||
|
||||
config();
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const robotoFlex = Roboto_Flex({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const isEmbed = pathname.split('/')[1] === 'embeds';
|
||||
|
||||
return (
|
||||
<html lang="en" className={robotoFlex.className}>
|
||||
<head>
|
||||
<title>Blitzkrieg</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="🎉 Tools for everything World of Tanks Blitz"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body
|
||||
style={{
|
||||
margin: 0,
|
||||
background: isEmbed
|
||||
? 'transparent'
|
||||
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||
}}
|
||||
>
|
||||
<Theme
|
||||
appearance="dark"
|
||||
panelBackground="translucent"
|
||||
radius="full"
|
||||
accentColor="blue"
|
||||
>
|
||||
{!isEmbed && <Navbar />}
|
||||
|
||||
<Flex direction="column">{children}</Flex>
|
||||
</Theme>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { styleVariants } from '@vanilla-extract/css';
|
||||
import { theme } from '../stitches.config';
|
||||
|
||||
export const tool = styleVariants({
|
||||
enabled: {
|
||||
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||
|
||||
':hover': {
|
||||
boxShadow: `inset 0 -160px 128px -128px ${theme.colors.appBackground1}`,
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Text, TextField } from '@radix-ui/themes';
|
||||
import Link from 'next/link';
|
||||
import { useRef } from 'react';
|
||||
import { Search, SearchItem } from 'react-fuzzysort';
|
||||
import PageWrapper from '../components/PageWrapper';
|
||||
import { TOOLS } from '../constants/tools';
|
||||
import { theme } from '../stitches.config';
|
||||
import * as styles from './page.css';
|
||||
|
||||
export default function Page() {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input ref={input} placeholder="Search tools..." />
|
||||
</TextField.Root>
|
||||
|
||||
<Flex gap="3" wrap="wrap" align="center" justify="center">
|
||||
<Search
|
||||
input={input}
|
||||
list={TOOLS.map(
|
||||
(tool) =>
|
||||
({
|
||||
node: (
|
||||
<Link
|
||||
href={`/tools/${tool.id}`}
|
||||
className={
|
||||
tool.disabled ? styles.tool.disabled : styles.tool.enabled
|
||||
}
|
||||
style={{
|
||||
backgroundImage: `url(/assets/banners/${tool.id}.webp)`,
|
||||
cursor: tool.disabled ? 'default' : 'pointer',
|
||||
opacity: tool.disabled ? 0.25 : 1,
|
||||
height: 128,
|
||||
minWidth: 256,
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
textDecoration: 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-end',
|
||||
padding: 16,
|
||||
gap: 4,
|
||||
transition: `box-shadow ${theme.durations.regular}`,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
if (tool.disabled) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 32,
|
||||
color: theme.colors.textHighContrast,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{tool.title}
|
||||
<CaretRightIcon style={{ width: '1em', height: '1em' }} />
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{tool.disabled ? 'Coming soon!' : tool.description}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
query: `${tool.title} ${tool.description}`,
|
||||
}) satisfies SearchItem,
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Text size="1" color="gray" align="center">
|
||||
Images courtesy of Wargaming Art Department
|
||||
</Text>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import { TOOLS } from '../../constants/tools';
|
||||
import { theme } from '../../stitches.config';
|
||||
|
||||
interface ToolsLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const toolId = pathname.split('/').at(-1);
|
||||
const tool = TOOLS.find((tool) => tool.id === toolId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
height: 128,
|
||||
backgroundImage: `url(/assets/banners/${toolId}.png)`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontSize: 32,
|
||||
}}
|
||||
>
|
||||
{tool?.title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{tool?.description}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { DropdownMenu } from '@radix-ui/themes';
|
||||
import mutateSession, { useSession } from '../../../../stores/session';
|
||||
|
||||
export type CustomColumnDisplay =
|
||||
| 'battles'
|
||||
| 'winrate'
|
||||
| 'wn8'
|
||||
| 'damage'
|
||||
| 'none';
|
||||
|
||||
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
|
||||
|
||||
interface CustomColumn {
|
||||
column: number;
|
||||
}
|
||||
|
||||
export function CustomColumn({ column }: CustomColumn) {
|
||||
const customColumn = useSession((state) => state.customColumns[column]);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>Column {column + 1}</DropdownMenu.SubTrigger>
|
||||
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Label>Display</DropdownMenu.Label>
|
||||
|
||||
<DropdownMenu.RadioGroup
|
||||
value={customColumn.display}
|
||||
onValueChange={(event) =>
|
||||
mutateSession((draft) => {
|
||||
draft.customColumns[column].display =
|
||||
event as CustomColumnDisplay;
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="battles">
|
||||
Battles
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="winrate">
|
||||
Winrate
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="wn8">WN8</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="damage">Damage</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="none">None</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
|
||||
{!NO_DELTA.includes(customColumn.display) && (
|
||||
<>
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={customColumn.showDelta}
|
||||
onCheckedChange={(checked) =>
|
||||
mutateSession((draft) => {
|
||||
draft.customColumns[column].showDelta = checked;
|
||||
})
|
||||
}
|
||||
>
|
||||
Show delta
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export const toolBar = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 640px)': {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
},
|
||||
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
});
|
||||
|
||||
export const toolbarButton = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 640px)': {
|
||||
flex: 0,
|
||||
},
|
||||
},
|
||||
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const searchButton = style({
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
fontSize: 16,
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: theme.colors.componentInteractiveHover,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,266 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CopyIcon,
|
||||
PersonIcon,
|
||||
ReloadIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import {
|
||||
AlertDialog,
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
Text,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash';
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { REGION_NAMES, Region } from '../../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../../../core/blitz/getWargamingResponse';
|
||||
import listPlayers, {
|
||||
AccountListWithServer,
|
||||
} from '../../../core/blitz/listPlayers';
|
||||
import { useSession } from '../../../stores/session';
|
||||
import { NormalizedTankStats, TanksStats } from '../../../types/tanksStats';
|
||||
import SessionPage from '../../embeds/session/page';
|
||||
import { CustomColumn } from './components/CustomColumn';
|
||||
import * as styles from './page.css';
|
||||
|
||||
export default function Page() {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
AccountListWithServer | undefined
|
||||
>(undefined);
|
||||
const [showSearchResults, setShowSearchResults] = useState(false);
|
||||
const handleChange = debounce(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.value) {
|
||||
setSearchResults(await listPlayers(event.target.value));
|
||||
} else {
|
||||
setSearchResults(undefined);
|
||||
setShowSearchResults(false);
|
||||
}
|
||||
},
|
||||
500,
|
||||
);
|
||||
const session = useSession();
|
||||
|
||||
async function setSession(region: Region, id: number, nickname: string) {
|
||||
input.current!.value = nickname;
|
||||
|
||||
const rawTankStats = (
|
||||
await getWargamingResponse<TanksStats>(
|
||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
)
|
||||
)[id];
|
||||
const tankStats = rawTankStats.reduce<NormalizedTankStats>(
|
||||
(accumulator, tank) => ({
|
||||
...accumulator,
|
||||
[tank.tank_id]: tank,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
useSession.setState({
|
||||
isTracking: true,
|
||||
id,
|
||||
region,
|
||||
nickname,
|
||||
tankStats,
|
||||
time: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
const session = useSession.getState();
|
||||
if (!session.isTracking) return;
|
||||
setSession(session.region, session.id, session.nickname);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<div className={styles.toolBar}>
|
||||
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<PersonIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
|
||||
<TextField.Input
|
||||
defaultValue={session.isTracking ? session.nickname : undefined}
|
||||
ref={input}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.target.value) {
|
||||
setShowSearchResults(true);
|
||||
setSearchResults(undefined);
|
||||
} else {
|
||||
setShowSearchResults(false);
|
||||
}
|
||||
|
||||
handleChange(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 is fixed
|
||||
if (showSearchResults) event.target.focus();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setShowSearchResults(false);
|
||||
input.current?.blur();
|
||||
}
|
||||
}}
|
||||
placeholder="Search for a player..."
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<DropdownMenu.Root open={showSearchResults} modal={false}>
|
||||
<DropdownMenu.Trigger>
|
||||
<div />
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{searchResults === undefined ? (
|
||||
<DropdownMenu.Item disabled>Searching...</DropdownMenu.Item>
|
||||
) : searchResults.length === 0 ? (
|
||||
<DropdownMenu.Item disabled>No results</DropdownMenu.Item>
|
||||
) : (
|
||||
searchResults?.map(
|
||||
({ account_id: id, nickname, region }, index) => (
|
||||
<DropdownMenu.Item
|
||||
key={id}
|
||||
onClick={() => {
|
||||
setShowSearchResults(false);
|
||||
setSession(region, id, nickname);
|
||||
}}
|
||||
shortcut={REGION_NAMES[region]}
|
||||
>
|
||||
{nickname}
|
||||
</DropdownMenu.Item>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger>
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={(event) => {
|
||||
if (!session.promptBeforeReset) {
|
||||
event.preventDefault();
|
||||
reset();
|
||||
}
|
||||
}}
|
||||
className={styles.toolbarButton}
|
||||
color="red"
|
||||
>
|
||||
<ReloadIcon width="16" height="16" /> Reset
|
||||
</Button>
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Content>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex direction="column" gap="0">
|
||||
<AlertDialog.Title>
|
||||
Are you sure you want to reset?
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This'll discard your current session and start tracking from
|
||||
scratch again.
|
||||
</AlertDialog.Description>
|
||||
</Flex>
|
||||
|
||||
<Text size="2">
|
||||
<Checkbox
|
||||
mr="1"
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({
|
||||
promptBeforeReset: !checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
Don't ask me again
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" mt="4">
|
||||
<AlertDialog.Cancel>
|
||||
<Button variant="soft" color="gray">
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialog.Cancel>
|
||||
|
||||
<AlertDialog.Action>
|
||||
<Button variant="solid" color="red" onClick={reset}>
|
||||
Reset
|
||||
</Button>
|
||||
</AlertDialog.Action>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<Tooltip content="Copy to clipboard">
|
||||
<Button
|
||||
variant="soft"
|
||||
className={styles.toolbarButton}
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`${location.origin}/embeds/session`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<CopyIcon width="16" height="16" /> Embed
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="soft" className={styles.toolbarButton}>
|
||||
Options
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
<CustomColumn column={0} />
|
||||
<CustomColumn column={1} />
|
||||
<CustomColumn column={2} />
|
||||
<CustomColumn column={3} />
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={session.showTotal}
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({ showTotal: checked })
|
||||
}
|
||||
>
|
||||
Show total
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={session.showCareer}
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({ showCareer: checked })
|
||||
}
|
||||
>
|
||||
Show career
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SessionPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,6 @@
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { DISCORD_TOKEN } from './core/node/arguments';
|
||||
import { client } from './core/discord/client';
|
||||
import { secrets } from './core/node/secrets';
|
||||
import { registerErrorHandlers } from './events/error';
|
||||
import guildMemberAdd from './events/guildMemberAdd';
|
||||
import interactionCreate from './events/interactionCreate';
|
||||
import ready from './events/ready';
|
||||
|
||||
export const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
|
||||
})
|
||||
.on('ready', ready)
|
||||
.on('guildMemberAdd', guildMemberAdd)
|
||||
.on('interactionCreate', interactionCreate);
|
||||
|
||||
registerErrorHandlers();
|
||||
client.login(DISCORD_TOKEN);
|
||||
client.login(secrets.DISCORD_TOKEN);
|
||||
@@ -8,14 +8,14 @@ const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/doc
|
||||
const DOCS = {
|
||||
permissions: 'guide/permissions',
|
||||
embeds: 'guide/embeds',
|
||||
about: 'guide/about',
|
||||
introduction: 'guide/introduction',
|
||||
invite: 'guide/invite',
|
||||
timezones: 'guide/timezones',
|
||||
};
|
||||
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||
permissions: 'The permissions needed for the bot to function',
|
||||
embeds: 'How embeds work',
|
||||
about: 'About the bot',
|
||||
introduction: 'About the bot',
|
||||
invite: 'How to invite the bot',
|
||||
timezones: 'How Blitzkrieg infers and uses your timezone',
|
||||
};
|
||||
@@ -28,15 +28,14 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
||||
return option;
|
||||
}
|
||||
|
||||
export const helpCommand: CommandRegistry = {
|
||||
export const aboutCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: addDocsSubcommands(
|
||||
new SlashCommandBuilder()
|
||||
.setName('help')
|
||||
.setDescription('All the help you need about the bot')
|
||||
.setName('about')
|
||||
.setDescription('All the info you need about the bot')
|
||||
.addSubcommand((option) =>
|
||||
option
|
||||
.setName('commands')
|
||||
@@ -44,13 +43,14 @@ export const helpCommand: CommandRegistry = {
|
||||
),
|
||||
),
|
||||
|
||||
handler(interaction) {
|
||||
async handler(interaction) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
if (subcommand === 'commands') {
|
||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${COMMANDS_RAW.filter(
|
||||
(registry) => registry.inPublic && registry.inProduction,
|
||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
||||
await Promise.all(COMMANDS_RAW)
|
||||
)
|
||||
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||
.map(
|
||||
(registry) =>
|
||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||
@@ -0,0 +1,97 @@
|
||||
import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import { PlayerHistoriesRaw } from '../core/blitzstars/getPlayerHistories';
|
||||
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||
import embedWarning from '../core/discord/embedWarning';
|
||||
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountAchievements } from '../types/accountAchievements';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { ClanInfo } from '../types/clanInfo';
|
||||
|
||||
export const clanEventCommand: CommandRegistry = {
|
||||
inProduction: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('clan-event')
|
||||
.setDescription('Lists all inactive players')
|
||||
.addStringOption(addClanChoices),
|
||||
|
||||
async handler(interaction) {
|
||||
const { region, id } = await resolveClanFromCommand(interaction);
|
||||
const clan = (
|
||||
await getWargamingResponse<ClanInfo>(
|
||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||
)
|
||||
)[id];
|
||||
const memberIds = clan.members_ids.join(',');
|
||||
const [players, achievements, previousJointVictories] = await Promise.all([
|
||||
getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
|
||||
),
|
||||
getWargamingResponse<AccountAchievements>(
|
||||
`https://api.wotblitz.${region}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
|
||||
),
|
||||
Promise.all(
|
||||
clan.members_ids.map((id) =>
|
||||
fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
|
||||
.then((response) => response.json() as Promise<PlayerHistoriesRaw>)
|
||||
.then((data) => ({
|
||||
id,
|
||||
jointVictory: data[0]?.achievements.jointVictoryCount as
|
||||
| number
|
||||
| undefined,
|
||||
})),
|
||||
),
|
||||
).then((data) =>
|
||||
data.reduce<Record<number, number | undefined>>(
|
||||
(accumulator, value) => ({
|
||||
...accumulator,
|
||||
[value.id]: value.jointVictory,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
),
|
||||
]);
|
||||
const jointVictories = clan.members_ids.map((id) => ({
|
||||
id,
|
||||
joinVictory:
|
||||
players[id].last_battle_time > getTimeDaysAgo(region, 1)
|
||||
? previousJointVictories[id] === undefined
|
||||
? 0
|
||||
: achievements[id].max_series.jointVictory -
|
||||
previousJointVictories[id]!
|
||||
: 0,
|
||||
}));
|
||||
|
||||
return [
|
||||
`# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
|
||||
clan.tag,
|
||||
)}]'s platoon wins today: ${(
|
||||
jointVictories.reduce(
|
||||
(accumulator, { joinVictory }) => accumulator + joinVictory,
|
||||
0,
|
||||
) / 2
|
||||
).toFixed(0)}\n${jointVictories
|
||||
.filter(({ joinVictory }) => joinVictory)
|
||||
.sort((a, b) => b.joinVictory - a.joinVictory)
|
||||
.map(
|
||||
({ id, joinVictory }) =>
|
||||
`- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
|
||||
)
|
||||
.join('\n')}`,
|
||||
|
||||
embedWarning(
|
||||
'This is an approximation!',
|
||||
"Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/about timezones` for more info)",
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: autocompleteClan,
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||
import { client } from '../bot';
|
||||
import { tankopedia } from '../core/blitz/tankopedia';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||
import { client } from '../core/discord/client';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable from '../core/discord/markdownTable';
|
||||
import getClientId from '../core/node/getClientId';
|
||||
@@ -12,7 +12,6 @@ const executionStart = new Date().getTime();
|
||||
|
||||
export const debugCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import sumStats from '../core/blitz/sumStats';
|
||||
@@ -12,7 +13,6 @@ import embedNegative from '../core/discord/embedNegative';
|
||||
import embedPositive from '../core/discord/embedPositive';
|
||||
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||
@@ -26,7 +26,6 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
|
||||
|
||||
export const eligibleCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: false,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -49,7 +48,7 @@ export const eligibleCommand: CommandRegistry = {
|
||||
|
||||
async handler(interaction) {
|
||||
const clan = interaction.options.getString('clan') as SkilledClan;
|
||||
const { id, server } = await resolvePlayerFromCommand(interaction);
|
||||
const { id, region: server } = await resolvePlayerFromCommand(interaction);
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
||||
@@ -12,76 +13,81 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import evolution from '../renderers/evolution';
|
||||
import { StatType } from '../renderers/fullStats';
|
||||
import { StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const evolutionCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('evolution')
|
||||
.setDescription('Evolution of statistics'),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.server, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return [
|
||||
await evolution(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}${
|
||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
||||
}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.server, interaction);
|
||||
|
||||
return await evolution(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('evolution')
|
||||
.setDescription('Evolution of statistics'),
|
||||
false,
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inPublic: true,
|
||||
|
||||
command,
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return [
|
||||
await evolution(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}${
|
||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
||||
}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.region, interaction);
|
||||
|
||||
return await evolution(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
);
|
||||
@@ -1,5 +1,7 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { TreeTypeString } from '../components/Tanks';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
||||
@@ -12,73 +14,101 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import fullStats, { StatType } from '../renderers/fullStats';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import fullStats from '../renderers/fullStats';
|
||||
import { MultiTankFilters, StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const fullStatsCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('fullstats')
|
||||
.setDescription('Full in-game statistics'),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.server, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
return [
|
||||
await fullStats(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.server, interaction);
|
||||
|
||||
return await fullStats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('full-stats')
|
||||
.setDescription('Full in-game statistics'),
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inPublic: true,
|
||||
|
||||
command,
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const resolvedPeriod = resolvePeriodFromCommand(
|
||||
player.region,
|
||||
interaction,
|
||||
);
|
||||
const period = interaction.options.getString('period');
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
const tierOption = interaction.options.getString('tier');
|
||||
|
||||
return [
|
||||
await fullStats(
|
||||
commandGroup,
|
||||
resolvedPeriod,
|
||||
player,
|
||||
commandGroup === 'tank'
|
||||
? tankId
|
||||
: commandGroup === 'multi-tank'
|
||||
? ({
|
||||
'tank-type':
|
||||
interaction.options.getString('tank-type') ?? undefined,
|
||||
nation: interaction.options.getString('nation') ?? undefined,
|
||||
tier: tierOption ? parseInt(tierOption) : undefined,
|
||||
'tree-type': (interaction.options.getString('tree-type') ??
|
||||
undefined) as TreeTypeString | undefined,
|
||||
} satisfies Partial<MultiTankFilters>)
|
||||
: null,
|
||||
),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.region, interaction);
|
||||
|
||||
return await fullStats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
);
|
||||
@@ -1,15 +1,13 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import GenericStats from '../components/GenericStats';
|
||||
import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import getBlitzClan from '../core/blitz/getBlitzClan';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { ClanInfo } from '../types/clanInfo';
|
||||
@@ -18,7 +16,6 @@ const DEFAULT_THRESHOLD = 7;
|
||||
|
||||
export const inactiveCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -35,19 +32,18 @@ export const inactiveCommand: CommandRegistry = {
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const clanName = interaction.options.getString('clan')!;
|
||||
const { server, id } = await getBlitzClan(clanName);
|
||||
const { region, id } = await resolveClanFromCommand(interaction);
|
||||
const threshold =
|
||||
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
||||
const time = new Date().getTime() / 1000;
|
||||
const clanData = (
|
||||
await getWargamingResponse<ClanInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||
)
|
||||
)[id];
|
||||
const memberIds = clanData.members_ids;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
|
||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
|
||||
',',
|
||||
)}`,
|
||||
);
|
||||
@@ -69,17 +65,12 @@ export const inactiveCommand: CommandRegistry = {
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={clanData.name}
|
||||
nameDiscriminator="(Inactivity)"
|
||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
|
||||
description={`${threshold}+ Days • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
||||
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
||||
|
||||
<PoweredBy type={PoweredByType.Wargaming} />
|
||||
</Wrapper>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import * as Tanks from '../components/Tanks';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getTankStats from '../core/blitz/getTankStats';
|
||||
import getTreeType from '../core/blitz/getTreeType';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
TankopediaEntry,
|
||||
Tier,
|
||||
tankopedia,
|
||||
tankopediaInfo,
|
||||
} from '../core/blitz/tankopedia';
|
||||
import addTierChoices from '../core/discord/addTierChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
@@ -45,107 +46,129 @@ const COMP_TANKS = [
|
||||
|
||||
export const ownedTanksCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('ownedtanks')
|
||||
.setName('owned-tanks')
|
||||
.setDescription("Shows a player's owned tanks")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('tier')
|
||||
.setDescription('The tier you want to see')
|
||||
.setChoices(
|
||||
{ name: 'Tier I', value: '1' },
|
||||
{ name: 'Tier II', value: '2' },
|
||||
{ name: 'Tier III', value: '3' },
|
||||
{ name: 'Tier IV', value: '4' },
|
||||
{ name: 'Tier V', value: '5' },
|
||||
{ name: 'Tier VI', value: '6' },
|
||||
{ name: 'Tier VII', value: '7' },
|
||||
{ name: 'Tier VIII', value: '8' },
|
||||
{ name: 'Tier IX', value: '9' },
|
||||
{ name: 'Tier X', value: '10' },
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addStringOption(addTierChoices)
|
||||
.addStringOption(addUsernameChoices),
|
||||
|
||||
async handler(interaction) {
|
||||
const tier = Number(interaction.options.getString('tier'));
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const tankStats = await getTankStats(server, id);
|
||||
const tanks = (
|
||||
const filteredTanks = (
|
||||
await Promise.all(
|
||||
tankStats.map(async (tankData) => ({
|
||||
...(await tankopedia)[tankData.tank_id],
|
||||
tankopedia: (await tankopedia)[tankData.tank_id]!,
|
||||
tank_id: tankData.tank_id,
|
||||
})),
|
||||
)
|
||||
).filter((tank) => tank?.tier === tier);
|
||||
).filter((tank) => tank.tankopedia?.tier === tier);
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
);
|
||||
const leftColumnSize = Math.ceil(tanks.length / 2);
|
||||
const leftColumn = tanks.slice(0, leftColumnSize);
|
||||
const rightColumn = tanks.slice(leftColumnSize);
|
||||
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
||||
const nations: string[] = [];
|
||||
|
||||
filteredTanks.forEach((tank) => {
|
||||
if (groupedTanks[tank.tankopedia.nation] === undefined) {
|
||||
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
|
||||
nations.push(tank.tankopedia.nation);
|
||||
} else {
|
||||
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
|
||||
}
|
||||
});
|
||||
|
||||
nations.sort();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`}
|
||||
nameDiscriminator={
|
||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
||||
}
|
||||
image={
|
||||
clanData[id]?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`Owned tanks • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
description={`Tier ${
|
||||
TIER_ROMAN_NUMERALS[tier as Tier]
|
||||
} tanks • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{tanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||
|
||||
{tanks.length > 0 && (
|
||||
<Tanks.Root>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
leftColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
rightColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
</Tanks.Root>
|
||||
)}
|
||||
{filteredTanks.length > 0 &&
|
||||
(await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tanks = groupedTanks[nation];
|
||||
const leftColumnSize = Math.ceil(tanks.length / 2);
|
||||
const leftColumn = tanks.slice(0, leftColumnSize);
|
||||
const rightColumn = tanks.slice(leftColumnSize);
|
||||
|
||||
<PoweredBy type={PoweredByType.Wargaming} />
|
||||
return (
|
||||
<Tanks.Root>
|
||||
<Tanks.Title>
|
||||
{(await tankopediaInfo).vehicle_nations[nation]}
|
||||
</Tanks.Title>
|
||||
|
||||
<Tanks.Row>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
leftColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
rightColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
</Tanks.Row>
|
||||
</Tanks.Root>
|
||||
);
|
||||
}),
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
},
|
||||
|
||||
autocomplete: autocompleteUsername,
|
||||
};
|
||||
|
||||
// TODO: make this work in the future
|
||||
// console.log('Caching tank images...');
|
||||
// TANKS.then((tanks) =>
|
||||
// jsxToSvg(
|
||||
// <div style={{ display: 'flex' }}>
|
||||
// {tanks.map(({ images, tank_id }) => {
|
||||
// return (
|
||||
// images.normal && <img key={tank_id} src={images.normal} width={0} />
|
||||
// );
|
||||
// })}
|
||||
// </div>,
|
||||
// ),
|
||||
// ).then(() => console.log('Cached tank images'));
|
||||
@@ -1,9 +1,8 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
export const pingCommand: CommandRegistry<true> = {
|
||||
export const pingCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
handlesInteraction: true,
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { startCase } from 'lodash';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountAchievements } from '../types/accountAchievements';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
@@ -16,11 +16,10 @@ type SortBy = 'name' | 'count';
|
||||
|
||||
export const playerAchievementsCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('playerachievements')
|
||||
.setName('player-achievements')
|
||||
.setDescription("All the player's achievements")
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addStringOption((option) =>
|
||||
@@ -36,7 +35,7 @@ export const playerAchievementsCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const playerInfoCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('playerinfo')
|
||||
.setName('player-info')
|
||||
.setDescription('Basic information about a player')
|
||||
.addStringOption(addUsernameChoices),
|
||||
|
||||
async handler(interaction) {
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
import {
|
||||
APIApplicationCommandOptionChoice,
|
||||
SlashCommandBuilder,
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
} from 'discord.js';
|
||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||
import * as Leaderboard from '../components/Leaderboard';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
|
||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||
import getMidnightLeaderboard, {
|
||||
DATABASE_REPO,
|
||||
} from '../core/database/getMidnightLeaderboard';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { octokit } from '../core/github/octokit';
|
||||
import throwError from '../core/node/throwError';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
|
||||
export interface RatingsPlayer {
|
||||
spa_id: number;
|
||||
mmr: number;
|
||||
season_number: number;
|
||||
calibrationBattlesLeft: number;
|
||||
number: number;
|
||||
percentile: number;
|
||||
skip: boolean;
|
||||
updated_at: string;
|
||||
score: number;
|
||||
nickname: string;
|
||||
clan_tag: string;
|
||||
}
|
||||
|
||||
export interface LeagueTop {
|
||||
result: RatingsPlayer[];
|
||||
}
|
||||
|
||||
export type RatingsReward = {
|
||||
from_position: number;
|
||||
to_position: number;
|
||||
count: number;
|
||||
} & (
|
||||
| {
|
||||
type: 'vehicle';
|
||||
|
||||
vehicle: {
|
||||
id: number;
|
||||
name: string;
|
||||
nation: string;
|
||||
subnation: string;
|
||||
use_subnation_flag: boolean;
|
||||
type_slug: string;
|
||||
level: number;
|
||||
roman_level: string;
|
||||
user_string: string;
|
||||
image_url: string;
|
||||
preview_url: string;
|
||||
is_premium: boolean;
|
||||
is_collectible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'stuff';
|
||||
|
||||
stuff: {
|
||||
name: string;
|
||||
count: number;
|
||||
title: string;
|
||||
image_url: string;
|
||||
type: string;
|
||||
|
||||
sizes: {};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export interface RatingsLeague {
|
||||
title: string;
|
||||
small_icon: string;
|
||||
big_icon: string;
|
||||
background: string;
|
||||
index: number;
|
||||
percentile: number;
|
||||
}
|
||||
|
||||
export type RatingsInfo =
|
||||
| {
|
||||
detail: undefined;
|
||||
|
||||
title: string;
|
||||
icon: string | null;
|
||||
start_at: string;
|
||||
finish_at: string;
|
||||
current_season: number;
|
||||
updated_at: string;
|
||||
count: number;
|
||||
|
||||
rewards: RatingsReward[];
|
||||
leagues: RatingsLeague[];
|
||||
}
|
||||
| { detail: { error: string } };
|
||||
|
||||
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,
|
||||
) {
|
||||
return option
|
||||
.addSubcommand((option) =>
|
||||
addOptions(
|
||||
option
|
||||
.setName('neighbours')
|
||||
.setDescription('Your position and neighbours'),
|
||||
)
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setDescription('How many neighbours to display (default: 10)')
|
||||
.setRequired(false)
|
||||
.setMinValue(5)
|
||||
.setMaxValue(30),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
addOptions(option.setName('league').setDescription('Top league players'))
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('league')
|
||||
.setDescription('The league to display')
|
||||
.addChoices(
|
||||
...LEAGUES.map(
|
||||
({ name }, value) =>
|
||||
({
|
||||
name,
|
||||
value: `${value}`,
|
||||
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||
),
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setDescription('How many rows to display (default: 10)')
|
||||
.setRequired(false)
|
||||
.setMinValue(5)
|
||||
.setMaxValue(30),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 command = new SlashCommandBuilder()
|
||||
.setName('ratings')
|
||||
.setDescription('Ratings battles statistics')
|
||||
.addSubcommandGroup((option) =>
|
||||
addSubcommands(
|
||||
option.setName('archive').setDescription('Any previous season'),
|
||||
(option) =>
|
||||
option.addStringOption((option) =>
|
||||
option
|
||||
.setName('season')
|
||||
.setDescription('The season number')
|
||||
.setChoices(...seasonChoices)
|
||||
.setRequired(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
.addSubcommandGroup((option) =>
|
||||
addSubcommands(
|
||||
option
|
||||
.setName('current')
|
||||
.setDescription('The current running Ratings season'),
|
||||
),
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inPublic: true,
|
||||
|
||||
command,
|
||||
|
||||
async handler(interaction) {
|
||||
interface SimplifiedPlayer {
|
||||
id: number;
|
||||
score: number;
|
||||
position: number;
|
||||
clan: string | undefined;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
|
||||
| 'archive'
|
||||
| 'current';
|
||||
const subcommand = interaction.options.getSubcommand(true) as
|
||||
| 'league'
|
||||
| 'neighbours';
|
||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||
const seasonOption = interaction.options.getString('season');
|
||||
const season = seasonOption ? parseInt(seasonOption) : undefined;
|
||||
let titleName: string;
|
||||
let titleImage: string | undefined;
|
||||
let titleNameDiscriminator: string | undefined;
|
||||
let titleDescription: string;
|
||||
let playersBefore: number;
|
||||
let playersAfter: number;
|
||||
let players: SimplifiedPlayer[] | undefined;
|
||||
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
||||
let playerId: number | undefined;
|
||||
let regionRatingsInfo: RatingsInfo;
|
||||
|
||||
if (subcommand === 'league') {
|
||||
const leagueIndex = parseInt(
|
||||
interaction.options.getString('league')!,
|
||||
);
|
||||
const region = interaction.options.getString('region') as Region;
|
||||
regionRatingsInfo = await (subcommandGroup === 'current'
|
||||
? getRatingsInfo(region)
|
||||
: getArchivedRatingsInfo(season!, region));
|
||||
const regionSubdomain = regionToRegionSubdomain(region);
|
||||
const result =
|
||||
subcommandGroup === 'current'
|
||||
? regionRatingsInfo.detail === undefined
|
||||
? await fetch(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
||||
)
|
||||
.then((response) => response.json() as Promise<LeagueTop>)
|
||||
.then(async ({ result }) =>
|
||||
result.slice(0, limit).map(
|
||||
(player) =>
|
||||
({
|
||||
id: player.spa_id,
|
||||
score: player.score,
|
||||
position: player.number,
|
||||
clan: player.clan_tag,
|
||||
nickname: player.nickname,
|
||||
}) satisfies SimplifiedPlayer,
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
: 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;
|
||||
const firstPlayerIndex =
|
||||
leagueIndex === 0
|
||||
? 0
|
||||
: players.findIndex(
|
||||
(player) =>
|
||||
player.score < LEAGUES[leagueIndex - 1].minScore,
|
||||
);
|
||||
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
||||
const trimmed = players.slice(
|
||||
firstPlayerIndex,
|
||||
lastPlayerIndex + 1,
|
||||
);
|
||||
const ids = trimmed.map((player) => player.id).join(',');
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
||||
);
|
||||
|
||||
return trimmed.map(
|
||||
(player, index) =>
|
||||
({
|
||||
id: player.id,
|
||||
score: player.score,
|
||||
position: firstPlayerIndex + index + 1,
|
||||
clan: clanData[player.id]?.clan?.tag,
|
||||
nickname:
|
||||
clanData[player.id]?.account_name ??
|
||||
`Unknown Player ${player.id}`,
|
||||
}) satisfies SimplifiedPlayer,
|
||||
);
|
||||
});
|
||||
const leagueInfo =
|
||||
regionRatingsInfo.detail === undefined
|
||||
? regionRatingsInfo.leagues[leagueIndex]
|
||||
: undefined;
|
||||
midnightLeaderboard =
|
||||
subcommandGroup === 'current'
|
||||
? regionRatingsInfo.detail === undefined
|
||||
? await getMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
)
|
||||
: undefined
|
||||
: await getMidnightLeaderboard(region, season!);
|
||||
|
||||
players = result;
|
||||
playersBefore = result ? result[0].position - 1 : 0;
|
||||
playersAfter =
|
||||
regionRatingsInfo.detail === undefined && result !== undefined
|
||||
? regionRatingsInfo.count - result[result.length - 1].position
|
||||
: 0;
|
||||
titleName = `${
|
||||
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
||||
} - ${REGION_NAMES_SHORT[region]}`;
|
||||
titleImage = leagueInfo
|
||||
? leagueInfo.big_icon.startsWith('http')
|
||||
? leagueInfo.big_icon
|
||||
: `https:${leagueInfo.big_icon}`
|
||||
: undefined;
|
||||
titleDescription = `Top ${limit} players`;
|
||||
} else {
|
||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||
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;
|
||||
});
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const clan = (
|
||||
await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
)
|
||||
)[id]?.clan;
|
||||
const regionSubdomain = regionToRegionSubdomain(region);
|
||||
const neighbors =
|
||||
subcommandGroup === 'current'
|
||||
? regionRatingsInfo.detail === undefined
|
||||
? await fetch(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||
limit / 2,
|
||||
)}`,
|
||||
)
|
||||
.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,
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
: 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;
|
||||
const playerIndex = players.findIndex(
|
||||
(player) => player.id === id,
|
||||
);
|
||||
|
||||
if (playerIndex === -1) {
|
||||
throw throwError(
|
||||
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
|
||||
'This player did not participate in this season or did not get past calibration.',
|
||||
);
|
||||
}
|
||||
|
||||
const halfRange = Math.round(limit / 2);
|
||||
const trimmed = players.slice(
|
||||
playerIndex - halfRange,
|
||||
playerIndex + halfRange + 1,
|
||||
);
|
||||
const ids = trimmed.map((player) => player.id).join(',');
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
||||
);
|
||||
|
||||
return trimmed.map((player, index) => ({
|
||||
id: player.id,
|
||||
score: player.score,
|
||||
position: playerIndex + index + 1,
|
||||
clan: clanData[player.id]!.clan?.tag,
|
||||
nickname: clanData[player.id]!.account_name,
|
||||
}));
|
||||
});
|
||||
midnightLeaderboard =
|
||||
subcommandGroup === 'current'
|
||||
? regionRatingsInfo.detail === undefined
|
||||
? await getMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
)
|
||||
: undefined
|
||||
: await getMidnightLeaderboard(region, season!);
|
||||
|
||||
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
||||
|
||||
players = neighbors;
|
||||
titleImage = neighbors
|
||||
? clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||
: undefined
|
||||
: undefined;
|
||||
titleName = neighbors
|
||||
? accountInfo[id].nickname
|
||||
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
||||
titleDescription = 'Ratings neighbours';
|
||||
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
||||
playersAfter =
|
||||
regionRatingsInfo.detail === undefined
|
||||
? neighbors
|
||||
? regionRatingsInfo.count -
|
||||
neighbors[neighbors.length - 1].position
|
||||
: 0
|
||||
: 0;
|
||||
playerId = id;
|
||||
}
|
||||
|
||||
const items = players?.map((player) => {
|
||||
const reward =
|
||||
regionRatingsInfo.detail === undefined
|
||||
? regionRatingsInfo.rewards.find(
|
||||
(reward) =>
|
||||
player.position >= reward.from_position &&
|
||||
player.position <= reward.to_position,
|
||||
)
|
||||
: undefined;
|
||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||
(item) => item.id === player.id,
|
||||
);
|
||||
const midnightScore =
|
||||
midnightIndex === undefined
|
||||
? player.score
|
||||
: midnightLeaderboard![midnightIndex].score;
|
||||
const midnightPosition =
|
||||
midnightIndex === undefined ? player.position : midnightIndex + 1;
|
||||
|
||||
return (
|
||||
<Leaderboard.Item
|
||||
nickname={player.nickname}
|
||||
score={player.score}
|
||||
position={player.position}
|
||||
clan={player.clan}
|
||||
reward={reward}
|
||||
deltaScore={player.score - midnightScore}
|
||||
deltaPosition={midnightPosition - player.position}
|
||||
key={player.id}
|
||||
highlight={player.id === playerId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={titleName}
|
||||
image={titleImage}
|
||||
nameDiscriminator={titleNameDiscriminator}
|
||||
description={`${titleDescription} • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{items && (
|
||||
<Leaderboard.Root>
|
||||
{playersBefore > 0 && (
|
||||
<Leaderboard.Gap number={playersBefore} />
|
||||
)}
|
||||
{items}
|
||||
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
||||
</Leaderboard.Root>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
},
|
||||
|
||||
autocomplete: autocompleteUsername,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,23 +1,22 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { REGION_DOMAIN_NAMES, RegionDomain } from '../constants/regions';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import addServerChoices from '../core/discord/addServerChoices';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { ClanList } from '../types/clanList';
|
||||
|
||||
export const searchClansCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchclans')
|
||||
.setName('search-clans')
|
||||
.setDescription('Search clans')
|
||||
.addStringOption(addServerChoices)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
@@ -28,7 +27,7 @@ export const searchClansCommand: CommandRegistry = {
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const server = interaction.options.getString('server') as RegionDomain;
|
||||
const server = interaction.options.getString('region') as Region;
|
||||
const clan = interaction.options.getString('clan')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const clanList = await getWargamingResponse<ClanList>(
|
||||
@@ -36,9 +35,7 @@ export const searchClansCommand: CommandRegistry = {
|
||||
);
|
||||
|
||||
return embedInfo(
|
||||
`Clan search for "${markdownEscape(clan)}" in ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`,
|
||||
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
|
||||
clanList.length === 0
|
||||
? 'No clans found.'
|
||||
: `\`\`\`\n${clanList
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { REGION_DOMAIN_NAMES, RegionDomain } from '../constants/regions';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import { usernamePattern } from '../core/blitz/listPlayers';
|
||||
import addServerChoices from '../core/discord/addServerChoices';
|
||||
import { AccountList, usernamePattern } from '../core/blitz/listPlayers';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountList } from '../types/accountList';
|
||||
|
||||
export const searchPlayersCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchplayers')
|
||||
.setName('search-players')
|
||||
.setDescription('Search players in a Blitz server')
|
||||
.addStringOption(addServerChoices)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addStringOption((option) =>
|
||||
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
|
||||
)
|
||||
@@ -31,7 +29,7 @@ export const searchPlayersCommand: CommandRegistry = {
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const server = interaction.options.getString('server') as RegionDomain;
|
||||
const server = interaction.options.getString('region') as Region;
|
||||
const name = interaction.options.getString('username')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const trimmedSearch = name.trim();
|
||||
@@ -44,9 +42,7 @@ export const searchPlayersCommand: CommandRegistry = {
|
||||
: [];
|
||||
|
||||
return embedInfo(
|
||||
`Player search for "${markdownEscape(name)}" in ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`,
|
||||
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
|
||||
`\`\`\`${
|
||||
players.length === 0
|
||||
? 'No players found.'
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { TANK_NAMES } from '../core/blitz/tankopedia';
|
||||
import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
|
||||
import addTankChoices from '../core/discord/addTankChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
export const searchTanksCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchtanks')
|
||||
.setName('search-tanks')
|
||||
.setDescription('Search tanks')
|
||||
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
|
||||
.addIntegerOption((option) =>
|
||||
@@ -26,9 +25,10 @@ export const searchTanksCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const tank = interaction.options.getString('tank')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const results = go(tank, await TANK_NAMES, { limit }).map(
|
||||
(result) => result.target,
|
||||
);
|
||||
const results = go(tank, await TANK_NAMES_DIACRITICS, {
|
||||
limit,
|
||||
keys: ['combined'],
|
||||
}).map((result) => result.obj.original);
|
||||
|
||||
return embedInfo(
|
||||
`Tank search for "${markdownEscape(tank)}"`,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { TreeTypeString } from '../components/Tanks';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
||||
@@ -12,74 +14,107 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { StatType } from '../renderers/fullStats';
|
||||
import stats from '../renderers/stats';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const statsCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('stats')
|
||||
.setDescription('In-game statistics'),
|
||||
),
|
||||
.setDescription('Regular battles statistics'),
|
||||
);
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.server, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inPublic: true,
|
||||
|
||||
return [
|
||||
await stats(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
command,
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const resolvedPeriod = resolvePeriodFromCommand(
|
||||
player.region,
|
||||
interaction,
|
||||
);
|
||||
const period = interaction.options.getString('period');
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.server, interaction);
|
||||
const tierOption = interaction.options.getString('tier');
|
||||
|
||||
return await stats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
};
|
||||
return [
|
||||
await stats(
|
||||
commandGroup,
|
||||
resolvedPeriod,
|
||||
player,
|
||||
commandGroup === 'tank'
|
||||
? tankId
|
||||
: commandGroup === 'multi-tank'
|
||||
? ({
|
||||
'tank-type':
|
||||
interaction.options.getString('tank-type') ?? undefined,
|
||||
nation: interaction.options.getString('nation') ?? undefined,
|
||||
tier: tierOption ? parseInt(tierOption) : undefined,
|
||||
'tree-type': (interaction.options.getString('tree-type') ??
|
||||
undefined) as TreeTypeString | undefined,
|
||||
} satisfies Partial<MultiTankFilters>)
|
||||
: null,
|
||||
),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.region, interaction);
|
||||
|
||||
return await stats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
commandGroup === 'tank'
|
||||
? parseInt(url.searchParams.get('tankId')!)
|
||||
: commandGroup === 'multi-tank'
|
||||
? ({
|
||||
'tank-type': url.searchParams.get('tank-type')!,
|
||||
nation: url.searchParams.get('nation')!,
|
||||
tier: parseInt(url.searchParams.get('tier')!),
|
||||
'tree-type': url.searchParams.get('tree-type') as TreeTypeString,
|
||||
} satisfies MultiTankFilters)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
@@ -8,14 +8,12 @@ import linkButton from '../core/discord/linkButton';
|
||||
import primaryButton from '../core/discord/primaryButton';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import today from '../renderers/today';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const todayCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -24,30 +22,52 @@ export const todayCommand: CommandRegistry = {
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setName('cutoff')
|
||||
.setDescription(
|
||||
'The maximum number of tanks to display (default: Infinity)',
|
||||
)
|
||||
.setRequired(false)
|
||||
.setMinValue(1),
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('maximized')
|
||||
.setDescription(
|
||||
'The number of rows with full stats after which all are collapsed (default: 4)',
|
||||
)
|
||||
.setMinValue(0)
|
||||
.setRequired(false),
|
||||
)
|
||||
.addBooleanOption((option) =>
|
||||
option
|
||||
.setName('show-total')
|
||||
.setDescription('Show the total stats at the top (default: true)')
|
||||
.setRequired(false),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const limit = interaction.options.getInteger('limit') ?? Infinity;
|
||||
const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
|
||||
const maximized = interaction.options.getInteger('maximized') ?? undefined;
|
||||
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, player);
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
cutoff,
|
||||
maximized,
|
||||
'show-total': showTotal,
|
||||
});
|
||||
|
||||
return [
|
||||
await today(player, limit, false),
|
||||
await today(player, cutoff, maximized, showTotal, false),
|
||||
primaryButton(path, 'Refresh'),
|
||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import discord from '../../discord.json' assert { type: 'json' };
|
||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedNegative from '../core/discord/embedNegative';
|
||||
import embedPositive from '../core/discord/embedPositive';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
|
||||
export const verifyCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -24,7 +23,7 @@ export const verifyCommand: CommandRegistry = {
|
||||
|
||||
async handler(interaction) {
|
||||
const blitzAccount = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = blitzAccount;
|
||||
const { id, region: server } = blitzAccount;
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Percentile } from '../../../../../constants/percentiles';
|
||||
import hslToHex from '../../../../../core/node/hslToHex';
|
||||
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator';
|
||||
|
||||
export enum GlowSide {
|
||||
@@ -14,8 +13,8 @@ export interface GlowProps {
|
||||
|
||||
export function Glow({ percentile, side }: GlowProps) {
|
||||
const color = PERCENTILE_COLORS[percentile];
|
||||
const color1 = hslToHex(color);
|
||||
const color2 = `${color1}00`;
|
||||
const color1 = color;
|
||||
const color2 = `${color}00`;
|
||||
|
||||
const background = `linear-gradient(${
|
||||
side === GlowSide.Top ? 0 : 180
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
export function BlitzkriegWormWide() {
|
||||
return (
|
||||
<svg
|
||||
width="74"
|
||||
height="17"
|
||||
viewBox="0 0 74 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M33.945 12.9858V0H36.875V7.50095L39.4608 4.63967H42.7879L36.875 10.7408V12.9858H33.945ZM39.7873 12.9858L37.0515 9.04164L38.8695 7.04314L43.2292 12.9858H39.7873Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M43.8823 12.9858V4.63967H46.8123L46.8299 5.74016H46.8476C47.0712 5.3704 47.3624 5.06813 47.7213 4.83335C48.0861 4.59271 48.4979 4.47239 48.9568 4.47239C49.1216 4.47239 49.2775 4.48413 49.4246 4.50761C49.5775 4.53109 49.6835 4.55457 49.7423 4.57804V6.78783C49.6364 6.74674 49.5069 6.7174 49.354 6.69979C49.2069 6.67631 49.0421 6.66457 48.8598 6.66457C48.4773 6.66457 48.1037 6.78783 47.7389 7.03434C47.3742 7.27498 47.0888 7.59192 46.8829 7.98517V12.9858H43.8823Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M50.7042 12.9858V4.63967H53.6872V12.9858H50.7042ZM50.5366 1.84883C50.5366 1.42624 50.6807 1.07115 50.969 0.78355C51.2573 0.495955 51.6691 0.35216 52.2045 0.35216C52.7399 0.35216 53.1488 0.493019 53.4313 0.774745C53.7137 1.05647 53.8549 1.41156 53.8549 1.84002C53.8549 2.26261 53.7078 2.62357 53.4136 2.9229C53.1253 3.21637 52.7164 3.3631 52.1869 3.3631C51.6574 3.3631 51.2485 3.21637 50.9602 2.9229C50.6778 2.62357 50.5366 2.26555 50.5366 1.84883Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M54.9492 8.92719V8.64546C54.9492 7.34835 55.3611 6.31535 56.1848 5.54648C57.0143 4.77173 58.1175 4.38436 59.4942 4.38436C60.8475 4.38436 61.8918 4.75412 62.6272 5.49365C63.3685 6.23318 63.7392 7.24857 63.7392 8.53981V9.36739H56.5731V7.86191H60.9063V7.77387C60.9063 7.38063 60.7857 7.05488 60.5445 6.79663C60.3091 6.53838 59.9502 6.40926 59.4678 6.40926C58.9441 6.40926 58.5441 6.59708 58.2675 6.97271C57.9969 7.34835 57.8616 7.81202 57.8616 8.36374V8.97121C57.8616 9.66966 58.0086 10.2096 58.3028 10.5911C58.6029 10.9726 59.0324 11.1634 59.5913 11.1634C59.9973 11.1634 60.3385 11.0783 60.6151 10.9081C60.8975 10.732 61.1152 10.506 61.2681 10.2302L63.6686 10.9873C63.3921 11.674 62.9243 12.2169 62.2654 12.616C61.6064 13.0093 60.7239 13.2059 59.6178 13.2059C58.141 13.2059 56.9937 12.8244 56.1759 12.0614C55.3581 11.2925 54.9492 10.2478 54.9492 8.92719Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M68.399 14.8787C68.9756 14.8787 69.4874 14.7114 69.9346 14.3768C70.3876 14.0423 70.6141 13.47 70.6141 12.6601V11.9029C70.3611 12.261 70.0287 12.5515 69.6169 12.7745C69.205 12.9975 68.6608 13.1091 67.9842 13.1091C66.9369 13.1091 66.0956 12.7246 65.4601 11.9557C64.8306 11.181 64.5158 10.1773 64.5158 8.94479V8.66307C64.5158 7.43052 64.8188 6.42393 65.4248 5.64332C66.0367 4.85683 66.8898 4.46359 67.9842 4.46359C68.549 4.46359 69.0668 4.58098 69.5374 4.81575C70.0081 5.05052 70.3582 5.34692 70.5876 5.70495H70.6141L70.6318 4.63967H73.5V12.2023C73.5 13.7165 73.0852 14.8728 72.2556 15.671C71.4261 16.4751 70.3082 16.8771 68.902 16.8771H0V14.8787H68.399ZM67.8342 10.5207C67.5694 10.1157 67.437 9.59042 67.437 8.94479V8.67187C67.437 8.03212 67.5694 7.50682 67.8342 7.09597C68.0989 6.68512 68.5078 6.47969 69.0609 6.47969C69.4374 6.47969 69.7551 6.58534 70.014 6.79663C70.2788 7.00793 70.47 7.26031 70.5876 7.55377V10.0541C70.4759 10.3299 70.2876 10.5765 70.0228 10.7936C69.7581 11.0108 69.4404 11.1194 69.0697 11.1194C68.5108 11.1194 68.0989 10.9198 67.8342 10.5207Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M0 12.977V0H2.92118V5.61692C3.20359 5.24715 3.5566 4.95662 3.98021 4.74533C4.40383 4.52816 4.90687 4.41958 5.48934 4.41958C6.54837 4.41958 7.40443 4.80989 8.0575 5.59051C8.71057 6.36525 9.03711 7.36303 9.03711 8.58385V8.86558C9.03711 10.0923 8.70469 11.1135 8.03985 11.9293C7.37501 12.7452 6.51307 13.1531 5.45404 13.1531C4.87157 13.1531 4.35676 13.0533 3.90961 12.8538C3.46246 12.6542 3.12122 12.3813 2.88587 12.035H2.86822L2.8594 12.977H0ZM2.92118 10.0717C3.04473 10.3534 3.23595 10.6 3.49482 10.8112C3.7537 11.0225 4.092 11.1282 4.50973 11.1282C5.02748 11.1282 5.41579 10.9169 5.67467 10.4943C5.93943 10.0658 6.07181 9.52881 6.07181 8.88318V8.61907C6.07181 7.97344 5.93943 7.45694 5.67467 7.06957C5.41579 6.67633 5.00983 6.47971 4.45678 6.47971C4.07435 6.47971 3.75075 6.58535 3.48599 6.79665C3.22712 7.00794 3.03885 7.25445 2.92118 7.53618V10.0717Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M10.2815 12.977V0H13.238V12.977H10.2815Z" fill="currentColor" />
|
||||
<path
|
||||
d="M14.9236 12.977V4.63088H17.9065V12.977H14.9236ZM14.7559 1.84002C14.7559 1.41744 14.9001 1.06234 15.1883 0.774748C15.4766 0.487152 15.8885 0.343354 16.4239 0.343354C16.9593 0.343354 17.3682 0.484217 17.6506 0.765944C17.933 1.04767 18.0742 1.40276 18.0742 1.83122C18.0742 2.25381 17.9271 2.61477 17.633 2.91411C17.3447 3.20757 16.9358 3.3543 16.4062 3.3543C15.8767 3.3543 15.4678 3.20757 15.1795 2.91411C14.8971 2.61477 14.7559 2.25675 14.7559 1.84002Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M18.8332 6.73502V4.63088H24.7726V6.73502H18.8332ZM20.1217 10.3887V5.15031L20.1923 5.00945V2.04252H23.0076V9.78119C23.0076 10.3329 23.0811 10.6909 23.2282 10.8553C23.3812 11.0137 23.6195 11.093 23.943 11.093C24.096 11.093 24.2402 11.0842 24.3755 11.0666C24.5108 11.0431 24.6432 11.0049 24.7726 10.9521V12.9418C24.6138 12.9946 24.3725 13.0445 24.0489 13.0915C23.7254 13.1443 23.3253 13.1707 22.8487 13.1707C21.9779 13.1707 21.3043 12.9535 20.8277 12.5192C20.357 12.079 20.1217 11.3688 20.1217 10.3887Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M25.3551 12.977V11.3307L29.1323 6.74382H25.461V4.63088H32.8125V6.33004L29.0264 10.9257H33.0684V12.977H25.3551Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +1,98 @@
|
||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
||||
import { Percentile } from '../../../constants/percentiles';
|
||||
import { TankType } from '../../../core/blitz/tankopedia';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { RowDiscriminator } from './RowDiscriminator';
|
||||
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
||||
import { RowStat } from './RowStat';
|
||||
|
||||
export interface RowProps {
|
||||
name: string;
|
||||
winrate: number;
|
||||
careerWinrate: number;
|
||||
WN8?: number;
|
||||
careerWN8?: number;
|
||||
damage: number;
|
||||
careerDamage: number;
|
||||
battles: number;
|
||||
careerBattles: number;
|
||||
icon?: string;
|
||||
minimized: boolean;
|
||||
isListing: boolean;
|
||||
naked?: boolean;
|
||||
export interface RowStatItem {
|
||||
title: string;
|
||||
current?: number | string;
|
||||
career?: number | string;
|
||||
delta?: number;
|
||||
percentile?: Percentile;
|
||||
}
|
||||
|
||||
export function Row({
|
||||
isListing,
|
||||
minimized,
|
||||
name,
|
||||
winrate,
|
||||
careerWinrate,
|
||||
WN8,
|
||||
careerWN8,
|
||||
damage,
|
||||
careerDamage,
|
||||
battles,
|
||||
careerBattles,
|
||||
icon,
|
||||
naked,
|
||||
}: RowProps) {
|
||||
interface RowProps {
|
||||
type?: 'tank' | 'summary';
|
||||
title: string;
|
||||
minimized?: boolean;
|
||||
treeType?: TreeTypeEnum;
|
||||
tankType?: TankType;
|
||||
|
||||
stats: (RowStatItem | undefined)[];
|
||||
}
|
||||
|
||||
export function Row(props: RowProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4,
|
||||
backgroundColor: isListing
|
||||
? theme.colors.componentInteractive
|
||||
: theme.colors.appBackground2,
|
||||
padding: 8,
|
||||
backgroundColor: theme.colors.appBackground2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<RowDiscriminator minimized={minimized} name={name} icon={icon} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
gap: 4,
|
||||
backgroundColor: theme.colors.componentInteractive,
|
||||
}}
|
||||
>
|
||||
{props.type === 'tank' && props.tankType !== undefined && (
|
||||
<img
|
||||
src={
|
||||
TREE_TYPE_ICONS[props.treeType ?? TreeTypeEnum.TechTree][
|
||||
props.tankType
|
||||
]
|
||||
}
|
||||
style={{ width: 16, height: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
|
||||
value={`${(winrate * 100).toFixed(2)}%`}
|
||||
delta={winrate - careerWinrate}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`WN8 • ${careerWN8 === undefined ? '--' : careerWN8.toFixed(0)}`}
|
||||
value={WN8 === undefined ? '--' : WN8.toFixed(0)}
|
||||
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Damage • ${careerDamage.toFixed(0)}`}
|
||||
value={damage.toFixed(0)}
|
||||
delta={damage - careerDamage}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Battles • ${careerBattles.toFixed(0)}`}
|
||||
value={battles.toFixed(0)}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
props.type === 'tank'
|
||||
? props.treeType === TreeTypeEnum.Collector
|
||||
? theme.colors.textLowContrast_blue
|
||||
: props.treeType === TreeTypeEnum.Premium
|
||||
? theme.colors.textLowContrast_amber
|
||||
: theme.colors.textHighContrast
|
||||
: theme.colors.textLowContrast,
|
||||
fontWeight: 900,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
{props.stats.map(
|
||||
(row, index) =>
|
||||
row && (
|
||||
<RowStat
|
||||
minimized={props.minimized}
|
||||
key={index}
|
||||
name={`${row.title} • ${row.career ?? '--'}`}
|
||||
value={row.current ?? '--'}
|
||||
delta={row.delta}
|
||||
percentile={row.percentile}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface RowDiscriminatorProps {
|
||||
name: string;
|
||||
icon?: string;
|
||||
minimized: boolean;
|
||||
}
|
||||
|
||||
export function RowDiscriminator({
|
||||
name,
|
||||
icon,
|
||||
minimized,
|
||||
}: RowDiscriminatorProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
width: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{!minimized && icon && (
|
||||
<img
|
||||
src={icon}
|
||||
height={0}
|
||||
style={{ flex: 1, width: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'clip',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import PercentileIndicator from '../../PercentileIndicator';
|
||||
|
||||
export interface RowStatProps {
|
||||
name: string;
|
||||
value: string | number;
|
||||
value?: string | number;
|
||||
delta?: number;
|
||||
percentile?: Percentile;
|
||||
minimized?: boolean;
|
||||
@@ -53,7 +53,9 @@ export function RowStat({
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
@@ -61,7 +63,13 @@ export function RowStat({
|
||||
</div>
|
||||
|
||||
{!minimized && (
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 12 }}>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './components/Root';
|
||||
export * from './components/Row';
|
||||
export * from './components/RowDiscriminator';
|
||||
export * from './components/RowStat';
|
||||
@@ -76,7 +76,10 @@ export function Card({ items, title }: CardProps) {
|
||||
<PercentileIndicator percentile={item.percentile} />
|
||||
)}
|
||||
<span
|
||||
style={{ fontSize: 16, color: theme.colors.textHighContrast }}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
>
|
||||
{item.value === undefined || Number.isNaN(item.value)
|
||||
? '--'
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function GenericAllStats({
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
title="Efficiency"
|
||||
title="Miscellaneous"
|
||||
items={[
|
||||
{
|
||||
label: 'Average tier',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { Margin, MarginOrientation } from './Margin';
|
||||
|
||||
export const GRAPH_WIDTH = 544;
|
||||
export const GRAPH_WIDTH = 384;
|
||||
export const GRAPH_HEIGHT = 320;
|
||||
|
||||
export interface MarginInputProps {
|
||||
@@ -20,7 +20,7 @@ export interface RootProps {
|
||||
separations?: number;
|
||||
xMinLabel: string;
|
||||
xMaxLabel: string;
|
||||
xTitle: string;
|
||||
xTitle?: string;
|
||||
}
|
||||
|
||||
export function Root({
|
||||
@@ -91,18 +91,20 @@ export function Root({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{xTitle}
|
||||
</span>
|
||||
</div>
|
||||
{xTitle !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{xTitle}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface GapProps {
|
||||
number: number;
|
||||
}
|
||||
|
||||
export function Gap({ number }: GapProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: theme.colors.componentNonInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
+ {number.toLocaleString()} more
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { RatingsReward } from '../../../commands/ratings';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface ItemProps {
|
||||
position: number;
|
||||
deltaPosition?: number;
|
||||
score: number;
|
||||
deltaScore?: number;
|
||||
reward?: RatingsReward;
|
||||
nickname: string;
|
||||
clan?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
function normalizeImage(url: string) {
|
||||
return url.endsWith('.webp') ? 'https://i.imgur.com/ZniCzbO.png' : url;
|
||||
}
|
||||
|
||||
export function Item({
|
||||
position,
|
||||
deltaPosition,
|
||||
score,
|
||||
deltaScore,
|
||||
reward,
|
||||
nickname,
|
||||
clan,
|
||||
highlight,
|
||||
}: ItemProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: highlight
|
||||
? theme.colors.componentInteractive_blue
|
||||
: theme.colors.componentInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{position.toLocaleString()}.
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{nickname}
|
||||
</span>
|
||||
{clan && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
[{clan}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{deltaPosition !== undefined && deltaPosition !== 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaPosition > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaPosition).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{reward && (
|
||||
<img
|
||||
style={{ width: 16, height: 16, objectFit: 'cover' }}
|
||||
src={normalizeImage(
|
||||
reward.type === 'vehicle'
|
||||
? reward.vehicle.image_url
|
||||
: reward.stuff.image_url,
|
||||
)}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
)}
|
||||
{reward && reward.count > 1 && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
x {reward.count.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
width: 96,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaScore).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<img
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaScore > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{score.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface RootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './components/Gap';
|
||||
export * from './components/Item';
|
||||
export * from './components/Root';
|
||||