Compare commits

...
51 Commits
Author SHA1 Message Date
TrèsAbhi e9a492fde6 Merge pull request #23 from tresabhi/1.12.0 2023-08-10 13:52:10 -05:00
TresAbhi 76a69f08b4 feat: docs 2023-08-10 18:51:22 +00:00
TresAbhi 893110d3ef feat: added 1 more decimal of precision in evolution 2023-08-10 01:49:05 +00:00
TresAbhi 70575fa9cf feat: accent-tolerant tank search 2023-08-10 01:46:39 +00:00
TresAbhi bb2450eb3c feat: added the new /today embeds 2023-08-10 01:31:05 +00:00
TresAbhi ab56689326 feat: updated /today with figma 2023-08-10 01:03:53 +00:00
TresAbhi a9cf07480a feat: added arabic numerals to owned tanks options 2023-08-10 00:37:54 +00:00
TresAbhi c81de2d758 feat: removed interaction based search param inference 2023-08-09 18:02:15 +00:00
TresAbhi aa1c7639f2 feat: auto refresh rate changed to every 10 seconds 2023-08-09 18:00:23 +00:00
TresAbhi d448101b32 fix: swapped start and end min and max 2023-08-09 17:56:35 +00:00
TresAbhi e806f0ede2 fix: swapped min and max for start and end 2023-08-09 17:51:19 +00:00
TresAbhi 8253c27333 fix: search parameters are now passed correctly 2023-08-09 17:39:21 +00:00
TresAbhi 254392370f feat: only interpret correctly formatter usernames 2023-08-09 17:27:38 +00:00
TresAbhi d2319688a5 fix: removed multi-tank from evolution 2023-08-09 17:25:05 +00:00
TresAbhi ffebe6c696 fix: use regional season info instead of hard-coded NA 2023-08-09 17:20:03 +00:00
TresAbhi 9e4094f3a9 feat: added ratings neighbours 2023-08-09 17:13:26 +00:00
TresAbhi f7eadb08a9 feat: made /ratings data-driven 2023-08-09 00:47:15 +00:00
TresAbhi d511cfa4cc fix: fixed ratings reward alignment 2023-08-09 00:35:33 +00:00
TresAbhi 270a3051b7 feat: updated /owned-tanks to new width 2023-08-09 00:19:50 +00:00
TresAbhi 81bc9f9ba6 feat: updated /inactive 2023-08-07 22:54:55 +00:00
TresAbhi 338cec4f18 feat: updated full-stats to new subcommands 2023-08-07 22:51:04 +00:00
TresAbhi b455e3380f feat: added minimum character limit to clan autocomplete 2023-08-06 19:13:28 +00:00
TresAbhi 92761f2c55 feat: made footer center aligned when without footer text 2023-08-06 19:08:02 +00:00
TresAbhi 7823b8ad2e feat: updated /evolution to new width 2023-08-06 19:07:44 +00:00
TresAbhi cac82f3124 fix: renamed statsfull to fullstats in power by bar 2023-08-06 19:00:32 +00:00
TresAbhi 4b892d06e4 fix: import types from stats instead of fullstats 2023-08-06 19:00:00 +00:00
TresAbhi 92149b0f00 feat: matched titlebar style to figma 2023-08-06 18:53:57 +00:00
TresAbhi 399aa4c03c feat: removed region text in title bar 2023-08-06 18:41:59 +00:00
TresAbhi d5de715032 fix: title bar spacing is now 4px just like figma 2023-08-06 18:39:23 +00:00
TresAbhi e6fb179184 feat: removed self-advert from title bar 2023-08-06 18:38:49 +00:00
TresAbhi af54f12ec3 feat: set width to 480px 2023-08-06 18:38:17 +00:00
TresAbhi 909cabd517 feat: filter functionality 2023-08-04 19:49:40 +00:00
TresAbhi a734a71d2f fix: duplicate efficienty cards 2023-08-04 18:25:20 +00:00
TresAbhi 91dc45c880 feat: added more mature periods 2023-08-04 18:24:20 +00:00
TresAbhi 91f364b76a feat: used dahses for command readibility 2023-08-04 02:24:14 +00:00
TresAbhi 23dd088466 feat: better subcommand organiztion 2023-08-04 02:13:21 +00:00
TresAbhi 2cabcc57f4 feat: moved ratings out of preview 2023-07-31 01:32:31 +00:00
TrèsAbhi dd6092a5e7 fix: GH_TOKEN 2023-07-17 19:37:12 -05:00
TrèsAbhi 3c2861f956 Merge pull request #22 from tresabhi/1.11.0 2023-07-17 19:30:50 -05:00
TresAbhi 894b841769 feat: changelogs 2023-07-18 00:29:49 +00:00
TresAbhi 62627c96a3 feat: added other regions 2023-07-18 00:23:59 +00:00
TresAbhi c173ece882 feat: completed /ratings league 2023-07-18 00:17:00 +00:00
TresAbhi eca7cbbbeb feat: added more mature secrets handling 2023-07-16 18:26:33 +00:00
TresAbhi c72bfa9f71 feat: added rewards 2023-07-16 18:15:34 +00:00
TresAbhi b4ae39a509 feat: added custom lighter ratings leaderboard 2023-07-16 17:56:37 +00:00
TresAbhi 631c051895 feat: ditch histories 2023-07-16 17:51:01 +00:00
TresAbhi 9a2e11a2a4 feat: added basic rendering 2023-07-16 17:45:29 +00:00
TresAbhi aa6011b18d feat: added basic infrastructure for ratings command 2023-07-15 18:44:20 +00:00
TresAbhi 9419aec116 fix: removed "Europe" from /verify example 2023-07-15 18:07:14 +00:00
TresAbhi 638402589f feat: switched scripts to typescript 2023-07-15 01:21:59 +00:00
TrèsAbhi 6d6a6bf738 fix: workflow region names 2023-07-14 12:41:45 -05:00
95 changed files with 2430 additions and 1575 deletions

No files matched your search

