mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: completed /ratings league
This commit is contained in:
1 parent
eca7cbbbeb
commit
c173ece882
9 files changed
+167
-45
No files matched your search
Vendored
+1
@@ -38,6 +38,7 @@
|
|||||||
"vickers",
|
"vickers",
|
||||||
"vitepress",
|
"vitepress",
|
||||||
"wargaming",
|
"wargaming",
|
||||||
|
"webp",
|
||||||
"winrate"
|
"winrate"
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"deno.enable": false,
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>;
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in new issue
Block a user