feat: added ratings neighbours

This commit is contained in:
TresAbhi committed 2023-08-09 17:13:26 +00:00
1 parent f7eadb08a9
commit 9e4094f3a9
5 files changed
+190 -76

No files matched your search

+120 -67
View File
@@ -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,
}); });
}, },
); );
+36 -8
View File
@@ -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
View File
@@ -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');
}
}
+1 -1
View File
@@ -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()}`}
/> />