+4 -4
View File
@@ -1,8 +1,8 @@
name: CD North America Ratings Latest name: CD Asia Ratings Latest
on: on:
schedule: schedule:
- cron: '0 0 * * *' - cron: "0 0 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
@@ -1,8 +1,8 @@
name: CD North America Ratings Leaderboards name: CD Asia Ratings Leaderboards
on: on:
schedule: schedule:
- cron: '0 17 * * *' - cron: "0 17 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
+4 -4
View File
@@ -1,8 +1,8 @@
name: CD North America Ratings Latest name: CD Europe Ratings Latest
on: on:
schedule: schedule:
- cron: '0 0 * * *' - cron: "0 0 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
@@ -1,8 +1,8 @@
name: CD North America Ratings Leaderboards name: CD Europe Ratings Leaderboards
on: on:
schedule: schedule:
- cron: '0 23 * * *' - cron: "0 23 * * *"
jobs: jobs:
host: host:
@@ -16,7 +16,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: 'npm' cache: "npm"
- name: Install Yarn - name: Install Yarn
run: npm install --location=global yarn run: npm install --location=global yarn
@@ -24,7 +24,7 @@ jobs:
- name: Use node_modules cache - name: Use node_modules cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: '**/node_modules' path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies - name: Install dependencies
+1 -1
View File
@@ -29,4 +29,4 @@ jobs:
unzip dist.zip unzip dist.zip
- name: Run bot - name: Run bot
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.js
+2
View File
@@ -28,6 +28,7 @@
"statsfull", "statsfull",
"Stringifying", "Stringifying",
"strvk", "strvk",
"subnation",
"tankaverages", "tankaverages",
"tankopedia", "tankopedia",
"tankstats", "tankstats",
@@ -37,6 +38,7 @@
"vickers", "vickers",
"vitepress", "vitepress",
"wargaming", "wargaming",
"webp",
"winrate" "winrate"
], ],
"deno.enable": false, "deno.enable": false,
+22
View File
@@ -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
+54
View File
@@ -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
View File
@@ -1,6 +1,7 @@
{ {
"main": "server.cjs", "main": "server.js",
"type": "module",
"scripts": { "scripts": {
"start": "node --enable-source-maps --trace-warnings server.cjs" "start": "node --enable-source-maps --trace-warnings server.js"
} }
} }
+10 -8
View File
@@ -1,44 +1,46 @@
{ {
"name": "blitzkrieg", "name": "blitzkrieg",
"version": "1.10.2", "version": "1.12.0",
"description": "", "description": "",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build:bot": "node scripts/build --build=bot", "build:bot": "vite-node --script scripts/build --build=bot",
"build:server": "node scripts/build --build=server", "build:server": "vite-node --script scripts/build --build=server",
"build:docs": "vitepress build docs", "build:docs": "vitepress build docs",
"build:ratingsleaderboard": "node scripts/generateRatingsLeaderboard", "build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"build": "node scripts/build --build=all", "build": "vite-node --script scripts/build --build=all",
"dev:bot": "yarn build:bot --dev && yarn start:bot", "dev:bot": "yarn build:bot --dev && yarn start:bot",
"dev:server": "yarn build:server --dev && yarn start:server", "dev:server": "yarn build:server --dev && yarn start:server",
"dev:docs": "vitepress dev docs", "dev:docs": "vitepress dev docs",
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs", "start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs", "start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
"host:docs": "vitepress preview docs", "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", "zip": "zip -r dist.zip dist",
"lint": "npx tsc", "lint": "npx tsc",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"" "format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\""
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/rest": "^20.0.1",
"@radix-ui/colors": "^0.1.8", "@radix-ui/colors": "^0.1.8",
"@resvg/resvg-js": "^2.4.1", "@resvg/resvg-js": "^2.4.1",
"bepaint": "^0.1.0-beta.3", "bepaint": "^0.1.0-beta.3",
"discord.js": "^14.9.0", "discord.js": "^14.12.0",
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"false": "^0.0.4",
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-escape": "^2.0.0", "markdown-escape": "^2.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"satori": "^0.9.1", "satori": "^0.9.1",
"vite-node": "^0.33.0",
"vitepress-plugin-auto-sidebar": "^1.1.0" "vitepress-plugin-auto-sidebar": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@octokit/rest": "^20.0.1",
"@total-typescript/ts-reset": "^0.4.2", "@total-typescript/ts-reset": "^0.4.2",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
+8 -9
View File
@@ -1,5 +1,5 @@
import { build } from 'esbuild'; import { BuildOptions, build } from 'esbuild';
import { copyFile, mkdir, rm } from 'fs/promises'; import { copyFile, mkdirSync, rmSync } from 'fs';
import { argv } from 'process'; import { argv } from 'process';
const isProd = !argv.includes('--dev'); const isProd = !argv.includes('--dev');
@@ -10,25 +10,26 @@ const buildServer = argv.includes('--build=server') || buildAll;
if (isProd) { if (isProd) {
// only remove in production to ensure no time is wasted in dev // only remove in production to ensure no time is wasted in dev
console.log('Removing dist...'); console.log('Removing dist...');
await rm('dist', { recursive: true, force: true }); rmSync('dist', { recursive: true, force: true });
console.log('Dist removed'); console.log('Dist removed');
console.log('Creating dist...'); console.log('Creating dist...');
await mkdir('dist'); mkdirSync('dist');
console.log('Dist created'); console.log('Dist created');
console.log('Copying package.json...'); 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'), console.log('package.json copied'),
); );
} }
const commonOptions = { const commonOptions: BuildOptions = {
platform: 'node', platform: 'node',
loader: { loader: {
'.node': 'copy', '.node': 'copy',
'.ttf': 'file', '.ttf': 'file',
}, },
format: 'cjs',
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
@@ -46,9 +47,7 @@ if (buildBot) {
entryPoints: ['src/bot.ts'], entryPoints: ['src/bot.ts'],
outfile: 'dist/bot.cjs', outfile: 'dist/bot.cjs',
}).then(() => { }).then(() => console.log('Bot built'));
console.log('Bot built');
});
} }
if (buildServer) { if (buildServer) {
@@ -1,6 +1,11 @@
import { Octokit } from '@octokit/rest'; import { Octokit } from '@octokit/rest';
import { rm, writeFile } from 'fs/promises'; import { rm, writeFile } from 'fs/promises';
import { argv, env } from 'process'; import { argv, env } from 'process';
import {
RatingsInfo,
RatingsNeighbors,
RatingsPlayer,
} from '../src/commands/ratings';
import commitMultipleFiles from './commitMultipleFiles.js'; import commitMultipleFiles from './commitMultipleFiles.js';
/* /*
@@ -8,11 +13,13 @@ import commitMultipleFiles from './commitMultipleFiles.js';
* Central European Time (UTC+1): use 0 23 * * *. * Central European Time (UTC+1): use 0 23 * * *.
* Central Asia Standard Time (UTC+7): use 0 17 * * *. * Central Asia Standard Time (UTC+7): use 0 17 * * *.
* *
* :region/ratings/:season/info.json: season info like rewards, start & end time, etc. * na/ratings/49/info.json: season info like rewards, start & end time, etc.
* :region/ratings/:season/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight * na/ratings/49/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/midnight.json: last midnight's leaderboard relative to the region
*/ */
export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[];
const publish = argv.includes('--publish'); const publish = argv.includes('--publish');
const latest = argv.includes('--latest'); const latest = argv.includes('--latest');
const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1]; const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
@@ -23,22 +30,22 @@ const NEIGHBORS = 1000;
const PLAYERS = await fetch( const PLAYERS = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
) )
.then((response) => response.json()) .then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => data.count); .then((data) => data.count);
const players = {}; const players: Record<number, RatingsPlayer> = {};
const leaderboard = []; const leaderboard: BlitzkriegRatingsLeaderboard = [];
let registered = 0; let registered = 0;
async function branchFromPlayer(id) { async function branchFromPlayer(id: number) {
const { neighbors } = await fetch( const { neighbors } = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`, `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 firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1]; const lastPlayer = neighbors[neighbors.length - 1];
const branches = []; const branches: Promise<void>[] = [];
if (!players[firstPlayer.number]) if (!players[firstPlayer.number])
branches.push(branchFromPlayer(firstPlayer.spa_id)); branches.push(branchFromPlayer(firstPlayer.spa_id));
if (!players[lastPlayer.number]) if (!players[lastPlayer.number])
@@ -61,10 +68,10 @@ async function branchFromPlayer(id) {
await Promise.all(branches); await Promise.all(branches);
} }
async function branchFromLeague(id) { async function branchFromLeague(id: number) {
const { result } = await fetch( const { result } = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`, `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 firstPlayer = result[0];
const lastPlayer = result[result.length - 1]; 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([ await Promise.all([
branchFromLeague(0), branchFromLeague(0),
branchFromLeague(1), branchFromLeague(1),
@@ -86,8 +93,13 @@ await Promise.all([
console.log('Converting to an array...'); console.log('Converting to an array...');
for (let index = 0; index < PLAYERS; index++) { for (let index = 0; index < PLAYERS; index++) {
leaderboard[index] = players[index + 1]; const listing = players[index + 1];
if (!leaderboard[index]) console.warn(`${index} had no player`); if (!listing) console.warn(`Position ${index + 1} had no player`);
leaderboard[index] = {
id: listing.spa_id,
score: listing.score,
};
} }
const leaderboardJSON = JSON.stringify(leaderboard); const leaderboardJSON = JSON.stringify(leaderboard);
@@ -97,14 +109,13 @@ if (publish) {
const info = await fetch( const info = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then((response) => response.json()); ).then((response) => response.json() as Promise<RatingsInfo>);
const time = Math.round(Date.now() / 1000);
const normalizedServer = server === 'na' ? 'com' : server; const normalizedServer = server === 'na' ? 'com' : server;
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`; const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${ const leaderboardPath = `${normalizedServer}/ratings/${info.current_season}/${
latest ? 'latest.json' : `leaderboards/${time}.json` latest ? 'latest' : 'midnight'
}`; }.json`;
const infoJSON = JSON.stringify(info); const infoJSON = JSON.stringify(info);
console.log(`Publishing to season ${info.current_season}...`); console.log(`Publishing to season ${info.current_season}...`);
@@ -113,7 +124,7 @@ if (publish) {
'tresabhi', 'tresabhi',
'blitzkrieg-db', 'blitzkrieg-db',
'main', 'main',
`${info.current_season}/${time}`, new Date().toString(),
[ [
{ path: infoPath, content: infoJSON }, { path: infoPath, content: infoJSON },
{ path: leaderboardPath, content: leaderboardJSON }, { path: leaderboardPath, content: leaderboardJSON },
@@ -1,10 +1,12 @@
import { Octokit } from '@octokit/rest';
export default async function commitMultipleFiles( export default async function commitMultipleFiles(
octokit, octokit: Octokit,
owner, owner: string,
repo, repo: string,
branch, branch: string,
message, message: string,
changes, changes: { path: string; content: string }[],
) { ) {
// Get the latest commit SHA on the specified branch // Get the latest commit SHA on the specified branch
const { data: refData } = await octokit.git.getRef({ const { data: refData } = await octokit.git.getRef({
@@ -31,12 +33,13 @@ export default async function commitMultipleFiles(
content: change.content, content: change.content,
encoding: 'utf-8', encoding: 'utf-8',
}); });
return { return {
sha: blobData.sha, sha: blobData.sha,
path: change.path, path: change.path,
mode: '100644', mode: '100644',
type: 'blob', type: 'blob',
}; } as const;
}), }),
); );
+5 -2
View File
@@ -1,5 +1,6 @@
import { Octokit } from '@octokit/rest';
import { Client, GatewayIntentBits } from 'discord.js'; 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 { registerErrorHandlers } from './events/error';
import guildMemberAdd from './events/guildMemberAdd'; import guildMemberAdd from './events/guildMemberAdd';
import interactionCreate from './events/interactionCreate'; import interactionCreate from './events/interactionCreate';
@@ -12,5 +13,7 @@ export const client = new Client({
.on('guildMemberAdd', guildMemberAdd) .on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate); .on('interactionCreate', interactionCreate);
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
registerErrorHandlers(); registerErrorHandlers();
client.login(DISCORD_TOKEN); client.login(secrets.DISCORD_TOKEN);
+1 -1
View File
@@ -12,7 +12,7 @@ const executionStart = new Date().getTime();
export const debugCommand: CommandRegistry = { export const debugCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
+4 -4
View File
@@ -12,7 +12,7 @@ import embedNegative from '../core/discord/embedNegative';
import embedPositive from '../core/discord/embedPositive'; import embedPositive from '../core/discord/embedPositive';
import markdownTable, { TableInput } from '../core/discord/markdownTable'; import markdownTable, { TableInput } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { PossiblyPromise } from '../types/possiblyPromise'; import { PossiblyPromise } from '../types/possiblyPromise';
@@ -26,7 +26,7 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
export const eligibleCommand: CommandRegistry = { export const eligibleCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: false, inPublic: false,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -49,9 +49,9 @@ export const eligibleCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const clan = interaction.options.getString('clan') as SkilledClan; const clan = interaction.options.getString('clan') as SkilledClan;
const { id, server } = await resolvePlayerFromCommand(interaction); const { id, region: server } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getWargamingResponse<AccountInfo>( 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 problems: TableInput = [];
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`; const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
+75 -68
View File
@@ -12,76 +12,83 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import evolution from '../renderers/evolution'; import evolution from '../renderers/evolution';
import { StatType } from '../renderers/fullStats'; import { StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const evolutionCommand: CommandRegistry = { export const evolutionCommand = new Promise<CommandRegistryRaw>(
inProduction: true, async (resolve) => {
inDevelopment: false, const command = await addStatTypeSubCommandGroups(
inPublic: true, new SlashCommandBuilder()
.setName('evolution')
command: addStatTypeSubCommandGroups( .setDescription('Evolution of statistics'),
new SlashCommandBuilder() false,
.setName('evolution')
.setDescription('Evolution of statistics'),
),
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.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')!),
); );
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
View File
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic'; import { CYCLIC_API } from '../constants/cyclic';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId'; import resolveTankId from '../core/blitz/resolveTankId';
@@ -12,73 +13,101 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import fullStats, { StatType } from '../renderers/fullStats'; import fullStats from '../renderers/fullStats';
import { MultiTankFilters, StatType } from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const fullStatsCommand: CommandRegistry = { export const fullStatsCommand = new Promise<CommandRegistryRaw>(
inProduction: true, async (resolve) => {
inDevelopment: false, const command = await addStatTypeSubCommandGroups(
inPublic: true, new SlashCommandBuilder()
.setName('full-stats')
command: addStatTypeSubCommandGroups( .setDescription('Full in-game statistics'),
new SlashCommandBuilder()
.setName('fullstats')
.setDescription('Full in-game statistics'),
),
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(player.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')!),
); );
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')!),
);
},
});
}, },
}; );
+5 -4
View File
@@ -30,7 +30,7 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
export const helpCommand: CommandRegistry = { export const helpCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: addDocsSubcommands( command: addDocsSubcommands(
@@ -44,13 +44,14 @@ export const helpCommand: CommandRegistry = {
), ),
), ),
handler(interaction) { async handler(interaction) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();
if (subcommand === 'commands') { if (subcommand === 'commands') {
return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${COMMANDS_RAW.filter( return `# Commands\n\nBlitzkrieg offers the following commands:\n\n${(
(registry) => registry.inPublic && registry.inProduction, await Promise.all(COMMANDS_RAW)
) )
.filter((registry) => registry.inPublic && registry.inProduction)
.map( .map(
(registry) => (registry) =>
`- \`/${registry.command.name}\`: ${registry.command.description}`, `- \`/${registry.command.name}\`: ${registry.command.description}`,
+7 -11
View File
@@ -4,12 +4,11 @@ import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy'; import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import getBlitzClan from '../core/blitz/getBlitzClan'; import getBlitzClan from '../core/blitz/getBlitzClan';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan'; import autocompleteClan from '../core/discord/autocompleteClan';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { ClanInfo } from '../types/clanInfo'; import { ClanInfo } from '../types/clanInfo';
@@ -18,7 +17,7 @@ const DEFAULT_THRESHOLD = 7;
export const inactiveCommand: CommandRegistry = { export const inactiveCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -42,14 +41,14 @@ export const inactiveCommand: CommandRegistry = {
const time = new Date().getTime() / 1000; const time = new Date().getTime() / 1000;
const clanData = ( const clanData = (
await getWargamingResponse<ClanInfo>( await getWargamingResponse<ClanInfo>(
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`, `https://api.wotblitz.${server}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
) )
)[id]; )[id];
const memberIds = clanData.members_ids; const memberIds = clanData.members_ids;
const accounts = await getWargamingResponse<AccountInfo>( const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${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 const inactive = memberIds
.map((memberId) => { .map((memberId) => {
@@ -69,11 +68,8 @@ export const inactiveCommand: CommandRegistry = {
<Wrapper> <Wrapper>
<TitleBar <TitleBar
name={clanData.name} name={clanData.name}
nameDiscriminator="(Inactivity)"
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`} image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
description={`${threshold}+ Days • ${new Date().toDateString()} • ${ description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
REGION_DOMAIN_NAMES[server]
}`}
/> />
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />} {!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
+101 -58
View File
@@ -4,20 +4,21 @@ import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import * as Tanks from '../components/Tanks'; import * as Tanks from '../components/Tanks';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import getTankStats from '../core/blitz/getTankStats'; import getTankStats from '../core/blitz/getTankStats';
import getTreeType from '../core/blitz/getTreeType'; import getTreeType from '../core/blitz/getTreeType';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName'; import resolveTankName from '../core/blitz/resolveTankName';
import { import {
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
TankopediaEntry,
Tier, Tier,
tankopedia, tankopedia,
tankopediaInfo,
} from '../core/blitz/tankopedia'; } from '../core/blitz/tankopedia';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
@@ -45,27 +46,27 @@ const COMP_TANKS = [
export const ownedTanksCommand: CommandRegistry = { export const ownedTanksCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('ownedtanks') .setName('owned-tanks')
.setDescription("Shows a player's owned tanks") .setDescription("Shows a player's owned tanks")
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('tier') .setName('tier')
.setDescription('The tier you want to see') .setDescription('The tier you want to see')
.setChoices( .setChoices(
{ name: 'Tier I', value: '1' }, { name: 'Tier I (1)', value: '1' },
{ name: 'Tier II', value: '2' }, { name: 'Tier II (2)', value: '2' },
{ name: 'Tier III', value: '3' }, { name: 'Tier III (3)', value: '3' },
{ name: 'Tier IV', value: '4' }, { name: 'Tier IV (4)', value: '4' },
{ name: 'Tier V', value: '5' }, { name: 'Tier V (5)', value: '5' },
{ name: 'Tier VI', value: '6' }, { name: 'Tier VI (6)', value: '6' },
{ name: 'Tier VII', value: '7' }, { name: 'Tier VII (7)', value: '7' },
{ name: 'Tier VIII', value: '8' }, { name: 'Tier VIII (8)', value: '8' },
{ name: 'Tier IX', value: '9' }, { name: 'Tier IX (9)', value: '9' },
{ name: 'Tier X', value: '10' }, { name: 'Tier X (10)', value: '10' },
) )
.setRequired(true), .setRequired(true),
) )
@@ -74,73 +75,101 @@ export const ownedTanksCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const tier = Number(interaction.options.getString('tier')); const tier = Number(interaction.options.getString('tier'));
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account; const { id, region: server } = account;
const accountInfo = await getWargamingResponse<AccountInfo>( 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 tankStats = await getTankStats(server, id);
const tanks = ( const filteredTanks = (
await Promise.all( await Promise.all(
tankStats.map(async (tankData) => ({ tankStats.map(async (tankData) => ({
...(await tankopedia)[tankData.tank_id], tankopedia: (await tankopedia)[tankData.tank_id]!,
tank_id: tankData.tank_id, tank_id: tankData.tank_id,
})), })),
) )
).filter((tank) => tank?.tier === tier); ).filter((tank) => tank.tankopedia?.tier === tier);
const clanData = await getWargamingResponse<PlayerClanData>( const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
); );
const leftColumnSize = Math.ceil(tanks.length / 2); const groupedTanks: Record<string, TankopediaEntry[]> = {};
const leftColumn = tanks.slice(0, leftColumnSize); const nations: string[] = [];
const rightColumn = tanks.slice(leftColumnSize);
filteredTanks.forEach((tank) => {
if (groupedTanks[tank.tankopedia.nation] === undefined) {
groupedTanks[tank.tankopedia.nation] = [tank.tankopedia];
nations.push(tank.tankopedia.nation);
} else {
groupedTanks[tank.tankopedia.nation].push(tank.tankopedia);
}
});
nations.sort();
return ( return (
<Wrapper> <Wrapper>
<TitleBar <TitleBar
name={accountInfo[id].nickname} name={accountInfo[id].nickname}
nameDiscriminator={`(Tier ${TIER_ROMAN_NUMERALS[tier as Tier]})`} nameDiscriminator={
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
}
image={ image={
clanData[id]?.clan clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined : undefined
} }
description={`Owned tanks • ${new Date().toDateString()} • ${ description={`Tier ${
REGION_DOMAIN_NAMES[server] TIER_ROMAN_NUMERALS[tier as Tier]
}`} } tanks • ${new Date().toDateString()}`}
/> />
{tanks.length === 0 && <NoData type={NoDataType.TanksFound} />} {filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
{tanks.length > 0 && ( {filteredTanks.length > 0 &&
<Tanks.Root> (await Promise.all(
<Tanks.Column> nations.map(async (nation) => {
{await Promise.all( const tanks = groupedTanks[nation];
leftColumn.map(async (tank) => ( const leftColumnSize = Math.ceil(tanks.length / 2);
<Tanks.Item const leftColumn = tanks.slice(0, leftColumnSize);
key={tank.tank_id} const rightColumn = tanks.slice(leftColumnSize);
name={await resolveTankName(tank.tank_id)}
tankType={tank.type} return (
image={tank.images?.normal} <Tanks.Root>
treeType={await getTreeType(tank.tank_id)} <Tanks.Title>
/> {(await tankopediaInfo).vehicle_nations[nation]}
)), </Tanks.Title>
)}
</Tanks.Column> <Tanks.Row>
<Tanks.Column> <Tanks.Column>
{await Promise.all( {await Promise.all(
rightColumn.map(async (tank) => ( leftColumn.map(async (tank) => (
<Tanks.Item <Tanks.Item
key={tank.tank_id} key={tank.tank_id}
name={await resolveTankName(tank.tank_id)} name={await resolveTankName(tank.tank_id)}
tankType={tank.type} tankType={tank.type}
image={tank.images?.normal} image={tank.images?.normal}
treeType={await getTreeType(tank.tank_id)} treeType={await getTreeType(tank.tank_id)}
/> />
)), )),
)} )}
</Tanks.Column> </Tanks.Column>
</Tanks.Root> <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} /> <PoweredBy type={PoweredByType.Wargaming} />
</Wrapper> </Wrapper>
@@ -149,3 +178,17 @@ export const ownedTanksCommand: CommandRegistry = {
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
}; };
// TODO: make this work in the future
// console.log('Caching tank images...');
// TANKS.then((tanks) =>
// jsxToSvg(
// <div style={{ display: 'flex' }}>
// {tanks.map(({ images, tank_id }) => {
// return (
// images.normal && <img key={tank_id} src={images.normal} width={0} />
// );
// })}
// </div>,
// ),
// ).then(() => console.log('Cached tank images'));
+2 -2
View File
@@ -1,9 +1,9 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const pingCommand: CommandRegistry<true> = { export const pingCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
handlesInteraction: true, handlesInteraction: true,
+6 -6
View File
@@ -7,7 +7,7 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import markdownTable, { TableInputEntry } from '../core/discord/markdownTable'; import markdownTable, { TableInputEntry } from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountAchievements } from '../types/accountAchievements'; import { AccountAchievements } from '../types/accountAchievements';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
@@ -16,11 +16,11 @@ type SortBy = 'name' | 'count';
export const playerAchievementsCommand: CommandRegistry = { export const playerAchievementsCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('playerachievements') .setName('player-achievements')
.setDescription("All the player's achievements") .setDescription("All the player's achievements")
.addStringOption(addUsernameChoices) .addStringOption(addUsernameChoices)
.addStringOption((option) => .addStringOption((option) =>
@@ -36,13 +36,13 @@ export const playerAchievementsCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy; const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account; const { id, region: server } = account;
const accounts = await getWargamingResponse<AccountInfo>( 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 = const accountsAchievements =
await getWargamingResponse<AccountAchievements>( 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 accountAchievements = accountsAchievements[id];
const compound = { const compound = {
+5 -5
View File
@@ -6,25 +6,25 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable'; import markdownTable from '../core/discord/markdownTable';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const playerInfoCommand: CommandRegistry = { export const playerInfoCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('playerinfo') .setName('player-info')
.setDescription('Basic information about a player') .setDescription('Basic information about a player')
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction); const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account; const { id, region: server } = account;
const accounts = await getWargamingResponse<AccountInfo>( 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]; const accountInfo = accounts[id];
+299
View File
@@ -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,
});
},
);
+9 -11
View File
@@ -1,23 +1,23 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; 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 getWargamingResponse from '../core/blitz/getWargamingResponse';
import addClanChoices from '../core/discord/addClanChoices'; 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 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 { CommandRegistry } from '../events/interactionCreate';
import { ClanList } from '../types/clanList'; import { ClanList } from '../types/clanList';
export const searchClansCommand: CommandRegistry = { export const searchClansCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('searchclans') .setName('search-clans')
.setDescription('Search clans') .setDescription('Search clans')
.addStringOption(addServerChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => addClanChoices(option).setAutocomplete(false)) .addStringOption((option) => addClanChoices(option).setAutocomplete(false))
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
@@ -28,17 +28,15 @@ export const searchClansCommand: CommandRegistry = {
), ),
async handler(interaction) { 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 clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const clanList = await getWargamingResponse<ClanList>( const clanList = await 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( return embedInfo(
`Clan search for "${markdownEscape(clan)}" in ${ `Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`,
REGION_DOMAIN_NAMES[server]
}`,
clanList.length === 0 clanList.length === 0
? 'No clans found.' ? 'No clans found.'
: `\`\`\`\n${clanList : `\`\`\`\n${clanList
+11 -13
View File
@@ -1,24 +1,24 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; 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 getWargamingResponse from '../core/blitz/getWargamingResponse';
import { usernamePattern } from '../core/blitz/listPlayers'; 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 addUsernameChoices from '../core/discord/addUsernameChoices';
import embedInfo from '../core/discord/embedInfo'; 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 { CommandRegistry } from '../events/interactionCreate';
import { AccountList } from '../types/accountList'; import { AccountList } from '../types/accountList';
export const searchPlayersCommand: CommandRegistry = { export const searchPlayersCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('searchplayers') .setName('search-players')
.setDescription('Search players in a Blitz server') .setDescription('Search players in a Blitz server')
.addStringOption(addServerChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true), addUsernameChoices(option).setAutocomplete(false).setRequired(true),
) )
@@ -31,22 +31,20 @@ export const searchPlayersCommand: CommandRegistry = {
), ),
async handler(interaction) { 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 name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim(); const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch) const players = usernamePattern.test(trimmedSearch)
? await getWargamingResponse<AccountList>( ? await getWargamingResponse<AccountList>(
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent( `https://api.wotblitz.${server}/wotb/account/list/?application_id=${
trimmedSearch, secrets.WARGAMING_APPLICATION_ID
)}&limit=${limit}`, }&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`,
) )
: []; : [];
return embedInfo( return embedInfo(
`Player search for "${markdownEscape(name)}" in ${ `Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`,
REGION_DOMAIN_NAMES[server]
}`,
`\`\`\`${ `\`\`\`${
players.length === 0 players.length === 0
? 'No players found.' ? 'No players found.'
+7 -6
View File
@@ -1,18 +1,18 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { TANK_NAMES } from '../core/blitz/tankopedia'; import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
import addTankChoices from '../core/discord/addTankChoices'; import addTankChoices from '../core/discord/addTankChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
export const searchTanksCommand: CommandRegistry = { export const searchTanksCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('searchtanks') .setName('search-tanks')
.setDescription('Search tanks') .setDescription('Search tanks')
.addStringOption((option) => addTankChoices(option).setAutocomplete(false)) .addStringOption((option) => addTankChoices(option).setAutocomplete(false))
.addIntegerOption((option) => .addIntegerOption((option) =>
@@ -26,9 +26,10 @@ export const searchTanksCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const tank = interaction.options.getString('tank')!; const tank = interaction.options.getString('tank')!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger('limit') ?? 25;
const results = go(tank, await TANK_NAMES, { limit }).map( const results = go(tank, await TANK_NAMES_DIACRITICS, {
(result) => result.target, limit,
); keys: ['combined'],
}).map((result) => result.obj.original);
return embedInfo( return embedInfo(
`Tank search for "${markdownEscape(tank)}"`, `Tank search for "${markdownEscape(tank)}"`,
+96 -62
View File
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../components/Tanks';
import { CYCLIC_API } from '../constants/cyclic'; import { CYCLIC_API } from '../constants/cyclic';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankId from '../core/blitz/resolveTankId'; import resolveTankId from '../core/blitz/resolveTankId';
@@ -12,74 +13,107 @@ import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand'; import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import { StatType } from '../renderers/fullStats'; import stats, { MultiTankFilters, StatType } from '../renderers/stats';
import stats from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const statsCommand: CommandRegistry = { export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
inProduction: true, const command = await addStatTypeSubCommandGroups(
inDevelopment: false,
inPublic: true,
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('stats') .setName('stats')
.setDescription('In-game statistics'), .setDescription('Regular battles statistics'),
), );
async handler(interaction) { resolve({
const commandGroup = interaction.options.getSubcommandGroup( inProduction: true,
true, inDevelopment: true,
) as StatType; inPublic: true,
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 [ command,
await stats(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
'BlitzStars',
),
];
},
autocomplete: (interaction) => { async handler(interaction) {
autocompleteUsername(interaction); const commandGroup = interaction.options.getSubcommandGroup(
autocompleteTanks(interaction); true,
}, ) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const resolvedPeriod = resolvePeriodFromCommand(
player.region,
interaction,
);
const period = interaction.options.getString('period');
const tankIdRaw = interaction.options.getString('tank')!;
const tankId =
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
period,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
async button(interaction) { return [
const url = new URL(`${CYCLIC_API}/${interaction.customId}`); await stats(
const path = url.pathname.split('/').filter(Boolean); commandGroup,
const commandGroup = path[1] as StatType; resolvedPeriod,
const player = await resolvePlayerFromButton(interaction); player,
const period = resolvePeriodFromButton(player.server, interaction); 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( autocomplete: (interaction) => {
commandGroup, autocompleteUsername(interaction);
period, autocompleteTanks(interaction);
player, },
parseInt(url.searchParams.get('tankId')!),
); 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
View File
@@ -8,14 +8,14 @@ import linkButton from '../core/discord/linkButton';
import primaryButton from '../core/discord/primaryButton'; import primaryButton from '../core/discord/primaryButton';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import today from '../renderers/today'; import today from '../renderers/today';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const todayCommand: CommandRegistry = { export const todayCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -24,30 +24,52 @@ export const todayCommand: CommandRegistry = {
.addStringOption(addUsernameChoices) .addStringOption(addUsernameChoices)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName('cutoff')
.setDescription( .setDescription(
'The maximum number of tanks to display (default: Infinity)', 'The maximum number of tanks to display (default: Infinity)',
) )
.setRequired(false) .setRequired(false)
.setMinValue(1), .setMinValue(1),
)
.addIntegerOption((option) =>
option
.setName('maximized')
.setDescription(
'The number of rows with full stats after which all are collapsed (default: 4)',
)
.setMinValue(0)
.setRequired(false),
)
.addBooleanOption((option) =>
option
.setName('show-total')
.setDescription('Show the total stats at the top (default: true)')
.setRequired(false),
), ),
async handler(interaction) { async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction); const player = await resolvePlayerFromCommand(interaction);
const limit = interaction.options.getInteger('limit') ?? Infinity; const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
const maximized = interaction.options.getInteger('maximized') ?? undefined;
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
const { nickname } = ( const { nickname } = (
await getWargamingResponse<AccountInfo>( await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.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]; )[player.id];
const path = interactionToURL(interaction, player); const path = interactionToURL(interaction, {
...player,
cutoff,
maximized,
'show-total': showTotal,
});
return [ return [
await today(player, limit, false), await today(player, cutoff, maximized, showTotal, false),
primaryButton(path, 'Refresh'), primaryButton(path, 'Refresh'),
linkButton(`${CYCLIC_API}/${path}`, 'Embed'), linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton( linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`, `https://www.blitzstars.com/player/${player.region}/${nickname}`,
'BlitzStars', 'BlitzStars',
), ),
]; ];
+5 -5
View File
@@ -7,14 +7,14 @@ import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative'; import embedNegative from '../core/discord/embedNegative';
import embedPositive from '../core/discord/embedPositive'; import embedPositive from '../core/discord/embedPositive';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; 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 { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
export const verifyCommand: CommandRegistry = { export const verifyCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -24,12 +24,12 @@ export const verifyCommand: CommandRegistry = {
async handler(interaction) { async handler(interaction) {
const blitzAccount = await resolvePlayerFromCommand(interaction); const blitzAccount = await resolvePlayerFromCommand(interaction);
const { id, server } = blitzAccount; const { id, region: server } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>( 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>( 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 clan = clanData[id]?.clan;
const clanTag = clan ? ` [${clan!.tag}]` : ''; const clanTag = clan ? ` [${clan!.tag}]` : '';
+76 -35
View File
@@ -1,6 +1,7 @@
import getWN8Percentile from '../../../core/blitz/getWN8Percentile'; import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
import { TankType } from '../../../core/blitz/tankopedia';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
import { RowDiscriminator } from './RowDiscriminator'; import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
import { RowStat } from './RowStat'; import { RowStat } from './RowStat';
export interface RowProps { export interface RowProps {
@@ -13,14 +14,14 @@ export interface RowProps {
careerDamage: number; careerDamage: number;
battles: number; battles: number;
careerBattles: number; careerBattles: number;
icon?: string;
minimized: boolean; minimized: boolean;
isListing: boolean; isTank: boolean;
naked?: boolean; treeType?: TreeTypeEnum;
tankType?: TankType;
} }
export function Row({ export function Row({
isListing, isTank,
minimized, minimized,
name, name,
winrate, winrate,
@@ -31,45 +32,85 @@ export function Row({
careerDamage, careerDamage,
battles, battles,
careerBattles, careerBattles,
icon, treeType,
naked, tankType,
}: RowProps) { }: RowProps) {
return ( return (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column',
borderRadius: 4, borderRadius: 4,
backgroundColor: isListing backgroundColor: theme.colors.appBackground2,
? theme.colors.componentInteractive overflow: 'hidden',
: theme.colors.appBackground2,
padding: 8,
}} }}
> >
<RowDiscriminator minimized={minimized} name={name} icon={icon} /> <div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 8,
gap: 4,
backgroundColor: theme.colors.componentInteractive,
}}
>
{isTank && treeType !== undefined && tankType !== undefined && (
<img
src={TREE_TYPE_ICONS[treeType][tankType]}
style={{ width: 16, height: 16 }}
/>
)}
<span
style={{
color: isTank
? treeType === TreeTypeEnum.Collector
? theme.colors.textLowContrast_blue
: treeType === TreeTypeEnum.Premium
? theme.colors.textLowContrast_amber
: theme.colors.textHighContrast
: theme.colors.textLowContrast,
fontWeight: 900,
fontSize: 16,
}}
>
{name}
</span>
</div>
<RowStat <div
minimized={minimized} style={{
name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`} display: 'flex',
value={`${(winrate * 100).toFixed(2)}%`} paddingTop: 8,
delta={winrate - careerWinrate} paddingBottom: 8,
/> }}
<RowStat >
minimized={minimized} <RowStat
name={`WN8 • ${careerWN8 === undefined ? '--' : careerWN8.toFixed(0)}`} minimized={minimized}
value={WN8 === undefined ? '--' : WN8.toFixed(0)} name={`Battles • ${careerBattles.toFixed(0)}`}
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)} value={battles.toFixed(0)}
/> />
<RowStat <RowStat
minimized={minimized} minimized={minimized}
name={`Damage • ${careerDamage.toFixed(0)}`} name={`Winrate • ${(careerWinrate * 100).toFixed(2)}%`}
value={damage.toFixed(0)} value={`${(winrate * 100).toFixed(2)}%`}
delta={damage - careerDamage} delta={winrate - careerWinrate}
/> />
<RowStat <RowStat
minimized={minimized} minimized={minimized}
name={`Battles • ${careerBattles.toFixed(0)}`} name={`WN8 • ${
value={battles.toFixed(0)} careerWN8 === undefined ? '--' : careerWN8.toFixed(0)
/> }`}
value={WN8 === undefined ? '--' : WN8.toFixed(0)}
percentile={WN8 === undefined ? undefined : getWN8Percentile(WN8)}
/>
<RowStat
minimized={minimized}
name={`Damage • ${careerDamage.toFixed(0)}`}
value={damage.toFixed(0)}
delta={damage - careerDamage}
/>
</div>
</div> </div>
); );
} }
@@ -1,51 +0,0 @@
import { theme } from '../../../stitches.config';
export interface RowDiscriminatorProps {
name: string;
icon?: string;
minimized: boolean;
}
export function RowDiscriminator({
name,
icon,
minimized,
}: RowDiscriminatorProps) {
return (
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
height: '100%',
boxSizing: 'border-box',
width: 0,
alignItems: 'center',
justifyContent: 'center',
}}
>
{!minimized && icon && (
<img
src={icon}
height={0}
style={{ flex: 1, width: '100%', objectFit: 'contain' }}
/>
)}
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
textAlign: 'center',
display: 'block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'clip',
maxWidth: '100%',
}}
>
{name}
</span>
</div>
);
}
@@ -53,6 +53,7 @@ export function RowStat({
<span <span
style={{ style={{
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold',
fontSize: 16, fontSize: 16,
}} }}
> >
+1 -1
View File
@@ -1,4 +1,4 @@
export * from './components/Root'; export * from './components/Root';
export * from './components/Row'; export * from './components/Row';
export * from './components/RowDiscriminator';
export * from './components/RowStat'; export * from './components/RowStat';
+1 -1
View File
@@ -96,7 +96,7 @@ export default function GenericAllStats({
]} ]}
/> />
<Card <Card
title="Efficiency" title="Miscellaneous"
items={[ items={[
{ {
label: 'Average tier', label: 'Average tier',
+16 -14
View File
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
import { Margin, MarginOrientation } from './Margin'; import { Margin, MarginOrientation } from './Margin';
export const GRAPH_WIDTH = 544; export const GRAPH_WIDTH = 384;
export const GRAPH_HEIGHT = 320; export const GRAPH_HEIGHT = 320;
export interface MarginInputProps { export interface MarginInputProps {
@@ -20,7 +20,7 @@ export interface RootProps {
separations?: number; separations?: number;
xMinLabel: string; xMinLabel: string;
xMaxLabel: string; xMaxLabel: string;
xTitle: string; xTitle?: string;
} }
export function Root({ export function Root({
@@ -91,18 +91,20 @@ export function Root({
/> />
)} )}
<div {xTitle !== undefined && (
style={{ <div
paddingLeft: 64, style={{
display: 'flex', paddingLeft: 64,
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
}} justifyContent: 'center',
> }}
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}> >
{xTitle} <span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
</span> {xTitle}
</div> </span>
</div>
)}
</div> </div>
); );
} }
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
export * from './components/Gap';
export * from './components/Item';
export * from './components/Root';
+7 -2
View File
@@ -24,8 +24,13 @@ const POWERED_BY_NAMES: Record<PoweredByType, string> = {
export default function PoweredBy({ type, footer }: PoweredByProps) { export default function PoweredBy({ type, footer }: PoweredByProps) {
return ( return (
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div
<div style={{ display: 'flex' }}>{footer}</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 }}> <div style={{ display: 'flex', gap: 4 }}>
<img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} /> <img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} />
+13 -10
View File
@@ -2,32 +2,35 @@ import {
TANK_ICONS, TANK_ICONS,
TANK_ICONS_COLLECTOR, TANK_ICONS_COLLECTOR,
TANK_ICONS_PREMIUM, TANK_ICONS_PREMIUM,
TankType,
} from '../../../core/blitz/tankopedia'; } from '../../../core/blitz/tankopedia';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
export enum TreeType { export enum TreeTypeEnum {
TechTree, TechTree,
Premium, Premium,
Collector, Collector,
} }
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
export interface ItemProps { export interface ItemProps {
image?: string; image?: string;
tankType?: string; tankType?: TankType;
name: string; name: string;
treeType: TreeType; treeType: TreeTypeEnum;
} }
const TREE_TYPE_COLOR = { const TREE_TYPE_COLOR = {
[TreeType.TechTree]: '', [TreeTypeEnum.TechTree]: '',
[TreeType.Premium]: '_amber', [TreeTypeEnum.Premium]: '_amber',
[TreeType.Collector]: '_blue', [TreeTypeEnum.Collector]: '_blue',
} as const; } as const;
const TREE_TYPE_ICONS = { export const TREE_TYPE_ICONS = {
[TreeType.TechTree]: TANK_ICONS, [TreeTypeEnum.TechTree]: TANK_ICONS,
[TreeType.Premium]: TANK_ICONS_PREMIUM, [TreeTypeEnum.Premium]: TANK_ICONS_PREMIUM,
[TreeType.Collector]: TANK_ICONS_COLLECTOR, [TreeTypeEnum.Collector]: TANK_ICONS_COLLECTOR,
}; };
export function Item({ image, tankType, name, treeType }: ItemProps) { export function Item({ image, tankType, name, treeType }: ItemProps) {
+2 -1
View File
@@ -9,7 +9,8 @@ export function Root({ children }: RootProps) {
<div <div
style={{ style={{
display: 'flex', display: 'flex',
gap: 8, flexDirection: 'column',
gap: 16,
}} }}
> >
{children} {children}
+18
View File
@@ -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>
);
}
+19
View File
@@ -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>
);
}
+2
View File
@@ -1,3 +1,5 @@
export * from './components/Column'; export * from './components/Column';
export * from './components/Item'; export * from './components/Item';
export * from './components/Root'; export * from './components/Root';
export * from './components/Row';
export * from './components/Title';
+12 -15
View File
@@ -1,4 +1,3 @@
import isDev from '../core/node/isDev';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
export interface TitleBarProps { export interface TitleBarProps {
@@ -19,10 +18,11 @@ export default function TitleBar({
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 16, justifyContent: 'center',
overflow: 'hidden',
}} }}
> >
<div style={{ display: 'flex', gap: 8, flex: 1 }}> <div style={{ display: 'flex', gap: 8 }}>
{image && ( {image && (
<img <img
style={{ width: 64, height: 64, objectFit: 'contain' }} style={{ width: 64, height: 64, objectFit: 'contain' }}
@@ -31,13 +31,18 @@ export default function TitleBar({
)} )}
<div <div
style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }} style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 4,
}}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
gap: 4, gap: 4,
overflow: 'hidden',
}} }}
> >
<span <span
@@ -46,6 +51,8 @@ export default function TitleBar({
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 900, fontWeight: 900,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
overflow: 'hidden',
}} }}
> >
{name} {name}
@@ -53,7 +60,6 @@ export default function TitleBar({
{nameDiscriminator && ( {nameDiscriminator && (
<span <span
style={{ style={{
flex: 1,
fontSize: 32, fontSize: 32,
color: theme.colors.textLowContrast, color: theme.colors.textLowContrast,
fontWeight: 900, fontWeight: 900,
@@ -70,15 +76,6 @@ export default function TitleBar({
</span> </span>
</div> </div>
</div> </div>
<img
style={{ width: 64, height: 64 }}
src={
isDev()
? 'https://i.imgur.com/j6TItdy.png'
: 'https://i.imgur.com/pP6RN0x.png'
}
/>
</div> </div>
); );
} }
+1 -1
View File
@@ -14,7 +14,7 @@ export default function Wrapper({ children, naked }: WrapperProps) {
flexDirection: 'column', flexDirection: 'column',
padding: 16, padding: 16,
gap: 32, gap: 32,
width: 640, width: 480,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
...(!naked && { ...(!naked && {
+9 -2
View File
@@ -1,12 +1,19 @@
export type RegionDomain = 'com' | 'eu' | 'asia'; export type Region = 'com' | 'eu' | 'asia';
export type RegionSubdomain = 'na' | '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', com: 'North America',
eu: 'Europe', eu: 'Europe',
asia: 'Asia', asia: 'Asia',
}; };
// https://planetarynames.wr.usgs.gov/Abbreviations
export const REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA',
eu: 'EU',
asia: 'AS',
};
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = { export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
na: 'North America', na: 'North America',
eu: 'Europe', eu: 'Europe',
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand'; import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand';
import throwError from '../node/throwError'; import throwError from '../node/throwError';
import listClans from './listClans'; import listClans from './listClans';
@@ -6,7 +6,7 @@ import listClans from './listClans';
export default async function getBlitzClan(clan: string) { export default async function getBlitzClan(clan: string) {
if (serverAndIdPattern.test(clan)) { if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/'); const [server, accountId] = clan.split('/');
return { server: server as RegionDomain, id: Number(accountId) }; return { server: server as Region, id: Number(accountId) };
} else { } else {
const accounts = await listClans(clan); const accounts = await listClans(clan);
+12
View File
@@ -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>;
}
+5 -5
View File
@@ -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 { TanksStats } from '../../types/tanksStats';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import throwError from '../node/throwError'; import throwError from '../node/throwError';
import getWargamingResponse from './getWargamingResponse'; 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>( 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) { if (tankStats[id] === null) {
throw throwError( throw throwError(
'No tank stats available', '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.`,
); );
} }
+4 -4
View File
@@ -1,11 +1,11 @@
import { TreeType } from '../../components/Tanks'; import { TreeTypeEnum } from '../../components/Tanks';
import { tankopedia } from './tankopedia'; import { tankopedia } from './tankopedia';
export default async function getTreeType(id: number) { export default async function getTreeType(id: number) {
const entry = (await tankopedia)[id]; const entry = (await tankopedia)[id];
if (entry?.is_collectible) return TreeType.Collector; if (entry?.is_collectible) return TreeTypeEnum.Collector;
if (entry?.is_premium) return TreeType.Premium; if (entry?.is_premium) return TreeTypeEnum.Premium;
return TreeType.TechTree; return TreeTypeEnum.TechTree;
} }
+8 -8
View File
@@ -1,6 +1,6 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { Clan, ClanList } from '../../types/clanList'; import { Clan, ClanList } from '../../types/clanList';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse'; import getWargamingResponse from './getWargamingResponse';
export type ClanListWithServer = (Clan & { export type ClanListWithServer = (Clan & {
@@ -15,33 +15,33 @@ export default async function listClans(search: string, limit = 9) {
return ( return (
await Promise.all([ await Promise.all([
getWargamingResponse<ClanList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'com' as RegionDomain, server: 'com' as Region,
})), })),
), ),
getWargamingResponse<ClanList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'eu' as RegionDomain, server: 'eu' as Region,
})), })),
), ),
getWargamingResponse<ClanList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'asia' as RegionDomain, server: 'asia' as Region,
})), })),
), ),
]) ])
+8 -8
View File
@@ -1,6 +1,6 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { Account, AccountList } from '../../types/accountList'; import { Account, AccountList } from '../../types/accountList';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse'; import getWargamingResponse from './getWargamingResponse';
export type AccountListWithServer = (Account & { export type AccountListWithServer = (Account & {
@@ -19,33 +19,33 @@ export default async function listPlayers(search: string, limit = 9) {
return ( return (
await Promise.all([ await Promise.all([
getWargamingResponse<AccountList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'com' as RegionDomain, server: 'com' as Region,
})), })),
), ),
getWargamingResponse<AccountList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'eu' as RegionDomain, server: 'eu' as Region,
})), })),
), ),
getWargamingResponse<AccountList>( 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( ).then(
(value) => (value) =>
value && value &&
value.map((account) => ({ value.map((account) => ({
...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;
}
+40 -4
View File
@@ -1,3 +1,9 @@
import { deburr } from 'lodash';
import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse';
export type TankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
export interface TankopediaEntry { export interface TankopediaEntry {
name: string; name: string;
nation: string; nation: string;
@@ -7,7 +13,7 @@ export interface TankopediaEntry {
cost: { price_credit: number; price_gold: number }; cost: { price_credit: number; price_gold: number };
images: { preview: string; normal: string }; images: { preview: string; normal: string };
tank_id: number; tank_id: number;
type: string; type: TankType;
description: string; description: string;
} }
@@ -31,22 +37,52 @@ export const TANKS = new Promise<TankopediaEntry[]>(async (resolve) => {
export const TANK_NAMES = new Promise<string[]>(async (resolve) => { export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
resolve((await entries).map(({ name }) => name)); resolve((await entries).map(({ name }) => name));
}); });
export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) =>
Promise.all(
tankNames.map(async (tankName, index) => {
const 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', 'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
lightTank: 'https://i.imgur.com/CSNha5V.png', lightTank: 'https://i.imgur.com/CSNha5V.png',
mediumTank: 'https://i.imgur.com/wvf3ltm.png', mediumTank: 'https://i.imgur.com/wvf3ltm.png',
heavyTank: 'https://i.imgur.com/ECeqlZa.png', heavyTank: 'https://i.imgur.com/ECeqlZa.png',
}; };
export const TANK_ICONS_PREMIUM: Record<string, string> = { export const TANK_ICONS_PREMIUM: Record<TankType, string> = {
'AT-SPG': 'https://i.imgur.com/TCu3EdR.png', 'AT-SPG': 'https://i.imgur.com/TCu3EdR.png',
lightTank: 'https://i.imgur.com/zdkpTRb.png', lightTank: 'https://i.imgur.com/zdkpTRb.png',
mediumTank: 'https://i.imgur.com/3z7eHX6.png', mediumTank: 'https://i.imgur.com/3z7eHX6.png',
heavyTank: 'https://i.imgur.com/P3vbmyA.png', heavyTank: 'https://i.imgur.com/P3vbmyA.png',
}; };
export const TANK_ICONS_COLLECTOR: Record<string, string> = { export const TANK_ICONS_COLLECTOR: Record<TankType, string> = {
'AT-SPG': 'https://i.imgur.com/WTjeirB.png', 'AT-SPG': 'https://i.imgur.com/WTjeirB.png',
lightTank: 'https://i.imgur.com/EwhtKkU.png', lightTank: 'https://i.imgur.com/EwhtKkU.png',
mediumTank: 'https://i.imgur.com/u8YDMBh.png', mediumTank: 'https://i.imgur.com/u8YDMBh.png',
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { AllStats } from '../../types/accountInfo'; import { AllStats } from '../../types/accountInfo';
import getTankHistories, { import getTankHistories, {
TankHistories, TankHistories,
@@ -35,7 +35,7 @@ export const emptyTankHistoryNode: TankHistoryRaw = {
}; };
export default async function getDiffedTankStats( export default async function getDiffedTankStats(
server: RegionDomain, server: Region,
id: number, id: number,
start: number, start: number,
end: number, end: number,
+4 -13
View File
@@ -1,16 +1,7 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { Period } from '../discord/addPeriodSubCommands'; import { PeriodSize } from '../discord/addPeriodSubCommands';
import getTimeDaysAgo from './getTimeDaysAgo'; import getTimeDaysAgo from './getTimeDaysAgo';
export default function getPeriodStart( export default function getPeriodStart(region: Region, period: PeriodSize) {
server: RegionDomain, return period === 'career' ? 0 : getTimeDaysAgo(region, parseInt(period));
period: Exclude<Period, 'custom'>,
) {
if (period === 'career') {
return -Infinity;
} else if (period === 'today') {
return getTimeDaysAgo(server, 1);
} else {
return getTimeDaysAgo(server, parseInt(period));
}
} }
+5 -5
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { AccountInfo, AllStats } from '../../types/accountInfo'; import { AccountInfo, AllStats } from '../../types/accountInfo';
import { import {
GetHistoriesOptions, GetHistoriesOptions,
@@ -7,7 +7,7 @@ import {
getHistoriesDefaultOptions, getHistoriesDefaultOptions,
} from '../../types/histories'; } from '../../types/histories';
import getWargamingResponse from '../blitz/getWargamingResponse'; import getWargamingResponse from '../blitz/getWargamingResponse';
import { WARGAMING_APPLICATION_ID } from '../node/arguments'; import { secrets } from '../node/secrets';
import { emptyAllStats } from './getDiffedTankStats'; import { emptyAllStats } from './getDiffedTankStats';
export interface PlayerHistoryRaw { export interface PlayerHistoryRaw {
@@ -25,13 +25,13 @@ export interface PlayerHistoryRaw {
account_id: number; account_id: number;
[key: string]: number; [key: string]: number;
}; };
region: RegionDomain; region: Region;
} }
export type PlayerHistoriesRaw = PlayerHistoryRaw[]; export type PlayerHistoriesRaw = PlayerHistoryRaw[];
export default async function getPlayerHistories( export default async function getPlayerHistories(
server: RegionDomain, server: Region,
id: number, id: number,
options?: Partial<GetHistoriesOptions>, options?: Partial<GetHistoriesOptions>,
) { ) {
@@ -67,7 +67,7 @@ export default async function getPlayerHistories(
statistics: { all }, statistics: { all },
} = ( } = (
await getWargamingResponse<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}`,
) )
)[id]; )[id];
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { import {
GetHistoriesOptions, GetHistoriesOptions,
Histories, Histories,
@@ -32,7 +32,7 @@ export const getTankHistoriesDefaultOptions: GetTankHistoriesOptions = {
}; };
export default async function getTankHistories( export default async function getTankHistories(
server: RegionDomain, server: Region,
id: number, id: number,
options?: Partial<GetTankHistoriesOptions>, options?: Partial<GetTankHistoriesOptions>,
) { ) {
+4 -4
View File
@@ -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 com: -5, // Central North American Time
eu: +1, // Central European Time eu: +1, // Central European Time
asia: +7, // Central Asia Standard 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(); const now = new Date();
if (daysAgo === 0) now.getTime() / 1000; if (daysAgo === 0) now.getTime() / 1000;
const time = new Date(); const time = new Date();
time.setUTCHours(-TIME_ZONE_MAPPINGS[server], 0, 0, 0); time.setUTCHours(-TIME_ZONE_MAPPINGS[region], 0, 0, 0);
if (time > now) time.setUTCDate(time.getUTCDate() - 1); if (time > now) time.setUTCDate(time.getUTCDate() - 1);
time.setUTCDate(time.getUTCDate() - daysAgo + 1); time.setUTCDate(time.getUTCDate() - daysAgo + 1);
@@ -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');
}
}
+24 -19
View File
@@ -2,9 +2,9 @@ import {
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder, SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { PERIOD_NAMES } from '../discord/resolvePeriodFromCommand';
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom'; export type PeriodType = 'period' | 'custom';
export type PeriodSize = `${number}` | 'career';
export default function addPeriodSubCommands( export default function addPeriodSubCommands(
option: SlashCommandSubcommandGroupBuilder, option: SlashCommandSubcommandGroupBuilder,
@@ -14,36 +14,41 @@ export default function addPeriodSubCommands(
) { ) {
return option return option
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('today').setDescription(PERIOD_NAMES.today)), extra(
) option
.addSubcommand((option) => .setName('period' satisfies PeriodType)
extra(option.setName('30').setDescription(PERIOD_NAMES[30])), .setDescription("Preset period's statistics")
) .addStringOption((option) =>
.addSubcommand((option) => option
extra(option.setName('60').setDescription(PERIOD_NAMES[60])), .setName('period')
) .setDescription('A preset period')
.addSubcommand((option) => .setChoices(
extra(option.setName('90').setDescription(PERIOD_NAMES[90])), { name: 'Today', value: '1' satisfies PeriodSize },
) { name: '30 days', value: '30' satisfies PeriodSize },
.addSubcommand((option) => { name: '60 days', value: '60' satisfies PeriodSize },
extra(option.setName('career').setDescription(PERIOD_NAMES.career)), { name: '90 days', value: '90' satisfies PeriodSize },
{ name: 'Career', value: 'career' satisfies PeriodSize },
)
.setRequired(true),
),
),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra( extra(
option option
.setName('custom') .setName('custom' satisfies PeriodType)
.setDescription(PERIOD_NAMES.custom) .setDescription("Custom period's statistics")
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('start') .setName('start')
.setDescription('Start of period in days ago from today') .setDescription('Days ago from today')
.setMinValue(0) .setMinValue(0)
.setRequired(true), .setRequired(true),
) )
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('end') .setName('end')
.setDescription('End of period in days ago from today') .setDescription('Days ago from today')
.setMinValue(0) .setMinValue(0)
.setRequired(true), .setRequired(true),
), ),
+14
View File
@@ -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);
}
-14
View File
@@ -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 { SlashCommandBuilder } from 'discord.js';
import { TreeTypeString } from '../../components/Tanks';
import { StatType } from '../../renderers/stats';
import { tankopediaInfo } from '../blitz/tankopedia';
import addPeriodSubCommands from './addPeriodSubCommands'; import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices'; import addTankChoices from './addTankChoices';
import addUsernameChoices from './addUsernameChoices'; import addUsernameChoices from './addUsernameChoices';
export default function addStatTypeSubCommandGroups( export default async function addStatTypeSubCommandGroups(
option: SlashCommandBuilder, option: SlashCommandBuilder,
addMultiTank = true,
) { ) {
return option const awaitedTankopediaInfo = await tankopediaInfo;
option
.addSubcommandGroup((option) => .addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) => addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices), option.addStringOption(addUsernameChoices),
) )
.setName('player') .setName('player' satisfies StatType)
.setDescription("Player's statistics"), .setDescription('Player statistics'),
) )
.addSubcommandGroup((option) => .addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) => addPeriodSubCommands(option, (option) =>
@@ -20,7 +26,70 @@ export default function addStatTypeSubCommandGroups(
.addStringOption(addTankChoices) .addStringOption(addTankChoices)
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
) )
.setName('tank') .setName('tank' satisfies StatType)
.setDescription("Tank's statistics"), .setDescription('Tank statistics'),
); );
if (addMultiTank)
option.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addUsernameChoices)
.addStringOption((option) => {
option
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
);
return option;
})
.addStringOption((option) =>
option
.setName('tank-type')
.setDescription('Type')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
)
.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 -2
View File
@@ -3,7 +3,7 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_DOMAIN_NAMES } from '../../constants/regions'; import { REGION_NAMES } from '../../constants/regions';
import listClans from '../blitz/listClans'; import listClans from '../blitz/listClans';
export default async function autocompleteClan( export default async function autocompleteClan(
@@ -11,6 +11,7 @@ export default async function autocompleteClan(
) { ) {
const focusedOption = interaction.options.getFocused(true); const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'clan') return; if (focusedOption.name !== 'clan') return;
if (focusedOption.value.length < 2) return interaction.respond([]);
const players = await listClans(focusedOption.value); const players = await listClans(focusedOption.value);
await interaction.respond( await interaction.respond(
@@ -18,7 +19,7 @@ export default async function autocompleteClan(
? players.map( ? players.map(
(player) => (player) =>
({ ({
name: `${player.name} (${REGION_DOMAIN_NAMES[player.server]})`, name: `${player.name} (${REGION_NAMES[player.server]})`,
value: `${player.server}/${player.clan_id}`, value: `${player.server}/${player.clan_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
) )
+6 -9
View File
@@ -4,8 +4,7 @@ import {
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import resolveTankName from '../blitz/resolveTankName'; import { TANKS, TANK_NAMES_DIACRITICS } from '../blitz/tankopedia';
import { TANKS } from '../blitz/tankopedia';
export default async function autocompleteTanks( export default async function autocompleteTanks(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
@@ -16,16 +15,14 @@ export default async function autocompleteTanks(
await interaction.respond( await interaction.respond(
focusedOption.value focusedOption.value
? await Promise.all( ? await Promise.all(
go(focusedOption.value, await TANKS, { go(focusedOption.value, await TANK_NAMES_DIACRITICS, {
keys: ['name'], keys: ['combined'],
limit: 10, limit: 10,
}).map( }).map(
async (item) => async (item, index) =>
({ ({
name: item.obj.name name: item.obj.original,
? item.obj.name value: `${(await TANKS)[index].tank_id}`,
: await resolveTankName(item.obj.tank_id),
value: `${item.obj.tank_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
), ),
) )
+2 -4
View File
@@ -3,7 +3,7 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_DOMAIN_NAMES } from '../../constants/regions'; import { REGION_NAMES } from '../../constants/regions';
import listPlayers from '../blitz/listPlayers'; import listPlayers from '../blitz/listPlayers';
export default async function autocompleteUsername( export default async function autocompleteUsername(
@@ -19,9 +19,7 @@ export default async function autocompleteUsername(
? players.map( ? players.map(
(player) => (player) =>
({ ({
name: `${player.nickname} (${ name: `${player.nickname} (${REGION_NAMES[player.server]})`,
REGION_DOMAIN_NAMES[player.server]
})`,
value: `${player.server}/${player.account_id}`, value: `${player.server}/${player.account_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
) )
+1 -6
View File
@@ -19,12 +19,7 @@ export default function interactionToURL(
] ]
.filter(Boolean) .filter(Boolean)
.join('/'); .join('/');
const params = [ const params = (parameters ? Object.entries(parameters) : [])
...interaction.options.data.map(
(option) => [option.name, option.value] as const,
),
...Object.entries(parameters ?? {}),
]
.filter(([, value]) => value !== undefined && value !== null) .filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join('&'); .join('&');
+2 -2
View File
@@ -1,10 +1,10 @@
import { ButtonInteraction, CacheType } from 'discord.js'; import { ButtonInteraction, CacheType } from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic'; import { CYCLIC_API } from '../../constants/cyclic';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import resolvePeriodFromURL from '../express/resolvePeriodFromURL'; import resolvePeriodFromURL from '../express/resolvePeriodFromURL';
export default function resolvePeriodFromButton( export default function resolvePeriodFromButton(
server: RegionDomain, server: Region,
interaction: ButtonInteraction<CacheType>, interaction: ButtonInteraction<CacheType>,
) { ) {
return resolvePeriodFromURL(server, `${CYCLIC_API}/${interaction.customId}`); return resolvePeriodFromURL(server, `${CYCLIC_API}/${interaction.customId}`);
+20 -33
View File
@@ -1,27 +1,13 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzstars/getPeriodNow'; import getPeriodNow from '../blitzstars/getPeriodNow';
import getPeriodStart from '../blitzstars/getPeriodStart'; import getPeriodStart from '../blitzstars/getPeriodStart';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo'; import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { Period } from '../discord/addPeriodSubCommands'; import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
export const PERIOD_NAMES: Record<Period, string> = { export function getPeriodOptionName(period: PeriodSize) {
today: "Today's statistics", return period === 'career' ? 'Career' : `${period}-day`;
30: "30 days' statistics", }
60: "60 days' statistics",
90: "90 days' statistics",
career: 'Career statistics',
custom: 'Custom period',
};
export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = {
today: "Today's evolution",
30: "30 days' evolution",
60: "60 days' evolution",
90: "90 days' evolution",
career: 'Career evolution',
custom: 'Custom period',
};
export interface ResolvedPeriod { export interface ResolvedPeriod {
statsName: string; statsName: string;
@@ -31,29 +17,30 @@ export interface ResolvedPeriod {
} }
export default function resolvePeriodFromCommand( export default function resolvePeriodFromCommand(
server: RegionDomain, region: Region,
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) { ) {
let statsName: string; let statsName: string;
let evolutionName: string; let evolutionName: string;
let start: number; let start: number;
let end: number; let end: number;
const period = interaction.options.getSubcommand() as Period; const periodSubcommand = interaction.options.getSubcommand() as PeriodType;
const periodOption = interaction.options.getString('period') as PeriodSize;
if (period === 'custom') { if (periodSubcommand === 'custom') {
const startRaw = interaction.options.getInteger('start')!; const startRaw = interaction.options.getInteger('start', true);
const endRaw = interaction.options.getInteger('end')!; const endRaw = interaction.options.getInteger('end', true);
const startMin = Math.min(startRaw, endRaw); const startDaysAgoMin = Math.min(startRaw, endRaw);
const endMax = Math.max(startRaw, endRaw); const endDaysAgoMax = Math.max(startRaw, endRaw);
statsName = `${startMin} to ${endMax} days' statistics`; statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
evolutionName = `${startMin} to ${endMax} days' evolution`; evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
start = getTimeDaysAgo(server, startMin); start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(server, endMax); end = getTimeDaysAgo(region, startDaysAgoMin);
} else { } else {
statsName = PERIOD_NAMES[period]; statsName = `${getPeriodOptionName(periodOption)} Statistics`;
evolutionName = EVOLUTION_PERIOD_NAMES[period]; evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
start = getPeriodStart(server, period); start = getPeriodStart(region, periodOption);
end = getPeriodNow(); end = getPeriodNow();
} }
+6 -5
View File
@@ -1,6 +1,7 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import listPlayers, { import listPlayers, {
usernamePattern,
usernamePatternWithoutPosition, usernamePatternWithoutPosition,
} from '../blitz/listPlayers'; } from '../blitz/listPlayers';
import throwError from '../node/throwError'; import throwError from '../node/throwError';
@@ -8,7 +9,7 @@ import throwError from '../node/throwError';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export interface ResolvedPlayer { export interface ResolvedPlayer {
server: RegionDomain; region: Region;
id: number; id: number;
} }
@@ -19,7 +20,7 @@ export default async function resolvePlayerFromCommand(
const displayName = interaction.guild?.members.cache const displayName = interaction.guild?.members.cache
.get(interaction.user.id) .get(interaction.user.id)
?.displayName.match(usernamePatternWithoutPosition)?.[0]; ?.displayName.match(usernamePatternWithoutPosition)?.[0];
const username = commandUsername ?? displayName; const username = commandUsername?.match(usernamePattern)?.[0] ?? displayName;
if (username === undefined) { if (username === undefined) {
throw throwError( throw throwError(
@@ -31,7 +32,7 @@ export default async function resolvePlayerFromCommand(
if (serverAndIdPattern.test(username)) { if (serverAndIdPattern.test(username)) {
const [server, accountId] = username.split('/'); const [server, accountId] = username.split('/');
return { return {
server: server as RegionDomain, region: server as Region,
id: Number(accountId), id: Number(accountId),
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
@@ -39,7 +40,7 @@ export default async function resolvePlayerFromCommand(
if (accounts[0]) { if (accounts[0]) {
return { return {
server: accounts[0].server, region: accounts[0].server,
id: accounts[0].account_id, id: accounts[0].account_id,
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
+3 -3
View File
@@ -1,11 +1,11 @@
import { Request } from 'express'; import { Request } from 'express';
import { CYCLIC_API } from '../../constants/cyclic'; import { CYCLIC_API } from '../../constants/cyclic';
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import resolvePeriodFromURL from './resolvePeriodFromURL'; import resolvePeriodFromURL from './resolvePeriodFromURL';
export default function resolvePeriodFromRequest( export default function resolvePeriodFromRequest(
server: RegionDomain, server: Region,
request: Request, request: Request,
) { ) {
return resolvePeriodFromURL(server, `${CYCLIC_API}${request.path}`); return resolvePeriodFromURL(server, `${CYCLIC_API}${request.url}`);
} }
+17 -22
View File
@@ -1,16 +1,15 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzstars/getPeriodNow'; import getPeriodNow from '../blitzstars/getPeriodNow';
import getPeriodStart from '../blitzstars/getPeriodStart'; import getPeriodStart from '../blitzstars/getPeriodStart';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo'; import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { Period } from '../discord/addPeriodSubCommands'; import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
import { import {
EVOLUTION_PERIOD_NAMES,
PERIOD_NAMES,
ResolvedPeriod, ResolvedPeriod,
getPeriodOptionName,
} from '../discord/resolvePeriodFromCommand'; } from '../discord/resolvePeriodFromCommand';
export default function resolvePeriodFromURL( export default function resolvePeriodFromURL(
server: RegionDomain, server: Region,
urlString: string, urlString: string,
) { ) {
let statsName: string; let statsName: string;
@@ -19,29 +18,25 @@ export default function resolvePeriodFromURL(
let end: number; let end: number;
const url = new URL(urlString); const url = new URL(urlString);
const path = url.pathname.split('/').filter(Boolean); const path = url.pathname.split('/').filter(Boolean);
const period = path[path.length - 1] as Period; const periodSubcommand = path[path.length - 1] as PeriodType;
const periodOption = url.searchParams.get('period') as PeriodSize;
if (period === 'custom') { if (periodSubcommand === 'custom') {
const startRaw = parseInt(url!.searchParams.get('start')!); const startRaw = parseInt(url!.searchParams.get('start')!);
const endRaw = parseInt(url!.searchParams.get('end')!); const endRaw = parseInt(url!.searchParams.get('end')!);
const startMin = Math.min(startRaw, endRaw); const startDaysAgoMin = Math.min(startRaw, endRaw);
const endMax = Math.max(startRaw, endRaw); const endDaysAgoMax = Math.max(startRaw, endRaw);
statsName = `${startMin} to ${endMax} days' statistics`; statsName = `${startDaysAgoMin} to ${endDaysAgoMax} days' statistics`;
evolutionName = `${startMin} to ${endMax} days' evolution`; evolutionName = `${startDaysAgoMin} to ${endDaysAgoMax} days' evolution`;
start = getTimeDaysAgo(server, startMin); start = getTimeDaysAgo(server, endDaysAgoMax);
end = getTimeDaysAgo(server, endMax); end = getTimeDaysAgo(server, startDaysAgoMin);
} else { } else {
statsName = PERIOD_NAMES[period]; statsName = `${getPeriodOptionName(periodOption)} Statistics`;
evolutionName = EVOLUTION_PERIOD_NAMES[period]; evolutionName = `${getPeriodOptionName(periodOption)} Evolution`;
start = getPeriodStart(server, period); start = getPeriodStart(server, periodOption);
end = getPeriodNow(); end = getPeriodNow();
} }
return { return { statsName, evolutionName, start, end } satisfies ResolvedPeriod;
statsName,
evolutionName,
start,
end,
} satisfies ResolvedPeriod;
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { RegionDomain } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { ResolvedPlayer } from '../discord/resolvePlayerFromCommand'; import { ResolvedPlayer } from '../discord/resolvePlayerFromCommand';
export default function resolvePlayerFromURL(urlString: string) { export default function resolvePlayerFromURL(urlString: string) {
@@ -6,6 +6,6 @@ export default function resolvePlayerFromURL(urlString: string) {
return { return {
id: parseInt(url.searchParams.get('id')!), id: parseInt(url.searchParams.get('id')!),
server: url.searchParams.get('server') as RegionDomain, region: url.searchParams.get('region') as Region,
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} }
-9
View File
@@ -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}');
}
+1 -1
View File
@@ -32,7 +32,7 @@ Promise.all(
export default async function jsxToSvg(jsx: JSX.Element) { export default async function jsxToSvg(jsx: JSX.Element) {
return await satori(jsx, { return await satori(jsx, {
width: 640, width: 480,
fonts: [ fonts: [
{ data: roboto, name: FONT_NAME, weight: 400 }, { data: roboto, name: FONT_NAME, weight: 400 },
{ data: robotoBold, name: FONT_NAME, weight: 700 }, { data: robotoBold, name: FONT_NAME, weight: 700 },
+21
View File
@@ -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>;
+1 -1
View File
@@ -15,7 +15,7 @@ export default async function (member: GuildMember) {
.setColor(ACCENT_COLOR) .setColor(ACCENT_COLOR)
.setTitle(`Welcome ${member.user.username}`) .setTitle(`Welcome ${member.user.username}`)
.setDescription( .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 { AutocompleteInteraction, CacheType } from 'discord.js';
import { commands } from '..'; import { commands } from '..';
export default function handleAutocomplete( export default async function handleAutocomplete(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
) { ) {
const command = commands[interaction.commandName]; const command = (await commands)[interaction.commandName];
if (!command) { if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`); console.error(`No command matching ${interaction.commandName} was found.`);
@@ -11,7 +11,7 @@ export default async function handleButton(
const url = new URL(`${CYCLIC_API}/${interaction.customId}`); const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const commandName = url.pathname.split('/')[1]; const commandName = url.pathname.split('/')[1];
const button = commands[commandName]?.button; const button = (await commands)[commandName]?.button;
if (!button) { if (!button) {
throwError(`Button handler not found for "${commandName}"`); throwError(`Button handler not found for "${commandName}"`);
@@ -6,6 +6,7 @@ import {
ChatInputCommandInteraction, ChatInputCommandInteraction,
} from 'discord.js'; } from 'discord.js';
import { commands } from '..'; import { commands } from '..';
import discord from '../../../../discord.json' assert { type: 'json' };
import embedNegative from '../../../core/discord/embedNegative'; import embedNegative from '../../../core/discord/embedNegative';
import embedWarning from '../../../core/discord/embedWarning'; import embedWarning from '../../../core/discord/embedWarning';
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
@@ -15,12 +16,25 @@ import { handleError } from '../../error';
export default async function handleChatInputCommand( export default async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) { ) {
const registry = commands[interaction.commandName]; const registry = (await commands)[interaction.commandName];
console.log(interaction.toString()); console.log(interaction.toString());
await interaction.deferReply(); await interaction.deferReply();
try { 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); const returnable = await registry.handler(interaction);
if (registry.handlesInteraction) return; if (registry.handlesInteraction) return;
+72 -52
View File
@@ -23,15 +23,16 @@ import { ownedTanksCommand } from '../../commands/ownedTanks';
import { pingCommand } from '../../commands/ping'; import { pingCommand } from '../../commands/ping';
import { playerAchievementsCommand } from '../../commands/playerAchievements'; import { playerAchievementsCommand } from '../../commands/playerAchievements';
import { playerInfoCommand } from '../../commands/playerInfo'; import { playerInfoCommand } from '../../commands/playerInfo';
import { ratingsCommand } from '../../commands/ratings';
import { searchClansCommand } from '../../commands/searchClans'; import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers'; import { searchPlayersCommand } from '../../commands/searchPlayers';
import { searchTanksCommand } from '../../commands/searchTanks'; import { searchTanksCommand } from '../../commands/searchTanks';
import { statsCommand } from '../../commands/stats'; import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today'; import { todayCommand } from '../../commands/today';
import { verifyCommand } from '../../commands/verify'; import { verifyCommand } from '../../commands/verify';
import { DISCORD_TOKEN } from '../../core/node/arguments';
import getClientId from '../../core/node/getClientId'; import getClientId from '../../core/node/getClientId';
import isDev from '../../core/node/isDev'; import isDev from '../../core/node/isDev';
import { secrets } from '../../core/node/secrets';
import handleAutocomplete from './handlers/autocomplete'; import handleAutocomplete from './handlers/autocomplete';
import handleButton from './handlers/button'; import handleButton from './handlers/button';
import handleChatInputCommand from './handlers/chatInputCommand'; import handleChatInputCommand from './handlers/chatInputCommand';
@@ -53,23 +54,32 @@ export interface Registry {
inProduction: boolean; inProduction: boolean;
} }
export interface CommandRegistry<HandlesInteraction extends boolean = boolean> export type CommandRegistryRaw = Registry & {
extends Registry {
inPublic: boolean; inPublic: boolean;
handlesInteraction?: HandlesInteraction; inPreview?: boolean;
command: command:
| SlashCommandBuilder | SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>; | Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
) => HandlesInteraction extends true ? void : InteractionReturnable;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void; autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable; button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
} } & (
| {
handlesInteraction: true;
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
}
| {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable;
}
);
export type CommandRegistry = CommandRegistryRaw | Promise<CommandRegistryRaw>;
const rest = new REST().setToken(DISCORD_TOKEN); const rest = new REST().setToken(secrets.DISCORD_TOKEN);
export const COMMANDS_RAW: CommandRegistry[] = [ export const COMMANDS_RAW: CommandRegistry[] = [
debugCommand, debugCommand,
@@ -88,13 +98,21 @@ export const COMMANDS_RAW: CommandRegistry[] = [
pingCommand, pingCommand,
evolutionCommand, evolutionCommand,
statsCommand, statsCommand,
ratingsCommand,
]; ];
export const commands: Record<string, CommandRegistry> = COMMANDS_RAW.reduce(
(accumulator, registry) => { export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
if (isDev()) registry.command.setDefaultMemberPermissions(0); rawCommands.reduce<Record<string, CommandRegistryRaw>>(
return { ...accumulator, [registry.command.name]: registry }; (accumulator, registry) => {
}, if (isDev()) registry.command.setDefaultMemberPermissions(0);
{},
return {
...accumulator,
[registry.command.name]: registry,
};
},
{},
),
); );
export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
@@ -102,48 +120,50 @@ export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
[]; [];
Object.entries(commands).forEach(([, registry]) => { commands.then((awaitedCommands) => {
if (isDev() ? registry.inDevelopment : registry.inProduction) { Object.entries(awaitedCommands).forEach(([, registry]) => {
const json = registry.command.toJSON(); if (isDev() ? registry.inDevelopment : registry.inProduction) {
const json = registry.command.toJSON();
if (registry.inPublic) { if (registry.inPublic) {
publicCommands.push(json); publicCommands.push(json);
} else { } else {
guildCommands.push(json); guildCommands.push(json);
}
} }
});
try {
console.log(`Refreshing ${publicCommands.length} public command(s).`);
rest
.put(Routes.applicationCommands(getClientId()), { body: publicCommands })
.then((publicData) => {
console.log(
`Successfully refreshed ${
(publicData as RESTPostAPIChatInputApplicationCommandsJSONBody[])
.length
} public command(s).`,
);
});
console.log(`Refreshing ${guildCommands.length} guild command(s).`);
rest
.put(
Routes.applicationGuildCommands(getClientId(), discord.tres_guild_id),
{ body: guildCommands },
)
.then((guildData) => {
console.log(
`Successfully refreshed ${
(guildData as unknown[]).length
} guild command(s).`,
);
});
} catch (error) {
console.error(error);
} }
}); });
try {
console.log(`Refreshing ${publicCommands.length} public command(s).`);
rest
.put(Routes.applicationCommands(getClientId()), { body: publicCommands })
.then((publicData) => {
console.log(
`Successfully refreshed ${
(publicData as RESTPostAPIChatInputApplicationCommandsJSONBody[])
.length
} public command(s).`,
);
});
console.log(`Refreshing ${guildCommands.length} guild command(s).`);
rest
.put(
Routes.applicationGuildCommands(getClientId(), discord.tres_guild_id),
{ body: guildCommands },
)
.then((guildData) => {
console.log(
`Successfully refreshed ${
(guildData as unknown[]).length
} guild command(s).`,
);
});
} catch (error) {
console.error(error);
}
export default async function interactionCreate( export default async function interactionCreate(
interaction: Interaction<CacheType>, interaction: Interaction<CacheType>,
) { ) {
+7 -11
View File
@@ -3,7 +3,6 @@ import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy'; import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName'; import resolveTankName from '../core/blitz/resolveTankName';
import { tankopedia } from '../core/blitz/tankopedia'; import { tankopedia } from '../core/blitz/tankopedia';
@@ -11,16 +10,16 @@ import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
import getTankHistories from '../core/blitzstars/getTankHistories'; import getTankHistories from '../core/blitzstars/getTankHistories';
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand'; import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand'; 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 { AccountInfo } from '../types/accountInfo';
import { Histories } from '../types/histories'; import { Histories } from '../types/histories';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
import { StatType } from './fullStats'; import { StatType } from './stats';
export default async function evolution<Type extends StatType>( export default async function evolution<Type extends StatType>(
type: Type, type: Type,
{ start, end, evolutionName }: ResolvedPeriod, { start, end, evolutionName }: ResolvedPeriod,
{ server, id }: ResolvedPlayer, { region: server, id }: ResolvedPlayer,
tankId: Type extends 'tank' ? number : null, tankId: Type extends 'tank' ? number : null,
) { ) {
let nameDiscriminator: string | undefined; let nameDiscriminator: string | undefined;
@@ -29,7 +28,7 @@ export default async function evolution<Type extends StatType>(
if (type === 'player') { if (type === 'player') {
const clan = ( const clan = (
await getWargamingResponse<PlayerClanData>( 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; )[id]?.clan;
@@ -79,7 +78,7 @@ export default async function evolution<Type extends StatType>(
const minX = Math.min(...xs); const minX = Math.min(...xs);
const accountInfo = await getWargamingResponse<AccountInfo>( 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 ( return (
@@ -88,20 +87,17 @@ export default async function evolution<Type extends StatType>(
name={accountInfo[id].nickname} name={accountInfo[id].nickname}
nameDiscriminator={nameDiscriminator} nameDiscriminator={nameDiscriminator}
image={image} image={image}
description={`${evolutionName} • ${new Date().toDateString()} • ${ description={`${evolutionName} • ${new Date().toDateString()}`}
REGION_DOMAIN_NAMES[server]
}`}
/> />
{/* goofy ahh bug forces me to call them as functions */} {/* goofy ahh bug forces me to call them as functions */}
{plot.length > 0 && ( {plot.length > 0 && (
<Graph.Root <Graph.Root
xTitle="Battles"
verticalMargin={{ verticalMargin={{
min: minY, min: minY,
max: maxY, max: maxY,
suffix: '%', suffix: '%',
precision: 1, precision: 2,
}} }}
horizontalMargin={{ horizontalMargin={{
min: minX, min: minX,
+84 -76
View File
@@ -4,7 +4,6 @@ import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TierWeights, { TierWeightsRecord } from '../components/TierWeights'; import TierWeights, { TierWeightsRecord } from '../components/TierWeights';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import calculateWN8 from '../core/blitz/calculateWN8'; import calculateWN8 from '../core/blitz/calculateWN8';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName'; import resolveTankName from '../core/blitz/resolveTankName';
@@ -14,22 +13,24 @@ import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
import { tankAverages } from '../core/blitzstars/tankAverages'; import { tankAverages } from '../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand'; import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand'; import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments'; import { secrets } from '../core/node/secrets';
import { import {
AccountInfo, AccountInfo,
AllStats, AllStats,
SupplementaryStats, SupplementaryStats,
} from '../types/accountInfo'; } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
import { PossiblyPromise } from '../types/possiblyPromise'; import { MultiTankFilters, StatType } from './stats';
export type StatType = 'player' | 'tank';
export default async function fullStats<Type extends StatType>( export default async function fullStats<Type extends StatType>(
type: Type, type: Type,
{ start, end, statsName }: ResolvedPeriod, { start, end, statsName }: ResolvedPeriod,
{ server, id }: ResolvedPlayer, { region: server, id }: ResolvedPlayer,
tankId: Type extends 'tank' ? number : null, filters: Type extends 'tank'
? number
: Type extends 'multi-tank'
? Partial<MultiTankFilters>
: null,
) { ) {
let nameDiscriminator: string | undefined; let nameDiscriminator: string | undefined;
let image: string | undefined; let image: string | undefined;
@@ -37,7 +38,7 @@ export default async function fullStats<Type extends StatType>(
if (type === 'player') { if (type === 'player') {
const clan = ( const clan = (
await getWargamingResponse<PlayerClanData>( 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; )[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` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined; : undefined;
} else if (type === 'tank') { } else if (type === 'tank') {
nameDiscriminator = `(${await resolveTankName(tankId!)})`; nameDiscriminator = `(${await resolveTankName(filters as number)})`;
image = (await tankopedia)[tankId!]?.images.normal; image = (await tankopedia)[filters as number]?.images.normal;
} else {
} }
const { diffed } = await getDiffedTankStats(server, id, start, end); const { diffed, order } = await getDiffedTankStats(server, id, start, end);
let stats: AllStats; const awaitedTankopedia = await tankopedia;
let stats: AllStats | undefined;
let supplementaryStats: SupplementaryStats; let supplementaryStats: SupplementaryStats;
let tierWeights: TierWeightsRecord; let tierWeights: TierWeightsRecord;
if (type === 'player') { if (type === 'player' || type === 'multi-tank') {
const entries = Object.entries(diffed); const typedFilters = filters as Partial<MultiTankFilters> | null;
stats = sumStats(entries.map(([, stats]) => stats)); const filteredOrder = order.filter(
const battlesOfTanksWithAverages = await entries.reduce< (id) =>
PossiblyPromise<number> typedFilters === null ||
>(async (accumulator, [tankIdString, stats]) => { ((typedFilters.nation === undefined ||
const tankId = parseInt(tankIdString); awaitedTankopedia[id]?.nation === typedFilters.nation) &&
const tankAverage = (await tankAverages)[tankId]; (typedFilters['tank-type'] === undefined ||
awaitedTankopedia[id]?.type === typedFilters['tank-type']) &&
return tankAverage ? (await accumulator) + stats.battles : accumulator; (typedFilters.tier === undefined ||
}, 0); awaitedTankopedia[id]?.tier === typedFilters.tier) &&
const battlesOfTanksWithTankopediaEntry = await entries.reduce< (typedFilters['tree-type'] === undefined ||
PossiblyPromise<number> (typedFilters['tree-type'] === 'collector' &&
>(async (accumulator, [tankIdString, stats]) => { awaitedTankopedia[id]?.is_collectible) ||
const tankId = parseInt(tankIdString); (typedFilters['tree-type'] === 'premium' &&
const tankopediaEntry = (await tankopedia)[tankId]; awaitedTankopedia[id]?.is_premium) ||
(typedFilters['tree-type'] === 'techtree' &&
return tankopediaEntry !awaitedTankopedia[id]?.is_collectible &&
? (await accumulator) + stats.battles !awaitedTankopedia[id]?.is_premium))),
: accumulator; );
}, 0); const awaitedTankAverages = await tankAverages;
stats = sumStats(filteredOrder.map((id) => diffed[id]));
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id]
? accumulator + diffed[id].battles
: accumulator,
0,
);
const battlesOfTanksWithTankopediaEntry = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankopedia[id] ? accumulator + diffed[id].battles : accumulator,
0,
);
supplementaryStats = { supplementaryStats = {
WN8: WN8:
(await entries.reduce<PossiblyPromise<number>>( filteredOrder.reduce<number>(
async (accumulator, [tankIdString, stats]) => { (accumulator, id) =>
const tankId = parseInt(tankIdString); awaitedTankAverages[id]
const tankAverage = (await tankAverages)[tankId]; ? accumulator +
calculateWN8(awaitedTankAverages[id].all, diffed[id]) *
return tankAverage diffed[id].battles
? (await accumulator) + : accumulator,
calculateWN8(tankAverage.all, stats) * stats.battles
: accumulator;
},
0, 0,
)) / battlesOfTanksWithAverages, ) / battlesOfTanksWithAverages,
tier: tier:
(await entries.reduce<PossiblyPromise<number>>( filteredOrder.reduce<number>(
async (accumulator, [tankIdString, stats]) => { (accumulator, id) =>
const tankId = parseInt(tankIdString); awaitedTankopedia[id]
const tankopediaEntry = (await tankopedia)[tankId]; ? accumulator + awaitedTankopedia[id]!.tier * diffed[id].battles
: accumulator,
return tankopediaEntry
? (await accumulator) + tankopediaEntry.tier * stats.battles
: accumulator;
},
0, 0,
)) / battlesOfTanksWithTankopediaEntry, ) / battlesOfTanksWithTankopediaEntry,
}; };
tierWeights = await entries.reduce<PossiblyPromise<TierWeightsRecord>>( tierWeights = filteredOrder.reduce<TierWeightsRecord>((accumulator, id) => {
async (accumulator, [tankIdString, stats]) => { const tankopediaEntry = awaitedTankopedia[id];
const tankId = parseInt(tankIdString);
const tankopediaEntry = (await tankopedia)[tankId];
if (!tankopediaEntry) return accumulator; if (!tankopediaEntry) return accumulator;
const tier = tankopediaEntry.tier as Tier; const tier = tankopediaEntry.tier as Tier;
if ((await accumulator)[tier]) { if (accumulator[tier]) {
(await accumulator)[tier]! += stats.battles; accumulator[tier]! += diffed[id].battles;
} else { } else {
(await accumulator)[tier] = stats.battles; accumulator[tier] = diffed[id].battles;
} }
return accumulator; return accumulator;
}, }, {});
{},
);
} else { } else {
stats = diffed[tankId!]; stats = diffed[(filters as number)!];
supplementaryStats = { supplementaryStats = {
WN8: calculateWN8((await tankAverages)[tankId!].all, diffed[tankId!]), WN8: stats
tier: (await tankopedia)[tankId!]?.tier, ? calculateWN8(
(await tankAverages)[(filters as number)!].all,
diffed[(filters as number)!],
)
: undefined,
tier: (await tankopedia)[(filters as number)!]?.tier,
}; };
} }
const accountInfo = await getWargamingResponse<AccountInfo>( 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 ( return (
@@ -142,9 +152,7 @@ export default async function fullStats<Type extends StatType>(
name={accountInfo[id].nickname} name={accountInfo[id].nickname}
nameDiscriminator={nameDiscriminator} nameDiscriminator={nameDiscriminator}
image={image} image={image}
description={`${statsName} • ${new Date().toDateString()} • ${ description={`${statsName} • ${new Date().toDateString()}`}
REGION_DOMAIN_NAMES[server]
}`}
/> />
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />} {stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
+81 -83
View File
@@ -1,20 +1,19 @@
import AllStatsOverview from '../components/AllStatsOverview'; import AllStatsOverview from '../components/AllStatsOverview';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy'; import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import { TierWeightsRecord } from '../components/TierWeights'; import { TreeTypeString } from '../components/Tanks';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import calculateWN8 from '../core/blitz/calculateWN8'; import calculateWN8 from '../core/blitz/calculateWN8';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName'; import resolveTankName from '../core/blitz/resolveTankName';
import sumStats from '../core/blitz/sumStats'; import sumStats from '../core/blitz/sumStats';
import { Tier, tankopedia } from '../core/blitz/tankopedia'; import { tankopedia } from '../core/blitz/tankopedia';
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats'; import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
import { tankAverages } from '../core/blitzstars/tankAverages'; import { tankAverages } from '../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand'; import { ResolvedPeriod } from '../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand'; 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 { theme } from '../stitches.config';
import { import {
AccountInfo, AccountInfo,
@@ -22,15 +21,25 @@ import {
SupplementaryStats, SupplementaryStats,
} from '../types/accountInfo'; } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
import { PossiblyPromise } from '../types/possiblyPromise';
export type StatType = 'player' | 'tank'; export type StatType = 'player' | 'tank' | 'multi-tank';
export interface MultiTankFilters {
nation: string;
'tank-type': string;
tier: number;
'tree-type': TreeTypeString;
}
export default async function stats<Type extends StatType>( export default async function stats<Type extends StatType>(
type: Type, type: Type,
{ start, end, statsName }: ResolvedPeriod, { start, end, statsName }: ResolvedPeriod,
{ server, id }: ResolvedPlayer, { region: server, id }: ResolvedPlayer,
tankId: Type extends 'tank' ? number : null, filters: Type extends 'tank'
? number
: Type extends 'multi-tank'
? Partial<MultiTankFilters>
: null,
naked = false, naked = false,
) { ) {
let nameDiscriminator: string | undefined; let nameDiscriminator: string | undefined;
@@ -39,7 +48,7 @@ export default async function stats<Type extends StatType>(
if (type === 'player') { if (type === 'player') {
const clan = ( const clan = (
await getWargamingResponse<PlayerClanData>( 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; )[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` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined; : undefined;
} else if (type === 'tank') { } else if (type === 'tank') {
nameDiscriminator = `(${await resolveTankName(tankId!)})`; nameDiscriminator = `(${await resolveTankName(filters as number)})`;
image = (await tankopedia)[tankId!]?.images.normal; image = (await tankopedia)[filters as number]?.images.normal;
} else {
} }
const { diffed } = await getDiffedTankStats(server, id, start, end); const { diffed, order } = await getDiffedTankStats(server, id, start, end);
const awaitedTankopedia = await tankopedia;
let stats: AllStats | undefined; let stats: AllStats | undefined;
let supplementaryStats: SupplementaryStats; let supplementaryStats: SupplementaryStats;
let tierWeights: TierWeightsRecord;
if (type === 'player') { if (type === 'player' || type === 'multi-tank') {
const entries = Object.entries(diffed); const typedFilters = filters as Partial<MultiTankFilters> | null;
stats = sumStats(entries.map(([, stats]) => stats)); const filteredOrder = order.filter(
const battlesOfTanksWithAverages = await entries.reduce< (id) =>
PossiblyPromise<number> typedFilters === null ||
>(async (accumulator, [tankIdString, stats]) => { ((typedFilters.nation === undefined ||
const tankId = parseInt(tankIdString); awaitedTankopedia[id]?.nation === typedFilters.nation) &&
const tankAverage = (await tankAverages)[tankId]; (typedFilters['tank-type'] === undefined ||
awaitedTankopedia[id]?.type === typedFilters['tank-type']) &&
return tankAverage ? (await accumulator) + stats.battles : accumulator; (typedFilters.tier === undefined ||
}, 0); awaitedTankopedia[id]?.tier === typedFilters.tier) &&
const battlesOfTanksWithTankopediaEntry = await entries.reduce< (typedFilters['tree-type'] === undefined ||
PossiblyPromise<number> (typedFilters['tree-type'] === 'collector' &&
>(async (accumulator, [tankIdString, stats]) => { awaitedTankopedia[id]?.is_collectible) ||
const tankId = parseInt(tankIdString); (typedFilters['tree-type'] === 'premium' &&
const tankopediaEntry = (await tankopedia)[tankId]; awaitedTankopedia[id]?.is_premium) ||
(typedFilters['tree-type'] === 'techtree' &&
return tankopediaEntry !awaitedTankopedia[id]?.is_collectible &&
? (await accumulator) + stats.battles !awaitedTankopedia[id]?.is_premium))),
: accumulator; );
}, 0); const awaitedTankAverages = await tankAverages;
stats = sumStats(filteredOrder.map((id) => diffed[id]));
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankAverages[id]
? accumulator + diffed[id].battles
: accumulator,
0,
);
const battlesOfTanksWithTankopediaEntry = filteredOrder.reduce<number>(
(accumulator, id) =>
awaitedTankopedia[id] ? accumulator + diffed[id].battles : accumulator,
0,
);
supplementaryStats = { supplementaryStats = {
WN8: WN8:
(await entries.reduce<PossiblyPromise<number>>( filteredOrder.reduce<number>(
async (accumulator, [tankIdString, stats]) => { (accumulator, id) =>
const tankId = parseInt(tankIdString); awaitedTankAverages[id]
const tankAverage = (await tankAverages)[tankId]; ? accumulator +
calculateWN8(awaitedTankAverages[id].all, diffed[id]) *
return tankAverage diffed[id].battles
? (await accumulator) + : accumulator,
calculateWN8(tankAverage.all, stats) * stats.battles
: accumulator;
},
0, 0,
)) / battlesOfTanksWithAverages, ) / battlesOfTanksWithAverages,
tier: tier:
(await entries.reduce<PossiblyPromise<number>>( filteredOrder.reduce<number>(
async (accumulator, [tankIdString, stats]) => { (accumulator, id) =>
const tankId = parseInt(tankIdString); awaitedTankopedia[id]
const tankopediaEntry = (await tankopedia)[tankId]; ? accumulator + awaitedTankopedia[id]!.tier * diffed[id].battles
: accumulator,
return tankopediaEntry
? (await accumulator) + tankopediaEntry.tier * stats.battles
: accumulator;
},
0, 0,
)) / battlesOfTanksWithTankopediaEntry, ) / battlesOfTanksWithTankopediaEntry,
}; };
tierWeights = await entries.reduce<PossiblyPromise<TierWeightsRecord>>(
async (accumulator, [tankIdString, stats]) => {
const tankId = parseInt(tankIdString);
const tankopediaEntry = (await tankopedia)[tankId];
if (!tankopediaEntry) return accumulator;
const tier = tankopediaEntry.tier as Tier;
if ((await accumulator)[tier]) {
(await accumulator)[tier]! += stats.battles;
} else {
(await accumulator)[tier] = stats.battles;
}
return accumulator;
},
{},
);
} else { } else {
stats = diffed[tankId!]; stats = diffed[(filters as number)!];
supplementaryStats = { supplementaryStats = {
WN8: stats WN8: stats
? calculateWN8((await tankAverages)[tankId!].all, diffed[tankId!]) ? calculateWN8(
(await tankAverages)[(filters as number)!].all,
diffed[(filters as number)!],
)
: undefined, : undefined,
tier: (await tankopedia)[tankId!]?.tier, tier: (await tankopedia)[(filters as number)!]?.tier,
}; };
} }
const accountInfo = await getWargamingResponse<AccountInfo>( 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 = ( const overview = (
<AllStatsOverview stats={stats} supplementaryStats={supplementaryStats} /> <AllStatsOverview stats={stats} supplementaryStats={supplementaryStats} />
@@ -151,7 +151,7 @@ export default async function stats<Type extends StatType>(
gap: 4, gap: 4,
}} }}
> >
<u>/statsfull</u> for all statistics <u>/full-stats</u> for all statistics
</span> </span>
); );
@@ -161,11 +161,9 @@ export default async function stats<Type extends StatType>(
<Wrapper> <Wrapper>
<TitleBar <TitleBar
name={accountInfo[id].nickname} name={accountInfo[id].nickname}
nameDiscriminator={nameDiscriminator}
image={image} image={image}
description={`${statsName} • ${new Date().toDateString()} • ${ nameDiscriminator={nameDiscriminator}
REGION_DOMAIN_NAMES[server] description={`${statsName} • ${new Date().toDateString()}`}
}`}
/> />
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />} {!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
+63 -57
View File
@@ -3,9 +3,9 @@ import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy'; import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_DOMAIN_NAMES } from '../constants/regions';
import calculateWN8 from '../core/blitz/calculateWN8'; import calculateWN8 from '../core/blitz/calculateWN8';
import getTankStats from '../core/blitz/getTankStats'; import getTankStats from '../core/blitz/getTankStats';
import getTreeType from '../core/blitz/getTreeType';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import resolveTankName from '../core/blitz/resolveTankName'; import resolveTankName from '../core/blitz/resolveTankName';
import sumStats from '../core/blitz/sumStats'; import sumStats from '../core/blitz/sumStats';
@@ -15,16 +15,20 @@ import getPeriodNow from '../core/blitzstars/getPeriodNow';
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo'; import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
import { tankAverages } from '../core/blitzstars/tankAverages'; import { tankAverages } from '../core/blitzstars/tankAverages';
import { ResolvedPlayer } from '../core/discord/resolvePlayerFromCommand'; 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 { AccountInfo, AllStats } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
import { PossiblyPromise } from '../types/possiblyPromise'; import { PossiblyPromise } from '../types/possiblyPromise';
export default async function today( export default async function today(
{ server, id }: ResolvedPlayer, { region: server, id }: ResolvedPlayer,
limit = Infinity, cutoff = Infinity,
maximized = 4,
showTotal = true,
naked?: boolean, naked?: boolean,
) { ) {
console.log(maximized);
const { diffed, order } = await getDiffedTankStats( const { diffed, order } = await getDiffedTankStats(
server, server,
id, id,
@@ -32,15 +36,17 @@ export default async function today(
getPeriodNow(), getPeriodNow(),
); );
const accountInfo = await getWargamingResponse<AccountInfo>( 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>( 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 careerTankStatsRaw = await getTankStats(server, id);
const careerStats: Record<number, AllStats> = { const careerStats: Record<number, AllStats> = showTotal
0: accountInfo[id].statistics.all, ? {
}; 0: accountInfo[id].statistics.all,
}
: {};
const allStatsToAccumulate: AllStats[] = []; const allStatsToAccumulate: AllStats[] = [];
Object.entries(diffed).forEach(([, tankStats]) => { Object.entries(diffed).forEach(([, tankStats]) => {
@@ -52,7 +58,7 @@ export default async function today(
const accumulatedStats = sumStats(allStatsToAccumulate); const accumulatedStats = sumStats(allStatsToAccumulate);
if (Object.keys(diffed).length > 0) { if (showTotal && Object.keys(diffed).length > 0) {
diffed[0] = accumulatedStats; diffed[0] = accumulatedStats;
} }
@@ -62,7 +68,7 @@ export default async function today(
>(async (accumulator, [tankIdString, tankStats]) => { >(async (accumulator, [tankIdString, tankStats]) => {
const tankId = parseInt(tankIdString); const tankId = parseInt(tankIdString);
return tankId === 0 || !(await tankAverages)[tankId] return (showTotal && tankId === 0) || !(await tankAverages)[tankId]
? accumulator ? accumulator
: { : {
...(await accumulator), ...(await accumulator),
@@ -73,7 +79,8 @@ export default async function today(
const careerWN8s = await careerTankStatsRaw.reduce< const careerWN8s = await careerTankStatsRaw.reduce<
PossiblyPromise<Record<number, number>> PossiblyPromise<Record<number, number>>
>(async (accumulator, { tank_id }) => { >(async (accumulator, { tank_id }) => {
return tank_id === 0 || (await tankAverages)[tank_id] === undefined return (showTotal && tank_id === 0) ||
(await tankAverages)[tank_id] === undefined
? accumulator ? accumulator
: { : {
...(await accumulator), ...(await accumulator),
@@ -86,60 +93,61 @@ export default async function today(
const todayWN8sEntries = Object.entries(todayWN8s); const todayWN8sEntries = Object.entries(todayWN8s);
const careerWN8sEntries = Object.entries(careerWN8s); const careerWN8sEntries = Object.entries(careerWN8s);
todayWN8s[0] = if (showTotal) {
todayWN8sEntries.reduce( todayWN8s[0] =
(accumulator, [tankIdString, wn8]) => todayWN8sEntries.reduce(
accumulator + wn8 * diffed[Number(tankIdString)].battles, (accumulator, [tankIdString, wn8]) =>
0, accumulator + wn8 * diffed[Number(tankIdString)].battles,
) / 0,
todayWN8sEntries.reduce( ) /
(accumulator, [tankIdString]) => todayWN8sEntries.reduce(
accumulator + diffed[Number(tankIdString)].battles, (accumulator, [tankIdString]) =>
0, accumulator + diffed[Number(tankIdString)].battles,
); 0,
careerWN8s[0] = );
careerWN8sEntries.reduce( careerWN8s[0] =
(accumulator, [tankIdString, WN8]) => careerWN8sEntries.reduce(
isNaN(WN8) (accumulator, [tankIdString, WN8]) =>
? accumulator isNaN(WN8)
: accumulator + WN8 * careerStats[Number(tankIdString)].battles, ? accumulator
0, : accumulator + WN8 * careerStats[Number(tankIdString)].battles,
) / 0,
careerWN8sEntries.reduce( ) /
(accumulator, [tankIdString, WN8]) => careerWN8sEntries.reduce(
isNaN(WN8) (accumulator, [tankIdString, WN8]) =>
? accumulator isNaN(WN8)
: accumulator + careerStats[Number(tankIdString)].battles, ? accumulator
0, : accumulator + careerStats[Number(tankIdString)].battles,
); 0,
);
}
const rows = await Promise.all( const rows = await Promise.all(
[ [
// this code unreadable on god will forget tomorrow no cap // this code unreadable on god will forget tomorrow no cap
...(order.length === 0 ? [] : [0]), ...(order.length === 0 || !showTotal ? [] : [0]),
...(isFinite(limit) ? order.slice(0, limit) : order), ...(isFinite(cutoff) ? order.slice(0, cutoff) : order),
].map(async (tankId, index) => { ].map(async (id, index) => {
const tankStats = diffed[tankId]; const tankStats = diffed[id];
const career = careerStats[tankId]; const career = careerStats[id];
const tankopediaEntry = (await tankopedia)[id];
return ( return (
<Breakdown.Row <Breakdown.Row
naked={naked} isTank={!showTotal || id !== 0}
isListing={tankId !== 0} tankType={tankopediaEntry?.type}
minimized={index > 4} treeType={tankopediaEntry ? await getTreeType(id) : undefined}
key={tankId} minimized={showTotal ? index > maximized : index + 1 > maximized}
name={tankId === 0 ? 'Total' : await resolveTankName(tankId)} key={id}
name={showTotal && id === 0 ? 'Total' : await resolveTankName(id)}
winrate={tankStats.wins / tankStats.battles} winrate={tankStats.wins / tankStats.battles}
careerWinrate={career.wins / career.battles} careerWinrate={career.wins / career.battles}
WN8={isNaN(todayWN8s[tankId]) ? undefined : todayWN8s[tankId]} WN8={isNaN(todayWN8s[id]) ? undefined : todayWN8s[id]}
careerWN8={isNaN(careerWN8s[tankId]) ? undefined : careerWN8s[tankId]} careerWN8={isNaN(careerWN8s[id]) ? undefined : careerWN8s[id]}
damage={tankStats.damage_dealt / tankStats.battles} damage={tankStats.damage_dealt / tankStats.battles}
careerDamage={career.damage_dealt / career.battles} careerDamage={career.damage_dealt / career.battles}
battles={tankStats.battles} battles={tankStats.battles}
careerBattles={career.battles} careerBattles={career.battles}
icon={
tankId === 0 ? undefined : (await tankopedia)[tankId]?.images.normal
}
/> />
); );
}), }),
@@ -147,7 +155,7 @@ export default async function today(
return naked ? ( return naked ? (
<Wrapper naked> <Wrapper naked>
<Breakdown.Root>{rows.slice(1)}</Breakdown.Root> <Breakdown.Root>{rows}</Breakdown.Root>
</Wrapper> </Wrapper>
) : ( ) : (
<Wrapper> <Wrapper>
@@ -161,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` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined : undefined
} }
description={`Today's breakdown • ${new Date().toDateString()} • ${ description={`Today's breakdown • ${new Date().toDateString()}`}
REGION_DOMAIN_NAMES[server]
}`}
/> />
{rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />} {rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
+2 -2
View File
@@ -6,12 +6,12 @@ import { RouteRegistry } from '../server';
export const statsRoute: RouteRegistry = { export const statsRoute: RouteRegistry = {
route: '/stats/:type/:period', route: '/stats/:type/:period',
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
async handler(req) { async handler(req) {
const type = req.params.type as StatType; const type = req.params.type as StatType;
const player = resolvePlayerFromRequest(req); const player = resolvePlayerFromRequest(req);
const period = resolvePeriodFromRequest(player.server, req); const period = resolvePeriodFromRequest(player.region, req);
return await stats( return await stats(
type, type,
+4 -2
View File
@@ -5,14 +5,16 @@ import { RouteRegistry } from '../server';
export const todayRoute: RouteRegistry = { export const todayRoute: RouteRegistry = {
route: '/today', route: '/today',
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: true,
async handler(req) { async handler(req) {
const player = resolvePlayerFromRequest(req); const player = resolvePlayerFromRequest(req);
return await today( return await today(
player, player,
req.query.limit ? parseInt(req.query.limit as string) : undefined, req.query.cutoff ? parseInt(req.query.cutoff as string) : undefined,
req.query.maximized ? parseInt(req.query.maximized as string) : undefined,
req.query['show-total'] ? req.query['show-total'] === 'true' : undefined,
true, true,
); );
}, },
+1 -1
View File
@@ -31,7 +31,7 @@ const app = express();
if (registry.handlesInteraction) return; if (registry.handlesInteraction) return;
res.send( res.send(
withAutoRefresh(await normalizeRouteReturnable(returnable), 60 * 1000), withAutoRefresh(await normalizeRouteReturnable(returnable), 30 * 1000),
); );
}); });
}); });
+1 -4
View File
@@ -11,8 +11,5 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src/**/*.ts", "src/components/Tanks/components/Row.tsx"]
"src/**/*.ts",
"src/components/AllStatsOverview/components/WN8SurroundingStat.tsx"
]
} }
+429 -561
View File
File diff suppressed because it is too large. Load diff