mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
495a830506 | ||
|
|
a730f67ca5 | ||
|
|
9a3867770d | ||
|
|
f8a66bb5e7 | ||
|
|
4b00e1342e | ||
|
|
f9e0584f00 | ||
|
|
900c290e75 | ||
|
|
69e1577d18 | ||
|
|
a6d9044c3a | ||
|
|
9e23d60ef1 | ||
|
|
f12449119c | ||
|
|
533bfc4458 | ||
|
|
1317c2879c | ||
|
|
f9e51f894a | ||
|
|
6fdd4ba833 | ||
|
|
9495ae2336 | ||
|
|
43f634e198 | ||
|
|
ad51be7afc | ||
|
|
15b8bfdf8f | ||
|
|
cb8ae47be6 | ||
|
|
db846c0680 | ||
|
|
630a37025e | ||
|
|
d3bbc7f2b0 | ||
|
|
4e0321ca5f | ||
|
|
ec00bdf9de | ||
|
|
01dd7ad9c6 | ||
|
|
d726005c38 | ||
|
|
4f3e868367 | ||
|
|
ed8bab7df8 | ||
|
|
9bdb14e13a | ||
|
|
b827f7f730 | ||
|
|
07e3660388 | ||
|
|
970ab9004e | ||
|
|
862445259e | ||
|
|
e96538bae0 | ||
|
|
7447ccc487 | ||
|
|
bcd3a00f91 | ||
|
|
8c3675546f | ||
|
|
ec64dab720 | ||
|
|
db976c4ae9 | ||
|
|
0db96795b3 | ||
|
|
59bae96c1b | ||
|
|
d062a0da1b | ||
|
|
d2668974f7 | ||
|
|
78e4c82f20 | ||
|
|
f965452c5d | ||
|
|
be644bb3d9 | ||
|
|
97cc9509c0 | ||
|
|
6dc7a10c2e | ||
|
|
19298b9ad6 | ||
|
|
97d427a6a8 | ||
|
|
bff6eb0503 | ||
|
|
dc9ab2067d | ||
|
|
e8e1f7bafb | ||
|
|
e50a456025 | ||
|
|
d442bbca1a |
No files matched your search
@@ -2,7 +2,7 @@ name: CD Host
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */5 * * *'
|
- cron: '0 */6 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -29,4 +29,4 @@ jobs:
|
|||||||
unzip bot.zip
|
unzip bot.zip
|
||||||
|
|
||||||
- name: Run bot
|
- name: Run bot
|
||||||
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps --trace-warnings index.cjs
|
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
|
||||||
Vendored
+4
@@ -5,6 +5,7 @@
|
|||||||
"bepaint",
|
"bepaint",
|
||||||
"blitzstars",
|
"blitzstars",
|
||||||
"cleandist",
|
"cleandist",
|
||||||
|
"cockroackdb",
|
||||||
"customperiod",
|
"customperiod",
|
||||||
"deburr",
|
"deburr",
|
||||||
"diacriticless",
|
"diacriticless",
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
"kranvagn",
|
"kranvagn",
|
||||||
"Leaderboard",
|
"Leaderboard",
|
||||||
"leaderboards",
|
"leaderboards",
|
||||||
|
"Oopsy",
|
||||||
|
"outdir",
|
||||||
"outfile",
|
"outfile",
|
||||||
"ownedtanks",
|
"ownedtanks",
|
||||||
"playerachievements",
|
"playerachievements",
|
||||||
@@ -42,6 +45,7 @@
|
|||||||
"wargaming",
|
"wargaming",
|
||||||
"webp",
|
"webp",
|
||||||
"winrate",
|
"winrate",
|
||||||
|
"wotblitz",
|
||||||
"zustand"
|
"zustand"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
|
|||||||
+3
-1
@@ -8,5 +8,7 @@
|
|||||||
"sklld_peasant_role": "896878529512370228",
|
"sklld_peasant_role": "896878529512370228",
|
||||||
|
|
||||||
"tres_guild_id": "734786591205359697",
|
"tres_guild_id": "734786591205359697",
|
||||||
"tres_log_channel": "1109262265850544148"
|
"tres_log_channel": "1109262265850544148",
|
||||||
|
|
||||||
|
"test_guild_id": "1124729294829142116"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Blitzkrieg 1.13.3
|
||||||
|
|
||||||
|
Streamer's delight!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added config context menu for the session tracker embed
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Blitzkrieg 1.14.0
|
||||||
|
|
||||||
|
Elegance has never been met like this before
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added `/link`
|
||||||
|
- Unlike `/verify`, `/link` links you Discord and Blitz accounts across all servers
|
||||||
|
- This command does not change your nickname
|
||||||
|
- `/link` now simply displays a deprecation message
|
||||||
|
- Usernames can still be inputted per-command to override your linked account
|
||||||
|
- `/stats` and `/full-stats` are now way simpler
|
||||||
|
- `/stats <type> <period>` has been replaced with `/stats <30|60|90|...>` (likewise, `/full-stats`)
|
||||||
|
- These commands now have filters without having to opt into `multi-tank`
|
||||||
|
- Added `/breakdown`
|
||||||
|
- Unlike `/today`, `/breakdown` lets you select any period of time
|
||||||
|
- `/breakdown` now simply displays a deprecation message
|
||||||
|
- `/about commands` are now sorted alphabetically
|
||||||
|
- Added a `/reset` button to the options dropdown and embed context-menu
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Simplified no ongoing ratings season edge case
|
||||||
|
- Updated welcome message for Skilled server to use `/link` and other up-to-date information
|
||||||
|
- Tier weights are now reported in Arabic numerals instead of Roman
|
||||||
|
- Removed tank images from `/owned-tanks`
|
||||||
|
- Wargaming messed up their API and almost no tanks have any images anymore
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Banners now use a more economical `.webp` format just like the home page
|
||||||
|
- Removed unused color palettes
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Guild commands are now handled more efficiently
|
||||||
|
- Removed nickname-based Blitz username inference
|
||||||
|
- Code split a lot of common functionalities
|
||||||
|
- Re-arranged `core` folder
|
||||||
|
- Added midnight and latest ratings leaderboard caches
|
||||||
|
- Merged renderers into their primary user files
|
||||||
|
- Added sharding
|
||||||
|
- Disabled for now but good for future proofing
|
||||||
|
- Switched to a way cleaner logging schema
|
||||||
|
- Hosting is now done in 6 hour periods instead of 5
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Blitzkrieg 1.14.1
|
||||||
|
|
||||||
|
Oopsy daisy, a memory leak!
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Simplified message for users to use `/link`
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed a memory leak in the render thread manager
|
||||||
|
|
||||||
|
## Technical changes
|
||||||
|
|
||||||
|
- Added `UserError` extending `Error`
|
||||||
|
- This lets the bot avoid spamming the debug console for mistakes users make
|
||||||
|
- Reduced render threads to 2
|
||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --enable-source-maps --trace-warnings server.cjs"
|
"start": "node --enable-source-maps server.cjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkrieg",
|
"name": "blitzkrieg",
|
||||||
"version": "1.13.2",
|
"version": "1.14.0",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"dev:bot": "yarn build:bot && yarn start:bot",
|
"dev:bot": "yarn build:bot && yarn start:bot",
|
||||||
"dev:docs": "vitepress dev docs",
|
"dev:docs": "vitepress dev docs",
|
||||||
"dev:website": "next dev",
|
"dev:website": "next dev",
|
||||||
"start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs",
|
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
|
||||||
"start:website": "next start",
|
"start:website": "next start",
|
||||||
"host:docs": "vitepress preview docs",
|
"host:docs": "vitepress preview docs",
|
||||||
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/rest": "^20.0.1",
|
"@octokit/rest": "^20.0.1",
|
||||||
|
"@prisma/client": "^5.4.1",
|
||||||
"@radix-ui/colors": "^3.0.0-rc.4",
|
"@radix-ui/colors": "^3.0.0-rc.4",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/themes": "^1.1.2",
|
"@radix-ui/themes": "^1.1.2",
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"knip": "^2.27.1",
|
"knip": "^2.27.1",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
|
"prisma": "^5.4.1",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "discord_blitz" (
|
||||||
|
"discord" INT8 NOT NULL,
|
||||||
|
"region" STRING NOT NULL,
|
||||||
|
"blitz" INT4 NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord")
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "cockroachdb"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "cockroachdb"
|
||||||
|
url = env("DISCORD_BLITZ_DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
model discord_blitz {
|
||||||
|
discord BigInt @id
|
||||||
|
region String
|
||||||
|
blitz Int
|
||||||
|
}
|
||||||
+36
-19
@@ -1,27 +1,44 @@
|
|||||||
import { build } from 'esbuild';
|
import { build } from 'esbuild';
|
||||||
|
import { cp, readdir } from 'fs/promises';
|
||||||
|
|
||||||
console.log('Building bot...');
|
readdir('src/workers').then((workers) => {
|
||||||
|
const files = [
|
||||||
|
'bot',
|
||||||
|
'index',
|
||||||
|
|
||||||
await build({
|
...workers.map((file) => `workers/${file.replace('.ts', '')}`),
|
||||||
entryPoints: ['src/bot.ts'],
|
];
|
||||||
outfile: 'dist/bot/index.cjs',
|
|
||||||
|
|
||||||
platform: 'node',
|
build({
|
||||||
tsconfig: 'tsconfig.esbuild.json',
|
entryPoints: files.map((file) => `src/${file}.ts`),
|
||||||
loader: {
|
outdir: 'dist/bot',
|
||||||
'.node': 'copy',
|
|
||||||
'.ttf': 'file',
|
|
||||||
},
|
|
||||||
format: 'cjs',
|
|
||||||
jsx: 'transform',
|
|
||||||
|
|
||||||
bundle: true,
|
platform: 'node',
|
||||||
sourcemap: true,
|
tsconfig: 'tsconfig.esbuild.json',
|
||||||
minifyIdentifiers: false, // cause goofy errors
|
loader: {
|
||||||
minifySyntax: true,
|
'.node': 'copy',
|
||||||
minifyWhitespace: true,
|
'.ttf': 'file',
|
||||||
|
},
|
||||||
|
format: 'cjs',
|
||||||
|
jsx: 'transform',
|
||||||
|
outExtension: {
|
||||||
|
'.js': '.cjs',
|
||||||
|
},
|
||||||
|
|
||||||
logLevel: 'info',
|
bundle: true,
|
||||||
|
sourcemap: true,
|
||||||
|
minifyIdentifiers: false, // cause goofy errors
|
||||||
|
minifySyntax: true,
|
||||||
|
minifyWhitespace: true,
|
||||||
|
|
||||||
|
logLevel: 'info',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Built bot');
|
// copy schema.prisma
|
||||||
|
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
|
||||||
|
readdir('node_modules/prisma').then((files) =>
|
||||||
|
files
|
||||||
|
.filter((file) => file.endsWith('.node'))
|
||||||
|
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
|
||||||
|
);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { tankopedia } from '../../../core/blitz/tankopedia';
|
import { tankopedia } from '../../../core/blitzstars/tankopedia';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
return NextResponse.json(await tankopedia);
|
return NextResponse.json(await tankopedia);
|
||||||
|
|||||||
+136
-116
@@ -1,18 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { ContextMenu } from '@radix-ui/themes';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import * as Breakdown from '../../../components/Breakdown';
|
import * as Breakdown from '../../../components/Breakdown';
|
||||||
import { TreeTypeEnum } from '../../../components/Tanks';
|
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||||
import calculateWN8 from '../../../core/blitz/calculateWN8';
|
import fetchBlitz from '../../../core/blitz/fetchWargaming';
|
||||||
import { diffNormalizedTankStats } from '../../../core/blitz/diffNormalizedTankStats';
|
|
||||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile';
|
|
||||||
import getWargamingResponse from '../../../core/blitz/getWargamingResponse';
|
|
||||||
import { TankopediaEntry, tankopedia } from '../../../core/blitz/tankopedia';
|
|
||||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||||
import { useSession } from '../../../stores/session';
|
import {
|
||||||
|
TankopediaEntry,
|
||||||
|
tankopedia,
|
||||||
|
} from '../../../core/blitzstars/tankopedia';
|
||||||
|
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
||||||
|
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
||||||
|
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
||||||
|
import { resetSession, useSession } from '../../../stores/session';
|
||||||
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
|
||||||
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
|
||||||
|
import { Menu } from '../../tools/session/components/Menu';
|
||||||
|
|
||||||
const REFRESH_RATE = 1 / 5;
|
const REFRESH_RATE = 1 / 5;
|
||||||
|
|
||||||
@@ -71,7 +76,7 @@ export default function SessionPage() {
|
|||||||
async function recalculateDiff() {
|
async function recalculateDiff() {
|
||||||
if (session.isTracking) {
|
if (session.isTracking) {
|
||||||
const { id, region } = session;
|
const { id, region } = session;
|
||||||
const careerRaw = await getWargamingResponse<TanksStats>(
|
const careerRaw = await fetchBlitz<TanksStats>(
|
||||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||||
);
|
);
|
||||||
const career = careerRaw[id].reduce<
|
const career = careerRaw[id].reduce<
|
||||||
@@ -109,7 +114,7 @@ export default function SessionPage() {
|
|||||||
|
|
||||||
list: (
|
list: (
|
||||||
Object.values(
|
Object.values(
|
||||||
diffNormalizedTankStats(session.tankStats, career),
|
deltaTankStats(session.tankStats, career),
|
||||||
) as IndividualTankStats[]
|
) as IndividualTankStats[]
|
||||||
).map((stats) => {
|
).map((stats) => {
|
||||||
return {
|
return {
|
||||||
@@ -193,118 +198,133 @@ export default function SessionPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breakdown.Root>
|
<ContextMenu.Root>
|
||||||
{session.showTotal &&
|
<ContextMenu.Trigger>
|
||||||
session.isTracking &&
|
<div>
|
||||||
since !== undefined &&
|
<Breakdown.Root>
|
||||||
diff !== undefined && (
|
{session.showTotal &&
|
||||||
<Breakdown.Row
|
session.isTracking &&
|
||||||
minimized={!session.showCareer}
|
since !== undefined &&
|
||||||
title={`Since ${since.toLocaleString(undefined, {
|
diff !== undefined && (
|
||||||
hour: 'numeric',
|
<Breakdown.Row
|
||||||
minute: 'numeric',
|
minimized={!session.showCareer}
|
||||||
hour12: true,
|
title={`Since ${since.toLocaleString(undefined, {
|
||||||
month: 'short',
|
hour: 'numeric',
|
||||||
year: 'numeric',
|
minute: 'numeric',
|
||||||
day: 'numeric',
|
hour12: true,
|
||||||
})}`}
|
month: 'short',
|
||||||
stats={session.customColumns.map((customColumn) => {
|
year: 'numeric',
|
||||||
const rowStat = totalRowStats[customColumn.display];
|
day: 'numeric',
|
||||||
|
})}`}
|
||||||
|
stats={session.customColumns.map((customColumn) => {
|
||||||
|
const rowStat = totalRowStats[customColumn.display];
|
||||||
|
|
||||||
if (!rowStat) return undefined;
|
if (!rowStat) return undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rowStat,
|
...rowStat,
|
||||||
delta:
|
delta:
|
||||||
customColumn.showDelta && rowStat.percentile === undefined
|
customColumn.showDelta &&
|
||||||
? rowStat.delta
|
rowStat.percentile === undefined
|
||||||
: undefined,
|
? rowStat.delta
|
||||||
};
|
: undefined,
|
||||||
})}
|
};
|
||||||
/>
|
})}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{diff?.list
|
{diff?.list
|
||||||
.sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time)
|
.sort(
|
||||||
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
|
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
|
||||||
const rowStats: Record<
|
)
|
||||||
CustomColumnDisplay,
|
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
|
||||||
Breakdown.RowStatItem | undefined
|
const rowStats: Record<
|
||||||
> = {
|
CustomColumnDisplay,
|
||||||
battles: {
|
Breakdown.RowStatItem | undefined
|
||||||
title: 'Battles',
|
> = {
|
||||||
current: stats.all.battles.toLocaleString(),
|
battles: {
|
||||||
career: career.all.battles.toLocaleString(),
|
title: 'Battles',
|
||||||
},
|
current: stats.all.battles.toLocaleString(),
|
||||||
winrate: {
|
career: career.all.battles.toLocaleString(),
|
||||||
title: 'Winrate',
|
},
|
||||||
current: `${(100 * (stats.all.wins / stats.all.battles)).toFixed(
|
winrate: {
|
||||||
2,
|
title: 'Winrate',
|
||||||
)}%`,
|
current: `${(
|
||||||
career: `${(100 * (career.all.wins / career.all.battles)).toFixed(
|
100 *
|
||||||
2,
|
(stats.all.wins / stats.all.battles)
|
||||||
)}%`,
|
).toFixed(2)}%`,
|
||||||
delta:
|
career: `${(
|
||||||
stats.all.wins / stats.all.battles -
|
100 *
|
||||||
career.all.wins / career.all.battles,
|
(career.all.wins / career.all.battles)
|
||||||
},
|
).toFixed(2)}%`,
|
||||||
wn8: {
|
delta:
|
||||||
title: 'WN8',
|
stats.all.wins / stats.all.battles -
|
||||||
current:
|
career.all.wins / career.all.battles,
|
||||||
currentWN8 === undefined
|
},
|
||||||
? undefined
|
wn8: {
|
||||||
: Math.round(currentWN8).toLocaleString(),
|
title: 'WN8',
|
||||||
career:
|
current:
|
||||||
careerWN8 === undefined
|
currentWN8 === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: Math.round(careerWN8).toLocaleString(),
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
percentile:
|
career:
|
||||||
currentWN8 === undefined
|
careerWN8 === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: getWN8Percentile(currentWN8),
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
},
|
percentile:
|
||||||
damage: {
|
currentWN8 === undefined
|
||||||
title: 'Damage',
|
? undefined
|
||||||
current: Math.round(
|
: getWN8Percentile(currentWN8),
|
||||||
stats.all.damage_dealt / stats.all.battles,
|
},
|
||||||
).toLocaleString(),
|
damage: {
|
||||||
career: Math.round(
|
title: 'Damage',
|
||||||
career.all.damage_dealt / career.all.battles,
|
current: Math.round(
|
||||||
).toLocaleString(),
|
stats.all.damage_dealt / stats.all.battles,
|
||||||
delta:
|
).toLocaleString(),
|
||||||
stats.all.damage_dealt / stats.all.battles -
|
career: Math.round(
|
||||||
career.all.damage_dealt / career.all.battles,
|
career.all.damage_dealt / career.all.battles,
|
||||||
},
|
).toLocaleString(),
|
||||||
none: undefined,
|
delta:
|
||||||
};
|
stats.all.damage_dealt / stats.all.battles -
|
||||||
|
career.all.damage_dealt / career.all.battles,
|
||||||
return (
|
},
|
||||||
<Breakdown.Row
|
none: undefined,
|
||||||
key={stats.tank_id}
|
|
||||||
minimized={!session.showCareer}
|
|
||||||
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
|
|
||||||
type="tank"
|
|
||||||
tankType={tankopedia?.type}
|
|
||||||
treeType={(() => {
|
|
||||||
if (tankopedia?.is_collectible) return TreeTypeEnum.Collector;
|
|
||||||
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
|
|
||||||
})()}
|
|
||||||
stats={session.customColumns.map((customColumn) => {
|
|
||||||
const rowStat = rowStats[customColumn.display];
|
|
||||||
|
|
||||||
if (!rowStat) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rowStat,
|
|
||||||
delta:
|
|
||||||
customColumn.showDelta && rowStat.percentile === undefined
|
|
||||||
? rowStat.delta
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breakdown.Row
|
||||||
|
key={stats.tank_id}
|
||||||
|
minimized={!session.showCareer}
|
||||||
|
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
|
||||||
|
type="tank"
|
||||||
|
tankType={tankopedia?.type}
|
||||||
|
treeType={(() => {
|
||||||
|
if (tankopedia?.is_collectible)
|
||||||
|
return TreeTypeEnum.Collector;
|
||||||
|
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
|
||||||
|
})()}
|
||||||
|
stats={session.customColumns.map((customColumn) => {
|
||||||
|
const rowStat = rowStats[customColumn.display];
|
||||||
|
|
||||||
|
if (!rowStat) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rowStat,
|
||||||
|
delta:
|
||||||
|
customColumn.showDelta &&
|
||||||
|
rowStat.percentile === undefined
|
||||||
|
? rowStat.delta
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
/>
|
</Breakdown.Root>
|
||||||
);
|
</div>
|
||||||
})}
|
</ContextMenu.Trigger>
|
||||||
</Breakdown.Root>
|
|
||||||
|
<Menu reset={resetSession} Builder={ContextMenu} />
|
||||||
|
</ContextMenu.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -19,17 +19,32 @@ export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 128,
|
height: 128,
|
||||||
backgroundImage: `url(/assets/banners/${toolId}.png)`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
boxShadow: `inset 0 -192px 128px -128px ${theme.colors.appBackground1}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
backgroundImage: `url(/assets/banners/${toolId}.webp)`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
filter: 'blur(4px)',
|
||||||
|
transform: 'scale(calc((128 + 8) / 128))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DropdownMenu } from '@radix-ui/themes';
|
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
|
||||||
import mutateSession, { useSession } from '../../../../stores/session';
|
import mutateSession, { useSession } from '../../../../stores/session';
|
||||||
|
|
||||||
export type CustomColumnDisplay =
|
export type CustomColumnDisplay =
|
||||||
@@ -11,20 +11,21 @@ export type CustomColumnDisplay =
|
|||||||
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
|
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
|
||||||
|
|
||||||
interface CustomColumn {
|
interface CustomColumn {
|
||||||
|
Builder: typeof DropdownMenu | typeof ContextMenu;
|
||||||
column: number;
|
column: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CustomColumn({ column }: CustomColumn) {
|
export function CustomColumn({ column, Builder }: CustomColumn) {
|
||||||
const customColumn = useSession((state) => state.customColumns[column]);
|
const customColumn = useSession((state) => state.customColumns[column]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Sub>
|
<Builder.Sub>
|
||||||
<DropdownMenu.SubTrigger>Column {column + 1}</DropdownMenu.SubTrigger>
|
<Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger>
|
||||||
|
|
||||||
<DropdownMenu.SubContent>
|
<Builder.SubContent>
|
||||||
<DropdownMenu.Label>Display</DropdownMenu.Label>
|
<Builder.Label>Display</Builder.Label>
|
||||||
|
|
||||||
<DropdownMenu.RadioGroup
|
<Builder.RadioGroup
|
||||||
value={customColumn.display}
|
value={customColumn.display}
|
||||||
onValueChange={(event) =>
|
onValueChange={(event) =>
|
||||||
mutateSession((draft) => {
|
mutateSession((draft) => {
|
||||||
@@ -33,22 +34,18 @@ export function CustomColumn({ column }: CustomColumn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DropdownMenu.RadioItem value="battles">
|
<Builder.RadioItem value="battles">Battles</Builder.RadioItem>
|
||||||
Battles
|
<Builder.RadioItem value="winrate">Winrate</Builder.RadioItem>
|
||||||
</DropdownMenu.RadioItem>
|
<Builder.RadioItem value="wn8">WN8</Builder.RadioItem>
|
||||||
<DropdownMenu.RadioItem value="winrate">
|
<Builder.RadioItem value="damage">Damage</Builder.RadioItem>
|
||||||
Winrate
|
<Builder.RadioItem value="none">None</Builder.RadioItem>
|
||||||
</DropdownMenu.RadioItem>
|
</Builder.RadioGroup>
|
||||||
<DropdownMenu.RadioItem value="wn8">WN8</DropdownMenu.RadioItem>
|
|
||||||
<DropdownMenu.RadioItem value="damage">Damage</DropdownMenu.RadioItem>
|
|
||||||
<DropdownMenu.RadioItem value="none">None</DropdownMenu.RadioItem>
|
|
||||||
</DropdownMenu.RadioGroup>
|
|
||||||
|
|
||||||
{!NO_DELTA.includes(customColumn.display) && (
|
{!NO_DELTA.includes(customColumn.display) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Separator />
|
<Builder.Separator />
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<Builder.CheckboxItem
|
||||||
checked={customColumn.showDelta}
|
checked={customColumn.showDelta}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
mutateSession((draft) => {
|
mutateSession((draft) => {
|
||||||
@@ -57,10 +54,10 @@ export function CustomColumn({ column }: CustomColumn) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
Show delta
|
Show delta
|
||||||
</DropdownMenu.CheckboxItem>
|
</Builder.CheckboxItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenu.SubContent>
|
</Builder.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</Builder.Sub>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
|
||||||
|
import { useSession } from '../../../../stores/session';
|
||||||
|
import { CustomColumn } from './CustomColumn';
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
Builder: typeof DropdownMenu | typeof ContextMenu;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Menu({ Builder, reset }: ContextMenuProps) {
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Builder.Content>
|
||||||
|
<CustomColumn Builder={Builder} column={0} />
|
||||||
|
<CustomColumn Builder={Builder} column={1} />
|
||||||
|
<CustomColumn Builder={Builder} column={2} />
|
||||||
|
<CustomColumn Builder={Builder} column={3} />
|
||||||
|
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={session.showTotal}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({ showTotal: checked })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show total
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
<Builder.CheckboxItem
|
||||||
|
checked={session.showCareer}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({ showCareer: checked })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show career
|
||||||
|
</Builder.CheckboxItem>
|
||||||
|
|
||||||
|
<Builder.Separator />
|
||||||
|
|
||||||
|
<Builder.Item color="red" onClick={reset}>
|
||||||
|
Reset
|
||||||
|
</Builder.Item>
|
||||||
|
</Builder.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,21 +14,17 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { ChangeEvent, useRef, useState } from 'react';
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
import PageWrapper from '../../../components/PageWrapper';
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
import { REGION_NAMES, Region } from '../../../constants/regions';
|
import { REGION_NAMES } from '../../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
import searchPlayersAcrossRegions, {
|
||||||
import getWargamingResponse from '../../../core/blitz/getWargamingResponse';
|
|
||||||
import listPlayers, {
|
|
||||||
AccountListWithServer,
|
AccountListWithServer,
|
||||||
} from '../../../core/blitz/listPlayers';
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||||
import { useSession } from '../../../stores/session';
|
import { resetSession, setSession, useSession } from '../../../stores/session';
|
||||||
import { NormalizedTankStats, TanksStats } from '../../../types/tanksStats';
|
|
||||||
import SessionPage from '../../embeds/session/page';
|
import SessionPage from '../../embeds/session/page';
|
||||||
import { CustomColumn } from './components/CustomColumn';
|
import { Menu } from './components/Menu';
|
||||||
import * as styles from './page.css';
|
import * as styles from './page.css';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -40,7 +36,7 @@ export default function Page() {
|
|||||||
const handleChange = debounce(
|
const handleChange = debounce(
|
||||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (event.target.value) {
|
if (event.target.value) {
|
||||||
setSearchResults(await listPlayers(event.target.value));
|
setSearchResults(await searchPlayersAcrossRegions(event.target.value));
|
||||||
} else {
|
} else {
|
||||||
setSearchResults(undefined);
|
setSearchResults(undefined);
|
||||||
setShowSearchResults(false);
|
setShowSearchResults(false);
|
||||||
@@ -50,37 +46,9 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
async function setSession(region: Region, id: number, nickname: string) {
|
useEffect(() => {
|
||||||
input.current!.value = nickname;
|
if (session.isTracking) input.current!.value = session.nickname;
|
||||||
|
}, [session]);
|
||||||
const rawTankStats = (
|
|
||||||
await getWargamingResponse<TanksStats>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
)
|
|
||||||
)[id];
|
|
||||||
const tankStats = rawTankStats.reduce<NormalizedTankStats>(
|
|
||||||
(accumulator, tank) => ({
|
|
||||||
...accumulator,
|
|
||||||
[tank.tank_id]: tank,
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
useSession.setState({
|
|
||||||
isTracking: true,
|
|
||||||
id,
|
|
||||||
region,
|
|
||||||
nickname,
|
|
||||||
tankStats,
|
|
||||||
time: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
const session = useSession.getState();
|
|
||||||
if (!session.isTracking) return;
|
|
||||||
setSession(session.region, session.id, session.nickname);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
@@ -158,7 +126,7 @@ export default function Page() {
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (!session.promptBeforeReset) {
|
if (!session.promptBeforeReset) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
reset();
|
resetSession();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={styles.toolbarButton}
|
className={styles.toolbarButton}
|
||||||
@@ -201,7 +169,7 @@ export default function Page() {
|
|||||||
</AlertDialog.Cancel>
|
</AlertDialog.Cancel>
|
||||||
|
|
||||||
<AlertDialog.Action>
|
<AlertDialog.Action>
|
||||||
<Button variant="solid" color="red" onClick={reset}>
|
<Button variant="solid" color="red" onClick={resetSession}>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialog.Action>
|
</AlertDialog.Action>
|
||||||
@@ -209,19 +177,60 @@ export default function Page() {
|
|||||||
</AlertDialog.Content>
|
</AlertDialog.Content>
|
||||||
</AlertDialog.Root>
|
</AlertDialog.Root>
|
||||||
|
|
||||||
<Tooltip content="Copy to clipboard">
|
<AlertDialog.Root>
|
||||||
<Button
|
<AlertDialog.Trigger>
|
||||||
variant="soft"
|
<Button
|
||||||
className={styles.toolbarButton}
|
variant="soft"
|
||||||
onClick={() =>
|
className={styles.toolbarButton}
|
||||||
navigator.clipboard.writeText(
|
onClick={(event) => {
|
||||||
`${location.origin}/embeds/session`,
|
if (!session.showEmbedPrompt) {
|
||||||
)
|
event.preventDefault();
|
||||||
}
|
resetSession();
|
||||||
>
|
}
|
||||||
<CopyIcon width="16" height="16" /> Embed
|
|
||||||
</Button>
|
navigator.clipboard.writeText(
|
||||||
</Tooltip>
|
`${location.origin}/embeds/session`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon width="16" height="16" /> Embed
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Copied link to your clipboard
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
Keep in mind:
|
||||||
|
<ul>
|
||||||
|
<li>Paste this link into your browser to view the embed</li>
|
||||||
|
<li>Embeds abide your custom options</li>
|
||||||
|
<li>
|
||||||
|
Options do not transfer across websites/streaming softwares
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<Text size="2">
|
||||||
|
<Checkbox
|
||||||
|
mr="1"
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
useSession.setState({
|
||||||
|
showEmbedPrompt: !checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Don't show this again
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4">
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
<Button variant="soft">Got it!</Button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
@@ -231,31 +240,7 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<Menu Builder={DropdownMenu} reset={resetSession} />
|
||||||
<CustomColumn column={0} />
|
|
||||||
<CustomColumn column={1} />
|
|
||||||
<CustomColumn column={2} />
|
|
||||||
<CustomColumn column={3} />
|
|
||||||
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={session.showTotal}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
useSession.setState({ showTotal: checked })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Show total
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={session.showCareer}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
useSession.setState({ showCareer: checked })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Show career
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-3
@@ -1,6 +1,8 @@
|
|||||||
|
import { registerProcesses } from './core/blitzkrieg/processes';
|
||||||
|
import { secrets } from './core/blitzkrieg/secrets';
|
||||||
import { client } from './core/discord/client';
|
import { client } from './core/discord/client';
|
||||||
import { secrets } from './core/node/secrets';
|
|
||||||
import { registerErrorHandlers } from './events/error';
|
|
||||||
|
|
||||||
registerErrorHandlers();
|
// console.log(`🟡 Launching bot ${client.shard?.ids[0]}`);
|
||||||
|
|
||||||
|
registerProcesses();
|
||||||
client.login(secrets.DISCORD_TOKEN);
|
client.login(secrets.DISCORD_TOKEN);
|
||||||
@@ -51,6 +51,7 @@ export const aboutCommand: CommandRegistry = {
|
|||||||
await Promise.all(COMMANDS_RAW)
|
await Promise.all(COMMANDS_RAW)
|
||||||
)
|
)
|
||||||
.filter((registry) => registry.inPublic && registry.inProduction)
|
.filter((registry) => registry.inPublic && registry.inProduction)
|
||||||
|
.sort((a, b) => (a.command.name < b.command.name ? -1 : 1))
|
||||||
.map(
|
.map(
|
||||||
(registry) =>
|
(registry) =>
|
||||||
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
`- \`/${registry.command.name}\`: ${registry.command.description}`,
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import * as Breakdown from '../components/Breakdown';
|
||||||
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
|
import addFilterOptions from '../core/discord/addFilterOptions';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import buttonPrimary from '../core/discord/buttonPrimary';
|
||||||
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
|
import resolvePlayerFromCommand, {
|
||||||
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
|
import getWN8Percentile from '../core/statistics/getWN8Percentile';
|
||||||
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const awaitedTankopedia = await tankopedia;
|
||||||
|
const awaitedTankAverages = await tankAverages;
|
||||||
|
const statsInPeriod = await getStatsInPeriod(region, id, start, end);
|
||||||
|
const { filteredOrder } = await filterStats(statsInPeriod, filters);
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clanData = await getClanAccountInfo(region, id, ['clan']);
|
||||||
|
const tankStats = await getTankStats(region, id);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters);
|
||||||
|
const orderedCurrentStats: AllStats[] = [];
|
||||||
|
const orderedCareerStats: AllStats[] = [];
|
||||||
|
const orderedCurrentWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8: (number | undefined)[] = [];
|
||||||
|
const orderedCareerWN8Full: (number | undefined)[] = [];
|
||||||
|
|
||||||
|
filteredOrder.forEach((id) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages[id];
|
||||||
|
const current = statsInPeriod.diff[id];
|
||||||
|
const career = tankStats.find((tank) => tank.tank_id === id)!.all;
|
||||||
|
const currentWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, current)
|
||||||
|
: undefined;
|
||||||
|
const careerWN8 = tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, career)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
orderedCurrentStats.push(current);
|
||||||
|
orderedCareerStats.push(career);
|
||||||
|
orderedCurrentWN8.push(currentWN8);
|
||||||
|
orderedCareerWN8.push(careerWN8);
|
||||||
|
});
|
||||||
|
tankStats.forEach((tank) => {
|
||||||
|
const tankAveragesEntry = awaitedTankAverages[tank.tank_id];
|
||||||
|
|
||||||
|
orderedCareerWN8Full.push(
|
||||||
|
tankAveragesEntry
|
||||||
|
? calculateWN8(tankAveragesEntry.all, tank.all)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentBattles = orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const careerBattles = tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.battles,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const currentWinrate =
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.wins,
|
||||||
|
0,
|
||||||
|
) / currentBattles;
|
||||||
|
const careerWinrate =
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.wins,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
const currentWN8 =
|
||||||
|
orderedCurrentWN8.reduce<number>(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
|
||||||
|
0,
|
||||||
|
) /
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator +
|
||||||
|
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const careerWN8 =
|
||||||
|
orderedCareerWN8Full.reduce<number>(
|
||||||
|
(accumulator, current) => accumulator + (current ?? 0),
|
||||||
|
0,
|
||||||
|
) /
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current, index) =>
|
||||||
|
accumulator +
|
||||||
|
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const currentDamage =
|
||||||
|
orderedCurrentStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / currentBattles;
|
||||||
|
const careerDamage =
|
||||||
|
tankStats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.all.damage_dealt,
|
||||||
|
0,
|
||||||
|
) / careerBattles;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={accountInfo.nickname}
|
||||||
|
image={
|
||||||
|
clanData?.clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{filteredOrder.length === 0 && (
|
||||||
|
<NoData type={NoDataType.BattlesInPeriod} />
|
||||||
|
)}
|
||||||
|
{filteredOrder.length > 0 && (
|
||||||
|
<Breakdown.Root>
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
type="summary"
|
||||||
|
title="Total"
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: 'Battles',
|
||||||
|
current: currentBattles,
|
||||||
|
career: careerBattles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Winrate',
|
||||||
|
current: `${(100 * currentWinrate).toFixed(2)}%`,
|
||||||
|
career: `${(100 * careerWinrate).toFixed(2)}%`,
|
||||||
|
delta: currentWinrate - careerWinrate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WN8',
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Damage',
|
||||||
|
current: Math.round(currentDamage).toLocaleString(),
|
||||||
|
career: Math.round(careerDamage).toLocaleString(),
|
||||||
|
delta: currentDamage - careerDamage,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{await Promise.all(
|
||||||
|
filteredOrder.map(async (id, index) => {
|
||||||
|
const tankopediaEntry = awaitedTankopedia[id];
|
||||||
|
const current = orderedCurrentStats[index];
|
||||||
|
const career = orderedCareerStats[index];
|
||||||
|
const currentWN8 = orderedCurrentWN8[index];
|
||||||
|
const careerWN8 = orderedCareerWN8[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breakdown.Row
|
||||||
|
key={id}
|
||||||
|
type="tank"
|
||||||
|
tankType={tankopediaEntry?.type}
|
||||||
|
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
|
||||||
|
title={await resolveTankName(id)}
|
||||||
|
stats={[
|
||||||
|
{
|
||||||
|
title: 'Battles',
|
||||||
|
current: current.battles.toLocaleString(),
|
||||||
|
career: career.battles.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Winrate',
|
||||||
|
current: `${(
|
||||||
|
100 *
|
||||||
|
(current.wins / current.battles)
|
||||||
|
).toFixed(2)}%`,
|
||||||
|
career: `${(100 * (career.wins / career.battles)).toFixed(
|
||||||
|
2,
|
||||||
|
)}%`,
|
||||||
|
delta:
|
||||||
|
current.wins / current.battles -
|
||||||
|
career.wins / career.battles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WN8',
|
||||||
|
current:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(currentWN8).toLocaleString(),
|
||||||
|
career:
|
||||||
|
careerWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(careerWN8).toLocaleString(),
|
||||||
|
percentile:
|
||||||
|
currentWN8 === undefined
|
||||||
|
? undefined
|
||||||
|
: getWN8Percentile(currentWN8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Damage',
|
||||||
|
current: Math.round(
|
||||||
|
current.damage_dealt / current.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
career: Math.round(
|
||||||
|
career.damage_dealt / career.battles,
|
||||||
|
).toLocaleString(),
|
||||||
|
delta:
|
||||||
|
current.damage_dealt / current.battles -
|
||||||
|
career.damage_dealt / career.battles,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
</Breakdown.Root>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const breakdownCommand = new Promise<CommandRegistryRaw>(
|
||||||
|
async (resolve) => {
|
||||||
|
const command = await addFilterOptions(
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('breakdown')
|
||||||
|
.setDescription("A period's breakdown by tanks played"),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
|
);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command,
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
|
const path = commandToURL(interaction, {
|
||||||
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
render(player, period, filters),
|
||||||
|
buttonPrimary(path, 'Refresh'),
|
||||||
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: (interaction) => {
|
||||||
|
autocompleteUsername(interaction);
|
||||||
|
autocompleteTanks(interaction);
|
||||||
|
},
|
||||||
|
|
||||||
|
async button(interaction) {
|
||||||
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
|
return await render(player, period, filters);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
+87
-91
@@ -1,97 +1,93 @@
|
|||||||
import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
|
// _____ _____ _ _ _ _ _____ ____ __ __ ______ ____ _____ _ __
|
||||||
import markdownEscape from 'markdown-escape';
|
// |_ _| / ____| | | | /\ | | | | / ____/ __ \| \/ | ____| | _ \ /\ / ____| |/ /
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
// | | | (___ | |__| | / \ | | | | | | | | | | \ / | |__ | |_) | / \ | | | ' /
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
// | | \___ \| __ | / /\ \ | | | | | | | | | | |\/| | __| | _ < / /\ \| | | <
|
||||||
import { PlayerHistoriesRaw } from '../core/blitzstars/getPlayerHistories';
|
// _| |_ ____) | | | |/ ____ \| |____| |____ | |___| |__| | | | | |____ | |_) / ____ \ |____| . \
|
||||||
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
|
// |_____| |_____/|_| |_/_/ \_\______|______| \_____\____/|_| |_|______| |____/_/ \_\_____|_|\_\
|
||||||
import addClanChoices from '../core/discord/addClanChoices';
|
|
||||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
|
||||||
import embedWarning from '../core/discord/embedWarning';
|
|
||||||
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
import { AccountAchievements } from '../types/accountAchievements';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { ClanInfo } from '../types/clanInfo';
|
|
||||||
|
|
||||||
export const clanEventCommand: CommandRegistry = {
|
// import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
|
||||||
inProduction: false,
|
// import markdownEscape from 'markdown-escape';
|
||||||
inPublic: true,
|
// import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
||||||
|
// import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
// import { getClanInfo } from '../core/blitz/getClanInfo';
|
||||||
|
// import { PlayerHistoriesRaw } from '../LEGACY_core/blitzstars/getPlayerHistories';
|
||||||
|
// import getTimeDaysAgo from '../LEGACY_core/blitzstars/getTimeDaysAgo';
|
||||||
|
// import addClanChoices from '../LEGACY_core/discord/addClanChoices';
|
||||||
|
// import autocompleteClan from '../LEGACY_core/discord/autocompleteClan';
|
||||||
|
// import embedWarning from '../LEGACY_core/discord/embedWarning';
|
||||||
|
// import resolveClanFromCommand from '../LEGACY_core/discord/resolveClanFromCommand';
|
||||||
|
// import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
// export const clanEventCommand: CommandRegistry = {
|
||||||
.setName('clan-event')
|
// inProduction: false,
|
||||||
.setDescription('Lists all inactive players')
|
// inPublic: true,
|
||||||
.addStringOption(addClanChoices),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
// command: new SlashCommandBuilder()
|
||||||
const { region, id } = await resolveClanFromCommand(interaction);
|
// .setName('clan-event')
|
||||||
const clan = (
|
// .setDescription('Lists all inactive players')
|
||||||
await getWargamingResponse<ClanInfo>(
|
// .addStringOption(addClanChoices),
|
||||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
|
||||||
)
|
|
||||||
)[id];
|
|
||||||
const memberIds = clan.members_ids.join(',');
|
|
||||||
const [players, achievements, previousJointVictories] = await Promise.all([
|
|
||||||
getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
|
|
||||||
),
|
|
||||||
getWargamingResponse<AccountAchievements>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
|
|
||||||
),
|
|
||||||
Promise.all(
|
|
||||||
clan.members_ids.map((id) =>
|
|
||||||
fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
|
|
||||||
.then((response) => response.json() as Promise<PlayerHistoriesRaw>)
|
|
||||||
.then((data) => ({
|
|
||||||
id,
|
|
||||||
jointVictory: data[0]?.achievements.jointVictoryCount as
|
|
||||||
| number
|
|
||||||
| undefined,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
).then((data) =>
|
|
||||||
data.reduce<Record<number, number | undefined>>(
|
|
||||||
(accumulator, value) => ({
|
|
||||||
...accumulator,
|
|
||||||
[value.id]: value.jointVictory,
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
const jointVictories = clan.members_ids.map((id) => ({
|
|
||||||
id,
|
|
||||||
joinVictory:
|
|
||||||
players[id].last_battle_time > getTimeDaysAgo(region, 1)
|
|
||||||
? previousJointVictories[id] === undefined
|
|
||||||
? 0
|
|
||||||
: achievements[id].max_series.jointVictory -
|
|
||||||
previousJointVictories[id]!
|
|
||||||
: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [
|
// async handler(interaction) {
|
||||||
`# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
|
// const { region, id } = await resolveClanFromCommand(interaction);
|
||||||
clan.tag,
|
// const clan = await getClanInfo(region, id);
|
||||||
)}]'s platoon wins today: ${(
|
// const [players, achievements, previousJointVictories] = await Promise.all([
|
||||||
jointVictories.reduce(
|
// getAccountInfo(region, clan.members_ids),
|
||||||
(accumulator, { joinVictory }) => accumulator + joinVictory,
|
// getAccountAchievements(region, clan.members_ids),
|
||||||
0,
|
// Promise.all(
|
||||||
) / 2
|
// clan.members_ids.map((id) =>
|
||||||
).toFixed(0)}\n${jointVictories
|
// fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
|
||||||
.filter(({ joinVictory }) => joinVictory)
|
// .then((response) => response.json() as Promise<PlayerHistoriesRaw>)
|
||||||
.sort((a, b) => b.joinVictory - a.joinVictory)
|
// .then((data) => ({
|
||||||
.map(
|
// id,
|
||||||
({ id, joinVictory }) =>
|
// jointVictory: data[0]?.achievements.jointVictoryCount as
|
||||||
`- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
|
// | number
|
||||||
)
|
// | undefined,
|
||||||
.join('\n')}`,
|
// })),
|
||||||
|
// ),
|
||||||
|
// ).then((data) =>
|
||||||
|
// data.reduce<Record<number, number | undefined>>(
|
||||||
|
// (accumulator, value) => ({
|
||||||
|
// ...accumulator,
|
||||||
|
// [value.id]: value.jointVictory,
|
||||||
|
// }),
|
||||||
|
// {},
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ]);
|
||||||
|
// const jointVictories = clan.members_ids.map((id) => ({
|
||||||
|
// id,
|
||||||
|
// joinVictory:
|
||||||
|
// players[id].last_battle_time > getTimeDaysAgo(region, 1)
|
||||||
|
// ? previousJointVictories[id] === undefined
|
||||||
|
// ? 0
|
||||||
|
// : achievements[id].max_series.jointVictory -
|
||||||
|
// previousJointVictories[id]!
|
||||||
|
// : 0,
|
||||||
|
// }));
|
||||||
|
|
||||||
embedWarning(
|
// return [
|
||||||
'This is an approximation!',
|
// `# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
|
||||||
"Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/about timezones` for more info)",
|
// clan.tag,
|
||||||
),
|
// )}]'s platoon wins today: ${(
|
||||||
];
|
// jointVictories.reduce(
|
||||||
},
|
// (accumulator, { joinVictory }) => accumulator + joinVictory,
|
||||||
|
// 0,
|
||||||
|
// ) / 2
|
||||||
|
// ).toFixed(0)}\n${jointVictories
|
||||||
|
// .filter(({ joinVictory }) => joinVictory)
|
||||||
|
// .sort((a, b) => b.joinVictory - a.joinVictory)
|
||||||
|
// .map(
|
||||||
|
// ({ id, joinVictory }) =>
|
||||||
|
// `- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
|
||||||
|
// )
|
||||||
|
// .join('\n')}`,
|
||||||
|
|
||||||
autocomplete: autocompleteClan,
|
// embedWarning(
|
||||||
};
|
// 'This is an approximation!',
|
||||||
|
// "Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/about timezones` for more info)",
|
||||||
|
// ),
|
||||||
|
// ];
|
||||||
|
// },
|
||||||
|
|
||||||
|
// autocomplete: autocompleteClan,
|
||||||
|
// };
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
import getClientId from '../core/blitzkrieg/getClientId';
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
import { client } from '../core/discord/client';
|
import { client } from '../core/discord/client';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable from '../core/discord/markdownTable';
|
import markdownTable from '../core/discord/markdownTable';
|
||||||
import getClientId from '../core/node/getClientId';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|
||||||
const executionStart = new Date().getTime();
|
const executionStart = new Date().getTime();
|
||||||
|
|||||||
+14
-18
@@ -1,20 +1,18 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import calculateWN8 from '../core/blitz/calculateWN8';
|
import getPeriodNow from '../core/blitzkrieg/getPeriodNow';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo';
|
||||||
import sumStats from '../core/blitz/sumStats';
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
import { tankopedia } from '../core/blitz/tankopedia';
|
|
||||||
import getDiffedTankStats from '../core/blitzstars/getDiffedTankStats';
|
|
||||||
import getPeriodNow from '../core/blitzstars/getPeriodNow';
|
|
||||||
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
|
|
||||||
import { tankAverages } from '../core/blitzstars/tankAverages';
|
import { tankAverages } from '../core/blitzstars/tankAverages';
|
||||||
|
import { tankopedia } from '../core/blitzstars/tankopedia';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import embedPositive from '../core/discord/embedPositive';
|
import embedPositive from '../core/discord/embedPositive';
|
||||||
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
import markdownTable, { TableInput } from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import calculateWN8 from '../core/statistics/calculateWN8';
|
||||||
|
import sumStats from '../core/statistics/sumStats';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PossiblyPromise } from '../types/possiblyPromise';
|
import { PossiblyPromise } from '../types/possiblyPromise';
|
||||||
|
|
||||||
export type SkilledClan = 'SKLLD' | 'SMRI';
|
export type SkilledClan = 'SKLLD' | 'SMRI';
|
||||||
@@ -48,20 +46,18 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const clan = interaction.options.getString('clan') as SkilledClan;
|
const clan = interaction.options.getString('clan') as SkilledClan;
|
||||||
const { id, region: server } = await resolvePlayerFromCommand(interaction);
|
const { id, region: region } = await resolvePlayerFromCommand(interaction);
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const problems: TableInput = [];
|
const problems: TableInput = [];
|
||||||
const title = `${accountInfo[id].nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
const title = `${accountInfo.nickname}'s eligibility for ${SKILLED_CLANS[clan]}`;
|
||||||
let isEligible = false;
|
let isEligible = false;
|
||||||
let body = '';
|
let body = '';
|
||||||
|
|
||||||
if (clan === 'SKLLD') {
|
if (clan === 'SKLLD') {
|
||||||
const { diffed } = await getDiffedTankStats(
|
const { diff: diffed } = await getStatsInPeriod(
|
||||||
server,
|
region,
|
||||||
id,
|
id,
|
||||||
getTimeDaysAgo(server, 30),
|
getTimeDaysAgo(region, 30),
|
||||||
getPeriodNow(),
|
getPeriodNow(),
|
||||||
);
|
);
|
||||||
const entries = Object.entries(diffed);
|
const entries = Object.entries(diffed);
|
||||||
@@ -171,7 +167,7 @@ export const eligibleCommand: CommandRegistry = {
|
|||||||
problems,
|
problems,
|
||||||
)}`;
|
)}`;
|
||||||
} else if (clan === 'SMRI') {
|
} else if (clan === 'SMRI') {
|
||||||
const stats = accountInfo[id].statistics.all;
|
const stats = accountInfo.statistics.all;
|
||||||
|
|
||||||
if (stats.battles < 1500) {
|
if (stats.battles < 1500) {
|
||||||
problems.push([
|
problems.push([
|
||||||
|
|||||||
+149
-47
@@ -1,31 +1,142 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import {
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
SlashCommandBuilder,
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
SlashCommandSubcommandGroupBuilder,
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
} from 'discord.js';
|
||||||
|
import * as Graph from '../components/Graph';
|
||||||
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
|
import TitleBar from '../components/TitleBar';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import resolveTankId from '../core/blitz/resolveTankId';
|
||||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import getTankHistories from '../core/blitzkrieg/getTankHistories';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getPlayerHistories from '../core/blitzstars/getPlayerHistories';
|
||||||
|
import addPeriodSubCommands from '../core/discord/addPeriodSubCommands';
|
||||||
|
import addTankChoices from '../core/discord/addTankChoices';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
import buttonPrimary from '../core/discord/buttonPrimary';
|
||||||
import linkButton from '../core/discord/linkButton';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
import primaryButton from '../core/discord/primaryButton';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand, {
|
||||||
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import evolution from '../renderers/evolution';
|
|
||||||
import { StatType } from '../renderers/stats';
|
type EvolutionStatType = 'player' | 'tank';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
type: 'player' | 'tank',
|
||||||
|
tankId: number | null,
|
||||||
|
) {
|
||||||
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const logo = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const histories = await (type === 'player'
|
||||||
|
? getPlayerHistories(region, id, {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
})
|
||||||
|
: getTankHistories(region, id, {
|
||||||
|
tankId: tankId!,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
includeLatestHistories: true,
|
||||||
|
includePreviousHistories: true,
|
||||||
|
}));
|
||||||
|
const plot = histories.map(
|
||||||
|
(history) =>
|
||||||
|
[
|
||||||
|
history.all.battles,
|
||||||
|
(history.all.wins / history.all.battles) * 100,
|
||||||
|
] as Graph.PlotItem,
|
||||||
|
);
|
||||||
|
const times = histories.map((history) => history.last_battle_time);
|
||||||
|
const minTime = Math.min(...times);
|
||||||
|
const maxTime = Math.max(...times);
|
||||||
|
const ys = plot.map(([, y]) => y);
|
||||||
|
const xs = plot.map(([x]) => x);
|
||||||
|
const maxY = Math.max(...ys);
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const maxX = Math.max(...xs);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar name={accountInfo.nickname} image={logo} description={name} />
|
||||||
|
|
||||||
|
{plot.length > 0 && (
|
||||||
|
<Graph.Root
|
||||||
|
verticalMargin={{
|
||||||
|
min: minY,
|
||||||
|
max: maxY,
|
||||||
|
suffix: '%',
|
||||||
|
precision: 2,
|
||||||
|
}}
|
||||||
|
horizontalMargin={{
|
||||||
|
min: minX,
|
||||||
|
max: maxX,
|
||||||
|
precision: 0,
|
||||||
|
}}
|
||||||
|
xMinLabel={new Date(minTime * 1000).toDateString()}
|
||||||
|
xMaxLabel={new Date(maxTime * 1000).toDateString()}
|
||||||
|
>
|
||||||
|
{Graph.Line({
|
||||||
|
color: Graph.LineColor.Red,
|
||||||
|
plot: plot,
|
||||||
|
minY,
|
||||||
|
maxY,
|
||||||
|
})}
|
||||||
|
</Graph.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addStatTypeSubCommandGroups(
|
let playerGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
new SlashCommandBuilder()
|
let tankGroup: SlashCommandSubcommandGroupBuilder;
|
||||||
.setName('evolution')
|
const command = new SlashCommandBuilder()
|
||||||
.setDescription('Evolution of statistics'),
|
.setName('evolution')
|
||||||
false,
|
.setDescription('Evolution of statistics')
|
||||||
);
|
.addSubcommandGroup((option) => {
|
||||||
|
playerGroup = option
|
||||||
|
.setName('player')
|
||||||
|
.setDescription('Player statistics');
|
||||||
|
|
||||||
|
return playerGroup;
|
||||||
|
})
|
||||||
|
.addSubcommandGroup((option) => {
|
||||||
|
tankGroup = option.setName('tank').setDescription('Tank statistics');
|
||||||
|
|
||||||
|
return tankGroup;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
addPeriodSubCommands(playerGroup!, (option) =>
|
||||||
|
option.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
addPeriodSubCommands(tankGroup!, (option) =>
|
||||||
|
option
|
||||||
|
.addStringOption(addTankChoices)
|
||||||
|
.addStringOption(addUsernameChoices),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -34,37 +145,28 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
|||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
const commandGroup =
|
||||||
true,
|
interaction.options.getSubcommandGroup() as EvolutionStatType;
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const period = resolvePeriodFromCommand(player.region, interaction);
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
const tankIdRaw = interaction.options.getString('tank')!;
|
||||||
const tankId =
|
const tankId =
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
||||||
const start = interaction.options.getInteger('start');
|
const path = commandToURL(interaction, {
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
...player,
|
||||||
|
...getCustomPeriodParams(interaction),
|
||||||
tankId,
|
tankId,
|
||||||
start,
|
|
||||||
end,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getBlitzStarsLinkButton;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
await evolution(commandGroup, period, player, tankId),
|
await render(player, period, commandGroup, tankId),
|
||||||
primaryButton(path, 'Refresh'),
|
buttonPrimary(path, 'Refresh'),
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
await getBlitzStarsLinkButton(
|
||||||
linkButton(
|
player.region,
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}${
|
player.id,
|
||||||
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
|
tankId ?? undefined,
|
||||||
}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -75,17 +177,17 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async button(interaction) {
|
async button(interaction) {
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
const url = new URL(`https://example.com/${interaction.customId}`);
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
const path = url.pathname.split('/');
|
||||||
const commandGroup = path[1] as StatType;
|
const commandGroup = path[2] as EvolutionStatType;
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
|
||||||
return await evolution(
|
return await render(
|
||||||
commandGroup,
|
|
||||||
period,
|
|
||||||
player,
|
player,
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
period,
|
||||||
|
commandGroup,
|
||||||
|
parseInt(url.searchParams.get('tankId') ?? '0') || null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+73
-74
@@ -1,30 +1,76 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { TreeTypeString } from '../components/Tanks';
|
import GenericAllStats from '../components/GenericAllStats';
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import TierWeights from '../components/TierWeights';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import TitleBar from '../components/TitleBar';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import Wrapper from '../components/Wrapper';
|
||||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
import addFilterOptions from '../core/discord/addFilterOptions';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import linkButton from '../core/discord/linkButton';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
import primaryButton from '../core/discord/primaryButton';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand, {
|
||||||
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
|
import { getTierWeights } from '../core/statistics/getTierWeights';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import fullStats from '../renderers/fullStats';
|
|
||||||
import { MultiTankFilters, StatType } from '../renderers/stats';
|
async function render(
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
|
||||||
|
const { stats, supplementary, filteredOrder } = await filterStats(
|
||||||
|
diffedTankStats,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters);
|
||||||
|
const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{stats.battles === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
{stats.battles > 0 && <TierWeights weights={tierWeights!} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<GenericAllStats stats={stats} supplementaryStats={supplementary} />
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const command = await addStatTypeSubCommandGroups(
|
const command = await addFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('full-stats')
|
.setName('full-stats')
|
||||||
.setDescription('Full in-game statistics'),
|
.setDescription('Full in-game statistics'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
@@ -34,60 +80,20 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
|
||||||
true,
|
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const resolvedPeriod = resolvePeriodFromCommand(
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
player.region,
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
interaction,
|
const path = commandToURL(interaction, {
|
||||||
);
|
|
||||||
const period = interaction.options.getString('period');
|
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
|
||||||
const tankId =
|
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
|
||||||
const start = interaction.options.getInteger('start');
|
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
...player,
|
||||||
tankId,
|
...getCustomPeriodParams(interaction),
|
||||||
start,
|
...filters,
|
||||||
end,
|
|
||||||
period,
|
|
||||||
});
|
});
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
|
|
||||||
const tierOption = interaction.options.getString('tier');
|
return Promise.all([
|
||||||
|
render(player, period, filters),
|
||||||
return [
|
buttonRefresh(interaction, path),
|
||||||
await fullStats(
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
commandGroup,
|
]);
|
||||||
resolvedPeriod,
|
|
||||||
player,
|
|
||||||
commandGroup === 'tank'
|
|
||||||
? tankId
|
|
||||||
: commandGroup === 'multi-tank'
|
|
||||||
? ({
|
|
||||||
'tank-type':
|
|
||||||
interaction.options.getString('tank-type') ?? undefined,
|
|
||||||
nation: interaction.options.getString('nation') ?? undefined,
|
|
||||||
tier: tierOption ? parseInt(tierOption) : undefined,
|
|
||||||
'tree-type': (interaction.options.getString('tree-type') ??
|
|
||||||
undefined) as TreeTypeString | undefined,
|
|
||||||
} satisfies Partial<MultiTankFilters>)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
|
||||||
linkButton(
|
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
autocomplete: (interaction) => {
|
||||||
@@ -96,18 +102,11 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async button(interaction) {
|
async button(interaction) {
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
|
||||||
const commandGroup = path[1] as StatType;
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await fullStats(
|
return await render(player, period, filters);
|
||||||
commandGroup,
|
|
||||||
period,
|
|
||||||
player,
|
|
||||||
parseInt(url.searchParams.get('tankId')!),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-23
@@ -3,14 +3,12 @@ import GenericStats from '../components/GenericStats';
|
|||||||
import NoData, { NoDataType } from '../components/NoData';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import { getClanInfo } from '../core/blitz/getClanInfo';
|
||||||
import addClanChoices from '../core/discord/addClanChoices';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import autocompleteClan from '../core/discord/autocompleteClan';
|
import autocompleteClan from '../core/discord/autocompleteClan';
|
||||||
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { ClanInfo } from '../types/clanInfo';
|
|
||||||
|
|
||||||
const DEFAULT_THRESHOLD = 7;
|
const DEFAULT_THRESHOLD = 7;
|
||||||
|
|
||||||
@@ -36,24 +34,16 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
const threshold =
|
const threshold =
|
||||||
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
|
||||||
const time = new Date().getTime() / 1000;
|
const time = new Date().getTime() / 1000;
|
||||||
const clanData = (
|
const clanInfo = await getClanInfo(region, id);
|
||||||
await getWargamingResponse<ClanInfo>(
|
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
|
||||||
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${id}`,
|
const inactive = accountInfo
|
||||||
|
.map(
|
||||||
|
(account) =>
|
||||||
|
[
|
||||||
|
account.nickname,
|
||||||
|
(time - account.last_battle_time) / 60 / 60 / 24,
|
||||||
|
] as [string, number],
|
||||||
)
|
)
|
||||||
)[id];
|
|
||||||
const memberIds = clanData.members_ids;
|
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${memberIds.join(
|
|
||||||
',',
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
const inactive = memberIds
|
|
||||||
.map((memberId) => {
|
|
||||||
const member = accounts[memberId];
|
|
||||||
const inactiveDays = (time - member.last_battle_time) / 60 / 60 / 24;
|
|
||||||
|
|
||||||
return [member.nickname, inactiveDays] as [string, number];
|
|
||||||
})
|
|
||||||
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
.filter(([, inactiveDays]) => inactiveDays >= threshold)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.map(
|
.map(
|
||||||
@@ -64,8 +54,8 @@ export const inactiveCommand: CommandRegistry = {
|
|||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={clanData.name}
|
name={clanInfo.name}
|
||||||
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData.emblem_set_id}.png`}
|
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
|
||||||
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
description={`Inactive for ${threshold}+ Days • ${new Date().toDateString()}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { teal } from '@radix-ui/colors';
|
||||||
|
import {
|
||||||
|
AttachmentBuilder,
|
||||||
|
GuildMemberRoleManager,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import markdownEscape from 'markdown-escape';
|
||||||
|
import discord from '../../discord.json' assert { type: 'json' };
|
||||||
|
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
|
||||||
|
import Wrapper from '../components/Wrapper';
|
||||||
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
|
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
||||||
|
|
||||||
|
export const verifyCommand: CommandRegistry = {
|
||||||
|
inProduction: true,
|
||||||
|
inPublic: true,
|
||||||
|
|
||||||
|
command: new SlashCommandBuilder()
|
||||||
|
.setName('link')
|
||||||
|
.setDescription('Links your Blitz and Discord account')
|
||||||
|
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
||||||
|
|
||||||
|
async handler(interaction) {
|
||||||
|
const username = interaction.options.getString('username', true);
|
||||||
|
|
||||||
|
if (serverAndIdPattern.test(username)) {
|
||||||
|
const [region, idString] = username.split('/');
|
||||||
|
const id = parseInt(idString);
|
||||||
|
const discordId = parseInt(interaction.user.id);
|
||||||
|
const accountInfo = await getAccountInfo(region as Region, id);
|
||||||
|
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [
|
||||||
|
'clan',
|
||||||
|
]);
|
||||||
|
|
||||||
|
await linkBlitzAndDiscord(discordId, region as Region, id);
|
||||||
|
|
||||||
|
if (interaction.guildId === discord.sklld_guild_id) {
|
||||||
|
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
||||||
|
return embedNegative(
|
||||||
|
`${markdownEscape(interaction.user.username)} failed to verify`,
|
||||||
|
"I don't have the permission to change your manage roles.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).remove(
|
||||||
|
discord.sklld_verify_role,
|
||||||
|
);
|
||||||
|
await (interaction.member!.roles as GuildMemberRoleManager).add(
|
||||||
|
discord.sklld_peasant_role,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper fat>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Glow color={teal.teal9} rotation={90} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
interaction.user.avatarURL({ extension: 'png' }) ??
|
||||||
|
'https://i.stack.imgur.com/l60Hf.png'
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interaction.user.displayName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
@{interaction.user.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
|
color: theme.colors.textLowContrast_teal,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://i.imgur.com/jIcRgog.png"
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 16 }}>Accounts linked</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan ? (
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: 32,
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
src={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanAccountInfo?.clan?.emblem_set_id}.png`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: theme.colors.textHighContrast,
|
||||||
|
fontWeight: 900,
|
||||||
|
maxWidth: 240,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{accountInfo.nickname}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.colors.textLowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{clanAccountInfo?.clan
|
||||||
|
? `[${clanAccountInfo?.clan?.tag}] - `
|
||||||
|
: ''}
|
||||||
|
{REGION_NAMES_SHORT[region as Region]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Glow color={teal.teal9} rotation={-90} />
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
embedNegative(
|
||||||
|
'Please select from the options',
|
||||||
|
'Follow the procedure like the video above. The search results may take a while to show up.',
|
||||||
|
),
|
||||||
|
new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
|
};
|
||||||
+10
-55
@@ -3,46 +3,24 @@ import NoData, { NoDataType } from '../components/NoData';
|
|||||||
import * as Tanks from '../components/Tanks';
|
import * as Tanks from '../components/Tanks';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getTankStats from '../core/blitz/getTankStats';
|
import getTankStats from '../core/blitz/getTankStats';
|
||||||
import getTreeType from '../core/blitz/getTreeType';
|
import getTreeType from '../core/blitz/getTreeType';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import resolveTankName from '../core/blitz/resolveTankName';
|
import resolveTankName from '../core/blitz/resolveTankName';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
import {
|
import {
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
TankopediaEntry,
|
TankopediaEntry,
|
||||||
Tier,
|
Tier,
|
||||||
tankopedia,
|
tankopedia,
|
||||||
tankopediaInfo,
|
tankopediaInfo,
|
||||||
} from '../core/blitz/tankopedia';
|
} from '../core/blitzstars/tankopedia';
|
||||||
import addTierChoices from '../core/discord/addTierChoices';
|
import addTierChoices from '../core/discord/addTierChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
|
||||||
|
|
||||||
const COMP_TANKS = [
|
|
||||||
// light tanks
|
|
||||||
24321, // t-100 lt
|
|
||||||
20257, // sheridan
|
|
||||||
3649, // bat chat
|
|
||||||
19537, // vickers
|
|
||||||
|
|
||||||
// heavy tanks
|
|
||||||
6145, // is-4
|
|
||||||
7169, // is-7
|
|
||||||
10785, // t110e5
|
|
||||||
12161, // strvk
|
|
||||||
4481, // kranvagn
|
|
||||||
22817, // m6 yoh
|
|
||||||
6225, // fv215b
|
|
||||||
5425, // wz 113
|
|
||||||
7297, // 60tp
|
|
||||||
58641, // vk 72
|
|
||||||
6753, // type 71
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ownedTanksCommand: CommandRegistry = {
|
export const ownedTanksCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -58,9 +36,7 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
const tier = Number(interaction.options.getString('tier'));
|
const tier = Number(interaction.options.getString('tier'));
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const { id, region: server } = account;
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(server, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const tankStats = await getTankStats(server, id);
|
const tankStats = await getTankStats(server, id);
|
||||||
const filteredTanks = (
|
const filteredTanks = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -70,9 +46,7 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).filter((tank) => tank.tankopedia?.tier === tier);
|
).filter((tank) => tank.tankopedia?.tier === tier);
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
const clanAccountInfo = await getClanAccountInfo(server, id, ['clan']);
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
|
||||||
);
|
|
||||||
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
const groupedTanks: Record<string, TankopediaEntry[]> = {};
|
||||||
const nations: string[] = [];
|
const nations: string[] = [];
|
||||||
|
|
||||||
@@ -90,18 +64,13 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo.nickname}
|
||||||
nameDiscriminator={
|
|
||||||
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
|
|
||||||
}
|
|
||||||
image={
|
image={
|
||||||
clanData[id]?.clan
|
clanAccountInfo?.clan
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
|
? emblemIdToURL(clanAccountInfo.clan.emblem_set_id)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
description={`Tier ${
|
description={`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]} tanks`}
|
||||||
TIER_ROMAN_NUMERALS[tier as Tier]
|
|
||||||
} tanks • ${new Date().toDateString()}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
{filteredTanks.length === 0 && <NoData type={NoDataType.TanksFound} />}
|
||||||
@@ -158,17 +127,3 @@ export const ownedTanksCommand: CommandRegistry = {
|
|||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
autocomplete: autocompleteUsername,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: make this work in the future
|
|
||||||
// console.log('Caching tank images...');
|
|
||||||
// TANKS.then((tanks) =>
|
|
||||||
// jsxToSvg(
|
|
||||||
// <div style={{ display: 'flex' }}>
|
|
||||||
// {tanks.map(({ images, tank_id }) => {
|
|
||||||
// return (
|
|
||||||
// images.normal && <img key={tank_id} src={images.normal} width={0} />
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </div>,
|
|
||||||
// ),
|
|
||||||
// ).then(() => console.log('Cached tank images'));
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { startCase } from 'lodash';
|
import { startCase } from 'lodash';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { getAccountAchievements } from '../core/blitz/getAccountAchievements';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable, { 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 { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountAchievements } from '../types/accountAchievements';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
|
||||||
type SortBy = 'name' | 'count';
|
type SortBy = 'name' | 'count';
|
||||||
|
|
||||||
@@ -35,22 +33,17 @@ export const playerAchievementsCommand: CommandRegistry = {
|
|||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const { id, region: region } = account;
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
const accountsAchievements = await getAccountAchievements(region, id);
|
||||||
);
|
const accountAchievements = accountsAchievements;
|
||||||
const accountsAchievements =
|
|
||||||
await getWargamingResponse<AccountAchievements>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const accountAchievements = accountsAchievements[id];
|
|
||||||
const compound = {
|
const compound = {
|
||||||
...accountAchievements.achievements,
|
...accountAchievements.achievements,
|
||||||
...accountAchievements.max_series,
|
...accountAchievements.max_series,
|
||||||
};
|
};
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`${markdownEscape(accounts[id].nickname)}'s information`,
|
`${markdownEscape(accountInfo.nickname)}'s information`,
|
||||||
|
|
||||||
markdownTable(
|
markdownTable(
|
||||||
Object.keys(compound)
|
Object.keys(compound)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import markdownTable from '../core/discord/markdownTable';
|
import markdownTable from '../core/discord/markdownTable';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
|
||||||
export const playerInfoCommand: CommandRegistry = {
|
export const playerInfoCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -21,11 +19,8 @@ export const playerInfoCommand: CommandRegistry = {
|
|||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const account = await resolvePlayerFromCommand(interaction);
|
const account = await resolvePlayerFromCommand(interaction);
|
||||||
const { id, region: server } = account;
|
const { id, region: region } = account;
|
||||||
const accounts = await getWargamingResponse<AccountInfo>(
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const accountInfo = accounts[id];
|
|
||||||
|
|
||||||
return embedInfo(
|
return embedInfo(
|
||||||
`${markdownEscape(accountInfo.nickname)}'s information`,
|
`${markdownEscape(accountInfo.nickname)}'s information`,
|
||||||
|
|||||||
+100
-114
@@ -9,23 +9,22 @@ import * as Leaderboard from '../components/Leaderboard';
|
|||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
import getRatingsInfo from '../core/blitz/getRatingsInfo';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||||
|
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
|
||||||
import getMidnightLeaderboard, {
|
import getMidnightLeaderboard, {
|
||||||
DATABASE_REPO,
|
DATABASE_REPO,
|
||||||
} from '../core/database/getMidnightLeaderboard';
|
} from '../core/blitzkrieg/getMidnightLeaderboard';
|
||||||
|
import { octokit } from '../core/blitzkrieg/octokit';
|
||||||
|
import { UserError } from '../core/blitzkrieg/userError';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import embedNegative from '../core/discord/embedNegative';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
import { octokit } from '../core/github/octokit';
|
|
||||||
import throwError from '../core/node/throwError';
|
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
|
||||||
|
|
||||||
export interface RatingsPlayer {
|
export interface RatingsPlayer {
|
||||||
spa_id: number;
|
spa_id: number;
|
||||||
@@ -172,6 +171,11 @@ function addSubcommands(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noOngoingSeason = embedNegative(
|
||||||
|
'No ongoing season',
|
||||||
|
"Wargaming didn't provide any data for this season.",
|
||||||
|
);
|
||||||
|
|
||||||
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const seasonChoices = await octokit.repos
|
const seasonChoices = await octokit.repos
|
||||||
@@ -181,7 +185,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
throw throwError('Archived ratings data is malformed');
|
throw new Error('Archived ratings data is malformed');
|
||||||
}
|
}
|
||||||
return data.map(
|
return data.map(
|
||||||
(folder) =>
|
(folder) =>
|
||||||
@@ -259,27 +263,28 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
regionRatingsInfo = await (subcommandGroup === 'current'
|
regionRatingsInfo = await (subcommandGroup === 'current'
|
||||||
? getRatingsInfo(region)
|
? getRatingsInfo(region)
|
||||||
: getArchivedRatingsInfo(season!, region));
|
: getArchivedRatingsInfo(season!, region));
|
||||||
|
|
||||||
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
|
|
||||||
const regionSubdomain = regionToRegionSubdomain(region);
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
const result =
|
const result =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await fetch(
|
||||||
? await fetch(
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
|
)
|
||||||
|
.then((response) => response.json() as Promise<LeagueTop>)
|
||||||
|
.then(async ({ result }) =>
|
||||||
|
result.slice(0, limit).map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
score: player.score,
|
||||||
|
position: player.number,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.then((response) => response.json() as Promise<LeagueTop>)
|
|
||||||
.then(async ({ result }) =>
|
|
||||||
result.slice(0, limit).map(
|
|
||||||
(player) =>
|
|
||||||
({
|
|
||||||
id: player.spa_id,
|
|
||||||
score: player.score,
|
|
||||||
position: player.number,
|
|
||||||
clan: player.clan_tag,
|
|
||||||
nickname: player.nickname,
|
|
||||||
}) satisfies SimplifiedPlayer,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
: await octokit.repos
|
: await octokit.repos
|
||||||
.getContent({
|
.getContent({
|
||||||
...DATABASE_REPO,
|
...DATABASE_REPO,
|
||||||
@@ -287,7 +292,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(async ({ data }) => {
|
.then(async ({ data }) => {
|
||||||
if (Array.isArray(data) || data.type !== 'file') {
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
throw throwError('Archived ratings data is malformed');
|
throw new Error('Archived ratings data is malformed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const players = (await fetch(data.download_url!).then(
|
const players = (await fetch(data.download_url!).then(
|
||||||
@@ -305,9 +310,10 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
firstPlayerIndex,
|
firstPlayerIndex,
|
||||||
lastPlayerIndex + 1,
|
lastPlayerIndex + 1,
|
||||||
);
|
);
|
||||||
const ids = trimmed.map((player) => player.id).join(',');
|
const clanData = await getClanAccountInfo(
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
region,
|
||||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
trimmed.map(({ id }) => id),
|
||||||
|
['clan'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return trimmed.map(
|
return trimmed.map(
|
||||||
@@ -323,34 +329,25 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
}) satisfies SimplifiedPlayer,
|
}) satisfies SimplifiedPlayer,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const leagueInfo =
|
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
|
||||||
regionRatingsInfo.detail === undefined
|
|
||||||
? regionRatingsInfo.leagues[leagueIndex]
|
|
||||||
: undefined;
|
|
||||||
midnightLeaderboard =
|
midnightLeaderboard =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await getMidnightLeaderboard(
|
||||||
? await getMidnightLeaderboard(
|
region,
|
||||||
region,
|
regionRatingsInfo.current_season,
|
||||||
regionRatingsInfo.current_season,
|
)
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
: await getMidnightLeaderboard(region, season!);
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
players = result;
|
players = result;
|
||||||
playersBefore = result ? result[0].position - 1 : 0;
|
playersBefore = result ? result[0].position - 1 : 0;
|
||||||
playersAfter =
|
playersAfter =
|
||||||
regionRatingsInfo.detail === undefined && result !== undefined
|
regionRatingsInfo.count - result[result.length - 1].position;
|
||||||
? regionRatingsInfo.count - result[result.length - 1].position
|
|
||||||
: 0;
|
|
||||||
titleName = `${
|
titleName = `${
|
||||||
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
|
||||||
} - ${REGION_NAMES_SHORT[region]}`;
|
} - ${REGION_NAMES_SHORT[region]}`;
|
||||||
titleImage = leagueInfo
|
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||||
? leagueInfo.big_icon.startsWith('http')
|
? leagueInfo.big_icon
|
||||||
? leagueInfo.big_icon
|
: `https:${leagueInfo.big_icon}`;
|
||||||
: `https:${leagueInfo.big_icon}`
|
|
||||||
: undefined;
|
|
||||||
titleDescription = `Top ${limit} players`;
|
titleDescription = `Top ${limit} players`;
|
||||||
} else {
|
} else {
|
||||||
const { region, id } = await resolvePlayerFromCommand(interaction);
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
@@ -364,7 +361,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(async ({ data }) => {
|
.then(async ({ data }) => {
|
||||||
if (Array.isArray(data) || data.type !== 'file') {
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
throw throwError('Archived ratings info is malformed');
|
throw new Error('Archived ratings info is malformed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = fetch(data.download_url!);
|
const response = fetch(data.download_url!);
|
||||||
@@ -372,40 +369,34 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
response.json(),
|
response.json(),
|
||||||
)) as RatingsInfo;
|
)) as RatingsInfo;
|
||||||
});
|
});
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
|
||||||
);
|
|
||||||
const clan = (
|
const accountInfo = await getAccountInfo(region, id);
|
||||||
await getWargamingResponse<PlayerClanData>(
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
|
||||||
)
|
|
||||||
)[id]?.clan;
|
|
||||||
const regionSubdomain = regionToRegionSubdomain(region);
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
const neighbors =
|
const neighbors =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await fetch(
|
||||||
? await fetch(
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
limit / 2,
|
||||||
limit / 2,
|
)}`,
|
||||||
)}`,
|
)
|
||||||
|
.then(
|
||||||
|
(response) => response.json() as Promise<RatingsNeighbors>,
|
||||||
|
)
|
||||||
|
.then((players) =>
|
||||||
|
players.neighbors.map(
|
||||||
|
(player) =>
|
||||||
|
({
|
||||||
|
id: player.spa_id,
|
||||||
|
position: player.number,
|
||||||
|
score: player.score,
|
||||||
|
clan: player.clan_tag,
|
||||||
|
nickname: player.nickname,
|
||||||
|
}) satisfies SimplifiedPlayer,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.then(
|
|
||||||
(response) =>
|
|
||||||
response.json() as Promise<RatingsNeighbors>,
|
|
||||||
)
|
|
||||||
.then((players) =>
|
|
||||||
players.neighbors.map(
|
|
||||||
(player) =>
|
|
||||||
({
|
|
||||||
id: player.spa_id,
|
|
||||||
position: player.number,
|
|
||||||
score: player.score,
|
|
||||||
clan: player.clan_tag,
|
|
||||||
nickname: player.nickname,
|
|
||||||
}) satisfies SimplifiedPlayer,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
: await octokit.repos
|
: await octokit.repos
|
||||||
.getContent({
|
.getContent({
|
||||||
...DATABASE_REPO,
|
...DATABASE_REPO,
|
||||||
@@ -413,7 +404,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
})
|
})
|
||||||
.then(async ({ data }) => {
|
.then(async ({ data }) => {
|
||||||
if (Array.isArray(data) || data.type !== 'file') {
|
if (Array.isArray(data) || data.type !== 'file') {
|
||||||
throw throwError(
|
throw new Error(
|
||||||
'Archived ratings latest data is malformed',
|
'Archived ratings latest data is malformed',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -427,9 +418,12 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (playerIndex === -1) {
|
if (playerIndex === -1) {
|
||||||
throw throwError(
|
throw new UserError(
|
||||||
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
|
`${accountInfo.nickname} didn't player ratings in season ${season}`,
|
||||||
'This player did not participate in this season or did not get past calibration.',
|
{
|
||||||
|
cause:
|
||||||
|
'This player did not participate in this season or did not get past calibration.',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,9 +432,10 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
playerIndex - halfRange,
|
playerIndex - halfRange,
|
||||||
playerIndex + halfRange + 1,
|
playerIndex + halfRange + 1,
|
||||||
);
|
);
|
||||||
const ids = trimmed.map((player) => player.id).join(',');
|
const clanData = await getClanAccountInfo(
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
region,
|
||||||
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
|
trimmed.map((player) => player.id),
|
||||||
|
['clan'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return trimmed.map((player, index) => ({
|
return trimmed.map((player, index) => ({
|
||||||
@@ -453,46 +448,38 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
});
|
});
|
||||||
midnightLeaderboard =
|
midnightLeaderboard =
|
||||||
subcommandGroup === 'current'
|
subcommandGroup === 'current'
|
||||||
? regionRatingsInfo.detail === undefined
|
? await getMidnightLeaderboard(
|
||||||
? await getMidnightLeaderboard(
|
region,
|
||||||
region,
|
regionRatingsInfo.current_season,
|
||||||
regionRatingsInfo.current_season,
|
)
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
: await getMidnightLeaderboard(region, season!);
|
: await getMidnightLeaderboard(region, season!);
|
||||||
|
|
||||||
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
|
||||||
|
|
||||||
players = neighbors;
|
players = neighbors;
|
||||||
titleImage = neighbors
|
titleImage = clan
|
||||||
? clan
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
|
||||||
: undefined
|
|
||||||
: undefined;
|
: undefined;
|
||||||
titleName = neighbors
|
titleName = neighbors
|
||||||
? accountInfo[id].nickname
|
? accountInfo.nickname
|
||||||
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
|
||||||
titleDescription = 'Ratings neighbours';
|
titleDescription = 'Ratings neighbours';
|
||||||
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
|
||||||
playersAfter =
|
playersAfter =
|
||||||
regionRatingsInfo.detail === undefined
|
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
|
||||||
? neighbors
|
|
||||||
? regionRatingsInfo.count -
|
|
||||||
neighbors[neighbors.length - 1].position
|
|
||||||
: 0
|
|
||||||
: 0;
|
|
||||||
playerId = id;
|
playerId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = players?.map((player) => {
|
const resolvedRegionRatingsInfo: typeof regionRatingsInfo & {
|
||||||
const reward =
|
detail: undefined;
|
||||||
regionRatingsInfo.detail === undefined
|
} = regionRatingsInfo;
|
||||||
? regionRatingsInfo.rewards.find(
|
|
||||||
(reward) =>
|
const items = players.map((player) => {
|
||||||
player.position >= reward.from_position &&
|
const reward = resolvedRegionRatingsInfo.rewards.find(
|
||||||
player.position <= reward.to_position,
|
(reward) =>
|
||||||
)
|
player.position >= reward.from_position &&
|
||||||
: undefined;
|
player.position <= reward.to_position,
|
||||||
|
);
|
||||||
const midnightIndex = midnightLeaderboard?.findIndex(
|
const midnightIndex = midnightLeaderboard?.findIndex(
|
||||||
(item) => item.id === player.id,
|
(item) => item.id === player.id,
|
||||||
);
|
);
|
||||||
@@ -523,8 +510,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
<TitleBar
|
<TitleBar
|
||||||
name={titleName}
|
name={titleName}
|
||||||
image={titleImage}
|
image={titleImage}
|
||||||
nameDiscriminator={titleNameDiscriminator}
|
description={titleDescription}
|
||||||
description={`${titleDescription} • ${new Date().toDateString()}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{items && (
|
{items && (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
|
|||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { REGION_NAMES, Region } from '../constants/regions';
|
import { REGION_NAMES, Region } from '../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import fetchBlitz from '../core/blitz/fetchWargaming';
|
||||||
import addClanChoices from '../core/discord/addClanChoices';
|
import addClanChoices from '../core/discord/addClanChoices';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
@@ -30,7 +30,7 @@ export const searchClansCommand: CommandRegistry = {
|
|||||||
const server = interaction.options.getString('region') as Region;
|
const server = interaction.options.getString('region') as Region;
|
||||||
const clan = interaction.options.getString('clan')!;
|
const clan = interaction.options.getString('clan')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const clanList = await getWargamingResponse<ClanList>(
|
const clanList = await fetchBlitz<ClanList>(
|
||||||
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { SlashCommandBuilder } from 'discord.js';
|
|||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { REGION_NAMES, Region } from '../constants/regions';
|
import { REGION_NAMES, Region } from '../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import fetchBlitz from '../core/blitz/fetchWargaming';
|
||||||
import { AccountList, usernamePattern } from '../core/blitz/listPlayers';
|
import {
|
||||||
|
AccountList,
|
||||||
|
usernamePattern,
|
||||||
|
} from '../core/blitz/searchPlayersAcrossRegions';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
@@ -34,7 +37,7 @@ export const searchPlayersCommand: CommandRegistry = {
|
|||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const trimmedSearch = name.trim();
|
const trimmedSearch = name.trim();
|
||||||
const players = usernamePattern.test(trimmedSearch)
|
const players = usernamePattern.test(trimmedSearch)
|
||||||
? await getWargamingResponse<AccountList>(
|
? await fetchBlitz<AccountList>(
|
||||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
|
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodeURIComponent(
|
||||||
trimmedSearch,
|
trimmedSearch,
|
||||||
)}&limit=${limit}`,
|
)}&limit=${limit}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { TANK_NAMES_DIACRITICS } from '../core/blitz/tankopedia';
|
import { TANK_NAMES_DIACRITICS } from '../core/blitzstars/tankopedia';
|
||||||
import addTankChoices from '../core/discord/addTankChoices';
|
import addTankChoices from '../core/discord/addTankChoices';
|
||||||
import embedInfo from '../core/discord/embedInfo';
|
import embedInfo from '../core/discord/embedInfo';
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
|
|||||||
+66
-82
@@ -1,28 +1,68 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { TreeTypeString } from '../components/Tanks';
|
import AllStatsOverview from '../components/AllStatsOverview';
|
||||||
import { CYCLIC_API } from '../constants/cyclic';
|
import NoData, { NoDataType } from '../components/NoData';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
import TitleBar from '../components/TitleBar';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
import Wrapper from '../components/Wrapper';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId';
|
import { getAccountInfo } from '../core/blitz/getAccountInfo';
|
||||||
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups';
|
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
|
||||||
|
import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
|
||||||
|
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
|
||||||
|
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
|
||||||
|
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
|
||||||
|
import addFilterOptions from '../core/discord/addFilterOptions';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
import autocompleteTanks from '../core/discord/autocompleteTanks';
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
import { buttonRefresh } from '../core/discord/buttonRefresh';
|
||||||
import linkButton from '../core/discord/linkButton';
|
import commandToURL from '../core/discord/commandToURL';
|
||||||
import primaryButton from '../core/discord/primaryButton';
|
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
|
||||||
|
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
|
||||||
|
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
|
||||||
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
|
||||||
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
|
import resolvePeriodFromCommand, {
|
||||||
|
ResolvedPeriod,
|
||||||
|
} from '../core/discord/resolvePeriodFromCommand';
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
import resolvePlayerFromCommand, {
|
||||||
|
ResolvedPlayer,
|
||||||
|
} from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { StatFilters, filterStats } from '../core/statistics/filterStats';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { CommandRegistryRaw } from '../events/interactionCreate';
|
||||||
import stats, { MultiTankFilters, StatType } from '../renderers/stats';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
async function render(
|
||||||
|
{ region, id }: ResolvedPlayer,
|
||||||
|
{ start, end, name }: ResolvedPeriod,
|
||||||
|
filters: StatFilters,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
|
||||||
|
const clanImage = clan ? emblemIdToURL(clan.emblem_set_id) : undefined;
|
||||||
|
const diffedTankStats = await getStatsInPeriod(region, id, start, end);
|
||||||
|
const { stats, supplementary } = await filterStats(diffedTankStats, filters);
|
||||||
|
const filterDescriptions = await filtersToDescription(filters);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<TitleBar
|
||||||
|
name={nickname}
|
||||||
|
image={clanImage}
|
||||||
|
description={`${name} • ${filterDescriptions}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!stats.battles && <NoData type={NoDataType.BattlesInPeriod} />}
|
||||||
|
{stats.battles > 0 && (
|
||||||
|
<AllStatsOverview stats={stats} supplementaryStats={supplementary} />
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
||||||
const command = await addStatTypeSubCommandGroups(
|
const command = await addFilterOptions(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName('stats')
|
.setName('stats')
|
||||||
.setDescription('Regular battles statistics'),
|
.setDescription('Regular battles statistics'),
|
||||||
|
(option) => option.addStringOption(addUsernameChoices),
|
||||||
);
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
@@ -32,60 +72,20 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
|||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const commandGroup = interaction.options.getSubcommandGroup(
|
|
||||||
true,
|
|
||||||
) as StatType;
|
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
const player = await resolvePlayerFromCommand(interaction);
|
||||||
const resolvedPeriod = resolvePeriodFromCommand(
|
const period = resolvePeriodFromCommand(player.region, interaction);
|
||||||
player.region,
|
const filters = await getFiltersFromCommand(interaction);
|
||||||
interaction,
|
const path = commandToURL(interaction, {
|
||||||
);
|
|
||||||
const period = interaction.options.getString('period');
|
|
||||||
const tankIdRaw = interaction.options.getString('tank')!;
|
|
||||||
const tankId =
|
|
||||||
commandGroup === 'tank' ? await resolveTankId(tankIdRaw) : null;
|
|
||||||
const start = interaction.options.getInteger('start');
|
|
||||||
const end = interaction.options.getInteger('end');
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
...player,
|
||||||
tankId,
|
...getCustomPeriodParams(interaction),
|
||||||
start,
|
...filters,
|
||||||
end,
|
|
||||||
period,
|
|
||||||
});
|
});
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
|
|
||||||
const tierOption = interaction.options.getString('tier');
|
return Promise.all([
|
||||||
|
render(player, period, filters),
|
||||||
return [
|
buttonRefresh(interaction, path),
|
||||||
await stats(
|
getBlitzStarsLinkButton(player.region, player.id),
|
||||||
commandGroup,
|
]);
|
||||||
resolvedPeriod,
|
|
||||||
player,
|
|
||||||
commandGroup === 'tank'
|
|
||||||
? tankId
|
|
||||||
: commandGroup === 'multi-tank'
|
|
||||||
? ({
|
|
||||||
'tank-type':
|
|
||||||
interaction.options.getString('tank-type') ?? undefined,
|
|
||||||
nation: interaction.options.getString('nation') ?? undefined,
|
|
||||||
tier: tierOption ? parseInt(tierOption) : undefined,
|
|
||||||
'tree-type': (interaction.options.getString('tree-type') ??
|
|
||||||
undefined) as TreeTypeString | undefined,
|
|
||||||
} satisfies Partial<MultiTankFilters>)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
|
||||||
linkButton(
|
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
autocomplete: (interaction) => {
|
autocomplete: (interaction) => {
|
||||||
@@ -94,27 +94,11 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async button(interaction) {
|
async button(interaction) {
|
||||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
|
||||||
const path = url.pathname.split('/').filter(Boolean);
|
|
||||||
const commandGroup = path[1] as StatType;
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
const player = await resolvePlayerFromButton(interaction);
|
||||||
const period = resolvePeriodFromButton(player.region, interaction);
|
const period = resolvePeriodFromButton(player.region, interaction);
|
||||||
|
const filters = getFiltersFromButton(interaction);
|
||||||
|
|
||||||
return await stats(
|
return await render(player, period, filters);
|
||||||
commandGroup,
|
|
||||||
period,
|
|
||||||
player,
|
|
||||||
commandGroup === 'tank'
|
|
||||||
? parseInt(url.searchParams.get('tankId')!)
|
|
||||||
: commandGroup === 'multi-tank'
|
|
||||||
? ({
|
|
||||||
'tank-type': url.searchParams.get('tank-type')!,
|
|
||||||
nation: url.searchParams.get('nation')!,
|
|
||||||
tier: parseInt(url.searchParams.get('tier')!),
|
|
||||||
'tree-type': url.searchParams.get('tree-type') as TreeTypeString,
|
|
||||||
} satisfies MultiTankFilters)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+6
-71
@@ -1,83 +1,18 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
|
||||||
import interactionToURL from '../core/discord/interactionToURL';
|
|
||||||
import linkButton from '../core/discord/linkButton';
|
|
||||||
import primaryButton from '../core/discord/primaryButton';
|
|
||||||
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
|
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
import { CommandRegistry } from '../events/interactionCreate';
|
||||||
import today from '../renderers/today';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: @deprecated
|
||||||
|
*/
|
||||||
export const todayCommand: CommandRegistry = {
|
export const todayCommand: CommandRegistry = {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
.setName('today')
|
.setName('today')
|
||||||
.setDescription('A general daily breakdown of your performance')
|
.setDescription('This command has been renamed to `/breakdown today`'),
|
||||||
.addStringOption(addUsernameChoices)
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('cutoff')
|
|
||||||
.setDescription(
|
|
||||||
'The maximum number of tanks to display (default: Infinity)',
|
|
||||||
)
|
|
||||||
.setRequired(false)
|
|
||||||
.setMinValue(1),
|
|
||||||
)
|
|
||||||
.addIntegerOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('maximized')
|
|
||||||
.setDescription(
|
|
||||||
'The number of rows with full stats after which all are collapsed (default: 4)',
|
|
||||||
)
|
|
||||||
.setMinValue(0)
|
|
||||||
.setRequired(false),
|
|
||||||
)
|
|
||||||
.addBooleanOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('show-total')
|
|
||||||
.setDescription('Show the total stats at the top (default: true)')
|
|
||||||
.setRequired(false),
|
|
||||||
),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
handler() {
|
||||||
const player = await resolvePlayerFromCommand(interaction);
|
return '# This command has been renamed to `/breakdown today`\n`/today` now comes with filters, periods and more! Use `/breakdown today` for same old functionality but also do checkout the other options.';
|
||||||
const cutoff = interaction.options.getInteger('cutoff') ?? undefined;
|
|
||||||
const maximized = interaction.options.getInteger('maximized') ?? undefined;
|
|
||||||
const showTotal = interaction.options.getBoolean('show-total') ?? undefined;
|
|
||||||
const { nickname } = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
|
|
||||||
)
|
|
||||||
)[player.id];
|
|
||||||
const path = interactionToURL(interaction, {
|
|
||||||
...player,
|
|
||||||
cutoff,
|
|
||||||
maximized,
|
|
||||||
'show-total': showTotal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
await today(player, cutoff, maximized, showTotal, false),
|
|
||||||
primaryButton(path, 'Refresh'),
|
|
||||||
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
|
|
||||||
linkButton(
|
|
||||||
`https://www.blitzstars.com/player/${player.region}/${nickname}`,
|
|
||||||
'BlitzStars',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
|
||||||
|
|
||||||
async button(interaction) {
|
|
||||||
const player = await resolvePlayerFromButton(interaction);
|
|
||||||
|
|
||||||
return await today(player);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
|
||||||
import markdownEscape from 'markdown-escape';
|
|
||||||
import discord from '../../discord.json' assert { type: 'json' };
|
|
||||||
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
|
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
|
||||||
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
|
||||||
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
|
||||||
import embedNegative from '../core/discord/embedNegative';
|
|
||||||
import embedPositive from '../core/discord/embedPositive';
|
|
||||||
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
|
||||||
import { CommandRegistry } from '../events/interactionCreate';
|
|
||||||
import { AccountInfo } from '../types/accountInfo';
|
|
||||||
import { PlayerClanData } from '../types/playerClanData';
|
|
||||||
|
|
||||||
export const verifyCommand: CommandRegistry = {
|
|
||||||
inProduction: true,
|
|
||||||
inPublic: true,
|
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
|
||||||
.setName('verify')
|
|
||||||
.setDescription("Set's the user's username to their in-game name")
|
|
||||||
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
|
|
||||||
|
|
||||||
async handler(interaction) {
|
|
||||||
const blitzAccount = await resolvePlayerFromCommand(interaction);
|
|
||||||
const { id, region: server } = blitzAccount;
|
|
||||||
const accountInfo = await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
);
|
|
||||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
|
||||||
);
|
|
||||||
const clan = clanData[id]?.clan;
|
|
||||||
const clanTag = clan ? ` [${clan!.tag}]` : '';
|
|
||||||
const member =
|
|
||||||
interaction.member &&
|
|
||||||
interaction.guild?.members.cache.get(interaction.member?.user.id);
|
|
||||||
|
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageNicknames')) {
|
|
||||||
return embedNegative(
|
|
||||||
`${markdownEscape(interaction.user.username)} failed to verify`,
|
|
||||||
"I don't have the permission to change your nickname.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!interaction.member || !member) {
|
|
||||||
return embedNegative(
|
|
||||||
`${interaction.user.username} is not in this server`,
|
|
||||||
"I couldn't find this person in this server. Did they leave?",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await member.setNickname(`${accountInfo[id].nickname}${clanTag}`);
|
|
||||||
|
|
||||||
if (interaction.guildId === discord.sklld_guild_id) {
|
|
||||||
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
|
|
||||||
return embedNegative(
|
|
||||||
`${markdownEscape(interaction.user.username)} failed to verify`,
|
|
||||||
"I don't have the permission to change your manage roles.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await member.roles.remove(discord.sklld_verify_role);
|
|
||||||
await member.roles.add(discord.sklld_peasant_role);
|
|
||||||
}
|
|
||||||
|
|
||||||
return embedPositive(
|
|
||||||
`${interaction.user.username} is verified`,
|
|
||||||
`The user is now verified as ${markdownEscape(
|
|
||||||
accountInfo[id].nickname,
|
|
||||||
)}${markdownEscape(clanTag)}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return embedNegative(
|
|
||||||
`${interaction.user.username} failed to verify`,
|
|
||||||
"I can't change your nickname because you have higher permissions than me. Try setting your nickname to your Blitz username manually.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: autocompleteUsername,
|
|
||||||
};
|
|
||||||
@@ -1,31 +1,28 @@
|
|||||||
import { Percentile } from '../../../../../constants/percentiles';
|
|
||||||
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator';
|
|
||||||
|
|
||||||
export enum GlowSide {
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GlowProps {
|
export interface GlowProps {
|
||||||
percentile: Percentile;
|
color: string;
|
||||||
side: GlowSide;
|
direction?: 'default' | 'reverse';
|
||||||
|
rotation?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Glow({ percentile, side }: GlowProps) {
|
export function Glow({
|
||||||
const color = PERCENTILE_COLORS[percentile];
|
color,
|
||||||
|
direction = 'default',
|
||||||
|
rotation = 0,
|
||||||
|
}: GlowProps) {
|
||||||
const color1 = color;
|
const color1 = color;
|
||||||
const color2 = `${color}00`;
|
const color2 = `${color}00`;
|
||||||
|
|
||||||
const background = `linear-gradient(${
|
const background = `linear-gradient(${
|
||||||
side === GlowSide.Top ? 0 : 180
|
direction === 'default' ? 0 : 180
|
||||||
}deg, ${color1} 0%, ${color2} 100%)`;
|
}deg, ${color1} 0%, ${color2} 100%)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: side === GlowSide.Top ? 'flex-end' : 'flex-start',
|
alignItems: direction === 'default' ? 'flex-end' : 'flex-start',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ height: 54, width: 16, background }} />
|
<div style={{ height: 54, width: 16, background }} />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Percentile } from '../../../../constants/percentiles';
|
import { Percentile } from '../../../../constants/percentiles';
|
||||||
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile';
|
import getWN8Percentile from '../../../../core/statistics/getWN8Percentile';
|
||||||
import { theme } from '../../../../stitches.config';
|
import { theme } from '../../../../stitches.config';
|
||||||
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
|
import { PERCENTILE_COLORS } from '../../../PercentileIndicator';
|
||||||
import { Glow, GlowSide } from './components/Glow';
|
import { Glow } from './components/Glow';
|
||||||
|
|
||||||
export interface WN8DisplayProps {
|
export interface WN8DisplayProps {
|
||||||
WN8?: number;
|
WN8?: number;
|
||||||
@@ -37,7 +37,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
gap: -16,
|
gap: -16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Glow percentile={percentile} side={GlowSide.Top} />
|
<Glow color={PERCENTILE_COLORS[percentile]} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -71,7 +71,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Glow percentile={percentile} side={GlowSide.Bottom} />
|
<Glow color={PERCENTILE_COLORS[percentile]} direction="reverse" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
|
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||||
import { WN8Display } from './components/WN8Display';
|
import { WN8Display } from './components/WN8Display';
|
||||||
import {
|
import {
|
||||||
WN8SurroundingStat,
|
WN8SurroundingStat,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Percentile } from '../../../constants/percentiles';
|
import { Percentile } from '../../../constants/percentiles';
|
||||||
import { TankType } from '../../../core/blitz/tankopedia';
|
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
|
||||||
import { RowStat } from './RowStat';
|
import { RowStat } from './RowStat';
|
||||||
@@ -17,7 +16,7 @@ interface RowProps {
|
|||||||
title: string;
|
title: string;
|
||||||
minimized?: boolean;
|
minimized?: boolean;
|
||||||
treeType?: TreeTypeEnum;
|
treeType?: TreeTypeEnum;
|
||||||
tankType?: TankType;
|
tankType?: string;
|
||||||
|
|
||||||
stats: (RowStatItem | undefined)[];
|
stats: (RowStatItem | undefined)[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function RowStat({
|
|||||||
value,
|
value,
|
||||||
delta,
|
delta,
|
||||||
percentile,
|
percentile,
|
||||||
minimized,
|
minimized = false,
|
||||||
}: RowStatProps) {
|
}: RowStatProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function Card({ items, title }: CardProps) {
|
|||||||
>
|
>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div
|
<div
|
||||||
|
key={item.label}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import getWN8Percentile from '../../core/blitz/getWN8Percentile';
|
import { AllStats, SupplementaryStats } from '../../core/blitz/getAccountInfo';
|
||||||
import isNumber from '../../core/node/isNumber';
|
import isNumber from '../../core/math/isNumber';
|
||||||
import { AllStats, SupplementaryStats } from '../../types/accountInfo';
|
import getWN8Percentile from '../../core/statistics/getWN8Percentile';
|
||||||
import { Card } from './components/Card';
|
import { Card } from './components/Card';
|
||||||
import { Root } from './components/Root';
|
import { Root } from './components/Root';
|
||||||
import { Row } from './components/Row';
|
import { Row } from './components/Row';
|
||||||
|
|
||||||
// TODO: remove the .js endings
|
|
||||||
|
|
||||||
export interface GenericAllStatsProps {
|
export interface GenericAllStatsProps {
|
||||||
stats: AllStats;
|
stats: AllStats;
|
||||||
supplementaryStats?: SupplementaryStats;
|
supplementaryStats?: SupplementaryStats;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import {
|
|||||||
TANK_ICONS,
|
TANK_ICONS,
|
||||||
TANK_ICONS_COLLECTOR,
|
TANK_ICONS_COLLECTOR,
|
||||||
TANK_ICONS_PREMIUM,
|
TANK_ICONS_PREMIUM,
|
||||||
TankType,
|
} from '../../../core/blitzstars/tankopedia';
|
||||||
} from '../../../core/blitz/tankopedia';
|
|
||||||
import { theme } from '../../../stitches.config';
|
import { theme } from '../../../stitches.config';
|
||||||
|
|
||||||
export enum TreeTypeEnum {
|
export enum TreeTypeEnum {
|
||||||
@@ -12,11 +11,17 @@ export enum TreeTypeEnum {
|
|||||||
Collector,
|
Collector,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TreeTypeString = 'techtree' | 'premium' | 'collector';
|
export type TreeTypeString = 'tech-tree' | 'premium' | 'collector';
|
||||||
|
|
||||||
|
export const TREE_TYPE_NAMES: Record<TreeTypeString, string> = {
|
||||||
|
'tech-tree': 'Tech tree',
|
||||||
|
premium: 'Premium',
|
||||||
|
collector: 'Collector',
|
||||||
|
};
|
||||||
|
|
||||||
export interface ItemProps {
|
export interface ItemProps {
|
||||||
image?: string;
|
image?: string;
|
||||||
tankType?: TankType;
|
tankType?: string;
|
||||||
name: string;
|
name: string;
|
||||||
treeType: TreeTypeEnum;
|
treeType: TreeTypeEnum;
|
||||||
}
|
}
|
||||||
@@ -75,7 +80,15 @@ export function Item({ image, tankType, name, treeType }: ItemProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{image && <img src={image} style={{ flex: 1 }} />}
|
{/* {image && (
|
||||||
|
// TODO: remove hardcoded dimensions when satori fixes non-width unloadable images
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
width={106}
|
||||||
|
height={32}
|
||||||
|
style={{ objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TIER_ROMAN_NUMERALS, Tier } from '../core/blitz/tankopedia';
|
import { Tier } from '../core/blitzstars/tankopedia';
|
||||||
import { theme } from '../stitches.config';
|
import { theme } from '../stitches.config';
|
||||||
|
|
||||||
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
export type TierWeightsRecord = Partial<Record<Tier, number>>;
|
||||||
@@ -26,7 +26,7 @@ export default function TierWeights({ weights }: TierWeightsProps) {
|
|||||||
minWidth: 16,
|
minWidth: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{TIER_ROMAN_NUMERALS[parseInt(tier) as Tier]}
|
{tier}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,17 +2,11 @@ import { theme } from '../stitches.config';
|
|||||||
|
|
||||||
export interface TitleBarProps {
|
export interface TitleBarProps {
|
||||||
name: string;
|
name: string;
|
||||||
nameDiscriminator?: string;
|
|
||||||
image?: string;
|
image?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleBar({
|
export default function TitleBar({ name, image, description }: TitleBarProps) {
|
||||||
name,
|
|
||||||
nameDiscriminator,
|
|
||||||
image,
|
|
||||||
description,
|
|
||||||
}: TitleBarProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -33,9 +27,8 @@ export default function TitleBar({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -51,24 +44,11 @@ export default function TitleBar({
|
|||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
maxWidth: nameDiscriminator === undefined ? Infinity : 240,
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
{nameDiscriminator && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 32,
|
|
||||||
color: theme.colors.textLowContrast,
|
|
||||||
fontWeight: 900,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{nameDiscriminator}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import { theme } from '../stitches.config';
|
|||||||
export interface WrapperProps {
|
export interface WrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
naked?: boolean;
|
naked?: boolean;
|
||||||
|
fat?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Wrapper({ children, naked }: WrapperProps) {
|
export default function Wrapper({ children, naked, fat }: WrapperProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: 16,
|
padding: `16px ${fat ? 0 : 16}px`,
|
||||||
gap: 32,
|
gap: 32,
|
||||||
width: 480,
|
width: 480,
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export type BlitzResponse<Data extends object> =
|
||||||
|
| {
|
||||||
|
status: 'error';
|
||||||
|
error: { field: string; message: string; code: 402; value: null };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: 'ok';
|
||||||
|
data: Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function fetchBlitz<Data extends object>(url: string) {
|
||||||
|
const data = (await fetch(url).then((response) =>
|
||||||
|
response.json(),
|
||||||
|
)) as BlitzResponse<Data>;
|
||||||
|
|
||||||
|
if (data.status === 'ok') {
|
||||||
|
return data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Wargaming response error status:"${data.status}"`, {
|
||||||
|
cause: `Message: "${data.error.message}"\nURL: "${url}"`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from './fetchWargaming';
|
||||||
|
import { normalizeIds } from './normalizeIds';
|
||||||
|
|
||||||
|
interface IndividualAccountAchievements {
|
||||||
|
achievements: {
|
||||||
|
[achievement: string]: number;
|
||||||
|
};
|
||||||
|
max_series: {
|
||||||
|
[achievement: string]: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountAchievements {
|
||||||
|
[id: number]: IndividualAccountAchievements;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAccountAchievements<
|
||||||
|
Ids extends number | number[],
|
||||||
|
ReturnType = Ids extends number
|
||||||
|
? IndividualAccountAchievements
|
||||||
|
: IndividualAccountAchievements[],
|
||||||
|
>(region: Region, ids: Ids) {
|
||||||
|
const object = await fetchBlitz<AccountAchievements>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/account/achievements/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
|
||||||
|
ids,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof ids === 'number') {
|
||||||
|
return object[ids as number] as ReturnType;
|
||||||
|
} else {
|
||||||
|
return ids.map((id) => object[id]) as ReturnType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from './fetchWargaming';
|
||||||
|
import { normalizeIds } from './normalizeIds';
|
||||||
|
|
||||||
|
export interface AllStats {
|
||||||
|
spotted: number;
|
||||||
|
hits: number;
|
||||||
|
frags: number;
|
||||||
|
max_xp: number;
|
||||||
|
wins: number;
|
||||||
|
losses: number;
|
||||||
|
capture_points: number;
|
||||||
|
battles: number;
|
||||||
|
damage_dealt: number;
|
||||||
|
damage_received: number;
|
||||||
|
max_frags: number;
|
||||||
|
shots: number;
|
||||||
|
frags8p: number;
|
||||||
|
xp: number;
|
||||||
|
win_and_survived: number;
|
||||||
|
survived_battles: number;
|
||||||
|
dropped_capture_points: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupplementaryStats {
|
||||||
|
// if undefined, WN8 is not available for tanks without averages
|
||||||
|
WN8?: number;
|
||||||
|
tier?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClanStats {
|
||||||
|
spotted: number;
|
||||||
|
max_frags_tank_id: number;
|
||||||
|
hits: number;
|
||||||
|
frags: number;
|
||||||
|
max_xp: number;
|
||||||
|
max_xp_tank_id: number;
|
||||||
|
wins: number;
|
||||||
|
losses: number;
|
||||||
|
capture_points: number;
|
||||||
|
battles: number;
|
||||||
|
damage_dealt: number;
|
||||||
|
damage_received: number;
|
||||||
|
max_frags: number;
|
||||||
|
shots: number;
|
||||||
|
frags8p: number;
|
||||||
|
xp: number;
|
||||||
|
win_and_survived: number;
|
||||||
|
survived_battles: number;
|
||||||
|
dropped_capture_points: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndividualAccountInfo {
|
||||||
|
account_id: number;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
private: null;
|
||||||
|
last_battle_time: number;
|
||||||
|
nickname: string;
|
||||||
|
statistics: {
|
||||||
|
all: AllStats;
|
||||||
|
clan: ClanStats;
|
||||||
|
frags: null | number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountInfo {
|
||||||
|
[accountId: number]: IndividualAccountInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAccountInfo<
|
||||||
|
Ids extends number | number[],
|
||||||
|
ReturnType = Ids extends number
|
||||||
|
? IndividualAccountInfo
|
||||||
|
: IndividualAccountInfo[],
|
||||||
|
>(region: Region, ids: Ids) {
|
||||||
|
const object = await fetchBlitz<AccountInfo>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
|
||||||
|
ids,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof ids === 'number') {
|
||||||
|
return object[ids as number] as ReturnType;
|
||||||
|
} else {
|
||||||
|
return ids.map((id) => object[id]) as ReturnType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from './fetchWargaming';
|
||||||
|
import { normalizeExtra } from './normalizeExtra';
|
||||||
|
import { normalizeIds } from './normalizeIds';
|
||||||
|
|
||||||
|
export type IndividualClanAccountInfo = {
|
||||||
|
account_id: number;
|
||||||
|
joined_at: number;
|
||||||
|
clan_id: number;
|
||||||
|
role: string;
|
||||||
|
account_name: string;
|
||||||
|
|
||||||
|
clan?: {
|
||||||
|
members_count: number;
|
||||||
|
name: string;
|
||||||
|
created_at: number;
|
||||||
|
tag: string;
|
||||||
|
clan_id: number;
|
||||||
|
emblem_set_id: number;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
export interface ClanAccountInfo {
|
||||||
|
[accountId: number]: IndividualClanAccountInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClanAccountInfo<
|
||||||
|
Ids extends number | number[],
|
||||||
|
ReturnType = Ids extends number
|
||||||
|
? IndividualClanAccountInfo
|
||||||
|
: IndividualClanAccountInfo[],
|
||||||
|
>(region: Region, ids: Ids, extra?: string[]) {
|
||||||
|
const object = await fetchBlitz<ClanAccountInfo>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
|
||||||
|
ids,
|
||||||
|
)}${normalizeExtra(extra)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof ids === 'number') {
|
||||||
|
return object[ids as number] as ReturnType;
|
||||||
|
} else {
|
||||||
|
return ids.map((id) => object[id]) as ReturnType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from './fetchWargaming';
|
||||||
|
import { normalizeIds } from './normalizeIds';
|
||||||
|
|
||||||
|
export interface IndividualClanInfo {
|
||||||
|
recruiting_options: {
|
||||||
|
vehicles_level: number;
|
||||||
|
wins_ratio: number;
|
||||||
|
average_battles_per_day: number;
|
||||||
|
battles: number;
|
||||||
|
average_damage: number;
|
||||||
|
};
|
||||||
|
members_count: number;
|
||||||
|
name: string;
|
||||||
|
creator_name: string;
|
||||||
|
clan_id: number;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
leader_name: string;
|
||||||
|
members_ids: number[];
|
||||||
|
recruiting_policy: string;
|
||||||
|
tag: string;
|
||||||
|
is_clan_disbanded: boolean;
|
||||||
|
old_name: string;
|
||||||
|
emblem_set_id: number;
|
||||||
|
creator_id: number;
|
||||||
|
motto: string;
|
||||||
|
renamed_at: number;
|
||||||
|
old_tag: null;
|
||||||
|
leader_id: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClanInfo {
|
||||||
|
[clanId: number]: IndividualClanInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClanInfo<
|
||||||
|
Ids extends number | number[],
|
||||||
|
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
|
||||||
|
>(region: Region, ids: Ids) {
|
||||||
|
const object = await fetchBlitz<ClanInfo>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds(
|
||||||
|
ids,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof ids === 'number') {
|
||||||
|
return object[ids as number] as ReturnType;
|
||||||
|
} else {
|
||||||
|
return ids.map((id) => object[id]) as ReturnType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { REGION_NAMES, Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
import { TanksStats } from '../../types/tanksStats';
|
import { TanksStats } from '../../types/tanksStats';
|
||||||
import throwError from '../node/throwError';
|
import { UserError } from '../blitzkrieg/userError';
|
||||||
import getWargamingResponse from './getWargamingResponse';
|
import fetchBlitz from './fetchWargaming';
|
||||||
|
|
||||||
export default async function getTankStats(region: Region, id: number) {
|
export default async function getTankStats(region: Region, id: number) {
|
||||||
const tankStats = await getWargamingResponse<TanksStats>(
|
const tankStats = await fetchBlitz<TanksStats>(
|
||||||
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
`https://api.wotblitz.${region}/wotb/tanks/stats/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tankStats[id] === null) {
|
if (tankStats[id] === null) {
|
||||||
throw throwError(
|
throw new UserError('No tank stats available', {
|
||||||
'No tank stats available',
|
cause:
|
||||||
`Wargaming says there is no tank stats for this account. This account may not have any battles/tanks or exist in the ${REGION_NAMES[region]} server.`,
|
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankStats[id];
|
return tankStats[id];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TreeTypeEnum } from '../../components/Tanks';
|
import { TreeTypeEnum } from '../../components/Tanks';
|
||||||
import { tankopedia } from './tankopedia';
|
import { tankopedia } from '../blitzstars/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];
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import throwError from '../node/throwError';
|
|
||||||
|
|
||||||
export type WargamingResponse<Data extends object> =
|
|
||||||
| {
|
|
||||||
status: 'error';
|
|
||||||
error: { field: string; message: string; code: 402; value: null };
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
status: 'ok';
|
|
||||||
data: Data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function getWargamingResponse<Data extends object>(
|
|
||||||
url: string,
|
|
||||||
) {
|
|
||||||
const data = (await fetch(url).then((response) =>
|
|
||||||
response.json(),
|
|
||||||
)) as WargamingResponse<Data>;
|
|
||||||
|
|
||||||
if (data.status === 'ok') {
|
|
||||||
return data.data;
|
|
||||||
} else {
|
|
||||||
throw throwError(
|
|
||||||
`Wargaming response error status:"${data.status}"`,
|
|
||||||
`Message: "${data.error.message}"\nURL: "${url}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function normalizeExtra(extra?: string[]) {
|
||||||
|
return extra ? `&extra=${extra.join(',')}` : '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function normalizeIds(ids: number | number[]): string {
|
||||||
|
return typeof ids === 'number' ? ids.toString() : ids.join(',');
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import throwError from '../node/throwError';
|
import { UserError } from '../blitzkrieg/userError';
|
||||||
import { TANKS, tankopedia } from './tankopedia';
|
import { TANKS, tankopedia } from '../blitzstars/tankopedia';
|
||||||
|
|
||||||
export default async function resolveTankId(tank: string) {
|
export default async function resolveTankId(tank: string | number) {
|
||||||
const number = parseInt(tank);
|
const number = typeof tank === 'string' ? parseInt(tank) : tank;
|
||||||
|
|
||||||
if (Number.isNaN(number)) {
|
if (Number.isNaN(number)) {
|
||||||
const searchResult = go(tank, await TANKS, { keys: ['name'], limit: 1 });
|
const searchResult = go(`${tank}`, await TANKS, {
|
||||||
|
keys: ['name'],
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
if (searchResult.length === 0) {
|
if (searchResult.length === 0) {
|
||||||
throw throwError(
|
throw new UserError('Tank not found', {
|
||||||
'Tank not found',
|
cause: `Could not find tank by the name "${tank}".`,
|
||||||
`Could not find tank by the name "${tank}".`,
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return searchResult[0].obj.tank_id;
|
return searchResult[0].obj.tank_id;
|
||||||
}
|
}
|
||||||
@@ -20,10 +22,9 @@ export default async function resolveTankId(tank: string) {
|
|||||||
if ((await tankopedia)[number]) {
|
if ((await tankopedia)[number]) {
|
||||||
return number;
|
return number;
|
||||||
} else {
|
} else {
|
||||||
throw throwError(
|
throw new UserError('Tank not found', {
|
||||||
'Tank not found',
|
cause: `Could not find tank by the ID "${number}".`,
|
||||||
`Could not find tank by the ID "${number}".`,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { tankopedia } from './tankopedia';
|
import { tankopedia } from '../blitzstars/tankopedia';
|
||||||
|
|
||||||
export default async function resolveTankName(tankId: number) {
|
export default async function resolveTankName(tankId: number) {
|
||||||
const tankopediaName = (await tankopedia)[tankId]?.name;
|
const tankopediaName = (await tankopedia)[tankId]?.name;
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
import { Clan, ClanList } from '../../types/clanList';
|
import { Clan, ClanList } from '../../types/clanList';
|
||||||
import getWargamingResponse from './getWargamingResponse';
|
import fetchBlitz from './fetchWargaming';
|
||||||
|
|
||||||
export type ClanListWithRegion = (Clan & {
|
export type ClanListWithRegion = (Clan & {
|
||||||
region: 'com' | 'eu' | 'asia';
|
region: 'com' | 'eu' | 'asia';
|
||||||
})[];
|
})[];
|
||||||
|
|
||||||
export default async function listClans(search: string, limit = 9) {
|
export default async function searchClansAcrossRegions(
|
||||||
|
search: string,
|
||||||
|
limit = 9,
|
||||||
|
) {
|
||||||
const trimmed = search.trim();
|
const trimmed = search.trim();
|
||||||
if (trimmed.length < 3 && trimmed.length > 100) return [];
|
if (trimmed.length < 3 && trimmed.length > 100) return [];
|
||||||
const normalizedLimit = Math.round(limit / 3);
|
const normalizedLimit = Math.round(limit / 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getWargamingResponse<ClanList>(
|
fetchBlitz<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=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||||
).then(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
@@ -24,7 +27,7 @@ export default async function listClans(search: string, limit = 9) {
|
|||||||
region: 'com' as Region,
|
region: 'com' as Region,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<ClanList>(
|
fetchBlitz<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=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||||
).then(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
@@ -34,7 +37,7 @@ export default async function listClans(search: string, limit = 9) {
|
|||||||
region: 'eu' as Region,
|
region: 'eu' as Region,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<ClanList>(
|
fetchBlitz<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=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`,
|
||||||
).then(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
import getWargamingResponse from './getWargamingResponse';
|
import fetchBlitz from './fetchWargaming';
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
nickname: string;
|
nickname: string;
|
||||||
@@ -14,7 +14,10 @@ export type AccountListWithServer = (Account & {
|
|||||||
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
|
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
|
||||||
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
|
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
|
||||||
|
|
||||||
export default async function listPlayers(search: string, limit = 9) {
|
export default async function searchPlayersAcrossRegions(
|
||||||
|
search: string,
|
||||||
|
limit = 9,
|
||||||
|
) {
|
||||||
const trimmedSearch = search.trim();
|
const trimmedSearch = search.trim();
|
||||||
const normalizedLimit = Math.round(limit / 3);
|
const normalizedLimit = Math.round(limit / 3);
|
||||||
const encodedSearch = encodeURIComponent(trimmedSearch);
|
const encodedSearch = encodeURIComponent(trimmedSearch);
|
||||||
@@ -22,7 +25,7 @@ export default async function listPlayers(search: string, limit = 9) {
|
|||||||
if (usernamePattern.test(trimmedSearch)) {
|
if (usernamePattern.test(trimmedSearch)) {
|
||||||
return (
|
return (
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getWargamingResponse<AccountList>(
|
fetchBlitz<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=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||||
).then(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
@@ -32,7 +35,7 @@ export default async function listPlayers(search: string, limit = 9) {
|
|||||||
region: 'com' as Region,
|
region: 'com' as Region,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<AccountList>(
|
fetchBlitz<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=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||||
).then(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
@@ -42,7 +45,7 @@ export default async function listPlayers(search: string, limit = 9) {
|
|||||||
region: 'eu' as Region,
|
region: 'eu' as Region,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<AccountList>(
|
fetchBlitz<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=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||||
).then(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
|
||||||
|
export const discordBlitzDB = new PrismaClient();
|
||||||
|
|
||||||
|
export function linkBlitzAndDiscord(
|
||||||
|
discord: number,
|
||||||
|
region: Region,
|
||||||
|
blitz: number,
|
||||||
|
) {
|
||||||
|
return discordBlitzDB.discord_blitz.upsert({
|
||||||
|
where: { discord },
|
||||||
|
update: { region, blitz },
|
||||||
|
create: { discord, region, blitz },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlitzFromDiscord(discord: number) {
|
||||||
|
return discordBlitzDB.discord_blitz.findUnique({
|
||||||
|
where: { discord },
|
||||||
|
select: { region: true, blitz: true },
|
||||||
|
}) as Promise<null | { region: Region; blitz: number }>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function emblemIdToURL(emblemId: number) {
|
||||||
|
return `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${emblemId}.png`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { TREE_TYPE_NAMES } from '../../components/Tanks';
|
||||||
|
import resolveTankName from '../blitz/resolveTankName';
|
||||||
|
import {
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
Tier,
|
||||||
|
tankopediaInfo,
|
||||||
|
} from '../blitzstars/tankopedia';
|
||||||
|
import { StatFilters } from '../statistics/filterStats';
|
||||||
|
|
||||||
|
export async function filtersToDescription({
|
||||||
|
nation,
|
||||||
|
tier,
|
||||||
|
tankType,
|
||||||
|
treeType,
|
||||||
|
tank,
|
||||||
|
}: StatFilters) {
|
||||||
|
const awaitedTankopediaInfo = await tankopediaInfo;
|
||||||
|
const info: string[] = [];
|
||||||
|
|
||||||
|
if (tank) return await resolveTankName(tank);
|
||||||
|
if (nation) info.push(awaitedTankopediaInfo.vehicle_nations[nation]);
|
||||||
|
if (tier) info.push(`Tier ${TIER_ROMAN_NUMERALS[tier as Tier]}`);
|
||||||
|
if (tankType) info.push(awaitedTankopediaInfo.vehicle_types[tankType]);
|
||||||
|
if (treeType) info.push(TREE_TYPE_NAMES[treeType]);
|
||||||
|
|
||||||
|
return info.length === 0 ? 'No filters' : info.join(', ');
|
||||||
|
}
|
||||||
+23
-4
@@ -1,25 +1,44 @@
|
|||||||
import { RatingsInfo } from '../../commands/ratings';
|
import { RatingsInfo } from '../../commands/ratings';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { DATABASE_REPO } from '../database/getMidnightLeaderboard';
|
import { DATABASE_REPO } from './getMidnightLeaderboard';
|
||||||
import { octokit } from '../github/octokit';
|
import { octokit } from './octokit';
|
||||||
import throwError from '../node/throwError';
|
|
||||||
|
const ARCHIVED_RATINGS_CACHE: Record<
|
||||||
|
Region,
|
||||||
|
Record<
|
||||||
|
number,
|
||||||
|
RatingsInfo & {
|
||||||
|
detail: undefined;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
> = {
|
||||||
|
com: {},
|
||||||
|
eu: {},
|
||||||
|
asia: {},
|
||||||
|
};
|
||||||
|
|
||||||
export default async function getArchivedRatingsInfo(
|
export default async function getArchivedRatingsInfo(
|
||||||
season: number,
|
season: number,
|
||||||
region: Region,
|
region: Region,
|
||||||
) {
|
) {
|
||||||
|
if (ARCHIVED_RATINGS_CACHE[region][season]) {
|
||||||
|
return ARCHIVED_RATINGS_CACHE[region][season];
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await octokit.repos.getContent({
|
const { data } = await octokit.repos.getContent({
|
||||||
...DATABASE_REPO,
|
...DATABASE_REPO,
|
||||||
path: `${region}/ratings/${season}/info.json`,
|
path: `${region}/ratings/${season}/info.json`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Array.isArray(data) || data.type !== 'file')
|
if (Array.isArray(data) || data.type !== 'file')
|
||||||
throw throwError('Archived ratings info is malformed');
|
throw new Error('Archived ratings info is malformed');
|
||||||
|
|
||||||
const content = Buffer.from(data.content, 'base64').toString();
|
const content = Buffer.from(data.content, 'base64').toString();
|
||||||
const jsonContent = JSON.parse(content) as RatingsInfo & {
|
const jsonContent = JSON.parse(content) as RatingsInfo & {
|
||||||
detail: undefined;
|
detail: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ARCHIVED_RATINGS_CACHE[region][season] = jsonContent;
|
||||||
|
|
||||||
return jsonContent;
|
return jsonContent;
|
||||||
}
|
}
|
||||||
File renamed without changes.
+18
-2
@@ -1,13 +1,26 @@
|
|||||||
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { octokit } from '../github/octokit';
|
import { octokit } from './octokit';
|
||||||
|
|
||||||
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
|
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
|
||||||
|
|
||||||
|
const MIDNIGHT_LEADERBOARD_CACHE: Record<
|
||||||
|
Region,
|
||||||
|
Record<number, BlitzkriegRatingsLeaderboard>
|
||||||
|
> = {
|
||||||
|
com: {},
|
||||||
|
eu: {},
|
||||||
|
asia: {},
|
||||||
|
};
|
||||||
|
|
||||||
export default async function getMidnightLeaderboard(
|
export default async function getMidnightLeaderboard(
|
||||||
region: Region,
|
region: Region,
|
||||||
season: number,
|
season: number,
|
||||||
) {
|
) {
|
||||||
|
if (MIDNIGHT_LEADERBOARD_CACHE[region][season]) {
|
||||||
|
return MIDNIGHT_LEADERBOARD_CACHE[region][season];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await octokit.repos.getContent({
|
const { data } = await octokit.repos.getContent({
|
||||||
...DATABASE_REPO,
|
...DATABASE_REPO,
|
||||||
@@ -16,8 +29,11 @@ export default async function getMidnightLeaderboard(
|
|||||||
|
|
||||||
if (!Array.isArray(data) && data.type === 'file') {
|
if (!Array.isArray(data) && data.type === 'file') {
|
||||||
const response = await fetch(data.download_url!);
|
const response = await fetch(data.download_url!);
|
||||||
|
const json = (await response.json()) as BlitzkriegRatingsLeaderboard;
|
||||||
|
|
||||||
return (await response.json()) as BlitzkriegRatingsLeaderboard;
|
MIDNIGHT_LEADERBOARD_CACHE[region][season] = json;
|
||||||
|
|
||||||
|
return json;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
File renamed without changes.
@@ -3,5 +3,7 @@ import { PeriodSize } from '../discord/addPeriodSubCommands';
|
|||||||
import getTimeDaysAgo from './getTimeDaysAgo';
|
import getTimeDaysAgo from './getTimeDaysAgo';
|
||||||
|
|
||||||
export default function getPeriodStart(region: Region, period: PeriodSize) {
|
export default function getPeriodStart(region: Region, period: PeriodSize) {
|
||||||
return period === 'career' ? 0 : getTimeDaysAgo(region, parseInt(period));
|
if (period === 'career') return 0;
|
||||||
|
if (period === 'today') return getTimeDaysAgo(region, 1);
|
||||||
|
return getTimeDaysAgo(region, parseInt(period));
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
getHistoriesDefaultOptions,
|
getHistoriesDefaultOptions,
|
||||||
} from '../../types/histories';
|
} from '../../types/histories';
|
||||||
import getTankStats from '../blitz/getTankStats';
|
import getTankStats from '../blitz/getTankStats';
|
||||||
import { emptyAllStats } from './getDiffedTankStats';
|
import { emptyAllStats } from '../blitzstars/getStatsInPeriod';
|
||||||
|
|
||||||
export interface TankHistory extends History {
|
export interface TankHistory extends History {
|
||||||
tank_id: number;
|
tank_id: number;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,9 @@
|
|||||||
|
import jsxToSvg from './jsxToSvg';
|
||||||
|
import { svgToPngThreaded } from './svgToPngThreaded';
|
||||||
|
|
||||||
|
export default async function jsxToPngThreaded(jsx: JSX.Element) {
|
||||||
|
const svg = await jsxToSvg(jsx);
|
||||||
|
const png = await svgToPngThreaded(svg);
|
||||||
|
|
||||||
|
return png;
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ let roboto: Buffer;
|
|||||||
let robotoBold: Buffer;
|
let robotoBold: Buffer;
|
||||||
let robotoBlack: Buffer;
|
let robotoBlack: Buffer;
|
||||||
|
|
||||||
console.log('Importing fonts...');
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
FONT_FILES.map(
|
FONT_FILES.map(
|
||||||
(file) =>
|
(file) =>
|
||||||
@@ -27,7 +26,6 @@ Promise.all(
|
|||||||
),
|
),
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
[roboto, robotoBold, robotoBlack] = data;
|
[roboto, robotoBold, robotoBlack] = data;
|
||||||
console.log('Fonts imported');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default async function jsxToSvg(jsx: JSX.Element) {
|
export default async function jsxToSvg(jsx: JSX.Element) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
import { secrets } from '../node/secrets';
|
import { secrets } from './secrets';
|
||||||
|
|
||||||
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { client } from '../discord/client';
|
||||||
|
import { discordBlitzDB } from './discordBlitz';
|
||||||
|
|
||||||
|
const EXIT_EVENTS = [
|
||||||
|
'beforeExit',
|
||||||
|
'exit',
|
||||||
|
'SIGINT',
|
||||||
|
'SIGTERM',
|
||||||
|
'SIGUSR1',
|
||||||
|
'SIGUSR2',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
console.log('Cleaning up...');
|
||||||
|
|
||||||
|
await Promise.all([discordBlitzDB.$disconnect(), client.destroy()]);
|
||||||
|
|
||||||
|
console.log('Gracefully exiting...');
|
||||||
|
|
||||||
|
EXIT_EVENTS.forEach((event) => process.off(event, cleanup));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerProcesses() {
|
||||||
|
process.on('uncaughtException', console.error);
|
||||||
|
client.on('error', console.error);
|
||||||
|
|
||||||
|
EXIT_EVENTS.forEach((event) => process.on(event, cleanup));
|
||||||
|
}
|
||||||
@@ -2,7 +2,11 @@ import { config } from 'dotenv';
|
|||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
export const SECRET_KEYS = ['DISCORD_TOKEN', 'GH_TOKEN'] as const;
|
export const SECRET_KEYS = [
|
||||||
|
'DISCORD_TOKEN',
|
||||||
|
'GH_TOKEN',
|
||||||
|
'DISCORD_BLITZ_DB',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[]
|
export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[]
|
||||||
? T
|
? T
|
||||||
File renamed without changes.
@@ -0,0 +1,46 @@
|
|||||||
|
import { Worker } from 'worker_threads';
|
||||||
|
|
||||||
|
const MAX_WORKERS = 2;
|
||||||
|
|
||||||
|
const workers = [...Array(MAX_WORKERS)].map(() => {
|
||||||
|
const worker = new Worker(`${__dirname}/workers/render.cjs`);
|
||||||
|
console.log(`🟡 Launching render worker ${worker.threadId}`);
|
||||||
|
return worker;
|
||||||
|
});
|
||||||
|
const queue: {
|
||||||
|
svg: string;
|
||||||
|
resolve: (png: Buffer) => void;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
function manageQueue() {
|
||||||
|
if (queue.length > 0 && workers.length > 0) {
|
||||||
|
const { svg, resolve } = queue.shift()!;
|
||||||
|
const worker = workers.shift()!;
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
workers.push(worker);
|
||||||
|
worker.off('message', handleMessage);
|
||||||
|
worker.off('error', handleError);
|
||||||
|
manageQueue();
|
||||||
|
}
|
||||||
|
function handleMessage(png: Uint8Array) {
|
||||||
|
cleanup();
|
||||||
|
resolve(Buffer.from(png));
|
||||||
|
}
|
||||||
|
function handleError(error: Error) {
|
||||||
|
cleanup();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage(svg);
|
||||||
|
worker.on('message', handleMessage);
|
||||||
|
worker.on('error', handleError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function svgToPngThreaded(svg: string) {
|
||||||
|
return new Promise<Buffer>((resolve) => {
|
||||||
|
queue.push({ svg, resolve });
|
||||||
|
manageQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Discriminator for user caused errors to not report in logs
|
||||||
|
*/
|
||||||
|
export class UserError extends Error {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { getAccountInfo } from '../blitz/getAccountInfo';
|
||||||
|
import buttonLink from '../discord/buttonLink';
|
||||||
|
|
||||||
|
export async function getBlitzStarsLinkButton(
|
||||||
|
region: Region,
|
||||||
|
id: number,
|
||||||
|
tankId?: number,
|
||||||
|
) {
|
||||||
|
const { nickname } = await getAccountInfo(region, id);
|
||||||
|
|
||||||
|
return buttonLink(
|
||||||
|
`https://www.blitzstars.com/player/${region}/${nickname}${
|
||||||
|
tankId ? `/tank/${tankId!}` : ''
|
||||||
|
}`,
|
||||||
|
'BlitzStars',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,34 +1,13 @@
|
|||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
|
||||||
import { AccountInfo, AllStats } from '../../types/accountInfo';
|
|
||||||
import {
|
import {
|
||||||
GetHistoriesOptions,
|
GetHistoriesOptions,
|
||||||
Histories,
|
Histories,
|
||||||
History,
|
History,
|
||||||
getHistoriesDefaultOptions,
|
getHistoriesDefaultOptions,
|
||||||
} from '../../types/histories';
|
} from '../../types/histories';
|
||||||
import getWargamingResponse from '../blitz/getWargamingResponse';
|
import { getAccountInfo } from '../blitz/getAccountInfo';
|
||||||
import { emptyAllStats } from './getDiffedTankStats';
|
import { PlayerStats } from './getPlayerStats';
|
||||||
|
import { emptyAllStats } from './getStatsInPeriod';
|
||||||
export interface PlayerHistoryRaw {
|
|
||||||
clan: { clan_id: number; name: string; tag: string };
|
|
||||||
statistics: {
|
|
||||||
all: AllStats;
|
|
||||||
wn8: number;
|
|
||||||
wn7: number;
|
|
||||||
average_tier: number;
|
|
||||||
};
|
|
||||||
last_battle_time: number;
|
|
||||||
nickname: string;
|
|
||||||
account_id: number;
|
|
||||||
achievements: {
|
|
||||||
account_id: number;
|
|
||||||
[key: string]: number;
|
|
||||||
};
|
|
||||||
region: Region;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PlayerHistoriesRaw = PlayerHistoryRaw[];
|
|
||||||
|
|
||||||
export default async function getPlayerHistories(
|
export default async function getPlayerHistories(
|
||||||
server: Region,
|
server: Region,
|
||||||
@@ -39,7 +18,7 @@ export default async function getPlayerHistories(
|
|||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://www.blitzstars.com/api/playerstats/${id}`,
|
`https://www.blitzstars.com/api/playerstats/${id}`,
|
||||||
);
|
);
|
||||||
const playerHistories = ((await response.json()) as PlayerHistoriesRaw).map(
|
const playerHistories = ((await response.json()) as PlayerStats).map(
|
||||||
(history) =>
|
(history) =>
|
||||||
({
|
({
|
||||||
all: history.statistics.all,
|
all: history.statistics.all,
|
||||||
@@ -62,16 +41,9 @@ export default async function getPlayerHistories(
|
|||||||
const latestNodes: Histories = [];
|
const latestNodes: Histories = [];
|
||||||
|
|
||||||
if (mergedOptions.includeLatestHistories) {
|
if (mergedOptions.includeLatestHistories) {
|
||||||
const {
|
const { last_battle_time, statistics } = await getAccountInfo(server, id);
|
||||||
last_battle_time,
|
|
||||||
statistics: { all },
|
|
||||||
} = (
|
|
||||||
await getWargamingResponse<AccountInfo>(
|
|
||||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
|
||||||
)
|
|
||||||
)[id];
|
|
||||||
|
|
||||||
latestNodes.push({ all, last_battle_time });
|
latestNodes.push({ all: statistics.all, last_battle_time });
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousNodes: Histories = [];
|
const previousNodes: Histories = [];
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
import { AllStats } from '../blitz/getAccountInfo';
|
||||||
|
|
||||||
|
export interface IndividualPlayerStats {
|
||||||
|
clan: { clan_id: number; name: string; tag: string };
|
||||||
|
statistics: {
|
||||||
|
all: AllStats;
|
||||||
|
wn8: number;
|
||||||
|
wn7: number;
|
||||||
|
average_tier: number;
|
||||||
|
};
|
||||||
|
last_battle_time: number;
|
||||||
|
nickname: string;
|
||||||
|
account_id: number;
|
||||||
|
achievements: {
|
||||||
|
account_id: number;
|
||||||
|
[key: string]: number;
|
||||||
|
};
|
||||||
|
region: Region;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlayerStats = IndividualPlayerStats[];
|
||||||
|
|
||||||
|
export async function getPlayerStats<
|
||||||
|
Ids extends number | number[],
|
||||||
|
ReturnType = Ids extends number ? PlayerStats : PlayerStats[],
|
||||||
|
>(region: Region, ids: Ids) {
|
||||||
|
const stats = await Promise.all(
|
||||||
|
((typeof ids === 'number' ? [ids] : ids) as number[]).map((id) =>
|
||||||
|
fetch(`https://www.blitzstars.com/api/playerstats/${id}`).then(
|
||||||
|
(response) => response.json() as Promise<PlayerStats>,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof ids === 'number') return stats[0] as ReturnType;
|
||||||
|
return stats as ReturnType;
|
||||||
|
}
|
||||||
+28
-23
@@ -1,9 +1,9 @@
|
|||||||
import { Region } from '../../constants/regions';
|
import { Region } from '../../constants/regions';
|
||||||
import { AllStats } from '../../types/accountInfo';
|
import { AllStats } from '../blitz/getAccountInfo';
|
||||||
import getTankHistories, {
|
import getTankHistories, {
|
||||||
TankHistories,
|
TankHistories,
|
||||||
TankHistoryRaw,
|
TankHistoryRaw,
|
||||||
} from './getTankHistories';
|
} from '../blitzkrieg/getTankHistories';
|
||||||
|
|
||||||
export const emptyAllStats: AllStats = {
|
export const emptyAllStats: AllStats = {
|
||||||
battles: 0,
|
battles: 0,
|
||||||
@@ -34,8 +34,13 @@ export const emptyTankHistoryNode: TankHistoryRaw = {
|
|||||||
all: emptyAllStats,
|
all: emptyAllStats,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DiffedTankStats {
|
||||||
|
diff: Record<number, AllStats>;
|
||||||
|
order: number[];
|
||||||
|
}
|
||||||
|
|
||||||
// BIG TODO: merge this neatly with diffNormalizedTankStats
|
// BIG TODO: merge this neatly with diffNormalizedTankStats
|
||||||
export default async function getDiffedTankStats(
|
export default async function getStatsInPeriod(
|
||||||
server: Region,
|
server: Region,
|
||||||
id: number,
|
id: number,
|
||||||
start: number,
|
start: number,
|
||||||
@@ -66,7 +71,7 @@ export default async function getDiffedTankStats(
|
|||||||
const playedTanks: number[] = [];
|
const playedTanks: number[] = [];
|
||||||
|
|
||||||
// fetch history node right before "start" and right before "end"
|
// fetch history node right before "start" and right before "end"
|
||||||
const diffed: Record<number, AllStats> = {};
|
const diff: Record<number, AllStats> = {};
|
||||||
inRangeTanks.forEach((tankId) => {
|
inRangeTanks.forEach((tankId) => {
|
||||||
const sortedHistory = tankSortedHistory[tankId];
|
const sortedHistory = tankSortedHistory[tankId];
|
||||||
const previousIndex =
|
const previousIndex =
|
||||||
@@ -77,30 +82,30 @@ export default async function getDiffedTankStats(
|
|||||||
const latest =
|
const latest =
|
||||||
sortedHistory[latestIndex] ?? sortedHistory[sortedHistory.length - 1];
|
sortedHistory[latestIndex] ?? sortedHistory[sortedHistory.length - 1];
|
||||||
|
|
||||||
function diff(get: (allStats: AllStats) => number) {
|
function d(get: (allStats: AllStats) => number) {
|
||||||
return get(latest.all) - get(previous.all);
|
return get(latest.all) - get(previous.all);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if there was a change in battles as games in ratings do update last_battle_time
|
// check if there was a change in battles as games in ratings do update last_battle_time
|
||||||
if (diff((a) => a.battles) > 0) {
|
if (d((a) => a.battles) > 0) {
|
||||||
diffed[tankId] = {
|
diff[tankId] = {
|
||||||
battles: diff((a) => a.battles),
|
battles: d((a) => a.battles),
|
||||||
capture_points: diff((a) => a.capture_points),
|
capture_points: d((a) => a.capture_points),
|
||||||
damage_dealt: diff((a) => a.damage_dealt),
|
damage_dealt: d((a) => a.damage_dealt),
|
||||||
damage_received: diff((a) => a.damage_received),
|
damage_received: d((a) => a.damage_received),
|
||||||
dropped_capture_points: diff((a) => a.dropped_capture_points),
|
dropped_capture_points: d((a) => a.dropped_capture_points),
|
||||||
frags: diff((a) => a.frags),
|
frags: d((a) => a.frags),
|
||||||
frags8p: diff((a) => a.frags8p),
|
frags8p: d((a) => a.frags8p),
|
||||||
hits: diff((a) => a.hits),
|
hits: d((a) => a.hits),
|
||||||
losses: diff((a) => a.losses),
|
losses: d((a) => a.losses),
|
||||||
max_frags: latest.all.max_frags,
|
max_frags: latest.all.max_frags,
|
||||||
max_xp: latest.all.max_xp,
|
max_xp: latest.all.max_xp,
|
||||||
shots: diff((a) => a.shots),
|
shots: d((a) => a.shots),
|
||||||
spotted: diff((a) => a.spotted),
|
spotted: d((a) => a.spotted),
|
||||||
survived_battles: diff((a) => a.survived_battles),
|
survived_battles: d((a) => a.survived_battles),
|
||||||
win_and_survived: diff((a) => a.win_and_survived),
|
win_and_survived: d((a) => a.win_and_survived),
|
||||||
wins: diff((a) => a.wins),
|
wins: d((a) => a.wins),
|
||||||
xp: diff((a) => a.xp),
|
xp: d((a) => a.xp),
|
||||||
};
|
};
|
||||||
|
|
||||||
playedTanks.push(tankId);
|
playedTanks.push(tankId);
|
||||||
@@ -116,5 +121,5 @@ export default async function getDiffedTankStats(
|
|||||||
return bLatest.last_battle_time - aLatest.last_battle_time;
|
return bLatest.last_battle_time - aLatest.last_battle_time;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { diffed, order };
|
return { diff, order } satisfies DiffedTankStats;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AllStats } from '../../types/accountInfo';
|
import { AllStats } from '../blitz/getAccountInfo';
|
||||||
import { context } from '../blitzkrieg/context';
|
import { context } from '../blitzkrieg/context';
|
||||||
|
import { tankopedia } from './tankopedia';
|
||||||
|
|
||||||
export interface SpecialStats {
|
export interface SpecialStats {
|
||||||
winrate: number;
|
winrate: number;
|
||||||
@@ -79,9 +80,10 @@ export interface IndividualTankAverage {
|
|||||||
last_update: number;
|
last_update: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tankopedia;
|
||||||
|
|
||||||
export type TankAverages = Record<number, IndividualTankAverage>;
|
export type TankAverages = Record<number, IndividualTankAverage>;
|
||||||
|
|
||||||
console.log('Caching tank averages...');
|
|
||||||
export const tankAverages = fetch(
|
export const tankAverages = fetch(
|
||||||
context === 'bot'
|
context === 'bot'
|
||||||
? 'https://www.blitzstars.com/api/tankaverages.json'
|
? 'https://www.blitzstars.com/api/tankaverages.json'
|
||||||
@@ -97,8 +99,6 @@ export const tankAverages = fetch(
|
|||||||
individualTankAverage;
|
individualTankAverage;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Tank averages cached');
|
|
||||||
|
|
||||||
return partialTankAverages;
|
return partialTankAverages;
|
||||||
} else {
|
} else {
|
||||||
return json as TankAverages;
|
return json as TankAverages;
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
import { deburr } from 'lodash';
|
import { deburr } from 'lodash';
|
||||||
|
import { TreeTypeString } from '../../components/Tanks';
|
||||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||||
|
import fetchBlitz from '../blitz/fetchWargaming';
|
||||||
import { context } from '../blitzkrieg/context';
|
import { context } from '../blitzkrieg/context';
|
||||||
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;
|
||||||
is_premium: boolean;
|
is_premium: boolean;
|
||||||
is_collectible: boolean;
|
is_collectible: boolean;
|
||||||
tier: number;
|
tier: Tier;
|
||||||
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: TankType;
|
type: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
type asd = TreeTypeString;
|
||||||
export interface Tankopedia {
|
export interface Tankopedia {
|
||||||
[id: number]: TankopediaEntry | undefined;
|
[id: number]: TankopediaEntry | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Caching tankopedia...');
|
|
||||||
export const tankopedia = fetch(
|
export const tankopedia = fetch(
|
||||||
context === 'bot'
|
context === 'bot'
|
||||||
? 'https://www.blitzstars.com/bs-tankopedia.json'
|
? 'https://www.blitzstars.com/bs-tankopedia.json'
|
||||||
: '/api/tankopedia',
|
: '/api/tankopedia',
|
||||||
)
|
)
|
||||||
.then(async (response) => response.json())
|
.then(async (response) => response.json())
|
||||||
.then((wrapperTankopedia) => {
|
.then((wrapperTankopedia) =>
|
||||||
console.log('Tankopedia cached');
|
context === 'bot'
|
||||||
return context === 'bot'
|
|
||||||
? (wrapperTankopedia as { data: Tankopedia }).data
|
? (wrapperTankopedia as { data: Tankopedia }).data
|
||||||
: (wrapperTankopedia as Tankopedia);
|
: (wrapperTankopedia as Tankopedia),
|
||||||
});
|
);
|
||||||
const entries = new Promise<TankopediaEntry[]>(async (resolve) => {
|
const entries = new Promise<TankopediaEntry[]>(async (resolve) => {
|
||||||
resolve(Object.entries(await tankopedia).map(([, entry]) => entry));
|
resolve(Object.entries(await tankopedia).map(([, entry]) => entry));
|
||||||
});
|
});
|
||||||
@@ -71,27 +68,25 @@ export interface TankopediaInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this is blocking because info is needed for command creation
|
// this is blocking because info is needed for command creation
|
||||||
console.log('Caching tankopedia info...');
|
export const tankopediaInfo = fetchBlitz<TankopediaInfo>(
|
||||||
export const tankopediaInfo = getWargamingResponse<TankopediaInfo>(
|
|
||||||
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
|
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`,
|
||||||
);
|
);
|
||||||
console.log('Cached tankopedia info');
|
|
||||||
|
|
||||||
export const TANK_ICONS: Record<TankType, string> = {
|
export const TANK_ICONS: Record<string, 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<TankType, string> = {
|
export const TANK_ICONS_PREMIUM: Record<string, 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<TankType, string> = {
|
export const TANK_ICONS_COLLECTOR: Record<string, 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',
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
SlashCommandSubcommandGroupBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
|
||||||
|
import { tankopediaInfo } from '../blitzstars/tankopedia';
|
||||||
|
import addPeriodSubCommands from './addPeriodSubCommands';
|
||||||
|
import addTankChoices from './addTankChoices';
|
||||||
|
import addTierChoices from './addTierChoices';
|
||||||
|
|
||||||
|
export default async function addFilterOptions<
|
||||||
|
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
|
||||||
|
>(
|
||||||
|
option: OptionType,
|
||||||
|
extra: (
|
||||||
|
option: SlashCommandSubcommandBuilder,
|
||||||
|
) => SlashCommandSubcommandBuilder = (option) => option,
|
||||||
|
): Promise<OptionType> {
|
||||||
|
const awaitedTankopediaInfo = await tankopediaInfo;
|
||||||
|
|
||||||
|
addPeriodSubCommands(option, (option) => {
|
||||||
|
extra(option);
|
||||||
|
|
||||||
|
return option
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('nation')
|
||||||
|
.setDescription('Nation')
|
||||||
|
.addChoices(
|
||||||
|
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
|
||||||
|
([type, name]) => ({ name, value: type }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(false),
|
||||||
|
)
|
||||||
|
.addStringOption((option) => addTierChoices(option).setRequired(false))
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('tank-type')
|
||||||
|
.setDescription('Tank type')
|
||||||
|
.addChoices(
|
||||||
|
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
|
||||||
|
([type, name]) => ({
|
||||||
|
name: name.replace(' Tank', ''),
|
||||||
|
value: type,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(false),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('tree-type')
|
||||||
|
.setDescription('Tech tree type')
|
||||||
|
.addChoices(
|
||||||
|
{
|
||||||
|
name: TREE_TYPE_NAMES['tech-tree'],
|
||||||
|
value: 'tech-tree' satisfies TreeTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: TREE_TYPE_NAMES.premium,
|
||||||
|
value: 'premium' satisfies TreeTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: TREE_TYPE_NAMES.collector,
|
||||||
|
value: 'collector' satisfies TreeTypeString,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.setRequired(false),
|
||||||
|
)
|
||||||
|
.addStringOption((option) => addTankChoices(option).setRequired(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
@@ -1,55 +1,49 @@
|
|||||||
import {
|
import {
|
||||||
|
SlashCommandBuilder,
|
||||||
SlashCommandSubcommandBuilder,
|
SlashCommandSubcommandBuilder,
|
||||||
SlashCommandSubcommandGroupBuilder,
|
SlashCommandSubcommandGroupBuilder,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
|
|
||||||
export type PeriodType = 'period' | 'custom';
|
export type PeriodType = 'today' | '30' | '60' | '90' | 'career' | 'custom';
|
||||||
export type PeriodSize = `${number}` | 'career';
|
export type PeriodSize = 'today' | `${number}` | 'career';
|
||||||
|
|
||||||
export default function addPeriodSubCommands(
|
export default function addPeriodSubCommands(
|
||||||
option: SlashCommandSubcommandGroupBuilder,
|
option: SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
|
||||||
extra: (
|
extra: (
|
||||||
option: SlashCommandSubcommandBuilder,
|
option: SlashCommandSubcommandBuilder,
|
||||||
) => SlashCommandSubcommandBuilder = (option) => option,
|
) => SlashCommandSubcommandBuilder = (option) => option,
|
||||||
) {
|
) {
|
||||||
return option
|
option
|
||||||
.addSubcommand((option) =>
|
.addSubcommand((option) =>
|
||||||
extra(
|
extra(option.setName('today').setDescription('Today')),
|
||||||
option
|
)
|
||||||
.setName('period' satisfies PeriodType)
|
.addSubcommand((option) =>
|
||||||
.setDescription("Preset period's statistics")
|
extra(option.setName('30').setDescription('30 days')),
|
||||||
.addStringOption((option) =>
|
)
|
||||||
option
|
.addSubcommand((option) =>
|
||||||
.setName('period')
|
extra(option.setName('60').setDescription('60 days')),
|
||||||
.setDescription('A preset period')
|
)
|
||||||
.setChoices(
|
.addSubcommand((option) =>
|
||||||
{ name: 'Today', value: '1' satisfies PeriodSize },
|
extra(option.setName('90').setDescription('90 days')),
|
||||||
{ name: '30 days', value: '30' satisfies PeriodSize },
|
)
|
||||||
{ name: '60 days', value: '60' satisfies PeriodSize },
|
.addSubcommand((option) =>
|
||||||
{ name: '90 days', value: '90' satisfies PeriodSize },
|
extra(option.setName('career').setDescription('Career')),
|
||||||
{ name: 'Career', value: 'career' satisfies PeriodSize },
|
|
||||||
)
|
|
||||||
.setRequired(true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.addSubcommand((option) =>
|
.addSubcommand((option) =>
|
||||||
extra(
|
extra(
|
||||||
option
|
option
|
||||||
.setName('custom' satisfies PeriodType)
|
.setName('custom')
|
||||||
.setDescription("Custom period's statistics")
|
.setDescription('Custom range')
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('start')
|
.setName('start')
|
||||||
.setDescription('Days ago from today')
|
.setDescription('Start in number of days ago from today')
|
||||||
.setMinValue(0)
|
|
||||||
.setRequired(true),
|
.setRequired(true),
|
||||||
)
|
)
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName('end')
|
.setName('end')
|
||||||
.setDescription('Days ago from today')
|
.setDescription('End in number of days')
|
||||||
.setMinValue(0)
|
|
||||||
.setRequired(true),
|
.setRequired(true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
|
||||||
import { TreeTypeString } from '../../components/Tanks';
|
|
||||||
import { StatType } from '../../renderers/stats';
|
|
||||||
import { tankopediaInfo } from '../blitz/tankopedia';
|
|
||||||
import addPeriodSubCommands from './addPeriodSubCommands';
|
|
||||||
import addTankChoices from './addTankChoices';
|
|
||||||
import addTierChoices from './addTierChoices';
|
|
||||||
import addUsernameChoices from './addUsernameChoices';
|
|
||||||
|
|
||||||
export default async function addStatTypeSubCommandGroups(
|
|
||||||
option: SlashCommandBuilder,
|
|
||||||
addMultiTank = true,
|
|
||||||
) {
|
|
||||||
const awaitedTankopediaInfo = await tankopediaInfo;
|
|
||||||
|
|
||||||
option
|
|
||||||
.addSubcommandGroup((option) =>
|
|
||||||
addPeriodSubCommands(option, (option) =>
|
|
||||||
option.addStringOption(addUsernameChoices),
|
|
||||||
)
|
|
||||||
.setName('player' satisfies StatType)
|
|
||||||
.setDescription('Player statistics'),
|
|
||||||
)
|
|
||||||
.addSubcommandGroup((option) =>
|
|
||||||
addPeriodSubCommands(option, (option) =>
|
|
||||||
option
|
|
||||||
.addStringOption(addTankChoices)
|
|
||||||
.addStringOption(addUsernameChoices),
|
|
||||||
)
|
|
||||||
.setName('tank' satisfies StatType)
|
|
||||||
.setDescription('Tank statistics'),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addMultiTank)
|
|
||||||
option.addSubcommandGroup((option) =>
|
|
||||||
addPeriodSubCommands(option, (option) =>
|
|
||||||
option
|
|
||||||
.addStringOption(addUsernameChoices)
|
|
||||||
.addStringOption((option) => {
|
|
||||||
option
|
|
||||||
.setName('nation')
|
|
||||||
.setDescription('Nation')
|
|
||||||
.addChoices(
|
|
||||||
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
|
|
||||||
([type, name]) => ({ name, value: type }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return option;
|
|
||||||
})
|
|
||||||
.addStringOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('tank-type')
|
|
||||||
.setDescription('Type')
|
|
||||||
.addChoices(
|
|
||||||
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
|
|
||||||
([type, name]) => ({
|
|
||||||
name: name.replace(' Tank', ''),
|
|
||||||
value: type,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setRequired(false),
|
|
||||||
)
|
|
||||||
.addStringOption((option) =>
|
|
||||||
addTierChoices(option).setRequired(false),
|
|
||||||
)
|
|
||||||
.addStringOption((option) =>
|
|
||||||
option
|
|
||||||
.setName('tree-type')
|
|
||||||
.setDescription('Tech tree type')
|
|
||||||
.addChoices(
|
|
||||||
{
|
|
||||||
name: 'Tech Tree',
|
|
||||||
value: 'techtree' satisfies TreeTypeString,
|
|
||||||
},
|
|
||||||
{ name: 'Premium', value: 'premium' satisfies TreeTypeString },
|
|
||||||
{
|
|
||||||
name: 'Collector',
|
|
||||||
value: 'collector' satisfies TreeTypeString,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.setRequired(false),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setName('multi-tank' satisfies StatType)
|
|
||||||
.setDescription('Multiple tanks'),
|
|
||||||
);
|
|
||||||
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SlashCommandStringOption } from 'discord.js';
|
import { SlashCommandStringOption } from 'discord.js';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash';
|
||||||
import numberToRomanNumeral from '../blitz/numberToRomanNumeral';
|
import numberToRomanNumeral from '../math/numberToRomanNumeral';
|
||||||
|
|
||||||
export default function addTierChoices(option: SlashCommandStringOption) {
|
export default function addTierChoices(option: SlashCommandStringOption) {
|
||||||
return option
|
return option
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
CacheType,
|
CacheType,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { REGION_NAMES } from '../../constants/regions';
|
import { REGION_NAMES } from '../../constants/regions';
|
||||||
import listClans from '../blitz/listClans';
|
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
|
||||||
|
|
||||||
export default async function autocompleteClan(
|
export default async function autocompleteClan(
|
||||||
interaction: AutocompleteInteraction<CacheType>,
|
interaction: AutocompleteInteraction<CacheType>,
|
||||||
@@ -12,7 +12,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([]);
|
if (focusedOption.value.length < 2) return interaction.respond([]);
|
||||||
const players = await listClans(focusedOption.value);
|
const players = await searchClansAcrossRegions(focusedOption.value);
|
||||||
|
|
||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
players
|
players
|
||||||
@@ -25,6 +25,4 @@ export default async function autocompleteClan(
|
|||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Clan autocomplete for ${focusedOption.value}`);
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
CacheType,
|
CacheType,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import { TANK_NAMES_DIACRITICS } from '../blitz/tankopedia';
|
import { TANK_NAMES_DIACRITICS } from '../blitzstars/tankopedia';
|
||||||
|
|
||||||
export default async function autocompleteTanks(
|
export default async function autocompleteTanks(
|
||||||
interaction: AutocompleteInteraction<CacheType>,
|
interaction: AutocompleteInteraction<CacheType>,
|
||||||
@@ -28,6 +28,4 @@ export default async function autocompleteTanks(
|
|||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Tanks autocomplete for ${focusedOption.value}`);
|
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,14 @@ import {
|
|||||||
CacheType,
|
CacheType,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { REGION_NAMES } from '../../constants/regions';
|
import { REGION_NAMES } from '../../constants/regions';
|
||||||
import listPlayers from '../blitz/listPlayers';
|
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
|
||||||
|
|
||||||
export default async function autocompleteUsername(
|
export default async function autocompleteUsername(
|
||||||
interaction: AutocompleteInteraction<CacheType>,
|
interaction: AutocompleteInteraction<CacheType>,
|
||||||
) {
|
) {
|
||||||
const focusedOption = interaction.options.getFocused(true);
|
const focusedOption = interaction.options.getFocused(true);
|
||||||
if (focusedOption.name !== 'username') return;
|
if (focusedOption.name !== 'username') return;
|
||||||
const players = await listPlayers(focusedOption.value);
|
const players = await searchPlayersAcrossRegions(focusedOption.value);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
@@ -25,11 +25,5 @@ export default async function autocompleteUsername(
|
|||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
} catch (error) {}
|
||||||
console.log(`Username autocomplete for ${focusedOption.value}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`Failed to autocomplete username for ${focusedOption.value} in time`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
ComponentEmojiResolvable,
|
ComponentEmojiResolvable,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
|
|
||||||
export default function linkButton(
|
export default function buttonLink(
|
||||||
url: string,
|
url: string,
|
||||||
label: string,
|
label: string,
|
||||||
emoji?: ComponentEmojiResolvable,
|
emoji?: ComponentEmojiResolvable,
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
ComponentEmojiResolvable,
|
ComponentEmojiResolvable,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
|
|
||||||
export default function primaryButton(
|
export default function buttonPrimary(
|
||||||
id: string,
|
id: string,
|
||||||
label: string,
|
label: string,
|
||||||
emoji?: ComponentEmojiResolvable,
|
emoji?: ComponentEmojiResolvable,
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ChatInputCommandInteraction } from 'discord.js';
|
||||||
|
import buttonPrimary from './buttonPrimary';
|
||||||
|
|
||||||
|
export function buttonRefresh(
|
||||||
|
interaction: ChatInputCommandInteraction,
|
||||||
|
path: string,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
interaction.appPermissions?.has('AttachFiles') &&
|
||||||
|
interaction.appPermissions?.has('ViewChannel')
|
||||||
|
) {
|
||||||
|
return buttonPrimary(path, 'Refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Loaded 100 of 145 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user