mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: added ratings neighbours
This commit is contained in:
1 parent
f7eadb08a9
commit
9e4094f3a9
5 files changed
+190
-76
No files matched your search
+120
-67
@@ -1,13 +1,23 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import {
|
||||||
|
APIApplicationCommandOptionChoice,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
|
||||||
import { octokit } from '../bot';
|
|
||||||
import * as Leaderboard from '../components/Leaderboard';
|
import * as Leaderboard from '../components/Leaderboard';
|
||||||
import TitleBar from '../components/TitleBar';
|
import TitleBar from '../components/TitleBar';
|
||||||
import Wrapper from '../components/Wrapper';
|
import Wrapper from '../components/Wrapper';
|
||||||
import { Region } from '../constants/regions';
|
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
|
||||||
|
import getWargamingResponse from '../core/blitz/getWargamingResponse';
|
||||||
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
|
||||||
|
import getMidnightLeaderboard from '../core/database/getMidnightLeaderboard';
|
||||||
import addRegionChoices from '../core/discord/addRegionChoices';
|
import addRegionChoices from '../core/discord/addRegionChoices';
|
||||||
|
import addUsernameChoices from '../core/discord/addUsernameChoices';
|
||||||
|
import autocompleteUsername from '../core/discord/autocompleteUsername';
|
||||||
|
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
|
||||||
|
import { secrets } from '../core/node/secrets';
|
||||||
import { CommandRegistryRaw } from '../events/interactionCreate';
|
import { 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;
|
||||||
@@ -102,10 +112,6 @@ const ratingsInfo = fetch(
|
|||||||
return info;
|
return info;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
|
|
||||||
|
|
||||||
const LEAGUE_SUBCOMMAND_TO_INDEX: Record<string, number> = {};
|
|
||||||
|
|
||||||
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
||||||
async (resolve) => {
|
async (resolve) => {
|
||||||
const awaitedRatingsInfo = await ratingsInfo;
|
const awaitedRatingsInfo = await ratingsInfo;
|
||||||
@@ -113,19 +119,25 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
const command = new SlashCommandBuilder()
|
const command = new SlashCommandBuilder()
|
||||||
.setName('ratings')
|
.setName('ratings')
|
||||||
.setDescription('Ratings battles statistics')
|
.setDescription('Ratings battles statistics')
|
||||||
.addSubcommandGroup((option) => {
|
.addSubcommand((option) =>
|
||||||
const subcommandGroup = option
|
option
|
||||||
.setName('league')
|
.setName('league')
|
||||||
.setDescription('Top league players');
|
.setDescription('Top league players')
|
||||||
|
.addStringOption((option) =>
|
||||||
awaitedRatingsInfo.leagues.forEach((league) => {
|
option
|
||||||
subcommandGroup.addSubcommand((option) => {
|
.setName('league')
|
||||||
const subcommandName = league.title.split(' ')[0].toLowerCase();
|
.setDescription('The league to display')
|
||||||
LEAGUE_SUBCOMMAND_TO_INDEX[subcommandName] = league.index;
|
.addChoices(
|
||||||
|
...awaitedRatingsInfo.leagues.map(
|
||||||
return option
|
(league) =>
|
||||||
.setName(subcommandName)
|
({
|
||||||
.setDescription(`Top ${league.title} players`)
|
name: league.title.split(' ')[0].toLowerCase(),
|
||||||
|
value: `${league.index}`,
|
||||||
|
}) satisfies APIApplicationCommandOptionChoice<string>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
.addStringOption(addRegionChoices)
|
.addStringOption(addRegionChoices)
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
@@ -134,12 +146,22 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
.setMinValue(5)
|
.setMinValue(5)
|
||||||
.setMaxValue(30),
|
.setMaxValue(30),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addSubcommand((option) =>
|
||||||
|
option
|
||||||
|
.setName('neighbours')
|
||||||
|
.setDescription('Your position and neighbours')
|
||||||
|
.addStringOption(addUsernameChoices)
|
||||||
|
.addIntegerOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('limit')
|
||||||
|
.setDescription('How many neighbours to display (default: 10)')
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(5)
|
||||||
|
.setMaxValue(30),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return subcommandGroup;
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
@@ -149,12 +171,22 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
command,
|
command,
|
||||||
|
|
||||||
async handler(interaction) {
|
async handler(interaction) {
|
||||||
const subcommandGroup = interaction.options.getSubcommandGroup(true);
|
|
||||||
|
|
||||||
if (subcommandGroup === 'league') {
|
|
||||||
const limit = interaction.options.getInteger('limit') ?? 10;
|
|
||||||
const subcommand = interaction.options.getSubcommand(true);
|
const subcommand = interaction.options.getSubcommand(true);
|
||||||
const leagueIndex = LEAGUE_SUBCOMMAND_TO_INDEX[subcommand];
|
const limit = interaction.options.getInteger('limit') ?? 10;
|
||||||
|
let titleName: string;
|
||||||
|
let titleImage: string | undefined;
|
||||||
|
let titleNameDiscriminator: string | undefined;
|
||||||
|
let titleDescription: string;
|
||||||
|
let playersBefore: number;
|
||||||
|
let playersAfter: number;
|
||||||
|
let players: RatingsPlayer[];
|
||||||
|
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
|
||||||
|
let playerId: number | undefined;
|
||||||
|
|
||||||
|
if (subcommand === 'league') {
|
||||||
|
const leagueIndex = parseInt(
|
||||||
|
interaction.options.getString('league')!,
|
||||||
|
);
|
||||||
const region = interaction.options.getString('region') as Region;
|
const region = interaction.options.getString('region') as Region;
|
||||||
const regionSubdomain = regionToRegionSubdomain(region);
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
const result = await fetch(
|
const result = await fetch(
|
||||||
@@ -162,27 +194,58 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
)
|
)
|
||||||
.then((response) => response.json() as Promise<LeagueTop>)
|
.then((response) => response.json() as Promise<LeagueTop>)
|
||||||
.then(({ result }) => result.slice(0, limit));
|
.then(({ result }) => result.slice(0, limit));
|
||||||
const awaitedInfo = await ratingsInfo;
|
const leagueInfo = awaitedRatingsInfo.leagues[leagueIndex];
|
||||||
const leagueInfo = awaitedInfo.leagues[leagueIndex];
|
midnightLeaderboard = await getMidnightLeaderboard(
|
||||||
const midnightLeaderboard = await octokit.repos
|
region,
|
||||||
.getContent({
|
awaitedRatingsInfo.current_season,
|
||||||
...DATABASE_REPO,
|
);
|
||||||
path: `${region}/ratings/${awaitedInfo.current_season}/midnight.json`,
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
if (!Array.isArray(data) && data.type === 'file') {
|
|
||||||
const content = Buffer.from(data.content, 'base64').toString();
|
|
||||||
const jsonContent = JSON.parse(
|
|
||||||
content,
|
|
||||||
) as BlitzkriegRatingsLeaderboard;
|
|
||||||
|
|
||||||
return jsonContent;
|
players = result;
|
||||||
|
playersBefore = result[0].number - 1;
|
||||||
|
playersAfter =
|
||||||
|
awaitedRatingsInfo.count - result[result.length - 1].number;
|
||||||
|
titleName = `${leagueInfo.title} - ${REGION_NAMES_SHORT[region]}`;
|
||||||
|
titleImage = leagueInfo.big_icon.startsWith('http')
|
||||||
|
? leagueInfo.big_icon
|
||||||
|
: `https:${leagueInfo.big_icon}`;
|
||||||
|
titleDescription = `Top ${limit} players`;
|
||||||
|
} else {
|
||||||
|
const { region, id } = await resolvePlayerFromCommand(interaction);
|
||||||
|
const accountInfo = await getWargamingResponse<AccountInfo>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
|
||||||
|
);
|
||||||
|
const clan = (
|
||||||
|
await getWargamingResponse<PlayerClanData>(
|
||||||
|
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
|
||||||
|
)
|
||||||
|
)[id]?.clan;
|
||||||
|
const regionSubdomain = regionToRegionSubdomain(region);
|
||||||
|
const { neighbors } = await fetch(
|
||||||
|
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
|
||||||
|
limit / 2,
|
||||||
|
)}`,
|
||||||
|
).then((response) => response.json() as Promise<RatingsNeighbors>);
|
||||||
|
midnightLeaderboard = await getMidnightLeaderboard(
|
||||||
|
region,
|
||||||
|
awaitedRatingsInfo.current_season,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clan) titleNameDiscriminator = `[${clan.tag}]`;
|
||||||
|
|
||||||
|
players = neighbors;
|
||||||
|
titleImage = clan
|
||||||
|
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
|
||||||
|
: undefined;
|
||||||
|
titleName = accountInfo[id].nickname;
|
||||||
|
titleDescription = 'Ratings neighbours';
|
||||||
|
playersBefore = neighbors[0].number - 1;
|
||||||
|
playersAfter =
|
||||||
|
awaitedRatingsInfo.count - neighbors[neighbors.length - 1].number;
|
||||||
|
playerId = id;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(() => console.warn('No midnight leaderboard found'));
|
const items = players.map((player) => {
|
||||||
const items = await Promise.all(
|
const reward = awaitedRatingsInfo.rewards.find(
|
||||||
result.map(async (player) => {
|
|
||||||
const reward = awaitedInfo.rewards.find(
|
|
||||||
(reward) =>
|
(reward) =>
|
||||||
player.number >= reward.from_position &&
|
player.number >= reward.from_position &&
|
||||||
player.number <= reward.to_position,
|
player.number <= reward.to_position,
|
||||||
@@ -207,40 +270,30 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
|
|||||||
deltaScore={player.score - midnightScore}
|
deltaScore={player.score - midnightScore}
|
||||||
deltaPosition={midnightPosition - player.number}
|
deltaPosition={midnightPosition - player.number}
|
||||||
key={player.spa_id}
|
key={player.spa_id}
|
||||||
|
highlight={player.spa_id === playerId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const playersBefore = result[0].number - 1;
|
|
||||||
const playersAfter =
|
|
||||||
awaitedInfo.count - result[result.length - 1].number;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={leagueInfo.title}
|
name={titleName}
|
||||||
image={
|
image={titleImage}
|
||||||
leagueInfo.big_icon.startsWith('http')
|
nameDiscriminator={titleNameDiscriminator}
|
||||||
? leagueInfo.big_icon
|
description={`${titleDescription} • ${new Date().toDateString()}`}
|
||||||
: `https:${leagueInfo.big_icon}`
|
|
||||||
}
|
|
||||||
description={`Top ${limit} players • ${new Date().toDateString()}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Leaderboard.Root>
|
<Leaderboard.Root>
|
||||||
{playersBefore > 0 && (
|
{playersBefore > 0 && <Leaderboard.Gap number={playersBefore} />}
|
||||||
<Leaderboard.Gap number={playersBefore} />
|
|
||||||
)}
|
|
||||||
{items}
|
{items}
|
||||||
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
|
||||||
</Leaderboard.Root>
|
</Leaderboard.Root>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
autocomplete: autocompleteUsername,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -9,6 +9,7 @@ export interface ItemProps {
|
|||||||
reward?: RatingsReward;
|
reward?: RatingsReward;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
clan?: string;
|
clan?: string;
|
||||||
|
highlight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeImage(url: string) {
|
function normalizeImage(url: string) {
|
||||||
@@ -23,13 +24,16 @@ export function Item({
|
|||||||
reward,
|
reward,
|
||||||
nickname,
|
nickname,
|
||||||
clan,
|
clan,
|
||||||
|
highlight,
|
||||||
}: ItemProps) {
|
}: ItemProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
backgroundColor: theme.colors.componentInteractive,
|
backgroundColor: highlight
|
||||||
|
? theme.colors.componentInteractive_blue
|
||||||
|
: theme.colors.componentInteractive,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -38,7 +42,9 @@ export function Item({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -55,7 +61,9 @@ export function Item({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textHighContrast,
|
color: highlight
|
||||||
|
? theme.colors.textHighContrast_blue
|
||||||
|
: theme.colors.textHighContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -64,7 +72,9 @@ export function Item({
|
|||||||
{clan && (
|
{clan && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -95,7 +105,9 @@ export function Item({
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -126,7 +138,9 @@ export function Item({
|
|||||||
{reward && reward.count > 1 && (
|
{reward && reward.count > 1 && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: theme.colors.textLowContrast,
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -145,7 +159,14 @@ export function Item({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{deltaScore !== 0 && deltaScore !== undefined && (
|
{deltaScore !== 0 && deltaScore !== undefined && (
|
||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textLowContrast_blue
|
||||||
|
: theme.colors.textLowContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{Math.abs(deltaScore).toLocaleString()}
|
{Math.abs(deltaScore).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -162,7 +183,14 @@ export function Item({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span style={{ color: theme.colors.textHighContrast, fontSize: 16 }}>
|
<span
|
||||||
|
style={{
|
||||||
|
color: highlight
|
||||||
|
? theme.colors.textHighContrast_blue
|
||||||
|
: theme.colors.textHighContrast,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{score.toLocaleString()}
|
{score.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ export const REGION_NAMES: Record<Region, string> = {
|
|||||||
asia: 'Asia',
|
asia: 'Asia',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// https://planetarynames.wr.usgs.gov/Abbreviations
|
||||||
|
export const REGION_NAMES_SHORT: Record<Region, string> = {
|
||||||
|
com: 'NA',
|
||||||
|
eu: 'EU',
|
||||||
|
asia: 'AS',
|
||||||
|
};
|
||||||
|
|
||||||
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
|
||||||
na: 'North America',
|
na: 'North America',
|
||||||
eu: 'Europe',
|
eu: 'Europe',
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
|
||||||
|
import { octokit } from '../../bot';
|
||||||
|
import { Region } from '../../constants/regions';
|
||||||
|
|
||||||
|
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
|
||||||
|
|
||||||
|
export default async function getMidnightLeaderboard(
|
||||||
|
region: Region,
|
||||||
|
season: number,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await octokit.repos.getContent({
|
||||||
|
...DATABASE_REPO,
|
||||||
|
path: `${region}/ratings/${season}/midnight.json`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Array.isArray(data) && data.type === 'file') {
|
||||||
|
const content = Buffer.from(data.content, 'base64').toString();
|
||||||
|
const jsonContent = JSON.parse(content) as BlitzkriegRatingsLeaderboard;
|
||||||
|
|
||||||
|
return jsonContent;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('No midnight leaderboard found');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,8 +178,8 @@ export default async function stats<Type extends StatType>(
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo[id].nickname}
|
||||||
nameDiscriminator={nameDiscriminator}
|
|
||||||
image={image}
|
image={image}
|
||||||
|
nameDiscriminator={nameDiscriminator}
|
||||||
description={`${statsName} • ${new Date().toDateString()}`}
|
description={`${statsName} • ${new Date().toDateString()}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user