Compare commits

..
21 Commits
Author SHA1 Message Date
TresAbhi f5bab8a698 faet: clan-event 2023-09-07 01:24:12 +00:00
TrèsAbhi 331040bbdf Merge pull request #26 from tresabhi/1.12.3 2023-09-03 13:21:00 -05:00
TresAbhi 70e1d4b30d feat: changelogs 2023-09-03 18:20:19 +00:00
TresAbhi 03dbd797fb feat: no ongoing season proof 2023-09-03 18:15:10 +00:00
TrèsAbhi 28172f7f8f Merge pull request #25 from tresabhi/1.12.2 2023-08-23 09:52:48 -05:00
TresAbhi cb5347cbb8 feat: changelog 2023-08-23 14:51:51 +00:00
TresAbhi 1ce0781d2d feat: use application directory 2023-08-23 00:34:37 +00:00
TresAbhi 1257aa76d7 feat: code split bot 2023-08-23 00:33:26 +00:00
TresAbhi 6ec3835896 feat: updated dependencies 2023-08-23 00:18:25 +00:00
TresAbhi 506fe7d9bd feat: removed unused dependencies 2023-08-23 00:17:25 +00:00
TresAbhi 14ef9f6295 feat: removed unused files 2023-08-23 00:17:03 +00:00
TresAbhi 0d76e1a29e feat: added knip 2023-08-23 00:16:39 +00:00
TresAbhi 9e42d7e8f5 feat: removed powered by footer 2023-08-23 00:15:45 +00:00
TresAbhi 364f97332b feat: better and unified tier selection 2023-08-23 00:14:41 +00:00
TresAbhi caa7bb8a65 fix: removed random dev console log 2023-08-22 23:49:34 +00:00
TresAbhi 3e956b34c1 fix: clan name and icon now show up with multi-tank 2023-08-22 23:49:00 +00:00
TresAbhi 038374e5aa fix: changed examples to guide in navbar 2023-08-22 23:47:55 +00:00
TresAbhi dafd54f830 feat: github link 2023-08-22 23:47:35 +00:00
TresAbhi cb65e6b0af fix: corrected order of changelog versions 2023-08-22 23:46:59 +00:00
TresAbhi 4ca8590c83 fix: guide link 2023-08-22 23:46:15 +00:00
TresAbhi c5ef02d7e6 feat: updated dependencies 2023-08-22 23:45:47 +00:00
35 changed files with 2095 additions and 574 deletions

No files matched your search

