feat: renamed /ratings to /ratings-leaderboard

This commit is contained in:
TrèsAbhi committed 2024-03-20 17:33:21 -05:00
1 parent 91d48eeadd
commit f7c6a065c1
35 files changed
+646 -680

No files matched your search

@@ -1,4 +1,4 @@
name: CD Ratings Asia
name: CD Rating Asia
on:
schedule:
@@ -31,4 +31,4 @@ jobs:
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=asia --target=midnight --production
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=asia --target=midnight --production
@@ -1,4 +1,4 @@
name: CD Ratings Europe
name: CD Rating Europe
on:
schedule:
@@ -31,4 +31,4 @@ jobs:
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=eu --target=midnight --production
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=eu --target=midnight --production
@@ -1,4 +1,4 @@
name: CD Ratings North America
name: CD Rating North America
on:
schedule:
@@ -31,13 +31,13 @@ jobs:
run: yarn
- name: Generate NA leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=na --target=latest --production
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=na --target=latest --production
if: always()
- name: Generate EU leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=eu --target=latest --production
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=eu --target=latest --production
if: always()
- name: Generate Asia leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=asia --target=latest --production
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=asia --target=latest --production
if: always()
@@ -1,4 +1,4 @@
name: CD Ratings North America
name: CD Rating North America
on:
schedule:
@@ -31,4 +31,4 @@ jobs:
run: yarn
- name: Generate leaderboard
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingsleaderboard --region=na --target=midnight --production
run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} yarn publish:ratingleaderboard --region=na --target=midnight --production
+1 -2
View File
@@ -100,8 +100,7 @@
"POSITIVEZ",
"Promisable",
"Pyogenics",
"ratingsleaderboard",
"ratingsplot",
"ratingleaderboard",
"raycaster",
"researchable",
"Resvg",
+3 -3
View File
@@ -1,9 +1,9 @@
# Blitzkrieg Ratings Leaderboard
# Blitzkrieg Rating Leaderboard
A straightforward way of storing the entire World of Tanks Blitz ratings leaderboard to disk.
A straightforward way of storing the entire World of Tanks Blitz rating leaderboard to disk.
```cpp
primary BlitzkriegRatingsLeaderboard {
primary BlitzkriegRatingLeaderboard {
utf8"BKRL" magic;
Header header;
primary Body<header> body;
+1 -1
View File
@@ -18,5 +18,5 @@ Blitzkrieg is available as a Discord bot and can be [installed very easily](/gui
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
- Session tracker
- Ratings leaderboard
- Rating leaderboard
- Way more to come
+2 -3
View File
@@ -8,17 +8,16 @@
"build:bot": "vite-node --script scripts/build",
"build:website": "next build",
"build:docs": "vitepress build docs",
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
"build:test": "vite-node --script scripts/buildTest",
"build:ratingsplot": "vite-node --script scripts/buildRatingsPlot",
"dev:bot": "yarn build:bot --dev && yarn start:bot",
"dev:docs": "vitepress dev docs",
"dev:website": "next dev",
"start:bot": "node --enable-source-maps dist/bot/index.cjs",
"start:website": "next start",
"host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
"publish:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard --publish",
"setup:patches": "vite-node --script scripts/applyPatches",
"lint": "npx tsc",
"lint:website": "next lint",
File renamed without changes.
File renamed without changes.
@@ -1,10 +1,10 @@
import { chunk } from 'lodash';
import { argv } from 'process';
import {
RatingsInfo,
RatingsNeighbors,
RatingsPlayer,
} from '../src/commands/ratings';
RatingInfo,
RatingNeighbors,
RatingPlayer,
} from '../src/commands/ratingLeaderboard';
import { RegionSubdomain } from '../src/constants/regions';
import { getAccountInfo } from '../src/core/blitz/getAccountInfo';
import regionSubdomainToRegion from '../src/core/blitz/regionSubdomainToRegion';
@@ -40,20 +40,20 @@ if (!regionSubdomain) throw new Error('Region parameter not specified');
const region = regionSubdomainToRegion(regionSubdomain);
const NEIGHBORS = 2 ** 8;
const PLAYERS = await patientFetchJSON<RatingsInfo>(
const PLAYERS = await patientFetchJSON<RatingInfo>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) {
throw new Error('No current ratings season running');
throw new Error('No current rating season running');
}
const players: Record<number, RatingsPlayer> = {};
const players: Record<number, RatingPlayer> = {};
const leaderboard: BkrlMinimalEntry[] = [];
let registered = 0;
async function branchFromPlayer(id: number) {
const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
);
@@ -84,7 +84,7 @@ async function branchFromPlayer(id: number) {
}
async function branchFromLeague(id: number) {
const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
);
@@ -164,7 +164,7 @@ for (const leaderboardChunk of chunk(leaderboard, 100)) {
chunkIndex++;
}
const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
);
const normalizedServer = regionSubdomain === 'na' ? 'com' : regionSubdomain;
@@ -180,19 +180,19 @@ leaderboardWriteStream.bkrl({
const changes: FileChange[] = [
{
path: `regions/${normalizedServer}/ratings/${info.current_season}/info.cdon.lz4`,
path: `regions/${normalizedServer}/rating/${info.current_season}/info.cdon.lz4`,
content: superCompress(infoContent),
encoding: 'base64',
},
{
path: `regions/${normalizedServer}/ratings/${info.current_season}/${target}.bkrl`,
path: `regions/${normalizedServer}/rating/${info.current_season}/${target}.bkrl`,
content: Buffer.from(leaderboardWriteStream.uint8Array).toString('base64'),
encoding: 'base64',
},
];
commitAssets(
`ratings leaderboard ${regionSubdomain} ${target}`,
`rating leaderboard ${regionSubdomain} ${target}`,
changes,
production,
);
-48
View File
@@ -1,48 +0,0 @@
// what the hell is this script???
import { writeFile } from 'fs/promises';
import { Region } from '../src/constants/regions';
import { getRatingsNeighbors } from '../src/core/blitz/getRatingsNeighbors';
const REGION: Region = 'com';
const PLAYER = 1041988373;
const ITERATIONS = 2 ** 4;
const MAX_NEIGHBORS = 2 ** 7;
const data: [number, number][] = [];
let finished = 0;
for (let index = 0; index < ITERATIONS; index++) {
const neighbors = Math.round((Math.random() * MAX_NEIGHBORS) / 2) * 2;
const start = Date.now();
console.log(`🟡 Iteration ${index} with ${neighbors} neighbors`);
try {
await getRatingsNeighbors(REGION, PLAYER, neighbors);
finished++;
console.log(
`🟢 (${Math.round(
100 * (finished / ITERATIONS),
)}%) Iteration ${index} with ${neighbors} neighbors`,
);
const end = Date.now();
const time = end - start;
data.push([neighbors, time]);
} catch (error) {
console.log(
`🔴 Iteration ${index} with ${neighbors} neighbors\n\n${error}`,
);
}
}
console.log('🟢 Done, sorting');
data.sort(([neighborsA], [neighborsB]) => neighborsA - neighborsB);
console.log('🟢 Writing');
writeFile(
'dist/ratingsPlot.csv',
data.map(([neighbors, time]) => `${neighbors},${time}`).join('\n'),
);
@@ -1,10 +1,10 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../constants/regions';
import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo';
import getRatingInfo from '../../../../../../core/blitz/getRatingInfo';
export async function GET(
request: Request,
{ params }: { params: { region: Region } },
) {
return NextResponse.json(await getRatingsInfo(params.region));
return NextResponse.json(await getRatingInfo(params.region));
}
@@ -1,12 +1,12 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions';
import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague';
import { getRatingLeague } from '../../../../../../../core/blitz/getRatingLeague';
export async function GET(
request: Request,
{ params }: { params: { region: Region; league: number } },
) {
return NextResponse.json(
await getRatingsLeague(params.region, params.league, true),
await getRatingLeague(params.region, params.league, true),
);
}
@@ -1,12 +1,12 @@
import { NextResponse } from 'next/server';
import { Region } from '../../../../../../../constants/regions';
import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers';
import { searchCurrentRatingPlayers } from '../../../../../../../core/blitz/searchCurrentRatingPlayers';
export async function GET(
request: Request,
{ params }: { params: { region: Region; username: string } },
) {
return NextResponse.json(
await searchCurrentRatingsPlayers(params.region, params.username),
await searchCurrentRatingPlayers(params.region, params.username),
);
}
+44 -44
View File
@@ -15,11 +15,11 @@ import { produce } from 'immer';
import { debounce, range } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { create } from 'zustand';
import { RatingsInfo, RatingsPlayer } from '../../../commands/ratings';
import { RatingInfo, RatingPlayer } from '../../../commands/ratingLeaderboard';
import PageWrapper from '../../../components/PageWrapper';
import { Skeleton } from '../../../components/Skeleton';
import { LEAGUES } from '../../../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings';
import { FIRST_ARCHIVED_RATING_SEASON } from '../../../constants/rating';
import {
REGIONS,
Region,
@@ -28,17 +28,17 @@ import {
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../../../core/blitz/getRatingsInfo';
import { getRatingsLeague } from '../../../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../../../core/blitz/getRatingsNeighbors';
import { searchCurrentRatingsPlayers } from '../../../core/blitz/searchCurrentRatingsPlayers';
import getRatingInfo from '../../../core/blitz/getRatingInfo';
import { getRatingLeague } from '../../../core/blitz/getRatingLeague';
import { getRatingNeighbors } from '../../../core/blitz/getRatingNeighbors';
import { searchCurrentRatingPlayers } from '../../../core/blitz/searchCurrentRatingPlayers';
import {
AccountList,
AccountListItem,
} from '../../../core/blitz/searchPlayersAcrossRegions';
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../../../core/blitzkrieg/getArchivedRatingsInfo';
import { getArchivedRatingsLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboard';
import getArchivedRatingInfo from '../../../core/blitzkrieg/getArchivedRatingInfo';
import { getArchivedRatingLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingLeaderboard';
import { theme } from '../../../stitches.config';
import { PageTurner } from './components/PageTurner';
@@ -51,7 +51,7 @@ interface NameCacheEntry {
}
type NameCache = Record<Region, Record<number, NameCacheEntry | null>>;
type LeaderboardCache = Record<Region, Record<number, Record<number, number>>>;
type InfoCache = Record<Region, Record<number, RatingsInfo | undefined>>;
type InfoCache = Record<Region, Record<number, RatingInfo | undefined>>;
type ScoreCache = Record<Region, Record<number, Record<number, number>>>;
const useNameCache = create<NameCache>(() => ({
@@ -93,7 +93,7 @@ export default function Page() {
{ type: 'id'; id: number } | { type: 'position'; position: number } | null
>(null);
const ratingsInfo = useInfoCache()[region][season];
const ratingInfo = useInfoCache()[region][season];
const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState<
number | null
>(null);
@@ -111,7 +111,7 @@ export default function Page() {
});
const pages = Math.ceil(
(ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) /
(ratingInfo && ratingInfo.detail === undefined ? ratingInfo?.count : 1) /
ROWS_PER_PAGE,
);
@@ -122,8 +122,8 @@ export default function Page() {
(async () => {
const newInfo =
season === 0
? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season);
? await getRatingInfo(region)
: await getArchivedRatingInfo(region, season);
useInfoCache.setState(
produce((draft: InfoCache) => {
draft[region][season] = newInfo;
@@ -140,7 +140,7 @@ export default function Page() {
const results = (
await Promise.all(
range(0, 5).map(async (league: number) => {
const data = await getRatingsLeague(region, league);
const data = await getRatingLeague(region, league);
leaguePositionCache.current[region][league] = data[0].number - 1;
return data;
@@ -178,12 +178,12 @@ export default function Page() {
);
})();
} else {
getArchivedRatingsLeaderboard(region, season).then((results) => {
getArchivedRatingLeaderboard(region, season).then((results) => {
useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => {
if (!(season in draft[region])) draft[region][season] = {};
results.forEach((result, index) => {
results.entries.forEach((result, index) => {
draft[region][season][index] = result.id;
});
}),
@@ -192,7 +192,7 @@ export default function Page() {
produce((draft: ScoreCache) => {
if (!(season in draft[region])) draft[region][season] = {};
results.forEach((result) => {
results.entries.forEach((result) => {
draft[region][season][result.id] = result.score;
});
}),
@@ -213,7 +213,7 @@ export default function Page() {
}, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]);
// neighboring page caching for all seasons
useEffect(() => {
if (!ratingsInfo || ratingsInfo?.detail) return;
if (!ratingInfo || ratingInfo?.detail) return;
if (season === 0) {
const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2;
@@ -236,7 +236,7 @@ export default function Page() {
const ids = range(
Math.max(0, (page - 1) * ROWS_PER_PAGE),
Math.min(
ratingsInfo.count - 1,
ratingInfo.count - 1,
(page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE,
),
)
@@ -265,7 +265,7 @@ export default function Page() {
page,
region,
season,
ratingsInfo?.detail,
ratingInfo?.detail,
leaderboard[region][season] !== undefined &&
0 in leaderboard[region][season],
]);
@@ -279,7 +279,7 @@ export default function Page() {
async function cacheNeighbors(
id: number,
size: number,
): Promise<RatingsPlayer[] | null> {
): Promise<RatingPlayer[] | null> {
const radius = Math.round(size / 2);
const targetPosition = Object.entries(leaderboard[region][season]).find(
([, player]) => id === player,
@@ -303,7 +303,7 @@ export default function Page() {
}
}
const { neighbors } = await getRatingsNeighbors(region, id, size, true);
const { neighbors } = await getRatingNeighbors(region, id, size, true);
useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => {
@@ -341,11 +341,11 @@ export default function Page() {
return neighbors;
}
async function jumpToPosition(position: number) {
if (!ratingsInfo || ratingsInfo?.detail) return;
if (!ratingInfo || ratingInfo?.detail) return;
const targetPosition = Math.min(
ratingsInfo.count - 1,
Math.max(0, Math.min(position, ratingsInfo.count - 1)),
ratingInfo.count - 1,
Math.max(0, Math.min(position, ratingInfo.count - 1)),
);
if (season === 0) {
@@ -378,7 +378,7 @@ export default function Page() {
const loadingDirection = Math.sign(targetPosition - closestPosition);
let seedingPlayer = closestPlayerId as number;
let targetPositionAcquired = false;
let neighbors: RatingsPlayer[] | null;
let neighbors: RatingPlayer[] | null;
setLoadingProgress([0, closestPositionDistance]);
@@ -423,7 +423,7 @@ export default function Page() {
}
}
async function jumpToScore() {
if (!ratingsInfo || ratingsInfo?.detail) return;
if (!ratingInfo || ratingInfo?.detail) return;
const targetScore = scoreInput.current!.valueAsNumber;
@@ -522,7 +522,7 @@ export default function Page() {
} else {
let playerIndex = -1;
for (let index = ratingsInfo.count; index >= 0; index--) {
for (let index = ratingInfo.count; index >= 0; index--) {
if (!(index in leaderboard[region][season])) continue;
if (scores[leaderboard[region][season][index]] >= targetScore) {
@@ -545,7 +545,7 @@ export default function Page() {
}
const totalPlayers =
!ratingsInfo?.detail && ratingsInfo?.count ? ratingsInfo.count : undefined;
!ratingInfo?.detail && ratingInfo?.count ? ratingInfo.count : undefined;
return (
<PageWrapper color="orange">
@@ -573,7 +573,7 @@ export default function Page() {
<Select.Trigger style={{ flex: 1 }} />
<Select.Content>
{ratingsInfo?.detail === undefined && (
{ratingInfo?.detail === undefined && (
<Select.Item value="0">Current season</Select.Item>
)}
@@ -582,7 +582,7 @@ export default function Page() {
{latestArchivedSeasonNumber &&
range(
FIRST_ARCHIVED_RATINGS_SEASON,
FIRST_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + 1,
)
.map((season) => (
@@ -681,8 +681,8 @@ export default function Page() {
<Select.Trigger />
<Select.Content>
{ratingsInfo?.detail === undefined &&
ratingsInfo?.leagues.map(({ index, title }) => (
{ratingInfo?.detail === undefined &&
ratingInfo?.leagues.map(({ index, title }) => (
<Select.Item key={index} value={`${index}`}>
{title}
</Select.Item>
@@ -715,8 +715,8 @@ export default function Page() {
}
} else {
if (
!ratingsInfo ||
ratingsInfo.detail ||
!ratingInfo ||
ratingInfo.detail ||
!leaderboard
)
return;
@@ -727,7 +727,7 @@ export default function Page() {
for (
let index = 0;
index < ratingsInfo.count;
index < ratingInfo.count;
index++
) {
if (
@@ -779,7 +779,7 @@ export default function Page() {
setSearchLoading(true);
const accountList =
await searchCurrentRatingsPlayers(
await searchCurrentRatingPlayers(
region,
trimmedSearch,
);
@@ -885,12 +885,12 @@ export default function Page() {
id: player.account_id,
});
} else {
if (!ratingsInfo || ratingsInfo.detail) return;
if (!ratingInfo || ratingInfo.detail) return;
let playerIndex = -1;
for (
let index = 0;
index < ratingsInfo.count;
index < ratingInfo.count;
index++
) {
if (
@@ -952,7 +952,7 @@ export default function Page() {
<PageTurner
totalPlayers={
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0
}
leaderboard={leaderboard[region][season] ?? {}}
rowsPerPage={ROWS_PER_PAGE}
@@ -982,7 +982,7 @@ export default function Page() {
page * ROWS_PER_PAGE,
Math.min(
page * ROWS_PER_PAGE + ROWS_PER_PAGE,
ratingsInfo?.detail ? 0 : (ratingsInfo?.count ?? 1) - 1,
ratingInfo?.detail ? 0 : (ratingInfo?.count ?? 1) - 1,
),
).map((index) => {
const id = leaderboard[region][season]?.[index];
@@ -994,8 +994,8 @@ export default function Page() {
: highlightedPlayer.position === index
: false;
const reward =
!ratingsInfo?.detail && ratingsInfo?.rewards
? ratingsInfo.rewards.find(
!ratingInfo?.detail && ratingInfo?.rewards
? ratingInfo.rewards.find(
(reward) =>
reward.from_position <= position &&
reward.to_position >= position,
@@ -1091,7 +1091,7 @@ export default function Page() {
<PageTurner
totalPlayers={
(ratingsInfo?.detail ? undefined : ratingsInfo?.count) ?? 0
(ratingInfo?.detail ? undefined : ratingInfo?.count) ?? 0
}
rowsPerPage={ROWS_PER_PAGE}
leaderboard={leaderboard[region][season] ?? {}}
+513
View File
@@ -0,0 +1,513 @@
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from 'discord.js';
import { range } from 'lodash';
import markdownEscape from 'markdown-escape';
import CommandWrapper from '../components/CommandWrapper';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
import { LEAGUES } from '../constants/leagues';
import { FIRST_ARCHIVED_RATING_SEASON } from '../constants/rating';
import { Region } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingInfo from '../core/blitz/getRatingInfo';
import { getRatingLeague } from '../core/blitz/getRatingLeague';
import { getRatingNeighbors } from '../core/blitz/getRatingNeighbors';
import { isOnGoingRatingSeason } from '../core/blitz/isOnGoingRatingSeason';
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingInfo from '../core/blitzkrieg/getArchivedRatingInfo';
import { getArchivedRatingLeaderboard } from '../core/blitzkrieg/getArchivedRatingLeaderboard';
import getArchivedRatingMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingMidnightLeaderboard';
import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { BkrlDiscriminatedEntries } from '../core/streams/bkrl';
import { CommandRegistry } from '../events/interactionCreate';
import { UserError } from '../hooks/userError';
export interface RatingPlayer {
spa_id: number;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
percentile: number;
skip: boolean;
updated_at: string;
score: number;
nickname: string;
clan_tag: string;
}
export interface LeagueTop {
result: RatingPlayer[];
}
export type RatingReward = {
from_position: number;
to_position: number;
count: number;
} & (
| {
type: 'vehicle';
vehicle: {
id: number;
name: string;
nation: string;
subnation: string;
use_subnation_flag: boolean;
type_slug: string;
level: number;
roman_level: string;
user_string: string;
image_url: string;
preview_url: string;
is_premium: boolean;
is_collectible: boolean;
};
}
| {
type: 'stuff';
stuff: {
name: string;
count: number;
title: string;
image_url: string;
type: string;
sizes: {};
};
}
);
export interface RatingLeague {
title: string;
small_icon: string;
big_icon: string;
background: string;
index: number;
percentile: number;
}
export type RatingInfo =
| {
detail: undefined;
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingReward[];
leagues: RatingLeague[];
}
| { detail: { error: string } };
export interface RatingNeighbors {
neighbors: RatingPlayer[];
}
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const DEFAULT_LIMIT = 10;
export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range(
FIRST_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
function addOptions(option: SlashCommandSubcommandBuilder) {
const { t, translate } = translator(Locale.EnglishUS);
return option
.addIntegerOption((option) =>
option
.setName(t`bot.commands.rating_leaderboard.options.limit`)
.setNameLocalizations(
localizationObject(
'bot.commands.rating_leaderboard.options.limit',
),
)
.setDescription(
translate(
'bot.commands.rating_leaderboard.options.limit.description',
[`${DEFAULT_LIMIT}`],
),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.rating_leaderboard.options.limit.description',
[`${DEFAULT_LIMIT}`],
),
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
)
.addStringOption((option) =>
option
.setName(t`bot.commands.rating_leaderboard.options.season`)
.setNameLocalizations(
localizationObject(
'bot.commands.rating_leaderboard.options.season',
),
)
.setDescription(
translate(
'bot.commands.rating_leaderboard.options.season.description',
),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.rating_leaderboard.options.season.description',
),
)
.setRequired(false)
.addChoices(
...seasonNumbers
.map((number, index) => {
const current =
index === seasonNumbers.length - 1 && onGoingSeason;
return {
name: current
? translate(
'bot.commands.rating_leaderboard.options.season.choices.current',
[`${number}`],
)
: `${number}`,
value: `${number}`,
name_localizations: current
? localizationObject(
'bot.commands.rating_leaderboard.options.season.choices.current',
[`${number}`],
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse(),
),
);
}
resolve({
inProduction: true,
inPublic: true,
command: createLocalizedCommand('rating-leaderboard', [
{
subcommand: 'neighbors',
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: 'league',
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
const { t, translate } = translator(Locale.EnglishUS);
return option
.setName(t`bot.commands.rating_leaderboard.options.league`)
.setNameLocalizations(
localizationObject(
'bot.commands.rating_leaderboard.options.league',
),
)
.setDescription(
translate(
'bot.commands.rating_leaderboard.options.league.description',
),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.rating_leaderboard.options.league.description',
),
)
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name: translate(`common.leagues.${name}`),
value: `${value}`,
name_localizations: localizationObject(
`common.leagues.${name}`,
),
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true);
})
.addStringOption(addRegionChoices);
addOptions(option);
},
},
]),
// BIG TODO: title seems to be in english
async handler(interaction) {
const { t, translate } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbors';
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString('season');
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string;
let titleImage: string | undefined;
let titleDescription: string;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BkrlDiscriminatedEntries | null;
let playerId: number | undefined;
let regionRatingInfo: RatingInfo;
if (subcommand === 'league') {
const leagueIndex = parseInt(
interaction.options.getString('league')!,
);
const region = interaction.options.getString('region') as Region;
regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region)
: getArchivedRatingInfo(region, season!));
if (regionRatingInfo.detail !== undefined)
return t`bot.commands.rating_leaderboard.body.no_ongoing_season`;
const result = isCurrentSeason
? await getRatingLeague(region, leagueIndex).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,
),
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag,
nickname:
clanData[index]?.account_name ??
translate(
'bot.commands.rating_leaderboard.body.deleted_player',
[`${player.id}`],
),
}) satisfies SimplifiedPlayer,
);
},
);
const leagueInfo = regionRatingInfo.leagues[leagueIndex];
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingMidnightLeaderboard(
region,
regionRatingInfo.current_season,
)
: await getArchivedRatingMidnightLeaderboard(region, season!);
players = result;
playersAfter =
regionRatingInfo.count - result[result.length - 1].position;
titleName = `${
leagueInfo
? translate(`common.leagues.${LEAGUES[leagueInfo.index].name}`)
: ''
} - ${translate(`common.regions.short.${region}`)}`;
titleImage = leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`;
titleDescription = translate(
'bot.commands.rating_leaderboard.body.top_players',
[`${limit}`],
);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingInfo = isCurrentSeason
? await getRatingInfo(region)
: await getArchivedRatingInfo(region, season!);
if (regionRatingInfo.detail !== undefined)
return t`bot.commands.rating_leaderboard.body.no_ongoing_season`;
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).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,
),
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.entries.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw new UserError(
translate(
'bot.commands.rating_leaderboard.body.no_participation',
[markdownEscape(accountInfo.nickname), `${season}`],
),
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
['clan'],
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[index]?.clan?.tag,
nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
},
);
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingMidnightLeaderboard(
region,
regionRatingInfo.current_season,
)
: await getArchivedRatingMidnightLeaderboard(region, season!);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : '';
titleDescription = t`bot.commands.rating_leaderboard.body.neighbors`;
playersAfter =
regionRatingInfo.count - neighbors[neighbors.length - 1].position;
playerId = id;
}
const items = players.map((player) => {
const midnightIndex = midnightLeaderboard?.entries.findIndex(
(item) => item.id === player.id,
);
const midnightScore =
midnightIndex === undefined
? player.score
: midnightLeaderboard!.entries[midnightIndex].score;
const midnightPosition =
midnightIndex === undefined ? player.position : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.position}
clan={player.clan}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return (
<CommandWrapper>
<TitleBar
title={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={translate(
'bot.commands.rating_leaderboard.body.more',
[`${playersAfter.toLocaleString(interaction.locale)}`],
)}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
);
},
autocomplete: autocompleteUsername,
});
},
);
-495
View File
@@ -1,495 +0,0 @@
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from 'discord.js';
import { range } from 'lodash';
import markdownEscape from 'markdown-escape';
import CommandWrapper from '../components/CommandWrapper';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
import { LEAGUES } from '../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings';
import { Region } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo';
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason';
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { BkrlDiscriminatedEntries } from '../core/streams/bkrl';
import { CommandRegistry } from '../events/interactionCreate';
import { UserError } from '../hooks/userError';
export interface RatingsPlayer {
spa_id: number;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
percentile: number;
skip: boolean;
updated_at: string;
score: number;
nickname: string;
clan_tag: string;
}
export interface LeagueTop {
result: RatingsPlayer[];
}
export type RatingsReward = {
from_position: number;
to_position: number;
count: number;
} & (
| {
type: 'vehicle';
vehicle: {
id: number;
name: string;
nation: string;
subnation: string;
use_subnation_flag: boolean;
type_slug: string;
level: number;
roman_level: string;
user_string: string;
image_url: string;
preview_url: string;
is_premium: boolean;
is_collectible: boolean;
};
}
| {
type: 'stuff';
stuff: {
name: string;
count: number;
title: string;
image_url: string;
type: string;
sizes: {};
};
}
);
export interface RatingsLeague {
title: string;
small_icon: string;
big_icon: string;
background: string;
index: number;
percentile: number;
}
export type RatingsInfo =
| {
detail: undefined;
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingsReward[];
leagues: RatingsLeague[];
}
| { detail: { error: string } };
export interface RatingsNeighbors {
neighbors: RatingsPlayer[];
}
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const DEFAULT_LIMIT = 10;
export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingsSeason();
const seasonNumbers = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
function addOptions(option: SlashCommandSubcommandBuilder) {
const { t, translate } = translator(Locale.EnglishUS);
return option
.addIntegerOption((option) =>
option
.setName(t`bot.commands.ratings.options.limit`)
.setNameLocalizations(
localizationObject('bot.commands.ratings.options.limit'),
)
.setDescription(
translate('bot.commands.ratings.options.limit.description', [
`${DEFAULT_LIMIT}`,
]),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.ratings.options.limit.description',
[`${DEFAULT_LIMIT}`],
),
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
)
.addStringOption((option) =>
option
.setName(t`bot.commands.ratings.options.season`)
.setNameLocalizations(
localizationObject('bot.commands.ratings.options.season'),
)
.setDescription(
translate('bot.commands.ratings.options.season.description'),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.ratings.options.season.description',
),
)
.setRequired(false)
.addChoices(
...seasonNumbers
.map((number, index) => {
const current =
index === seasonNumbers.length - 1 && onGoingSeason;
return {
name: current
? translate(
'bot.commands.ratings.options.season.choices.current',
[`${number}`],
)
: `${number}`,
value: `${number}`,
name_localizations: current
? localizationObject(
'bot.commands.ratings.options.season.choices.current',
[`${number}`],
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse(),
),
);
}
resolve({
inProduction: true,
inPublic: true,
command: createLocalizedCommand('ratings', [
{
subcommand: 'neighbors',
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: 'league',
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
const { t, translate } = translator(Locale.EnglishUS);
return option
.setName(t`bot.commands.ratings.options.league`)
.setNameLocalizations(
localizationObject('bot.commands.ratings.options.league'),
)
.setDescription(
translate('bot.commands.ratings.options.league.description'),
)
.setDescriptionLocalizations(
localizationObject(
'bot.commands.ratings.options.league.description',
),
)
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name: translate(`common.leagues.${name}`),
value: `${value}`,
name_localizations: localizationObject(
`common.leagues.${name}`,
),
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true);
})
.addStringOption(addRegionChoices);
addOptions(option);
},
},
]),
// BIG TODO: title seems to be in english
async handler(interaction) {
const { t, translate } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbors';
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString('season');
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string;
let titleImage: string | undefined;
let titleDescription: string;
let playersAfter: number;
let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BkrlDiscriminatedEntries | null;
let playerId: number | undefined;
let regionRatingsInfo: RatingsInfo;
if (subcommand === 'league') {
const leagueIndex = parseInt(interaction.options.getString('league')!);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (isCurrentSeason
? getRatingsInfo(region)
: getArchivedRatingsInfo(region, season!));
if (regionRatingsInfo.detail !== undefined)
return t`bot.commands.ratings.body.no_ongoing_season`;
const result = isCurrentSeason
? await getRatingsLeague(region, leagueIndex).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,
),
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
['clan'],
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag,
nickname:
clanData[index]?.account_name ??
translate('bot.commands.ratings.body.deleted_player', [
`${player.id}`,
]),
}) satisfies SimplifiedPlayer,
);
},
);
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
players = result;
playersAfter =
regionRatingsInfo.count - result[result.length - 1].position;
titleName = `${
leagueInfo
? translate(`common.leagues.${LEAGUES[leagueInfo.index].name}`)
: ''
} - ${translate(`common.regions.short.${region}`)}`;
titleImage = leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`;
titleDescription = translate('bot.commands.ratings.body.top_players', [
`${limit}`,
]);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo = isCurrentSeason
? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season!);
if (regionRatingsInfo.detail !== undefined)
return t`bot.commands.ratings.body.no_ongoing_season`;
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors = isCurrentSeason
? await getRatingsNeighbors(region, id, Math.round(limit / 2)).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,
),
)
: await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.entries.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw new UserError(
translate('bot.commands.ratings.body.no_participation', [
markdownEscape(accountInfo.nickname),
`${season}`,
]),
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
['clan'],
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[index]?.clan?.tag,
nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
},
);
midnightLeaderboard = isCurrentSeason
? await getArchivedRatingsMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: await getArchivedRatingsMidnightLeaderboard(region, season!);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : '';
titleDescription = t`bot.commands.ratings.body.neighbors`;
playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
playerId = id;
}
const items = players.map((player) => {
const midnightIndex = midnightLeaderboard?.entries.findIndex(
(item) => item.id === player.id,
);
const midnightScore =
midnightIndex === undefined
? player.score
: midnightLeaderboard!.entries[midnightIndex].score;
const midnightPosition =
midnightIndex === undefined ? player.position : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.position}
clan={player.clan}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
return (
<CommandWrapper>
<TitleBar
title={titleName}
image={titleImage}
description={titleDescription}
/>
{items && (
<Leaderboard.Root>
{items}
{playersAfter > 0 && (
<Leaderboard.Gap
message={translate('bot.commands.ratings.body.more', [
`${playersAfter.toLocaleString(interaction.locale)}`,
])}
/>
)}
</Leaderboard.Root>
)}
</CommandWrapper>
);
},
autocomplete: autocompleteUsername,
});
});
@@ -154,6 +154,7 @@ export function Item({
}
/>
)}
{score !== undefined && (
<span
style={{
+1
View File
@@ -0,0 +1 @@
export const FIRST_ARCHIVED_RATING_SEASON = 49;
-1
View File
@@ -1 +0,0 @@
export const FIRST_ARCHIVED_RATINGS_SEASON = 49;
+3 -3
View File
@@ -18,9 +18,9 @@ export const TOOLS: Tool[] = [
description: 'Blitz tank encyclopedia',
},
{
id: 'ratings',
title: 'Ratings',
description: 'Ratings gamemode leaderboards',
id: 'rating-leaderboard',
title: 'Rating leaderboard',
description: 'Full rating gamemode leaderboard',
},
{
id: 'discord',
@@ -1,26 +1,26 @@
import { RatingsInfo } from '../../commands/ratings';
import { RatingInfo } from '../../commands/ratingLeaderboard';
import { Region } from '../../constants/regions';
import { context } from '../blitzkrieg/context';
import { patientFetch } from '../blitzkrieg/patientFetch';
import regionToRegionSubdomain from './regionToRegionSubdomain';
const ratingsInfoCache: Partial<Record<Region, RatingsInfo>> = {};
const cache: Partial<Record<Region, RatingInfo>> = {};
export default async function getRatingsInfo(region: Region) {
if (ratingsInfoCache[region]) return ratingsInfoCache[region]!;
export default async function getRatingInfo(region: Region) {
if (cache[region]) return cache[region]!;
const response = await patientFetch(
context === 'website'
? `/api/${region}/ratings/current/info`
? `/api/${region}/rating/current/info`
: `https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
undefined,
{ cache: 'no-store' },
);
const data = (await response.json()) as RatingsInfo;
const data = (await response.json()) as RatingInfo;
ratingsInfoCache[region] = data;
cache[region] = data;
return data;
}
@@ -1,10 +1,10 @@
import { LeagueTop, RatingsPlayer } from '../../commands/ratings';
import { LeagueTop, RatingPlayer } from '../../commands/ratingLeaderboard';
import { Region } from '../../constants/regions';
import { context } from '../blitzkrieg/context';
import { patientFetch } from '../blitzkrieg/patientFetch';
import regionToRegionSubdomain from './regionToRegionSubdomain';
export async function getRatingsLeague(
export async function getRatingLeague(
region: Region,
league: number,
usePatientFetcher = false,
@@ -15,7 +15,7 @@ export async function getRatingsLeague(
: fetch(url, { cache: 'no-store' });
const response = await fetcher(
context === 'website'
? `/api/${region}/ratings/current/league/${league}`
? `/api/${region}/rating/current/league/${league}`
: `https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
@@ -23,6 +23,6 @@ export async function getRatingsLeague(
const json = await response.json();
return context === 'website'
? (json as RatingsPlayer[])
? (json as RatingPlayer[])
: (json as LeagueTop).result;
}
@@ -1,10 +1,10 @@
import { RatingsNeighbors } from '../../commands/ratings';
import { RatingNeighbors } from '../../commands/ratingLeaderboard';
import { Region } from '../../constants/regions';
import { patientFetch } from '../blitzkrieg/patientFetch';
import { withCORSProxy } from '../blitzkrieg/withCORSProxy';
import regionToRegionSubdomain from './regionToRegionSubdomain';
export async function getRatingsNeighbors(
export async function getRatingNeighbors(
region: Region,
id: number,
count: number,
@@ -21,7 +21,7 @@ export async function getRatingsNeighbors(
)}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`,
),
);
const json = (await response.json()) as RatingsNeighbors;
const json = (await response.json()) as RatingNeighbors;
return json;
}
+6
View File
@@ -0,0 +1,6 @@
import getRatingInfo from './getRatingInfo';
export async function isOnGoingRatingSeason() {
const ratingInfo = await getRatingInfo('com');
return ratingInfo.detail === undefined;
}
-6
View File
@@ -1,6 +0,0 @@
import getRatingsInfo from './getRatingsInfo';
export async function isOnGoingRatingsSeason() {
const ratingsInfo = await getRatingsInfo('com');
return ratingsInfo.detail === undefined;
}
@@ -2,7 +2,7 @@ import { Region } from '../../constants/regions';
import { context } from '../blitzkrieg/context';
import regionToRegionSubdomain from './regionToRegionSubdomain';
type CurrentRatingsPlayerSearch = Record<
type CurrentRatingPlayerSearch = Record<
number,
{
spa_id: number;
@@ -24,20 +24,17 @@ type CurrentRatingsPlayerSearch = Record<
)
>;
export async function searchCurrentRatingsPlayers(
region: Region,
name: string,
) {
export async function searchCurrentRatingPlayers(region: Region, name: string) {
const encodedSearch = encodeURIComponent(name);
const response = await fetch(
context === 'website'
? `/api/${region}/ratings/current/search/${encodedSearch}`
? `/api/${region}/rating/current/search/${encodedSearch}`
: `https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
{ cache: 'no-store' },
);
const accountList = (await response.json()) as CurrentRatingsPlayerSearch;
const accountList = (await response.json()) as CurrentRatingPlayerSearch;
return accountList;
}
@@ -1,4 +1,4 @@
import getRatingsInfo from '../blitz/getRatingsInfo';
import getRatingInfo from '../blitz/getRatingInfo';
interface GitHubTrees {
tree: { path: string; url: string }[];
@@ -11,9 +11,9 @@ export async function getArchivedLatestSeasonNumber() {
return cachedLatestArchivedSeasonNumber;
}
const ratingsInfo = await getRatingsInfo('com');
const ratingInfo = await getRatingInfo('com');
if (ratingsInfo.detail) {
if (ratingInfo.detail) {
const regionsURL = (
(await fetch(
'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main',
@@ -24,14 +24,14 @@ export async function getArchivedLatestSeasonNumber() {
response.json(),
)) as GitHubTrees
).tree.find(({ path }) => path === 'com')!.url;
const ratingsURL = (
const ratingURL = (
(await fetch(comURL).then((response) => response.json())) as {
tree: [{ url: string }];
}
).tree[0].url;
const latestSeason = Math.max(
...(
(await fetch(ratingsURL).then((response) => response.json())) as {
(await fetch(ratingURL).then((response) => response.json())) as {
tree: { path: string }[];
}
).tree.map(({ path }) => Number(path)),
@@ -39,7 +39,7 @@ export async function getArchivedLatestSeasonNumber() {
cachedLatestArchivedSeasonNumber = latestSeason;
} else {
cachedLatestArchivedSeasonNumber = ratingsInfo.current_season - 1;
cachedLatestArchivedSeasonNumber = ratingInfo.current_season - 1;
}
return cachedLatestArchivedSeasonNumber;
@@ -1,4 +1,4 @@
import { RatingsInfo } from '../../commands/ratings';
import { RatingInfo } from '../../commands/ratingLeaderboard';
import { Region } from '../../constants/regions';
import { asset } from './asset';
import { superDecompress } from './superDecompress';
@@ -7,7 +7,7 @@ const cache: Record<
Region,
Record<
number,
RatingsInfo & {
RatingInfo & {
detail: undefined;
}
>
@@ -17,17 +17,17 @@ const cache: Record<
asia: {},
};
export default async function getArchivedRatingsInfo(
export default async function getArchivedRatingInfo(
region: Region,
season: number,
) {
if (!cache[region][season]) {
const info = await fetch(
asset(`regions/${region}/ratings/${season}/info.cdon.lz4`),
asset(`regions/${region}/rating/${season}/info.cdon.lz4`),
)
.then((response) => response.arrayBuffer())
.then((buffer) =>
superDecompress<RatingsInfo & { detail: undefined }>(
superDecompress<RatingInfo & { detail: undefined }>(
new Uint8Array(buffer),
),
);
@@ -8,13 +8,13 @@ const cache: Record<Region, Record<number, BkrlDiscriminatedEntries>> = {
com: {},
};
export async function getArchivedRatingsLeaderboard(
export async function getArchivedRatingLeaderboard(
region: Region,
season: number,
) {
if (!cache[region][season]) {
const info = await fetch(
asset(`regions/${region}/ratings/${season}/latest.bkrl`),
asset(`regions/${region}/rating/${season}/latest.bkrl`),
)
.then((response) => response.arrayBuffer())
.then((buffer) => new BkrlReadStream(buffer).bkrl());
@@ -8,13 +8,13 @@ const cache: Record<Region, Record<number, BkrlDiscriminatedEntries | null>> = {
asia: {},
};
export default async function getArchivedRatingsMidnightLeaderboard(
export default async function getArchivedRatingMidnightLeaderboard(
region: Region,
season: number,
) {
if (!cache[region][season]) {
const info = await fetch(
asset(`regions/${region}/ratings/${season}/midnight.bkrl`),
asset(`regions/${region}/rating/${season}/midnight.bkrl`),
)
.then((response) =>
response.status === 404 ? null : response.arrayBuffer(),
@@ -69,7 +69,7 @@ export default async function getStatsInPeriod(
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 rating do update last_battle_time
if (d((a) => a.battles) > 0) {
diff[tankId] = {
battles: d((a) => a.battles),
+2 -2
View File
@@ -25,7 +25,7 @@ import { ownedTanksCommand } from '../../commands/ownedTanks';
import { permissionsCommand } from '../../commands/permissions';
import { pingCommand } from '../../commands/ping';
import { playerInfoCommand } from '../../commands/playerInfo';
import { ratingsCommand } from '../../commands/ratings';
import { ratingLeaderboardCommand } from '../../commands/ratingLeaderboard';
import { researchCommand } from '../../commands/research';
import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers';
@@ -93,7 +93,7 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
pingCommand,
evolutionCommand,
statsCommand,
ratingsCommand,
ratingLeaderboardCommand,
todayCommand,
researchCommand,
testCommand,
+6 -6
View File
@@ -414,9 +414,9 @@
}
},
"ratings": {
"$": "ratings",
"description": "Ratings battles statistics",
"rating_leaderboard": {
"$": "rating-leaderboard",
"description": "Rating battles statistics",
"subcommands": {
"neighbors": {
@@ -453,10 +453,10 @@
"body": {
"top_players": "Top %s players",
"no_participation": "# %s didn't play ratings in season %s\n\nThis player did not participate in this season or did not get past calibration.",
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running ratings season.",
"no_participation": "# %s didn't play rating in season %s\n\nThis player did not participate in this season or did not get past calibration.",
"no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted %s",
"neighbors": "Ratings neighbors",
"neighbors": "Rating neighbors",
"more": "+ %s more"
}
},