mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e012319a | ||
|
|
18de9cbaab | ||
|
|
42a76cbd38 | ||
|
|
eb95634801 | ||
|
|
de8bfff47f | ||
|
|
07c18911df | ||
|
|
cc769cc330 | ||
|
|
9794f58936 | ||
|
|
a44fcff3b1 | ||
|
|
7e4a3e88b9 | ||
|
|
8fd45e2205 | ||
|
|
7d235057e8 | ||
|
|
63a8a67771 | ||
|
|
bac39ca176 | ||
|
|
626b1e8601 | ||
|
|
04086f31b9 | ||
|
|
7037aa9c3b | ||
|
|
f5bab8a698 | ||
|
|
ee44b1b1d8 | ||
|
|
d0f91df95b | ||
|
|
dc1c49da6a | ||
|
|
331040bbdf | ||
|
|
70e1d4b30d | ||
|
|
03dbd797fb | ||
|
|
eb4cb1fbfe | ||
|
|
28172f7f8f | ||
|
|
cb5347cbb8 | ||
|
|
1ce0781d2d | ||
|
|
1257aa76d7 | ||
|
|
6ec3835896 | ||
|
|
506fe7d9bd | ||
|
|
14ef9f6295 | ||
|
|
0d76e1a29e | ||
|
|
9e42d7e8f5 | ||
|
|
364f97332b | ||
|
|
caa7bb8a65 | ||
|
|
3e956b34c1 | ||
|
|
038374e5aa | ||
|
|
dafd54f830 | ||
|
|
cb65e6b0af | ||
|
|
4ca8590c83 | ||
|
|
c5ef02d7e6 | ||
|
|
8175aa8715 | ||
|
|
b426634659 | ||
|
|
e92da1b700 | ||
|
|
7e0a2e88ee | ||
|
|
16afa8c2d4 | ||
|
|
907288455a | ||
|
|
64689e4e3b | ||
|
|
294377846a | ||
|
|
70e4cbaba2 | ||
|
|
9e2f9526fb | ||
|
|
84bec20fa5 | ||
|
|
e9a492fde6 | ||
|
|
76a69f08b4 | ||
|
|
893110d3ef | ||
|
|
70575fa9cf | ||
|
|
bb2450eb3c | ||
|
|
ab56689326 | ||
|
|
a9cf07480a | ||
|
|
c81de2d758 | ||
|
|
aa1c7639f2 | ||
|
|
d448101b32 | ||
|
|
e806f0ede2 | ||
|
|
8253c27333 | ||
|
|
254392370f | ||
|
|
d2319688a5 | ||
|
|
ffebe6c696 | ||
|
|
9e4094f3a9 | ||
|
|
f7eadb08a9 | ||
|
|
d511cfa4cc | ||
|
|
270a3051b7 | ||
|
|
81bc9f9ba6 | ||
|
|
338cec4f18 | ||
|
|
b455e3380f | ||
|
|
92761f2c55 | ||
|
|
7823b8ad2e | ||
|
|
cac82f3124 | ||
|
|
4b892d06e4 | ||
|
|
92149b0f00 | ||
|
|
399aa4c03c | ||
|
|
d5de715032 | ||
|
|
e6fb179184 | ||
|
|
af54f12ec3 | ||
|
|
909cabd517 | ||
|
|
a734a71d2f | ||
|
|
91dc45c880 | ||
|
|
91f364b76a | ||
|
|
23dd088466 | ||
|
|
2cabcc57f4 | ||
|
|
dd6092a5e7 |
No files matched your search
@@ -22,11 +22,11 @@ jobs:
|
|||||||
- name: Download release assets
|
- name: Download release assets
|
||||||
run: |
|
run: |
|
||||||
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
|
||||||
| grep "browser_download_url.*dist.zip" \
|
| grep "browser_download_url.*bot.zip" \
|
||||||
| cut -d : -f 2,3 \
|
| cut -d : -f 2,3 \
|
||||||
| tr -d \")
|
| tr -d \")
|
||||||
curl -L -o dist.zip $DOWNLOAD_URL
|
curl -L -o bot.zip $DOWNLOAD_URL
|
||||||
unzip dist.zip
|
unzip bot.zip
|
||||||
|
|
||||||
- name: Run bot
|
- name: Run bot
|
||||||
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
|
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot/index.cjs
|
||||||
+3
-1
@@ -9,6 +9,8 @@ dist.*
|
|||||||
yarn-error.log
|
yarn-error.log
|
||||||
commands.txt
|
commands.txt
|
||||||
/test.*
|
/test.*
|
||||||
|
test.*
|
||||||
/docs/.vitepress/dist
|
/docs/.vitepress/dist
|
||||||
/docs/.vitepress/cache
|
/docs/.vitepress/cache
|
||||||
servers.csv
|
servers.csv
|
||||||
|
.next/
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
import { compareVersions } from 'compare-versions';
|
||||||
|
import { readdirSync } from 'fs';
|
||||||
|
import { parse } from 'path';
|
||||||
|
import { defineConfig } from 'vitepress';
|
||||||
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -9,17 +12,27 @@ export default defineConfig({
|
|||||||
themeConfig: {
|
themeConfig: {
|
||||||
nav: [
|
nav: [
|
||||||
{ text: 'Home', link: '/' },
|
{ text: 'Home', link: '/' },
|
||||||
{ text: 'Examples', link: '/markdown-examples' },
|
{ text: 'Guide', link: '/guide/about' },
|
||||||
],
|
],
|
||||||
|
|
||||||
sidebar: getSidebar({
|
sidebar: [
|
||||||
collapsed: false,
|
...getSidebar({
|
||||||
contentDirs: ['guide', 'legal', 'changelogs'],
|
collapsed: false,
|
||||||
contentRoot: '/docs/',
|
contentDirs: ['guide', 'legal'],
|
||||||
}) as DefaultTheme.Sidebar,
|
contentRoot: '/docs/',
|
||||||
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'Changelogs',
|
||||||
|
items: readdirSync('./docs/changelogs')
|
||||||
|
.map((file) => parse(file).name)
|
||||||
|
.sort(compareVersions)
|
||||||
|
.map((name) => ({ text: name, link: `/changelogs/${name}.html` })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
socialLinks: [
|
socialLinks: [
|
||||||
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
|
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -12,7 +12,7 @@ I am going on vacation so I thought I'd publish this a bit prematurely without a
|
|||||||
- Blitzkrieg now internally generates and keeps a permanent record of all leaderboards updated every `UTC+0` midnight
|
- Blitzkrieg now internally generates and keeps a permanent record of all leaderboards updated every `UTC+0` midnight
|
||||||
- This approach will allow us to capture the absolute latest state of the leaderboard before it is wiped for the next season
|
- This approach will allow us to capture the absolute latest state of the leaderboard before it is wiped for the next season
|
||||||
|
|
||||||
## Fixed
|
## Fixes
|
||||||
|
|
||||||
- Removed region "Europe" in `/verify` example
|
- Removed region "Europe" in `/verify` example
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Blitzkrieg 1.12.0
|
||||||
|
|
||||||
|
_Houston, permission to return to Earth? A new update dropped._
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `multi-tank` filters to `/stats` and `/full-stats`
|
||||||
|
- This lets you pull up the stats by combining filters of `nation`, `tank-type`, `tier`, and `tree-type`
|
||||||
|
- Added `/ratings neighbours`
|
||||||
|
- Lets you see your rank and the neighbours around you
|
||||||
|
- Added a bunch more `/today` parameters (thank you for the suggestions, HisRoyalFatness!)
|
||||||
|
- `cutoff`: the max number of rows to show (excluding the total row)
|
||||||
|
- `maximized`: the max number of rows that show the full non-collapsed stats (default: `4`)
|
||||||
|
- `show-total`: show the total row or not (default: `true`)
|
||||||
|
- Tank search is now accent-tolerant
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/ratings` has been moved out of preview and is now accessible to all
|
||||||
|
- `/today` now has a more appealing look with more efficient space management
|
||||||
|
- `/stats`, `/full-stats`, and `/evolution` now have less convoluted commands
|
||||||
|
- All periods now fall under `period` and `custom`
|
||||||
|
- `period` has preset periods like `today`, `career`, `30`, `60`, `90`
|
||||||
|
- `custom` lets you set a start and end day
|
||||||
|
- Added dashes to command names, subcommands, options, etc. to make them more readable
|
||||||
|
- Titlebar
|
||||||
|
- Removed the Blitzkrieg logo
|
||||||
|
- Removed region
|
||||||
|
- Center aligned items
|
||||||
|
- Footers are now center alinged when they do no have footer text on the left
|
||||||
|
- Moved several title discriminators into the description
|
||||||
|
- `/ratings` is now data-driven
|
||||||
|
- Embeds now auto refresh every `30` seconds instead of `60`
|
||||||
|
- Added numbers besides roman numerals in tier choice
|
||||||
|
- I forgot to implement this in places other than `/owned-tanks`... oh well!
|
||||||
|
- Increased precision of winrate in `/evolution` to `2` decimals
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed duplicate "Efficiency" cards in `/full-stats`
|
||||||
|
- Titlebar gap is now `4px` just like Figma
|
||||||
|
- Renamed `/statsfull` to `/full-stats` in the `/stats` footer
|
||||||
|
- Rewards in `/ratings` are now properly aligned
|
||||||
|
- `/ratings` now uses the correct source of information per region instead of getting it from a cached North American source
|
||||||
|
- Search parameters are now passed onto the URL parser correctly
|
||||||
|
- Custom date ranges now work correctly once again as the start is now the maximum days ago from today and the end is the minimum
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- All generated images are now `480px` wide instead of `640px` to combat the unnecessary amounts of negative space
|
||||||
|
- Changed all relevant typings to be sourced from `stats` instead of `full-stats`
|
||||||
|
- Added minimum character limit to clan search
|
||||||
|
- Username auto-completing now only takes in regex-accepted parts of the input
|
||||||
|
- Search parameters are now longer automatically generated by inferring command interactions
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Blitzkrieg 1.12.1
|
||||||
|
|
||||||
|
A duo of nasty bugs fixed.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed a tank index and id mismatch
|
||||||
|
- Reverted username pattern infrence in command username inputs
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Blitzkrieg 1.12.2
|
||||||
|
|
||||||
|
A much needed room service is here!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Tier selection is now easier
|
||||||
|
- Tier options across all commands have been unified to a single style
|
||||||
|
- Tiers are now sorted from `10` to `1`
|
||||||
|
- Tiers now show up in the format `Tier {tier_number} - {tier_roman_numeral}` (example: `Tier 8 - VIII`)
|
||||||
|
- Removed powered by footer as links are provided regardless
|
||||||
|
- Changed invite instructions to instead use application directory
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Guide link on the website is now fixed
|
||||||
|
- Versions now show up in the correct order in the navigation bar
|
||||||
|
- Added Blitzkrieg's GitHub link instead of the default Vitepress
|
||||||
|
- Changed examples to guide in the navigation bar
|
||||||
|
- Clan name and icon now show up with multi-tank filters
|
||||||
|
- Removed a random `console.log`
|
||||||
|
|
||||||
|
## Technical Changes
|
||||||
|
|
||||||
|
- Added `knip`
|
||||||
|
- Removed a lot of unused files and dependencies
|
||||||
|
- Updated all dependencies to their latest
|
||||||
|
- Code split `bot.ts`
|
||||||
|
- `client` and `octokit` now reside separately
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.12.3
|
||||||
|
|
||||||
|
3-day downtime fixed!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Split `/ratings` into `/ratings current` and `/ratings archive`
|
||||||
|
- `current` now properly handles when there is no ongoing ratings season
|
||||||
|
- `archive` displays older archived seasons
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The bot is now no ratings seasons tolerant
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.12.4
|
||||||
|
|
||||||
|
Added very badly-coded temporary `/clan-event` command (without linting)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added `/clan-event` command
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Blitzkrieg 1.12.5
|
||||||
|
|
||||||
|
Some patch up work
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/clan-event` now is more legible and comes with an approximation warning
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- `/clan-event` now uses a more optimized way of fetching large data
|
||||||
|
- This should fix the issue where too many people using the command broke it
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Blitzkrieg 1.12.6
|
||||||
|
|
||||||
|
Stops making the bot lie!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- The approximation warning for `/clan-event` has been made more concise
|
||||||
|
- Players with `0` wins are now omitted
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Players who haven't player within the current day are now omitted
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Blitzkrieg 1.12.7
|
||||||
|
|
||||||
|
A few fixes and clarifications
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- `/clan-event`
|
||||||
|
- Made statement about the limited publicly available data
|
||||||
|
- Added a statement about reset times
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Commands with images now once again work
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
# Inviting the Bot
|
# Inviting the Bot
|
||||||
|
|
||||||
It's quite simple really. Just follow the steps below:
|
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
|
||||||
|
|
||||||
1. Join a Discord server that already has Blitzkrieg installed or [the official Discord Server](https://discord.gg/nDt7AjGJQH).
|
|
||||||
2. Click on the bot's profile.
|
|
||||||
3. Click the "Add to Server" button under the bot's name and ID.
|
|
||||||
4. Select what server you want it in and see what permissions you seem fit for the bot.
|
|
||||||
|
|
||||||
Voila! You're ready to go.
|
|
||||||
+1
-1
@@ -15,7 +15,7 @@ hero:
|
|||||||
link: guide/install
|
link: guide/install
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: Guide
|
text: Guide
|
||||||
link: /guide
|
link: /guide/about
|
||||||
|
|
||||||
features:
|
features:
|
||||||
- title: Clean and Efficient UI
|
- title: Clean and Efficient UI
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
distDir: 'dist/website',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"main": "server.cjs",
|
"main": "server.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-24
@@ -1,55 +1,63 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.10.2",
|
"version": "1.12.7",
|
||||||
"description": "",
|
"description": "🎉 All-in-one Discord bot for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bot": "vite-node --script scripts/build --build=bot",
|
"build": "node scripts/build && yarn build:website",
|
||||||
"build:server": "vite-node --script scripts/build --build=server",
|
"build:bot": "node scripts/build --build=bot",
|
||||||
|
"build:server": "node scripts/build --build=server",
|
||||||
|
"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",
|
||||||
"build": "vite-node --script scripts/build --build=all",
|
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||||
"dev:bot": "yarn build:bot --dev && yarn start:bot",
|
"dev:server": "yarn build:server && yarn start:server",
|
||||||
"dev:server": "yarn build:server --dev && yarn start:server",
|
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
|
"dev:website": "next dev",
|
||||||
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
|
"start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs",
|
||||||
|
"start:server": "node --enable-source-maps --trace-warnings dist/server/index.cjs",
|
||||||
|
"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",
|
||||||
"zip": "zip -r dist.zip dist",
|
"zip": "zip -r dist.zip dist",
|
||||||
"lint": "npx tsc",
|
"lint": "npx tsc",
|
||||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\""
|
"lint:website": "next lint",
|
||||||
|
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
|
||||||
|
"knip": "knip",
|
||||||
|
"update": "yarn upgrade --latest"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/rest": "^20.0.1",
|
"@octokit/rest": "^20.0.1",
|
||||||
"@radix-ui/colors": "^0.1.8",
|
"@radix-ui/colors": "^2.1.0",
|
||||||
"@resvg/resvg-js": "^2.4.1",
|
"@resvg/resvg-js": "^2.4.1",
|
||||||
"bepaint": "^0.1.0-beta.3",
|
"bepaint": "^0.1.0-beta.3",
|
||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.13.0",
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.19.2",
|
||||||
"escape-html": "^1.0.3",
|
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"false": "^0.0.4",
|
|
||||||
"fuzzysort": "^2.0.4",
|
"fuzzysort": "^2.0.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-escape": "^2.0.0",
|
"markdown-escape": "^2.0.0",
|
||||||
|
"next": "^13.4.19",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"satori": "^0.9.1",
|
"react-dom": "^18.2.0",
|
||||||
"vite-node": "^0.33.0",
|
"satori": "^0.10.4",
|
||||||
|
"vite-node": "^0.34.4",
|
||||||
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@total-typescript/ts-reset": "^0.4.2",
|
"@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.17",
|
||||||
"@types/lodash": "^4.14.195",
|
"@types/lodash": "^4.14.198",
|
||||||
"@types/markdown-escape": "^1.1.0",
|
"@types/markdown-escape": "^1.1.0",
|
||||||
"@types/node": "^20.2.1",
|
"@types/node": "^20.6.0",
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.21",
|
||||||
"prettier": "^3.0.0",
|
"compare-versions": "^6.1.0",
|
||||||
"typescript": "^5.0.4",
|
"knip": "^2.22.0",
|
||||||
"vitepress": "^1.0.0-beta.3"
|
"prettier": "^3.0.3",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vitepress": "^1.0.0-rc.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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}`);
|
||||||
|
});
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { BuildOptions, build } from 'esbuild';
|
|
||||||
import { copyFile, mkdir, rm } from 'fs/promises';
|
|
||||||
import { argv } from 'process';
|
|
||||||
|
|
||||||
const isProd = !argv.includes('--dev');
|
|
||||||
const buildAll = argv.includes('--build=all');
|
|
||||||
const buildBot = argv.includes('--build=bot') || buildAll;
|
|
||||||
const buildServer = argv.includes('--build=server') || buildAll;
|
|
||||||
|
|
||||||
if (isProd) {
|
|
||||||
// only remove in production to ensure no time is wasted in dev
|
|
||||||
console.log('Removing dist...');
|
|
||||||
await rm('dist', { recursive: true, force: true });
|
|
||||||
console.log('Dist removed');
|
|
||||||
|
|
||||||
console.log('Creating dist...');
|
|
||||||
await mkdir('dist');
|
|
||||||
console.log('Dist created');
|
|
||||||
|
|
||||||
console.log('Copying package.json...');
|
|
||||||
copyFile('package.dist.json', 'dist/package.json').then(() =>
|
|
||||||
console.log('package.json copied'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonOptions: BuildOptions = {
|
|
||||||
platform: 'node',
|
|
||||||
loader: {
|
|
||||||
'.node': 'copy',
|
|
||||||
'.ttf': 'file',
|
|
||||||
},
|
|
||||||
|
|
||||||
bundle: true,
|
|
||||||
sourcemap: true,
|
|
||||||
minifyIdentifiers: false, // causes errors
|
|
||||||
minifySyntax: isProd,
|
|
||||||
minifyWhitespace: isProd,
|
|
||||||
|
|
||||||
logLevel: isProd ? 'info' : 'silent',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (buildBot) {
|
|
||||||
console.log('Building bot...');
|
|
||||||
build({
|
|
||||||
...commonOptions,
|
|
||||||
|
|
||||||
entryPoints: ['src/bot.ts'],
|
|
||||||
outfile: 'dist/bot.cjs',
|
|
||||||
}).then(() => console.log('Bot built'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildServer) {
|
|
||||||
console.log('Building server...');
|
|
||||||
build({
|
|
||||||
...commonOptions,
|
|
||||||
|
|
||||||
entryPoints: ['src/server.ts'],
|
|
||||||
outfile: 'dist/server.cjs',
|
|
||||||
}).then(() => {
|
|
||||||
console.log('Server built');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,11 @@ const PLAYERS = await fetch(
|
|||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
)
|
)
|
||||||
.then((response) => response.json() as Promise<RatingsInfo>)
|
.then((response) => response.json() as Promise<RatingsInfo>)
|
||||||
.then((data) => data.count);
|
.then((data) => (data.detail === undefined ? data.count : undefined));
|
||||||
|
|
||||||
|
if (PLAYERS === undefined) {
|
||||||
|
throw new Error('No current ratings season running');
|
||||||
|
}
|
||||||
|
|
||||||
const players: Record<number, RatingsPlayer> = {};
|
const players: Record<number, RatingsPlayer> = {};
|
||||||
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
||||||
@@ -60,7 +64,7 @@ async function branchFromPlayer(id: number) {
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Registered ${registered} / ${PLAYERS} (${(
|
`Registered ${registered} / ${PLAYERS} (${(
|
||||||
(registered / PLAYERS) *
|
(registered / PLAYERS!) *
|
||||||
100
|
100
|
||||||
).toFixed(2)}%)`,
|
).toFixed(2)}%)`,
|
||||||
);
|
);
|
||||||
@@ -109,7 +113,10 @@ if (publish) {
|
|||||||
|
|
||||||
const info = await fetch(
|
const info = await fetch(
|
||||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
).then((response) => response.json() as Promise<RatingsInfo>);
|
).then(
|
||||||
|
(response) =>
|
||||||
|
response.json() as Promise<RatingsInfo & { detail: undefined }>,
|
||||||
|
);
|
||||||
const normalizedServer = server === 'na' ? 'com' : server;
|
const normalizedServer = server === 'na' ? 'com' : server;
|
||||||
|
|
||||||
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import Navbar from '../components/Navbar';
|
||||||
|
|
||||||
|
interface RootLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <h1>Hello, Next.js!</h1>;
|
||||||
|
}
|
||||||
+1
-14
@@ -1,19 +1,6 @@
|
|||||||
import { Octokit } from '@octokit/rest';
|
import { client } from './core/discord/client';
|
||||||
import { Client, GatewayIntentBits } from 'discord.js';
|
|
||||||
import { secrets } from './core/node/secrets';
|
import { secrets } from './core/node/secrets';
|
||||||
import { registerErrorHandlers } from './events/error';
|
import { registerErrorHandlers } from './events/error';
|
||||||
import guildMemberAdd from './events/guildMemberAdd';
|
|
||||||
import interactionCreate from './events/interactionCreate';
|
|
||||||
import ready from './events/ready';
|
|
||||||
|
|
||||||
export const client = new Client({
|
|
||||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
|
|
||||||
})
|
|
||||||
.on('ready', ready)
|
|
||||||
.on('guildMemberAdd', guildMemberAdd)
|
|
||||||
.on('interactionCreate', interactionCreate);
|
|
||||||
|
|
||||||
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
|
||||||
|
|
||||||
registerErrorHandlers();
|
registerErrorHandlers();
|
||||||
client.login(secrets.DISCORD_TOKEN);
|
client.login(secrets.DISCORD_TOKEN);
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
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 = {
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: new SlashCommandBuilder()
|
||||||
|
.setName('clan-event')
|
||||||
|
.setDescription('Lists all inactive players')
|
||||||
|
.addStringOption(addClanChoices),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
|
const clan = (
|
||||||
|
await getWargamingResponse<ClanInfo>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${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 [
|
||||||
|
`# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
|
||||||
|
clan.tag,
|
||||||
|
)}]'s platoon wins today: ${(
|
||||||
|
jointVictories.reduce(
|
||||||
|
(accumulator, { joinVictory }) => accumulator + joinVictory,
|
||||||
|
0,
|
||||||
|
) / 2
|
||||||
|
).toFixed(0)}\n${jointVictories
|
||||||
|
.filter(({ joinVictory }) => joinVictory)
|
||||||
|
.sort((a, b) => b.joinVictory - a.joinVictory)
|
||||||
|
.map(
|
||||||
|
({ id, joinVictory }) =>
|
||||||
|
`- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
|
||||||
|
)
|
||||||
|
.join('\n')}`,
|
||||||
|
|
||||||
|
embedWarning(
|
||||||
|
'This is an approximation!',
|
||||||
|
"Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/help timezones` for more info)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteClan,
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
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 { client } from '../bot';
|
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
import { tankopedia } from '../core/blitz/tankopedia';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
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 getClientId from '../core/node/getClientId';
|
||||||
@@ -12,7 +12,6 @@ const executionStart = new Date().getTime();
|
|||||||
|
|
||||||
export const debugCommand: CommandRegistry = {
|
export const debugCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
|
|||||||
|
|
||||||
export const eligibleCommand: CommandRegistry = {
|
export const eligibleCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: false,
|
inPublic: false,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
|
|||||||
+73
-67
@@ -13,75 +13,81 @@ import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
|||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
import { secrets } from '../core/node/secrets';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import evolution from '../renderers/evolution';
|
import evolution from '../renderers/evolution';
|
||||||
import { StatType } from '../renderers/fullStats';
|
import { StatType } from '../renderers/stats';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
import { AccountInfo } from '../types/accountInfo';
|
||||||
|
|
||||||
export const evolutionCommand: CommandRegistry = {
|
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
||||||
inProduction: true,
|
async (resolve) => {
|
||||||
inDevelopment: false,
|
const command = await addStatTypeSubCommandGroups(
|
||||||
inPublic: true,
|
new SlashCommandBuilder()
|
||||||
|
.setName('evolution')
|
||||||
command: addStatTypeSubCommandGroups(
|
.setDescription('Evolution of statistics'),
|
||||||
new SlashCommandBuilder()
|
false,
|
||||||
.setName('evolution')
|
|
||||||
.setDescription('Evolution of statistics'),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
|
||||||
true,
|
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
|
||||||
const tankId =
|
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
|
||||||
const start = interaction.options.getInteger('start');
|
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
tankId,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
await evolution(commandGroup, period, player, tankId),
|
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
|
||||||
linkButton(
|
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}${
|
|
||||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
|
||||||
}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
|
||||||
autocompleteUsername(interaction);
|
|
||||||
autocompleteTanks(interaction);
|
|
||||||
},
|
|
||||||
|
|
||||||
async button(interaction) {
|
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
|
||||||
const commandGroup = path[1] as StatType;
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
|
||||||
|
|
||||||
return await evolution(
|
|
||||||
commandGroup,
|
|
||||||
period,
|
|
||||||
player,
|
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const commandGroup = interaction.options.getSubcommandGroup(
|
||||||
|
true,
|
||||||
|
) as StatType;
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
|
const tankId =
|
||||||
|
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||||
|
const start = interaction.options.getInteger('start');
|
||||||
|
const end = interaction.options.getInteger('end');
|
||||||
|
const { nickname } = (
|
||||||
|
await getWargamingResponse<AccountInfo>(
|
||||||
|
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||||
|
)
|
||||||
|
)[player.id];
|
||||||
|
const path = interactionToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
tankId,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
await evolution(commandGroup, period, player, tankId),
|
||||||
|
primaryButton(path, 'Refresh'),
|
||||||
|
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||||
|
linkButton(
|
||||||
|
`https://www.blitzstars.com/player/${player.region}/${nickname}${
|
||||||
|
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
||||||
|
}`,
|
||||||
|
'BlitzStars',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||||
|
const path = url.pathname.split('/').filter(Boolean);
|
||||||
|
const commandGroup = path[1] as StatType;
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
|
||||||
|
return await evolution(
|
||||||
|
commandGroup,
|
||||||
|
period,
|
||||||
|
player,
|
||||||
|
parseInt(url.searchParams.get('tankId')!),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
);
|
||||||
+95
-65
@@ -1,4 +1,5 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { TreeTypeString } from '../components/Tanks';
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
import { CYCLIC_API } from '../constants/cyclic';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
@@ -13,72 +14,101 @@ import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
|||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
import { secrets } from '../core/node/secrets';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import fullStats, { StatType } from '../renderers/fullStats';
|
import fullStats from '../renderers/fullStats';
|
||||||
|
import { MultiTankFilters, StatType } from '../renderers/stats';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
import { AccountInfo } from '../types/accountInfo';
|
||||||
|
|
||||||
export const fullStatsCommand: CommandRegistry = {
|
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
||||||
inProduction: true,
|
async (resolve) => {
|
||||||
inDevelopment: false,
|
const command = await addStatTypeSubCommandGroups(
|
||||||
inPublic: true,
|
new SlashCommandBuilder()
|
||||||
|
.setName('full-stats')
|
||||||
command: addStatTypeSubCommandGroups(
|
.setDescription('Full in-game statistics'),
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('fullstats')
|
|
||||||
.setDescription('Full in-game statistics'),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
|
||||||
true,
|
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
|
||||||
const tankId =
|
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
|
||||||
const start = interaction.options.getInteger('start');
|
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
tankId,
|
|
||||||
start,
|
|
||||||
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];
|
|
||||||
|
|
||||||
return [
|
|
||||||
await fullStats(commandGroup, period, player, tankId),
|
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
|
||||||
linkButton(
|
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
|
||||||
autocompleteUsername(interaction);
|
|
||||||
autocompleteTanks(interaction);
|
|
||||||
},
|
|
||||||
|
|
||||||
async button(interaction) {
|
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
|
||||||
const commandGroup = path[1] as StatType;
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
|
||||||
|
|
||||||
return await fullStats(
|
|
||||||
commandGroup,
|
|
||||||
period,
|
|
||||||
player,
|
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const commandGroup = interaction.options.getSubcommandGroup(
|
||||||
|
true,
|
||||||
|
) as StatType;
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const resolvedPeriod = resolvePeriodFromCommand(
|
||||||
|
player.region,
|
||||||
|
interaction,
|
||||||
|
);
|
||||||
|
const period = interaction.options.getString('period');
|
||||||
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
|
const tankId =
|
||||||
|
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||||
|
const start = interaction.options.getInteger('start');
|
||||||
|
const end = interaction.options.getInteger('end');
|
||||||
|
const path = interactionToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
tankId,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
period,
|
||||||
|
});
|
||||||
|
const { nickname } = (
|
||||||
|
await getWargamingResponse<AccountInfo>(
|
||||||
|
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||||
|
)
|
||||||
|
)[player.id];
|
||||||
|
|
||||||
|
const tierOption = interaction.options.getString('tier');
|
||||||
|
|
||||||
|
return [
|
||||||
|
await fullStats(
|
||||||
|
commandGroup,
|
||||||
|
resolvedPeriod,
|
||||||
|
player,
|
||||||
|
commandGroup === 'tank'
|
||||||
|
? tankId
|
||||||
|
: commandGroup === 'multi-tank'
|
||||||
|
? ({
|
||||||
|
'tank-type':
|
||||||
|
interaction.options.getString('tank-type') ?? undefined,
|
||||||
|
nation: interaction.options.getString('nation') ?? undefined,
|
||||||
|
tier: tierOption ? parseInt(tierOption) : undefined,
|
||||||
|
'tree-type': (interaction.options.getString('tree-type') ??
|
||||||
|
undefined) as TreeTypeString | undefined,
|
||||||
|
} satisfies Partial<MultiTankFilters>)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
primaryButton(path, 'Refresh'),
|
||||||
|
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||||
|
linkButton(
|
||||||
|
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||||
|
'BlitzStars',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||||
|
const path = url.pathname.split('/').filter(Boolean);
|
||||||
|
const commandGroup = path[1] as StatType;
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
|
||||||
|
return await fullStats(
|
||||||
|
commandGroup,
|
||||||
|
period,
|
||||||
|
player,
|
||||||
|
parseInt(url.searchParams.get('tankId')!),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
);
|
||||||
@@ -30,7 +30,6 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
|||||||
|
|
||||||
export const helpCommand: CommandRegistry = {
|
export const helpCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: addDocsSubcommands(
|
command: addDocsSubcommands(
|
||||||
@@ -44,13 +43,14 @@ export const helpCommand: CommandRegistry = {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
handler(interaction) {
|
async handler(interaction) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
if (subcommand === 'commands') {
|
if (subcommand === 'commands') {
|
||||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${COMMANDS_RAW.filter(
|
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
||||||
(registry) => registry.inPublic && registry.inProduction,
|
await Promise.all(COMMANDS_RAW)
|
||||||
)
|
)
|
||||||
|
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||||
.map(
|
.map(
|
||||||
(registry) =>
|
(registry) =>
|
||||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import GenericStats from '../components/GenericStats';
|
import GenericStats from '../components/GenericStats';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES } from '../constants/regions';
|
|
||||||
import getBlitzClan from '../core/blitz/getBlitzClan';
|
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
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 { secrets } from '../core/node/secrets';
|
import { secrets } from '../core/node/secrets';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
import { AccountInfo } from '../types/accountInfo';
|
||||||
@@ -18,7 +16,6 @@ const DEFAULT_THRESHOLD = 7;
|
|||||||
|
|
||||||
export const inactiveCommand: CommandRegistry = {
|
export const inactiveCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
@@ -35,19 +32,18 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
),
|
),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const clanName = interaction.options.getString('clan')!;
|
const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
const { server, id } = await getBlitzClan(clanName);
|
|
||||||
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 clanData = (
|
||||||
await getWargamingResponse<ClanInfo>(
|
await getWargamingResponse<ClanInfo>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||||
)
|
)
|
||||||
)[id];
|
)[id];
|
||||||
const memberIds = clanData.members_ids;
|
const memberIds = clanData.members_ids;
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
const accounts = await getWargamingResponse<AccountInfo>(
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${
|
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${
|
||||||
secrets.WARGAMING_APPLICATION_ID
|
secrets.WARGAMING_APPLICATION_ID
|
||||||
}&account_id=${memberIds.join(',')}`,
|
}&account_id=${memberIds.join(',')}`,
|
||||||
);
|
);
|
||||||
@@ -69,17 +65,12 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={clanData.name}
|
name={clanData.name}
|
||||||
nameDiscriminator="(Inactivity)"
|
|
||||||
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-${clanData.emblem_set_id}.png`}
|
||||||
description={`${threshold}+ Days • ${new Date().toDateString()} • ${
|
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
||||||
REGION_NAMES[server]
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
||||||
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
{hasInactiveMembers && <GenericStats stats={inactive} />}
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.Wargaming} />
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+87
-64
@@ -1,19 +1,20 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
|
||||||
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 { REGION_NAMES } from '../constants/regions';
|
|
||||||
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 getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
import {
|
import {
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
|
TankopediaEntry,
|
||||||
Tier,
|
Tier,
|
||||||
tankopedia,
|
tankopedia,
|
||||||
|
tankopediaInfo,
|
||||||
} from '../core/blitz/tankopedia';
|
} from '../core/blitz/tankopedia';
|
||||||
|
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';
|
||||||
@@ -45,30 +46,12 @@ const COMP_TANKS = [
|
|||||||
|
|
||||||
export const ownedTanksCommand: CommandRegistry = {
|
export const ownedTanksCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('ownedtanks')
|
.setName('owned-tanks')
|
||||||
.setDescription("Shows a player's owned tanks")
|
.setDescription("Shows a player's owned tanks")
|
||||||
.addStringOption((option) =>
|
.addStringOption(addTierChoices)
|
||||||
option
|
|
||||||
.setName('tier')
|
|
||||||
.setDescription('The tier you want to see')
|
|
||||||
.setChoices(
|
|
||||||
{ name: 'Tier I', value: '1' },
|
|
||||||
{ name: 'Tier II', value: '2' },
|
|
||||||
{ name: 'Tier III', value: '3' },
|
|
||||||
{ name: 'Tier IV', value: '4' },
|
|
||||||
{ name: 'Tier V', value: '5' },
|
|
||||||
{ name: 'Tier VI', value: '6' },
|
|
||||||
{ name: 'Tier VII', value: '7' },
|
|
||||||
{ name: 'Tier VIII', value: '8' },
|
|
||||||
{ name: 'Tier IX', value: '9' },
|
|
||||||
{ name: 'Tier X', value: '10' },
|
|
||||||
)
|
|
||||||
.setRequired(true),
|
|
||||||
)
|
|
||||||
.addStringOption(addUsernameChoices),
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
@@ -79,73 +62,113 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${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 tanks = (
|
const filteredTanks = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tankStats.map(async (tankData) => ({
|
tankStats.map(async (tankData) => ({
|
||||||
...(await tankopedia)[tankData.tank_id],
|
tankopedia: (await tankopedia)[tankData.tank_id]!,
|
||||||
tank_id: tankData.tank_id,
|
tank_id: tankData.tank_id,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).filter((tank) => tank?.tier === tier);
|
).filter((tank) => tank.tankopedia?.tier === tier);
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||||
);
|
);
|
||||||
const leftColumnSize = Math.ceil(tanks.length / 2);
|
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
||||||
const leftColumn = tanks.slice(0, leftColumnSize);
|
const nations: string[] = [];
|
||||||
const rightColumn = tanks.slice(leftColumnSize);
|
|
||||||
|
filteredTanks.forEach((tank) => {
|
||||||
|
if (groupedTanks[tank.tankopedia.nation] === undefined) {
|
||||||
|
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
|
||||||
|
nations.push(tank.tankopedia.nation);
|
||||||
|
} else {
|
||||||
|
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nations.sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo[id].nickname}
|
||||||
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`}
|
nameDiscriminator={
|
||||||
|
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
||||||
|
}
|
||||||
image={
|
image={
|
||||||
clanData[id]?.clan
|
clanData[id]?.clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
description={`Owned tanks • ${new Date().toDateString()} • ${
|
description={`Tier ${
|
||||||
REGION_NAMES[server]
|
TIER_ROMAN_NUMERALS[tier as Tier]
|
||||||
}`}
|
} tanks • ${new Date().toDateString()}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{tanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||||
|
|
||||||
{tanks.length > 0 && (
|
{filteredTanks.length > 0 &&
|
||||||
<Tanks.Root>
|
(await Promise.all(
|
||||||
<Tanks.Column>
|
nations.map(async (nation) => {
|
||||||
{await Promise.all(
|
const tanks = groupedTanks[nation];
|
||||||
leftColumn.map(async (tank) => (
|
const leftColumnSize = Math.ceil(tanks.length / 2);
|
||||||
<Tanks.Item
|
const leftColumn = tanks.slice(0, leftColumnSize);
|
||||||
key={tank.tank_id}
|
const rightColumn = tanks.slice(leftColumnSize);
|
||||||
name={await resolveTankName(tank.tank_id)}
|
|
||||||
tankType={tank.type}
|
|
||||||
image={tank.images?.normal}
|
|
||||||
treeType={await getTreeType(tank.tank_id)}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
)}
|
|
||||||
</Tanks.Column>
|
|
||||||
<Tanks.Column>
|
|
||||||
{await Promise.all(
|
|
||||||
rightColumn.map(async (tank) => (
|
|
||||||
<Tanks.Item
|
|
||||||
key={tank.tank_id}
|
|
||||||
name={await resolveTankName(tank.tank_id)}
|
|
||||||
tankType={tank.type}
|
|
||||||
image={tank.images?.normal}
|
|
||||||
treeType={await getTreeType(tank.tank_id)}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
)}
|
|
||||||
</Tanks.Column>
|
|
||||||
</Tanks.Root>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.Wargaming} />
|
return (
|
||||||
|
<Tanks.Root>
|
||||||
|
<Tanks.Title>
|
||||||
|
{(await tankopediaInfo).vehicle_nations[nation]}
|
||||||
|
</Tanks.Title>
|
||||||
|
|
||||||
|
<Tanks.Row>
|
||||||
|
<Tanks.Column>
|
||||||
|
{await Promise.all(
|
||||||
|
leftColumn.map(async (tank) => (
|
||||||
|
<Tanks.Item
|
||||||
|
key={tank.tank_id}
|
||||||
|
name={await resolveTankName(tank.tank_id)}
|
||||||
|
tankType={tank.type}
|
||||||
|
image={tank.images?.normal}
|
||||||
|
treeType={await getTreeType(tank.tank_id)}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</Tanks.Column>
|
||||||
|
<Tanks.Column>
|
||||||
|
{await Promise.all(
|
||||||
|
rightColumn.map(async (tank) => (
|
||||||
|
<Tanks.Item
|
||||||
|
key={tank.tank_id}
|
||||||
|
name={await resolveTankName(tank.tank_id)}
|
||||||
|
tankType={tank.type}
|
||||||
|
image={tank.images?.normal}
|
||||||
|
treeType={await getTreeType(tank.tank_id)}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</Tanks.Column>
|
||||||
|
</Tanks.Row>
|
||||||
|
</Tanks.Root>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
))}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
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,9 +1,8 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
export const pingCommand: CommandRegistry<true> = {
|
export const pingCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
handlesInteraction: true,
|
handlesInteraction: true,
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,10 @@ type SortBy = 'name' | 'count';
|
|||||||
|
|
||||||
export const playerAchievementsCommand: CommandRegistry = {
|
export const playerAchievementsCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('playerachievements')
|
.setName('player-achievements')
|
||||||
.setDescription("All the player's achievements")
|
.setDescription("All the player's achievements")
|
||||||
.addStringOption(addUsernameChoices)
|
.addStringOption(addUsernameChoices)
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import { AccountInfo } from '../types/accountInfo';
|
|||||||
|
|
||||||
export const playerInfoCommand: CommandRegistry = {
|
export const playerInfoCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('playerinfo')
|
.setName('player-info')
|
||||||
.setDescription('Basic information about a player')
|
.setDescription('Basic information about a player')
|
||||||
.addStringOption(addUsernameChoices),
|
.addStringOption(addUsernameChoices),
|
||||||
|
|
||||||
|
|||||||
+435
-153
@@ -1,13 +1,31 @@
|
|||||||
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
|
import {
|
||||||
|
APIApplicationCommandOptionChoice,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
SlashCommandSubcommandGroupBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||||
import { octokit } from '../bot';
|
|
||||||
import * as Leaderboard from '../components/Leaderboard';
|
import * as Leaderboard from '../components/Leaderboard';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES, Region } from '../constants/regions';
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
|
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
|
||||||
|
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||||
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||||
|
import getMidnightLeaderboard, {
|
||||||
|
DATABASE_REPO,
|
||||||
|
} from '../core/database/getMidnightLeaderboard';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
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 { AccountInfo } from '../types/accountInfo';
|
||||||
|
import { PlayerClanData } from '../types/playerClanData';
|
||||||
|
|
||||||
export interface RatingsPlayer {
|
export interface RatingsPlayer {
|
||||||
spa_id: number;
|
spa_id: number;
|
||||||
@@ -75,188 +93,452 @@ export interface RatingsLeague {
|
|||||||
percentile: number;
|
percentile: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RatingsInfo {
|
export type RatingsInfo =
|
||||||
title: string;
|
| {
|
||||||
icon: string | null;
|
detail: undefined;
|
||||||
start_at: string;
|
|
||||||
finish_at: string;
|
|
||||||
current_season: number;
|
|
||||||
updated_at: string;
|
|
||||||
count: number;
|
|
||||||
|
|
||||||
rewards: RatingsReward[];
|
title: string;
|
||||||
leagues: RatingsLeague[];
|
icon: string | null;
|
||||||
}
|
start_at: string;
|
||||||
|
finish_at: string;
|
||||||
|
current_season: number;
|
||||||
|
updated_at: string;
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
rewards: RatingsReward[];
|
||||||
|
leagues: RatingsLeague[];
|
||||||
|
}
|
||||||
|
| { detail: { error: string } };
|
||||||
|
|
||||||
export interface RatingsNeighbors {
|
export interface RatingsNeighbors {
|
||||||
neighbors: RatingsPlayer[];
|
neighbors: RatingsPlayer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Caching ratings info...');
|
const LEAGUES = [
|
||||||
const ratingsInfo = fetch(
|
{ name: 'Diamond', minScore: 5000 },
|
||||||
'https://na.wotblitz.com/en/api/rating-leaderboards/season/',
|
{ name: 'Platinum', minScore: 4000 },
|
||||||
)
|
{ name: 'Gold', minScore: 3000 },
|
||||||
.then((response) => response.json() as Promise<RatingsInfo>)
|
{ name: 'Silver', minScore: 2000 },
|
||||||
.then((info) => {
|
{ name: 'Bronze', minScore: 0 },
|
||||||
console.log('Cached ratings info');
|
];
|
||||||
return info;
|
|
||||||
});
|
|
||||||
|
|
||||||
function optionalParameters(option: SlashCommandSubcommandBuilder) {
|
function addSubcommands(
|
||||||
|
option: SlashCommandSubcommandGroupBuilder,
|
||||||
|
addOptions = (option: SlashCommandSubcommandBuilder) => option,
|
||||||
|
) {
|
||||||
return option
|
return option
|
||||||
.addStringOption(addRegionChoices)
|
.addSubcommand((option) =>
|
||||||
.addIntegerOption((option) =>
|
addOptions(
|
||||||
option
|
option
|
||||||
.setName('limit')
|
.setName('neighbours')
|
||||||
.setDescription('How many rows to display (default: 10)')
|
.setDescription('Your position and neighbours'),
|
||||||
.setRequired(false)
|
)
|
||||||
.setMinValue(5)
|
.addStringOption(addUsernameChoices)
|
||||||
.setMaxValue(30),
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('limit')
|
||||||
|
.setDescription('How many neighbours to display (default: 10)')
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addSubcommand((option) =>
|
||||||
|
addOptions(option.setName('league').setDescription('Top league players'))
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('league')
|
||||||
|
.setDescription('The league to display')
|
||||||
|
.addChoices(
|
||||||
|
...LEAGUES.map(
|
||||||
|
({ name }, value) =>
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
value: `${value}`,
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption(addRegionChoices)
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('limit')
|
||||||
|
.setDescription('How many rows to display (default: 10)')
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEAGUE_INDEXES = ['diamond', 'platinum', 'gold', 'silver', 'bronze'];
|
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||||
|
async (resolve) => {
|
||||||
|
const seasonChoices = await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: 'com/ratings',
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw throwError('Archived ratings data is malformed');
|
||||||
|
}
|
||||||
|
return data.map(
|
||||||
|
(folder) =>
|
||||||
|
({
|
||||||
|
name: `Season ${folder.name}`,
|
||||||
|
value: folder.name,
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
|
const command = new SlashCommandBuilder()
|
||||||
|
.setName('ratings')
|
||||||
export const ratingsCommand: CommandRegistry = {
|
.setDescription('Ratings battles statistics')
|
||||||
inProduction: true,
|
.addSubcommandGroup((option) =>
|
||||||
inDevelopment: true,
|
addSubcommands(
|
||||||
inPublic: true,
|
option.setName('archive').setDescription('Any previous season'),
|
||||||
inPreview: true,
|
(option) =>
|
||||||
|
option.addStringOption((option) =>
|
||||||
command: new SlashCommandBuilder()
|
option
|
||||||
.setName('ratings')
|
.setName('season')
|
||||||
.setDescription('Ratings battles statistics')
|
.setDescription('The season number')
|
||||||
.addSubcommandGroup((option) =>
|
.setChoices(...seasonChoices)
|
||||||
option
|
.setRequired(true),
|
||||||
.setName('league')
|
),
|
||||||
.setDescription('Top league players')
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
optionalParameters(
|
|
||||||
option
|
|
||||||
.setName('diamond')
|
|
||||||
.setDescription('Top Diamond League players'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
optionalParameters(
|
|
||||||
option
|
|
||||||
.setName('platinum')
|
|
||||||
.setDescription('Top Platinum League players'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
optionalParameters(
|
|
||||||
option.setName('gold').setDescription('Top Gold League players'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
optionalParameters(
|
|
||||||
option
|
|
||||||
.setName('silver')
|
|
||||||
.setDescription('Top Silver League players'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addSubcommand((option) =>
|
|
||||||
optionalParameters(
|
|
||||||
option
|
|
||||||
.setName('bronze')
|
|
||||||
.setDescription('Top Bronze League players'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
const subcommandGroup = interaction.options.getSubcommandGroup(true);
|
|
||||||
|
|
||||||
if (subcommandGroup === 'league') {
|
|
||||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
|
||||||
const subcommand = interaction.options.getSubcommand(true);
|
|
||||||
const leagueIndex = LEAGUE_INDEXES.indexOf(subcommand);
|
|
||||||
const region = interaction.options.getString('region') as Region;
|
|
||||||
const regionSubdomain = regionToRegionSubdomain(region);
|
|
||||||
const result = await fetch(
|
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
|
||||||
)
|
)
|
||||||
.then((response) => response.json() as Promise<LeagueTop>)
|
.addSubcommandGroup((option) =>
|
||||||
.then(({ result }) => result.slice(0, limit));
|
addSubcommands(
|
||||||
const awaitedInfo = await ratingsInfo;
|
option
|
||||||
const leagueInfo = awaitedInfo.leagues[leagueIndex];
|
.setName('current')
|
||||||
const midnightLeaderboard = await octokit.repos
|
.setDescription('The current running Ratings season'),
|
||||||
.getContent({
|
),
|
||||||
...DATABASE_REPO,
|
);
|
||||||
path: `${region}/ratings/${awaitedInfo.current_season}/midnight.json`,
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
if (!Array.isArray(data) && data.type === 'file') {
|
|
||||||
const content = Buffer.from(data.content, 'base64').toString();
|
|
||||||
const jsonContent = JSON.parse(
|
|
||||||
content,
|
|
||||||
) as BlitzkriegRatingsLeaderboard;
|
|
||||||
|
|
||||||
return jsonContent;
|
resolve({
|
||||||
}
|
inProduction: true,
|
||||||
})
|
inPublic: true,
|
||||||
.catch(() => console.warn('No midnight leaderboard found'));
|
|
||||||
const items = await Promise.all(
|
command,
|
||||||
result.map(async (player) => {
|
|
||||||
const reward = awaitedInfo.rewards.find(
|
async handler(interaction) {
|
||||||
(reward) =>
|
interface SimplifiedPlayer {
|
||||||
player.number >= reward.from_position &&
|
id: number;
|
||||||
player.number <= reward.to_position,
|
score: number;
|
||||||
|
position: number;
|
||||||
|
clan: string | undefined;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
|
||||||
|
| 'archive'
|
||||||
|
| 'current';
|
||||||
|
const subcommand = interaction.options.getSubcommand(true) as
|
||||||
|
| 'league'
|
||||||
|
| 'neighbours';
|
||||||
|
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||||
|
const seasonOption = interaction.options.getString('season');
|
||||||
|
const season = seasonOption ? parseInt(seasonOption) : undefined;
|
||||||
|
let titleName: string;
|
||||||
|
let titleImage: string | undefined;
|
||||||
|
let titleNameDiscriminator: string | undefined;
|
||||||
|
let titleDescription: string;
|
||||||
|
let playersBefore: number;
|
||||||
|
let playersAfter: number;
|
||||||
|
let players: SimplifiedPlayer[] | undefined;
|
||||||
|
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
||||||
|
let playerId: number | undefined;
|
||||||
|
let regionRatingsInfo: RatingsInfo;
|
||||||
|
|
||||||
|
if (subcommand === 'league') {
|
||||||
|
const leagueIndex = parseInt(
|
||||||
|
interaction.options.getString('league')!,
|
||||||
);
|
);
|
||||||
|
const region = interaction.options.getString('region') as Region;
|
||||||
|
regionRatingsInfo = await (subcommandGroup === 'current'
|
||||||
|
? getRatingsInfo(region)
|
||||||
|
: getArchivedRatingsInfo(season!, region));
|
||||||
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
|
const result =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? regionRatingsInfo.detail === undefined
|
||||||
|
? await fetch(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
||||||
|
)
|
||||||
|
.then((response) => response.json() as Promise<LeagueTop>)
|
||||||
|
.then(async ({ result }) =>
|
||||||
|
result.slice(0, limit).map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
score: player.score,
|
||||||
|
position: player.number,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
: await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/latest.json`,
|
||||||
|
})
|
||||||
|
.then(async ({ data }) => {
|
||||||
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
|
throw throwError('Archived ratings data is malformed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const players = (await fetch(data.download_url!).then(
|
||||||
|
(response) => response.json(),
|
||||||
|
)) as BlitzkriegRatingsLeaderboard;
|
||||||
|
const firstPlayerIndex =
|
||||||
|
leagueIndex === 0
|
||||||
|
? 0
|
||||||
|
: players.findIndex(
|
||||||
|
(player) =>
|
||||||
|
player.score < LEAGUES[leagueIndex - 1].minScore,
|
||||||
|
);
|
||||||
|
const lastPlayerIndex = firstPlayerIndex + limit - 1;
|
||||||
|
const trimmed = players.slice(
|
||||||
|
firstPlayerIndex,
|
||||||
|
lastPlayerIndex + 1,
|
||||||
|
);
|
||||||
|
const ids = trimmed.map((player) => player.id).join(',');
|
||||||
|
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return trimmed.map(
|
||||||
|
(player, index) =>
|
||||||
|
({
|
||||||
|
id: player.id,
|
||||||
|
score: player.score,
|
||||||
|
position: firstPlayerIndex + index + 1,
|
||||||
|
clan: clanData[player.id]!.clan?.tag,
|
||||||
|
nickname: clanData[player.id]!.account_name,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const leagueInfo =
|
||||||
|
regionRatingsInfo.detail === undefined
|
||||||
|
? regionRatingsInfo.leagues[leagueIndex]
|
||||||
|
: undefined;
|
||||||
|
midnightLeaderboard =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? regionRatingsInfo.detail === undefined
|
||||||
|
? await getMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
regionRatingsInfo.current_season,
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
|
players = result;
|
||||||
|
playersBefore = result ? result[0].position - 1 : 0;
|
||||||
|
playersAfter =
|
||||||
|
regionRatingsInfo.detail === undefined && result !== undefined
|
||||||
|
? regionRatingsInfo.count - result[result.length - 1].position
|
||||||
|
: 0;
|
||||||
|
titleName = `${
|
||||||
|
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
||||||
|
} - ${REGION_NAMES_SHORT[region]}`;
|
||||||
|
titleImage = leagueInfo
|
||||||
|
? leagueInfo.big_icon.startsWith('http')
|
||||||
|
? leagueInfo.big_icon
|
||||||
|
: `https:${leagueInfo.big_icon}`
|
||||||
|
: undefined;
|
||||||
|
titleDescription = `Top ${limit} players`;
|
||||||
|
} else {
|
||||||
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
|
regionRatingsInfo =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? await getRatingsInfo(region)
|
||||||
|
: await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/info.json`,
|
||||||
|
})
|
||||||
|
.then(async ({ data }) => {
|
||||||
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
|
throw throwError('Archived ratings info is malformed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = fetch(data.download_url!);
|
||||||
|
return (await response.then((response) =>
|
||||||
|
response.json(),
|
||||||
|
)) as RatingsInfo;
|
||||||
|
});
|
||||||
|
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||||
|
);
|
||||||
|
const clan = (
|
||||||
|
await getWargamingResponse<PlayerClanData>(
|
||||||
|
`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 neighbors =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? regionRatingsInfo.detail === undefined
|
||||||
|
? await fetch(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||||
|
limit / 2,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
(response) =>
|
||||||
|
response.json() as Promise<RatingsNeighbors>,
|
||||||
|
)
|
||||||
|
.then((players) =>
|
||||||
|
players.neighbors.map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
position: player.number,
|
||||||
|
score: player.score,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
: await octokit.repos
|
||||||
|
.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/latest.json`,
|
||||||
|
})
|
||||||
|
.then(async ({ data }) => {
|
||||||
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
|
throw throwError(
|
||||||
|
'Archived ratings latest data is malformed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = fetch(data.download_url!);
|
||||||
|
const players = (await response.then((response) =>
|
||||||
|
response.json(),
|
||||||
|
)) as BlitzkriegRatingsLeaderboard;
|
||||||
|
const playerIndex = players.findIndex(
|
||||||
|
(player) => player.id === id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerIndex === -1) {
|
||||||
|
throw throwError(
|
||||||
|
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
|
||||||
|
'This player did not participate in this season or did not get past calibration.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const halfRange = Math.round(limit / 2);
|
||||||
|
const trimmed = players.slice(
|
||||||
|
playerIndex - halfRange,
|
||||||
|
playerIndex + halfRange + 1,
|
||||||
|
);
|
||||||
|
const ids = trimmed.map((player) => player.id).join(',');
|
||||||
|
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return trimmed.map((player, index) => ({
|
||||||
|
id: player.id,
|
||||||
|
score: player.score,
|
||||||
|
position: playerIndex + index + 1,
|
||||||
|
clan: clanData[player.id]!.clan?.tag,
|
||||||
|
nickname: clanData[player.id]!.account_name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
midnightLeaderboard =
|
||||||
|
subcommandGroup === 'current'
|
||||||
|
? regionRatingsInfo.detail === undefined
|
||||||
|
? await getMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
regionRatingsInfo.current_season,
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
|
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
||||||
|
|
||||||
|
players = neighbors;
|
||||||
|
titleImage = neighbors
|
||||||
|
? clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
|
: undefined
|
||||||
|
: undefined;
|
||||||
|
titleName = neighbors
|
||||||
|
? accountInfo[id].nickname
|
||||||
|
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
||||||
|
titleDescription = 'Ratings neighbours';
|
||||||
|
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
||||||
|
playersAfter =
|
||||||
|
regionRatingsInfo.detail === undefined
|
||||||
|
? neighbors
|
||||||
|
? regionRatingsInfo.count -
|
||||||
|
neighbors[neighbors.length - 1].position
|
||||||
|
: 0
|
||||||
|
: 0;
|
||||||
|
playerId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = players?.map((player) => {
|
||||||
|
const reward =
|
||||||
|
regionRatingsInfo.detail === undefined
|
||||||
|
? regionRatingsInfo.rewards.find(
|
||||||
|
(reward) =>
|
||||||
|
player.position >= reward.from_position &&
|
||||||
|
player.position <= reward.to_position,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||||
(item) => item.id === player.spa_id,
|
(item) => item.id === player.id,
|
||||||
);
|
);
|
||||||
const midnightScore =
|
const midnightScore =
|
||||||
midnightIndex === undefined
|
midnightIndex === undefined
|
||||||
? player.score
|
? player.score
|
||||||
: midnightLeaderboard![midnightIndex].score;
|
: midnightLeaderboard![midnightIndex].score;
|
||||||
const midnightPosition =
|
const midnightPosition =
|
||||||
midnightIndex === undefined ? player.number : midnightIndex + 1;
|
midnightIndex === undefined ? player.position : midnightIndex + 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Leaderboard.Item
|
<Leaderboard.Item
|
||||||
nickname={player.nickname}
|
nickname={player.nickname}
|
||||||
score={player.score}
|
score={player.score}
|
||||||
position={player.number}
|
position={player.position}
|
||||||
clan={player.clan_tag}
|
clan={player.clan}
|
||||||
reward={reward}
|
reward={reward}
|
||||||
deltaScore={player.score - midnightScore}
|
deltaScore={player.score - midnightScore}
|
||||||
deltaPosition={midnightPosition - player.number}
|
deltaPosition={midnightPosition - player.position}
|
||||||
key={player.spa_id}
|
key={player.id}
|
||||||
|
highlight={player.id === playerId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const playersBefore = result[0].number - 1;
|
return (
|
||||||
const playersAfter = awaitedInfo.count - result[result.length - 1].number;
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={titleName}
|
||||||
|
image={titleImage}
|
||||||
|
nameDiscriminator={titleNameDiscriminator}
|
||||||
|
description={`${titleDescription} • ${new Date().toDateString()}`}
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
{items && (
|
||||||
<Wrapper>
|
<Leaderboard.Root>
|
||||||
<TitleBar
|
{playersBefore > 0 && (
|
||||||
name={leagueInfo.title}
|
<Leaderboard.Gap number={playersBefore} />
|
||||||
image={
|
)}
|
||||||
leagueInfo.big_icon.startsWith('http')
|
{items}
|
||||||
? leagueInfo.big_icon
|
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
||||||
: `https:${leagueInfo.big_icon}`
|
</Leaderboard.Root>
|
||||||
}
|
)}
|
||||||
description={`Top ${limit} players • ${new Date().toDateString()} • ${
|
</Wrapper>
|
||||||
REGION_NAMES[region]
|
);
|
||||||
}`}
|
},
|
||||||
/>
|
|
||||||
|
|
||||||
<Leaderboard.Root>
|
autocomplete: autocompleteUsername,
|
||||||
{playersBefore > 0 && <Leaderboard.Gap number={playersBefore} />}
|
});
|
||||||
{items}
|
|
||||||
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
|
||||||
</Leaderboard.Root>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
},
|
},
|
||||||
};
|
);
|
||||||
@@ -11,11 +11,10 @@ import { ClanList } from '../types/clanList';
|
|||||||
|
|
||||||
export const searchClansCommand: CommandRegistry = {
|
export const searchClansCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('searchclans')
|
.setName('search-clans')
|
||||||
.setDescription('Search clans')
|
.setDescription('Search clans')
|
||||||
.addStringOption(addRegionChoices)
|
.addStringOption(addRegionChoices)
|
||||||
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import { AccountList } from '../types/accountList';
|
|||||||
|
|
||||||
export const searchPlayersCommand: CommandRegistry = {
|
export const searchPlayersCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('searchplayers')
|
.setName('search-players')
|
||||||
.setDescription('Search players in a Blitz server')
|
.setDescription('Search players in a Blitz server')
|
||||||
.addStringOption(addRegionChoices)
|
.addStringOption(addRegionChoices)
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
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 } from '../core/blitz/tankopedia';
|
import { TANK_NAMES_DIACRITICS } from '../core/blitz/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';
|
||||||
|
|
||||||
export const searchTanksCommand: CommandRegistry = {
|
export const searchTanksCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('searchtanks')
|
.setName('search-tanks')
|
||||||
.setDescription('Search tanks')
|
.setDescription('Search tanks')
|
||||||
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
|
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
@@ -26,9 +25,10 @@ export const searchTanksCommand: CommandRegistry = {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const tank = interaction.options.getString('tank')!;
|
const tank = interaction.options.getString('tank')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const results = go(tank, await TANK_NAMES, { limit }).map(
|
const results = go(tank, await TANK_NAMES_DIACRITICS, {
|
||||||
(result) => result.target,
|
limit,
|
||||||
);
|
keys: ['combined'],
|
||||||
|
}).map((result) => result.obj.original);
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`Tank search for "${markdownEscape(tank)}"`,
|
`Tank search for "${markdownEscape(tank)}"`,
|
||||||
|
|||||||
+95
-60
@@ -1,4 +1,5 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { TreeTypeString } from '../components/Tanks';
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
import { CYCLIC_API } from '../constants/cyclic';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
@@ -13,73 +14,107 @@ import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
|||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { secrets } from '../core/node/secrets';
|
import { secrets } from '../core/node/secrets';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import { StatType } from '../renderers/fullStats';
|
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
|
||||||
import stats from '../renderers/stats';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
import { AccountInfo } from '../types/accountInfo';
|
||||||
|
|
||||||
export const statsCommand: CommandRegistry = {
|
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
||||||
inProduction: true,
|
const command = await addStatTypeSubCommandGroups(
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: addStatTypeSubCommandGroups(
|
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('stats')
|
.setName('stats')
|
||||||
.setDescription('Regular battles statistics'),
|
.setDescription('Regular battles statistics'),
|
||||||
),
|
);
|
||||||
|
|
||||||
async handler(interaction) {
|
resolve({
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
inProduction: true,
|
||||||
true,
|
inPublic: true,
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
|
||||||
const tankId =
|
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
|
||||||
const start = interaction.options.getInteger('start');
|
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
tankId,
|
|
||||||
start,
|
|
||||||
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];
|
|
||||||
|
|
||||||
return [
|
command,
|
||||||
await stats(commandGroup, period, player, tankId),
|
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
|
||||||
linkButton(
|
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
async handler(interaction) {
|
||||||
autocompleteUsername(interaction);
|
const commandGroup = interaction.options.getSubcommandGroup(
|
||||||
autocompleteTanks(interaction);
|
true,
|
||||||
},
|
) as StatType;
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const resolvedPeriod = resolvePeriodFromCommand(
|
||||||
|
player.region,
|
||||||
|
interaction,
|
||||||
|
);
|
||||||
|
const period = interaction.options.getString('period');
|
||||||
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
|
const tankId =
|
||||||
|
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||||
|
const start = interaction.options.getInteger('start');
|
||||||
|
const end = interaction.options.getInteger('end');
|
||||||
|
const path = interactionToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
tankId,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
period,
|
||||||
|
});
|
||||||
|
const { nickname } = (
|
||||||
|
await getWargamingResponse<AccountInfo>(
|
||||||
|
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||||
|
)
|
||||||
|
)[player.id];
|
||||||
|
|
||||||
async button(interaction) {
|
const tierOption = interaction.options.getString('tier');
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
|
||||||
const commandGroup = path[1] as StatType;
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
|
||||||
|
|
||||||
return await stats(
|
return [
|
||||||
commandGroup,
|
await stats(
|
||||||
period,
|
commandGroup,
|
||||||
player,
|
resolvedPeriod,
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
player,
|
||||||
);
|
commandGroup === 'tank'
|
||||||
},
|
? tankId
|
||||||
};
|
: commandGroup === 'multi-tank'
|
||||||
|
? ({
|
||||||
|
'tank-type':
|
||||||
|
interaction.options.getString('tank-type') ?? undefined,
|
||||||
|
nation: interaction.options.getString('nation') ?? undefined,
|
||||||
|
tier: tierOption ? parseInt(tierOption) : undefined,
|
||||||
|
'tree-type': (interaction.options.getString('tree-type') ??
|
||||||
|
undefined) as TreeTypeString | undefined,
|
||||||
|
} satisfies Partial<MultiTankFilters>)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
primaryButton(path, 'Refresh'),
|
||||||
|
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||||
|
linkButton(
|
||||||
|
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||||
|
'BlitzStars',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||||
|
const path = url.pathname.split('/').filter(Boolean);
|
||||||
|
const commandGroup = path[1] as StatType;
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
|
||||||
|
return await stats(
|
||||||
|
commandGroup,
|
||||||
|
period,
|
||||||
|
player,
|
||||||
|
commandGroup === 'tank'
|
||||||
|
? parseInt(url.searchParams.get('tankId')!)
|
||||||
|
: commandGroup === 'multi-tank'
|
||||||
|
? ({
|
||||||
|
'tank-type': url.searchParams.get('tank-type')!,
|
||||||
|
nation: url.searchParams.get('nation')!,
|
||||||
|
tier: parseInt(url.searchParams.get('tier')!),
|
||||||
|
'tree-type': url.searchParams.get('tree-type') as TreeTypeString,
|
||||||
|
} satisfies MultiTankFilters)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
+26
-5
@@ -15,7 +15,6 @@ import { AccountInfo } from '../types/accountInfo';
|
|||||||
|
|
||||||
export const todayCommand: CommandRegistry = {
|
export const todayCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
@@ -24,26 +23,48 @@ export const todayCommand: CommandRegistry = {
|
|||||||
.addStringOption(addUsernameChoices)
|
.addStringOption(addUsernameChoices)
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('limit')
|
.setName('cutoff')
|
||||||
.setDescription(
|
.setDescription(
|
||||||
'The maximum number of tanks to display (default: Infinity)',
|
'The maximum number of tanks to display (default: Infinity)',
|
||||||
)
|
)
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
.setMinValue(1),
|
.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) {
|
async handler(interaction) {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const limit = interaction.options.getInteger('limit') ?? Infinity;
|
const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
|
||||||
|
const maximized = interaction.options.getInteger('maximized') ?? undefined;
|
||||||
|
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
|
||||||
const { nickname } = (
|
const { nickname } = (
|
||||||
await getWargamingResponse<AccountInfo>(
|
await getWargamingResponse<AccountInfo>(
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||||
)
|
)
|
||||||
)[player.id];
|
)[player.id];
|
||||||
const path = interactionToURL(interaction, player);
|
const path = interactionToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
cutoff,
|
||||||
|
maximized,
|
||||||
|
'show-total': showTotal,
|
||||||
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
await today(player, limit, false),
|
await today(player, cutoff, maximized, showTotal, false),
|
||||||
primaryButton(path, 'Refresh'),
|
primaryButton(path, 'Refresh'),
|
||||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||||
linkButton(
|
linkButton(
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { PlayerClanData } from '../types/playerClanData';
|
|||||||
|
|
||||||
export const verifyCommand: CommandRegistry = {
|
export const verifyCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
||||||
|
import { TankType } from '../../../core/blitz/tankopedia';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import { RowDiscriminator } from './RowDiscriminator';
|
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
||||||
import { RowStat } from './RowStat';
|
import { RowStat } from './RowStat';
|
||||||
|
|
||||||
export interface RowProps {
|
export interface RowProps {
|
||||||
@@ -13,14 +14,14 @@ export interface RowProps {
|
|||||||
careerDamage: number;
|
careerDamage: number;
|
||||||
battles: number;
|
battles: number;
|
||||||
careerBattles: number;
|
careerBattles: number;
|
||||||
icon?: string;
|
|
||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
isListing: boolean;
|
isTank: boolean;
|
||||||
naked?: boolean;
|
treeType?: TreeTypeEnum;
|
||||||
|
tankType?: TankType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Row({
|
export function Row({
|
||||||
isListing,
|
isTank,
|
||||||
minimized,
|
minimized,
|
||||||
name,
|
name,
|
||||||
winrate,
|
winrate,
|
||||||
@@ -31,45 +32,85 @@ export function Row({
|
|||||||
careerDamage,
|
careerDamage,
|
||||||
battles,
|
battles,
|
||||||
careerBattles,
|
careerBattles,
|
||||||
icon,
|
treeType,
|
||||||
naked,
|
tankType,
|
||||||
}: RowProps) {
|
}: RowProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
backgroundColor: isListing
|
backgroundColor: theme.colors.appBackground2,
|
||||||
? theme.colors.componentInteractive
|
overflow: 'hidden',
|
||||||
: theme.colors.appBackground2,
|
|
||||||
padding: 8,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RowDiscriminator minimized={minimized} name={name} icon={icon} />
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 8,
|
||||||
|
gap: 4,
|
||||||
|
backgroundColor: theme.colors.componentInteractive,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isTank && treeType !== undefined && tankType !== undefined && (
|
||||||
|
<img
|
||||||
|
src={TREE_TYPE_ICONS[treeType][tankType]}
|
||||||
|
style={{ width: 16, height: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: isTank
|
||||||
|
? treeType === TreeTypeEnum.Collector
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: treeType === TreeTypeEnum.Premium
|
||||||
|
? theme.colors.textLowContrast_amber
|
||||||
|
: theme.colors.textHighContrast
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<RowStat
|
<div
|
||||||
minimized={minimized}
|
style={{
|
||||||
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
|
display: 'flex',
|
||||||
value={`${(winrate * 100).toFixed(2)}%`}
|
paddingTop: 8,
|
||||||
delta={winrate - careerWinrate}
|
paddingBottom: 8,
|
||||||
/>
|
}}
|
||||||
<RowStat
|
>
|
||||||
minimized={minimized}
|
<RowStat
|
||||||
name={`WN8 • ${careerWN8 === undefined ? '--' : careerWN8.toFixed(0)}`}
|
minimized={minimized}
|
||||||
value={WN8 === undefined ? '--' : WN8.toFixed(0)}
|
name={`Battles • ${careerBattles.toFixed(0)}`}
|
||||||
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)}
|
value={battles.toFixed(0)}
|
||||||
/>
|
/>
|
||||||
<RowStat
|
<RowStat
|
||||||
minimized={minimized}
|
minimized={minimized}
|
||||||
name={`Damage • ${careerDamage.toFixed(0)}`}
|
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
|
||||||
value={damage.toFixed(0)}
|
value={`${(winrate * 100).toFixed(2)}%`}
|
||||||
delta={damage - careerDamage}
|
delta={winrate - careerWinrate}
|
||||||
/>
|
/>
|
||||||
<RowStat
|
<RowStat
|
||||||
minimized={minimized}
|
minimized={minimized}
|
||||||
name={`Battles • ${careerBattles.toFixed(0)}`}
|
name={`WN8 • ${
|
||||||
value={battles.toFixed(0)}
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { theme } from '../../../stitches.config';
|
|
||||||
|
|
||||||
export interface RowDiscriminatorProps {
|
|
||||||
name: string;
|
|
||||||
icon?: string;
|
|
||||||
minimized: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RowDiscriminator({
|
|
||||||
name,
|
|
||||||
icon,
|
|
||||||
minimized,
|
|
||||||
}: RowDiscriminatorProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
width: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!minimized && icon && (
|
|
||||||
<img
|
|
||||||
src={icon}
|
|
||||||
height={0}
|
|
||||||
style={{ flex: 1, width: '100%', objectFit: 'contain' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: theme.colors.textLowContrast,
|
|
||||||
fontSize: 16,
|
|
||||||
textAlign: 'center',
|
|
||||||
display: 'block',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'clip',
|
|
||||||
maxWidth: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -53,6 +53,7 @@ export function RowStat({
|
|||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 'bold',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './components/Root';
|
export * from './components/Root';
|
||||||
export * from './components/Row';
|
export * from './components/Row';
|
||||||
export * from './components/RowDiscriminator';
|
|
||||||
export * from './components/RowStat';
|
export * from './components/RowStat';
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export default function GenericAllStats({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Efficiency"
|
title="Miscellaneous"
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: 'Average tier',
|
label: 'Average tier',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
|||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import { Margin, MarginOrientation } from './Margin';
|
import { Margin, MarginOrientation } from './Margin';
|
||||||
|
|
||||||
export const GRAPH_WIDTH = 544;
|
export const GRAPH_WIDTH = 384;
|
||||||
export const GRAPH_HEIGHT = 320;
|
export const GRAPH_HEIGHT = 320;
|
||||||
|
|
||||||
export interface MarginInputProps {
|
export interface MarginInputProps {
|
||||||
@@ -20,7 +20,7 @@ export interface RootProps {
|
|||||||
separations?: number;
|
separations?: number;
|
||||||
xMinLabel: string;
|
xMinLabel: string;
|
||||||
xMaxLabel: string;
|
xMaxLabel: string;
|
||||||
xTitle: string;
|
xTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Root({
|
export function Root({
|
||||||
@@ -91,18 +91,20 @@ export function Root({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
{xTitle !== undefined && (
|
||||||
style={{
|
<div
|
||||||
paddingLeft: 64,
|
style={{
|
||||||
display: 'flex',
|
paddingLeft: 64,
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
}}
|
justifyContent: 'center',
|
||||||
>
|
}}
|
||||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
>
|
||||||
{xTitle}
|
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||||
</span>
|
{xTitle}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ export interface ItemProps {
|
|||||||
reward?: RatingsReward;
|
reward?: RatingsReward;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
clan?: string;
|
clan?: string;
|
||||||
|
highlight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeImage(url: string) {
|
function normalizeImage(url: string) {
|
||||||
@@ -23,13 +24,16 @@ export function Item({
|
|||||||
reward,
|
reward,
|
||||||
nickname,
|
nickname,
|
||||||
clan,
|
clan,
|
||||||
|
highlight,
|
||||||
}: ItemProps) {
|
}: ItemProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
backgroundColor: theme.colors.componentInteractive,
|
backgroundColor: highlight
|
||||||
|
? theme.colors.componentInteractive_blue
|
||||||
|
: theme.colors.componentInteractive,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -38,7 +42,9 @@ export function Item({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -55,7 +61,9 @@ export function Item({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textHighContrast,
|
color: highlight
|
||||||
|
? theme.colors.textHighContrast_blue
|
||||||
|
: theme.colors.textHighContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -64,7 +72,9 @@ export function Item({
|
|||||||
{clan && (
|
{clan && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -95,7 +105,9 @@ export function Item({
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -109,7 +121,7 @@ export function Item({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'flex-end',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -126,7 +138,9 @@ export function Item({
|
|||||||
{reward && reward.count > 1 && (
|
{reward && reward.count > 1 && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -140,11 +154,19 @@ export function Item({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
width: 96,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{Math.abs(deltaScore).toLocaleString()}
|
{Math.abs(deltaScore).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -161,7 +183,14 @@ export function Item({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span style={{ color: theme.colors.textHighContrast, fontSize: 16 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textHighContrast_blue
|
||||||
|
: theme.colors.textHighContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{score.toLocaleString()}
|
{score.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default function Navbar() {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', height: 100, backgroundColor: 'red' }}></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { theme } from '../stitches.config';
|
|
||||||
|
|
||||||
export enum PoweredByType {
|
|
||||||
BlitzStars,
|
|
||||||
Wargaming,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PoweredByProps {
|
|
||||||
type: PoweredByType;
|
|
||||||
footer?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const POWERED_BY_IMAGES: Record<PoweredByType, string> = {
|
|
||||||
[PoweredByType.BlitzStars]:
|
|
||||||
'https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png',
|
|
||||||
[PoweredByType.Wargaming]: 'https://i.imgur.com/jADGWm0.png',
|
|
||||||
};
|
|
||||||
|
|
||||||
const POWERED_BY_NAMES: Record<PoweredByType, string> = {
|
|
||||||
[PoweredByType.BlitzStars]: 'BlitzStars',
|
|
||||||
[PoweredByType.Wargaming]: 'Wargaming',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PoweredBy({ type, footer }: PoweredByProps) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<div style={{ display: 'flex' }}>{footer}</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
|
||||||
<img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} />
|
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
|
||||||
Powered by {POWERED_BY_NAMES[type]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,32 +2,35 @@ import {
|
|||||||
TANK_ICONS,
|
TANK_ICONS,
|
||||||
TANK_ICONS_COLLECTOR,
|
TANK_ICONS_COLLECTOR,
|
||||||
TANK_ICONS_PREMIUM,
|
TANK_ICONS_PREMIUM,
|
||||||
|
TankType,
|
||||||
} from '../../../core/blitz/tankopedia';
|
} from '../../../core/blitz/tankopedia';
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export enum TreeType {
|
export enum TreeTypeEnum {
|
||||||
TechTree,
|
TechTree,
|
||||||
Premium,
|
Premium,
|
||||||
Collector,
|
Collector,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
|
||||||
|
|
||||||
export interface ItemProps {
|
export interface ItemProps {
|
||||||
image?: string;
|
image?: string;
|
||||||
tankType?: string;
|
tankType?: TankType;
|
||||||
name: string;
|
name: string;
|
||||||
treeType: TreeType;
|
treeType: TreeTypeEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TREE_TYPE_COLOR = {
|
const TREE_TYPE_COLOR = {
|
||||||
[TreeType.TechTree]: '',
|
[TreeTypeEnum.TechTree]: '',
|
||||||
[TreeType.Premium]: '_amber',
|
[TreeTypeEnum.Premium]: '_amber',
|
||||||
[TreeType.Collector]: '_blue',
|
[TreeTypeEnum.Collector]: '_blue',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const TREE_TYPE_ICONS = {
|
export const TREE_TYPE_ICONS = {
|
||||||
[TreeType.TechTree]: TANK_ICONS,
|
[TreeTypeEnum.TechTree]: TANK_ICONS,
|
||||||
[TreeType.Premium]: TANK_ICONS_PREMIUM,
|
[TreeTypeEnum.Premium]: TANK_ICONS_PREMIUM,
|
||||||
[TreeType.Collector]: TANK_ICONS_COLLECTOR,
|
[TreeTypeEnum.Collector]: TANK_ICONS_COLLECTOR,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Item({ image, tankType, name, treeType }: ItemProps) {
|
export function Item({ image, tankType, name, treeType }: ItemProps) {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export function Root({ children }: RootProps) {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 8,
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface RowProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Row({ children }: RowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
|
export interface TitleProps {
|
||||||
|
children: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Title({ children }: TitleProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: 900,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontSize: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './components/Column';
|
export * from './components/Column';
|
||||||
export * from './components/Item';
|
export * from './components/Item';
|
||||||
export * from './components/Root';
|
export * from './components/Root';
|
||||||
|
export * from './components/Row';
|
||||||
|
export * from './components/Title';
|
||||||
+12
-15
@@ -1,4 +1,3 @@
|
|||||||
import isDev from '../core/node/isDev';
|
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export interface TitleBarProps {
|
export interface TitleBarProps {
|
||||||
@@ -19,10 +18,11 @@ export default function TitleBar({
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 16,
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', gap: 8, flex: 1 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
{image && (
|
{image && (
|
||||||
<img
|
<img
|
||||||
style={{ width: 64, height: 64, objectFit: 'contain' }}
|
style={{ width: 64, height: 64, objectFit: 'contain' }}
|
||||||
@@ -31,13 +31,18 @@ export default function TitleBar({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -46,6 +51,8 @@ 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',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
@@ -53,7 +60,6 @@ export default function TitleBar({
|
|||||||
{nameDiscriminator && (
|
{nameDiscriminator && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
color: theme.colors.textLowContrast,
|
color: theme.colors.textLowContrast,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
@@ -70,15 +76,6 @@ export default function TitleBar({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
|
||||||
style={{ width: 64, height: 64 }}
|
|
||||||
src={
|
|
||||||
isDev()
|
|
||||||
? 'https://i.imgur.com/j6TItdy.png'
|
|
||||||
: 'https://i.imgur.com/pP6RN0x.png'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ export default function Wrapper({ children, naked }: WrapperProps) {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
gap: 32,
|
gap: 32,
|
||||||
width: 640,
|
width: 480,
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
|
|
||||||
...(!naked && {
|
...(!naked && {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const CYCLIC_API = 'https://blitzkrieg.cyclic.app';
|
export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
|
||||||
@@ -7,6 +7,13 @@ export const REGION_NAMES: Record<Region, string> = {
|
|||||||
asia: 'Asia',
|
asia: 'Asia',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// https://planetarynames.wr.usgs.gov/Abbreviations
|
||||||
|
export const REGION_NAMES_SHORT: Record<Region, string> = {
|
||||||
|
com: 'NA',
|
||||||
|
eu: 'EU',
|
||||||
|
asia: 'AS',
|
||||||
|
};
|
||||||
|
|
||||||
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
||||||
na: 'North America',
|
na: 'North America',
|
||||||
eu: 'Europe',
|
eu: 'Europe',
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { AllStats } from '../../types/accountInfo';
|
|
||||||
|
|
||||||
export default function diffStats(stats1: AllStats, stats2: AllStats) {
|
|
||||||
function diff(value: (allStats: AllStats) => number) {
|
|
||||||
return value(stats2) - value(stats1);
|
|
||||||
}
|
|
||||||
function max(value: (allStats: AllStats) => number) {
|
|
||||||
return Math.max(value(stats1), value(stats2));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
battles: diff((a) => a.battles),
|
|
||||||
wins: diff((a) => a.wins),
|
|
||||||
capture_points: diff((a) => a.capture_points),
|
|
||||||
damage_dealt: diff((a) => a.damage_dealt),
|
|
||||||
damage_received: diff((a) => a.damage_received),
|
|
||||||
dropped_capture_points: diff((a) => a.dropped_capture_points),
|
|
||||||
frags: diff((a) => a.frags),
|
|
||||||
frags8p: diff((a) => a.frags8p),
|
|
||||||
hits: diff((a) => a.hits),
|
|
||||||
losses: diff((a) => a.losses),
|
|
||||||
max_frags: max((a) => a.max_frags),
|
|
||||||
max_xp: max((a) => a.max_xp),
|
|
||||||
shots: diff((a) => a.shots),
|
|
||||||
spotted: diff((a) => a.spotted),
|
|
||||||
survived_battles: diff((a) => a.survived_battles),
|
|
||||||
win_and_survived: diff((a) => a.win_and_survived),
|
|
||||||
xp: diff((a) => a.xp),
|
|
||||||
} satisfies AllStats;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { RatingsInfo } from '../../commands/ratings';
|
||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { DATABASE_REPO } from '../database/getMidnightLeaderboard';
|
||||||
|
import { octokit } from '../express/octokit';
|
||||||
|
import throwError from '../node/throwError';
|
||||||
|
|
||||||
|
export default async function getArchivedRatingsInfo(
|
||||||
|
season: number,
|
||||||
|
region: Region,
|
||||||
|
) {
|
||||||
|
const { data } = await octokit.repos.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/info.json`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(data) || data.type !== 'file')
|
||||||
|
throw throwError('Archived ratings info is malformed');
|
||||||
|
|
||||||
|
const content = Buffer.from(data.content, 'base64').toString();
|
||||||
|
const jsonContent = JSON.parse(content) as RatingsInfo & {
|
||||||
|
detail: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
return jsonContent;
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Region } from '../../constants/regions';
|
|
||||||
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand';
|
|
||||||
import throwError from '../node/throwError';
|
|
||||||
import listClans from './listClans';
|
|
||||||
|
|
||||||
export default async function getBlitzClan(clan: string) {
|
|
||||||
if (serverAndIdPattern.test(clan)) {
|
|
||||||
const [server, accountId] = clan.split('/');
|
|
||||||
return { server: server as Region, id: Number(accountId) };
|
|
||||||
} else {
|
|
||||||
const accounts = await listClans(clan);
|
|
||||||
|
|
||||||
if (accounts[0]) {
|
|
||||||
return { server: accounts[0].server, id: accounts[0].clan_id };
|
|
||||||
} else {
|
|
||||||
throw throwError(
|
|
||||||
'Could not find clan',
|
|
||||||
`I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { RatingsInfo } from '../../commands/ratings';
|
||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import regionToRegionSubdomain from './regionToRegionSubdomain';
|
||||||
|
|
||||||
|
export default async function getRatingsInfo(region: Region) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://${regionToRegionSubdomain(
|
||||||
|
region,
|
||||||
|
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await response.json()) as Promise<RatingsInfo>;
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { TreeType } from '../../components/Tanks';
|
import { TreeTypeEnum } from '../../components/Tanks';
|
||||||
import { tankopedia } from './tankopedia';
|
import { tankopedia } from './tankopedia';
|
||||||
|
|
||||||
export default async function getTreeType(id: number) {
|
export default async function getTreeType(id: number) {
|
||||||
const entry = (await tankopedia)[id];
|
const entry = (await tankopedia)[id];
|
||||||
|
|
||||||
if (entry?.is_collectible) return TreeType.Collector;
|
if (entry?.is_collectible) return TreeTypeEnum.Collector;
|
||||||
if (entry?.is_premium) return TreeType.Premium;
|
if (entry?.is_premium) return TreeTypeEnum.Premium;
|
||||||
|
|
||||||
return TreeType.TechTree;
|
return TreeTypeEnum.TechTree;
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,8 @@ import { Clan, ClanList } from '../../types/clanList';
|
|||||||
import { secrets } from '../node/secrets';
|
import { secrets } from '../node/secrets';
|
||||||
import getWargamingResponse from './getWargamingResponse';
|
import getWargamingResponse from './getWargamingResponse';
|
||||||
|
|
||||||
export type ClanListWithServer = (Clan & {
|
export type ClanListWithRegion = (Clan & {
|
||||||
server: 'com' | 'eu' | 'asia';
|
region: 'com' | 'eu' | 'asia';
|
||||||
})[];
|
})[];
|
||||||
|
|
||||||
export default async function listClans(search: string, limit = 9) {
|
export default async function listClans(search: string, limit = 9) {
|
||||||
@@ -21,7 +21,7 @@ export default async function listClans(search: string, limit = 9) {
|
|||||||
value &&
|
value &&
|
||||||
value.map((account) => ({
|
value.map((account) => ({
|
||||||
...account,
|
...account,
|
||||||
server: 'com' as Region,
|
region: 'com' as Region,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<ClanList>(
|
getWargamingResponse<ClanList>(
|
||||||
@@ -31,7 +31,7 @@ export default async function listClans(search: string, limit = 9) {
|
|||||||
value &&
|
value &&
|
||||||
value.map((account) => ({
|
value.map((account) => ({
|
||||||
...account,
|
...account,
|
||||||
server: 'eu' as Region,
|
region: 'eu' as Region,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<ClanList>(
|
getWargamingResponse<ClanList>(
|
||||||
@@ -41,7 +41,7 @@ export default async function listClans(search: string, limit = 9) {
|
|||||||
value &&
|
value &&
|
||||||
value.map((account) => ({
|
value.map((account) => ({
|
||||||
...account,
|
...account,
|
||||||
server: 'asia' as Region,
|
region: 'asia' as Region,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Thanks, August!
|
||||||
|
* https://stackoverflow.com/a/41358305/12294756
|
||||||
|
*/
|
||||||
|
const ROMAN = {
|
||||||
|
M: 1000,
|
||||||
|
CM: 900,
|
||||||
|
D: 500,
|
||||||
|
CD: 400,
|
||||||
|
C: 100,
|
||||||
|
XC: 90,
|
||||||
|
L: 50,
|
||||||
|
XL: 40,
|
||||||
|
X: 10,
|
||||||
|
IX: 9,
|
||||||
|
V: 5,
|
||||||
|
IV: 4,
|
||||||
|
I: 1,
|
||||||
|
};
|
||||||
|
const ROMAN_KEYS = Object.keys(ROMAN) as (keyof typeof ROMAN)[];
|
||||||
|
|
||||||
|
export default function numberToRomanNumeral(value: number) {
|
||||||
|
let string = '';
|
||||||
|
|
||||||
|
ROMAN_KEYS.forEach((index) => {
|
||||||
|
const part = Math.floor(value / ROMAN[index]);
|
||||||
|
value -= part * ROMAN[index];
|
||||||
|
string += index.repeat(part);
|
||||||
|
});
|
||||||
|
|
||||||
|
return string;
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { deburr } from 'lodash';
|
||||||
|
import { secrets } from '../node/secrets';
|
||||||
|
import getWargamingResponse from './getWargamingResponse';
|
||||||
|
|
||||||
|
export type TankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
|
||||||
|
|
||||||
export interface TankopediaEntry {
|
export interface TankopediaEntry {
|
||||||
name: string;
|
name: string;
|
||||||
nation: string;
|
nation: string;
|
||||||
@@ -7,7 +13,7 @@ export interface TankopediaEntry {
|
|||||||
cost: { price_credit: number; price_gold: number };
|
cost: { price_credit: number; price_gold: number };
|
||||||
images: { preview: string; normal: string };
|
images: { preview: string; normal: string };
|
||||||
tank_id: number;
|
tank_id: number;
|
||||||
type: string;
|
type: TankType;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,22 +37,54 @@ export const TANKS = new Promise<TankopediaEntry[]>(async (resolve) => {
|
|||||||
export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
|
export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
|
||||||
resolve((await entries).map(({ name }) => name));
|
resolve((await entries).map(({ name }) => name));
|
||||||
});
|
});
|
||||||
|
export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) =>
|
||||||
|
Promise.all(
|
||||||
|
tankNames.map(async (tankName, index) => {
|
||||||
|
const id = (await TANKS)[index].tank_id;
|
||||||
|
const name = tankName ?? `Unknown Tank ${id}`;
|
||||||
|
const diacriticless = deburr(name);
|
||||||
|
|
||||||
export const TANK_ICONS: Record<string, string> = {
|
return {
|
||||||
|
original: name,
|
||||||
|
diacriticless,
|
||||||
|
combined: `${name}${diacriticless}`,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface TankopediaInfo {
|
||||||
|
achievement_sections: Record<string, { name: string; order: number }>;
|
||||||
|
tanks_updated_at: number;
|
||||||
|
languages: Record<string, string>;
|
||||||
|
vehicle_types: Record<string, string>;
|
||||||
|
vehicle_nations: Record<string, string>;
|
||||||
|
game_version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is blocking because info is needed for command creation
|
||||||
|
console.log('Caching tankopedia info...');
|
||||||
|
export const tankopediaInfo = getWargamingResponse<TankopediaInfo>(
|
||||||
|
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}`,
|
||||||
|
);
|
||||||
|
console.log('Cached tankopedia info');
|
||||||
|
|
||||||
|
export const TANK_ICONS: Record<TankType, string> = {
|
||||||
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
|
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
|
||||||
lightTank: 'https://i.imgur.com/CSNha5V.png',
|
lightTank: 'https://i.imgur.com/CSNha5V.png',
|
||||||
mediumTank: 'https://i.imgur.com/wvf3ltm.png',
|
mediumTank: 'https://i.imgur.com/wvf3ltm.png',
|
||||||
heavyTank: 'https://i.imgur.com/ECeqlZa.png',
|
heavyTank: 'https://i.imgur.com/ECeqlZa.png',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TANK_ICONS_PREMIUM: Record<string, string> = {
|
export const TANK_ICONS_PREMIUM: Record<TankType, string> = {
|
||||||
'AT-SPG': 'https://i.imgur.com/TCu3EdR.png',
|
'AT-SPG': 'https://i.imgur.com/TCu3EdR.png',
|
||||||
lightTank: 'https://i.imgur.com/zdkpTRb.png',
|
lightTank: 'https://i.imgur.com/zdkpTRb.png',
|
||||||
mediumTank: 'https://i.imgur.com/3z7eHX6.png',
|
mediumTank: 'https://i.imgur.com/3z7eHX6.png',
|
||||||
heavyTank: 'https://i.imgur.com/P3vbmyA.png',
|
heavyTank: 'https://i.imgur.com/P3vbmyA.png',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TANK_ICONS_COLLECTOR: Record<string, string> = {
|
export const TANK_ICONS_COLLECTOR: Record<TankType, string> = {
|
||||||
'AT-SPG': 'https://i.imgur.com/WTjeirB.png',
|
'AT-SPG': 'https://i.imgur.com/WTjeirB.png',
|
||||||
lightTank: 'https://i.imgur.com/EwhtKkU.png',
|
lightTank: 'https://i.imgur.com/EwhtKkU.png',
|
||||||
mediumTank: 'https://i.imgur.com/u8YDMBh.png',
|
mediumTank: 'https://i.imgur.com/u8YDMBh.png',
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { Period } from '../discord/addPeriodSubCommands';
|
import { PeriodSize } from '../discord/addPeriodSubCommands';
|
||||||
import getTimeDaysAgo from './getTimeDaysAgo';
|
import getTimeDaysAgo from './getTimeDaysAgo';
|
||||||
|
|
||||||
export default function getPeriodStart(
|
export default function getPeriodStart(region: Region, period: PeriodSize) {
|
||||||
server: Region,
|
return period === 'career' ? 0 : getTimeDaysAgo(region, parseInt(period));
|
||||||
period: Exclude<Period, 'custom'>,
|
|
||||||
) {
|
|
||||||
if (period === 'career') {
|
|
||||||
return -Infinity;
|
|
||||||
} else if (period === 'today') {
|
|
||||||
return getTimeDaysAgo(server, 1);
|
|
||||||
} else {
|
|
||||||
return getTimeDaysAgo(server, parseInt(period));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,13 @@ export const TIME_ZONE_MAPPINGS: Record<Region, number> = {
|
|||||||
asia: +7, // Central Asia Standard Time
|
asia: +7, // Central Asia Standard Time
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function getTimeDaysAgo(server: Region, daysAgo: number) {
|
export default function getTimeDaysAgo(region: Region, daysAgo: number) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
if (daysAgo === 0) now.getTime() / 1000;
|
if (daysAgo === 0) now.getTime() / 1000;
|
||||||
|
|
||||||
const time = new Date();
|
const time = new Date();
|
||||||
|
|
||||||
time.setUTCHours(-TIME_ZONE_MAPPINGS[server], 0, 0, 0);
|
time.setUTCHours(-TIME_ZONE_MAPPINGS[region], 0, 0, 0);
|
||||||
if (time > now) time.setUTCDate(time.getUTCDate() - 1);
|
if (time > now) time.setUTCDate(time.getUTCDate() - 1);
|
||||||
time.setUTCDate(time.getUTCDate() - daysAgo + 1);
|
time.setUTCDate(time.getUTCDate() - daysAgo + 1);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import sumStats from '../blitz/sumStats';
|
|
||||||
import { emptyAllStats } from './getDiffedTankStats';
|
|
||||||
import { tankAverages } from './tankAverages';
|
|
||||||
|
|
||||||
console.log('Calculating player average...');
|
|
||||||
export const playerAverage = Object.entries(await tankAverages).reduce(
|
|
||||||
(accumulator, [, tankAverage]) => {
|
|
||||||
return sumStats([accumulator, tankAverage.all]);
|
|
||||||
},
|
|
||||||
emptyAllStats,
|
|
||||||
);
|
|
||||||
console.log('Calculated player average');
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { octokit } from '../express/octokit';
|
||||||
|
|
||||||
|
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
|
||||||
|
|
||||||
|
export default async function getMidnightLeaderboard(
|
||||||
|
region: Region,
|
||||||
|
season: number,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await octokit.repos.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/midnight.json`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Array.isArray(data) && data.type === 'file') {
|
||||||
|
const response = await fetch(data.download_url!);
|
||||||
|
|
||||||
|
return (await response.json()) as BlitzkriegRatingsLeaderboard;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`No midnight leaderboard found for season ${season}, region ${region}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
SlashCommandSubcommandBuilder,
|
SlashCommandSubcommandBuilder,
|
||||||
SlashCommandSubcommandGroupBuilder,
|
SlashCommandSubcommandGroupBuilder,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { PERIOD_NAMES } from '../discord/resolvePeriodFromCommand';
|
|
||||||
|
|
||||||
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom';
|
export type PeriodType = 'period' | 'custom';
|
||||||
|
export type PeriodSize = `${number}` | 'career';
|
||||||
|
|
||||||
export default function addPeriodSubCommands(
|
export default function addPeriodSubCommands(
|
||||||
option: SlashCommandSubcommandGroupBuilder,
|
option: SlashCommandSubcommandGroupBuilder,
|
||||||
@@ -14,36 +14,41 @@ export default function addPeriodSubCommands(
|
|||||||
) {
|
) {
|
||||||
return option
|
return option
|
||||||
.addSubcommand((option) =>
|
.addSubcommand((option) =>
|
||||||
extra(option.setName('today').setDescription(PERIOD_NAMES.today)),
|
extra(
|
||||||
)
|
option
|
||||||
.addSubcommand((option) =>
|
.setName('period' satisfies PeriodType)
|
||||||
extra(option.setName('30').setDescription(PERIOD_NAMES[30])),
|
.setDescription("Preset period's statistics")
|
||||||
)
|
.addStringOption((option) =>
|
||||||
.addSubcommand((option) =>
|
option
|
||||||
extra(option.setName('60').setDescription(PERIOD_NAMES[60])),
|
.setName('period')
|
||||||
)
|
.setDescription('A preset period')
|
||||||
.addSubcommand((option) =>
|
.setChoices(
|
||||||
extra(option.setName('90').setDescription(PERIOD_NAMES[90])),
|
{ name: 'Today', value: '1' satisfies PeriodSize },
|
||||||
)
|
{ name: '30 days', value: '30' satisfies PeriodSize },
|
||||||
.addSubcommand((option) =>
|
{ name: '60 days', value: '60' satisfies PeriodSize },
|
||||||
extra(option.setName('career').setDescription(PERIOD_NAMES.career)),
|
{ name: '90 days', value: '90' satisfies PeriodSize },
|
||||||
|
{ name: 'Career', value: 'career' satisfies PeriodSize },
|
||||||
|
)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.addSubcommand((option) =>
|
.addSubcommand((option) =>
|
||||||
extra(
|
extra(
|
||||||
option
|
option
|
||||||
.setName('custom')
|
.setName('custom' satisfies PeriodType)
|
||||||
.setDescription(PERIOD_NAMES.custom)
|
.setDescription("Custom period's statistics")
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('start')
|
.setName('start')
|
||||||
.setDescription('Start of period in days ago from today')
|
.setDescription('Days ago from today')
|
||||||
.setMinValue(0)
|
.setMinValue(0)
|
||||||
.setRequired(true),
|
.setRequired(true),
|
||||||
)
|
)
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('end')
|
.setName('end')
|
||||||
.setDescription('End of period in days ago from today')
|
.setDescription('Days ago from today')
|
||||||
.setMinValue(0)
|
.setMinValue(0)
|
||||||
.setRequired(true),
|
.setRequired(true),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import { TreeTypeString } from '../../components/Tanks';
|
||||||
|
import { StatType } from '../../renderers/stats';
|
||||||
|
import { tankopediaInfo } from '../blitz/tankopedia';
|
||||||
import addPeriodSubCommands from './addPeriodSubCommands';
|
import addPeriodSubCommands from './addPeriodSubCommands';
|
||||||
import addTankChoices from './addTankChoices';
|
import addTankChoices from './addTankChoices';
|
||||||
|
import addTierChoices from './addTierChoices';
|
||||||
import addUsernameChoices from './addUsernameChoices';
|
import addUsernameChoices from './addUsernameChoices';
|
||||||
|
|
||||||
export default function addStatTypeSubCommandGroups(
|
export default async function addStatTypeSubCommandGroups(
|
||||||
option: SlashCommandBuilder,
|
option: SlashCommandBuilder,
|
||||||
|
addMultiTank = true,
|
||||||
) {
|
) {
|
||||||
return option
|
const awaitedTankopediaInfo = await tankopediaInfo;
|
||||||
|
|
||||||
|
option
|
||||||
.addSubcommandGroup((option) =>
|
.addSubcommandGroup((option) =>
|
||||||
addPeriodSubCommands(option, (option) =>
|
addPeriodSubCommands(option, (option) =>
|
||||||
option.addStringOption(addUsernameChoices),
|
option.addStringOption(addUsernameChoices),
|
||||||
)
|
)
|
||||||
.setName('player')
|
.setName('player' satisfies StatType)
|
||||||
.setDescription("Player's statistics"),
|
.setDescription('Player statistics'),
|
||||||
)
|
)
|
||||||
.addSubcommandGroup((option) =>
|
.addSubcommandGroup((option) =>
|
||||||
addPeriodSubCommands(option, (option) =>
|
addPeriodSubCommands(option, (option) =>
|
||||||
@@ -20,7 +27,65 @@ export default function addStatTypeSubCommandGroups(
|
|||||||
.addStringOption(addTankChoices)
|
.addStringOption(addTankChoices)
|
||||||
.addStringOption(addUsernameChoices),
|
.addStringOption(addUsernameChoices),
|
||||||
)
|
)
|
||||||
.setName('tank')
|
.setName('tank' satisfies StatType)
|
||||||
.setDescription("Tank's statistics"),
|
.setDescription('Tank statistics'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (addMultiTank)
|
||||||
|
option.addSubcommandGroup((option) =>
|
||||||
|
addPeriodSubCommands(option, (option) =>
|
||||||
|
option
|
||||||
|
.addStringOption(addUsernameChoices)
|
||||||
|
.addStringOption((option) => {
|
||||||
|
option
|
||||||
|
.setName('nation')
|
||||||
|
.setDescription('Nation')
|
||||||
|
.addChoices(
|
||||||
|
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
|
||||||
|
([type, name]) => ({ name, value: type }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return option;
|
||||||
|
})
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('tank-type')
|
||||||
|
.setDescription('Type')
|
||||||
|
.addChoices(
|
||||||
|
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
|
||||||
|
([type, name]) => ({
|
||||||
|
name: name.replace(' Tank', ''),
|
||||||
|
value: type,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(false),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
addTierChoices(option).setRequired(false),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('tree-type')
|
||||||
|
.setDescription('Tech tree type')
|
||||||
|
.addChoices(
|
||||||
|
{
|
||||||
|
name: 'Tech Tree',
|
||||||
|
value: 'techtree' satisfies TreeTypeString,
|
||||||
|
},
|
||||||
|
{ name: 'Premium', value: 'premium' satisfies TreeTypeString },
|
||||||
|
{
|
||||||
|
name: 'Collector',
|
||||||
|
value: 'collector' satisfies TreeTypeString,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.setRequired(false),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setName('multi-tank' satisfies StatType)
|
||||||
|
.setDescription('Multiple tanks'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return option;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { SlashCommandStringOption } from 'discord.js';
|
||||||
|
import { range } from 'lodash';
|
||||||
|
import numberToRomanNumeral from '../blitz/numberToRomanNumeral';
|
||||||
|
|
||||||
|
export default function addTierChoices(option: SlashCommandStringOption) {
|
||||||
|
return option
|
||||||
|
.setName('tier')
|
||||||
|
.setDescription('The tier you want to see')
|
||||||
|
.setChoices(
|
||||||
|
...range(10, 0).map((tier) => ({
|
||||||
|
name: `Tier ${tier} - ${numberToRomanNumeral(tier)}`,
|
||||||
|
value: `${tier}`,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.setRequired(true);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export default async function autocompleteClan(
|
|||||||
) {
|
) {
|
||||||
const focusedOption = interaction.options.getFocused(true);
|
const focusedOption = interaction.options.getFocused(true);
|
||||||
if (focusedOption.name !== 'clan') return;
|
if (focusedOption.name !== 'clan') return;
|
||||||
|
if (focusedOption.value.length < 2) return interaction.respond([]);
|
||||||
const players = await listClans(focusedOption.value);
|
const players = await listClans(focusedOption.value);
|
||||||
|
|
||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
@@ -18,8 +19,8 @@ export default async function autocompleteClan(
|
|||||||
? players.map(
|
? players.map(
|
||||||
(player) =>
|
(player) =>
|
||||||
({
|
({
|
||||||
name: `${player.name} (${REGION_NAMES[player.server]})`,
|
name: `${player.name} (${REGION_NAMES[player.region]})`,
|
||||||
value: `${player.server}/${player.clan_id}`,
|
value: `${player.region}/${player.clan_id}`,
|
||||||
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import {
|
|||||||
CacheType,
|
CacheType,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import resolveTankName from '../blitz/resolveTankName';
|
import { TANK_NAMES_DIACRITICS } from '../blitz/tankopedia';
|
||||||
import { TANKS } from '../blitz/tankopedia';
|
|
||||||
|
|
||||||
export default async function autocompleteTanks(
|
export default async function autocompleteTanks(
|
||||||
interaction: AutocompleteInteraction<CacheType>,
|
interaction: AutocompleteInteraction<CacheType>,
|
||||||
@@ -16,16 +15,14 @@ export default async function autocompleteTanks(
|
|||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
focusedOption.value
|
focusedOption.value
|
||||||
? await Promise.all(
|
? await Promise.all(
|
||||||
go(focusedOption.value, await TANKS, {
|
go(focusedOption.value, await TANK_NAMES_DIACRITICS, {
|
||||||
keys: ['name'],
|
keys: ['combined'],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
}).map(
|
}).map(
|
||||||
async (item) =>
|
async (item) =>
|
||||||
({
|
({
|
||||||
name: item.obj.name
|
name: item.obj.original,
|
||||||
? item.obj.name
|
value: `${item.obj.id}`,
|
||||||
: await resolveTankName(item.obj.tank_id),
|
|
||||||
value: `${item.obj.tank_id}`,
|
|
||||||
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Client, GatewayIntentBits } from 'discord.js';
|
||||||
|
import guildMemberAdd from '../../events/guildMemberAdd';
|
||||||
|
import interactionCreate from '../../events/interactionCreate';
|
||||||
|
import ready from '../../events/ready';
|
||||||
|
|
||||||
|
export const client = new Client({
|
||||||
|
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
|
||||||
|
})
|
||||||
|
.on('ready', ready)
|
||||||
|
.on('guildMemberAdd', guildMemberAdd)
|
||||||
|
.on('interactionCreate', interactionCreate);
|
||||||
@@ -19,12 +19,7 @@ export default function interactionToURL(
|
|||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('/');
|
.join('/');
|
||||||
const params = [
|
const params = (parameters ? Object.entries(parameters) : [])
|
||||||
...interaction.options.data.map(
|
|
||||||
(option) => [option.name, option.value] as const,
|
|
||||||
),
|
|
||||||
...Object.entries(parameters ?? {}),
|
|
||||||
]
|
|
||||||
.filter(([, value]) => value !== undefined && value !== null)
|
.filter(([, value]) => value !== undefined && value !== null)
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
.join('&');
|
.join('&');
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ChatInputCommandInteraction } from 'discord.js';
|
||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import listClans from '../blitz/listClans';
|
||||||
|
import throwError from '../node/throwError';
|
||||||
|
import { serverAndIdPattern } from './resolvePlayerFromCommand';
|
||||||
|
|
||||||
|
export default async function resolveClanFromCommand(
|
||||||
|
interaction: ChatInputCommandInteraction,
|
||||||
|
) {
|
||||||
|
const clan = interaction.options.getString('clan', true);
|
||||||
|
|
||||||
|
if (serverAndIdPattern.test(clan)) {
|
||||||
|
const [server, accountId] = clan.split('/');
|
||||||
|
return { region: server as Region, id: Number(accountId) };
|
||||||
|
} else {
|
||||||
|
const accounts = await listClans(clan);
|
||||||
|
|
||||||
|
if (accounts[0]) {
|
||||||
|
return { region: accounts[0].region, id: accounts[0].clan_id };
|
||||||
|
} else {
|
||||||
|
throw throwError(
|
||||||
|
'Could not find clan',
|
||||||
|
`I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,25 +3,11 @@ import { Region } from '../../constants/regions';
|
|||||||
import getPeriodNow from '../blitzstars/getPeriodNow';
|
import getPeriodNow from '../blitzstars/getPeriodNow';
|
||||||
import getPeriodStart from '../blitzstars/getPeriodStart';
|
import getPeriodStart from '../blitzstars/getPeriodStart';
|
||||||
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
|
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
|
||||||
import { Period } from '../discord/addPeriodSubCommands';
|
import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
|
||||||
|
|
||||||
export const PERIOD_NAMES: Record<Period, string> = {
|
export function getPeriodOptionName(period: PeriodSize) {
|
||||||
today: "Today's statistics",
|
return period === 'career' ? 'Career' : `${period}-day`;
|
||||||
30: "30 days' statistics",
|
}
|
||||||
60: "60 days' statistics",
|
|
||||||
90: "90 days' statistics",
|
|
||||||
career: 'Career statistics',
|
|
||||||
custom: 'Custom period',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = {
|
|
||||||
today: "Today's evolution",
|
|
||||||
30: "30 days' evolution",
|
|
||||||
60: "60 days' evolution",
|
|
||||||
90: "90 days' evolution",
|
|
||||||
career: 'Career evolution',
|
|
||||||
custom: 'Custom period',
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ResolvedPeriod {
|
export interface ResolvedPeriod {
|
||||||
statsName: string;
|
statsName: string;
|
||||||
@@ -31,29 +17,30 @@ export interface ResolvedPeriod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function resolvePeriodFromCommand(
|
export default function resolvePeriodFromCommand(
|
||||||
server: Region,
|
region: Region,
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
) {
|
) {
|
||||||
let statsName: string;
|
let statsName: string;
|
||||||
let evolutionName: string;
|
let evolutionName: string;
|
||||||
let start: number;
|
let start: number;
|
||||||
let end: number;
|
let end: number;
|
||||||
const period = interaction.options.getSubcommand() as Period;
|
const periodSubcommand = interaction.options.getSubcommand() as PeriodType;
|
||||||
|
const periodOption = interaction.options.getString('period') as PeriodSize;
|
||||||
|
|
||||||
if (period === 'custom') {
|
if (periodSubcommand === 'custom') {
|
||||||
const startRaw = interaction.options.getInteger('start')!;
|
const startRaw = interaction.options.getInteger('start', true);
|
||||||
const endRaw = interaction.options.getInteger('end')!;
|
const endRaw = interaction.options.getInteger('end', true);
|
||||||
const startMin = Math.min(startRaw, endRaw);
|
const startDaysAgoMin = Math.min(startRaw, endRaw);
|
||||||
const endMax = Math.max(startRaw, endRaw);
|
const endDaysAgoMax = Math.max(startRaw, endRaw);
|
||||||
|
|
||||||
statsName = `${startMin} to ${endMax} days' statistics`;
|
statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
|
||||||
evolutionName = `${startMin} to ${endMax} days' evolution`;
|
evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
|
||||||
start = getTimeDaysAgo(server, startMin);
|
start = getTimeDaysAgo(region, endDaysAgoMax);
|
||||||
end = getTimeDaysAgo(server, endMax);
|
end = getTimeDaysAgo(region, startDaysAgoMin);
|
||||||
} else {
|
} else {
|
||||||
statsName = PERIOD_NAMES[period];
|
statsName = `${getPeriodOptionName(periodOption)} Statistics`;
|
||||||
evolutionName = EVOLUTION_PERIOD_NAMES[period];
|
evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
|
||||||
start = getPeriodStart(server, period);
|
start = getPeriodStart(region, periodOption);
|
||||||
end = getPeriodNow();
|
end = getPeriodNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { ButtonBuilder, ButtonStyle } from 'discord.js';
|
|
||||||
|
|
||||||
// TODO: ADD THIS AGAIN IMMEDIATELY
|
|
||||||
export const supportBlitzStars = new ButtonBuilder()
|
|
||||||
.setLabel('Support BlitzStars')
|
|
||||||
.setURL('https://www.blitzstars.com/supporters')
|
|
||||||
.setStyle(ButtonStyle.Link);
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
import { secrets } from '../node/secrets';
|
||||||
|
|
||||||
|
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
||||||
@@ -7,5 +7,5 @@ export default function resolvePeriodFromRequest(
|
|||||||
server: Region,
|
server: Region,
|
||||||
request: Request,
|
request: Request,
|
||||||
) {
|
) {
|
||||||
return resolvePeriodFromURL(server, `${CYCLIC_API}${request.path}`);
|
return resolvePeriodFromURL(server, `${CYCLIC_API}${request.url}`);
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,10 @@ import { Region } from '../../constants/regions';
|
|||||||
import getPeriodNow from '../blitzstars/getPeriodNow';
|
import getPeriodNow from '../blitzstars/getPeriodNow';
|
||||||
import getPeriodStart from '../blitzstars/getPeriodStart';
|
import getPeriodStart from '../blitzstars/getPeriodStart';
|
||||||
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
|
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
|
||||||
import { Period } from '../discord/addPeriodSubCommands';
|
import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
|
||||||
import {
|
import {
|
||||||
EVOLUTION_PERIOD_NAMES,
|
|
||||||
PERIOD_NAMES,
|
|
||||||
ResolvedPeriod,
|
ResolvedPeriod,
|
||||||
|
getPeriodOptionName,
|
||||||
} from '../discord/resolvePeriodFromCommand';
|
} from '../discord/resolvePeriodFromCommand';
|
||||||
|
|
||||||
export default function resolvePeriodFromURL(
|
export default function resolvePeriodFromURL(
|
||||||
@@ -19,29 +18,25 @@ export default function resolvePeriodFromURL(
|
|||||||
let end: number;
|
let end: number;
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
const path = url.pathname.split('/').filter(Boolean);
|
||||||
const period = path[path.length - 1] as Period;
|
const periodSubcommand = path[path.length - 1] as PeriodType;
|
||||||
|
const periodOption = url.searchParams.get('period') as PeriodSize;
|
||||||
|
|
||||||
if (period === 'custom') {
|
if (periodSubcommand === 'custom') {
|
||||||
const startRaw = parseInt(url!.searchParams.get('start')!);
|
const startRaw = parseInt(url!.searchParams.get('start')!);
|
||||||
const endRaw = parseInt(url!.searchParams.get('end')!);
|
const endRaw = parseInt(url!.searchParams.get('end')!);
|
||||||
const startMin = Math.min(startRaw, endRaw);
|
const startDaysAgoMin = Math.min(startRaw, endRaw);
|
||||||
const endMax = Math.max(startRaw, endRaw);
|
const endDaysAgoMax = Math.max(startRaw, endRaw);
|
||||||
|
|
||||||
statsName = `${startMin} to ${endMax} days' statistics`;
|
statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
|
||||||
evolutionName = `${startMin} to ${endMax} days' evolution`;
|
evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
|
||||||
start = getTimeDaysAgo(server, startMin);
|
start = getTimeDaysAgo(server, endDaysAgoMax);
|
||||||
end = getTimeDaysAgo(server, endMax);
|
end = getTimeDaysAgo(server, startDaysAgoMin);
|
||||||
} else {
|
} else {
|
||||||
statsName = PERIOD_NAMES[period];
|
statsName = `${getPeriodOptionName(periodOption)} Statistics`;
|
||||||
evolutionName = EVOLUTION_PERIOD_NAMES[period];
|
evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
|
||||||
start = getPeriodStart(server, period);
|
start = getPeriodStart(server, periodOption);
|
||||||
end = getPeriodNow();
|
end = getPeriodNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { statsName, evolutionName, start, end } satisfies ResolvedPeriod;
|
||||||
statsName,
|
|
||||||
evolutionName,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
} satisfies ResolvedPeriod;
|
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ Promise.all(
|
|||||||
|
|
||||||
export default async function jsxToSvg(jsx: JSX.Element) {
|
export default async function jsxToSvg(jsx: JSX.Element) {
|
||||||
return await satori(jsx, {
|
return await satori(jsx, {
|
||||||
width: 640,
|
width: 480,
|
||||||
fonts: [
|
fonts: [
|
||||||
{ data: roboto, name: FONT_NAME, weight: 400 },
|
{ data: roboto, name: FONT_NAME, weight: 400 },
|
||||||
{ data: robotoBold, name: FONT_NAME, weight: 700 },
|
{ data: robotoBold, name: FONT_NAME, weight: 700 },
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||||
import discord from '../../discord.json' assert { type: 'json' };
|
import discord from '../../discord.json' assert { type: 'json' };
|
||||||
import { client } from '../bot';
|
|
||||||
import { NEGATIVE_COLOR } from '../constants/colors';
|
import { NEGATIVE_COLOR } from '../constants/colors';
|
||||||
|
import { client } from '../core/discord/client';
|
||||||
import isDev from '../core/node/isDev';
|
import isDev from '../core/node/isDev';
|
||||||
|
|
||||||
const PROCESS_ERROR_EVENTS = ['uncaughtException'];
|
const PROCESS_ERROR_EVENTS = ['uncaughtException'];
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AutocompleteInteraction, CacheType } from 'discord.js';
|
import { AutocompleteInteraction, CacheType } from 'discord.js';
|
||||||
import { commands } from '..';
|
import { commands } from '..';
|
||||||
|
|
||||||
export default function handleAutocomplete(
|
export default async function handleAutocomplete(
|
||||||
interaction: AutocompleteInteraction<CacheType>,
|
interaction: AutocompleteInteraction<CacheType>,
|
||||||
) {
|
) {
|
||||||
const command = commands[interaction.commandName];
|
const command = (await commands)[interaction.commandName];
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default async function handleButton(
|
|||||||
|
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||||
const commandName = url.pathname.split('/')[1];
|
const commandName = url.pathname.split('/')[1];
|
||||||
const button = commands[commandName]?.button;
|
const button = (await commands)[commandName]?.button;
|
||||||
|
|
||||||
if (!button) {
|
if (!button) {
|
||||||
throwError(`Button handler not found for "${commandName}"`);
|
throwError(`Button handler not found for "${commandName}"`);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { handleError } from '../../error';
|
|||||||
export default async function handleChatInputCommand(
|
export default async function handleChatInputCommand(
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
) {
|
) {
|
||||||
const registry = commands[interaction.commandName];
|
const registry = (await commands)[interaction.commandName];
|
||||||
|
|
||||||
console.log(interaction.toString());
|
console.log(interaction.toString());
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
SlashCommandSubcommandsOnlyBuilder,
|
SlashCommandSubcommandsOnlyBuilder,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import discord from '../../../discord.json' assert { type: 'json' };
|
import discord from '../../../discord.json' assert { type: 'json' };
|
||||||
|
import { clanEventCommand } from '../../commands/clanEvent';
|
||||||
import { debugCommand } from '../../commands/debug';
|
import { debugCommand } from '../../commands/debug';
|
||||||
import { eligibleCommand } from '../../commands/eligible';
|
import { eligibleCommand } from '../../commands/eligible';
|
||||||
import { evolutionCommand } from '../../commands/evolution';
|
import { evolutionCommand } from '../../commands/evolution';
|
||||||
@@ -50,26 +51,33 @@ export type InteractionReturnable =
|
|||||||
| Promise<InteractionIterableReturnable>;
|
| Promise<InteractionIterableReturnable>;
|
||||||
|
|
||||||
export interface Registry {
|
export interface Registry {
|
||||||
inDevelopment: boolean;
|
|
||||||
inProduction: boolean;
|
inProduction: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandRegistry<HandlesInteraction extends boolean = boolean>
|
export type CommandRegistryRaw = Registry & {
|
||||||
extends Registry {
|
|
||||||
inPublic: boolean;
|
inPublic: boolean;
|
||||||
inPreview?: boolean;
|
inPreview?: boolean;
|
||||||
handlesInteraction?: HandlesInteraction;
|
|
||||||
command:
|
command:
|
||||||
| SlashCommandBuilder
|
| SlashCommandBuilder
|
||||||
| SlashCommandSubcommandsOnlyBuilder
|
| SlashCommandSubcommandsOnlyBuilder
|
||||||
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
||||||
|
|
||||||
handler: (
|
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
|
||||||
) => HandlesInteraction extends true ? void : InteractionReturnable;
|
|
||||||
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
||||||
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
handlesInteraction: true;
|
||||||
|
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
handlesInteraction?: false;
|
||||||
|
handler: (
|
||||||
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
|
) => InteractionReturnable;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export type CommandRegistry = CommandRegistryRaw | Promise<CommandRegistryRaw>;
|
||||||
|
|
||||||
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
|
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
|
||||||
|
|
||||||
@@ -91,13 +99,21 @@ export const COMMANDS_RAW: CommandRegistry[] = [
|
|||||||
evolutionCommand,
|
evolutionCommand,
|
||||||
statsCommand,
|
statsCommand,
|
||||||
ratingsCommand,
|
ratingsCommand,
|
||||||
|
clanEventCommand,
|
||||||
];
|
];
|
||||||
export const commands: Record<string, CommandRegistry> = COMMANDS_RAW.reduce(
|
|
||||||
(accumulator, registry) => {
|
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
|
||||||
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
rawCommands.reduce<Record<string, CommandRegistryRaw>>(
|
||||||
return { ...accumulator, [registry.command.name]: registry };
|
(accumulator, registry) => {
|
||||||
},
|
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
||||||
{},
|
|
||||||
|
return {
|
||||||
|
...accumulator,
|
||||||
|
[registry.command.name]: registry,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
||||||
@@ -105,8 +121,8 @@ export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
|||||||
export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
||||||
[];
|
[];
|
||||||
|
|
||||||
Object.entries(commands).forEach(([, registry]) => {
|
commands.then((awaitedCommands) => {
|
||||||
if (isDev() ? registry.inDevelopment : registry.inProduction) {
|
Object.entries(awaitedCommands).forEach(([, registry]) => {
|
||||||
const json = registry.command.toJSON();
|
const json = registry.command.toJSON();
|
||||||
|
|
||||||
if (registry.inPublic) {
|
if (registry.inPublic) {
|
||||||
@@ -114,39 +130,39 @@ Object.entries(commands).forEach(([, registry]) => {
|
|||||||
} else {
|
} else {
|
||||||
guildCommands.push(json);
|
guildCommands.push(json);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Refreshing ${publicCommands.length} public command(s).`);
|
||||||
|
rest
|
||||||
|
.put(Routes.applicationCommands(getClientId()), { body: publicCommands })
|
||||||
|
.then((publicData) => {
|
||||||
|
console.log(
|
||||||
|
`Successfully refreshed ${
|
||||||
|
(publicData as RESTPostAPIChatInputApplicationCommandsJSONBody[])
|
||||||
|
.length
|
||||||
|
} public command(s).`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Refreshing ${guildCommands.length} guild command(s).`);
|
||||||
|
rest
|
||||||
|
.put(
|
||||||
|
Routes.applicationGuildCommands(getClientId(), discord.tres_guild_id),
|
||||||
|
{ body: guildCommands },
|
||||||
|
)
|
||||||
|
.then((guildData) => {
|
||||||
|
console.log(
|
||||||
|
`Successfully refreshed ${
|
||||||
|
(guildData as unknown[]).length
|
||||||
|
} guild command(s).`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`Refreshing ${publicCommands.length} public command(s).`);
|
|
||||||
rest
|
|
||||||
.put(Routes.applicationCommands(getClientId()), { body: publicCommands })
|
|
||||||
.then((publicData) => {
|
|
||||||
console.log(
|
|
||||||
`Successfully refreshed ${
|
|
||||||
(publicData as RESTPostAPIChatInputApplicationCommandsJSONBody[])
|
|
||||||
.length
|
|
||||||
} public command(s).`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Refreshing ${guildCommands.length} guild command(s).`);
|
|
||||||
rest
|
|
||||||
.put(
|
|
||||||
Routes.applicationGuildCommands(getClientId(), discord.tres_guild_id),
|
|
||||||
{ body: guildCommands },
|
|
||||||
)
|
|
||||||
.then((guildData) => {
|
|
||||||
console.log(
|
|
||||||
`Successfully refreshed ${
|
|
||||||
(guildData as unknown[]).length
|
|
||||||
} guild command(s).`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function interactionCreate(
|
export default async function interactionCreate(
|
||||||
interaction: Interaction<CacheType>,
|
interaction: Interaction<CacheType>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import * as Graph from '../components/Graph';
|
import * as Graph from '../components/Graph';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES } from '../constants/regions';
|
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
import { tankopedia } from '../core/blitz/tankopedia';
|
||||||
@@ -15,7 +13,7 @@ import { secrets } from '../core/node/secrets';
|
|||||||
import { AccountInfo } from '../types/accountInfo';
|
import { AccountInfo } from '../types/accountInfo';
|
||||||
import { Histories } from '../types/histories';
|
import { Histories } from '../types/histories';
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
import { PlayerClanData } from '../types/playerClanData';
|
||||||
import { StatType } from './fullStats';
|
import { StatType } from './stats';
|
||||||
|
|
||||||
export default async function evolution<Type extends StatType>(
|
export default async function evolution<Type extends StatType>(
|
||||||
type: Type,
|
type: Type,
|
||||||
@@ -88,20 +86,17 @@ export default async function evolution<Type extends StatType>(
|
|||||||
name={accountInfo[id].nickname}
|
name={accountInfo[id].nickname}
|
||||||
nameDiscriminator={nameDiscriminator}
|
nameDiscriminator={nameDiscriminator}
|
||||||
image={image}
|
image={image}
|
||||||
description={`${evolutionName} • ${new Date().toDateString()} • ${
|
description={`${evolutionName} • ${new Date().toDateString()}`}
|
||||||
REGION_NAMES[server]
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* goofy ahh bug forces me to call them as functions */}
|
{/* goofy ahh bug forces me to call them as functions */}
|
||||||
{plot.length > 0 && (
|
{plot.length > 0 && (
|
||||||
<Graph.Root
|
<Graph.Root
|
||||||
xTitle="Battles"
|
|
||||||
verticalMargin={{
|
verticalMargin={{
|
||||||
min: minY,
|
min: minY,
|
||||||
max: maxY,
|
max: maxY,
|
||||||
suffix: '%',
|
suffix: '%',
|
||||||
precision: 1,
|
precision: 2,
|
||||||
}}
|
}}
|
||||||
horizontalMargin={{
|
horizontalMargin={{
|
||||||
min: minX,
|
min: minX,
|
||||||
@@ -122,8 +117,6 @@ export default async function evolution<Type extends StatType>(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.BlitzStars} />
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+80
-76
@@ -1,10 +1,8 @@
|
|||||||
import GenericAllStats from '../components/GenericAllStats';
|
import GenericAllStats from '../components/GenericAllStats';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
|
||||||
import TierWeights, { TierWeightsRecord } from '../components/TierWeights';
|
import TierWeights, { TierWeightsRecord } from '../components/TierWeights';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES } from '../constants/regions';
|
|
||||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
@@ -21,20 +19,22 @@ import {
|
|||||||
SupplementaryStats,
|
SupplementaryStats,
|
||||||
} from '../types/accountInfo';
|
} from '../types/accountInfo';
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
import { PlayerClanData } from '../types/playerClanData';
|
||||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
import { MultiTankFilters, StatType } from './stats';
|
||||||
|
|
||||||
export type StatType = 'player' | 'tank';
|
|
||||||
|
|
||||||
export default async function fullStats<Type extends StatType>(
|
export default async function fullStats<Type extends StatType>(
|
||||||
type: Type,
|
type: Type,
|
||||||
{ start, end, statsName }: ResolvedPeriod,
|
{ start, end, statsName }: ResolvedPeriod,
|
||||||
{ region: server, id }: ResolvedPlayer,
|
{ region: server, id }: ResolvedPlayer,
|
||||||
tankId: Type extends 'tank' ? number : null,
|
filters: Type extends 'tank'
|
||||||
|
? number
|
||||||
|
: Type extends 'multi-tank'
|
||||||
|
? Partial<MultiTankFilters>
|
||||||
|
: null,
|
||||||
) {
|
) {
|
||||||
let nameDiscriminator: string | undefined;
|
let nameDiscriminator: string | undefined;
|
||||||
let image: string | undefined;
|
let image: string | undefined;
|
||||||
|
|
||||||
if (type === 'player') {
|
if (type === 'player' || type === 'multi-tank') {
|
||||||
const clan = (
|
const clan = (
|
||||||
await getWargamingResponse<PlayerClanData>(
|
await getWargamingResponse<PlayerClanData>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||||
@@ -46,89 +46,97 @@ export default async function fullStats<Type extends StatType>(
|
|||||||
? `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;
|
||||||
} else if (type === 'tank') {
|
} else if (type === 'tank') {
|
||||||
nameDiscriminator = `(${await resolveTankName(tankId!)})`;
|
nameDiscriminator = `(${await resolveTankName(filters as number)})`;
|
||||||
image = (await tankopedia)[tankId!]?.images.normal;
|
image = (await tankopedia)[filters as number]?.images.normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { diffed } = await getDiffedTankStats(server, id, start, end);
|
const { diffed, order } = await getDiffedTankStats(server, id, start, end);
|
||||||
let stats: AllStats;
|
const awaitedTankopedia = await tankopedia;
|
||||||
|
let stats: AllStats | undefined;
|
||||||
let supplementaryStats: SupplementaryStats;
|
let supplementaryStats: SupplementaryStats;
|
||||||
let tierWeights: TierWeightsRecord;
|
let tierWeights: TierWeightsRecord;
|
||||||
|
|
||||||
if (type === 'player') {
|
if (type === 'player' || type === 'multi-tank') {
|
||||||
const entries = Object.entries(diffed);
|
const typedFilters = filters as Partial<MultiTankFilters> | null;
|
||||||
stats = sumStats(entries.map(([, stats]) => stats));
|
const filteredOrder = order.filter(
|
||||||
const battlesOfTanksWithAverages = await entries.reduce<
|
(id) =>
|
||||||
PossiblyPromise<number>
|
typedFilters === null ||
|
||||||
>(async (accumulator, [tankIdString, stats]) => {
|
((typedFilters.nation === undefined ||
|
||||||
const tankId = parseInt(tankIdString);
|
awaitedTankopedia[id]?.nation === typedFilters.nation) &&
|
||||||
const tankAverage = (await tankAverages)[tankId];
|
(typedFilters['tank-type'] === undefined ||
|
||||||
|
awaitedTankopedia[id]?.type === typedFilters['tank-type']) &&
|
||||||
return tankAverage ? (await accumulator) + stats.battles : accumulator;
|
(typedFilters.tier === undefined ||
|
||||||
}, 0);
|
awaitedTankopedia[id]?.tier === typedFilters.tier) &&
|
||||||
const battlesOfTanksWithTankopediaEntry = await entries.reduce<
|
(typedFilters['tree-type'] === undefined ||
|
||||||
PossiblyPromise<number>
|
(typedFilters['tree-type'] === 'collector' &&
|
||||||
>(async (accumulator, [tankIdString, stats]) => {
|
awaitedTankopedia[id]?.is_collectible) ||
|
||||||
const tankId = parseInt(tankIdString);
|
(typedFilters['tree-type'] === 'premium' &&
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
awaitedTankopedia[id]?.is_premium) ||
|
||||||
|
(typedFilters['tree-type'] === 'techtree' &&
|
||||||
return tankopediaEntry
|
!awaitedTankopedia[id]?.is_collectible &&
|
||||||
? (await accumulator) + stats.battles
|
!awaitedTankopedia[id]?.is_premium))),
|
||||||
: accumulator;
|
);
|
||||||
}, 0);
|
const awaitedTankAverages = await tankAverages;
|
||||||
|
stats = sumStats(filteredOrder.map((id) => diffed[id]));
|
||||||
|
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
|
||||||
|
(accumulator, id) =>
|
||||||
|
awaitedTankAverages[id]
|
||||||
|
? accumulator + diffed[id].battles
|
||||||
|
: accumulator,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const battlesOfTanksWithTankopediaEntry = filteredOrder.reduce<number>(
|
||||||
|
(accumulator, id) =>
|
||||||
|
awaitedTankopedia[id] ? accumulator + diffed[id].battles : accumulator,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
supplementaryStats = {
|
supplementaryStats = {
|
||||||
WN8:
|
WN8:
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
filteredOrder.reduce<number>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
(accumulator, id) =>
|
||||||
const tankId = parseInt(tankIdString);
|
awaitedTankAverages[id]
|
||||||
const tankAverage = (await tankAverages)[tankId];
|
? accumulator +
|
||||||
|
calculateWN8(awaitedTankAverages[id].all, diffed[id]) *
|
||||||
return tankAverage
|
diffed[id].battles
|
||||||
? (await accumulator) +
|
: accumulator,
|
||||||
calculateWN8(tankAverage.all, stats) * stats.battles
|
|
||||||
: accumulator;
|
|
||||||
},
|
|
||||||
0,
|
0,
|
||||||
)) / battlesOfTanksWithAverages,
|
) / battlesOfTanksWithAverages,
|
||||||
tier:
|
tier:
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
filteredOrder.reduce<number>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
(accumulator, id) =>
|
||||||
const tankId = parseInt(tankIdString);
|
awaitedTankopedia[id]
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
? accumulator + awaitedTankopedia[id]!.tier * diffed[id].battles
|
||||||
|
: accumulator,
|
||||||
return tankopediaEntry
|
|
||||||
? (await accumulator) + tankopediaEntry.tier * stats.battles
|
|
||||||
: accumulator;
|
|
||||||
},
|
|
||||||
0,
|
0,
|
||||||
)) / battlesOfTanksWithTankopediaEntry,
|
) / battlesOfTanksWithTankopediaEntry,
|
||||||
};
|
};
|
||||||
tierWeights = await entries.reduce<PossiblyPromise<TierWeightsRecord>>(
|
tierWeights = filteredOrder.reduce<TierWeightsRecord>((accumulator, id) => {
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
const tankopediaEntry = awaitedTankopedia[id];
|
||||||
const tankId = parseInt(tankIdString);
|
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
|
||||||
|
|
||||||
if (!tankopediaEntry) return accumulator;
|
if (!tankopediaEntry) return accumulator;
|
||||||
|
|
||||||
const tier = tankopediaEntry.tier as Tier;
|
const tier = tankopediaEntry.tier as Tier;
|
||||||
|
|
||||||
if ((await accumulator)[tier]) {
|
if (accumulator[tier]) {
|
||||||
(await accumulator)[tier]! += stats.battles;
|
accumulator[tier]! += diffed[id].battles;
|
||||||
} else {
|
} else {
|
||||||
(await accumulator)[tier] = stats.battles;
|
accumulator[tier] = diffed[id].battles;
|
||||||
}
|
}
|
||||||
|
|
||||||
return accumulator;
|
return accumulator;
|
||||||
},
|
}, {});
|
||||||
{},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
stats = diffed[tankId!];
|
stats = diffed[(filters as number)!];
|
||||||
|
|
||||||
supplementaryStats = {
|
supplementaryStats = {
|
||||||
WN8: calculateWN8((await tankAverages)[tankId!].all, diffed[tankId!]),
|
WN8: stats
|
||||||
tier: (await tankopedia)[tankId!]?.tier,
|
? calculateWN8(
|
||||||
|
(await tankAverages)[(filters as number)!].all,
|
||||||
|
diffed[(filters as number)!],
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
tier: (await tankopedia)[(filters as number)!]?.tier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +150,7 @@ export default async function fullStats<Type extends StatType>(
|
|||||||
name={accountInfo[id].nickname}
|
name={accountInfo[id].nickname}
|
||||||
nameDiscriminator={nameDiscriminator}
|
nameDiscriminator={nameDiscriminator}
|
||||||
image={image}
|
image={image}
|
||||||
description={`${statsName} • ${new Date().toDateString()} • ${
|
description={`${statsName} • ${new Date().toDateString()}`}
|
||||||
REGION_NAMES[server]
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
@@ -157,8 +163,6 @@ export default async function fullStats<Type extends StatType>(
|
|||||||
supplementaryStats={supplementaryStats}
|
supplementaryStats={supplementaryStats}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.BlitzStars} />
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+78
-83
@@ -1,15 +1,13 @@
|
|||||||
import AllStatsOverview from '../components/AllStatsOverview';
|
import AllStatsOverview from '../components/AllStatsOverview';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
import { TreeTypeString } from '../components/Tanks';
|
||||||
import { TierWeightsRecord } from '../components/TierWeights';
|
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES } from '../constants/regions';
|
|
||||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
import sumStats from '../core/blitz/sumStats';
|
import sumStats from '../core/blitz/sumStats';
|
||||||
import { Tier, tankopedia } from '../core/blitz/tankopedia';
|
import { tankopedia } from '../core/blitz/tankopedia';
|
||||||
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
|
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
|
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
|
||||||
@@ -22,21 +20,31 @@ import {
|
|||||||
SupplementaryStats,
|
SupplementaryStats,
|
||||||
} from '../types/accountInfo';
|
} from '../types/accountInfo';
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
import { PlayerClanData } from '../types/playerClanData';
|
||||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
|
||||||
|
|
||||||
export type StatType = 'player' | 'tank';
|
export type StatType = 'player' | 'tank' | 'multi-tank';
|
||||||
|
|
||||||
|
export interface MultiTankFilters {
|
||||||
|
nation: string;
|
||||||
|
'tank-type': string;
|
||||||
|
tier: number;
|
||||||
|
'tree-type': TreeTypeString;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function stats<Type extends StatType>(
|
export default async function stats<Type extends StatType>(
|
||||||
type: Type,
|
type: Type,
|
||||||
{ start, end, statsName }: ResolvedPeriod,
|
{ start, end, statsName }: ResolvedPeriod,
|
||||||
{ region: server, id }: ResolvedPlayer,
|
{ region: server, id }: ResolvedPlayer,
|
||||||
tankId: Type extends 'tank' ? number : null,
|
filters: Type extends 'tank'
|
||||||
|
? number
|
||||||
|
: Type extends 'multi-tank'
|
||||||
|
? Partial<MultiTankFilters>
|
||||||
|
: null,
|
||||||
naked = false,
|
naked = false,
|
||||||
) {
|
) {
|
||||||
let nameDiscriminator: string | undefined;
|
let nameDiscriminator: string | undefined;
|
||||||
let image: string | undefined;
|
let image: string | undefined;
|
||||||
|
|
||||||
if (type === 'player') {
|
if (type === 'player' || type === 'multi-tank') {
|
||||||
const clan = (
|
const clan = (
|
||||||
await getWargamingResponse<PlayerClanData>(
|
await getWargamingResponse<PlayerClanData>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||||
@@ -48,91 +56,82 @@ export default async function stats<Type extends StatType>(
|
|||||||
? `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;
|
||||||
} else if (type === 'tank') {
|
} else if (type === 'tank') {
|
||||||
nameDiscriminator = `(${await resolveTankName(tankId!)})`;
|
nameDiscriminator = `(${await resolveTankName(filters as number)})`;
|
||||||
image = (await tankopedia)[tankId!]?.images.normal;
|
image = (await tankopedia)[filters as number]?.images.normal;
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { diffed } = await getDiffedTankStats(server, id, start, end);
|
const { diffed, order } = await getDiffedTankStats(server, id, start, end);
|
||||||
|
const awaitedTankopedia = await tankopedia;
|
||||||
let stats: AllStats | undefined;
|
let stats: AllStats | undefined;
|
||||||
let supplementaryStats: SupplementaryStats;
|
let supplementaryStats: SupplementaryStats;
|
||||||
let tierWeights: TierWeightsRecord;
|
|
||||||
|
|
||||||
if (type === 'player') {
|
if (type === 'player' || type === 'multi-tank') {
|
||||||
const entries = Object.entries(diffed);
|
const typedFilters = filters as Partial<MultiTankFilters> | null;
|
||||||
stats = sumStats(entries.map(([, stats]) => stats));
|
const filteredOrder = order.filter(
|
||||||
const battlesOfTanksWithAverages = await entries.reduce<
|
(id) =>
|
||||||
PossiblyPromise<number>
|
typedFilters === null ||
|
||||||
>(async (accumulator, [tankIdString, stats]) => {
|
((typedFilters.nation === undefined ||
|
||||||
const tankId = parseInt(tankIdString);
|
awaitedTankopedia[id]?.nation === typedFilters.nation) &&
|
||||||
const tankAverage = (await tankAverages)[tankId];
|
(typedFilters['tank-type'] === undefined ||
|
||||||
|
awaitedTankopedia[id]?.type === typedFilters['tank-type']) &&
|
||||||
return tankAverage ? (await accumulator) + stats.battles : accumulator;
|
(typedFilters.tier === undefined ||
|
||||||
}, 0);
|
awaitedTankopedia[id]?.tier === typedFilters.tier) &&
|
||||||
const battlesOfTanksWithTankopediaEntry = await entries.reduce<
|
(typedFilters['tree-type'] === undefined ||
|
||||||
PossiblyPromise<number>
|
(typedFilters['tree-type'] === 'collector' &&
|
||||||
>(async (accumulator, [tankIdString, stats]) => {
|
awaitedTankopedia[id]?.is_collectible) ||
|
||||||
const tankId = parseInt(tankIdString);
|
(typedFilters['tree-type'] === 'premium' &&
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
awaitedTankopedia[id]?.is_premium) ||
|
||||||
|
(typedFilters['tree-type'] === 'techtree' &&
|
||||||
return tankopediaEntry
|
!awaitedTankopedia[id]?.is_collectible &&
|
||||||
? (await accumulator) + stats.battles
|
!awaitedTankopedia[id]?.is_premium))),
|
||||||
: accumulator;
|
);
|
||||||
}, 0);
|
const awaitedTankAverages = await tankAverages;
|
||||||
|
stats = sumStats(filteredOrder.map((id) => diffed[id]));
|
||||||
|
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
|
||||||
|
(accumulator, id) =>
|
||||||
|
awaitedTankAverages[id]
|
||||||
|
? accumulator + diffed[id].battles
|
||||||
|
: accumulator,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const battlesOfTanksWithTankopediaEntry = filteredOrder.reduce<number>(
|
||||||
|
(accumulator, id) =>
|
||||||
|
awaitedTankopedia[id] ? accumulator + diffed[id].battles : accumulator,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
supplementaryStats = {
|
supplementaryStats = {
|
||||||
WN8:
|
WN8:
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
filteredOrder.reduce<number>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
(accumulator, id) =>
|
||||||
const tankId = parseInt(tankIdString);
|
awaitedTankAverages[id]
|
||||||
const tankAverage = (await tankAverages)[tankId];
|
? accumulator +
|
||||||
|
calculateWN8(awaitedTankAverages[id].all, diffed[id]) *
|
||||||
return tankAverage
|
diffed[id].battles
|
||||||
? (await accumulator) +
|
: accumulator,
|
||||||
calculateWN8(tankAverage.all, stats) * stats.battles
|
|
||||||
: accumulator;
|
|
||||||
},
|
|
||||||
0,
|
0,
|
||||||
)) / battlesOfTanksWithAverages,
|
) / battlesOfTanksWithAverages,
|
||||||
tier:
|
tier:
|
||||||
(await entries.reduce<PossiblyPromise<number>>(
|
filteredOrder.reduce<number>(
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
(accumulator, id) =>
|
||||||
const tankId = parseInt(tankIdString);
|
awaitedTankopedia[id]
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
? accumulator + awaitedTankopedia[id]!.tier * diffed[id].battles
|
||||||
|
: accumulator,
|
||||||
return tankopediaEntry
|
|
||||||
? (await accumulator) + tankopediaEntry.tier * stats.battles
|
|
||||||
: accumulator;
|
|
||||||
},
|
|
||||||
0,
|
0,
|
||||||
)) / battlesOfTanksWithTankopediaEntry,
|
) / battlesOfTanksWithTankopediaEntry,
|
||||||
};
|
};
|
||||||
tierWeights = await entries.reduce<PossiblyPromise<TierWeightsRecord>>(
|
|
||||||
async (accumulator, [tankIdString, stats]) => {
|
|
||||||
const tankId = parseInt(tankIdString);
|
|
||||||
const tankopediaEntry = (await tankopedia)[tankId];
|
|
||||||
|
|
||||||
if (!tankopediaEntry) return accumulator;
|
|
||||||
|
|
||||||
const tier = tankopediaEntry.tier as Tier;
|
|
||||||
|
|
||||||
if ((await accumulator)[tier]) {
|
|
||||||
(await accumulator)[tier]! += stats.battles;
|
|
||||||
} else {
|
|
||||||
(await accumulator)[tier] = stats.battles;
|
|
||||||
}
|
|
||||||
|
|
||||||
return accumulator;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
stats = diffed[tankId!];
|
stats = diffed[(filters as number)!];
|
||||||
|
|
||||||
supplementaryStats = {
|
supplementaryStats = {
|
||||||
WN8: stats
|
WN8: stats
|
||||||
? calculateWN8((await tankAverages)[tankId!].all, diffed[tankId!])
|
? calculateWN8(
|
||||||
|
(await tankAverages)[(filters as number)!].all,
|
||||||
|
diffed[(filters as number)!],
|
||||||
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
tier: (await tankopedia)[tankId!]?.tier,
|
tier: (await tankopedia)[(filters as number)!]?.tier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +150,7 @@ export default async function stats<Type extends StatType>(
|
|||||||
gap: 4,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<u>/statsfull</u> for all statistics
|
<u>/full-stats</u> for all statistics
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -161,17 +160,13 @@ export default async function stats<Type extends StatType>(
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo[id].nickname}
|
||||||
nameDiscriminator={nameDiscriminator}
|
|
||||||
image={image}
|
image={image}
|
||||||
description={`${statsName} • ${new Date().toDateString()} • ${
|
nameDiscriminator={nameDiscriminator}
|
||||||
REGION_NAMES[server]
|
description={`${statsName} • ${new Date().toDateString()}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
|
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
{stats?.battles > 0 && overview}
|
{stats?.battles > 0 && overview}
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.BlitzStars} footer={footer} />
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+57
-56
@@ -1,11 +1,10 @@
|
|||||||
import * as Breakdown from '../components/Breakdown';
|
import * as Breakdown from '../components/Breakdown';
|
||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES } from '../constants/regions';
|
|
||||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||||
import getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
import sumStats from '../core/blitz/sumStats';
|
import sumStats from '../core/blitz/sumStats';
|
||||||
@@ -22,7 +21,9 @@ import { PossiblyPromise } from '../types/possiblyPromise';
|
|||||||
|
|
||||||
export default async function today(
|
export default async function today(
|
||||||
{ region: server, id }: ResolvedPlayer,
|
{ region: server, id }: ResolvedPlayer,
|
||||||
limit = Infinity,
|
cutoff = Infinity,
|
||||||
|
maximized = 4,
|
||||||
|
showTotal = true,
|
||||||
naked?: boolean,
|
naked?: boolean,
|
||||||
) {
|
) {
|
||||||
const { diffed, order } = await getDiffedTankStats(
|
const { diffed, order } = await getDiffedTankStats(
|
||||||
@@ -38,9 +39,11 @@ export default async function today(
|
|||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||||
);
|
);
|
||||||
const careerTankStatsRaw = await getTankStats(server, id);
|
const careerTankStatsRaw = await getTankStats(server, id);
|
||||||
const careerStats: Record<number, AllStats> = {
|
const careerStats: Record<number, AllStats> = showTotal
|
||||||
0: accountInfo[id].statistics.all,
|
? {
|
||||||
};
|
0: accountInfo[id].statistics.all,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
const allStatsToAccumulate: AllStats[] = [];
|
const allStatsToAccumulate: AllStats[] = [];
|
||||||
|
|
||||||
Object.entries(diffed).forEach(([, tankStats]) => {
|
Object.entries(diffed).forEach(([, tankStats]) => {
|
||||||
@@ -52,7 +55,7 @@ export default async function today(
|
|||||||
|
|
||||||
const accumulatedStats = sumStats(allStatsToAccumulate);
|
const accumulatedStats = sumStats(allStatsToAccumulate);
|
||||||
|
|
||||||
if (Object.keys(diffed).length > 0) {
|
if (showTotal && Object.keys(diffed).length > 0) {
|
||||||
diffed[0] = accumulatedStats;
|
diffed[0] = accumulatedStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ export default async function today(
|
|||||||
>(async (accumulator, [tankIdString, tankStats]) => {
|
>(async (accumulator, [tankIdString, tankStats]) => {
|
||||||
const tankId = parseInt(tankIdString);
|
const tankId = parseInt(tankIdString);
|
||||||
|
|
||||||
return tankId === 0 || !(await tankAverages)[tankId]
|
return (showTotal && tankId === 0) || !(await tankAverages)[tankId]
|
||||||
? accumulator
|
? accumulator
|
||||||
: {
|
: {
|
||||||
...(await accumulator),
|
...(await accumulator),
|
||||||
@@ -73,7 +76,8 @@ export default async function today(
|
|||||||
const careerWN8s = await careerTankStatsRaw.reduce<
|
const careerWN8s = await careerTankStatsRaw.reduce<
|
||||||
PossiblyPromise<Record<number, number>>
|
PossiblyPromise<Record<number, number>>
|
||||||
>(async (accumulator, { tank_id }) => {
|
>(async (accumulator, { tank_id }) => {
|
||||||
return tank_id === 0 || (await tankAverages)[tank_id] === undefined
|
return (showTotal && tank_id === 0) ||
|
||||||
|
(await tankAverages)[tank_id] === undefined
|
||||||
? accumulator
|
? accumulator
|
||||||
: {
|
: {
|
||||||
...(await accumulator),
|
...(await accumulator),
|
||||||
@@ -86,60 +90,61 @@ export default async function today(
|
|||||||
const todayWN8sEntries = Object.entries(todayWN8s);
|
const todayWN8sEntries = Object.entries(todayWN8s);
|
||||||
const careerWN8sEntries = Object.entries(careerWN8s);
|
const careerWN8sEntries = Object.entries(careerWN8s);
|
||||||
|
|
||||||
todayWN8s[0] =
|
if (showTotal) {
|
||||||
todayWN8sEntries.reduce(
|
todayWN8s[0] =
|
||||||
(accumulator, [tankIdString, wn8]) =>
|
todayWN8sEntries.reduce(
|
||||||
accumulator + wn8 * diffed[Number(tankIdString)].battles,
|
(accumulator, [tankIdString, wn8]) =>
|
||||||
0,
|
accumulator + wn8 * diffed[Number(tankIdString)].battles,
|
||||||
) /
|
0,
|
||||||
todayWN8sEntries.reduce(
|
) /
|
||||||
(accumulator, [tankIdString]) =>
|
todayWN8sEntries.reduce(
|
||||||
accumulator + diffed[Number(tankIdString)].battles,
|
(accumulator, [tankIdString]) =>
|
||||||
0,
|
accumulator + diffed[Number(tankIdString)].battles,
|
||||||
);
|
0,
|
||||||
careerWN8s[0] =
|
);
|
||||||
careerWN8sEntries.reduce(
|
careerWN8s[0] =
|
||||||
(accumulator, [tankIdString, WN8]) =>
|
careerWN8sEntries.reduce(
|
||||||
isNaN(WN8)
|
(accumulator, [tankIdString, WN8]) =>
|
||||||
? accumulator
|
isNaN(WN8)
|
||||||
: accumulator + WN8 * careerStats[Number(tankIdString)].battles,
|
? accumulator
|
||||||
0,
|
: accumulator + WN8 * careerStats[Number(tankIdString)].battles,
|
||||||
) /
|
0,
|
||||||
careerWN8sEntries.reduce(
|
) /
|
||||||
(accumulator, [tankIdString, WN8]) =>
|
careerWN8sEntries.reduce(
|
||||||
isNaN(WN8)
|
(accumulator, [tankIdString, WN8]) =>
|
||||||
? accumulator
|
isNaN(WN8)
|
||||||
: accumulator + careerStats[Number(tankIdString)].battles,
|
? accumulator
|
||||||
0,
|
: accumulator + careerStats[Number(tankIdString)].battles,
|
||||||
);
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const rows = await Promise.all(
|
const rows = await Promise.all(
|
||||||
[
|
[
|
||||||
// this code unreadable on god will forget tomorrow no cap
|
// this code unreadable on god will forget tomorrow no cap
|
||||||
...(order.length === 0 ? [] : [0]),
|
...(order.length === 0 || !showTotal ? [] : [0]),
|
||||||
...(isFinite(limit) ? order.slice(0, limit) : order),
|
...(isFinite(cutoff) ? order.slice(0, cutoff) : order),
|
||||||
].map(async (tankId, index) => {
|
].map(async (id, index) => {
|
||||||
const tankStats = diffed[tankId];
|
const tankStats = diffed[id];
|
||||||
const career = careerStats[tankId];
|
const career = careerStats[id];
|
||||||
|
const tankopediaEntry = (await tankopedia)[id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breakdown.Row
|
<Breakdown.Row
|
||||||
naked={naked}
|
isTank={!showTotal || id !== 0}
|
||||||
isListing={tankId !== 0}
|
tankType={tankopediaEntry?.type}
|
||||||
minimized={index > 4}
|
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||||
key={tankId}
|
minimized={showTotal ? index > maximized : index + 1 > maximized}
|
||||||
name={tankId === 0 ? 'Total' : await resolveTankName(tankId)}
|
key={id}
|
||||||
|
name={showTotal && id === 0 ? 'Total' : await resolveTankName(id)}
|
||||||
winrate={tankStats.wins / tankStats.battles}
|
winrate={tankStats.wins / tankStats.battles}
|
||||||
careerWinrate={career.wins / career.battles}
|
careerWinrate={career.wins / career.battles}
|
||||||
WN8={isNaN(todayWN8s[tankId]) ? undefined : todayWN8s[tankId]}
|
WN8={isNaN(todayWN8s[id]) ? undefined : todayWN8s[id]}
|
||||||
careerWN8={isNaN(careerWN8s[tankId]) ? undefined : careerWN8s[tankId]}
|
careerWN8={isNaN(careerWN8s[id]) ? undefined : careerWN8s[id]}
|
||||||
damage={tankStats.damage_dealt / tankStats.battles}
|
damage={tankStats.damage_dealt / tankStats.battles}
|
||||||
careerDamage={career.damage_dealt / career.battles}
|
careerDamage={career.damage_dealt / career.battles}
|
||||||
battles={tankStats.battles}
|
battles={tankStats.battles}
|
||||||
careerBattles={career.battles}
|
careerBattles={career.battles}
|
||||||
icon={
|
|
||||||
tankId === 0 ? undefined : (await tankopedia)[tankId]?.images.normal
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -147,7 +152,7 @@ export default async function today(
|
|||||||
|
|
||||||
return naked ? (
|
return naked ? (
|
||||||
<Wrapper naked>
|
<Wrapper naked>
|
||||||
<Breakdown.Root>{rows.slice(1)}</Breakdown.Root>
|
<Breakdown.Root>{rows}</Breakdown.Root>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
) : (
|
) : (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
@@ -161,15 +166,11 @@ export default async function today(
|
|||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
description={`Today's breakdown • ${new Date().toDateString()} • ${
|
description={`Today's breakdown • ${new Date().toDateString()}`}
|
||||||
REGION_NAMES[server]
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
{rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
{rows.length > 0 && <Breakdown.Root>{rows}</Breakdown.Root>}
|
{rows.length > 0 && <Breakdown.Root>{rows}</Breakdown.Root>}
|
||||||
|
|
||||||
<PoweredBy type={PoweredByType.BlitzStars} />
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import { RouteRegistry } from '../server';
|
|||||||
export const statsRoute: RouteRegistry = {
|
export const statsRoute: RouteRegistry = {
|
||||||
route: '/stats/:type/:period',
|
route: '/stats/:type/:period',
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
|
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
const type = req.params.type as StatType;
|
const type = req.params.type as StatType;
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { RouteRegistry } from '../server';
|
|||||||
export const todayRoute: RouteRegistry = {
|
export const todayRoute: RouteRegistry = {
|
||||||
route: '/today',
|
route: '/today',
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
|
||||||
|
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
const player = resolvePlayerFromRequest(req);
|
const player = resolvePlayerFromRequest(req);
|
||||||
|
|
||||||
return await today(
|
return await today(
|
||||||
player,
|
player,
|
||||||
req.query.limit ? parseInt(req.query.limit as string) : undefined,
|
req.query.cutoff ? parseInt(req.query.cutoff as string) : undefined,
|
||||||
|
req.query.maximized ? parseInt(req.query.maximized as string) : undefined,
|
||||||
|
req.query['show-total'] ? req.query['show-total'] === 'true' : undefined,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-4
@@ -1,7 +1,6 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import normalizeRouteReturnable from './core/express/normalizeRouteReturnable';
|
import normalizeRouteReturnable from './core/express/normalizeRouteReturnable';
|
||||||
import withAutoRefresh from './core/express/withAutoRefresh';
|
import withAutoRefresh from './core/express/withAutoRefresh';
|
||||||
import isDev from './core/node/isDev';
|
|
||||||
import { Registry } from './events/interactionCreate';
|
import { Registry } from './events/interactionCreate';
|
||||||
import { statsRoute } from './routes/stats';
|
import { statsRoute } from './routes/stats';
|
||||||
import { todayRoute } from './routes/today';
|
import { todayRoute } from './routes/today';
|
||||||
@@ -22,8 +21,6 @@ export interface RouteRegistry<HandlesInteraction extends boolean = false>
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
([statsRoute, todayRoute] as RouteRegistry[]).forEach((registry) => {
|
([statsRoute, todayRoute] as RouteRegistry[]).forEach((registry) => {
|
||||||
if (!(isDev() ? registry.inDevelopment : registry.inProduction)) return;
|
|
||||||
|
|
||||||
app.get(registry.route, async (req, res) => {
|
app.get(registry.route, async (req, res) => {
|
||||||
console.log(req.originalUrl);
|
console.log(req.originalUrl);
|
||||||
|
|
||||||
@@ -31,7 +28,7 @@ const app = express();
|
|||||||
if (registry.handlesInteraction) return;
|
if (registry.handlesInteraction) return;
|
||||||
|
|
||||||
res.send(
|
res.send(
|
||||||
withAutoRefresh(await normalizeRouteReturnable(returnable), 60 * 1000),
|
withAutoRefresh(await normalizeRouteReturnable(returnable), 30 * 1000),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-2
@@ -9,7 +9,17 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"incremental": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*", ".next/types/**/*.ts", "dist/website/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user