feat: completed /ratings league

This commit is contained in:
TresAbhi committed 2023-07-18 00:17:00 +00:00
1 parent eca7cbbbeb
commit c173ece882
9 files changed
+167 -45

No files matched your search

+1
View File
@@ -38,6 +38,7 @@
"vickers",
"vitepress",
"wargaming",
"webp",
"winrate"
],
"deno.enable": false,
+1
View File
@@ -31,6 +31,7 @@
"esbuild": "^0.18.10",
"escape-html": "^1.0.3",
"express": "^4.18.2",
"false": "^0.0.4",
"fuzzysort": "^2.0.4",
"lodash": "^4.17.21",
"markdown-escape": "^2.0.0",
+2 -4
View File
@@ -1,5 +1,5 @@
import { BuildOptions, build } from 'esbuild';
import { copyFile, mkdir, rm } from 'fs/promises';
import { mkdir, rm } from 'fs/promises';
import { argv } from 'process';
const isProd = !argv.includes('--dev');
@@ -46,9 +46,7 @@ if (buildBot) {
entryPoints: ['src/bot.ts'],
outfile: 'dist/bot.cjs',
}).then(() => {
console.log('Bot built');
});
}).then(() => console.log('Bot built'));
}
if (buildServer) {
+3
View File
@@ -1,3 +1,4 @@
import { Octokit } from '@octokit/rest';
import { Client, GatewayIntentBits } from 'discord.js';
import { secrets } from './core/node/secrets';
import { registerErrorHandlers } from './events/error';
@@ -12,5 +13,7 @@ export const client = new Client({
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
registerErrorHandlers();
client.login(secrets.DISCORD_TOKEN);
+69 -25
View File
@@ -1,4 +1,6 @@
import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import { octokit } from '../bot';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
@@ -110,7 +112,7 @@ function optionalParameters(option: SlashCommandSubcommandBuilder) {
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 16)')
.setDescription('How many rows to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
@@ -120,6 +122,8 @@ function optionalParameters(option: SlashCommandSubcommandBuilder) {
const LEAGUE_INDEXES = ['diamond', 'platinum', 'gold', 'silver', 'bronze'];
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
export const ratingsCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
@@ -172,51 +176,91 @@ export const ratingsCommand: CommandRegistry = {
const subcommandGroup = interaction.options.getSubcommandGroup(true);
if (subcommandGroup === 'league') {
const limit = interaction.options.getInteger('limit') ?? 16;
const limit = interaction.options.getInteger('limit') ?? 10;
const subcommand = interaction.options.getSubcommand(true);
const leagueIndex = LEAGUE_INDEXES.indexOf(subcommand);
const player = await resolvePlayerFromCommand(interaction);
const regionSubdomain = regionToRegionSubdomain(player.region);
const { result } = await fetch(
const result = await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
).then((response) => response.json() as Promise<LeagueTop>);
)
.then((response) => response.json() as Promise<LeagueTop>)
.then(({ result }) => result.slice(0, limit));
const { nickname } = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
).then((accounts) => accounts[player.id]);
const awaitedInfo = await ratingsInfo;
const leagueInfo = awaitedInfo.leagues[leagueIndex];
const items = result.slice(0, limit).map((player) => {
const reward = awaitedInfo.rewards.find(
(reward) =>
player.number >= reward.from_position &&
player.number <= reward.to_position,
);
const midnightLeaderboard = await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${player.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 (
<Leaderboard.Item
nickname={player.nickname}
points={player.score}
position={player.number}
clan={player.clan_tag}
reward={reward}
deltaPoints={Math.round(Math.random() * 10 - 5)}
deltaPosition={Math.round(Math.random() * 10 - 5)}
key={player.spa_id}
/>
);
});
return jsonContent;
}
})
.catch(() => console.warn('No midnight leaderboard found'));
const items = await Promise.all(
result.map(async (player) => {
const reward = awaitedInfo.rewards.find(
(reward) =>
player.number >= reward.from_position &&
player.number <= reward.to_position,
);
const midnightIndex = midnightLeaderboard?.findIndex(
(item) => item.id === player.spa_id,
);
const midnightScore =
midnightIndex === undefined
? player.score
: midnightLeaderboard![midnightIndex].score;
const midnightPosition =
midnightIndex === undefined ? player.number : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.number}
clan={player.clan_tag}
reward={reward}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.number}
key={player.spa_id}
/>
);
}),
);
const playersBefore = result[0].number - 1;
const playersAfter = awaitedInfo.count - result[result.length - 1].number;
return (
<Wrapper>
<TitleBar
name={leagueInfo.title}
image={`https:${leagueInfo.small_icon}`}
image={
leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`
}
description={`Top ${limit} players • ${new Date().toDateString()} • ${
REGION_NAMES[player.region]
}`}
/>
<Leaderboard.Root>{items}</Leaderboard.Root>
<Leaderboard.Root>
{playersBefore > 0 && <Leaderboard.Gap number={playersBefore} />}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
</Leaderboard.Root>
</Wrapper>
);
}
@@ -0,0 +1,29 @@
import { theme } from '../../../stitches.config';
export interface GapProps {
number: number;
}
export function Gap({ number }: GapProps) {
return (
<div
style={{
display: 'flex',
padding: 8,
backgroundColor: theme.colors.componentNonInteractive,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
+ {number.toLocaleString()} more
</span>
</div>
);
}
+47 -12
View File
@@ -4,18 +4,22 @@ import { theme } from '../../../stitches.config';
export interface ItemProps {
position: number;
deltaPosition?: number;
points: number;
deltaPoints?: number;
score: number;
deltaScore?: number;
reward?: RatingsReward;
nickname: string;
clan?: string;
}
function normalizeImage(url: string) {
return url.endsWith('.webp') ? 'https://i.imgur.com/ZniCzbO.png' : url;
}
export function Item({
position,
deltaPosition,
points,
deltaPoints,
score,
deltaScore,
reward,
nickname,
clan,
@@ -38,7 +42,7 @@ export function Item({
fontSize: 16,
}}
>
{position}.
{position.toLocaleString()}.
</span>
<div
@@ -95,7 +99,7 @@ export function Item({
fontSize: 16,
}}
>
{deltaPosition}
{Math.abs(deltaPosition).toLocaleString()}
</span>
</div>
)}
@@ -111,25 +115,56 @@ export function Item({
>
{reward && (
<img
style={{ width: 16, height: 16 }}
src={
style={{ width: 16, height: 16, objectFit: 'fill' }}
src={normalizeImage(
reward.type === 'vehicle'
? reward.vehicle.image_url
: reward.stuff.image_url
}
: reward.stuff.image_url,
)}
/>
)}
{reward?.type === 'stuff' && (
{reward && reward.count > 1 && (
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
x {reward.count}
x {reward.count.toLocaleString()}
</span>
)}
</div>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
{deltaScore !== 0 && deltaScore !== undefined && (
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{Math.abs(deltaScore).toLocaleString()}
</span>
)}
{deltaScore !== 0 && deltaScore !== undefined && (
<img
style={{
width: 8,
height: 8,
}}
src={
deltaScore > 0
? 'https://i.imgur.com/qbjiXa1.png'
: 'https://i.imgur.com/3uyNhun.png'
}
/>
)}
<span style={{ color: theme.colors.textHighContrast, fontSize: 16 }}>
{score.toLocaleString()}
</span>
</div>
</div>
);
}
+10 -4
View File
@@ -1,8 +1,14 @@
const SECRET_KEYS = ['DISCORD_TOKEN', 'WARGAMING_APPLICATION_ID'] as const;
export const SECRET_KEYS = [
'DISCORD_TOKEN',
'WARGAMING_APPLICATION_ID',
'GH_TOKEN',
] as const;
type SECRET_KEY = typeof SECRET_KEYS extends readonly (infer T)[] ? T : never;
export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[]
? T
: never;
export const secrets = SECRET_KEYS.reduce<Partial<Record<SECRET_KEY, string>>>(
export const secrets = SECRET_KEYS.reduce<Partial<Record<SECRET, string>>>(
(accumulator, key) => ({
...accumulator,
@@ -12,4 +18,4 @@ export const secrets = SECRET_KEYS.reduce<Partial<Record<SECRET_KEY, string>>>(
},
}),
{},
) as Record<SECRET_KEY, string>;
) as Record<SECRET, string>;
+5
View File
@@ -1269,6 +1269,11 @@ express@^4.18.2:
utils-merge "1.0.1"
vary "~1.1.2"
"false@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/false/-/false-0.0.4.tgz#8f55de5af5c03f50bf68aed57ed80e3d2176a517"
integrity sha512-CPAG3ud9Cz8NvLKlsTLr/F1aoJL6tPyhNTzaDFzS5CX6id21hVuS5sBK8wlTLuvFklpvH1idvPbW8Lbri3G7qA==
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"