Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06ae024c2c | ||
|
|
ce8fd0f22f | ||
|
|
7dc1aff1dc | ||
|
|
a60003127f | ||
|
|
7d1ba58806 | ||
|
|
963d71e221 | ||
|
|
2a804fda1c | ||
|
|
597a1a04d2 | ||
|
|
aec6f49ea1 | ||
|
|
e1ad710de5 | ||
|
|
edc8447617 | ||
|
|
f726d4f8e8 | ||
|
|
1f333f6b88 | ||
|
|
9b855fca47 | ||
|
|
c5f433fdf4 | ||
|
|
fd61dae1c1 | ||
|
|
2c45f9759f | ||
|
|
0d3261f748 | ||
|
|
d38e873800 | ||
|
|
ca38914095 | ||
|
|
ee5beeea45 | ||
|
|
a9d40d9f49 | ||
|
|
b8be916c18 | ||
|
|
24d0313fd3 | ||
|
|
c3d2a81327 | ||
|
|
dfeb117663 | ||
|
|
495a830506 | ||
|
|
a730f67ca5 | ||
|
|
9a3867770d | ||
|
|
f8a66bb5e7 | ||
|
|
4b00e1342e | ||
|
|
f9e0584f00 | ||
|
|
900c290e75 | ||
|
|
69e1577d18 | ||
|
|
a6d9044c3a | ||
|
|
9e23d60ef1 | ||
|
|
f12449119c | ||
|
|
533bfc4458 | ||
|
|
1317c2879c | ||
|
|
f9e51f894a | ||
|
|
6fdd4ba833 | ||
|
|
9495ae2336 | ||
|
|
43f634e198 | ||
|
|
ad51be7afc | ||
|
|
15b8bfdf8f | ||
|
|
cb8ae47be6 | ||
|
|
db846c0680 | ||
|
|
630a37025e | ||
|
|
d3bbc7f2b0 | ||
|
|
4e0321ca5f | ||
|
|
ec00bdf9de | ||
|
|
01dd7ad9c6 | ||
|
|
d726005c38 | ||
|
|
4f3e868367 | ||
|
|
ed8bab7df8 | ||
|
|
9bdb14e13a | ||
|
|
b827f7f730 | ||
|
|
07e3660388 | ||
|
|
970ab9004e | ||
|
|
862445259e | ||
|
|
e96538bae0 | ||
|
|
7447ccc487 | ||
|
|
bcd3a00f91 | ||
|
|
8c3675546f | ||
|
|
ec64dab720 | ||
|
|
db976c4ae9 | ||
|
|
0db96795b3 | ||
|
|
59bae96c1b | ||
|
|
d062a0da1b | ||
|
|
d2668974f7 | ||
|
|
78e4c82f20 | ||
|
|
f965452c5d | ||
|
|
be644bb3d9 | ||
|
|
97cc9509c0 | ||
|
|
6dc7a10c2e | ||
|
|
19298b9ad6 | ||
|
|
97d427a6a8 | ||
|
|
bff6eb0503 | ||
|
|
dc9ab2067d | ||
|
|
e8e1f7bafb | ||
|
|
e50a456025 | ||
|
|
d442bbca1a | ||
|
|
92e35bd0ac | ||
|
|
cb9d283f62 | ||
|
|
8aba19e05b | ||
|
|
b7b4035492 | ||
|
|
3667fd82b7 | ||
|
|
691d5cde27 | ||
|
|
7f431b07e1 | ||
|
|
8264f19ad4 | ||
|
|
e3864b86a4 | ||
|
|
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 | ||
|
|
1979047bbc | ||
|
|
8860c1b0f8 |
No files matched your search
@@ -47,7 +47,7 @@ jobs:
|
|||||||
- name: Upload pages artifact
|
- name: Upload pages artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v1
|
||||||
with:
|
with:
|
||||||
path: docs/.vitepress/dist
|
path: dist/docs
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: CD Host
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */5 * * *'
|
- cron: '0 */6 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -29,4 +29,4 @@ jobs:
|
|||||||
unzip bot.zip
|
unzip bot.zip
|
||||||
|
|
||||||
- name: Run bot
|
- name: Run bot
|
||||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot/index.cjs
|
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: CI Build
|
name: CI Build Bot
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
@@ -34,10 +34,10 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build
|
run: yarn build:bot
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: bot
|
||||||
path: dist
|
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,10 +7,10 @@ package-lock.json
|
|||||||
dist/
|
dist/
|
||||||
dist.*
|
dist.*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
commands.txt
|
|
||||||
/test.*
|
/test.*
|
||||||
test.*
|
test.*
|
||||||
/docs/.vitepress/dist
|
/docs/.vitepress/dist
|
||||||
/docs/.vitepress/cache
|
/docs/.vitepress/cache
|
||||||
servers.csv
|
servers.csv
|
||||||
.next/
|
.next/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
tasks:
|
|
||||||
- init: yarn
|
|
||||||
- command: yarn run pw
|
|
||||||
@@ -5,13 +5,18 @@
|
|||||||
"bepaint",
|
"bepaint",
|
||||||
"blitzstars",
|
"blitzstars",
|
||||||
"cleandist",
|
"cleandist",
|
||||||
|
"cockroackdb",
|
||||||
"customperiod",
|
"customperiod",
|
||||||
|
"deburr",
|
||||||
|
"diacriticless",
|
||||||
"Everytime",
|
"Everytime",
|
||||||
"fullstats",
|
"fullstats",
|
||||||
"fuzzysort",
|
"fuzzysort",
|
||||||
"kranvagn",
|
"kranvagn",
|
||||||
"Leaderboard",
|
"Leaderboard",
|
||||||
"leaderboards",
|
"leaderboards",
|
||||||
|
"Oopsy",
|
||||||
|
"outdir",
|
||||||
"outfile",
|
"outfile",
|
||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
@@ -39,7 +44,9 @@
|
|||||||
"vitepress",
|
"vitepress",
|
||||||
"wargaming",
|
"wargaming",
|
||||||
"webp",
|
"webp",
|
||||||
"winrate"
|
"winrate",
|
||||||
|
"wotblitz",
|
||||||
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
"deno.unstable": 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
|
```bash
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running
|
### Create `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
GH_TOKEN={YOUR_GITHUB_TOKEN_HERE}
|
||||||
|
DISCORD_TOKEN={YOUR_DISCORD_TOKE_HERE}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start discord-token=<token> wargaming-application-id=<id>
|
yarn dev:bot
|
||||||
```
|
```
|
||||||
@@ -8,5 +8,7 @@
|
|||||||
"sklld_peasant_role": "896878529512370228",
|
"sklld_peasant_role": "896878529512370228",
|
||||||
|
|
||||||
"tres_guild_id": "734786591205359697",
|
"tres_guild_id": "734786591205359697",
|
||||||
"tres_log_channel": "1109262265850544148"
|
"tres_log_channel": "1109262265850544148",
|
||||||
|
|
||||||
|
"test_guild_id": "1124729294829142116"
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
|||||||
title: 'blitzkrieg',
|
title: 'blitzkrieg',
|
||||||
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
description: '🎉 All-in-one Discord bot for everything World of Tanks Blitz',
|
||||||
base: '/blitzkrieg/',
|
base: '/blitzkrieg/',
|
||||||
|
outDir: '../dist/docs',
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
nav: [
|
nav: [
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.13.3
|
||||||
|
|
||||||
|
Streamer's delight!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added config context menu for the session tracker embed
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Blitzkrieg 1.14.0
|
||||||
|
|
||||||
|
Elegance has never been met like this before
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `/link`
|
||||||
|
- Unlike `/verify`, `/link` links you Discord and Blitz accounts across all servers
|
||||||
|
- This command does not change your nickname
|
||||||
|
- `/link` now simply displays a deprecation message
|
||||||
|
- Usernames can still be inputted per-command to override your linked account
|
||||||
|
- `/stats` and `/full-stats` are now way simpler
|
||||||
|
- `/stats <type> <period>` has been replaced with `/stats <30|60|90|...>` (likewise, `/full-stats`)
|
||||||
|
- These commands now have filters without having to opt into `multi-tank`
|
||||||
|
- Added `/breakdown`
|
||||||
|
- Unlike `/today`, `/breakdown` lets you select any period of time
|
||||||
|
- `/breakdown` now simply displays a deprecation message
|
||||||
|
- `/about commands` are now sorted alphabetically
|
||||||
|
- Added a `/reset` button to the options dropdown and embed context-menu
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Simplified no ongoing ratings season edge case
|
||||||
|
- Updated welcome message for Skilled server to use `/link` and other up-to-date information
|
||||||
|
- Tier weights are now reported in Arabic numerals instead of Roman
|
||||||
|
- Removed tank images from `/owned-tanks`
|
||||||
|
- Wargaming messed up their API and almost no tanks have any images anymore
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Banners now use a more economical `.webp` format just like the home page
|
||||||
|
- Removed unused color palettes
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Guild commands are now handled more efficiently
|
||||||
|
- Removed nickname-based Blitz username inference
|
||||||
|
- Code split a lot of common functionalities
|
||||||
|
- Re-arranged `core` folder
|
||||||
|
- Added midnight and latest ratings leaderboard caches
|
||||||
|
- Merged renderers into their primary user files
|
||||||
|
- Added sharding
|
||||||
|
- Disabled for now but good for future proofing
|
||||||
|
- Switched to a way cleaner logging schema
|
||||||
|
- Hosting is now done in 6 hour periods instead of 5
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Blitzkrieg 1.14.1
|
||||||
|
|
||||||
|
Oopsy daisy, a memory leak!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Simplified message for users to use `/link`
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed a memory leak in the render thread manager
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Added `UserError` extending `Error`
|
||||||
|
- This lets the bot avoid spamming the debug console for mistakes users make
|
||||||
|
- Reduced render threads to 2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Blitzkrieg 1.14.2
|
||||||
|
|
||||||
|
A late night hot fix for my homies
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Clan info is now fetched using `clan_id` instead of `account_id`
|
||||||
|
- Generic stats row names are now bounded by a max width
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Blitzkrieg 1.14.3
|
||||||
|
|
||||||
|
Pull your brush out and start polishing.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Rows in `/breakdown` are now chunked into `8` per image
|
||||||
|
- This is true unless the number of pages exceeds `10` in which case the rows are even distributed across all `10` pages
|
||||||
|
- `10` is the maximum number of images that Discord allows
|
||||||
|
- Short region names are used in username autocomplete instead of their full names
|
||||||
|
- Example: "North America" is "NA" now
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed reference to username inference in the username option description
|
||||||
|
- Removed an unnecessary type `asd` that did absolutely nothing used nowhere
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Image orders are now preserved even if processed in parallel and all completed out of order
|
||||||
|
- `fetchBlitz` is now throttled to `10` requests per second
|
||||||
|
- Request overflows are managed by queueing
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.14.4
|
||||||
|
|
||||||
|
Streamers get some knowledge today
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added a video in the embed button prompt
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
# Embeds
|
# 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
|
## Creating Embeds
|
||||||
|
|
||||||
1. Use a command of your liking (for example, `/stats`).
|
1. Use a command or tool and click on "Embed" if it's available
|
||||||
2. Customize the command however you like and the URL generated for the embed will match your needs.
|
2. Make sure to tweak the settings of the command or tool because embeds replicate everything
|
||||||
3. Send the command and when the bot is "done thinking," an "Embed" button will appear below the bot's reply.
|
|
||||||
|
|
||||||
## Using Embeds in Streaming Softwares
|
## Using Embeds in Streaming Softwares
|
||||||
|
|
||||||
1. Click on the "Embed" button.
|
1. Click on the "Embed" button
|
||||||
2. Copy the URL from your browser.
|
- If this was generated by a command, copy the URL from the browser
|
||||||
3. Paste the URL into a Browser Source.
|
- If this was generated by a tool, the URL has been copied to your clipboard already
|
||||||
4. Set the refresh rate/FPS to something very low like `0.0167` (that's 1 frame every 60 seconds).
|
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
|
## How Embeds Behave
|
||||||
|
|
||||||
- They load like any normal website albeit with a significant delay.
|
- They load like any normal website
|
||||||
- They auto refresh every `60s`.
|
- They auto refresh every `1s`
|
||||||
- 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.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# About
|
# Introduction
|
||||||
|
|
||||||
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
|
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz.
|
||||||
|
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
The following Discord permissions are required by the bot to ensure full functionality:
|
The following Discord permissions are required by the bot to ensure full functionality:
|
||||||
|
|
||||||
|
- Manage nicknames
|
||||||
|
- Read messages / view channels
|
||||||
- Attach files
|
- Attach files
|
||||||
- View channels
|
|
||||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 183 KiB |
@@ -1,6 +1,11 @@
|
|||||||
|
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';
|
||||||
|
|
||||||
|
const withVanillaExtract = createVanillaExtractPlugin();
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
/* config options here */
|
...withVanillaExtract(),
|
||||||
|
|
||||||
distDir: 'dist/website',
|
distDir: 'dist/website',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
"start": "node --enable-source-maps server.cjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.12.7",
|
"version": "1.14.4",
|
||||||
"description": "🎉 All-in-one Discord bot for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build && yarn build:website",
|
"build:bot": "vite-node --script scripts/build",
|
||||||
"build:bot": "node scripts/build --build=bot",
|
|
||||||
"build:server": "node scripts/build --build=server",
|
|
||||||
"build:website": "next build",
|
"build:website": "next build",
|
||||||
"build:docs": "vitepress build docs",
|
"build:docs": "vitepress build docs",
|
||||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||||
"dev:server": "yarn build:server && yarn start:server",
|
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"dev:website": "next dev",
|
"dev:website": "next dev",
|
||||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs",
|
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
||||||
"start:server": "node --enable-source-maps --trace-warnings dist/server/index.cjs",
|
|
||||||
"start:website": "next start",
|
"start:website": "next start",
|
||||||
"host:docs": "vitepress preview docs",
|
"host:docs": "vitepress preview docs",
|
||||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||||
@@ -30,34 +26,44 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/rest": "^20.0.1",
|
"@octokit/rest": "^20.0.1",
|
||||||
"@radix-ui/colors": "^2.1.0",
|
"@prisma/client": "^5.4.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",
|
"@resvg/resvg-js": "^2.4.1",
|
||||||
|
"@vanilla-extract/css": "^1.13.0",
|
||||||
"bepaint": "^0.1.0-beta.3",
|
"bepaint": "^0.1.0-beta.3",
|
||||||
"discord.js": "^14.13.0",
|
"discord.js": "^14.13.0",
|
||||||
"esbuild": "^0.19.2",
|
"dotenv": "^16.3.1",
|
||||||
|
"esbuild": "^0.19.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
|
"immer": "^10.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-escape": "^2.0.0",
|
"markdown-escape": "^2.0.0",
|
||||||
"next": "^13.4.19",
|
"next": "^13.5.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"satori": "^0.10.4",
|
"react-fuzzysort": "^1.0.1",
|
||||||
"vite-node": "^0.34.4",
|
"satori": "^0.10.8",
|
||||||
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
"vite-node": "^0.34.5",
|
||||||
|
"vitepress": "^1.0.0-rc.20",
|
||||||
|
"vitepress-plugin-auto-sidebar": "^1.1.0",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@total-typescript/ts-reset": "^0.5.1",
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/escape-html": "^1.0.2",
|
"@types/escape-html": "^1.0.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.18",
|
||||||
"@types/lodash": "^4.14.198",
|
"@types/lodash": "^4.14.199",
|
||||||
"@types/markdown-escape": "^1.1.0",
|
"@types/markdown-escape": "^1.1.0",
|
||||||
"@types/node": "^20.6.0",
|
"@types/node": "^20.6.5",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.22",
|
||||||
|
"@vanilla-extract/next-plugin": "^2.3.1",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"knip": "^2.22.0",
|
"knip": "^2.27.1",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^5.2.2",
|
"prisma": "^5.4.1",
|
||||||
"vitepress": "^1.0.0-rc.10"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "discord_blitz" (
|
||||||
|
"discord" INT8 NOT NULL,
|
||||||
|
"region" STRING NOT NULL,
|
||||||
|
"blitz" INT4 NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "cockroachdb"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "cockroachdb"
|
||||||
|
url = env("DISCORD_BLITZ_DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
model discord_blitz {
|
||||||
|
discord BigInt @id
|
||||||
|
region String
|
||||||
|
blitz Int
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 177 KiB |
@@ -1,33 +0,0 @@
|
|||||||
import { build } from 'esbuild';
|
|
||||||
import { argv } from 'process';
|
|
||||||
|
|
||||||
const targetFile = argv
|
|
||||||
.find((arg) => arg.startsWith('--build='))
|
|
||||||
?.split('=')[1];
|
|
||||||
const files = targetFile ? [targetFile] : ['bot', 'server'];
|
|
||||||
|
|
||||||
files.forEach(async (file) => {
|
|
||||||
console.log(`Building ${file}...`);
|
|
||||||
|
|
||||||
await build({
|
|
||||||
entryPoints: [`src/${file}.ts`],
|
|
||||||
outfile: `dist/${file}/index.cjs`,
|
|
||||||
|
|
||||||
platform: 'node',
|
|
||||||
loader: {
|
|
||||||
'.node': 'copy',
|
|
||||||
'.ttf': 'file',
|
|
||||||
},
|
|
||||||
format: 'cjs',
|
|
||||||
|
|
||||||
bundle: true,
|
|
||||||
sourcemap: true,
|
|
||||||
minifyIdentifiers: false, // cause goofy errors
|
|
||||||
minifySyntax: true,
|
|
||||||
minifyWhitespace: true,
|
|
||||||
|
|
||||||
logLevel: 'info',
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Built ${file}`);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { build } from 'esbuild';
|
||||||
|
import { cp, readdir } from 'fs/promises';
|
||||||
|
|
||||||
|
readdir('src/workers').then((workers) => {
|
||||||
|
const files = [
|
||||||
|
'bot',
|
||||||
|
'index',
|
||||||
|
|
||||||
|
...workers.map((file) => `workers/${file.replace('.ts', '')}`),
|
||||||
|
];
|
||||||
|
|
||||||
|
build({
|
||||||
|
entryPoints: files.map((file) => `src/${file}.ts`),
|
||||||
|
outdir: 'dist/bot',
|
||||||
|
|
||||||
|
platform: 'node',
|
||||||
|
tsconfig: 'tsconfig.esbuild.json',
|
||||||
|
loader: {
|
||||||
|
'.node': 'copy',
|
||||||
|
'.ttf': 'file',
|
||||||
|
},
|
||||||
|
format: 'cjs',
|
||||||
|
jsx: 'transform',
|
||||||
|
outExtension: {
|
||||||
|
'.js': '.cjs',
|
||||||
|
},
|
||||||
|
|
||||||
|
bundle: true,
|
||||||
|
sourcemap: true,
|
||||||
|
minifyIdentifiers: false, // causes goofy errors
|
||||||
|
minifySyntax: true,
|
||||||
|
minifyWhitespace: true,
|
||||||
|
|
||||||
|
logLevel: 'info',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// copy schema.prisma
|
||||||
|
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||||
|
readdir('node_modules/prisma').then((files) =>
|
||||||
|
files
|
||||||
|
.filter((file) => file.endsWith('.node'))
|
||||||
|
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
||||||
|
);
|
||||||
@@ -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/blitzstars/tankopedia';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json(await tankopedia);
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ContextMenu } from '@radix-ui/themes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import * as Breakdown from '../../../components/Breakdown';
|
||||||
|
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||||
|
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||||
|
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||||
|
import {
|
||||||
|
TankopediaEntry,
|
||||||
|
tankopedia,
|
||||||
|
} from '../../../core/blitzstars/tankopedia';
|
||||||
|
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
||||||
|
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
||||||
|
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
||||||
|
import { resetSession, useSession } from '../../../stores/session';
|
||||||
|
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
||||||
|
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
||||||
|
import { Menu } from '../../tools/session/components/Menu';
|
||||||
|
|
||||||
|
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 fetchBlitz<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(
|
||||||
|
deltaTankStats(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 (
|
||||||
|
<ContextMenu.Root>
|
||||||
|
<ContextMenu.Trigger>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
|
<Menu reset={resetSession} Builder={ContextMenu} />
|
||||||
|
</ContextMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,30 +1,56 @@
|
|||||||
|
'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 { ReactNode } from 'react';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
interface RootLayoutProps {
|
interface RootLayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const robotoFlex = Roboto_Flex({
|
||||||
|
subsets: ['latin'],
|
||||||
|
display: 'swap',
|
||||||
|
});
|
||||||
|
|
||||||
export default function RootLayout({ children }: RootLayoutProps) {
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isEmbed = pathname.split('/')[1] === 'embeds';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html>
|
<html lang="en" className={robotoFlex.className}>
|
||||||
|
<head>
|
||||||
|
<title>Blitzkrieg</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="🎉 Tools for everything World of Tanks Blitz"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
background: isEmbed
|
||||||
|
? 'transparent'
|
||||||
|
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<Theme
|
||||||
style={{
|
appearance="dark"
|
||||||
width: '100vw',
|
panelBackground="translucent"
|
||||||
height: '100vh',
|
radius="full"
|
||||||
display: 'flex',
|
accentColor="blue"
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Navbar />
|
{!isEmbed && <Navbar />}
|
||||||
|
|
||||||
{children}
|
<Flex direction="column">{children}</Flex>
|
||||||
</div>
|
</Theme>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,3 +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() {
|
export default function Page() {
|
||||||
return <h1>Hello, Next.js!</h1>;
|
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,69 @@
|
|||||||
|
'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,
|
||||||
|
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
backgroundImage: `url(/assets/banners/${toolId}.webp)`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
filter: 'blur(4px)',
|
||||||
|
transform: 'scale(calc((128 + 8) / 128))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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,63 @@
|
|||||||
|
import { ContextMenu, 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 {
|
||||||
|
Builder: typeof DropdownMenu | typeof ContextMenu;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomColumn({ column, Builder }: CustomColumn) {
|
||||||
|
const customColumn = useSession((state) => state.customColumns[column]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Builder.Sub>
|
||||||
|
<Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger>
|
||||||
|
|
||||||
|
<Builder.SubContent>
|
||||||
|
<Builder.Label>Display</Builder.Label>
|
||||||
|
|
||||||
|
<Builder.RadioGroup
|
||||||
|
value={customColumn.display}
|
||||||
|
onValueChange={(event) =>
|
||||||
|
mutateSession((draft) => {
|
||||||
|
draft.customColumns[column].display =
|
||||||
|
event as CustomColumnDisplay;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Builder.RadioItem value="battles">Battles</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="winrate">Winrate</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="wn8">WN8</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="damage">Damage</Builder.RadioItem>
|
||||||
|
<Builder.RadioItem value="none">None</Builder.RadioItem>
|
||||||
|
</Builder.RadioGroup>
|
||||||
|
|
||||||
|
{!NO_DELTA.includes(customColumn.display) && (
|
||||||
|
<>
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={customColumn.showDelta}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
mutateSession((draft) => {
|
||||||
|
draft.customColumns[column].showDelta = checked;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show delta
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Builder.SubContent>
|
||||||
|
</Builder.Sub>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
|
||||||
|
import { useSession } from '../../../../stores/session';
|
||||||
|
import { CustomColumn } from './CustomColumn';
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
Builder: typeof DropdownMenu | typeof ContextMenu;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Menu({ Builder, reset }: ContextMenuProps) {
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Builder.Content>
|
||||||
|
<CustomColumn Builder={Builder} column={0} />
|
||||||
|
<CustomColumn Builder={Builder} column={1} />
|
||||||
|
<CustomColumn Builder={Builder} column={2} />
|
||||||
|
<CustomColumn Builder={Builder} column={3} />
|
||||||
|
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={session.showTotal}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({ showTotal: checked })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show total
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={session.showCareer}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({ showCareer: checked })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show career
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.Item color="red" onClick={reset}>
|
||||||
|
Reset
|
||||||
|
</Builder.Item>
|
||||||
|
</Builder.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,254 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CaretDownIcon,
|
||||||
|
CopyIcon,
|
||||||
|
PersonIcon,
|
||||||
|
ReloadIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
DropdownMenu,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
} from '@radix-ui/themes';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
|
import { REGION_NAMES } from '../../../constants/regions';
|
||||||
|
import searchPlayersAcrossRegions, {
|
||||||
|
AccountListWithServer,
|
||||||
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||||
|
import { resetSession, setSession, useSession } from '../../../stores/session';
|
||||||
|
import SessionPage from '../../embeds/session/page';
|
||||||
|
import { Menu } from './components/Menu';
|
||||||
|
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 searchPlayersAcrossRegions(event.target.value));
|
||||||
|
} else {
|
||||||
|
setSearchResults(undefined);
|
||||||
|
setShowSearchResults(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session.isTracking) input.current!.value = session.nickname;
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
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.resetPrompt) {
|
||||||
|
event.preventDefault();
|
||||||
|
resetSession();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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({
|
||||||
|
resetPrompt: !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={resetSession}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger>
|
||||||
|
<Button
|
||||||
|
variant="soft"
|
||||||
|
className={styles.toolbarButton}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!session.embedPrompt) {
|
||||||
|
event.preventDefault();
|
||||||
|
resetSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${location.origin}/embeds/session`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon width="16" height="16" /> Embed
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Copied link to your clipboard
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
Instructions for streamers:
|
||||||
|
<iframe
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
|
||||||
|
title="YouTube video player"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
></iframe>
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<Text size="2">
|
||||||
|
<Checkbox
|
||||||
|
mr="1"
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({
|
||||||
|
embedPrompt: !checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Don't show this again
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4">
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
<Button variant="soft">Got it!</Button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button variant="soft" className={styles.toolbarButton}>
|
||||||
|
Options
|
||||||
|
<CaretDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<Menu Builder={DropdownMenu} reset={resetSession} />
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SessionPage />
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { registerProcesses } from './core/blitzkrieg/processes';
|
||||||
|
import { secrets } from './core/blitzkrieg/secrets';
|
||||||
import { client } from './core/discord/client';
|
import { client } from './core/discord/client';
|
||||||
import { secrets } from './core/node/secrets';
|
|
||||||
import { registerErrorHandlers } from './events/error';
|
|
||||||
|
|
||||||
registerErrorHandlers();
|
// console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
|
||||||
|
|
||||||
|
registerProcesses();
|
||||||
client.login(secrets.DISCORD_TOKEN);
|
client.login(secrets.DISCORD_TOKEN);
|
||||||
@@ -8,14 +8,14 @@ const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/doc
|
|||||||
const DOCS = {
|
const DOCS = {
|
||||||
permissions: 'guide/permissions',
|
permissions: 'guide/permissions',
|
||||||
embeds: 'guide/embeds',
|
embeds: 'guide/embeds',
|
||||||
about: 'guide/about',
|
introduction: 'guide/introduction',
|
||||||
invite: 'guide/invite',
|
invite: 'guide/invite',
|
||||||
timezones: 'guide/timezones',
|
timezones: 'guide/timezones',
|
||||||
};
|
};
|
||||||
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
const DOC_DESCRIPTIONS: Record<keyof typeof DOCS, string> = {
|
||||||
permissions: 'The permissions needed for the bot to function',
|
permissions: 'The permissions needed for the bot to function',
|
||||||
embeds: 'How embeds work',
|
embeds: 'How embeds work',
|
||||||
about: 'About the bot',
|
introduction: 'About the bot',
|
||||||
invite: 'How to invite the bot',
|
invite: 'How to invite the bot',
|
||||||
timezones: 'How Blitzkrieg infers and uses your timezone',
|
timezones: 'How Blitzkrieg infers and uses your timezone',
|
||||||
};
|
};
|
||||||
@@ -28,14 +28,14 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
|||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const helpCommand: CommandRegistry = {
|
export const aboutCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: addDocsSubcommands(
|
command: addDocsSubcommands(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('help')
|
.setName('about')
|
||||||
.setDescription('All the help you need about the bot')
|
.setDescription('All the info you need about the bot')
|
||||||
.addSubcommand((option) =>
|
.addSubcommand((option) =>
|
||||||
option
|
option
|
||||||
.setName('commands')
|
.setName('commands')
|
||||||
@@ -51,6 +51,7 @@ export const helpCommand: CommandRegistry = {
|
|||||||
await Promise.all(COMMANDS_RAW)
|
await Promise.all(COMMANDS_RAW)
|
||||||
)
|
)
|
||||||
.filter((registry) => registry.inPublic && registry.inProduction)
|
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||||
|
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
|
||||||
.map(
|
.map(
|
||||||
(registry) =>
|
(registry) =>
|
||||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { chunk } from 'lodash';
|
||||||
|
import * as Breakdown from '../components/Breakdown';
|
||||||
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
|
import addFilterOptions from '../core/discord/addFilterOptions';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import buttonPrimary from '../core/discord/buttonPrimary';
|
||||||
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
|
import resolvePlayerFromCommand, {
|
||||||
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
|
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
||||||
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
const ROWS_PER_PAGE = 8;
|
||||||
|
const MAX_PAGES = 10;
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const awaitedTankopedia = await tankopedia;
|
||||||
|
const awaitedTankAverages = await tankAverages;
|
||||||
|
const statsInPeriod = await getStatsInPeriod(region, id, start, end);
|
||||||
|
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clanData = await getClanAccountInfo(region, id, ['clan']);
|
||||||
|
const tankStats = await getTankStats(region, id);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters);
|
||||||
|
const orderedCurrentStats: AllStats[] = [];
|
||||||
|
const orderedCareerStats: AllStats[] = [];
|
||||||
|
const orderedCurrentWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8Full: (number | undefined)[] = [];
|
||||||
|
|
||||||
|
filteredOrder.forEach((id) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages[id];
|
||||||
|
const current = statsInPeriod.diff[id];
|
||||||
|
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
|
||||||
|
const currentWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, current)
|
||||||
|
: undefined;
|
||||||
|
const careerWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, career)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
orderedCurrentStats.push(current);
|
||||||
|
orderedCareerStats.push(career);
|
||||||
|
orderedCurrentWN8.push(currentWN8);
|
||||||
|
orderedCareerWN8.push(careerWN8);
|
||||||
|
});
|
||||||
|
tankStats.forEach((tank) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages[tank.tank_id];
|
||||||
|
|
||||||
|
orderedCareerWN8Full.push(
|
||||||
|
tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, tank.all)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentBattles = orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const careerBattles = tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const currentWinrate =
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.wins,
|
||||||
|
0,
|
||||||
|
) / currentBattles;
|
||||||
|
const careerWinrate =
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.wins,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
const currentWN8 =
|
||||||
|
orderedCurrentWN8.reduce<number>(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
|
||||||
|
0,
|
||||||
|
) /
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator +
|
||||||
|
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const careerWN8 =
|
||||||
|
orderedCareerWN8Full.reduce<number>(
|
||||||
|
(accumulator, current) => accumulator + (current ?? 0),
|
||||||
|
0,
|
||||||
|
) /
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator +
|
||||||
|
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const currentDamage =
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / currentBattles;
|
||||||
|
const careerDamage =
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
|
||||||
|
const children: JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (filteredOrder.length > 0) {
|
||||||
|
children.push(
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
type="summary"
|
||||||
|
title="Total"
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: 'Battles',
|
||||||
|
current: currentBattles,
|
||||||
|
career: careerBattles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Winrate',
|
||||||
|
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
||||||
|
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||||
|
delta: currentWinrate - careerWinrate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WN8',
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Damage',
|
||||||
|
current: Math.round(currentDamage).toLocaleString(),
|
||||||
|
career: Math.round(careerDamage).toLocaleString(),
|
||||||
|
delta: currentDamage - careerDamage,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
filteredOrder.map(async (id, index) => {
|
||||||
|
const tankopediaEntry = awaitedTankopedia[id];
|
||||||
|
const current = orderedCurrentStats[index];
|
||||||
|
const career = orderedCareerStats[index];
|
||||||
|
const currentWN8 = orderedCurrentWN8[index];
|
||||||
|
const careerWN8 = orderedCareerWN8[index];
|
||||||
|
|
||||||
|
children.push(
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
type="tank"
|
||||||
|
tankType={tankopediaEntry?.type}
|
||||||
|
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||||
|
title={await resolveTankName(id)}
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: 'Battles',
|
||||||
|
current: current.battles.toLocaleString(),
|
||||||
|
career: career.battles.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Winrate',
|
||||||
|
current: `${(100 * (current.wins / current.battles)).toFixed(
|
||||||
|
2,
|
||||||
|
)}%`,
|
||||||
|
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
|
||||||
|
delta:
|
||||||
|
current.wins / current.battles - career.wins / career.battles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WN8',
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Damage',
|
||||||
|
current: Math.round(
|
||||||
|
current.damage_dealt / current.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
career: Math.round(
|
||||||
|
career.damage_dealt / career.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
delta:
|
||||||
|
current.damage_dealt / current.battles -
|
||||||
|
career.damage_dealt / career.battles,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = chunk(
|
||||||
|
children,
|
||||||
|
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredOrder.length > 0) {
|
||||||
|
return pages
|
||||||
|
.map((page, index) => (
|
||||||
|
<Wrapper>
|
||||||
|
{index === 0 && (
|
||||||
|
<TitleBar
|
||||||
|
name={accountInfo.nickname}
|
||||||
|
image={
|
||||||
|
clanData?.clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Breakdown.Root>{page}</Breakdown.Root>
|
||||||
|
</Wrapper>
|
||||||
|
))
|
||||||
|
.reverse();
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={accountInfo.nickname}
|
||||||
|
image={
|
||||||
|
clanData?.clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{filteredOrder.length === 0 && (
|
||||||
|
<NoData type={NoDataType.BattlesInPeriod} />
|
||||||
|
)}
|
||||||
|
</Wrapper>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
||||||
|
async (resolve) => {
|
||||||
|
const command = await addFilterOptions(
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('breakdown')
|
||||||
|
.setDescription("A period's breakdown by tanks played"),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(await render(player, period, filters)),
|
||||||
|
buttonPrimary(path, 'Refresh'),
|
||||||
|
await getBlitzStarsLinkButton(player.region, player.id),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
|
return await render(player, period, filters);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,97 +1,93 @@
|
|||||||
import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
|
// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
|
||||||
import markdownEscape from 'markdown-escape';
|
// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
|
||||||
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 { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
import { AccountAchievements } from '../types/accountAchievements';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { ClanInfo } from '../types/clanInfo';
|
|
||||||
|
|
||||||
export const clanEventCommand: CommandRegistry = {
|
// import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
|
||||||
inProduction: true,
|
// import markdownEscape from 'markdown-escape';
|
||||||
inPublic: true,
|
// import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
||||||
|
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
// import { getClanInfo } from '../core/blitz/getClanInfo';
|
||||||
|
// import { PlayerHistoriesRaw } from '../LEGACY_core/blitzstars/getPlayerHistories';
|
||||||
|
// import getTimeDaysAgo from '../LEGACY_core/blitzstars/getTimeDaysAgo';
|
||||||
|
// import addClanChoices from '../LEGACY_core/discord/addClanChoices';
|
||||||
|
// import autocompleteClan from '../LEGACY_core/discord/autocompleteClan';
|
||||||
|
// import embedWarning from '../LEGACY_core/discord/embedWarning';
|
||||||
|
// import resolveClanFromCommand from '../LEGACY_core/discord/resolveClanFromCommand';
|
||||||
|
// import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
// export const clanEventCommand: CommandRegistry = {
|
||||||
.setName('clan-event')
|
// inProduction: false,
|
||||||
.setDescription('Lists all inactive players')
|
// inPublic: true,
|
||||||
.addStringOption(addClanChoices),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
// command: new SlashCommandBuilder()
|
||||||
const { region, id } = await resolveClanFromCommand(interaction);
|
// .setName('clan-event')
|
||||||
const clan = (
|
// .setDescription('Lists all inactive players')
|
||||||
await getWargamingResponse<ClanInfo>(
|
// .addStringOption(addClanChoices),
|
||||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${secrets.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=${secrets.WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
|
|
||||||
),
|
|
||||||
getWargamingResponse<AccountAchievements>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/achievements/?application_id=${secrets.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 [
|
// async handler(interaction) {
|
||||||
`# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
|
// const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
clan.tag,
|
// const clan = await getClanInfo(region, id);
|
||||||
)}]'s platoon wins today: ${(
|
// const [players, achievements, previousJointVictories] = await Promise.all([
|
||||||
jointVictories.reduce(
|
// getAccountInfo(region, clan.members_ids),
|
||||||
(accumulator, { joinVictory }) => accumulator + joinVictory,
|
// getAccountAchievements(region, clan.members_ids),
|
||||||
0,
|
// Promise.all(
|
||||||
) / 2
|
// clan.members_ids.map((id) =>
|
||||||
).toFixed(0)}\n${jointVictories
|
// fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
|
||||||
.filter(({ joinVictory }) => joinVictory)
|
// .then((response) => response.json() as Promise<PlayerHistoriesRaw>)
|
||||||
.sort((a, b) => b.joinVictory - a.joinVictory)
|
// .then((data) => ({
|
||||||
.map(
|
// id,
|
||||||
({ id, joinVictory }) =>
|
// jointVictory: data[0]?.achievements.jointVictoryCount as
|
||||||
`- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
|
// | number
|
||||||
)
|
// | undefined,
|
||||||
.join('\n')}`,
|
// })),
|
||||||
|
// ),
|
||||||
|
// ).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,
|
||||||
|
// }));
|
||||||
|
|
||||||
embedWarning(
|
// return [
|
||||||
'This is an approximation!',
|
// `# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
|
||||||
"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 `/help timezones` for more info)",
|
// 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')}`,
|
||||||
|
|
||||||
autocomplete: autocompleteClan,
|
// 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,11 +1,11 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
import getClientId from '../core/blitzkrieg/getClientId';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
import { client } from '../core/discord/client';
|
import { client } from '../core/discord/client';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable from '../core/discord/markdownTable';
|
import markdownTable from '../core/discord/markdownTable';
|
||||||
import getClientId from '../core/node/getClientId';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
const executionStart = new Date().getTime();
|
const executionStart = new Date().getTime();
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
||||||
import sumStats from '../core/blitz/sumStats';
|
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
|
|
||||||
import getPeriodNow from '../core/blitzstars/getPeriodNow';
|
|
||||||
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
|
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import embedPositive from '../core/discord/embedPositive';
|
import embedPositive from '../core/discord/embedPositive';
|
||||||
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
|
import sumStats from '../core/statistics/sumStats';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||||
|
|
||||||
export type SkilledClan = 'SKLLD' | 'SMRI';
|
export type SkilledClan = 'SKLLD' | 'SMRI';
|
||||||
@@ -48,20 +46,18 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const clan = interaction.options.getString('clan') as SkilledClan;
|
const clan = interaction.options.getString('clan') as SkilledClan;
|
||||||
const { id, region: server } = await resolvePlayerFromCommand(interaction);
|
const { id, region: region } = await resolvePlayerFromCommand(interaction);
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const problems: TableInput = [];
|
const problems: TableInput = [];
|
||||||
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
||||||
let isEligible = false;
|
let isEligible = false;
|
||||||
let body = '';
|
let body = '';
|
||||||
|
|
||||||
if (clan === 'SKLLD') {
|
if (clan === 'SKLLD') {
|
||||||
const { diffed } = await getDiffedTankStats(
|
const { diff: diffed } = await getStatsInPeriod(
|
||||||
server,
|
region,
|
||||||
id,
|
id,
|
||||||
getTimeDaysAgo(server, 30),
|
getTimeDaysAgo(region, 30),
|
||||||
getPeriodNow(),
|
getPeriodNow(),
|
||||||
);
|
);
|
||||||
const entries = Object.entries(diffed);
|
const entries = Object.entries(diffed);
|
||||||
@@ -171,7 +167,7 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
problems,
|
problems,
|
||||||
)}`;
|
)}`;
|
||||||
} else if (clan === 'SMRI') {
|
} else if (clan === 'SMRI') {
|
||||||
const stats = accountInfo[id].statistics.all;
|
const stats = accountInfo.statistics.all;
|
||||||
|
|
||||||
if (stats.battles < 1500) {
|
if (stats.battles < 1500) {
|
||||||
problems.push([
|
problems.push([
|
||||||
|
|||||||
@@ -1,31 +1,142 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import {
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
SlashCommandBuilder,
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
SlashCommandSubcommandGroupBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import * as Graph from '../components/Graph';
|
||||||
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import getTankHistories from '../core/blitzkrieg/getTankHistories';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
||||||
|
import addPeriodSubCommands from '../core/discord/addPeriodSubCommands';
|
||||||
|
import addTankChoices from '../core/discord/addTankChoices';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
import buttonPrimary from '../core/discord/buttonPrimary';
|
||||||
import linkButton from '../core/discord/linkButton';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
import primaryButton from '../core/discord/primaryButton';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand, {
|
||||||
import { secrets } from '../core/node/secrets';
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import evolution from '../renderers/evolution';
|
|
||||||
import { StatType } from '../renderers/stats';
|
type EvolutionStatType = 'player' | 'tank';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
type: 'player' | 'tank',
|
||||||
|
tankId: number | null,
|
||||||
|
) {
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const logo = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const histories = await (type === 'player'
|
||||||
|
? getPlayerHistories(region, id, {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
})
|
||||||
|
: getTankHistories(region, id, {
|
||||||
|
tankId: tankId!,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
}));
|
||||||
|
const plot = histories.map(
|
||||||
|
(history) =>
|
||||||
|
[
|
||||||
|
history.all.battles,
|
||||||
|
(history.all.wins / history.all.battles) * 100,
|
||||||
|
] as Graph.PlotItem,
|
||||||
|
);
|
||||||
|
const times = histories.map((history) => history.last_battle_time);
|
||||||
|
const minTime = Math.min(...times);
|
||||||
|
const maxTime = Math.max(...times);
|
||||||
|
const ys = plot.map(([, y]) => y);
|
||||||
|
const xs = plot.map(([x]) => x);
|
||||||
|
const maxY = Math.max(...ys);
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const maxX = Math.max(...xs);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar name={accountInfo.nickname} image={logo} description={name} />
|
||||||
|
|
||||||
|
{plot.length > 0 && (
|
||||||
|
<Graph.Root
|
||||||
|
verticalMargin={{
|
||||||
|
min: minY,
|
||||||
|
max: maxY,
|
||||||
|
suffix: '%',
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
horizontalMargin={{
|
||||||
|
min: minX,
|
||||||
|
max: maxX,
|
||||||
|
precision: 0,
|
||||||
|
}}
|
||||||
|
xMinLabel={new Date(minTime * 1000).toDateString()}
|
||||||
|
xMaxLabel={new Date(maxTime * 1000).toDateString()}
|
||||||
|
>
|
||||||
|
{Graph.Line({
|
||||||
|
color: Graph.LineColor.Red,
|
||||||
|
plot: plot,
|
||||||
|
minY,
|
||||||
|
maxY,
|
||||||
|
})}
|
||||||
|
</Graph.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addStatTypeSubCommandGroups(
|
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
new SlashCommandBuilder()
|
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
.setName('evolution')
|
const command = new SlashCommandBuilder()
|
||||||
.setDescription('Evolution of statistics'),
|
.setName('evolution')
|
||||||
false,
|
.setDescription('Evolution of statistics')
|
||||||
);
|
.addSubcommandGroup((option) => {
|
||||||
|
playerGroup = option
|
||||||
|
.setName('player')
|
||||||
|
.setDescription('Player statistics');
|
||||||
|
|
||||||
|
return playerGroup;
|
||||||
|
})
|
||||||
|
.addSubcommandGroup((option) => {
|
||||||
|
tankGroup = option.setName('tank').setDescription('Tank statistics');
|
||||||
|
|
||||||
|
return tankGroup;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
addPeriodSubCommands(playerGroup!, (option) =>
|
||||||
|
option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
addPeriodSubCommands(tankGroup!, (option) =>
|
||||||
|
option
|
||||||
|
.addStringOption(addTankChoices)
|
||||||
|
.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -34,37 +145,28 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
|||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
const commandGroup =
|
||||||
true,
|
interaction.options.getSubcommandGroup() as EvolutionStatType;
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
const tankId =
|
const tankId =
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||||
const start = interaction.options.getInteger('start');
|
const path = commandToURL(interaction, {
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
tankId,
|
tankId,
|
||||||
start,
|
|
||||||
end,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getBlitzStarsLinkButton;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
await evolution(commandGroup, period, player, tankId),
|
await render(player, period, commandGroup, tankId),
|
||||||
primaryButton(path, 'Refresh'),
|
buttonPrimary(path, 'Refresh'),
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
await getBlitzStarsLinkButton(
|
||||||
linkButton(
|
player.region,
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}${
|
player.id,
|
||||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
tankId ?? undefined,
|
||||||
}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -75,17 +177,17 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async button(interaction) {
|
async button(interaction) {
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
const url = new URL(`https://example.com/${interaction.customId}`);
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
const path = url.pathname.split('/');
|
||||||
const commandGroup = path[1] as StatType;
|
const commandGroup = path[2] as EvolutionStatType;
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
|
||||||
return await evolution(
|
return await render(
|
||||||
commandGroup,
|
|
||||||
period,
|
|
||||||
player,
|
player,
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
period,
|
||||||
|
commandGroup,
|
||||||
|
parseInt(url.searchParams.get('tankId') ?? '0') || null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +1,76 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { TreeTypeString } from '../components/Tanks';
|
import GenericAllStats from '../components/GenericAllStats';
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import TierWeights from '../components/TierWeights';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import TitleBar from '../components/TitleBar';
|
||||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
import addFilterOptions from '../core/discord/addFilterOptions';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import linkButton from '../core/discord/linkButton';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
import primaryButton from '../core/discord/primaryButton';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand, {
|
||||||
import { secrets } from '../core/node/secrets';
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
|
import { getTierWeights } from '../core/statistics/getTierWeights';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import fullStats from '../renderers/fullStats';
|
|
||||||
import { MultiTankFilters, StatType } from '../renderers/stats';
|
async function render(
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
|
||||||
|
const { stats, supplementary, filteredOrder } = await filterStats(
|
||||||
|
diffedTankStats,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters);
|
||||||
|
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<GenericAllStats stats={stats} supplementaryStats={supplementary} />
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addStatTypeSubCommandGroups(
|
const command = await addFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('full-stats')
|
.setName('full-stats')
|
||||||
.setDescription('Full in-game statistics'),
|
.setDescription('Full in-game statistics'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
@@ -34,60 +80,20 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
|
||||||
true,
|
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const resolvedPeriod = resolvePeriodFromCommand(
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
player.region,
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
interaction,
|
const path = commandToURL(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,
|
...player,
|
||||||
tankId,
|
...getCustomPeriodParams(interaction),
|
||||||
start,
|
...filters,
|
||||||
end,
|
|
||||||
period,
|
|
||||||
});
|
});
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
|
|
||||||
const tierOption = interaction.options.getString('tier');
|
return Promise.all([
|
||||||
|
render(player, period, filters),
|
||||||
return [
|
buttonRefresh(interaction, path),
|
||||||
await fullStats(
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
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) => {
|
autocomplete: (interaction) => {
|
||||||
@@ -96,18 +102,11 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async button(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 player = await resolvePlayerFromButton(interaction);
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await fullStats(
|
return await render(player, period, filters);
|
||||||
commandGroup,
|
|
||||||
period,
|
|
||||||
player,
|
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import GenericStats from '../components/GenericStats';
|
|||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanInfo } from '../core/blitz/getClanInfo';
|
||||||
import addClanChoices from '../core/discord/addClanChoices';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||||
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { ClanInfo } from '../types/clanInfo';
|
|
||||||
|
|
||||||
const DEFAULT_THRESHOLD = 7;
|
const DEFAULT_THRESHOLD = 7;
|
||||||
|
|
||||||
@@ -36,24 +34,16 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
const threshold =
|
const threshold =
|
||||||
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
||||||
const time = new Date().getTime() / 1000;
|
const time = new Date().getTime() / 1000;
|
||||||
const clanData = (
|
const clanInfo = await getClanInfo(region, id);
|
||||||
await getWargamingResponse<ClanInfo>(
|
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
|
||||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
const inactive = accountInfo
|
||||||
|
.map(
|
||||||
|
(account) =>
|
||||||
|
[
|
||||||
|
account.nickname,
|
||||||
|
(time - account.last_battle_time) / 60 / 60 / 24,
|
||||||
|
] as [string, number],
|
||||||
)
|
)
|
||||||
)[id];
|
|
||||||
const memberIds = clanData.members_ids;
|
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${
|
|
||||||
secrets.WARGAMING_APPLICATION_ID
|
|
||||||
}&account_id=${memberIds.join(',')}`,
|
|
||||||
);
|
|
||||||
const inactive = memberIds
|
|
||||||
.map((memberId) => {
|
|
||||||
const member = accounts[memberId];
|
|
||||||
const inactiveDays = (time - member.last_battle_time) / 60 / 60 / 24;
|
|
||||||
|
|
||||||
return [member.nickname, inactiveDays] as [string, number];
|
|
||||||
})
|
|
||||||
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.map(
|
.map(
|
||||||
@@ -64,8 +54,8 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={clanData.name}
|
name={clanInfo.name}
|
||||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
|
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
||||||
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { teal } from '@radix-ui/colors';
|
||||||
|
import {
|
||||||
|
AttachmentBuilder,
|
||||||
|
GuildMemberRoleManager,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
|
import discord from '../../discord.json' assert { type: 'json' };
|
||||||
|
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
||||||
|
|
||||||
|
export const verifyCommand: CommandRegistry = {
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: new SlashCommandBuilder()
|
||||||
|
.setName('link')
|
||||||
|
.setDescription('Links your Blitz and Discord account')
|
||||||
|
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const username = interaction.options.getString('username', true);
|
||||||
|
|
||||||
|
if (serverAndIdPattern.test(username)) {
|
||||||
|
const [region, idString] = username.split('/');
|
||||||
|
const id = parseInt(idString);
|
||||||
|
const discordId = parseInt(interaction.user.id);
|
||||||
|
const accountInfo = await getAccountInfo(region as Region, id);
|
||||||
|
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [
|
||||||
|
'clan',
|
||||||
|
]);
|
||||||
|
|
||||||
|
await linkBlitzAndDiscord(discordId, region as Region, id);
|
||||||
|
|
||||||
|
if (interaction.guildId === discord.sklld_guild_id) {
|
||||||
|
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
||||||
|
return embedNegative(
|
||||||
|
`${markdownEscape(interaction.user.username)} failed to verify`,
|
||||||
|
"I don't have the permission to change your manage roles.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).remove(
|
||||||
|
discord.sklld_verify_role,
|
||||||
|
);
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).add(
|
||||||
|
discord.sklld_peasant_role,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper fat>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glow color={teal.teal9} rotation={90} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
interaction.user.avatarURL({ extension: 'png' }) ??
|
||||||
|
'https://i.stack.imgur.com/l60Hf.png'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interaction.user.displayName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
@{interaction.user.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
color: theme.colors.textLowContrast_teal,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://i.imgur.com/jIcRgog.png"
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 16 }}>Accounts linked</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan ? (
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{accountInfo.nickname}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan
|
||||||
|
? `[${clanAccountInfo?.clan?.tag}] - `
|
||||||
|
: ''}
|
||||||
|
{REGION_NAMES_SHORT[region as Region]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
embedNegative(
|
||||||
|
'Please select from the options',
|
||||||
|
'Follow the procedure like the video above. The search results may take a while to show up.',
|
||||||
|
),
|
||||||
|
new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
};
|
||||||
@@ -3,46 +3,24 @@ import NoData, { NoDataType } from '../components/NoData';
|
|||||||
import * as Tanks from '../components/Tanks';
|
import * as Tanks from '../components/Tanks';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
import getTreeType from '../core/blitz/getTreeType';
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
import {
|
import {
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
TankopediaEntry,
|
TankopediaEntry,
|
||||||
Tier,
|
Tier,
|
||||||
tankopedia,
|
tankopedia,
|
||||||
tankopediaInfo,
|
tankopediaInfo,
|
||||||
} from '../core/blitz/tankopedia';
|
} from '../core/blitzstars/tankopedia';
|
||||||
import addTierChoices from '../core/discord/addTierChoices';
|
import addTierChoices from '../core/discord/addTierChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
|
||||||
|
|
||||||
const COMP_TANKS = [
|
|
||||||
// light tanks
|
|
||||||
24321, // t-100 lt
|
|
||||||
20257, // sheridan
|
|
||||||
3649, // bat chat
|
|
||||||
19537, // vickers
|
|
||||||
|
|
||||||
// heavy tanks
|
|
||||||
6145, // is-4
|
|
||||||
7169, // is-7
|
|
||||||
10785, // t110e5
|
|
||||||
12161, // strvk
|
|
||||||
4481, // kranvagn
|
|
||||||
22817, // m6 yoh
|
|
||||||
6225, // fv215b
|
|
||||||
5425, // wz 113
|
|
||||||
7297, // 60tp
|
|
||||||
58641, // vk 72
|
|
||||||
6753, // type 71
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ownedTanksCommand: CommandRegistry = {
|
export const ownedTanksCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -58,9 +36,7 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
const tier = Number(interaction.options.getString('tier'));
|
const tier = Number(interaction.options.getString('tier'));
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const { id, region: server } = account;
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(server, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const tankStats = await getTankStats(server, id);
|
const tankStats = await getTankStats(server, id);
|
||||||
const filteredTanks = (
|
const filteredTanks = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -70,9 +46,7 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).filter((tank) => tank.tankopedia?.tier === tier);
|
).filter((tank) => tank.tankopedia?.tier === tier);
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
|
||||||
);
|
|
||||||
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
||||||
const nations: string[] = [];
|
const nations: string[] = [];
|
||||||
|
|
||||||
@@ -90,18 +64,13 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo.nickname}
|
||||||
nameDiscriminator={
|
|
||||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
|
||||||
}
|
|
||||||
image={
|
image={
|
||||||
clanData[id]?.clan
|
clanAccountInfo?.clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
description={`Tier ${
|
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`}
|
||||||
TIER_ROMAN_NUMERALS[tier as Tier]
|
|
||||||
} tanks • ${new Date().toDateString()}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||||
@@ -158,17 +127,3 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
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,16 +1,14 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { startCase } from 'lodash';
|
import { startCase } from 'lodash';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountAchievements } from '../types/accountAchievements';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
|
||||||
type SortBy = 'name' | 'count';
|
type SortBy = 'name' | 'count';
|
||||||
|
|
||||||
@@ -35,22 +33,17 @@ export const playerAchievementsCommand: CommandRegistry = {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const { id, region: region } = account;
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
const accountsAchievements = await getAccountAchievements(region, id);
|
||||||
);
|
const accountAchievements = accountsAchievements;
|
||||||
const accountsAchievements =
|
|
||||||
await getWargamingResponse<AccountAchievements>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const accountAchievements = accountsAchievements[id];
|
|
||||||
const compound = {
|
const compound = {
|
||||||
...accountAchievements.achievements,
|
...accountAchievements.achievements,
|
||||||
...accountAchievements.max_series,
|
...accountAchievements.max_series,
|
||||||
};
|
};
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`${markdownEscape(accounts[id].nickname)}'s information`,
|
`${markdownEscape(accountInfo.nickname)}'s information`,
|
||||||
|
|
||||||
markdownTable(
|
markdownTable(
|
||||||
Object.keys(compound)
|
Object.keys(compound)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable from '../core/discord/markdownTable';
|
import markdownTable from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
|
||||||
export const playerInfoCommand: CommandRegistry = {
|
export const playerInfoCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -21,11 +19,8 @@ export const playerInfoCommand: CommandRegistry = {
|
|||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const { id, region: region } = account;
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const accountInfo = accounts[id];
|
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`${markdownEscape(accountInfo.nickname)}'s information`,
|
`${markdownEscape(accountInfo.nickname)}'s information`,
|
||||||
|
|||||||
@@ -9,23 +9,22 @@ import * as Leaderboard from '../components/Leaderboard';
|
|||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||||
|
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
||||||
import getMidnightLeaderboard, {
|
import getMidnightLeaderboard, {
|
||||||
DATABASE_REPO,
|
DATABASE_REPO,
|
||||||
} from '../core/database/getMidnightLeaderboard';
|
} from '../core/blitzkrieg/getMidnightLeaderboard';
|
||||||
|
import { octokit } from '../core/blitzkrieg/octokit';
|
||||||
|
import { UserError } from '../core/blitzkrieg/userError';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { octokit } from '../core/express/octokit';
|
|
||||||
import { secrets } from '../core/node/secrets';
|
|
||||||
import throwError from '../core/node/throwError';
|
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
|
||||||
|
|
||||||
export interface RatingsPlayer {
|
export interface RatingsPlayer {
|
||||||
spa_id: number;
|
spa_id: number;
|
||||||
@@ -172,6 +171,11 @@ function addSubcommands(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noOngoingSeason = embedNegative(
|
||||||
|
'No ongoing season',
|
||||||
|
"Wargaming didn't provide any data for this season.",
|
||||||
|
);
|
||||||
|
|
||||||
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const seasonChoices = await octokit.repos
|
const seasonChoices = await octokit.repos
|
||||||
@@ -181,7 +185,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
throw throwError('Archived ratings data is malformed');
|
throw new Error('Archived ratings data is malformed');
|
||||||
}
|
}
|
||||||
return data.map(
|
return data.map(
|
||||||
(folder) =>
|
(folder) =>
|
||||||
@@ -259,27 +263,28 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
regionRatingsInfo = await (subcommandGroup === 'current'
|
regionRatingsInfo = await (subcommandGroup === 'current'
|
||||||
? getRatingsInfo(region)
|
? getRatingsInfo(region)
|
||||||
: getArchivedRatingsInfo(season!, region));
|
: getArchivedRatingsInfo(season!, region));
|
||||||
|
|
||||||
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
|
|
||||||
const regionSubdomain = regionToRegionSubdomain(region);
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
const result =
|
const result =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await fetch(
|
||||||
? await fetch(
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
||||||
`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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.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
|
: await octokit.repos
|
||||||
.getContent({
|
.getContent({
|
||||||
...DATABASE_REPO,
|
...DATABASE_REPO,
|
||||||
@@ -287,7 +292,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(async ({ data }) => {
|
.then(async ({ data }) => {
|
||||||
if (Array.isArray(data) || data.type !== 'file') {
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
throw throwError('Archived ratings data is malformed');
|
throw new Error('Archived ratings data is malformed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const players = (await fetch(data.download_url!).then(
|
const players = (await fetch(data.download_url!).then(
|
||||||
@@ -305,9 +310,10 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
firstPlayerIndex,
|
firstPlayerIndex,
|
||||||
lastPlayerIndex + 1,
|
lastPlayerIndex + 1,
|
||||||
);
|
);
|
||||||
const ids = trimmed.map((player) => player.id).join(',');
|
const clanData = await getClanAccountInfo(
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
region,
|
||||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
trimmed.map(({ id }) => id),
|
||||||
|
['clan'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return trimmed.map(
|
return trimmed.map(
|
||||||
@@ -316,39 +322,32 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
id: player.id,
|
id: player.id,
|
||||||
score: player.score,
|
score: player.score,
|
||||||
position: firstPlayerIndex + index + 1,
|
position: firstPlayerIndex + index + 1,
|
||||||
clan: clanData[player.id]!.clan?.tag,
|
clan: clanData[player.id]?.clan?.tag,
|
||||||
nickname: clanData[player.id]!.account_name,
|
nickname:
|
||||||
|
clanData[player.id]?.account_name ??
|
||||||
|
`Unknown Player ${player.id}`,
|
||||||
}) satisfies SimplifiedPlayer,
|
}) satisfies SimplifiedPlayer,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const leagueInfo =
|
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||||
regionRatingsInfo.detail === undefined
|
|
||||||
? regionRatingsInfo.leagues[leagueIndex]
|
|
||||||
: undefined;
|
|
||||||
midnightLeaderboard =
|
midnightLeaderboard =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await getMidnightLeaderboard(
|
||||||
? await getMidnightLeaderboard(
|
region,
|
||||||
region,
|
regionRatingsInfo.current_season,
|
||||||
regionRatingsInfo.current_season,
|
)
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
: await getMidnightLeaderboard(region, season!);
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
players = result;
|
players = result;
|
||||||
playersBefore = result ? result[0].position - 1 : 0;
|
playersBefore = result ? result[0].position - 1 : 0;
|
||||||
playersAfter =
|
playersAfter =
|
||||||
regionRatingsInfo.detail === undefined && result !== undefined
|
regionRatingsInfo.count - result[result.length - 1].position;
|
||||||
? regionRatingsInfo.count - result[result.length - 1].position
|
|
||||||
: 0;
|
|
||||||
titleName = `${
|
titleName = `${
|
||||||
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
||||||
} - ${REGION_NAMES_SHORT[region]}`;
|
} - ${REGION_NAMES_SHORT[region]}`;
|
||||||
titleImage = leagueInfo
|
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||||
? leagueInfo.big_icon.startsWith('http')
|
? leagueInfo.big_icon
|
||||||
? leagueInfo.big_icon
|
: `https:${leagueInfo.big_icon}`;
|
||||||
: `https:${leagueInfo.big_icon}`
|
|
||||||
: undefined;
|
|
||||||
titleDescription = `Top ${limit} players`;
|
titleDescription = `Top ${limit} players`;
|
||||||
} else {
|
} else {
|
||||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
@@ -362,7 +361,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(async ({ data }) => {
|
.then(async ({ data }) => {
|
||||||
if (Array.isArray(data) || data.type !== 'file') {
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
throw throwError('Archived ratings info is malformed');
|
throw new Error('Archived ratings info is malformed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = fetch(data.download_url!);
|
const response = fetch(data.download_url!);
|
||||||
@@ -370,40 +369,34 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
response.json(),
|
response.json(),
|
||||||
)) as RatingsInfo;
|
)) as RatingsInfo;
|
||||||
});
|
});
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
);
|
|
||||||
const clan = (
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
await getWargamingResponse<PlayerClanData>(
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
|
||||||
)
|
|
||||||
)[id]?.clan;
|
|
||||||
const regionSubdomain = regionToRegionSubdomain(region);
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
const neighbors =
|
const neighbors =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await fetch(
|
||||||
? await fetch(
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
limit / 2,
|
||||||
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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.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
|
: await octokit.repos
|
||||||
.getContent({
|
.getContent({
|
||||||
...DATABASE_REPO,
|
...DATABASE_REPO,
|
||||||
@@ -411,7 +404,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(async ({ data }) => {
|
.then(async ({ data }) => {
|
||||||
if (Array.isArray(data) || data.type !== 'file') {
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
throw throwError(
|
throw new Error(
|
||||||
'Archived ratings latest data is malformed',
|
'Archived ratings latest data is malformed',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -425,9 +418,12 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (playerIndex === -1) {
|
if (playerIndex === -1) {
|
||||||
throw throwError(
|
throw new UserError(
|
||||||
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
|
`${accountInfo.nickname} didn't player ratings in season ${season}`,
|
||||||
'This player did not participate in this season or did not get past calibration.',
|
{
|
||||||
|
cause:
|
||||||
|
'This player did not participate in this season or did not get past calibration.',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,9 +432,10 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
playerIndex - halfRange,
|
playerIndex - halfRange,
|
||||||
playerIndex + halfRange + 1,
|
playerIndex + halfRange + 1,
|
||||||
);
|
);
|
||||||
const ids = trimmed.map((player) => player.id).join(',');
|
const clanData = await getClanAccountInfo(
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
region,
|
||||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
trimmed.map((player) => player.id),
|
||||||
|
['clan'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return trimmed.map((player, index) => ({
|
return trimmed.map((player, index) => ({
|
||||||
@@ -451,46 +448,38 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
});
|
});
|
||||||
midnightLeaderboard =
|
midnightLeaderboard =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await getMidnightLeaderboard(
|
||||||
? await getMidnightLeaderboard(
|
region,
|
||||||
region,
|
regionRatingsInfo.current_season,
|
||||||
regionRatingsInfo.current_season,
|
)
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
: await getMidnightLeaderboard(region, season!);
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
||||||
|
|
||||||
players = neighbors;
|
players = neighbors;
|
||||||
titleImage = neighbors
|
titleImage = clan
|
||||||
? clan
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
|
||||||
: undefined
|
|
||||||
: undefined;
|
: undefined;
|
||||||
titleName = neighbors
|
titleName = neighbors
|
||||||
? accountInfo[id].nickname
|
? accountInfo.nickname
|
||||||
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
||||||
titleDescription = 'Ratings neighbours';
|
titleDescription = 'Ratings neighbours';
|
||||||
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
||||||
playersAfter =
|
playersAfter =
|
||||||
regionRatingsInfo.detail === undefined
|
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
||||||
? neighbors
|
|
||||||
? regionRatingsInfo.count -
|
|
||||||
neighbors[neighbors.length - 1].position
|
|
||||||
: 0
|
|
||||||
: 0;
|
|
||||||
playerId = id;
|
playerId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = players?.map((player) => {
|
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
|
||||||
const reward =
|
detail: undefined;
|
||||||
regionRatingsInfo.detail === undefined
|
} = regionRatingsInfo;
|
||||||
? regionRatingsInfo.rewards.find(
|
|
||||||
(reward) =>
|
const items = players.map((player) => {
|
||||||
player.position >= reward.from_position &&
|
const reward = resolvedRegionRatingsInfo.rewards.find(
|
||||||
player.position <= reward.to_position,
|
(reward) =>
|
||||||
)
|
player.position >= reward.from_position &&
|
||||||
: undefined;
|
player.position <= reward.to_position,
|
||||||
|
);
|
||||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||||
(item) => item.id === player.id,
|
(item) => item.id === player.id,
|
||||||
);
|
);
|
||||||
@@ -521,8 +510,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
<TitleBar
|
<TitleBar
|
||||||
name={titleName}
|
name={titleName}
|
||||||
image={titleImage}
|
image={titleImage}
|
||||||
nameDiscriminator={titleNameDiscriminator}
|
description={titleDescription}
|
||||||
description={`${titleDescription} • ${new Date().toDateString()}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{items && (
|
{items && (
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { REGION_NAMES, Region } from '../constants/regions';
|
import { REGION_NAMES, Region } from '../constants/regions';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from '../core/blitz/fetchBlitz';
|
||||||
import addClanChoices from '../core/discord/addClanChoices';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { ClanList } from '../types/clanList';
|
import { ClanList } from '../types/clanList';
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ export const searchClansCommand: CommandRegistry = {
|
|||||||
const server = interaction.options.getString('region') as Region;
|
const server = interaction.options.getString('region') as Region;
|
||||||
const clan = interaction.options.getString('clan')!;
|
const clan = interaction.options.getString('clan')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const clanList = await getWargamingResponse<ClanList>(
|
const clanList = await fetchBlitz<ClanList>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { REGION_NAMES, Region } from '../constants/regions';
|
import { REGION_NAMES, Region } from '../constants/regions';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||||
import { usernamePattern } from '../core/blitz/listPlayers';
|
import fetchBlitz from '../core/blitz/fetchBlitz';
|
||||||
|
import {
|
||||||
|
AccountList,
|
||||||
|
usernamePattern,
|
||||||
|
} from '../core/blitz/searchPlayersAcrossRegions';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountList } from '../types/accountList';
|
|
||||||
|
|
||||||
export const searchPlayersCommand: CommandRegistry = {
|
export const searchPlayersCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -35,10 +37,10 @@ export const searchPlayersCommand: CommandRegistry = {
|
|||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const trimmedSearch = name.trim();
|
const trimmedSearch = name.trim();
|
||||||
const players = usernamePattern.test(trimmedSearch)
|
const players = usernamePattern.test(trimmedSearch)
|
||||||
? await getWargamingResponse<AccountList>(
|
? await fetchBlitz<AccountList>(
|
||||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${
|
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
|
||||||
secrets.WARGAMING_APPLICATION_ID
|
trimmedSearch,
|
||||||
}&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`,
|
)}&limit=${limit}`,
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
|
import { TANK_NAMES_DIACRITICS } from '../core/blitzstars/tankopedia';
|
||||||
import addTankChoices from '../core/discord/addTankChoices';
|
import addTankChoices from '../core/discord/addTankChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|||||||
@@ -1,28 +1,68 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { TreeTypeString } from '../components/Tanks';
|
import AllStatsOverview from '../components/AllStatsOverview';
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import TitleBar from '../components/TitleBar';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import Wrapper from '../components/Wrapper';
|
||||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
import addFilterOptions from '../core/discord/addFilterOptions';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import linkButton from '../core/discord/linkButton';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
import primaryButton from '../core/discord/primaryButton';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand, {
|
||||||
import { secrets } from '../core/node/secrets';
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
|
||||||
|
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!stats.battles && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<AllStatsOverview stats={stats} supplementaryStats={supplementary} />
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
||||||
const command = await addStatTypeSubCommandGroups(
|
const command = await addFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('stats')
|
.setName('stats')
|
||||||
.setDescription('Regular battles statistics'),
|
.setDescription('Regular battles statistics'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
@@ -32,60 +72,20 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
|||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
|
||||||
true,
|
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const resolvedPeriod = resolvePeriodFromCommand(
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
player.region,
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
interaction,
|
const path = commandToURL(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,
|
...player,
|
||||||
tankId,
|
...getCustomPeriodParams(interaction),
|
||||||
start,
|
...filters,
|
||||||
end,
|
|
||||||
period,
|
|
||||||
});
|
});
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
|
|
||||||
const tierOption = interaction.options.getString('tier');
|
return Promise.all([
|
||||||
|
render(player, period, filters),
|
||||||
return [
|
buttonRefresh(interaction, path),
|
||||||
await stats(
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
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) => {
|
autocomplete: (interaction) => {
|
||||||
@@ -94,27 +94,11 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async button(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 player = await resolvePlayerFromButton(interaction);
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await stats(
|
return await render(player, period, filters);
|
||||||
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,84 +1,18 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
|
||||||
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 { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import today from '../renderers/today';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: @deprecated
|
||||||
|
*/
|
||||||
export const todayCommand: CommandRegistry = {
|
export const todayCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('today')
|
.setName('today')
|
||||||
.setDescription('A general daily breakdown of your performance')
|
.setDescription('This command has been renamed to `/breakdown today`'),
|
||||||
.addStringOption(addUsernameChoices)
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.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) {
|
handler() {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
return '# This command has been renamed to `/breakdown today`\n`/today` now comes with filters, periods and more! Use `/breakdown today` for same old functionality but also do checkout the other options.';
|
||||||
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.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
cutoff,
|
|
||||||
maximized,
|
|
||||||
'show-total': showTotal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
await today(player, cutoff, maximized, showTotal, false),
|
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
|
||||||
linkButton(
|
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
|
||||||
|
|
||||||
async button(interaction) {
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
|
|
||||||
return await today(player);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
|
||||||
import markdownEscape from 'markdown-escape';
|
|
||||||
import discord from '../../discord.json' assert { type: 'json' };
|
|
||||||
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 { secrets } from '../core/node/secrets';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
|
||||||
|
|
||||||
export const verifyCommand: CommandRegistry = {
|
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
|
||||||
.setName('verify')
|
|
||||||
.setDescription("Set's the user's username to their in-game name")
|
|
||||||
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
const blitzAccount = await resolvePlayerFromCommand(interaction);
|
|
||||||
const { id, region: server } = blitzAccount;
|
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
|
||||||
);
|
|
||||||
const clan = clanData[id]?.clan;
|
|
||||||
const clanTag = clan ? ` [${clan!.tag}]` : '';
|
|
||||||
const member =
|
|
||||||
interaction.member &&
|
|
||||||
interaction.guild?.members.cache.get(interaction.member?.user.id);
|
|
||||||
|
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageNicknames')) {
|
|
||||||
return embedNegative(
|
|
||||||
`${markdownEscape(interaction.user.username)} failed to verify`,
|
|
||||||
"I don't have the permission to change your nickname.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!interaction.member || !member) {
|
|
||||||
return embedNegative(
|
|
||||||
`${interaction.user.username} is not in this server`,
|
|
||||||
"I couldn't find this person in this server. Did they leave?",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await member.setNickname(`${accountInfo[id].nickname}${clanTag}`);
|
|
||||||
|
|
||||||
if (interaction.guildId === discord.sklld_guild_id) {
|
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
|
||||||
return embedNegative(
|
|
||||||
`${markdownEscape(interaction.user.username)} failed to verify`,
|
|
||||||
"I don't have the permission to change your manage roles.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await member.roles.remove(discord.sklld_verify_role);
|
|
||||||
await member.roles.add(discord.sklld_peasant_role);
|
|
||||||
}
|
|
||||||
|
|
||||||
return embedPositive(
|
|
||||||
`${interaction.user.username} is verified`,
|
|
||||||
`The user is now verified as ${markdownEscape(
|
|
||||||
accountInfo[id].nickname,
|
|
||||||
)}${markdownEscape(clanTag)}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return embedNegative(
|
|
||||||
`${interaction.user.username} failed to verify`,
|
|
||||||
"I can't change your nickname because you have higher permissions than me. Try setting your nickname to your Blitz username manually.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
|
||||||
};
|
|
||||||
@@ -1,32 +1,28 @@
|
|||||||
import { Percentile } from '../../../../../constants/percentiles';
|
|
||||||
import hslToHex from '../../../../../core/node/hslToHex';
|
|
||||||
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator';
|
|
||||||
|
|
||||||
export enum GlowSide {
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GlowProps {
|
export interface GlowProps {
|
||||||
percentile: Percentile;
|
color: string;
|
||||||
side: GlowSide;
|
direction?: 'default' | 'reverse';
|
||||||
|
rotation?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Glow({ percentile, side }: GlowProps) {
|
export function Glow({
|
||||||
const color = PERCENTILE_COLORS[percentile];
|
color,
|
||||||
const color1 = hslToHex(color);
|
direction = 'default',
|
||||||
const color2 = `${color1}00`;
|
rotation = 0,
|
||||||
|
}: GlowProps) {
|
||||||
|
const color1 = color;
|
||||||
|
const color2 = `${color}00`;
|
||||||
|
|
||||||
const background = `linear-gradient(${
|
const background = `linear-gradient(${
|
||||||
side === GlowSide.Top ? 0 : 180
|
direction === 'default' ? 0 : 180
|
||||||
}deg, ${color1} 0%, ${color2} 100%)`;
|
}deg, ${color1} 0%, ${color2} 100%)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: side === GlowSide.Top ? 'flex-end' : 'flex-start',
|
alignItems: direction === 'default' ? 'flex-end' : 'flex-start',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ height: 54, width: 16, background }} />
|
<div style={{ height: 54, width: 16, background }} />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Percentile } from '../../../../constants/percentiles';
|
import { Percentile } from '../../../../constants/percentiles';
|
||||||
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile';
|
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
|
||||||
import { theme } from '../../../../stitches.config';
|
import { theme } from '../../../../stitches.config';
|
||||||
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
|
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
|
||||||
import { Glow, GlowSide } from './components/Glow';
|
import { Glow } from './components/Glow';
|
||||||
|
|
||||||
export interface WN8DisplayProps {
|
export interface WN8DisplayProps {
|
||||||
WN8?: number;
|
WN8?: number;
|
||||||
@@ -37,7 +37,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
gap: -16,
|
gap: -16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Glow percentile={percentile} side={GlowSide.Top} />
|
<Glow color={PERCENTILE_COLORS[percentile]} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -71,7 +71,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Glow percentile={percentile} side={GlowSide.Bottom} />
|
<Glow color={PERCENTILE_COLORS[percentile]} direction="reverse" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
|
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||||
import { WN8Display } from './components/WN8Display';
|
import { WN8Display } from './components/WN8Display';
|
||||||
import {
|
import {
|
||||||
WN8SurroundingStat,
|
WN8SurroundingStat,
|
||||||
|
|||||||
@@ -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,40 +1,27 @@
|
|||||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
import { Percentile } from '../../../constants/percentiles';
|
||||||
import { TankType } from '../../../core/blitz/tankopedia';
|
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
||||||
import { RowStat } from './RowStat';
|
import { RowStat } from './RowStat';
|
||||||
|
|
||||||
export interface RowProps {
|
export interface RowStatItem {
|
||||||
name: string;
|
title: string;
|
||||||
winrate: number;
|
current?: number | string;
|
||||||
careerWinrate: number;
|
career?: number | string;
|
||||||
WN8?: number;
|
delta?: number;
|
||||||
careerWN8?: number;
|
percentile?: Percentile;
|
||||||
damage: number;
|
|
||||||
careerDamage: number;
|
|
||||||
battles: number;
|
|
||||||
careerBattles: number;
|
|
||||||
minimized: boolean;
|
|
||||||
isTank: boolean;
|
|
||||||
treeType?: TreeTypeEnum;
|
|
||||||
tankType?: TankType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Row({
|
interface RowProps {
|
||||||
isTank,
|
type?: 'tank' | 'summary';
|
||||||
minimized,
|
title: string;
|
||||||
name,
|
minimized?: boolean;
|
||||||
winrate,
|
treeType?: TreeTypeEnum;
|
||||||
careerWinrate,
|
tankType?: string;
|
||||||
WN8,
|
|
||||||
careerWN8,
|
stats: (RowStatItem | undefined)[];
|
||||||
damage,
|
}
|
||||||
careerDamage,
|
|
||||||
battles,
|
export function Row(props: RowProps) {
|
||||||
careerBattles,
|
|
||||||
treeType,
|
|
||||||
tankType,
|
|
||||||
}: RowProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -55,26 +42,32 @@ export function Row({
|
|||||||
backgroundColor: theme.colors.componentInteractive,
|
backgroundColor: theme.colors.componentInteractive,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isTank && treeType !== undefined && tankType !== undefined && (
|
{props.type === 'tank' && props.tankType !== undefined && (
|
||||||
<img
|
<img
|
||||||
src={TREE_TYPE_ICONS[treeType][tankType]}
|
src={
|
||||||
|
TREE_TYPE_ICONS[props.treeType ?? TreeTypeEnum.TechTree][
|
||||||
|
props.tankType
|
||||||
|
]
|
||||||
|
}
|
||||||
style={{ width: 16, height: 16 }}
|
style={{ width: 16, height: 16 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: isTank
|
color:
|
||||||
? treeType === TreeTypeEnum.Collector
|
props.type === 'tank'
|
||||||
? theme.colors.textLowContrast_blue
|
? props.treeType === TreeTypeEnum.Collector
|
||||||
: treeType === TreeTypeEnum.Premium
|
? theme.colors.textLowContrast_blue
|
||||||
? theme.colors.textLowContrast_amber
|
: props.treeType === TreeTypeEnum.Premium
|
||||||
: theme.colors.textHighContrast
|
? theme.colors.textLowContrast_amber
|
||||||
: theme.colors.textLowContrast,
|
: theme.colors.textHighContrast
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{props.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,31 +78,19 @@ export function Row({
|
|||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RowStat
|
{props.stats.map(
|
||||||
minimized={minimized}
|
(row, index) =>
|
||||||
name={`Battles • ${careerBattles.toFixed(0)}`}
|
row && (
|
||||||
value={battles.toFixed(0)}
|
<RowStat
|
||||||
/>
|
minimized={props.minimized}
|
||||||
<RowStat
|
key={index}
|
||||||
minimized={minimized}
|
name={`${row.title} • ${row.career ?? '--'}`}
|
||||||
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
|
value={row.current ?? '--'}
|
||||||
value={`${(winrate * 100).toFixed(2)}%`}
|
delta={row.delta}
|
||||||
delta={winrate - careerWinrate}
|
percentile={row.percentile}
|
||||||
/>
|
/>
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PercentileIndicator from '../../PercentileIndicator';
|
|||||||
|
|
||||||
export interface RowStatProps {
|
export interface RowStatProps {
|
||||||
name: string;
|
name: string;
|
||||||
value: string | number;
|
value?: string | number;
|
||||||
delta?: number;
|
delta?: number;
|
||||||
percentile?: Percentile;
|
percentile?: Percentile;
|
||||||
minimized?: boolean;
|
minimized?: boolean;
|
||||||
@@ -15,7 +15,7 @@ export function RowStat({
|
|||||||
value,
|
value,
|
||||||
delta,
|
delta,
|
||||||
percentile,
|
percentile,
|
||||||
minimized,
|
minimized = false,
|
||||||
}: RowStatProps) {
|
}: RowStatProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -55,6 +55,7 @@ export function RowStat({
|
|||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
@@ -62,7 +63,13 @@ export function RowStat({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!minimized && (
|
{!minimized && (
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 12 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './components/Root';
|
export * from './components/Root';
|
||||||
export * from './components/Row';
|
export * from './components/Row';
|
||||||
export * from './components/RowStat';
|
export * from './components/RowStat';
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ export function Card({ items, title }: CardProps) {
|
|||||||
>
|
>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div
|
<div
|
||||||
|
key={item.label}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
@@ -76,7 +77,10 @@ export function Card({ items, title }: CardProps) {
|
|||||||
<PercentileIndicator percentile={item.percentile} />
|
<PercentileIndicator percentile={item.percentile} />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
style={{ fontSize: 16, color: theme.colors.textHighContrast }}
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.value === undefined || Number.isNaN(item.value)
|
{item.value === undefined || Number.isNaN(item.value)
|
||||||
? '--'
|
? '--'
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import getWN8Percentile from '../../core/blitz/getWN8Percentile';
|
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||||
import isNumber from '../../core/node/isNumber';
|
import isNumber from '../../core/math/isNumber';
|
||||||
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
|
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
||||||
import { Card } from './components/Card';
|
import { Card } from './components/Card';
|
||||||
import { Root } from './components/Root';
|
import { Root } from './components/Root';
|
||||||
import { Row } from './components/Row';
|
import { Row } from './components/Row';
|
||||||
|
|
||||||
// TODO: remove the .js endings
|
|
||||||
|
|
||||||
export interface GenericAllStatsProps {
|
export interface GenericAllStatsProps {
|
||||||
stats: AllStats;
|
stats: AllStats;
|
||||||
supplementaryStats?: SupplementaryStats;
|
supplementaryStats?: SupplementaryStats;
|
||||||
|
|||||||
@@ -15,9 +15,18 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: 128,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{stat[0]}
|
{stat[0]}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ export function Item({
|
|||||||
? reward.vehicle.image_url
|
? reward.vehicle.image_url
|
||||||
: reward.stuff.image_url,
|
: reward.stuff.image_url,
|
||||||
)}
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{reward && reward.count > 1 && (
|
{reward && reward.count > 1 && (
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export default function Navbar() {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', height: 100, backgroundColor: 'red' }}></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({ children }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { theme } from '../../stitches.config';
|
||||||
|
import { BlitzkriegWormWide } from '../BlitzkriegWormWide';
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
backgroundColor: theme.colors.appBackground2,
|
||||||
|
borderBottom: theme.borderStyles.nonInteractive,
|
||||||
|
height: 48, // TODO: remove this when the icons are implemented
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <Button>
|
||||||
|
<HamburgerMenuIcon />
|
||||||
|
</Button> */}
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
style={{
|
||||||
|
color: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BlitzkriegWormWide />
|
||||||
|
</Link>
|
||||||
|
{/* <Button>
|
||||||
|
<PersonIcon />
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export default function PageWrapper({
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 800,
|
||||||
|
margin: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
padding: 16,
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@ import {
|
|||||||
TANK_ICONS,
|
TANK_ICONS,
|
||||||
TANK_ICONS_COLLECTOR,
|
TANK_ICONS_COLLECTOR,
|
||||||
TANK_ICONS_PREMIUM,
|
TANK_ICONS_PREMIUM,
|
||||||
TankType,
|
} from '../../../core/blitzstars/tankopedia';
|
||||||
} from '../../../core/blitz/tankopedia';
|
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export enum TreeTypeEnum {
|
export enum TreeTypeEnum {
|
||||||
@@ -12,11 +11,17 @@ export enum TreeTypeEnum {
|
|||||||
Collector,
|
Collector,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
|
export type TreeTypeString = 'tech-tree' | 'premium' | 'collector';
|
||||||
|
|
||||||
|
export const TREE_TYPE_NAMES: Record<TreeTypeString, string> = {
|
||||||
|
'tech-tree': 'Tech tree',
|
||||||
|
premium: 'Premium',
|
||||||
|
collector: 'Collector',
|
||||||
|
};
|
||||||
|
|
||||||
export interface ItemProps {
|
export interface ItemProps {
|
||||||
image?: string;
|
image?: string;
|
||||||
tankType?: TankType;
|
tankType?: string;
|
||||||
name: string;
|
name: string;
|
||||||
treeType: TreeTypeEnum;
|
treeType: TreeTypeEnum;
|
||||||
}
|
}
|
||||||
@@ -75,7 +80,15 @@ export function Item({ image, tankType, name, treeType }: ItemProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{image && <img src={image} style={{ flex: 1 }} />}
|
{/* {image && (
|
||||||
|
// TODO: remove hardcoded dimensions when satori fixes non-width unloadable images
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
width={106}
|
||||||
|
height={32}
|
||||||
|
style={{ objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TIER_ROMAN_NUMERALS, Tier } from '../core/blitz/tankopedia';
|
import { Tier } from '../core/blitzstars/tankopedia';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
||||||
@@ -26,7 +26,7 @@ export default function TierWeights({ weights }: TierWeightsProps) {
|
|||||||
minWidth: 16,
|
minWidth: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{TIER_ROMAN_NUMERALS[parseInt(tier) as Tier]}
|
{tier}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,17 +2,11 @@ import { theme } from '../stitches.config';
|
|||||||
|
|
||||||
export interface TitleBarProps {
|
export interface TitleBarProps {
|
||||||
name: string;
|
name: string;
|
||||||
nameDiscriminator?: string;
|
|
||||||
image?: string;
|
image?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleBar({
|
export default function TitleBar({ name, image, description }: TitleBarProps) {
|
||||||
name,
|
|
||||||
nameDiscriminator,
|
|
||||||
image,
|
|
||||||
description,
|
|
||||||
}: TitleBarProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -33,9 +27,8 @@ export default function TitleBar({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -51,24 +44,11 @@ export default function TitleBar({
|
|||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
{nameDiscriminator && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 32,
|
|
||||||
color: theme.colors.textLowContrast,
|
|
||||||
fontWeight: 900,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{nameDiscriminator}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import { theme } from '../stitches.config';
|
|||||||
export interface WrapperProps {
|
export interface WrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
naked?: boolean;
|
naked?: boolean;
|
||||||
|
fat?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Wrapper({ children, naked }: WrapperProps) {
|
export default function Wrapper({ children, naked, fat }: WrapperProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: 16,
|
padding: `16px ${fat ? 0 : 16}px`,
|
||||||
gap: 32,
|
gap: 32,
|
||||||
width: 480,
|
width: 480,
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
|
|
||||||
@@ -11,7 +11,7 @@ export const REGION_NAMES: Record<Region, string> = {
|
|||||||
export const REGION_NAMES_SHORT: Record<Region, string> = {
|
export const REGION_NAMES_SHORT: Record<Region, string> = {
|
||||||
com: 'NA',
|
com: 'NA',
|
||||||
eu: 'EU',
|
eu: 'EU',
|
||||||
asia: 'AS',
|
asia: 'ASIA',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
interface Tool {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOOLS: Tool[] = [
|
||||||
|
{
|
||||||
|
id: 'session',
|
||||||
|
title: 'Session tracker',
|
||||||
|
description: 'Track your performance in a session',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ratings',
|
||||||
|
title: 'Ratings',
|
||||||
|
description: 'Current and old season leader boards',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tankopedia',
|
||||||
|
title: 'Tankopedia',
|
||||||
|
description: 'Full Blitz tank encyclopedia',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inactive',
|
||||||
|
title: 'Inactivity tracker',
|
||||||
|
description: 'Find inactive members of a clan',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'profile',
|
||||||
|
title: 'Player profile',
|
||||||
|
description: "A player's basic non-statistical info",
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: 'More coming soon',
|
||||||
|
// description: 'New tools are added often',
|
||||||
|
// banner: '/assets/banners/more.png',
|
||||||
|
// id: 'https://discord.gg/nDt7AjGJQH',
|
||||||
|
// },
|
||||||
|
];
|
||||||