mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a492fde6 | ||
|
|
76a69f08b4 | ||
|
|
893110d3ef | ||
|
|
70575fa9cf | ||
|
|
bb2450eb3c | ||
|
|
ab56689326 | ||
|
|
a9cf07480a | ||
|
|
c81de2d758 | ||
|
|
aa1c7639f2 | ||
|
|
d448101b32 | ||
|
|
e806f0ede2 | ||
|
|
8253c27333 | ||
|
|
254392370f | ||
|
|
d2319688a5 | ||
|
|
ffebe6c696 | ||
|
|
9e4094f3a9 | ||
|
|
f7eadb08a9 | ||
|
|
d511cfa4cc | ||
|
|
270a3051b7 | ||
|
|
81bc9f9ba6 | ||
|
|
338cec4f18 | ||
|
|
b455e3380f | ||
|
|
92761f2c55 | ||
|
|
7823b8ad2e | ||
|
|
cac82f3124 | ||
|
|
4b892d06e4 | ||
|
|
92149b0f00 | ||
|
|
399aa4c03c | ||
|
|
d5de715032 | ||
|
|
e6fb179184 | ||
|
|
af54f12ec3 | ||
|
|
909cabd517 | ||
|
|
a734a71d2f | ||
|
|
91dc45c880 | ||
|
|
91f364b76a | ||
|
|
23dd088466 | ||
|
|
2cabcc57f4 | ||
|
|
dd6092a5e7 | ||
|
|
3c2861f956 | ||
|
|
894b841769 | ||
|
|
62627c96a3 | ||
|
|
c173ece882 | ||
|
|
eca7cbbbeb | ||
|
|
c72bfa9f71 | ||
|
|
b4ae39a509 | ||
|
|
631c051895 | ||
|
|
9a2e11a2a4 | ||
|
|
aa6011b18d | ||
|
|
9419aec116 | ||
|
|
638402589f | ||
|
|
6d6a6bf738 |
No files matched your search
@@ -1,8 +1,8 @@
|
||||
name: CD North America Ratings Latest
|
||||
name: CD Asia Ratings Latest
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: CD North America Ratings Leaderboards
|
||||
name: CD Asia Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 17 * * *'
|
||||
- cron: "0 17 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: CD North America Ratings Latest
|
||||
name: CD Europe Ratings Latest
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: CD North America Ratings Leaderboards
|
||||
name: CD Europe Ratings Leaderboards
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 23 * * *'
|
||||
- cron: "0 23 * * *"
|
||||
|
||||
jobs:
|
||||
host:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install --location=global yarn
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Use node_modules cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -29,4 +29,4 @@ jobs:
|
||||
unzip dist.zip
|
||||
|
||||
- name: Run bot
|
||||
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
|
||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.js
|
||||
Vendored
+2
@@ -28,6 +28,7 @@
|
||||
"statsfull",
|
||||
"Stringifying",
|
||||
"strvk",
|
||||
"subnation",
|
||||
"tankaverages",
|
||||
"tankopedia",
|
||||
"tankstats",
|
||||
@@ -37,6 +38,7 @@
|
||||
"vickers",
|
||||
"vitepress",
|
||||
"wargaming",
|
||||
"webp",
|
||||
"winrate"
|
||||
],
|
||||
"deno.enable": false,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Blitzkrieg 1.11.0
|
||||
|
||||
I am going on vacation so I thought I'd publish this a bit prematurely without all the planned features.
|
||||
|
||||
## Changes
|
||||
|
||||
- Added `/ratings`
|
||||
- This command currently only has the `leagues` subcommand group
|
||||
- Displays the top players in a league
|
||||
- Also shows the changes in their positions and scores since last region midnight
|
||||
- Use `/help timezones` to know when Blitzkrieg records the leaderboard
|
||||
- Blitzkrieg now internally generates and keeps a permanent record of all leaderboards updated every `UTC+0` midnight
|
||||
- This approach will allow us to capture the absolute latest state of the leaderboard before it is wiped for the next season
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed region "Europe" in `/verify` example
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- Add scripts were upgraded to TypeScript
|
||||
- Secrets now only throw errors if they're used when not provided
|
||||
@@ -0,0 +1,54 @@
|
||||
# Blitzkrieg 1.12.0
|
||||
|
||||
_Houston, permission to return to Earth? A new update dropped._
|
||||
|
||||
## Features
|
||||
|
||||
- Added `multi-tank` filters to `/stats` and `/full-stats`
|
||||
- This lets you pull up the stats by combining filters of `nation`, `tank-type`, `tier`, and `tree-type`
|
||||
- Added `/ratings neighbours`
|
||||
- Lets you see your rank and the neighbours around you
|
||||
- Added a bunch more `/today` parameters (thank you for the suggestions, HisRoyalFatness!)
|
||||
- `cutoff`: the max number of rows to show (excluding the total row)
|
||||
- `maximized`: the max number of rows that show the full non-collapsed stats (default: `4`)
|
||||
- `show-total`: show the total row or not (default: `true`)
|
||||
- Tank search is now accent-tolerant
|
||||
|
||||
## Changes
|
||||
|
||||
- `/ratings` has been moved out of preview and is now accessible to all
|
||||
- `/today` now has a more appealing look with more efficient space management
|
||||
- `/stats`, `/full-stats`, and `/evolution` now have less convoluted commands
|
||||
- All periods now fall under `period` and `custom`
|
||||
- `period` has preset periods like `today`, `career`, `30`, `60`, `90`
|
||||
- `custom` lets you set a start and end day
|
||||
- Added dashes to command names, subcommands, options, etc. to make them more readable
|
||||
- Titlebar
|
||||
- Removed the Blitzkrieg logo
|
||||
- Removed region
|
||||
- Center aligned items
|
||||
- Footers are now center alinged when they do no have footer text on the left
|
||||
- Moved several title discriminators into the description
|
||||
- `/ratings` is now data-driven
|
||||
- Embeds now auto refresh every `30` seconds instead of `60`
|
||||
- Added numbers besides roman numerals in tier choice
|
||||
- I forgot to implement this in places other than `/owned-tanks`... oh well!
|
||||
- Increased precision of winrate in `/evolution` to `2` decimals
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed duplicate "Efficiency" cards in `/full-stats`
|
||||
- Titlebar gap is now `4px` just like Figma
|
||||
- Renamed `/statsfull` to `/full-stats` in the `/stats` footer
|
||||
- Rewards in `/ratings` are now properly aligned
|
||||
- `/ratings` now uses the correct source of information per region instead of getting it from a cached North American source
|
||||
- Search parameters are now passed onto the URL parser correctly
|
||||
- Custom date ranges now work correctly once again as the start is now the maximum days ago from today and the end is the minimum
|
||||
|
||||
## Technical Changes
|
||||
|
||||
- All generated images are now `480px` wide instead of `640px` to combat the unnecessary amounts of negative space
|
||||
- Changed all relevant typings to be sourced from `stats` instead of `full-stats`
|
||||
- Added minimum character limit to clan search
|
||||
- Username auto-completing now only takes in regex-accepted parts of the input
|
||||
- Search parameters are now longer automatically generated by inferring command interactions
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"main": "server.cjs",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
||||
"start": "node --enable-source-maps --trace-warnings server.js"
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -1,44 +1,46 @@
|
||||
{
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.10.2",
|
||||
"version": "1.12.0",
|
||||
"description": "",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:bot": "node scripts/build --build=bot",
|
||||
"build:server": "node scripts/build --build=server",
|
||||
"build:bot": "vite-node --script scripts/build --build=bot",
|
||||
"build:server": "vite-node --script scripts/build --build=server",
|
||||
"build:docs": "vitepress build docs",
|
||||
"build:ratingsleaderboard": "node scripts/generateRatingsLeaderboard",
|
||||
"build": "node scripts/build --build=all",
|
||||
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
|
||||
"build": "vite-node --script scripts/build --build=all",
|
||||
"dev:bot": "yarn build:bot --dev && yarn start:bot",
|
||||
"dev:server": "yarn build:server --dev && yarn start:server",
|
||||
"dev:docs": "vitepress dev docs",
|
||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
|
||||
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
|
||||
"host:docs": "vitepress preview docs",
|
||||
"publish:ratingsleaderboard": "node scripts/generateRatingsLeaderboard --publish",
|
||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||
"zip": "zip -r dist.zip dist",
|
||||
"lint": "npx tsc",
|
||||
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\""
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^20.0.1",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"bepaint": "^0.1.0-beta.3",
|
||||
"discord.js": "^14.9.0",
|
||||
"discord.js": "^14.12.0",
|
||||
"esbuild": "^0.18.10",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.18.2",
|
||||
"false": "^0.0.4",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-escape": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"satori": "^0.9.1",
|
||||
"vite-node": "^0.33.0",
|
||||
"vitepress-plugin-auto-sidebar": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^20.0.1",
|
||||
"@total-typescript/ts-reset": "^0.4.2",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { build } from 'esbuild';
|
||||
import { copyFile, mkdir, rm } from 'fs/promises';
|
||||
import { BuildOptions, build } from 'esbuild';
|
||||
import { copyFile, mkdirSync, rmSync } from 'fs';
|
||||
import { argv } from 'process';
|
||||
|
||||
const isProd = !argv.includes('--dev');
|
||||
@@ -10,25 +10,26 @@ 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 });
|
||||
rmSync('dist', { recursive: true, force: true });
|
||||
console.log('Dist removed');
|
||||
|
||||
console.log('Creating dist...');
|
||||
await mkdir('dist');
|
||||
mkdirSync('dist');
|
||||
console.log('Dist created');
|
||||
|
||||
console.log('Copying package.json...');
|
||||
copyFile('package.dist.json', 'dist/package.json').then(() =>
|
||||
copyFile('package.dist.json', 'dist/package.json', () =>
|
||||
console.log('package.json copied'),
|
||||
);
|
||||
}
|
||||
|
||||
const commonOptions = {
|
||||
const commonOptions: BuildOptions = {
|
||||
platform: 'node',
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
'.ttf': 'file',
|
||||
},
|
||||
format: 'cjs',
|
||||
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
@@ -46,9 +47,7 @@ if (buildBot) {
|
||||
|
||||
entryPoints: ['src/bot.ts'],
|
||||
outfile: 'dist/bot.cjs',
|
||||
}).then(() => {
|
||||
console.log('Bot built');
|
||||
});
|
||||
}).then(() => console.log('Bot built'));
|
||||
}
|
||||
|
||||
if (buildServer) {
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { rm, writeFile } from 'fs/promises';
|
||||
import { argv, env } from 'process';
|
||||
import {
|
||||
RatingsInfo,
|
||||
RatingsNeighbors,
|
||||
RatingsPlayer,
|
||||
} from '../src/commands/ratings';
|
||||
import commitMultipleFiles from './commitMultipleFiles.js';
|
||||
|
||||
/*
|
||||
@@ -8,11 +13,13 @@ import commitMultipleFiles from './commitMultipleFiles.js';
|
||||
* Central European Time (UTC+1): use 0 23 * * *.
|
||||
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
||||
*
|
||||
* :region/ratings/:season/info.json: season info like rewards, start & end time, etc.
|
||||
* :region/ratings/:season/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||
* :region/ratings/:season/leaderboards/:time.json: collection of all recorded leaderboards at local midnights
|
||||
* na/ratings/49/info.json: season info like rewards, start & end time, etc.
|
||||
* na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight
|
||||
* na/ratings/49/midnight.json: last midnight's leaderboard relative to the region
|
||||
*/
|
||||
|
||||
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
|
||||
|
||||
const publish = argv.includes('--publish');
|
||||
const latest = argv.includes('--latest');
|
||||
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
|
||||
@@ -23,22 +30,22 @@ const NEIGHBORS = 1000;
|
||||
const PLAYERS = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((response) => response.json() as Promise<RatingsInfo>)
|
||||
.then((data) => data.count);
|
||||
|
||||
const players = {};
|
||||
const leaderboard = [];
|
||||
const players: Record<number, RatingsPlayer> = {};
|
||||
const leaderboard: BlitzkriegRatingsLeaderboard = [];
|
||||
let registered = 0;
|
||||
|
||||
async function branchFromPlayer(id) {
|
||||
async function branchFromPlayer(id: number) {
|
||||
const { neighbors } = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
||||
).then((response) => response.json());
|
||||
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||
|
||||
const firstPlayer = neighbors[0];
|
||||
const lastPlayer = neighbors[neighbors.length - 1];
|
||||
|
||||
const branches = [];
|
||||
const branches: Promise<void>[] = [];
|
||||
if (!players[firstPlayer.number])
|
||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
||||
if (!players[lastPlayer.number])
|
||||
@@ -61,10 +68,10 @@ async function branchFromPlayer(id) {
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
async function branchFromLeague(id) {
|
||||
async function branchFromLeague(id: number) {
|
||||
const { result } = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
||||
).then((response) => response.json());
|
||||
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>);
|
||||
|
||||
const firstPlayer = result[0];
|
||||
const lastPlayer = result[result.length - 1];
|
||||
@@ -75,7 +82,7 @@ async function branchFromLeague(id) {
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('Branching form leagues...');
|
||||
console.log('Branching from leagues...');
|
||||
await Promise.all([
|
||||
branchFromLeague(0),
|
||||
branchFromLeague(1),
|
||||
@@ -86,8 +93,13 @@ await Promise.all([
|
||||
|
||||
console.log('Converting to an array...');
|
||||
for (let index = 0; index < PLAYERS; index++) {
|
||||
leaderboard[index] = players[index + 1];
|
||||
if (!leaderboard[index]) console.warn(`${index} had no player`);
|
||||
const listing = players[index + 1];
|
||||
if (!listing) console.warn(`Position ${index + 1} had no player`);
|
||||
|
||||
leaderboard[index] = {
|
||||
id: listing.spa_id,
|
||||
score: listing.score,
|
||||
};
|
||||
}
|
||||
|
||||
const leaderboardJSON = JSON.stringify(leaderboard);
|
||||
@@ -97,14 +109,13 @@ if (publish) {
|
||||
|
||||
const info = await fetch(
|
||||
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
||||
).then((response) => response.json());
|
||||
const time = Math.round(Date.now() / 1000);
|
||||
).then((response) => response.json() as Promise<RatingsInfo>);
|
||||
const normalizedServer = server === 'na' ? 'com' : server;
|
||||
|
||||
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
|
||||
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
|
||||
latest ? 'latest.json' : `leaderboards/${time}.json`
|
||||
}`;
|
||||
latest ? 'latest' : 'midnight'
|
||||
}.json`;
|
||||
const infoJSON = JSON.stringify(info);
|
||||
|
||||
console.log(`Publishing to season ${info.current_season}...`);
|
||||
@@ -113,7 +124,7 @@ if (publish) {
|
||||
'tresabhi',
|
||||
'blitzkrieg-db',
|
||||
'main',
|
||||
`${info.current_season}/${time}`,
|
||||
new Date().toString(),
|
||||
[
|
||||
{ path: infoPath, content: infoJSON },
|
||||
{ path: leaderboardPath, content: leaderboardJSON },
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
export default async function commitMultipleFiles(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
message,
|
||||
changes,
|
||||
octokit: Octokit,
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
message: string,
|
||||
changes: { path: string; content: string }[],
|
||||
) {
|
||||
// Get the latest commit SHA on the specified branch
|
||||
const { data: refData } = await octokit.git.getRef({
|
||||
@@ -31,12 +33,13 @@ export default async function commitMultipleFiles(
|
||||
content: change.content,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
return {
|
||||
sha: blobData.sha,
|
||||
path: change.path,
|
||||
mode: '100644',
|
||||
type: 'blob',
|
||||
};
|
||||
} as const;
|
||||
}),
|
||||
);
|
||||
|
||||
+5
-2
@@ -1,5 +1,6 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { DISCORD_TOKEN } from './core/node/arguments';
|
||||
import { secrets } from './core/node/secrets';
|
||||
import { registerErrorHandlers } from './events/error';
|
||||
import guildMemberAdd from './events/guildMemberAdd';
|
||||
import interactionCreate from './events/interactionCreate';
|
||||
@@ -12,5 +13,7 @@ export const client = new Client({
|
||||
.on('guildMemberAdd', guildMemberAdd)
|
||||
.on('interactionCreate', interactionCreate);
|
||||
|
||||
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
||||
|
||||
registerErrorHandlers();
|
||||
client.login(DISCORD_TOKEN);
|
||||
client.login(secrets.DISCORD_TOKEN);
|
||||
@@ -12,7 +12,7 @@ const executionStart = new Date().getTime();
|
||||
|
||||
export const debugCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
|
||||
@@ -12,7 +12,7 @@ import embedNegative from '../core/discord/embedNegative';
|
||||
import embedPositive from '../core/discord/embedPositive';
|
||||
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||
@@ -26,7 +26,7 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
|
||||
|
||||
export const eligibleCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: false,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -49,9 +49,9 @@ export const eligibleCommand: CommandRegistry = {
|
||||
|
||||
async handler(interaction) {
|
||||
const clan = interaction.options.getString('clan') as SkilledClan;
|
||||
const { id, server } = await resolvePlayerFromCommand(interaction);
|
||||
const { id, region: server } = await resolvePlayerFromCommand(interaction);
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const problems: TableInput = [];
|
||||
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
||||
|
||||
+75
-68
@@ -12,76 +12,83 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import evolution from '../renderers/evolution';
|
||||
import { StatType } from '../renderers/fullStats';
|
||||
import { StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const evolutionCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('evolution')
|
||||
.setDescription('Evolution of statistics'),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.server, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return [
|
||||
await evolution(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}${
|
||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
||||
}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.server, interaction);
|
||||
|
||||
return await evolution(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('evolution')
|
||||
.setDescription('Evolution of statistics'),
|
||||
false,
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: 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
-66
@@ -1,4 +1,5 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { TreeTypeString } from '../components/Tanks';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
@@ -12,73 +13,101 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import fullStats, { StatType } from '../renderers/fullStats';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import fullStats from '../renderers/fullStats';
|
||||
import { MultiTankFilters, StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const fullStatsCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('fullstats')
|
||||
.setDescription('Full in-game statistics'),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.server, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
return [
|
||||
await fullStats(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.server, interaction);
|
||||
|
||||
return await fullStats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('full-stats')
|
||||
.setDescription('Full in-game statistics'),
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: 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];
|
||||
|
||||
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: interaction.options.getInteger('tier') ?? 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,7 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
|
||||
|
||||
export const helpCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: addDocsSubcommands(
|
||||
@@ -44,13 +44,14 @@ export const helpCommand: CommandRegistry = {
|
||||
),
|
||||
),
|
||||
|
||||
handler(interaction) {
|
||||
async handler(interaction) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
if (subcommand === 'commands') {
|
||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${COMMANDS_RAW.filter(
|
||||
(registry) => registry.inPublic && registry.inProduction,
|
||||
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
|
||||
await Promise.all(COMMANDS_RAW)
|
||||
)
|
||||
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||
.map(
|
||||
(registry) =>
|
||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||
|
||||
@@ -4,12 +4,11 @@ import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import getBlitzClan from '../core/blitz/getBlitzClan';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { ClanInfo } from '../types/clanInfo';
|
||||
@@ -18,7 +17,7 @@ const DEFAULT_THRESHOLD = 7;
|
||||
|
||||
export const inactiveCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -42,14 +41,14 @@ export const inactiveCommand: CommandRegistry = {
|
||||
const time = new Date().getTime() / 1000;
|
||||
const clanData = (
|
||||
await getWargamingResponse<ClanInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
||||
)
|
||||
)[id];
|
||||
const memberIds = clanData.members_ids;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
|
||||
',',
|
||||
)}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${
|
||||
secrets.WARGAMING_APPLICATION_ID
|
||||
}&account_id=${memberIds.join(',')}`,
|
||||
);
|
||||
const inactive = memberIds
|
||||
.map((memberId) => {
|
||||
@@ -69,11 +68,8 @@ export const inactiveCommand: CommandRegistry = {
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={clanData.name}
|
||||
nameDiscriminator="(Inactivity)"
|
||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
|
||||
description={`${threshold}+ Days • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
|
||||
|
||||
+101
-58
@@ -4,20 +4,21 @@ import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import * as Tanks from '../components/Tanks';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import getTankStats from '../core/blitz/getTankStats';
|
||||
import getTreeType from '../core/blitz/getTreeType';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
TankopediaEntry,
|
||||
Tier,
|
||||
tankopedia,
|
||||
tankopediaInfo,
|
||||
} from '../core/blitz/tankopedia';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
@@ -45,27 +46,27 @@ const COMP_TANKS = [
|
||||
|
||||
export const ownedTanksCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('ownedtanks')
|
||||
.setName('owned-tanks')
|
||||
.setDescription("Shows a player's owned tanks")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('tier')
|
||||
.setDescription('The tier you want to see')
|
||||
.setChoices(
|
||||
{ name: 'Tier I', value: '1' },
|
||||
{ name: 'Tier II', value: '2' },
|
||||
{ name: 'Tier III', value: '3' },
|
||||
{ name: 'Tier IV', value: '4' },
|
||||
{ name: 'Tier V', value: '5' },
|
||||
{ name: 'Tier VI', value: '6' },
|
||||
{ name: 'Tier VII', value: '7' },
|
||||
{ name: 'Tier VIII', value: '8' },
|
||||
{ name: 'Tier IX', value: '9' },
|
||||
{ name: 'Tier X', value: '10' },
|
||||
{ name: 'Tier I (1)', value: '1' },
|
||||
{ name: 'Tier II (2)', value: '2' },
|
||||
{ name: 'Tier III (3)', value: '3' },
|
||||
{ name: 'Tier IV (4)', value: '4' },
|
||||
{ name: 'Tier V (5)', value: '5' },
|
||||
{ name: 'Tier VI (6)', value: '6' },
|
||||
{ name: 'Tier VII (7)', value: '7' },
|
||||
{ name: 'Tier VIII (8)', value: '8' },
|
||||
{ name: 'Tier IX (9)', value: '9' },
|
||||
{ name: 'Tier X (10)', value: '10' },
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
@@ -74,73 +75,101 @@ export const ownedTanksCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const tier = Number(interaction.options.getString('tier'));
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const tankStats = await getTankStats(server, id);
|
||||
const tanks = (
|
||||
const filteredTanks = (
|
||||
await Promise.all(
|
||||
tankStats.map(async (tankData) => ({
|
||||
...(await tankopedia)[tankData.tank_id],
|
||||
tankopedia: (await tankopedia)[tankData.tank_id]!,
|
||||
tank_id: tankData.tank_id,
|
||||
})),
|
||||
)
|
||||
).filter((tank) => tank?.tier === tier);
|
||||
).filter((tank) => tank.tankopedia?.tier === tier);
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||
`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 leftColumn = tanks.slice(0, leftColumnSize);
|
||||
const rightColumn = tanks.slice(leftColumnSize);
|
||||
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
||||
const nations: string[] = [];
|
||||
|
||||
filteredTanks.forEach((tank) => {
|
||||
if (groupedTanks[tank.tankopedia.nation] === undefined) {
|
||||
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
|
||||
nations.push(tank.tankopedia.nation);
|
||||
} else {
|
||||
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
|
||||
}
|
||||
});
|
||||
|
||||
nations.sort();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`}
|
||||
nameDiscriminator={
|
||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
||||
}
|
||||
image={
|
||||
clanData[id]?.clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`Owned tanks • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
description={`Tier ${
|
||||
TIER_ROMAN_NUMERALS[tier as Tier]
|
||||
} tanks • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{tanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||
|
||||
{tanks.length > 0 && (
|
||||
<Tanks.Root>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
leftColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
<Tanks.Column>
|
||||
{await Promise.all(
|
||||
rightColumn.map(async (tank) => (
|
||||
<Tanks.Item
|
||||
key={tank.tank_id}
|
||||
name={await resolveTankName(tank.tank_id)}
|
||||
tankType={tank.type}
|
||||
image={tank.images?.normal}
|
||||
treeType={await getTreeType(tank.tank_id)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Tanks.Column>
|
||||
</Tanks.Root>
|
||||
)}
|
||||
{filteredTanks.length > 0 &&
|
||||
(await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tanks = groupedTanks[nation];
|
||||
const leftColumnSize = Math.ceil(tanks.length / 2);
|
||||
const leftColumn = tanks.slice(0, leftColumnSize);
|
||||
const rightColumn = tanks.slice(leftColumnSize);
|
||||
|
||||
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>
|
||||
);
|
||||
}),
|
||||
))}
|
||||
|
||||
<PoweredBy type={PoweredByType.Wargaming} />
|
||||
</Wrapper>
|
||||
@@ -149,3 +178,17 @@ export const ownedTanksCommand: CommandRegistry = {
|
||||
|
||||
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,9 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
export const pingCommand: CommandRegistry<true> = {
|
||||
export const pingCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
handlesInteraction: true,
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountAchievements } from '../types/accountAchievements';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
@@ -16,11 +16,11 @@ type SortBy = 'name' | 'count';
|
||||
|
||||
export const playerAchievementsCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('playerachievements')
|
||||
.setName('player-achievements')
|
||||
.setDescription("All the player's achievements")
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addStringOption((option) =>
|
||||
@@ -36,13 +36,13 @@ export const playerAchievementsCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const accountsAchievements =
|
||||
await getWargamingResponse<AccountAchievements>(
|
||||
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const accountAchievements = accountsAchievements[id];
|
||||
const compound = {
|
||||
|
||||
@@ -6,25 +6,25 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import markdownTable from '../core/discord/markdownTable';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const playerInfoCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('playerinfo')
|
||||
.setName('player-info')
|
||||
.setDescription('Basic information about a player')
|
||||
.addStringOption(addUsernameChoices),
|
||||
|
||||
async handler(interaction) {
|
||||
const account = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = account;
|
||||
const { id, region: server } = account;
|
||||
const accounts = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const accountInfo = accounts[id];
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
APIApplicationCommandOptionChoice,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||
import * as Leaderboard from '../components/Leaderboard';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||
import getMidnightLeaderboard from '../core/database/getMidnightLeaderboard';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
|
||||
export interface RatingsPlayer {
|
||||
spa_id: number;
|
||||
mmr: number;
|
||||
season_number: number;
|
||||
calibrationBattlesLeft: number;
|
||||
number: number;
|
||||
percentile: number;
|
||||
skip: boolean;
|
||||
updated_at: string;
|
||||
score: number;
|
||||
nickname: string;
|
||||
clan_tag: string;
|
||||
}
|
||||
|
||||
export interface LeagueTop {
|
||||
result: RatingsPlayer[];
|
||||
}
|
||||
|
||||
export type RatingsReward = {
|
||||
from_position: number;
|
||||
to_position: number;
|
||||
count: number;
|
||||
} & (
|
||||
| {
|
||||
type: 'vehicle';
|
||||
|
||||
vehicle: {
|
||||
id: number;
|
||||
name: string;
|
||||
nation: string;
|
||||
subnation: string;
|
||||
use_subnation_flag: boolean;
|
||||
type_slug: string;
|
||||
level: number;
|
||||
roman_level: string;
|
||||
user_string: string;
|
||||
image_url: string;
|
||||
preview_url: string;
|
||||
is_premium: boolean;
|
||||
is_collectible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'stuff';
|
||||
|
||||
stuff: {
|
||||
name: string;
|
||||
count: number;
|
||||
title: string;
|
||||
image_url: string;
|
||||
type: string;
|
||||
|
||||
sizes: {};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export interface RatingsLeague {
|
||||
title: string;
|
||||
small_icon: string;
|
||||
big_icon: string;
|
||||
background: string;
|
||||
index: number;
|
||||
percentile: number;
|
||||
}
|
||||
|
||||
export interface RatingsInfo {
|
||||
title: string;
|
||||
icon: string | null;
|
||||
start_at: string;
|
||||
finish_at: string;
|
||||
current_season: number;
|
||||
updated_at: string;
|
||||
count: number;
|
||||
|
||||
rewards: RatingsReward[];
|
||||
leagues: RatingsLeague[];
|
||||
}
|
||||
|
||||
export interface RatingsNeighbors {
|
||||
neighbors: RatingsPlayer[];
|
||||
}
|
||||
|
||||
console.log('Caching ratings info...');
|
||||
const ratingsInfo = getRatingsInfo('com').then((info) => {
|
||||
console.log('Cached ratings info');
|
||||
return info;
|
||||
});
|
||||
|
||||
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||
async (resolve) => {
|
||||
const awaitedRatingsInfo = await ratingsInfo;
|
||||
|
||||
const command = new SlashCommandBuilder()
|
||||
.setName('ratings')
|
||||
.setDescription('Ratings battles statistics')
|
||||
.addSubcommand((option) =>
|
||||
option
|
||||
.setName('league')
|
||||
.setDescription('Top league players')
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('league')
|
||||
.setDescription('The league to display')
|
||||
.addChoices(
|
||||
...awaitedRatingsInfo.leagues.map(
|
||||
(league) =>
|
||||
({
|
||||
name: league.title.split(' ')[0].toLowerCase(),
|
||||
value: `${league.index}`,
|
||||
}) 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),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
option
|
||||
.setName('neighbours')
|
||||
.setDescription('Your position and neighbours')
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setDescription('How many neighbours to display (default: 10)')
|
||||
.setRequired(false)
|
||||
.setMinValue(5)
|
||||
.setMaxValue(30),
|
||||
),
|
||||
);
|
||||
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command,
|
||||
|
||||
async handler(interaction) {
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||
let titleName: string;
|
||||
let titleImage: string | undefined;
|
||||
let titleNameDiscriminator: string | undefined;
|
||||
let titleDescription: string;
|
||||
let playersBefore: number;
|
||||
let playersAfter: number;
|
||||
let players: RatingsPlayer[];
|
||||
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 getRatingsInfo(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>)
|
||||
.then(({ result }) => result.slice(0, limit));
|
||||
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||
midnightLeaderboard = await getMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
);
|
||||
|
||||
players = result;
|
||||
playersBefore = result[0].number - 1;
|
||||
playersAfter =
|
||||
regionRatingsInfo.count - result[result.length - 1].number;
|
||||
titleName = `${leagueInfo.title} - ${REGION_NAMES_SHORT[region]}`;
|
||||
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||
? leagueInfo.big_icon
|
||||
: `https:${leagueInfo.big_icon}`;
|
||||
titleDescription = `Top ${limit} players`;
|
||||
} else {
|
||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||
regionRatingsInfo = await getRatingsInfo(region);
|
||||
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 } = await fetch(
|
||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||
limit / 2,
|
||||
)}`,
|
||||
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||
midnightLeaderboard = await getMidnightLeaderboard(
|
||||
region,
|
||||
regionRatingsInfo.current_season,
|
||||
);
|
||||
|
||||
if (clan) titleNameDiscriminator = `[${clan.tag}]`;
|
||||
|
||||
players = neighbors;
|
||||
titleImage = clan
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||
: undefined;
|
||||
titleName = accountInfo[id].nickname;
|
||||
titleDescription = 'Ratings neighbours';
|
||||
playersBefore = neighbors[0].number - 1;
|
||||
playersAfter =
|
||||
regionRatingsInfo.count - neighbors[neighbors.length - 1].number;
|
||||
playerId = id;
|
||||
}
|
||||
|
||||
const items = players.map((player) => {
|
||||
const reward = regionRatingsInfo.rewards.find(
|
||||
(reward) =>
|
||||
player.number >= reward.from_position &&
|
||||
player.number <= reward.to_position,
|
||||
);
|
||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||
(item) => item.id === player.spa_id,
|
||||
);
|
||||
const midnightScore =
|
||||
midnightIndex === undefined
|
||||
? player.score
|
||||
: midnightLeaderboard![midnightIndex].score;
|
||||
const midnightPosition =
|
||||
midnightIndex === undefined ? player.number : midnightIndex + 1;
|
||||
|
||||
return (
|
||||
<Leaderboard.Item
|
||||
nickname={player.nickname}
|
||||
score={player.score}
|
||||
position={player.number}
|
||||
clan={player.clan_tag}
|
||||
reward={reward}
|
||||
deltaScore={player.score - midnightScore}
|
||||
deltaPosition={midnightPosition - player.number}
|
||||
key={player.spa_id}
|
||||
highlight={player.spa_id === playerId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={titleName}
|
||||
image={titleImage}
|
||||
nameDiscriminator={titleNameDiscriminator}
|
||||
description={`${titleDescription} • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
<Leaderboard.Root>
|
||||
{playersBefore > 0 && <Leaderboard.Gap number={playersBefore} />}
|
||||
{items}
|
||||
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
||||
</Leaderboard.Root>
|
||||
</Wrapper>
|
||||
);
|
||||
},
|
||||
|
||||
autocomplete: autocompleteUsername,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,23 +1,23 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { REGION_DOMAIN_NAMES, RegionDomain } from '../constants/regions';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import addClanChoices from '../core/discord/addClanChoices';
|
||||
import addServerChoices from '../core/discord/addServerChoices';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { ClanList } from '../types/clanList';
|
||||
|
||||
export const searchClansCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchclans')
|
||||
.setName('search-clans')
|
||||
.setDescription('Search clans')
|
||||
.addStringOption(addServerChoices)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addStringOption((option) => addClanChoices(option).setAutocomplete(false))
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
@@ -28,17 +28,15 @@ export const searchClansCommand: CommandRegistry = {
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const server = interaction.options.getString('server') as RegionDomain;
|
||||
const server = interaction.options.getString('region') as Region;
|
||||
const clan = interaction.options.getString('clan')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const clanList = await getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||
);
|
||||
|
||||
return embedInfo(
|
||||
`Clan search for "${markdownEscape(clan)}" in ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`,
|
||||
`Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
|
||||
clanList.length === 0
|
||||
? 'No clans found.'
|
||||
: `\`\`\`\n${clanList
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { REGION_DOMAIN_NAMES, RegionDomain } from '../constants/regions';
|
||||
import { REGION_NAMES, Region } from '../constants/regions';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import { usernamePattern } from '../core/blitz/listPlayers';
|
||||
import addServerChoices from '../core/discord/addServerChoices';
|
||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountList } from '../types/accountList';
|
||||
|
||||
export const searchPlayersCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchplayers')
|
||||
.setName('search-players')
|
||||
.setDescription('Search players in a Blitz server')
|
||||
.addStringOption(addServerChoices)
|
||||
.addStringOption(addRegionChoices)
|
||||
.addStringOption((option) =>
|
||||
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
|
||||
)
|
||||
@@ -31,22 +31,20 @@ export const searchPlayersCommand: CommandRegistry = {
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const server = interaction.options.getString('server') as RegionDomain;
|
||||
const server = interaction.options.getString('region') as Region;
|
||||
const name = interaction.options.getString('username')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const trimmedSearch = name.trim();
|
||||
const players = usernamePattern.test(trimmedSearch)
|
||||
? await getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
|
||||
trimmedSearch,
|
||||
)}&limit=${limit}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${
|
||||
secrets.WARGAMING_APPLICATION_ID
|
||||
}&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`,
|
||||
)
|
||||
: [];
|
||||
|
||||
return embedInfo(
|
||||
`Player search for "${markdownEscape(name)}" in ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`,
|
||||
`Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
|
||||
`\`\`\`${
|
||||
players.length === 0
|
||||
? 'No players found.'
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { TANK_NAMES } from '../core/blitz/tankopedia';
|
||||
import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
|
||||
import addTankChoices from '../core/discord/addTankChoices';
|
||||
import embedInfo from '../core/discord/embedInfo';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
|
||||
export const searchTanksCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
.setName('searchtanks')
|
||||
.setName('search-tanks')
|
||||
.setDescription('Search tanks')
|
||||
.addStringOption((option) => addTankChoices(option).setAutocomplete(false))
|
||||
.addIntegerOption((option) =>
|
||||
@@ -26,9 +26,10 @@ export const searchTanksCommand: CommandRegistry = {
|
||||
async handler(interaction) {
|
||||
const tank = interaction.options.getString('tank')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const results = go(tank, await TANK_NAMES, { limit }).map(
|
||||
(result) => result.target,
|
||||
);
|
||||
const results = go(tank, await TANK_NAMES_DIACRITICS, {
|
||||
limit,
|
||||
keys: ['combined'],
|
||||
}).map((result) => result.obj.original);
|
||||
|
||||
return embedInfo(
|
||||
`Tank search for "${markdownEscape(tank)}"`,
|
||||
|
||||
+96
-62
@@ -1,4 +1,5 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { TreeTypeString } from '../components/Tanks';
|
||||
import { CYCLIC_API } from '../constants/cyclic';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankId from '../core/blitz/resolveTankId';
|
||||
@@ -12,74 +13,107 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { StatType } from '../renderers/fullStats';
|
||||
import stats from '../renderers/stats';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const statsCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inPublic: true,
|
||||
|
||||
command: addStatTypeSubCommandGroups(
|
||||
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
||||
const command = await addStatTypeSubCommandGroups(
|
||||
new SlashCommandBuilder()
|
||||
.setName('stats')
|
||||
.setDescription('In-game statistics'),
|
||||
),
|
||||
.setDescription('Regular battles statistics'),
|
||||
);
|
||||
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const period = resolvePeriodFromCommand(player.server, interaction);
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
resolve({
|
||||
inProduction: true,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
return [
|
||||
await stats(commandGroup, period, player, tankId),
|
||||
primaryButton(path, 'Refresh'),
|
||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
},
|
||||
command,
|
||||
|
||||
autocomplete: (interaction) => {
|
||||
autocompleteUsername(interaction);
|
||||
autocompleteTanks(interaction);
|
||||
},
|
||||
async handler(interaction) {
|
||||
const commandGroup = interaction.options.getSubcommandGroup(
|
||||
true,
|
||||
) as StatType;
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const resolvedPeriod = resolvePeriodFromCommand(
|
||||
player.region,
|
||||
interaction,
|
||||
);
|
||||
const period = interaction.options.getString('period');
|
||||
const tankIdRaw = interaction.options.getString('tank')!;
|
||||
const tankId =
|
||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||
const start = interaction.options.getInteger('start');
|
||||
const end = interaction.options.getInteger('end');
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
tankId,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
});
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
|
||||
async button(interaction) {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
const commandGroup = path[1] as StatType;
|
||||
const player = await resolvePlayerFromButton(interaction);
|
||||
const period = resolvePeriodFromButton(player.server, interaction);
|
||||
return [
|
||||
await stats(
|
||||
commandGroup,
|
||||
resolvedPeriod,
|
||||
player,
|
||||
commandGroup === 'tank'
|
||||
? tankId
|
||||
: commandGroup === 'multi-tank'
|
||||
? ({
|
||||
'tank-type':
|
||||
interaction.options.getString('tank-type') ?? undefined,
|
||||
nation: interaction.options.getString('nation') ?? undefined,
|
||||
tier: interaction.options.getInteger('tier') ?? 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',
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
return await stats(
|
||||
commandGroup,
|
||||
period,
|
||||
player,
|
||||
parseInt(url.searchParams.get('tankId')!),
|
||||
);
|
||||
},
|
||||
};
|
||||
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,
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
+30
-8
@@ -8,14 +8,14 @@ import linkButton from '../core/discord/linkButton';
|
||||
import primaryButton from '../core/discord/primaryButton';
|
||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import today from '../renderers/today';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
|
||||
export const todayCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -24,30 +24,52 @@ export const todayCommand: CommandRegistry = {
|
||||
.addStringOption(addUsernameChoices)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('limit')
|
||||
.setName('cutoff')
|
||||
.setDescription(
|
||||
'The maximum number of tanks to display (default: Infinity)',
|
||||
)
|
||||
.setRequired(false)
|
||||
.setMinValue(1),
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('maximized')
|
||||
.setDescription(
|
||||
'The number of rows with full stats after which all are collapsed (default: 4)',
|
||||
)
|
||||
.setMinValue(0)
|
||||
.setRequired(false),
|
||||
)
|
||||
.addBooleanOption((option) =>
|
||||
option
|
||||
.setName('show-total')
|
||||
.setDescription('Show the total stats at the top (default: true)')
|
||||
.setRequired(false),
|
||||
),
|
||||
|
||||
async handler(interaction) {
|
||||
const player = await resolvePlayerFromCommand(interaction);
|
||||
const limit = interaction.options.getInteger('limit') ?? Infinity;
|
||||
const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
|
||||
const maximized = interaction.options.getInteger('maximized') ?? undefined;
|
||||
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
|
||||
const { nickname } = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
||||
)
|
||||
)[player.id];
|
||||
const path = interactionToURL(interaction, player);
|
||||
const path = interactionToURL(interaction, {
|
||||
...player,
|
||||
cutoff,
|
||||
maximized,
|
||||
'show-total': showTotal,
|
||||
});
|
||||
|
||||
return [
|
||||
await today(player, limit, false),
|
||||
await today(player, cutoff, maximized, showTotal, false),
|
||||
primaryButton(path, 'Refresh'),
|
||||
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
||||
linkButton(
|
||||
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
|
||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
||||
'BlitzStars',
|
||||
),
|
||||
];
|
||||
|
||||
@@ -7,14 +7,14 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||
import embedNegative from '../core/discord/embedNegative';
|
||||
import embedPositive from '../core/discord/embedPositive';
|
||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { CommandRegistry } from '../events/interactionCreate';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
|
||||
export const verifyCommand: CommandRegistry = {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -24,12 +24,12 @@ export const verifyCommand: CommandRegistry = {
|
||||
|
||||
async handler(interaction) {
|
||||
const blitzAccount = await resolvePlayerFromCommand(interaction);
|
||||
const { id, server } = blitzAccount;
|
||||
const { id, region: server } = blitzAccount;
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${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 clan = clanData[id]?.clan;
|
||||
const clanTag = clan ? ` [${clan!.tag}]` : '';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
||||
import { TankType } from '../../../core/blitz/tankopedia';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { RowDiscriminator } from './RowDiscriminator';
|
||||
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
||||
import { RowStat } from './RowStat';
|
||||
|
||||
export interface RowProps {
|
||||
@@ -13,14 +14,14 @@ export interface RowProps {
|
||||
careerDamage: number;
|
||||
battles: number;
|
||||
careerBattles: number;
|
||||
icon?: string;
|
||||
minimized: boolean;
|
||||
isListing: boolean;
|
||||
naked?: boolean;
|
||||
isTank: boolean;
|
||||
treeType?: TreeTypeEnum;
|
||||
tankType?: TankType;
|
||||
}
|
||||
|
||||
export function Row({
|
||||
isListing,
|
||||
isTank,
|
||||
minimized,
|
||||
name,
|
||||
winrate,
|
||||
@@ -31,45 +32,85 @@ export function Row({
|
||||
careerDamage,
|
||||
battles,
|
||||
careerBattles,
|
||||
icon,
|
||||
naked,
|
||||
treeType,
|
||||
tankType,
|
||||
}: RowProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4,
|
||||
backgroundColor: isListing
|
||||
? theme.colors.componentInteractive
|
||||
: theme.colors.appBackground2,
|
||||
padding: 8,
|
||||
backgroundColor: theme.colors.appBackground2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<RowDiscriminator minimized={minimized} name={name} icon={icon} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
gap: 4,
|
||||
backgroundColor: theme.colors.componentInteractive,
|
||||
}}
|
||||
>
|
||||
{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
|
||||
minimized={minimized}
|
||||
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
|
||||
value={`${(winrate * 100).toFixed(2)}%`}
|
||||
delta={winrate - careerWinrate}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`WN8 • ${careerWN8 === undefined ? '--' : careerWN8.toFixed(0)}`}
|
||||
value={WN8 === undefined ? '--' : WN8.toFixed(0)}
|
||||
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Damage • ${careerDamage.toFixed(0)}`}
|
||||
value={damage.toFixed(0)}
|
||||
delta={damage - careerDamage}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Battles • ${careerBattles.toFixed(0)}`}
|
||||
value={battles.toFixed(0)}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Battles • ${careerBattles.toFixed(0)}`}
|
||||
value={battles.toFixed(0)}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
|
||||
value={`${(winrate * 100).toFixed(2)}%`}
|
||||
delta={winrate - careerWinrate}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`WN8 • ${
|
||||
careerWN8 === undefined ? '--' : careerWN8.toFixed(0)
|
||||
}`}
|
||||
value={WN8 === undefined ? '--' : WN8.toFixed(0)}
|
||||
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)}
|
||||
/>
|
||||
<RowStat
|
||||
minimized={minimized}
|
||||
name={`Damage • ${careerDamage.toFixed(0)}`}
|
||||
value={damage.toFixed(0)}
|
||||
delta={damage - careerDamage}
|
||||
/>
|
||||
</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
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './components/Root';
|
||||
export * from './components/Row';
|
||||
export * from './components/RowDiscriminator';
|
||||
export * from './components/RowStat';
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function GenericAllStats({
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
title="Efficiency"
|
||||
title="Miscellaneous"
|
||||
items={[
|
||||
{
|
||||
label: 'Average tier',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import { Margin, MarginOrientation } from './Margin';
|
||||
|
||||
export const GRAPH_WIDTH = 544;
|
||||
export const GRAPH_WIDTH = 384;
|
||||
export const GRAPH_HEIGHT = 320;
|
||||
|
||||
export interface MarginInputProps {
|
||||
@@ -20,7 +20,7 @@ export interface RootProps {
|
||||
separations?: number;
|
||||
xMinLabel: string;
|
||||
xMaxLabel: string;
|
||||
xTitle: string;
|
||||
xTitle?: string;
|
||||
}
|
||||
|
||||
export function Root({
|
||||
@@ -91,18 +91,20 @@ export function Root({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{xTitle}
|
||||
</span>
|
||||
</div>
|
||||
{xTitle !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
|
||||
{xTitle}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface GapProps {
|
||||
number: number;
|
||||
}
|
||||
|
||||
export function Gap({ number }: GapProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: theme.colors.componentNonInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
+ {number.toLocaleString()} more
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { RatingsReward } from '../../../commands/ratings';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export interface ItemProps {
|
||||
position: number;
|
||||
deltaPosition?: number;
|
||||
score: number;
|
||||
deltaScore?: number;
|
||||
reward?: RatingsReward;
|
||||
nickname: string;
|
||||
clan?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
function normalizeImage(url: string) {
|
||||
return url.endsWith('.webp') ? 'https://i.imgur.com/ZniCzbO.png' : url;
|
||||
}
|
||||
|
||||
export function Item({
|
||||
position,
|
||||
deltaPosition,
|
||||
score,
|
||||
deltaScore,
|
||||
reward,
|
||||
nickname,
|
||||
clan,
|
||||
highlight,
|
||||
}: ItemProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: 8,
|
||||
backgroundColor: highlight
|
||||
? theme.colors.componentInteractive_blue
|
||||
: theme.colors.componentInteractive,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{position.toLocaleString()}.
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{nickname}
|
||||
</span>
|
||||
{clan && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
[{clan}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{deltaPosition !== undefined && deltaPosition !== 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaPosition > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaPosition).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{reward && (
|
||||
<img
|
||||
style={{ width: 16, height: 16, objectFit: 'cover' }}
|
||||
src={normalizeImage(
|
||||
reward.type === 'vehicle'
|
||||
? reward.vehicle.image_url
|
||||
: reward.stuff.image_url,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{reward && reward.count > 1 && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
x {reward.count.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
width: 96,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textLowContrast_blue
|
||||
: theme.colors.textLowContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{Math.abs(deltaScore).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||
<img
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
}}
|
||||
src={
|
||||
deltaScore > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
color: highlight
|
||||
? theme.colors.textHighContrast_blue
|
||||
: theme.colors.textHighContrast,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{score.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface RootProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './components/Gap';
|
||||
export * from './components/Item';
|
||||
export * from './components/Root';
|
||||
@@ -24,8 +24,13 @@ const POWERED_BY_NAMES: Record<PoweredByType, string> = {
|
||||
|
||||
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',
|
||||
justifyContent: footer === undefined ? 'space-around' : 'space-between',
|
||||
}}
|
||||
>
|
||||
{footer !== undefined && <div style={{ display: 'flex' }}>{footer}</div>}
|
||||
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} />
|
||||
|
||||
@@ -2,32 +2,35 @@ import {
|
||||
TANK_ICONS,
|
||||
TANK_ICONS_COLLECTOR,
|
||||
TANK_ICONS_PREMIUM,
|
||||
TankType,
|
||||
} from '../../../core/blitz/tankopedia';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export enum TreeType {
|
||||
export enum TreeTypeEnum {
|
||||
TechTree,
|
||||
Premium,
|
||||
Collector,
|
||||
}
|
||||
|
||||
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
|
||||
|
||||
export interface ItemProps {
|
||||
image?: string;
|
||||
tankType?: string;
|
||||
tankType?: TankType;
|
||||
name: string;
|
||||
treeType: TreeType;
|
||||
treeType: TreeTypeEnum;
|
||||
}
|
||||
|
||||
const TREE_TYPE_COLOR = {
|
||||
[TreeType.TechTree]: '',
|
||||
[TreeType.Premium]: '_amber',
|
||||
[TreeType.Collector]: '_blue',
|
||||
[TreeTypeEnum.TechTree]: '',
|
||||
[TreeTypeEnum.Premium]: '_amber',
|
||||
[TreeTypeEnum.Collector]: '_blue',
|
||||
} as const;
|
||||
|
||||
const TREE_TYPE_ICONS = {
|
||||
[TreeType.TechTree]: TANK_ICONS,
|
||||
[TreeType.Premium]: TANK_ICONS_PREMIUM,
|
||||
[TreeType.Collector]: TANK_ICONS_COLLECTOR,
|
||||
export const TREE_TYPE_ICONS = {
|
||||
[TreeTypeEnum.TechTree]: TANK_ICONS,
|
||||
[TreeTypeEnum.Premium]: TANK_ICONS_PREMIUM,
|
||||
[TreeTypeEnum.Collector]: TANK_ICONS_COLLECTOR,
|
||||
};
|
||||
|
||||
export function Item({ image, tankType, name, treeType }: ItemProps) {
|
||||
|
||||
@@ -9,7 +9,8 @@ export function Root({ children }: RootProps) {
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{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/Item';
|
||||
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';
|
||||
|
||||
export interface TitleBarProps {
|
||||
@@ -19,10 +18,11 @@ export default function TitleBar({
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8, flex: 1 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{image && (
|
||||
<img
|
||||
style={{ width: 64, height: 64, objectFit: 'contain' }}
|
||||
@@ -31,13 +31,18 @@ export default function TitleBar({
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@@ -46,6 +51,8 @@ export default function TitleBar({
|
||||
color: theme.colors.textHighContrast,
|
||||
fontWeight: 900,
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
@@ -53,7 +60,6 @@ export default function TitleBar({
|
||||
{nameDiscriminator && (
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 32,
|
||||
color: theme.colors.textLowContrast,
|
||||
fontWeight: 900,
|
||||
@@ -70,15 +76,6 @@ export default function TitleBar({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
style={{ width: 64, height: 64 }}
|
||||
src={
|
||||
isDev()
|
||||
? 'https://i.imgur.com/j6TItdy.png'
|
||||
: 'https://i.imgur.com/pP6RN0x.png'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export default function Wrapper({ children, naked }: WrapperProps) {
|
||||
flexDirection: 'column',
|
||||
padding: 16,
|
||||
gap: 32,
|
||||
width: 640,
|
||||
width: 480,
|
||||
color: theme.colors.textHighContrast,
|
||||
|
||||
...(!naked && {
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
export type RegionDomain = 'com' | 'eu' | 'asia';
|
||||
export type Region = 'com' | 'eu' | 'asia';
|
||||
export type RegionSubdomain = 'na' | 'eu' | 'asia';
|
||||
|
||||
export const REGION_DOMAIN_NAMES: Record<RegionDomain, string> = {
|
||||
export const REGION_NAMES: Record<Region, string> = {
|
||||
com: 'North America',
|
||||
eu: 'Europe',
|
||||
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> = {
|
||||
na: 'North America',
|
||||
eu: 'Europe',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand';
|
||||
import throwError from '../node/throwError';
|
||||
import listClans from './listClans';
|
||||
@@ -6,7 +6,7 @@ import listClans from './listClans';
|
||||
export default async function getBlitzClan(clan: string) {
|
||||
if (serverAndIdPattern.test(clan)) {
|
||||
const [server, accountId] = clan.split('/');
|
||||
return { server: server as RegionDomain, id: Number(accountId) };
|
||||
return { server: server as Region, id: Number(accountId) };
|
||||
} else {
|
||||
const accounts = await listClans(clan);
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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,18 +1,18 @@
|
||||
import { REGION_DOMAIN_NAMES, RegionDomain } from '../../constants/regions';
|
||||
import { REGION_NAMES, Region } from '../../constants/regions';
|
||||
import { TanksStats } from '../../types/tanksStats';
|
||||
import { WARGAMING_APPLICATION_ID } from '../node/arguments';
|
||||
import { secrets } from '../node/secrets';
|
||||
import throwError from '../node/throwError';
|
||||
import getWargamingResponse from './getWargamingResponse';
|
||||
|
||||
export default async function getTankStats(server: RegionDomain, id: number) {
|
||||
export default async function getTankStats(server: Region, id: number) {
|
||||
const tankStats = await getWargamingResponse<TanksStats>(
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
if (tankStats[id] === null) {
|
||||
throw throwError(
|
||||
'No tank stats available',
|
||||
`Wargaming says there is no tank stats for this account. This account may not have any battles/tanks or exist in the ${REGION_DOMAIN_NAMES[server]} server.`,
|
||||
`Wargaming says there is no tank stats for this account. This account may not have any battles/tanks or exist in the ${REGION_NAMES[server]} server.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { TreeType } from '../../components/Tanks';
|
||||
import { TreeTypeEnum } from '../../components/Tanks';
|
||||
import { tankopedia } from './tankopedia';
|
||||
|
||||
export default async function getTreeType(id: number) {
|
||||
const entry = (await tankopedia)[id];
|
||||
|
||||
if (entry?.is_collectible) return TreeType.Collector;
|
||||
if (entry?.is_premium) return TreeType.Premium;
|
||||
if (entry?.is_collectible) return TreeTypeEnum.Collector;
|
||||
if (entry?.is_premium) return TreeTypeEnum.Premium;
|
||||
|
||||
return TreeType.TechTree;
|
||||
return TreeTypeEnum.TechTree;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { Clan, ClanList } from '../../types/clanList';
|
||||
import { WARGAMING_APPLICATION_ID } from '../node/arguments';
|
||||
import { secrets } from '../node/secrets';
|
||||
import getWargamingResponse from './getWargamingResponse';
|
||||
|
||||
export type ClanListWithServer = (Clan & {
|
||||
@@ -15,33 +15,33 @@ export default async function listClans(search: string, limit = 9) {
|
||||
return (
|
||||
await Promise.all([
|
||||
getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.com/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.com/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'com' as RegionDomain,
|
||||
server: 'com' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.eu/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.eu/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'eu' as RegionDomain,
|
||||
server: 'eu' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<ClanList>(
|
||||
`https://api.wotblitz.asia/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.asia/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'asia' as RegionDomain,
|
||||
server: 'asia' as Region,
|
||||
})),
|
||||
),
|
||||
])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { Account, AccountList } from '../../types/accountList';
|
||||
import { WARGAMING_APPLICATION_ID } from '../node/arguments';
|
||||
import { secrets } from '../node/secrets';
|
||||
import getWargamingResponse from './getWargamingResponse';
|
||||
|
||||
export type AccountListWithServer = (Account & {
|
||||
@@ -19,33 +19,33 @@ export default async function listPlayers(search: string, limit = 9) {
|
||||
return (
|
||||
await Promise.all([
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.com/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.com/wotb/account/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'com' as RegionDomain,
|
||||
server: 'com' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.eu/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.eu/wotb/account/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'eu' as RegionDomain,
|
||||
server: 'eu' as Region,
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.asia/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.asia/wotb/account/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
value.map((account) => ({
|
||||
...account,
|
||||
server: 'asia' as RegionDomain,
|
||||
server: 'asia' as Region,
|
||||
})),
|
||||
),
|
||||
])
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Region, RegionSubdomain } from '../../constants/regions';
|
||||
|
||||
export default function regionToRegionSubdomain(
|
||||
regionDomain: Region,
|
||||
): RegionSubdomain {
|
||||
return regionDomain === 'com' ? 'na' : regionDomain;
|
||||
}
|
||||
@@ -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 {
|
||||
name: string;
|
||||
nation: string;
|
||||
@@ -7,7 +13,7 @@ export interface TankopediaEntry {
|
||||
cost: { price_credit: number; price_gold: number };
|
||||
images: { preview: string; normal: string };
|
||||
tank_id: number;
|
||||
type: string;
|
||||
type: TankType;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -31,22 +37,52 @@ export const TANKS = new Promise<TankopediaEntry[]>(async (resolve) => {
|
||||
export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
|
||||
resolve((await entries).map(({ name }) => name));
|
||||
});
|
||||
export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) =>
|
||||
Promise.all(
|
||||
tankNames.map(async (tankName, index) => {
|
||||
const name = tankName ?? `Unknown Tank ${(await TANKS)[index].tank_id}`;
|
||||
const diacriticless = deburr(name);
|
||||
|
||||
export const TANK_ICONS: Record<string, string> = {
|
||||
return {
|
||||
original: name,
|
||||
diacriticless,
|
||||
combined: `${name}${diacriticless}`,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
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',
|
||||
lightTank: 'https://i.imgur.com/CSNha5V.png',
|
||||
mediumTank: 'https://i.imgur.com/wvf3ltm.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',
|
||||
lightTank: 'https://i.imgur.com/zdkpTRb.png',
|
||||
mediumTank: 'https://i.imgur.com/3z7eHX6.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',
|
||||
lightTank: 'https://i.imgur.com/EwhtKkU.png',
|
||||
mediumTank: 'https://i.imgur.com/u8YDMBh.png',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { AllStats } from '../../types/accountInfo';
|
||||
import getTankHistories, {
|
||||
TankHistories,
|
||||
@@ -35,7 +35,7 @@ export const emptyTankHistoryNode: TankHistoryRaw = {
|
||||
};
|
||||
|
||||
export default async function getDiffedTankStats(
|
||||
server: RegionDomain,
|
||||
server: Region,
|
||||
id: number,
|
||||
start: number,
|
||||
end: number,
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Period } from '../discord/addPeriodSubCommands';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { PeriodSize } from '../discord/addPeriodSubCommands';
|
||||
import getTimeDaysAgo from './getTimeDaysAgo';
|
||||
|
||||
export default function getPeriodStart(
|
||||
server: RegionDomain,
|
||||
period: Exclude<Period, 'custom'>,
|
||||
) {
|
||||
if (period === 'career') {
|
||||
return -Infinity;
|
||||
} else if (period === 'today') {
|
||||
return getTimeDaysAgo(server, 1);
|
||||
} else {
|
||||
return getTimeDaysAgo(server, parseInt(period));
|
||||
}
|
||||
export default function getPeriodStart(region: Region, period: PeriodSize) {
|
||||
return period === 'career' ? 0 : getTimeDaysAgo(region, parseInt(period));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { AccountInfo, AllStats } from '../../types/accountInfo';
|
||||
import {
|
||||
GetHistoriesOptions,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getHistoriesDefaultOptions,
|
||||
} from '../../types/histories';
|
||||
import getWargamingResponse from '../blitz/getWargamingResponse';
|
||||
import { WARGAMING_APPLICATION_ID } from '../node/arguments';
|
||||
import { secrets } from '../node/secrets';
|
||||
import { emptyAllStats } from './getDiffedTankStats';
|
||||
|
||||
export interface PlayerHistoryRaw {
|
||||
@@ -25,13 +25,13 @@ export interface PlayerHistoryRaw {
|
||||
account_id: number;
|
||||
[key: string]: number;
|
||||
};
|
||||
region: RegionDomain;
|
||||
region: Region;
|
||||
}
|
||||
|
||||
export type PlayerHistoriesRaw = PlayerHistoryRaw[];
|
||||
|
||||
export default async function getPlayerHistories(
|
||||
server: RegionDomain,
|
||||
server: Region,
|
||||
id: number,
|
||||
options?: Partial<GetHistoriesOptions>,
|
||||
) {
|
||||
@@ -67,7 +67,7 @@ export default async function getPlayerHistories(
|
||||
statistics: { all },
|
||||
} = (
|
||||
await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
)
|
||||
)[id];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import {
|
||||
GetHistoriesOptions,
|
||||
Histories,
|
||||
@@ -32,7 +32,7 @@ export const getTankHistoriesDefaultOptions: GetTankHistoriesOptions = {
|
||||
};
|
||||
|
||||
export default async function getTankHistories(
|
||||
server: RegionDomain,
|
||||
server: Region,
|
||||
id: number,
|
||||
options?: Partial<GetTankHistoriesOptions>,
|
||||
) {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
|
||||
export const TIME_ZONE_MAPPINGS: Record<RegionDomain, number> = {
|
||||
export const TIME_ZONE_MAPPINGS: Record<Region, number> = {
|
||||
com: -5, // Central North American Time
|
||||
eu: +1, // Central European Time
|
||||
asia: +7, // Central Asia Standard Time
|
||||
};
|
||||
|
||||
export default function getTimeDaysAgo(server: RegionDomain, daysAgo: number) {
|
||||
export default function getTimeDaysAgo(region: Region, daysAgo: number) {
|
||||
const now = new Date();
|
||||
if (daysAgo === 0) now.getTime() / 1000;
|
||||
|
||||
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);
|
||||
time.setUTCDate(time.getUTCDate() - daysAgo + 1);
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
||||
import { octokit } from '../../bot';
|
||||
import { Region } from '../../constants/regions';
|
||||
|
||||
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 content = Buffer.from(data.content, 'base64').toString();
|
||||
const jsonContent = JSON.parse(content) as BlitzkriegRatingsLeaderboard;
|
||||
|
||||
return jsonContent;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('No midnight leaderboard found');
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
} 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(
|
||||
option: SlashCommandSubcommandGroupBuilder,
|
||||
@@ -14,36 +14,41 @@ export default function addPeriodSubCommands(
|
||||
) {
|
||||
return option
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('today').setDescription(PERIOD_NAMES.today)),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('30').setDescription(PERIOD_NAMES[30])),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('60').setDescription(PERIOD_NAMES[60])),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('90').setDescription(PERIOD_NAMES[90])),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('career').setDescription(PERIOD_NAMES.career)),
|
||||
extra(
|
||||
option
|
||||
.setName('period' satisfies PeriodType)
|
||||
.setDescription("Preset period's statistics")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('period')
|
||||
.setDescription('A preset period')
|
||||
.setChoices(
|
||||
{ name: 'Today', value: '1' satisfies PeriodSize },
|
||||
{ name: '30 days', value: '30' satisfies PeriodSize },
|
||||
{ name: '60 days', value: '60' satisfies PeriodSize },
|
||||
{ name: '90 days', value: '90' satisfies PeriodSize },
|
||||
{ name: 'Career', value: 'career' satisfies PeriodSize },
|
||||
)
|
||||
.setRequired(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName('custom')
|
||||
.setDescription(PERIOD_NAMES.custom)
|
||||
.setName('custom' satisfies PeriodType)
|
||||
.setDescription("Custom period's statistics")
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('start')
|
||||
.setDescription('Start of period in days ago from today')
|
||||
.setDescription('Days ago from today')
|
||||
.setMinValue(0)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('end')
|
||||
.setDescription('End of period in days ago from today')
|
||||
.setDescription('Days ago from today')
|
||||
.setMinValue(0)
|
||||
.setRequired(true),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SlashCommandStringOption } from 'discord.js';
|
||||
import { REGION_NAMES } from '../../constants/regions';
|
||||
|
||||
export default function addRegionChoices(option: SlashCommandStringOption) {
|
||||
return option
|
||||
.setName('region')
|
||||
.setDescription('The Blitz region you are in')
|
||||
.addChoices(
|
||||
{ name: REGION_NAMES.com, value: 'com' },
|
||||
{ name: REGION_NAMES.eu, value: 'eu' },
|
||||
{ name: REGION_NAMES.asia, value: 'asia' },
|
||||
)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { SlashCommandStringOption } from 'discord.js';
|
||||
import { REGION_DOMAIN_NAMES } from '../../constants/regions';
|
||||
|
||||
export default function addServerChoices(option: SlashCommandStringOption) {
|
||||
return option
|
||||
.setName('server')
|
||||
.setDescription('The Blitz server you are in')
|
||||
.addChoices(
|
||||
{ name: REGION_DOMAIN_NAMES.com, value: 'com' },
|
||||
{ name: REGION_DOMAIN_NAMES.eu, value: 'eu' },
|
||||
{ name: REGION_DOMAIN_NAMES.asia, value: 'asia' },
|
||||
)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -1,18 +1,24 @@
|
||||
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 addTankChoices from './addTankChoices';
|
||||
import addUsernameChoices from './addUsernameChoices';
|
||||
|
||||
export default function addStatTypeSubCommandGroups(
|
||||
export default async function addStatTypeSubCommandGroups(
|
||||
option: SlashCommandBuilder,
|
||||
addMultiTank = true,
|
||||
) {
|
||||
return option
|
||||
const awaitedTankopediaInfo = await tankopediaInfo;
|
||||
|
||||
option
|
||||
.addSubcommandGroup((option) =>
|
||||
addPeriodSubCommands(option, (option) =>
|
||||
option.addStringOption(addUsernameChoices),
|
||||
)
|
||||
.setName('player')
|
||||
.setDescription("Player's statistics"),
|
||||
.setName('player' satisfies StatType)
|
||||
.setDescription('Player statistics'),
|
||||
)
|
||||
.addSubcommandGroup((option) =>
|
||||
addPeriodSubCommands(option, (option) =>
|
||||
@@ -20,7 +26,70 @@ export default function addStatTypeSubCommandGroups(
|
||||
.addStringOption(addTankChoices)
|
||||
.addStringOption(addUsernameChoices),
|
||||
)
|
||||
.setName('tank')
|
||||
.setDescription("Tank's statistics"),
|
||||
.setName('tank' satisfies StatType)
|
||||
.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),
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('tier')
|
||||
.setDescription('Tier')
|
||||
.setMinValue(1)
|
||||
.setMaxValue(10)
|
||||
.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;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { REGION_DOMAIN_NAMES } from '../../constants/regions';
|
||||
import { REGION_NAMES } from '../../constants/regions';
|
||||
import listClans from '../blitz/listClans';
|
||||
|
||||
export default async function autocompleteClan(
|
||||
@@ -11,6 +11,7 @@ export default async function autocompleteClan(
|
||||
) {
|
||||
const focusedOption = interaction.options.getFocused(true);
|
||||
if (focusedOption.name !== 'clan') return;
|
||||
if (focusedOption.value.length < 2) return interaction.respond([]);
|
||||
const players = await listClans(focusedOption.value);
|
||||
|
||||
await interaction.respond(
|
||||
@@ -18,7 +19,7 @@ export default async function autocompleteClan(
|
||||
? players.map(
|
||||
(player) =>
|
||||
({
|
||||
name: `${player.name} (${REGION_DOMAIN_NAMES[player.server]})`,
|
||||
name: `${player.name} (${REGION_NAMES[player.server]})`,
|
||||
value: `${player.server}/${player.clan_id}`,
|
||||
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
||||
)
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import resolveTankName from '../blitz/resolveTankName';
|
||||
import { TANKS } from '../blitz/tankopedia';
|
||||
import { TANKS, TANK_NAMES_DIACRITICS } from '../blitz/tankopedia';
|
||||
|
||||
export default async function autocompleteTanks(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
@@ -16,16 +15,14 @@ export default async function autocompleteTanks(
|
||||
await interaction.respond(
|
||||
focusedOption.value
|
||||
? await Promise.all(
|
||||
go(focusedOption.value, await TANKS, {
|
||||
keys: ['name'],
|
||||
go(focusedOption.value, await TANK_NAMES_DIACRITICS, {
|
||||
keys: ['combined'],
|
||||
limit: 10,
|
||||
}).map(
|
||||
async (item) =>
|
||||
async (item, index) =>
|
||||
({
|
||||
name: item.obj.name
|
||||
? item.obj.name
|
||||
: await resolveTankName(item.obj.tank_id),
|
||||
value: `${item.obj.tank_id}`,
|
||||
name: item.obj.original,
|
||||
value: `${(await TANKS)[index].tank_id}`,
|
||||
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { REGION_DOMAIN_NAMES } from '../../constants/regions';
|
||||
import { REGION_NAMES } from '../../constants/regions';
|
||||
import listPlayers from '../blitz/listPlayers';
|
||||
|
||||
export default async function autocompleteUsername(
|
||||
@@ -19,9 +19,7 @@ export default async function autocompleteUsername(
|
||||
? players.map(
|
||||
(player) =>
|
||||
({
|
||||
name: `${player.nickname} (${
|
||||
REGION_DOMAIN_NAMES[player.server]
|
||||
})`,
|
||||
name: `${player.nickname} (${REGION_NAMES[player.server]})`,
|
||||
value: `${player.server}/${player.account_id}`,
|
||||
}) satisfies ApplicationCommandOptionChoiceData<string>,
|
||||
)
|
||||
|
||||
@@ -19,12 +19,7 @@ export default function interactionToURL(
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
const params = [
|
||||
...interaction.options.data.map(
|
||||
(option) => [option.name, option.value] as const,
|
||||
),
|
||||
...Object.entries(parameters ?? {}),
|
||||
]
|
||||
const params = (parameters ? Object.entries(parameters) : [])
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('&');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ButtonInteraction, CacheType } from 'discord.js';
|
||||
import { CYCLIC_API } from '../../constants/cyclic';
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import resolvePeriodFromURL from '../express/resolvePeriodFromURL';
|
||||
|
||||
export default function resolvePeriodFromButton(
|
||||
server: RegionDomain,
|
||||
server: Region,
|
||||
interaction: ButtonInteraction<CacheType>,
|
||||
) {
|
||||
return resolvePeriodFromURL(server, `${CYCLIC_API}/${interaction.customId}`);
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import getPeriodNow from '../blitzstars/getPeriodNow';
|
||||
import getPeriodStart from '../blitzstars/getPeriodStart';
|
||||
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
|
||||
import { Period } from '../discord/addPeriodSubCommands';
|
||||
import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
|
||||
|
||||
export const PERIOD_NAMES: Record<Period, string> = {
|
||||
today: "Today's statistics",
|
||||
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 function getPeriodOptionName(period: PeriodSize) {
|
||||
return period === 'career' ? 'Career' : `${period}-day`;
|
||||
}
|
||||
|
||||
export interface ResolvedPeriod {
|
||||
statsName: string;
|
||||
@@ -31,29 +17,30 @@ export interface ResolvedPeriod {
|
||||
}
|
||||
|
||||
export default function resolvePeriodFromCommand(
|
||||
server: RegionDomain,
|
||||
region: Region,
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
) {
|
||||
let statsName: string;
|
||||
let evolutionName: string;
|
||||
let start: 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') {
|
||||
const startRaw = interaction.options.getInteger('start')!;
|
||||
const endRaw = interaction.options.getInteger('end')!;
|
||||
const startMin = Math.min(startRaw, endRaw);
|
||||
const endMax = Math.max(startRaw, endRaw);
|
||||
if (periodSubcommand === 'custom') {
|
||||
const startRaw = interaction.options.getInteger('start', true);
|
||||
const endRaw = interaction.options.getInteger('end', true);
|
||||
const startDaysAgoMin = Math.min(startRaw, endRaw);
|
||||
const endDaysAgoMax = Math.max(startRaw, endRaw);
|
||||
|
||||
statsName = `${startMin} to ${endMax} days' statistics`;
|
||||
evolutionName = `${startMin} to ${endMax} days' evolution`;
|
||||
start = getTimeDaysAgo(server, startMin);
|
||||
end = getTimeDaysAgo(server, endMax);
|
||||
statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
|
||||
evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
|
||||
start = getTimeDaysAgo(region, endDaysAgoMax);
|
||||
end = getTimeDaysAgo(region, startDaysAgoMin);
|
||||
} else {
|
||||
statsName = PERIOD_NAMES[period];
|
||||
evolutionName = EVOLUTION_PERIOD_NAMES[period];
|
||||
start = getPeriodStart(server, period);
|
||||
statsName = `${getPeriodOptionName(periodOption)} Statistics`;
|
||||
evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
|
||||
start = getPeriodStart(region, periodOption);
|
||||
end = getPeriodNow();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import listPlayers, {
|
||||
usernamePattern,
|
||||
usernamePatternWithoutPosition,
|
||||
} from '../blitz/listPlayers';
|
||||
import throwError from '../node/throwError';
|
||||
@@ -8,7 +9,7 @@ import throwError from '../node/throwError';
|
||||
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
||||
|
||||
export interface ResolvedPlayer {
|
||||
server: RegionDomain;
|
||||
region: Region;
|
||||
id: number;
|
||||
}
|
||||
|
||||
@@ -19,7 +20,7 @@ export default async function resolvePlayerFromCommand(
|
||||
const displayName = interaction.guild?.members.cache
|
||||
.get(interaction.user.id)
|
||||
?.displayName.match(usernamePatternWithoutPosition)?.[0];
|
||||
const username = commandUsername ?? displayName;
|
||||
const username = commandUsername?.match(usernamePattern)?.[0] ?? displayName;
|
||||
|
||||
if (username === undefined) {
|
||||
throw throwError(
|
||||
@@ -31,7 +32,7 @@ export default async function resolvePlayerFromCommand(
|
||||
if (serverAndIdPattern.test(username)) {
|
||||
const [server, accountId] = username.split('/');
|
||||
return {
|
||||
server: server as RegionDomain,
|
||||
region: server as Region,
|
||||
id: Number(accountId),
|
||||
} satisfies ResolvedPlayer;
|
||||
} else {
|
||||
@@ -39,7 +40,7 @@ export default async function resolvePlayerFromCommand(
|
||||
|
||||
if (accounts[0]) {
|
||||
return {
|
||||
server: accounts[0].server,
|
||||
region: accounts[0].server,
|
||||
id: accounts[0].account_id,
|
||||
} satisfies ResolvedPlayer;
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Request } from 'express';
|
||||
import { CYCLIC_API } from '../../constants/cyclic';
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import resolvePeriodFromURL from './resolvePeriodFromURL';
|
||||
|
||||
export default function resolvePeriodFromRequest(
|
||||
server: RegionDomain,
|
||||
server: Region,
|
||||
request: Request,
|
||||
) {
|
||||
return resolvePeriodFromURL(server, `${CYCLIC_API}${request.path}`);
|
||||
return resolvePeriodFromURL(server, `${CYCLIC_API}${request.url}`);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import getPeriodNow from '../blitzstars/getPeriodNow';
|
||||
import getPeriodStart from '../blitzstars/getPeriodStart';
|
||||
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
|
||||
import { Period } from '../discord/addPeriodSubCommands';
|
||||
import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
|
||||
import {
|
||||
EVOLUTION_PERIOD_NAMES,
|
||||
PERIOD_NAMES,
|
||||
ResolvedPeriod,
|
||||
getPeriodOptionName,
|
||||
} from '../discord/resolvePeriodFromCommand';
|
||||
|
||||
export default function resolvePeriodFromURL(
|
||||
server: RegionDomain,
|
||||
server: Region,
|
||||
urlString: string,
|
||||
) {
|
||||
let statsName: string;
|
||||
@@ -19,29 +18,25 @@ export default function resolvePeriodFromURL(
|
||||
let end: number;
|
||||
const url = new URL(urlString);
|
||||
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 endRaw = parseInt(url!.searchParams.get('end')!);
|
||||
const startMin = Math.min(startRaw, endRaw);
|
||||
const endMax = Math.max(startRaw, endRaw);
|
||||
const startDaysAgoMin = Math.min(startRaw, endRaw);
|
||||
const endDaysAgoMax = Math.max(startRaw, endRaw);
|
||||
|
||||
statsName = `${startMin} to ${endMax} days' statistics`;
|
||||
evolutionName = `${startMin} to ${endMax} days' evolution`;
|
||||
start = getTimeDaysAgo(server, startMin);
|
||||
end = getTimeDaysAgo(server, endMax);
|
||||
statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
|
||||
evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
|
||||
start = getTimeDaysAgo(server, endDaysAgoMax);
|
||||
end = getTimeDaysAgo(server, startDaysAgoMin);
|
||||
} else {
|
||||
statsName = PERIOD_NAMES[period];
|
||||
evolutionName = EVOLUTION_PERIOD_NAMES[period];
|
||||
start = getPeriodStart(server, period);
|
||||
statsName = `${getPeriodOptionName(periodOption)} Statistics`;
|
||||
evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
|
||||
start = getPeriodStart(server, periodOption);
|
||||
end = getPeriodNow();
|
||||
}
|
||||
|
||||
return {
|
||||
statsName,
|
||||
evolutionName,
|
||||
start,
|
||||
end,
|
||||
} satisfies ResolvedPeriod;
|
||||
return { statsName, evolutionName, start, end } satisfies ResolvedPeriod;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RegionDomain } from '../../constants/regions';
|
||||
import { Region } from '../../constants/regions';
|
||||
import { ResolvedPlayer } from '../discord/resolvePlayerFromCommand';
|
||||
|
||||
export default function resolvePlayerFromURL(urlString: string) {
|
||||
@@ -6,6 +6,6 @@ export default function resolvePlayerFromURL(urlString: string) {
|
||||
|
||||
return {
|
||||
id: parseInt(url.searchParams.get('id')!),
|
||||
server: url.searchParams.get('server') as RegionDomain,
|
||||
region: url.searchParams.get('region') as Region,
|
||||
} satisfies ResolvedPlayer;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export const DISCORD_TOKEN = process.env.DISCORD_TOKEN!;
|
||||
export const WARGAMING_APPLICATION_ID = process.env.WARGAMING_APPLICATION_ID!;
|
||||
|
||||
if (!DISCORD_TOKEN) {
|
||||
throw new Error('No Discord token provided');
|
||||
}
|
||||
if (!WARGAMING_APPLICATION_ID) {
|
||||
throw new Error('No Wargaming application id provided}');
|
||||
}
|
||||
@@ -32,7 +32,7 @@ Promise.all(
|
||||
|
||||
export default async function jsxToSvg(jsx: JSX.Element) {
|
||||
return await satori(jsx, {
|
||||
width: 640,
|
||||
width: 480,
|
||||
fonts: [
|
||||
{ data: roboto, name: FONT_NAME, weight: 400 },
|
||||
{ data: robotoBold, name: FONT_NAME, weight: 700 },
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const SECRET_KEYS = [
|
||||
'DISCORD_TOKEN',
|
||||
'WARGAMING_APPLICATION_ID',
|
||||
'GH_TOKEN',
|
||||
] as const;
|
||||
|
||||
export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[]
|
||||
? T
|
||||
: never;
|
||||
|
||||
export const secrets = SECRET_KEYS.reduce<Partial<Record<SECRET, string>>>(
|
||||
(accumulator, key) => ({
|
||||
...accumulator,
|
||||
|
||||
get [key](): string {
|
||||
if (!(key in process.env)) throw new Error(`Key ${key} not provided`);
|
||||
return process.env[key]!;
|
||||
},
|
||||
}),
|
||||
{},
|
||||
) as Record<SECRET, string>;
|
||||
@@ -15,7 +15,7 @@ export default async function (member: GuildMember) {
|
||||
.setColor(ACCENT_COLOR)
|
||||
.setTitle(`Welcome ${member.user.username}`)
|
||||
.setDescription(
|
||||
`Welcome to the Skilled server, **${member.user.username}**! To continue, please use the \`/verify\` command.\n\nExample: \`/verify Europe Sabina244\``,
|
||||
`Welcome to the Skilled server, **${member.user.username}**! To continue, please use the \`/verify\` command.\n\nExample: \`/verify Sabina244\``,
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AutocompleteInteraction, CacheType } from 'discord.js';
|
||||
import { commands } from '..';
|
||||
|
||||
export default function handleAutocomplete(
|
||||
export default async function handleAutocomplete(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
) {
|
||||
const command = commands[interaction.commandName];
|
||||
const command = (await commands)[interaction.commandName];
|
||||
|
||||
if (!command) {
|
||||
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 commandName = url.pathname.split('/')[1];
|
||||
const button = commands[commandName]?.button;
|
||||
const button = (await commands)[commandName]?.button;
|
||||
|
||||
if (!button) {
|
||||
throwError(`Button handler not found for "${commandName}"`);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ChatInputCommandInteraction,
|
||||
} from 'discord.js';
|
||||
import { commands } from '..';
|
||||
import discord from '../../../../discord.json' assert { type: 'json' };
|
||||
import embedNegative from '../../../core/discord/embedNegative';
|
||||
import embedWarning from '../../../core/discord/embedWarning';
|
||||
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
|
||||
@@ -15,12 +16,25 @@ import { handleError } from '../../error';
|
||||
export default async function handleChatInputCommand(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
) {
|
||||
const registry = commands[interaction.commandName];
|
||||
const registry = (await commands)[interaction.commandName];
|
||||
|
||||
console.log(interaction.toString());
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
if (registry.inPreview && interaction.guildId !== discord.tres_guild_id) {
|
||||
interaction.editReply({
|
||||
embeds: [
|
||||
embedWarning(
|
||||
`\`/${registry.command.name}\` is in Public Preview`,
|
||||
'[Join the official Discord server](https://discord.gg/nDt7AjGJQH) to gain early access to this command before it is released to the public.',
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const returnable = await registry.handler(interaction);
|
||||
|
||||
if (registry.handlesInteraction) return;
|
||||
|
||||
@@ -23,15 +23,16 @@ import { ownedTanksCommand } from '../../commands/ownedTanks';
|
||||
import { pingCommand } from '../../commands/ping';
|
||||
import { playerAchievementsCommand } from '../../commands/playerAchievements';
|
||||
import { playerInfoCommand } from '../../commands/playerInfo';
|
||||
import { ratingsCommand } from '../../commands/ratings';
|
||||
import { searchClansCommand } from '../../commands/searchClans';
|
||||
import { searchPlayersCommand } from '../../commands/searchPlayers';
|
||||
import { searchTanksCommand } from '../../commands/searchTanks';
|
||||
import { statsCommand } from '../../commands/stats';
|
||||
import { todayCommand } from '../../commands/today';
|
||||
import { verifyCommand } from '../../commands/verify';
|
||||
import { DISCORD_TOKEN } from '../../core/node/arguments';
|
||||
import getClientId from '../../core/node/getClientId';
|
||||
import isDev from '../../core/node/isDev';
|
||||
import { secrets } from '../../core/node/secrets';
|
||||
import handleAutocomplete from './handlers/autocomplete';
|
||||
import handleButton from './handlers/button';
|
||||
import handleChatInputCommand from './handlers/chatInputCommand';
|
||||
@@ -53,23 +54,32 @@ export interface Registry {
|
||||
inProduction: boolean;
|
||||
}
|
||||
|
||||
export interface CommandRegistry<HandlesInteraction extends boolean = boolean>
|
||||
extends Registry {
|
||||
export type CommandRegistryRaw = Registry & {
|
||||
inPublic: boolean;
|
||||
handlesInteraction?: HandlesInteraction;
|
||||
inPreview?: boolean;
|
||||
|
||||
command:
|
||||
| SlashCommandBuilder
|
||||
| SlashCommandSubcommandsOnlyBuilder
|
||||
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
|
||||
|
||||
handler: (
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
) => HandlesInteraction extends true ? void : InteractionReturnable;
|
||||
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
|
||||
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(DISCORD_TOKEN);
|
||||
const rest = new REST().setToken(secrets.DISCORD_TOKEN);
|
||||
|
||||
export const COMMANDS_RAW: CommandRegistry[] = [
|
||||
debugCommand,
|
||||
@@ -88,13 +98,21 @@ export const COMMANDS_RAW: CommandRegistry[] = [
|
||||
pingCommand,
|
||||
evolutionCommand,
|
||||
statsCommand,
|
||||
ratingsCommand,
|
||||
];
|
||||
export const commands: Record<string, CommandRegistry> = COMMANDS_RAW.reduce(
|
||||
(accumulator, registry) => {
|
||||
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
||||
return { ...accumulator, [registry.command.name]: registry };
|
||||
},
|
||||
{},
|
||||
|
||||
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
|
||||
rawCommands.reduce<Record<string, CommandRegistryRaw>>(
|
||||
(accumulator, registry) => {
|
||||
if (isDev()) registry.command.setDefaultMemberPermissions(0);
|
||||
|
||||
return {
|
||||
...accumulator,
|
||||
[registry.command.name]: registry,
|
||||
};
|
||||
},
|
||||
{},
|
||||
),
|
||||
);
|
||||
|
||||
export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
||||
@@ -102,48 +120,50 @@ export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
||||
export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
|
||||
[];
|
||||
|
||||
Object.entries(commands).forEach(([, registry]) => {
|
||||
if (isDev() ? registry.inDevelopment : registry.inProduction) {
|
||||
const json = registry.command.toJSON();
|
||||
commands.then((awaitedCommands) => {
|
||||
Object.entries(awaitedCommands).forEach(([, registry]) => {
|
||||
if (isDev() ? registry.inDevelopment : registry.inProduction) {
|
||||
const json = registry.command.toJSON();
|
||||
|
||||
if (registry.inPublic) {
|
||||
publicCommands.push(json);
|
||||
} else {
|
||||
guildCommands.push(json);
|
||||
if (registry.inPublic) {
|
||||
publicCommands.push(json);
|
||||
} else {
|
||||
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(
|
||||
interaction: Interaction<CacheType>,
|
||||
) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
import { tankopedia } from '../core/blitz/tankopedia';
|
||||
@@ -11,16 +10,16 @@ import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
||||
import getTankHistories from '../core/blitzstars/getTankHistories';
|
||||
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
|
||||
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { AccountInfo } from '../types/accountInfo';
|
||||
import { Histories } from '../types/histories';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
import { StatType } from './fullStats';
|
||||
import { StatType } from './stats';
|
||||
|
||||
export default async function evolution<Type extends StatType>(
|
||||
type: Type,
|
||||
{ start, end, evolutionName }: ResolvedPeriod,
|
||||
{ server, id }: ResolvedPlayer,
|
||||
{ region: server, id }: ResolvedPlayer,
|
||||
tankId: Type extends 'tank' ? number : null,
|
||||
) {
|
||||
let nameDiscriminator: string | undefined;
|
||||
@@ -29,7 +28,7 @@ export default async function evolution<Type extends StatType>(
|
||||
if (type === 'player') {
|
||||
const clan = (
|
||||
await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${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`,
|
||||
)
|
||||
)[id]?.clan;
|
||||
|
||||
@@ -79,7 +78,7 @@ export default async function evolution<Type extends StatType>(
|
||||
const minX = Math.min(...xs);
|
||||
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -88,20 +87,17 @@ export default async function evolution<Type extends StatType>(
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={nameDiscriminator}
|
||||
image={image}
|
||||
description={`${evolutionName} • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
description={`${evolutionName} • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{/* goofy ahh bug forces me to call them as functions */}
|
||||
{plot.length > 0 && (
|
||||
<Graph.Root
|
||||
xTitle="Battles"
|
||||
verticalMargin={{
|
||||
min: minY,
|
||||
max: maxY,
|
||||
suffix: '%',
|
||||
precision: 1,
|
||||
precision: 2,
|
||||
}}
|
||||
horizontalMargin={{
|
||||
min: minX,
|
||||
|
||||
+84
-76
@@ -4,7 +4,6 @@ import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import TierWeights, { TierWeightsRecord } from '../components/TierWeights';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
@@ -14,22 +13,24 @@ import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
|
||||
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import {
|
||||
AccountInfo,
|
||||
AllStats,
|
||||
SupplementaryStats,
|
||||
} from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||
|
||||
export type StatType = 'player' | 'tank';
|
||||
import { MultiTankFilters, StatType } from './stats';
|
||||
|
||||
export default async function fullStats<Type extends StatType>(
|
||||
type: Type,
|
||||
{ start, end, statsName }: ResolvedPeriod,
|
||||
{ server, id }: ResolvedPlayer,
|
||||
tankId: Type extends 'tank' ? number : null,
|
||||
{ region: server, id }: ResolvedPlayer,
|
||||
filters: Type extends 'tank'
|
||||
? number
|
||||
: Type extends 'multi-tank'
|
||||
? Partial<MultiTankFilters>
|
||||
: null,
|
||||
) {
|
||||
let nameDiscriminator: string | undefined;
|
||||
let image: string | undefined;
|
||||
@@ -37,7 +38,7 @@ export default async function fullStats<Type extends StatType>(
|
||||
if (type === 'player') {
|
||||
const clan = (
|
||||
await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${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`,
|
||||
)
|
||||
)[id]?.clan;
|
||||
|
||||
@@ -46,94 +47,103 @@ export default async function fullStats<Type extends StatType>(
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||
: undefined;
|
||||
} else if (type === 'tank') {
|
||||
nameDiscriminator = `(${await resolveTankName(tankId!)})`;
|
||||
image = (await tankopedia)[tankId!]?.images.normal;
|
||||
nameDiscriminator = `(${await resolveTankName(filters as number)})`;
|
||||
image = (await tankopedia)[filters as number]?.images.normal;
|
||||
} else {
|
||||
}
|
||||
|
||||
const { diffed } = await getDiffedTankStats(server, id, start, end);
|
||||
let stats: AllStats;
|
||||
const { diffed, order } = await getDiffedTankStats(server, id, start, end);
|
||||
const awaitedTankopedia = await tankopedia;
|
||||
let stats: AllStats | undefined;
|
||||
let supplementaryStats: SupplementaryStats;
|
||||
let tierWeights: TierWeightsRecord;
|
||||
|
||||
if (type === 'player') {
|
||||
const entries = Object.entries(diffed);
|
||||
stats = sumStats(entries.map(([, stats]) => stats));
|
||||
const battlesOfTanksWithAverages = await entries.reduce<
|
||||
PossiblyPromise<number>
|
||||
>(async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankAverage = (await tankAverages)[tankId];
|
||||
|
||||
return tankAverage ? (await accumulator) + stats.battles : accumulator;
|
||||
}, 0);
|
||||
const battlesOfTanksWithTankopediaEntry = await entries.reduce<
|
||||
PossiblyPromise<number>
|
||||
>(async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankopediaEntry = (await tankopedia)[tankId];
|
||||
|
||||
return tankopediaEntry
|
||||
? (await accumulator) + stats.battles
|
||||
: accumulator;
|
||||
}, 0);
|
||||
if (type === 'player' || type === 'multi-tank') {
|
||||
const typedFilters = filters as Partial<MultiTankFilters> | null;
|
||||
const filteredOrder = order.filter(
|
||||
(id) =>
|
||||
typedFilters === null ||
|
||||
((typedFilters.nation === undefined ||
|
||||
awaitedTankopedia[id]?.nation === typedFilters.nation) &&
|
||||
(typedFilters['tank-type'] === undefined ||
|
||||
awaitedTankopedia[id]?.type === typedFilters['tank-type']) &&
|
||||
(typedFilters.tier === undefined ||
|
||||
awaitedTankopedia[id]?.tier === typedFilters.tier) &&
|
||||
(typedFilters['tree-type'] === undefined ||
|
||||
(typedFilters['tree-type'] === 'collector' &&
|
||||
awaitedTankopedia[id]?.is_collectible) ||
|
||||
(typedFilters['tree-type'] === 'premium' &&
|
||||
awaitedTankopedia[id]?.is_premium) ||
|
||||
(typedFilters['tree-type'] === 'techtree' &&
|
||||
!awaitedTankopedia[id]?.is_collectible &&
|
||||
!awaitedTankopedia[id]?.is_premium))),
|
||||
);
|
||||
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 = {
|
||||
WN8:
|
||||
(await entries.reduce<PossiblyPromise<number>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankAverage = (await tankAverages)[tankId];
|
||||
|
||||
return tankAverage
|
||||
? (await accumulator) +
|
||||
calculateWN8(tankAverage.all, stats) * stats.battles
|
||||
: accumulator;
|
||||
},
|
||||
filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankAverages[id]
|
||||
? accumulator +
|
||||
calculateWN8(awaitedTankAverages[id].all, diffed[id]) *
|
||||
diffed[id].battles
|
||||
: accumulator,
|
||||
0,
|
||||
)) / battlesOfTanksWithAverages,
|
||||
) / battlesOfTanksWithAverages,
|
||||
tier:
|
||||
(await entries.reduce<PossiblyPromise<number>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankopediaEntry = (await tankopedia)[tankId];
|
||||
|
||||
return tankopediaEntry
|
||||
? (await accumulator) + tankopediaEntry.tier * stats.battles
|
||||
: accumulator;
|
||||
},
|
||||
filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankopedia[id]
|
||||
? accumulator + awaitedTankopedia[id]!.tier * diffed[id].battles
|
||||
: accumulator,
|
||||
0,
|
||||
)) / battlesOfTanksWithTankopediaEntry,
|
||||
) / battlesOfTanksWithTankopediaEntry,
|
||||
};
|
||||
tierWeights = await entries.reduce<PossiblyPromise<TierWeightsRecord>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankopediaEntry = (await tankopedia)[tankId];
|
||||
tierWeights = filteredOrder.reduce<TierWeightsRecord>((accumulator, id) => {
|
||||
const tankopediaEntry = awaitedTankopedia[id];
|
||||
|
||||
if (!tankopediaEntry) return accumulator;
|
||||
if (!tankopediaEntry) return accumulator;
|
||||
|
||||
const tier = tankopediaEntry.tier as Tier;
|
||||
const tier = tankopediaEntry.tier as Tier;
|
||||
|
||||
if ((await accumulator)[tier]) {
|
||||
(await accumulator)[tier]! += stats.battles;
|
||||
} else {
|
||||
(await accumulator)[tier] = stats.battles;
|
||||
}
|
||||
if (accumulator[tier]) {
|
||||
accumulator[tier]! += diffed[id].battles;
|
||||
} else {
|
||||
accumulator[tier] = diffed[id].battles;
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return accumulator;
|
||||
}, {});
|
||||
} else {
|
||||
stats = diffed[tankId!];
|
||||
stats = diffed[(filters as number)!];
|
||||
|
||||
supplementaryStats = {
|
||||
WN8: calculateWN8((await tankAverages)[tankId!].all, diffed[tankId!]),
|
||||
tier: (await tankopedia)[tankId!]?.tier,
|
||||
WN8: stats
|
||||
? calculateWN8(
|
||||
(await tankAverages)[(filters as number)!].all,
|
||||
diffed[(filters as number)!],
|
||||
)
|
||||
: undefined,
|
||||
tier: (await tankopedia)[(filters as number)!]?.tier,
|
||||
};
|
||||
}
|
||||
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -142,9 +152,7 @@ export default async function fullStats<Type extends StatType>(
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={nameDiscriminator}
|
||||
image={image}
|
||||
description={`${statsName} • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
description={`${statsName} • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||
|
||||
+81
-83
@@ -1,20 +1,19 @@
|
||||
import AllStatsOverview from '../components/AllStatsOverview';
|
||||
import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import { TierWeightsRecord } from '../components/TierWeights';
|
||||
import { TreeTypeString } from '../components/Tanks';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
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 { tankAverages } from '../core/blitzstars/tankAverages';
|
||||
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
|
||||
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { theme } from '../stitches.config';
|
||||
import {
|
||||
AccountInfo,
|
||||
@@ -22,15 +21,25 @@ import {
|
||||
SupplementaryStats,
|
||||
} from '../types/accountInfo';
|
||||
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>(
|
||||
type: Type,
|
||||
{ start, end, statsName }: ResolvedPeriod,
|
||||
{ server, id }: ResolvedPlayer,
|
||||
tankId: Type extends 'tank' ? number : null,
|
||||
{ region: server, id }: ResolvedPlayer,
|
||||
filters: Type extends 'tank'
|
||||
? number
|
||||
: Type extends 'multi-tank'
|
||||
? Partial<MultiTankFilters>
|
||||
: null,
|
||||
naked = false,
|
||||
) {
|
||||
let nameDiscriminator: string | undefined;
|
||||
@@ -39,7 +48,7 @@ export default async function stats<Type extends StatType>(
|
||||
if (type === 'player') {
|
||||
const clan = (
|
||||
await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${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`,
|
||||
)
|
||||
)[id]?.clan;
|
||||
|
||||
@@ -48,96 +57,87 @@ export default async function stats<Type extends StatType>(
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||
: undefined;
|
||||
} else if (type === 'tank') {
|
||||
nameDiscriminator = `(${await resolveTankName(tankId!)})`;
|
||||
image = (await tankopedia)[tankId!]?.images.normal;
|
||||
nameDiscriminator = `(${await resolveTankName(filters as number)})`;
|
||||
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 supplementaryStats: SupplementaryStats;
|
||||
let tierWeights: TierWeightsRecord;
|
||||
|
||||
if (type === 'player') {
|
||||
const entries = Object.entries(diffed);
|
||||
stats = sumStats(entries.map(([, stats]) => stats));
|
||||
const battlesOfTanksWithAverages = await entries.reduce<
|
||||
PossiblyPromise<number>
|
||||
>(async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankAverage = (await tankAverages)[tankId];
|
||||
|
||||
return tankAverage ? (await accumulator) + stats.battles : accumulator;
|
||||
}, 0);
|
||||
const battlesOfTanksWithTankopediaEntry = await entries.reduce<
|
||||
PossiblyPromise<number>
|
||||
>(async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankopediaEntry = (await tankopedia)[tankId];
|
||||
|
||||
return tankopediaEntry
|
||||
? (await accumulator) + stats.battles
|
||||
: accumulator;
|
||||
}, 0);
|
||||
if (type === 'player' || type === 'multi-tank') {
|
||||
const typedFilters = filters as Partial<MultiTankFilters> | null;
|
||||
const filteredOrder = order.filter(
|
||||
(id) =>
|
||||
typedFilters === null ||
|
||||
((typedFilters.nation === undefined ||
|
||||
awaitedTankopedia[id]?.nation === typedFilters.nation) &&
|
||||
(typedFilters['tank-type'] === undefined ||
|
||||
awaitedTankopedia[id]?.type === typedFilters['tank-type']) &&
|
||||
(typedFilters.tier === undefined ||
|
||||
awaitedTankopedia[id]?.tier === typedFilters.tier) &&
|
||||
(typedFilters['tree-type'] === undefined ||
|
||||
(typedFilters['tree-type'] === 'collector' &&
|
||||
awaitedTankopedia[id]?.is_collectible) ||
|
||||
(typedFilters['tree-type'] === 'premium' &&
|
||||
awaitedTankopedia[id]?.is_premium) ||
|
||||
(typedFilters['tree-type'] === 'techtree' &&
|
||||
!awaitedTankopedia[id]?.is_collectible &&
|
||||
!awaitedTankopedia[id]?.is_premium))),
|
||||
);
|
||||
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 = {
|
||||
WN8:
|
||||
(await entries.reduce<PossiblyPromise<number>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankAverage = (await tankAverages)[tankId];
|
||||
|
||||
return tankAverage
|
||||
? (await accumulator) +
|
||||
calculateWN8(tankAverage.all, stats) * stats.battles
|
||||
: accumulator;
|
||||
},
|
||||
filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankAverages[id]
|
||||
? accumulator +
|
||||
calculateWN8(awaitedTankAverages[id].all, diffed[id]) *
|
||||
diffed[id].battles
|
||||
: accumulator,
|
||||
0,
|
||||
)) / battlesOfTanksWithAverages,
|
||||
) / battlesOfTanksWithAverages,
|
||||
tier:
|
||||
(await entries.reduce<PossiblyPromise<number>>(
|
||||
async (accumulator, [tankIdString, stats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
const tankopediaEntry = (await tankopedia)[tankId];
|
||||
|
||||
return tankopediaEntry
|
||||
? (await accumulator) + tankopediaEntry.tier * stats.battles
|
||||
: accumulator;
|
||||
},
|
||||
filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankopedia[id]
|
||||
? accumulator + awaitedTankopedia[id]!.tier * diffed[id].battles
|
||||
: accumulator,
|
||||
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 {
|
||||
stats = diffed[tankId!];
|
||||
stats = diffed[(filters as number)!];
|
||||
|
||||
supplementaryStats = {
|
||||
WN8: stats
|
||||
? calculateWN8((await tankAverages)[tankId!].all, diffed[tankId!])
|
||||
? calculateWN8(
|
||||
(await tankAverages)[(filters as number)!].all,
|
||||
diffed[(filters as number)!],
|
||||
)
|
||||
: undefined,
|
||||
tier: (await tankopedia)[tankId!]?.tier,
|
||||
tier: (await tankopedia)[(filters as number)!]?.tier,
|
||||
};
|
||||
}
|
||||
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const overview = (
|
||||
<AllStatsOverview stats={stats} supplementaryStats={supplementaryStats} />
|
||||
@@ -151,7 +151,7 @@ export default async function stats<Type extends StatType>(
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<u>/statsfull</u> for all statistics
|
||||
<u>/full-stats</u> for all statistics
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -161,11 +161,9 @@ export default async function stats<Type extends StatType>(
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={nameDiscriminator}
|
||||
image={image}
|
||||
description={`${statsName} • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
nameDiscriminator={nameDiscriminator}
|
||||
description={`${statsName} • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||
|
||||
+63
-57
@@ -3,9 +3,9 @@ import NoData, { NoDataType } from '../components/NoData';
|
||||
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import Wrapper from '../components/Wrapper';
|
||||
import { REGION_DOMAIN_NAMES } from '../constants/regions';
|
||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
||||
import getTankStats from '../core/blitz/getTankStats';
|
||||
import getTreeType from '../core/blitz/getTreeType';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||
import resolveTankName from '../core/blitz/resolveTankName';
|
||||
import sumStats from '../core/blitz/sumStats';
|
||||
@@ -15,16 +15,20 @@ import getPeriodNow from '../core/blitzstars/getPeriodNow';
|
||||
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand';
|
||||
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments';
|
||||
import { secrets } from '../core/node/secrets';
|
||||
import { AccountInfo, AllStats } from '../types/accountInfo';
|
||||
import { PlayerClanData } from '../types/playerClanData';
|
||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||
|
||||
export default async function today(
|
||||
{ server, id }: ResolvedPlayer,
|
||||
limit = Infinity,
|
||||
{ region: server, id }: ResolvedPlayer,
|
||||
cutoff = Infinity,
|
||||
maximized = 4,
|
||||
showTotal = true,
|
||||
naked?: boolean,
|
||||
) {
|
||||
console.log(maximized);
|
||||
|
||||
const { diffed, order } = await getDiffedTankStats(
|
||||
server,
|
||||
id,
|
||||
@@ -32,15 +36,17 @@ export default async function today(
|
||||
getPeriodNow(),
|
||||
);
|
||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||
);
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${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 careerStats: Record<number, AllStats> = {
|
||||
0: accountInfo[id].statistics.all,
|
||||
};
|
||||
const careerStats: Record<number, AllStats> = showTotal
|
||||
? {
|
||||
0: accountInfo[id].statistics.all,
|
||||
}
|
||||
: {};
|
||||
const allStatsToAccumulate: AllStats[] = [];
|
||||
|
||||
Object.entries(diffed).forEach(([, tankStats]) => {
|
||||
@@ -52,7 +58,7 @@ export default async function today(
|
||||
|
||||
const accumulatedStats = sumStats(allStatsToAccumulate);
|
||||
|
||||
if (Object.keys(diffed).length > 0) {
|
||||
if (showTotal && Object.keys(diffed).length > 0) {
|
||||
diffed[0] = accumulatedStats;
|
||||
}
|
||||
|
||||
@@ -62,7 +68,7 @@ export default async function today(
|
||||
>(async (accumulator, [tankIdString, tankStats]) => {
|
||||
const tankId = parseInt(tankIdString);
|
||||
|
||||
return tankId === 0 || !(await tankAverages)[tankId]
|
||||
return (showTotal && tankId === 0) || !(await tankAverages)[tankId]
|
||||
? accumulator
|
||||
: {
|
||||
...(await accumulator),
|
||||
@@ -73,7 +79,8 @@ export default async function today(
|
||||
const careerWN8s = await careerTankStatsRaw.reduce<
|
||||
PossiblyPromise<Record<number, number>>
|
||||
>(async (accumulator, { tank_id }) => {
|
||||
return tank_id === 0 || (await tankAverages)[tank_id] === undefined
|
||||
return (showTotal && tank_id === 0) ||
|
||||
(await tankAverages)[tank_id] === undefined
|
||||
? accumulator
|
||||
: {
|
||||
...(await accumulator),
|
||||
@@ -86,60 +93,61 @@ export default async function today(
|
||||
const todayWN8sEntries = Object.entries(todayWN8s);
|
||||
const careerWN8sEntries = Object.entries(careerWN8s);
|
||||
|
||||
todayWN8s[0] =
|
||||
todayWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, wn8]) =>
|
||||
accumulator + wn8 * diffed[Number(tankIdString)].battles,
|
||||
0,
|
||||
) /
|
||||
todayWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString]) =>
|
||||
accumulator + diffed[Number(tankIdString)].battles,
|
||||
0,
|
||||
);
|
||||
careerWN8s[0] =
|
||||
careerWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, WN8]) =>
|
||||
isNaN(WN8)
|
||||
? accumulator
|
||||
: accumulator + WN8 * careerStats[Number(tankIdString)].battles,
|
||||
0,
|
||||
) /
|
||||
careerWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, WN8]) =>
|
||||
isNaN(WN8)
|
||||
? accumulator
|
||||
: accumulator + careerStats[Number(tankIdString)].battles,
|
||||
0,
|
||||
);
|
||||
if (showTotal) {
|
||||
todayWN8s[0] =
|
||||
todayWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, wn8]) =>
|
||||
accumulator + wn8 * diffed[Number(tankIdString)].battles,
|
||||
0,
|
||||
) /
|
||||
todayWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString]) =>
|
||||
accumulator + diffed[Number(tankIdString)].battles,
|
||||
0,
|
||||
);
|
||||
careerWN8s[0] =
|
||||
careerWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, WN8]) =>
|
||||
isNaN(WN8)
|
||||
? accumulator
|
||||
: accumulator + WN8 * careerStats[Number(tankIdString)].battles,
|
||||
0,
|
||||
) /
|
||||
careerWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, WN8]) =>
|
||||
isNaN(WN8)
|
||||
? accumulator
|
||||
: accumulator + careerStats[Number(tankIdString)].battles,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
const rows = await Promise.all(
|
||||
[
|
||||
// this code unreadable on god will forget tomorrow no cap
|
||||
...(order.length === 0 ? [] : [0]),
|
||||
...(isFinite(limit) ? order.slice(0, limit) : order),
|
||||
].map(async (tankId, index) => {
|
||||
const tankStats = diffed[tankId];
|
||||
const career = careerStats[tankId];
|
||||
...(order.length === 0 || !showTotal ? [] : [0]),
|
||||
...(isFinite(cutoff) ? order.slice(0, cutoff) : order),
|
||||
].map(async (id, index) => {
|
||||
const tankStats = diffed[id];
|
||||
const career = careerStats[id];
|
||||
const tankopediaEntry = (await tankopedia)[id];
|
||||
|
||||
return (
|
||||
<Breakdown.Row
|
||||
naked={naked}
|
||||
isListing={tankId !== 0}
|
||||
minimized={index > 4}
|
||||
key={tankId}
|
||||
name={tankId === 0 ? 'Total' : await resolveTankName(tankId)}
|
||||
isTank={!showTotal || id !== 0}
|
||||
tankType={tankopediaEntry?.type}
|
||||
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||
minimized={showTotal ? index > maximized : index + 1 > maximized}
|
||||
key={id}
|
||||
name={showTotal && id === 0 ? 'Total' : await resolveTankName(id)}
|
||||
winrate={tankStats.wins / tankStats.battles}
|
||||
careerWinrate={career.wins / career.battles}
|
||||
WN8={isNaN(todayWN8s[tankId]) ? undefined : todayWN8s[tankId]}
|
||||
careerWN8={isNaN(careerWN8s[tankId]) ? undefined : careerWN8s[tankId]}
|
||||
WN8={isNaN(todayWN8s[id]) ? undefined : todayWN8s[id]}
|
||||
careerWN8={isNaN(careerWN8s[id]) ? undefined : careerWN8s[id]}
|
||||
damage={tankStats.damage_dealt / tankStats.battles}
|
||||
careerDamage={career.damage_dealt / career.battles}
|
||||
battles={tankStats.battles}
|
||||
careerBattles={career.battles}
|
||||
icon={
|
||||
tankId === 0 ? undefined : (await tankopedia)[tankId]?.images.normal
|
||||
}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
@@ -147,7 +155,7 @@ export default async function today(
|
||||
|
||||
return naked ? (
|
||||
<Wrapper naked>
|
||||
<Breakdown.Root>{rows.slice(1)}</Breakdown.Root>
|
||||
<Breakdown.Root>{rows}</Breakdown.Root>
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Wrapper>
|
||||
@@ -161,9 +169,7 @@ export default async function today(
|
||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
||||
: undefined
|
||||
}
|
||||
description={`Today's breakdown • ${new Date().toDateString()} • ${
|
||||
REGION_DOMAIN_NAMES[server]
|
||||
}`}
|
||||
description={`Today's breakdown • ${new Date().toDateString()}`}
|
||||
/>
|
||||
|
||||
{rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||
|
||||
@@ -6,12 +6,12 @@ import { RouteRegistry } from '../server';
|
||||
export const statsRoute: RouteRegistry = {
|
||||
route: '/stats/:type/:period',
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
|
||||
async handler(req) {
|
||||
const type = req.params.type as StatType;
|
||||
const player = resolvePlayerFromRequest(req);
|
||||
const period = resolvePeriodFromRequest(player.server, req);
|
||||
const period = resolvePeriodFromRequest(player.region, req);
|
||||
|
||||
return await stats(
|
||||
type,
|
||||
|
||||
@@ -5,14 +5,16 @@ import { RouteRegistry } from '../server';
|
||||
export const todayRoute: RouteRegistry = {
|
||||
route: '/today',
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
|
||||
async handler(req) {
|
||||
const player = resolvePlayerFromRequest(req);
|
||||
|
||||
return await today(
|
||||
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,
|
||||
);
|
||||
},
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ const app = express();
|
||||
if (registry.handlesInteraction) return;
|
||||
|
||||
res.send(
|
||||
withAutoRefresh(await normalizeRouteReturnable(returnable), 60 * 1000),
|
||||
withAutoRefresh(await normalizeRouteReturnable(returnable), 30 * 1000),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-4
@@ -11,8 +11,5 @@
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/components/AllStatsOverview/components/WN8SurroundingStat.tsx"
|
||||
]
|
||||
"include": ["src/**/*.ts", "src/components/Tanks/components/Row.tsx"]
|
||||
}
|
||||
Reference in new issue
Block a user