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