+21 -8
View File
@@ -1,4 +1,7 @@
import { DefaultTheme, defineConfig } from 'vitepress';
import { compareVersions } from 'compare-versions';
import { readdirSync } from 'fs';
import { parse } from 'path';
import { defineConfig } from 'vitepress';
import { getSidebar } from 'vitepress-plugin-auto-sidebar';
export default defineConfig({
@@ -9,17 +12,27 @@ export default defineConfig({
themeConfig: {
nav: [
{ text: 'Home', link: '/' },
{ text: 'Examples', link: '/markdown-examples' },
{ text: 'Guide', link: '/guide/about' },
],
sidebar: getSidebar({
collapsed: false,
contentDirs: ['guide', 'legal', 'changelogs'],
contentRoot: '/docs/',
}) as DefaultTheme.Sidebar,
sidebar: [
...getSidebar({
collapsed: false,
contentDirs: ['guide', 'legal'],
contentRoot: '/docs/',
}),
{
text: 'Changelogs',
items: readdirSync('./docs/changelogs')
.map((file) => parse(file).name)
.sort(compareVersions)
.map((name) => ({ text: name, link: `/changelogs/${name}.html` })),
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
],
},
});
+29
View File
@@ -0,0 +1,29 @@
# Blitzkrieg 1.12.2
A much needed room service is here!
## Changes
- Tier selection is now easier
- Tier options across all commands have been unified to a single style
- Tiers are now sorted from `10` to `1`
- Tiers now show up in the format `Tier {tier_number} - {tier_roman_numeral}` (example: `Tier 8 - VIII`)
- Removed powered by footer as links are provided regardless
- Changed invite instructions to instead use application directory
## Fixes
- Guide link on the website is now fixed
- Versions now show up in the correct order in the navigation bar
- Added Blitzkrieg's GitHub link instead of the default Vitepress
- Changed examples to guide in the navigation bar
- Clan name and icon now show up with multi-tank filters
- Removed a random `console.log`
## Technical Changes
- Added `knip`
- Removed a lot of unused files and dependencies
- Updated all dependencies to their latest
- Code split `bot.ts`
- `client` and `octokit` now reside separately
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.12.3
3-day downtime fixed!
## Changes
- Split `/ratings` into `/ratings current` and `/ratings archive`
- `current` now properly handles when there is no ongoing ratings season
- `archive` displays older archived seasons
## Fixes
- The bot is now no ratings seasons tolerant
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.12.4
Added very badly-coded temporary `/clan-event` command (without linting)
## Changes
- Added `/clan-event` command
+1 -8
View File
@@ -1,10 +1,3 @@
# Inviting the Bot
It's quite simple really. Just follow the steps below:
1. Join a Discord server that already has Blitzkrieg installed or [the official Discord Server](https://discord.gg/nDt7AjGJQH).
2. Click on the bot's profile.
3. Click the "Add to Server" button under the bot's name and ID.
4. Select what server you want it in and see what permissions you seem fit for the bot.
Voila! You're ready to go.
Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
+1 -1
View File
@@ -15,7 +15,7 @@ hero:
link: guide/install
- theme: alt
text: Guide
link: /guide
link: /guide/about
features:
- title: Clean and Efficient UI
+16 -14
View File
@@ -19,37 +19,39 @@
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
"zip": "zip -r dist.zip dist",
"lint": "npx tsc",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\""
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"knip": "knip",
"update": "yarn upgrade --latest"
},
"license": "MIT",
"dependencies": {
"@octokit/rest": "^20.0.1",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/colors": "^2.1.0",
"@resvg/resvg-js": "^2.4.1",
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.12.0",
"esbuild": "^0.18.10",
"escape-html": "^1.0.3",
"discord.js": "^14.13.0",
"esbuild": "^0.19.2",
"express": "^4.18.2",
"false": "^0.0.4",
"fuzzysort": "^2.0.4",
"lodash": "^4.17.21",
"markdown-escape": "^2.0.0",
"react": "^18.2.0",
"satori": "^0.9.1",
"vite-node": "^0.33.0",
"satori": "^0.10.3",
"vite-node": "^0.34.2",
"vitepress-plugin-auto-sidebar": "^1.1.0"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.4.2",
"@total-typescript/ts-reset": "^0.5.1",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17",
"@types/lodash": "^4.14.195",
"@types/lodash": "^4.14.197",
"@types/markdown-escape": "^1.1.0",
"@types/node": "^20.2.1",
"@types/react": "^18.2.7",
"prettier": "^3.0.0",
"@types/node": "^20.5.3",
"@types/react": "^18.2.21",
"compare-versions": "^6.1.0",
"knip": "^2.19.9",
"prettier": "^3.0.2",
"typescript": "^5.0.4",
"vitepress": "^1.0.0-beta.3"
"vitepress": "^1.0.0-rc.4"
}
}
+1 -14
View File
@@ -1,19 +1,6 @@
import { Octokit } from '@octokit/rest';
import { Client, GatewayIntentBits } from 'discord.js';
import { client } from './core/discord/client';
import { secrets } from './core/node/secrets';
import { registerErrorHandlers } from './events/error';
import guildMemberAdd from './events/guildMemberAdd';
import interactionCreate from './events/interactionCreate';
import ready from './events/ready';
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
registerErrorHandlers();
client.login(secrets.DISCORD_TOKEN);
+107
View File
@@ -0,0 +1,107 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import autocompleteClan from '../core/discord/autocompleteClan';
import embedInfo from '../core/discord/embedInfo';
import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_THRESHOLD = 7;
export const clanEventCommand: CommandRegistry = {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('clan-event')
.setDescription('Lists all inactive players')
.addStringOption((option) =>
option
.setName('clan')
.setDescription('The clan name or tag you are checking')
.setRequired(true),
),
async handler(interaction) {
const REGION = 'com';
const CLAN_SEARCH = interaction.options.getString('clan', true);
const {
clan_id,
name: clan_name,
tag: clan_tag,
} = await fetch(
`https://api.wotblitz.${REGION}/wotb/clans/list/?application_id=${secrets.WARGAMING_APPLICATION_ID}&search=${CLAN_SEARCH}`,
)
.then((response) => response.json())
.then(({ data }) => data[0]);
const memebers = await fetch(
`https://api.wotblitz.${REGION}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${clan_id}`,
)
.then((response) => response.json())
.then(({ data }) => data[clan_id].members_ids);
const stats = await Promise.all(
memebers.map(async (id, index) => {
await new Promise((resolve) => setTimeout(resolve, index * 200));
const [yesterdayPlatoonWins, name, todayPlatoonWins] =
await Promise.all([
fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
.then((response) => response.json())
.then((data) => data[0]?.achievements.jointVictoryCount),
fetch(
`https://api.wotblitz.${REGION}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
)
.then((response) => response.json())
.then(({ data }) => data[id].nickname),
fetch(
`https://api.wotblitz.${REGION}/wotb/account/achievements/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
)
.then((response) => response.json())
.then(({ data }) => data[id].max_series.jointVictory),
]);
return {
id,
name,
yesterdayPlatoonWins,
todayPlatoonWins,
};
}),
);
return embedInfo(
`Estimation of ${clan_name} [${clan_tag}]'s total platoon wins today: ${(
stats.reduce(
(accumulator, player) =>
accumulator +
(player.yesterdayPlatoonWins === undefined
? 0
: player.todayPlatoonWins - player.yesterdayPlatoonWins),
0,
) / 2
).toFixed(0)}`,
stats
.sort(
(playerA, playerB) =>
(playerB.yesterdayPlatoonWins === undefined
? 0
: playerB.todayPlatoonWins - playerB.yesterdayPlatoonWins) -
(playerA.yesterdayPlatoonWins === undefined
? 0
: playerA.todayPlatoonWins - playerA.yesterdayPlatoonWins),
)
.map(
(player) =>
`${markdownEscape(player.name)}: ${
player.yesterdayPlatoonWins === undefined
? 0
: player.todayPlatoonWins - player.yesterdayPlatoonWins
}`,
)
.join('\n'),
);
},
autocomplete: autocompleteClan,
};
+1 -1
View File
@@ -1,8 +1,8 @@
import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' };
import { client } from '../bot';
import { tankopedia } from '../core/blitz/tankopedia';
import { tankAverages } from '../core/blitzstars/tankAverages';
import { client } from '../core/discord/client';
import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable';
import getClientId from '../core/node/getClientId';
+3 -1
View File
@@ -62,6 +62,8 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
)
)[player.id];
const tierOption = interaction.options.getString('tier');
return [
await fullStats(
commandGroup,
@@ -74,7 +76,7 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
'tank-type':
interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined,
tier: interaction.options.getInteger('tier') ?? undefined,
tier: tierOption ? parseInt(tierOption) : undefined,
'tree-type': (interaction.options.getString('tree-type') ??
undefined) as TreeTypeString | undefined,
} satisfies Partial<MultiTankFilters>)
-3
View File
@@ -1,7 +1,6 @@
import { SlashCommandBuilder } from 'discord.js';
import GenericStats from '../components/GenericStats';
import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import getBlitzClan from '../core/blitz/getBlitzClan';
@@ -74,8 +73,6 @@ export const inactiveCommand: CommandRegistry = {
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
{hasInactiveMembers && <GenericStats stats={inactive} />}
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper>
);
},
+2 -21
View File
@@ -1,6 +1,5 @@
import { SlashCommandBuilder } from 'discord.js';
import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import * as Tanks from '../components/Tanks';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
@@ -15,6 +14,7 @@ import {
tankopedia,
tankopediaInfo,
} from '../core/blitz/tankopedia';
import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
@@ -52,24 +52,7 @@ export const ownedTanksCommand: CommandRegistry = {
command: new SlashCommandBuilder()
.setName('owned-tanks')
.setDescription("Shows a player's owned tanks")
.addStringOption((option) =>
option
.setName('tier')
.setDescription('The tier you want to see')
.setChoices(
{ name: 'Tier I (1)', value: '1' },
{ name: 'Tier II (2)', value: '2' },
{ name: 'Tier III (3)', value: '3' },
{ name: 'Tier IV (4)', value: '4' },
{ name: 'Tier V (5)', value: '5' },
{ name: 'Tier VI (6)', value: '6' },
{ name: 'Tier VII (7)', value: '7' },
{ name: 'Tier VIII (8)', value: '8' },
{ name: 'Tier IX (9)', value: '9' },
{ name: 'Tier X (10)', value: '10' },
)
.setRequired(true),
)
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
@@ -170,8 +153,6 @@ export const ownedTanksCommand: CommandRegistry = {
);
}),
))}
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper>
);
},
+358 -112
View File
@@ -1,21 +1,28 @@
import {
APIApplicationCommandOptionChoice,
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import { REGION_NAMES_SHORT, Region } from '../constants/regions';
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
import getMidnightLeaderboard from '../core/database/getMidnightLeaderboard';
import getMidnightLeaderboard, {
DATABASE_REPO,
} from '../core/database/getMidnightLeaderboard';
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 { octokit } from '../core/express/octokit';
import { secrets } from '../core/node/secrets';
import throwError from '../core/node/throwError';
import { CommandRegistryRaw } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData';
@@ -86,78 +93,127 @@ export interface RatingsLeague {
percentile: number;
}
export interface RatingsInfo {
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
export type RatingsInfo =
| {
detail: undefined;
rewards: RatingsReward[];
leagues: RatingsLeague[];
}
title: string;
icon: string | null;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingsReward[];
leagues: RatingsLeague[];
}
| { detail: { error: string } };
export interface RatingsNeighbors {
neighbors: RatingsPlayer[];
}
console.log('Caching ratings info...');
const ratingsInfo = getRatingsInfo('com').then((info) => {
console.log('Cached ratings info');
return info;
});
const LEAGUES = [
{ name: 'Diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 },
];
function addSubcommands(
option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option,
) {
return option
.addSubcommand((option) =>
addOptions(
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),
),
)
.addSubcommand((option) =>
addOptions(option.setName('league').setDescription('Top league players'))
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
value: `${value}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
);
}
export const ratingsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const awaitedRatingsInfo = await ratingsInfo;
const seasonChoices = await octokit.repos
.getContent({
...DATABASE_REPO,
path: 'com/ratings',
})
.then(({ data }) => {
if (!Array.isArray(data)) {
throw throwError('Archived ratings data is malformed');
}
return data.map(
(folder) =>
({
name: `Season ${folder.name}`,
value: folder.name,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
});
const command = new SlashCommandBuilder()
.setName('ratings')
.setDescription('Ratings battles statistics')
.addSubcommand((option) =>
option
.setName('league')
.setDescription('Top league players')
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...awaitedRatingsInfo.leagues.map(
(league) =>
({
name: league.title.split(' ')[0].toLowerCase(),
value: `${league.index}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
.addSubcommandGroup((option) =>
addSubcommands(
option.setName('archive').setDescription('Any previous season'),
(option) =>
option.addStringOption((option) =>
option
.setName('season')
.setDescription('The season number')
.setChoices(...seasonChoices)
.setRequired(true),
),
),
)
.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),
),
.addSubcommandGroup((option) =>
addSubcommands(
option
.setName('current')
.setDescription('The current running Ratings season'),
),
);
resolve({
@@ -168,15 +224,30 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
command,
async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true);
interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
| 'archive'
| 'current';
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbours';
const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined;
let titleName: string;
let titleImage: string | undefined;
let titleNameDiscriminator: string | undefined;
let titleDescription: string;
let playersBefore: number;
let playersAfter: number;
let players: RatingsPlayer[];
let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
let playerId: number | undefined;
let regionRatingsInfo: RatingsInfo;
@@ -186,31 +257,120 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
interaction.options.getString('league')!,
);
const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await getRatingsInfo(region);
regionRatingsInfo = await (subcommandGroup === 'current'
? getRatingsInfo(region)
: getArchivedRatingsInfo(season!, region));
const regionSubdomain = regionToRegionSubdomain(region);
const result = await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
)
.then((response) => response.json() as Promise<LeagueTop>)
.then(({ result }) => result.slice(0, limit));
const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
);
const result =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
)
.then((response) => response.json() as Promise<LeagueTop>)
.then(async ({ result }) =>
result.slice(0, limit).map(
(player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings data is malformed');
}
const players = (await fetch(data.download_url!).then(
(response) => response.json(),
)) as BlitzkriegRatingsLeaderboard;
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}) satisfies SimplifiedPlayer,
);
});
const leagueInfo =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.leagues[leagueIndex]
: undefined;
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
players = result;
playersBefore = result[0].number - 1;
playersBefore = result ? result[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.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}`;
regionRatingsInfo.detail === undefined && result !== undefined
? regionRatingsInfo.count - result[result.length - 1].position
: 0;
titleName = `${
leagueInfo ? leagueInfo.title : 'No Ongoing Season'
} - ${REGION_NAMES_SHORT[region]}`;
titleImage = leagueInfo
? leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`
: undefined;
titleDescription = `Top ${limit} players`;
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo = await getRatingsInfo(region);
regionRatingsInfo =
subcommandGroup === 'current'
? await getRatingsInfo(region)
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings info is malformed');
}
const response = fetch(data.download_url!);
return (await response.then((response) =>
response.json(),
)) as RatingsInfo;
});
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
@@ -220,57 +380,139 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
)
)[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,
regionRatingsInfo.current_season,
);
const neighbors =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await fetch(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
limit / 2,
)}`,
)
.then(
(response) =>
response.json() as Promise<RatingsNeighbors>,
)
.then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError(
'Archived ratings latest data is malformed',
);
}
if (clan) titleNameDiscriminator = `[${clan.tag}]`;
const response = fetch(data.download_url!);
const players = (await response.then((response) =>
response.json(),
)) as BlitzkriegRatingsLeaderboard;
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw throwError(
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
'This player did not participate in this season or did not get past calibration.',
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}));
});
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
titleImage = neighbors
? clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined
: undefined;
titleName = accountInfo[id].nickname;
titleName = neighbors
? accountInfo[id].nickname
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbours';
playersBefore = neighbors[0].number - 1;
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].number;
regionRatingsInfo.detail === undefined
? neighbors
? regionRatingsInfo.count -
neighbors[neighbors.length - 1].position
: 0
: 0;
playerId = id;
}
const items = players.map((player) => {
const reward = regionRatingsInfo.rewards.find(
(reward) =>
player.number >= reward.from_position &&
player.number <= reward.to_position,
);
const items = players?.map((player) => {
const reward =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.rewards.find(
(reward) =>
player.position >= reward.from_position &&
player.position <= reward.to_position,
)
: undefined;
const midnightIndex = midnightLeaderboard?.findIndex(
(item) => item.id === player.spa_id,
(item) => item.id === player.id,
);
const midnightScore =
midnightIndex === undefined
? player.score
: midnightLeaderboard![midnightIndex].score;
const midnightPosition =
midnightIndex === undefined ? player.number : midnightIndex + 1;
midnightIndex === undefined ? player.position : midnightIndex + 1;
return (
<Leaderboard.Item
nickname={player.nickname}
score={player.score}
position={player.number}
clan={player.clan_tag}
position={player.position}
clan={player.clan}
reward={reward}
deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.number}
key={player.spa_id}
highlight={player.spa_id === playerId}
deltaPosition={midnightPosition - player.position}
key={player.id}
highlight={player.id === playerId}
/>
);
});
@@ -284,11 +526,15 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
description={`${titleDescription} • ${new Date().toDateString()}`}
/>
<Leaderboard.Root>
{playersBefore > 0 && <Leaderboard.Gap number={playersBefore} />}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
</Leaderboard.Root>
{items && (
<Leaderboard.Root>
{playersBefore > 0 && (
<Leaderboard.Gap number={playersBefore} />
)}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
</Leaderboard.Root>
)}
</Wrapper>
);
},
+3 -1
View File
@@ -60,6 +60,8 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
)
)[player.id];
const tierOption = interaction.options.getString('tier');
return [
await stats(
commandGroup,
@@ -72,7 +74,7 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
'tank-type':
interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined,
tier: interaction.options.getInteger('tier') ?? undefined,
tier: tierOption ? parseInt(tierOption) : undefined,
'tree-type': (interaction.options.getString('tree-type') ??
undefined) as TreeTypeString | undefined,
} satisfies Partial<MultiTankFilters>)
-43
View File
@@ -1,43 +0,0 @@
import { ReactNode } from 'react';
import { theme } from '../stitches.config';
export enum PoweredByType {
BlitzStars,
Wargaming,
}
export interface PoweredByProps {
type: PoweredByType;
footer?: ReactNode;
}
const POWERED_BY_IMAGES: Record<PoweredByType, string> = {
[PoweredByType.BlitzStars]:
'https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png',
[PoweredByType.Wargaming]: 'https://i.imgur.com/jADGWm0.png',
};
const POWERED_BY_NAMES: Record<PoweredByType, string> = {
[PoweredByType.BlitzStars]: 'BlitzStars',
[PoweredByType.Wargaming]: 'Wargaming',
};
export default function PoweredBy({ type, footer }: PoweredByProps) {
return (
<div
style={{
display: 'flex',
justifyContent: footer === undefined ? 'space-around' : 'space-between',
}}
>
{footer !== undefined && <div style={{ display: 'flex' }}>{footer}</div>}
<div style={{ display: 'flex', gap: 4 }}>
<img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} />
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
Powered by {POWERED_BY_NAMES[type]}
</span>
</div>
</div>
);
}
-30
View File
@@ -1,30 +0,0 @@
import { AllStats } from '../../types/accountInfo';
export default function diffStats(stats1: AllStats, stats2: AllStats) {
function diff(value: (allStats: AllStats) => number) {
return value(stats2) - value(stats1);
}
function max(value: (allStats: AllStats) => number) {
return Math.max(value(stats1), value(stats2));
}
return {
battles: diff((a) => a.battles),
wins: diff((a) => a.wins),
capture_points: diff((a) => a.capture_points),
damage_dealt: diff((a) => a.damage_dealt),
damage_received: diff((a) => a.damage_received),
dropped_capture_points: diff((a) => a.dropped_capture_points),
frags: diff((a) => a.frags),
frags8p: diff((a) => a.frags8p),
hits: diff((a) => a.hits),
losses: diff((a) => a.losses),
max_frags: max((a) => a.max_frags),
max_xp: max((a) => a.max_xp),
shots: diff((a) => a.shots),
spotted: diff((a) => a.spotted),
survived_battles: diff((a) => a.survived_battles),
win_and_survived: diff((a) => a.win_and_survived),
xp: diff((a) => a.xp),
} satisfies AllStats;
}
+25
View File
@@ -0,0 +1,25 @@
import { RatingsInfo } from '../../commands/ratings';
import { Region } from '../../constants/regions';
import { DATABASE_REPO } from '../database/getMidnightLeaderboard';
import { octokit } from '../express/octokit';
import throwError from '../node/throwError';
export default async function getArchivedRatingsInfo(
season: number,
region: Region,
) {
const { data } = await octokit.repos.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
});
if (Array.isArray(data) || data.type !== 'file')
throw throwError('Archived ratings info is malformed');
const content = Buffer.from(data.content, 'base64').toString();
const jsonContent = JSON.parse(content) as RatingsInfo & {
detail: undefined;
};
return jsonContent;
}
+1
View File
@@ -8,5 +8,6 @@ export default async function getRatingsInfo(region: Region) {
region,
)}.wotblitz.com/en/api/rating-leaderboards/season/`,
);
return (await response.json()) as Promise<RatingsInfo>;
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Thanks, August!
* https://stackoverflow.com/a/41358305/12294756
*/
const ROMAN = {
M: 1000,
CM: 900,
D: 500,
CD: 400,
C: 100,
XC: 90,
L: 50,
XL: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1,
};
const ROMAN_KEYS = Object.keys(ROMAN) as (keyof typeof ROMAN)[];
export default function numberToRomanNumeral(value: number) {
let string = '';
ROMAN_KEYS.forEach((index) => {
const part = Math.floor(value / ROMAN[index]);
value -= part * ROMAN[index];
string += index.repeat(part);
});
return string;
}
-12
View File
@@ -1,12 +0,0 @@
import sumStats from '../blitz/sumStats';
import { emptyAllStats } from './getDiffedTankStats';
import { tankAverages } from './tankAverages';
console.log('Calculating player average...');
export const playerAverage = Object.entries(await tankAverages).reduce(
(accumulator, [, tankAverage]) => {
return sumStats([accumulator, tankAverage.all]);
},
emptyAllStats,
);
console.log('Calculated player average');
+6 -5
View File
@@ -1,6 +1,6 @@
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
import { octokit } from '../../bot';
import { Region } from '../../constants/regions';
import { octokit } from '../express/octokit';
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
@@ -15,12 +15,13 @@ export default async function getMidnightLeaderboard(
});
if (!Array.isArray(data) && data.type === 'file') {
const content = Buffer.from(data.content, 'base64').toString();
const jsonContent = JSON.parse(content) as BlitzkriegRatingsLeaderboard;
const response = await fetch(data.download_url!);
return jsonContent;
return (await response.json()) as BlitzkriegRatingsLeaderboard;
}
} catch (error) {
console.warn('No midnight leaderboard found');
console.warn(
`No midnight leaderboard found for season ${season}, region ${region}`,
);
}
}
@@ -4,6 +4,7 @@ import { StatType } from '../../renderers/stats';
import { tankopediaInfo } from '../blitz/tankopedia';
import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices';
import addUsernameChoices from './addUsernameChoices';
export default async function addStatTypeSubCommandGroups(
@@ -61,13 +62,8 @@ export default async function addStatTypeSubCommandGroups(
)
.setRequired(false),
)
.addIntegerOption((option) =>
option
.setName('tier')
.setDescription('Tier')
.setMinValue(1)
.setMaxValue(10)
.setRequired(false),
.addStringOption((option) =>
addTierChoices(option).setRequired(false),
)
.addStringOption((option) =>
option
+16
View File
@@ -0,0 +1,16 @@
import { SlashCommandStringOption } from 'discord.js';
import { range } from 'lodash';
import numberToRomanNumeral from '../blitz/numberToRomanNumeral';
export default function addTierChoices(option: SlashCommandStringOption) {
return option
.setName('tier')
.setDescription('The tier you want to see')
.setChoices(
...range(10, 0).map((tier) => ({
name: `Tier ${tier} - ${numberToRomanNumeral(tier)}`,
value: `${tier}`,
})),
)
.setRequired(true);
}
+11
View File
@@ -0,0 +1,11 @@
import { Client, GatewayIntentBits } from 'discord.js';
import guildMemberAdd from '../../events/guildMemberAdd';
import interactionCreate from '../../events/interactionCreate';
import ready from '../../events/ready';
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
-7
View File
@@ -1,7 +0,0 @@
import { ButtonBuilder, ButtonStyle } from 'discord.js';
// TODO: ADD THIS AGAIN IMMEDIATELY
export const supportBlitzStars = new ButtonBuilder()
.setLabel('Support BlitzStars')
.setURL('https://www.blitzstars.com/supporters')
.setStyle(ButtonStyle.Link);
+4
View File
@@ -0,0 +1,4 @@
import { Octokit } from '@octokit/rest';
import { secrets } from '../node/secrets';
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
+1 -1
View File
@@ -1,7 +1,7 @@
import { EmbedBuilder, TextChannel } from 'discord.js';
import discord from '../../discord.json' assert { type: 'json' };
import { client } from '../bot';
import { NEGATIVE_COLOR } from '../constants/colors';
import { client } from '../core/discord/client';
import isDev from '../core/node/isDev';
const PROCESS_ERROR_EVENTS = ['uncaughtException'];
+2
View File
@@ -13,6 +13,7 @@ import {
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
import discord from '../../../discord.json' assert { type: 'json' };
import { clanEventCommand } from '../../commands/clanEvent';
import { debugCommand } from '../../commands/debug';
import { eligibleCommand } from '../../commands/eligible';
import { evolutionCommand } from '../../commands/evolution';
@@ -99,6 +100,7 @@ export const COMMANDS_RAW: CommandRegistry[] = [
evolutionCommand,
statsCommand,
ratingsCommand,
clanEventCommand,
];
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
-3
View File
@@ -1,6 +1,5 @@
import * as Graph from '../components/Graph';
import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
@@ -118,8 +117,6 @@ export default async function evolution<Type extends StatType>(
)}
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
<PoweredBy type={PoweredByType.BlitzStars} />
</Wrapper>
);
}
+1 -5
View File
@@ -1,6 +1,5 @@
import GenericAllStats from '../components/GenericAllStats';
import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TierWeights, { TierWeightsRecord } from '../components/TierWeights';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
@@ -35,7 +34,7 @@ export default async function fullStats<Type extends StatType>(
let nameDiscriminator: string | undefined;
let image: string | undefined;
if (type === 'player') {
if (type === 'player' || type === 'multi-tank') {
const clan = (
await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
@@ -49,7 +48,6 @@ export default async function fullStats<Type extends StatType>(
} else if (type === 'tank') {
nameDiscriminator = `(${await resolveTankName(filters as number)})`;
image = (await tankopedia)[filters as number]?.images.normal;
} else {
}
const { diffed, order } = await getDiffedTankStats(server, id, start, end);
@@ -165,8 +163,6 @@ export default async function fullStats<Type extends StatType>(
supplementaryStats={supplementaryStats}
/>
)}
<PoweredBy type={PoweredByType.BlitzStars} />
</Wrapper>
);
}
+1 -4
View File
@@ -1,6 +1,5 @@
import AllStatsOverview from '../components/AllStatsOverview';
import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import { TreeTypeString } from '../components/Tanks';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
@@ -45,7 +44,7 @@ export default async function stats<Type extends StatType>(
let nameDiscriminator: string | undefined;
let image: string | undefined;
if (type === 'player') {
if (type === 'player' || type === 'multi-tank') {
const clan = (
await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
@@ -168,8 +167,6 @@ export default async function stats<Type extends StatType>(
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
{stats?.battles > 0 && overview}
<PoweredBy type={PoweredByType.BlitzStars} footer={footer} />
</Wrapper>
);
}
-5
View File
@@ -1,6 +1,5 @@
import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper';
import calculateWN8 from '../core/blitz/calculateWN8';
@@ -27,8 +26,6 @@ export default async function today(
showTotal = true,
naked?: boolean,
) {
console.log(maximized);
const { diffed, order } = await getDiffedTankStats(
server,
id,
@@ -174,8 +171,6 @@ export default async function today(
{rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{rows.length > 0 && <Breakdown.Root>{rows}</Breakdown.Root>}
<PoweredBy type={PoweredByType.BlitzStars} />
</Wrapper>
);
}
+1 -1
View File
@@ -11,5 +11,5 @@
"resolveJsonModule": true,
"jsx": "react-jsx"
},
"include": ["src/**/*.ts", "src/components/Tanks/components/Row.tsx"]
"include": ["src/**/*.ts", "src/components/Tanks/components/Row.tsx", "src/commands/clanEvent.ts"]
}
+1428 -267
View File
File diff suppressed because it is too large. Load diff