Merge pull request #13 from tresabhi/1.9.0

This commit is contained in:
TrèsAbhi authored and GitHub committed 2023-06-30 12:50:45 -05:00
commit fc4390d22f
119 files changed
+1931 -747

No files matched your search

+1 -1
View File
@@ -35,4 +35,4 @@ jobs:
unzip dist.zip
- name: Run index.cjs
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings index.cjs
run: NODE_ENV=production DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs
+3
View File
@@ -13,13 +13,16 @@
"playerachievements",
"playerinfo",
"Resvg",
"roboto",
"searchclans",
"searchplayers",
"searchtanks",
"SKLLD",
"Smri",
"startcase",
"statsfull",
"strvk",
"tankaverages",
"tankopedia",
"tankstats",
"Unicum",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"main": "server.js",
"main": "server.cjs",
"scripts": {
"start": "node --enable-source-maps server.cjs"
"start": "node --enable-source-maps --trace-warnings server.cjs"
}
}
+10 -5
View File
@@ -1,15 +1,18 @@
{
"name": "blitzkrieg",
"version": "1.7.7",
"version": "1.9.0",
"description": "",
"main": "src/index.ts",
"type": "module",
"scripts": {
"dev": "yarn build:raw --dev && yarn start --dev",
"start": "node --enable-source-maps --trace-warnings dist/index.cjs",
"build:bot": "node scripts/build --build=bot",
"build:server": "node scripts/build --build=server",
"build": "yarn lint && node scripts/build --build=all",
"dev:bot": "yarn build:bot --dev && yarn start:bot",
"dev:server": "yarn build:server --dev && yarn start:server",
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs",
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs",
"zip": "zip -r dist.zip dist",
"build:raw": "node scripts/build",
"build": "yarn lint && yarn build:raw",
"lint": "tsc"
},
"license": "MIT",
@@ -19,6 +22,7 @@
"bepaint": "^0.1.0-beta.3",
"discord.js": "^14.9.0",
"escape-html": "^1.0.3",
"express": "^4.18.2",
"fuzzysort": "^2.0.4",
"lodash": "^4.17.21",
"markdown-escape": "^2.0.0",
@@ -28,6 +32,7 @@
"devDependencies": {
"@total-typescript/ts-reset": "^0.4.2",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17",
"@types/lodash": "^4.14.195",
"@types/markdown-escape": "^1.1.0",
"@types/node": "^20.2.1",
+68 -10
View File
@@ -1,18 +1,52 @@
import { build } from 'esbuild';
import { rmSync } from 'fs';
import { copyFile, mkdir, rm, writeFile } from 'fs/promises';
import { argv } from 'process';
const isDev = argv.includes('--dev');
const isProd = !isDev;
const isProd = !argv.includes('--dev');
const buildAll = argv.includes('--build=all');
const buildBot = argv.includes('--build=bot') | buildAll;
const buildServer = argv.includes('--build=server') | buildAll;
console.log('Removing dist...');
rmSync('dist', { recursive: true, force: true });
if (isProd) {
// only remove in production to ensure no time is wasted in dev
console.log('🟡 Removing dist...');
await rm('dist', { recursive: true, force: true });
console.log('✅ Dist removed');
console.log('Building...');
build({
entryPoints: ['src/index.ts'],
outfile: 'dist/index.cjs',
console.log('🟡 Creating dist...');
await mkdir('dist');
console.log('✅ Dist created');
console.log('🟡 Caching tankopedia...');
fetch('https://www.blitzstars.com/bs-tankopedia.json')
.then((response) => response.text())
.then((tankopedia) => writeFile('./dist/tankopedia.json', tankopedia))
.then(() => console.log('✅ Tankopedia cached'));
console.log('🟡 Caching tank averages...');
fetch('https://www.blitzstars.com/api/tankaverages.json')
.then((response) => response.json())
.then((individualTankAverages) => {
const tankAverages = {};
individualTankAverages.forEach((individualTankAverage) => {
tankAverages[individualTankAverage.tank_id] = individualTankAverage;
});
return writeFile(
'./dist/tankaverages.json',
JSON.stringify(tankAverages),
);
})
.then(() => console.log('✅ Tank averages cached'));
console.log('🟡 Copying package.json...');
copyFile('package.dist.json', 'dist/package.json').then(() =>
console.log('✅ package.json copied'),
);
}
const commonOptions = {
platform: 'node',
loader: {
'.node': 'copy',
@@ -26,4 +60,28 @@ build({
minifyWhitespace: isProd,
logLevel: isProd ? 'info' : 'silent',
});
};
if (buildBot) {
console.log('🟡 Building bot...');
build({
...commonOptions,
entryPoints: ['src/bot.ts'],
outfile: 'dist/bot.cjs',
}).then(() => {
console.log('✅ Bot built');
});
}
if (buildServer) {
console.log('🟡 Building server...');
build({
...commonOptions,
entryPoints: ['src/server.ts'],
outfile: 'dist/server.cjs',
}).then(() => {
console.log('✅ Server built');
});
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { Client, GatewayIntentBits } from 'discord.js';
import { DISCORD_TOKEN } from './core/process/args.js';
import { DISCORD_TOKEN } from './core/node/arguments.js';
import { registerErrorHandlers } from './events/error.js';
import guildMemberAdd from './events/guildMemberAdd.js';
import interactionCreate from './events/interactionCreate/index.js';
+7 -7
View File
@@ -1,12 +1,12 @@
import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' };
import { client } from '../bot.js';
import { tankopedia } from '../core/blitz/tankopedia.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import cleanTable from '../core/interaction/cleanTable.js';
import infoEmbed from '../core/interaction/infoEmbed.js';
import getClientId from '../core/process/getClientId.js';
import embedInfo from '../core/discord/embedInfo.js';
import markdownTable from '../core/discord/markdownTable.js';
import getClientId from '../core/node/getClientId.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { client } from '../index.js';
const executionStart = new Date().getTime();
@@ -19,13 +19,13 @@ export default {
.setName('debug')
.setDescription('Debug information about the bot'),
execute(interaction) {
handler(interaction) {
const currentTime = new Date().getTime();
const uptime = currentTime - executionStart;
return infoEmbed(
return embedInfo(
`${client.user?.username} debug information`,
cleanTable([
markdownTable([
['Version', packageJSON.version],
['Client ID', getClientId()],
['Tag', interaction.client.user.tag],
+16 -16
View File
@@ -1,18 +1,18 @@
import { SlashCommandBuilder } from 'discord.js';
import getWN8 from '../core/blitz/getWN8.js';
import calculateWN8 from '../core/blitz/calculateWN8.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import sumStats from '../core/blitz/sumStats.js';
import { tankopedia } from '../core/blitz/tankopedia.js';
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
import getPeriodStart from '../core/blitzstars/getPeriodStart.js';
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
import getTankStatsDiffed from '../core/blitzstars/getTankStatsDiffed.js';
import { tankAverages } from '../core/blitzstars/tankAverages.js';
import cleanTable, { TableInput } from '../core/interaction/cleanTable.js';
import negativeEmbed from '../core/interaction/negativeEmbed.js';
import positiveEmbed from '../core/interaction/positiveEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import embedNegative from '../core/discord/embedNegative.js';
import embedPositive from '../core/discord/embedPositive.js';
import markdownTable, { TableInput } from '../core/discord/markdownTable.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
@@ -44,11 +44,11 @@ export default {
)
.setRequired(true),
)
.addStringOption(addUsernameOption),
.addStringOption(addUsernameChoices),
async execute(interaction) {
async handler(interaction) {
const clan = interaction.options.getString('clan') as SkilledClan;
const { id, server } = await resolvePlayer(interaction);
const { id, server } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
@@ -58,7 +58,7 @@ export default {
let body = '';
if (clan === 'SKLLD') {
const tankStatsOverTime = await getTankStatsOverTime(
const tankStatsOverTime = await getTankStatsDiffed(
server,
id,
getPeriodStart('30'),
@@ -79,7 +79,7 @@ export default {
// edge case where new tanks don't have averages
if (tankAverages[tankId]) {
const tankWN8 = getWN8(tankAverages[tankId].all, stats);
const tankWN8 = calculateWN8(tankAverages[tankId].all, stats);
if (isNaN(tankWN8)) return accumulator;
return accumulator + tankWN8 * stats.battles;
@@ -137,7 +137,7 @@ export default {
isEligible = problems.length === 0;
body = isEligible
? 'Player is fully eligible.'
: `Player is not eligible. Shortcomings in 30-day statistics:\n\n${cleanTable(
: `Player is not eligible. Shortcomings in 30-day statistics:\n\n${markdownTable(
problems,
)}`;
} else if (clan === 'SMRI') {
@@ -162,11 +162,11 @@ export default {
isEligible = problems.length === 0;
body = isEligible
? 'Player is fully eligible.'
: `Player is not eligible. Shortcomings in career statistics:\n\n${cleanTable(
: `Player is not eligible. Shortcomings in career statistics:\n\n${markdownTable(
problems,
)}`;
}
return (isEligible ? positiveEmbed : negativeEmbed)(title, body);
return (isEligible ? embedPositive : embedNegative)(title, body);
},
} satisfies CommandRegistry;
+61 -25
View File
@@ -1,50 +1,86 @@
import { SlashCommandBuilder } from 'discord.js';
import tanksAutocomplete from '../core/autocomplete/tanks.js';
import usernameAutocomplete from '../core/autocomplete/username.js';
import { CYCLIC_API } from '../constants/cyclic.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js';
import resolvePeriod from '../core/options/resolvePeriod.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import evolution from '../renderers/evolution.js';
import { StatType } from '../renderers/stats.js';
import { StatType } from '../renderers/statsfull.js';
import { AccountInfo } from '../types/accountInfo.js';
export default {
inProduction: true,
inDevelopment: true,
inPublic: true,
command: addStatsSubCommandGroups(
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics'),
),
async execute(interaction) {
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayer(interaction);
const period = resolvePeriod(interaction);
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(interaction);
const tankIdRaw = interaction.options.getString('tank')!;
const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : 0;
const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
});
return await evolution(commandGroup, period, player, tankId);
// new ButtonBuilder()
// .setStyle(ButtonStyle.Link)
// .setLabel('View on BlitzStars')
// .setURL(
// commandGroup === 'tank'
// ? `https://www.blitzstars.com/player/com/${
// accountInfo[id].nickname
// }/tank/${tankId!}`
// : `https://www.blitzstars.com/player/com/${accountInfo[id].nickname}`,
// );
return [
await evolution(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}${
commandGroup === 'tank' ? `/tank/${tankId!}` : ''
}`,
'BlitzStars',
),
];
},
autocomplete: (interaction) => {
usernameAutocomplete(interaction);
tanksAutocomplete(interaction);
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const period = resolvePeriodFromButton(interaction);
const player = await resolvePlayerFromButton(interaction);
return await evolution(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
);
},
} satisfies CommandRegistry;
+4 -4
View File
@@ -1,12 +1,12 @@
import { SlashCommandBuilder } from 'discord.js';
import discord from '../../discord.json' assert { type: 'json' };
import infoEmbed from '../core/interaction/infoEmbed.js';
import { client } from '../bot.js';
import embedInfo from '../core/discord/embedInfo.js';
import {
CommandRegistry,
guildCommands,
publicCommands,
} from '../events/interactionCreate/index.js';
import { client } from '../index.js';
export default {
inProduction: true,
@@ -17,8 +17,8 @@ export default {
.setName('help')
.setDescription('All the help you need about the bot'),
execute(interaction) {
return infoEmbed(
handler(interaction) {
return embedInfo(
`${client.user?.username} help`,
`**About**\n${
client.user?.username
+8 -8
View File
@@ -1,15 +1,15 @@
import { SlashCommandBuilder } from 'discord.js';
import GenericStats from '../components/GenericStats/index.js';
import NoData, { NoDataType } from '../components/NoData.js';
import PoweredByWargaming from '../components/PoweredByWargaming.js';
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js';
import clanAutocomplete from '../core/autocomplete/clan.js';
import getBlitzClan from '../core/blitz/getBlitzClan.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addClanChoices from '../core/options/addClanChoices.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import addClanChoices from '../core/discord/addClanChoices.js';
import autocompleteClan from '../core/discord/autocompleteClan.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
import { ClanInfo } from '../types/clanInfo.js';
@@ -34,9 +34,9 @@ export default {
.setMinValue(0),
),
async execute(interaction) {
async handler(interaction) {
const clanName = interaction.options.getString('clan')!;
const { server, id } = await getBlitzClan(interaction, clanName);
const { server, id } = await getBlitzClan(clanName);
const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
@@ -79,10 +79,10 @@ export default {
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
{hasInactiveMembers && <GenericStats stats={inactive} />}
<PoweredByWargaming />
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper>
);
},
autocomplete: clanAutocomplete,
autocomplete: autocompleteClan,
} satisfies CommandRegistry;
+10 -10
View File
@@ -1,11 +1,10 @@
import { SlashCommandBuilder } from 'discord.js';
import NoData, { NoDataType } from '../components/NoData.js';
import PoweredByWargaming from '../components/PoweredByWargaming.js';
import PoweredBy, { PoweredByType } from '../components/PoweredBy.js';
import * as Tanks from '../components/Tanks/index.js';
import TitleBar from '../components/TitleBar.js';
import Wrapper from '../components/Wrapper.js';
import { BLITZ_SERVERS } from '../constants/servers.js';
import usernameAutocomplete from '../core/autocomplete/username.js';
import getTankStats from '../core/blitz/getTankStats.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import {
@@ -13,9 +12,10 @@ import {
Tier,
tankopedia,
} from '../core/blitz/tankopedia.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
@@ -67,11 +67,11 @@ export default {
)
.setRequired(true),
)
.addStringOption(addUsernameOption),
.addStringOption(addUsernameChoices),
async execute(interaction) {
async handler(interaction) {
const tier = Number(interaction.options.getString('tier'));
const account = await resolvePlayer(interaction);
const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
@@ -115,10 +115,10 @@ export default {
</Tanks.Root>
)}
<PoweredByWargaming />
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper>
);
},
autocomplete: usernameAutocomplete,
autocomplete: autocompleteUsername,
} satisfies CommandRegistry;
+2 -2
View File
@@ -17,7 +17,7 @@ export default {
option.setName('blitz').setDescription('Ping Blitz'),
),
async execute(interaction) {
async handler(interaction) {
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
@@ -30,4 +30,4 @@ export default {
const executionTime = Date.now() - executionStart;
interaction.editReply(`Pong 🏓 - ${executionTime}ms`);
},
} satisfies CommandRegistry;
} satisfies CommandRegistry<true>;
+14 -12
View File
@@ -1,13 +1,15 @@
import { SlashCommandBuilder } from 'discord.js';
import { startCase } from 'lodash';
import markdownEscape from 'markdown-escape';
import usernameAutocomplete from '../core/autocomplete/username.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import cleanTable, { TableInputEntry } from '../core/interaction/cleanTable.js';
import infoEmbed from '../core/interaction/infoEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import embedInfo from '../core/discord/embedInfo.js';
import markdownTable, {
TableInputEntry,
} from '../core/discord/markdownTable.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountAchievements } from '../types/accountAchievements.js';
import { AccountInfo } from '../types/accountInfo.js';
@@ -22,7 +24,7 @@ export default {
command: new SlashCommandBuilder()
.setName('playerachievements')
.setDescription("All the player's achievements")
.addStringOption(addUsernameOption)
.addStringOption(addUsernameChoices)
.addStringOption((option) =>
option
.setName('sort')
@@ -33,9 +35,9 @@ export default {
),
),
async execute(interaction) {
async handler(interaction) {
const sortBy = (interaction.options.getString('sort') ?? 'name') as SortBy;
const account = await resolvePlayer(interaction);
const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account;
const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
@@ -50,10 +52,10 @@ export default {
...accountAchievements.max_series,
};
return infoEmbed(
return embedInfo(
`${markdownEscape(accounts[id].nickname)}'s information`,
cleanTable(
markdownTable(
Object.keys(compound)
.filter((achievement) => compound[achievement] > 0)
.sort(
@@ -72,5 +74,5 @@ export default {
);
},
autocomplete: usernameAutocomplete,
autocomplete: autocompleteUsername,
} satisfies CommandRegistry;
+12 -12
View File
@@ -1,12 +1,12 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import usernameAutocomplete from '../core/autocomplete/username.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import cleanTable from '../core/interaction/cleanTable.js';
import infoEmbed from '../core/interaction/infoEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import embedInfo from '../core/discord/embedInfo.js';
import markdownTable from '../core/discord/markdownTable.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
@@ -18,20 +18,20 @@ export default {
command: new SlashCommandBuilder()
.setName('playerinfo')
.setDescription('Basic information about a player')
.addStringOption(addUsernameOption),
.addStringOption(addUsernameChoices),
async execute(interaction) {
const account = await resolvePlayer(interaction);
async handler(interaction) {
const account = await resolvePlayerFromCommand(interaction);
const { id, server } = account;
const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const accountInfo = accounts[id];
return infoEmbed(
return embedInfo(
`${markdownEscape(accountInfo.nickname)}'s information`,
cleanTable([
markdownTable([
['Nickname', `${accountInfo.nickname}`],
['Battles', `${accountInfo.statistics.all.battles}`],
[
@@ -56,5 +56,5 @@ export default {
);
},
autocomplete: usernameAutocomplete,
autocomplete: autocompleteUsername,
} satisfies CommandRegistry;
+6 -6
View File
@@ -2,10 +2,10 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import infoEmbed from '../core/interaction/infoEmbed.js';
import addClanChoices from '../core/options/addClanChoices.js';
import addServerChoices from '../core/options/addServerChoices.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import addClanChoices from '../core/discord/addClanChoices.js';
import addServerChoices from '../core/discord/addServerChoices.js';
import embedInfo from '../core/discord/embedInfo.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { ClanList } from '../types/clanList.js';
@@ -27,7 +27,7 @@ export default {
.setMaxValue(100),
),
async execute(interaction) {
async handler(interaction) {
const server = interaction.options.getString('server') as BlitzServer;
const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? 25;
@@ -35,7 +35,7 @@ export default {
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
);
return infoEmbed(
return embedInfo(
`Clan search for "${markdownEscape(clan)}" in ${BLITZ_SERVERS[server]}`,
clanList.length === 0
? 'No clans found.'
+8 -8
View File
@@ -2,11 +2,11 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import { usernamePattern } from '../core/blitz/listAccountsPanServer.js';
import infoEmbed from '../core/interaction/infoEmbed.js';
import addServerChoices from '../core/options/addServerChoices.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import { usernamePattern } from '../core/blitz/listPlayers.js';
import addServerChoices from '../core/discord/addServerChoices.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import embedInfo from '../core/discord/embedInfo.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountList } from '../types/accountList.js';
@@ -20,7 +20,7 @@ export default {
.setDescription('Search players in a Blitz server')
.addStringOption(addServerChoices)
.addStringOption((option) =>
addUsernameOption(option).setAutocomplete(false).setRequired(true),
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
)
.addIntegerOption((option) =>
option
@@ -30,7 +30,7 @@ export default {
.setMaxValue(100),
),
async execute(interaction) {
async handler(interaction) {
const server = interaction.options.getString('server') as BlitzServer;
const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25;
@@ -43,7 +43,7 @@ export default {
)
: [];
return infoEmbed(
return embedInfo(
`Player search for "${markdownEscape(name)}" in ${BLITZ_SERVERS[server]}`,
`\`\`\`${
players.length === 0
+4 -4
View File
@@ -2,8 +2,8 @@ import { SlashCommandBuilder } from 'discord.js';
import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape';
import { TANK_NAMES } from '../core/blitz/tankopedia.js';
import infoEmbed from '../core/interaction/infoEmbed.js';
import addTankChoices from '../core/options/addTankChoices.js';
import addTankChoices from '../core/discord/addTankChoices.js';
import embedInfo from '../core/discord/embedInfo.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
export default {
@@ -23,14 +23,14 @@ export default {
.setMaxValue(100),
),
async execute(interaction) {
async handler(interaction) {
const tank = interaction.options.getString('tank')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const results = go(tank, TANK_NAMES, { limit }).map(
(result) => result.target,
);
return infoEmbed(
return embedInfo(
`Tank search for "${markdownEscape(tank)}"`,
results.length === 0
? 'No tanks found.'
+41 -27
View File
@@ -1,64 +1,78 @@
import { SlashCommandBuilder } from 'discord.js';
import { CYCLIC_API } from '../constants/cyclic.js';
import tanksAutocomplete from '../core/autocomplete/tanks.js';
import usernameAutocomplete from '../core/autocomplete/username.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import interactionToURL from '../core/interaction/interactionToURL.js';
import linkButton from '../core/interaction/linkButton.js';
import primaryButton from '../core/interaction/primaryButton.js';
import { Period } from '../core/options/addPeriodSubCommands.js';
import addStatsSubCommandGroups from '../core/options/addStatsSubCommandGroups.js';
import resolvePeriod from '../core/options/resolvePeriod.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import stats, { StatType } from '../renderers/stats.js';
import stats from '../renderers/stats.js';
import { StatType } from '../renderers/statsfull.js';
import { AccountInfo } from '../types/accountInfo.js';
export default {
inProduction: true,
inDevelopment: false,
inDevelopment: true,
inPublic: true,
command: addStatsSubCommandGroups(
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('stats')
.setDescription('In-game statistics'),
),
async execute(interaction) {
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const subcommand = interaction.options.getSubcommand() as Period;
const player = await resolvePlayer(interaction);
const period = resolvePeriod(interaction);
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(interaction);
const tankIdRaw = interaction.options.getString('tank')!;
const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : 0;
const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const path = `stats/${commandGroup}/${subcommand}?server=${
player.server
}&id=${player.id}&tankId=${tankId}&start=${start ?? 0}&end=${end ?? 0}`;
interactionToURL(interaction);
const path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
return [
await stats(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
linkButton(path, 'Embed'),
linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
'BlitzStars',
),
];
},
autocomplete: (interaction) => {
usernameAutocomplete(interaction);
tanksAutocomplete(interaction);
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const period = resolvePeriod(interaction);
const player = await resolvePlayer(interaction);
const period = resolvePeriodFromButton(interaction);
const player = await resolvePlayerFromButton(interaction);
return await stats(
commandGroup,
+83
View File
@@ -0,0 +1,83 @@
import { SlashCommandBuilder } from 'discord.js';
import { CYCLIC_API } from '../constants/cyclic.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import resolveTankId from '../core/blitz/resolveTankId.js';
import addStatTypeSubCommandGroups from '../core/discord/addStatTypeSubCommandGroups.js';
import autocompleteTanks from '../core/discord/autocompleteTanks.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton.js';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import statsfull, { StatType } from '../renderers/statsfull.js';
import { AccountInfo } from '../types/accountInfo.js';
export default {
inProduction: true,
inDevelopment: false,
inPublic: true,
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('statsfull')
.setDescription('Full in-game statistics'),
),
async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup(
true,
) as StatType;
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(interaction);
const tankIdRaw = interaction.options.getString('tank')!;
const tankId = commandGroup === 'tank' ? resolveTankId(tankIdRaw) : null;
const start = interaction.options.getInteger('start');
const end = interaction.options.getInteger('end');
const path = interactionToURL(interaction, {
...player,
tankId,
start,
end,
});
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
return [
await statsfull(commandGroup, period, player, tankId),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
'BlitzStars',
),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
const path = url.pathname.split('/').filter(Boolean);
const commandGroup = path[1] as StatType;
const period = resolvePeriodFromButton(interaction);
const player = await resolvePlayerFromButton(interaction);
return await statsfull(
commandGroup,
period,
player,
parseInt(url.searchParams.get('tankId')!),
);
},
} satisfies CommandRegistry;
+35 -8
View File
@@ -1,9 +1,16 @@
import { SlashCommandBuilder } from 'discord.js';
import usernameAutocomplete from '../core/autocomplete/username.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import interactionToURL from '../core/discord/interactionToURL.js';
import linkButton from '../core/discord/linkButton.js';
import primaryButton from '../core/discord/primaryButton.js';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import today from '../renderers/today.js';
import { AccountInfo } from '../types/accountInfo.js';
export default {
inProduction: true,
@@ -13,13 +20,33 @@ export default {
command: new SlashCommandBuilder()
.setName('today')
.setDescription('A general daily breakdown of your performance')
.addStringOption(addUsernameOption),
.addStringOption(addUsernameChoices),
async execute(interaction) {
const player = await resolvePlayer(interaction);
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const { nickname } = (
await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${player.server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${player.id}`,
)
)[player.id];
const path = interactionToURL(interaction, player);
return [
await today(player),
primaryButton(path, 'Refresh'),
// linkButton(`${CYCLIC_API}/${path}`, 'Embed'),
linkButton(
`https://www.blitzstars.com/player/${player.server}/${nickname}`,
'BlitzStars',
),
];
},
autocomplete: autocompleteUsername,
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
return await today(player);
},
autocomplete: usernameAutocomplete,
} satisfies CommandRegistry;
+15 -15
View File
@@ -1,13 +1,13 @@
import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' };
import usernameAutocomplete from '../core/autocomplete/username.js';
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
import negativeEmbed from '../core/interaction/negativeEmbed.js';
import positiveEmbed from '../core/interaction/positiveEmbed.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import resolvePlayer from '../core/options/resolvePlayer.js';
import { WARGAMING_APPLICATION_ID } from '../core/process/args.js';
import addUsernameChoices from '../core/discord/addUsernameChoices.js';
import autocompleteUsername from '../core/discord/autocompleteUsername.js';
import embedNegative from '../core/discord/embedNegative.js';
import embedPositive from '../core/discord/embedPositive.js';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand.js';
import { WARGAMING_APPLICATION_ID } from '../core/node/arguments.js';
import { CommandRegistry } from '../events/interactionCreate/index.js';
import { AccountInfo } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
@@ -20,10 +20,10 @@ export default {
command: new SlashCommandBuilder()
.setName('verify')
.setDescription("Set's the user's username to their in-game name")
.addStringOption((option) => addUsernameOption(option).setRequired(true)),
.addStringOption((option) => addUsernameChoices(option).setRequired(true)),
async execute(interaction) {
const blitzAccount = await resolvePlayer(interaction);
async handler(interaction) {
const blitzAccount = await resolvePlayerFromCommand(interaction);
const { id, server } = blitzAccount;
const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
@@ -38,13 +38,13 @@ export default {
interaction.guild?.members.cache.get(interaction.member?.user.id);
if (!interaction.guild?.members.me?.permissions.has('ManageNicknames')) {
return negativeEmbed(
return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`,
"I don't have the permission to change your nickname.",
);
}
if (!interaction.member || !member) {
return negativeEmbed(
return embedNegative(
`${interaction.user.username} is not in this server`,
"I couldn't find this person in this server. Did they leave?",
);
@@ -55,7 +55,7 @@ export default {
if (interaction.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
return negativeEmbed(
return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`,
"I don't have the permission to change your manage roles.",
);
@@ -65,19 +65,19 @@ export default {
await member.roles.add(discord.sklld_peasant_role);
}
return positiveEmbed(
return embedPositive(
`${interaction.user.username} is verified`,
`The user is now verified as ${markdownEscape(
accountInfo[id].nickname,
)}${markdownEscape(clanTag)}`,
);
} catch (error) {
return negativeEmbed(
return embedNegative(
`${interaction.user.username} failed to verify`,
"I can't change your nickname because you have higher permissions than me. Try setting your nickname to your Blitz username manually.",
);
}
},
autocomplete: usernameAutocomplete,
autocomplete: autocompleteUsername,
} satisfies CommandRegistry;
@@ -0,0 +1,39 @@
import { Percentile } from '../../../../../constants/percentiles.js';
import hslToHex from '../../../../../core/node/hslToHex.js';
import { PERCENTILE_COLORS } from '../../../../PercentileIndicator.js';
export enum GlowSide {
Top,
Bottom,
}
export interface GlowProps {
percentile: Percentile;
side: GlowSide;
}
export function Glow({ percentile, side }: GlowProps) {
const color = PERCENTILE_COLORS[percentile];
const color1 = hslToHex(color);
const color2 = `${color1}00`;
const background = `linear-gradient(${
side === GlowSide.Top ? 0 : 180
}deg, ${color1} 0%, ${color2} 100%)`;
return (
<div
style={{
display: 'flex',
alignItems: side === GlowSide.Top ? 'flex-end' : 'flex-start',
justifyContent: 'center',
}}
>
<div style={{ height: 54, width: 16, background }} />
<div style={{ height: 72, width: 16, background }} />
<div style={{ height: 80, width: 16, background }} />
<div style={{ height: 72, width: 16, background }} />
<div style={{ height: 54, width: 16, background }} />
</div>
);
}
@@ -0,0 +1,77 @@
import { Percentile } from '../../../../constants/percentiles.js';
import getWN8Percentile from '../../../../core/blitz/getWN8Percentile.js';
import { theme } from '../../../../stitches.config.js';
import { PERCENTILE_COLORS } from '../../../PercentileIndicator.js';
import { Glow, GlowSide } from './components/Glow.js';
export interface WN8DisplayProps {
WN8?: number;
}
export const PERCENTILE_NAMES = {
[Percentile.VeryBad]: 'Very Bad',
[Percentile.Bad]: 'Bad',
[Percentile.BelowAverage]: 'Below Average',
[Percentile.Average]: 'Average',
[Percentile.AboveAverage]: 'Above Average',
[Percentile.Good]: 'Good',
[Percentile.VeryGood]: 'Very Good',
[Percentile.Great]: 'Great',
[Percentile.Unicum]: 'Unicum',
[Percentile.SuperUnicum]: 'Super Unicum',
};
export function WN8Display({ WN8 }: WN8DisplayProps) {
const percentile =
WN8 === undefined ? Percentile.VeryBad : getWN8Percentile(WN8);
const color = PERCENTILE_COLORS[percentile];
return (
<div
style={{
width: 128 * 2, // idk why twice the width turns into the correct width
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: -16,
}}
>
<Glow percentile={percentile} side={GlowSide.Top} />
<div
style={{
width: 128,
height: 80,
backgroundColor: color,
borderRadius: 40,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: -4,
}}
>
<span
style={{
fontWeight: 900,
fontSize: 32,
color: theme.colors.textHighContrast,
}}
>
{WN8 === undefined ? '--' : WN8.toFixed(0)}
</span>
<span
style={{
fontSize: 16,
color: theme.colors.textHighContrast,
}}
>
{PERCENTILE_NAMES[percentile]}
</span>
</div>
<Glow percentile={percentile} side={GlowSide.Bottom} />
</div>
);
}
@@ -0,0 +1,47 @@
import { theme } from '../../../stitches.config.js';
export enum WN8SurroundingStatAlign {
Left,
Right,
}
export interface WN8SurroundingStatProps {
label: string;
value: string | number;
align?: WN8SurroundingStatAlign;
padded?: boolean;
}
export function WN8SurroundingStat({
label,
value,
align = WN8SurroundingStatAlign.Left,
padded = false,
}: WN8SurroundingStatProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems:
align === WN8SurroundingStatAlign.Left ? 'flex-start' : 'flex-end',
paddingLeft: padded && align === WN8SurroundingStatAlign.Left ? 32 : 0,
paddingRight:
padded && align === WN8SurroundingStatAlign.Right ? 32 : 0,
}}
>
<span
style={{
color: theme.colors.textHighContrast,
fontSize: 24,
fontWeight: 'bold',
}}
>
{value}
</span>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
{label}
</span>
</div>
);
}
+96
View File
@@ -0,0 +1,96 @@
import { AllStats, SupplementaryStats } from '../../types/accountInfo.js';
import { WN8Display } from './components/WN8Display/index.js';
import {
WN8SurroundingStat,
WN8SurroundingStatAlign,
} from './components/WN8SurroundingStat.js';
export interface AllStatsOverviewProps {
stats: AllStats;
supplementaryStats: SupplementaryStats;
}
export default function AllStatsOverview({
stats,
supplementaryStats,
}: AllStatsOverviewProps) {
return (
<div
style={{
display: 'flex',
gap: -64,
width: '100%',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
alignItems: 'flex-end',
flex: 1,
}}
>
<WN8SurroundingStat
label="Battles"
value={stats.battles.toLocaleString()}
align={WN8SurroundingStatAlign.Right}
/>
<WN8SurroundingStat
label="Winrate"
value={`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label="Damage"
value={Math.round(
stats.damage_dealt / stats.battles,
).toLocaleString()}
align={WN8SurroundingStatAlign.Right}
padded
/>
<WN8SurroundingStat
label="Dmg. ratio"
value={(stats.damage_dealt / stats.damage_received).toFixed(2)}
align={WN8SurroundingStatAlign.Right}
/>
</div>
<WN8Display WN8={supplementaryStats.WN8} />
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
flex: 1,
}}
>
<WN8SurroundingStat
label="Tier"
value={supplementaryStats.tier.toFixed(2)}
/>
<WN8SurroundingStat
label="Survival"
value={`${(100 * (stats.survived_battles / stats.battles)).toFixed(
2,
)}%`}
padded
/>
<WN8SurroundingStat
label="Accuracy"
value={`${(100 * (stats.hits / stats.shots)).toFixed(2)}%`}
padded
/>
<WN8SurroundingStat
label="Kill ratio"
value={(
stats.frags /
(stats.battles - stats.survived_battles)
).toFixed(2)}
/>
</div>
</div>
);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import getWN8Percentile from '../core/blitz/getWN8Percentile.js';
import isNumber from '../core/process/isNumber.js';
import isNumber from '../core/node/isNumber.js';
import { AllStats, SupplementaryStats } from '../types/accountInfo.js';
import GenericStats from './GenericStats/index.js';
+3 -1
View File
@@ -1,16 +1,18 @@
import { blue, green, red } from '@radix-ui/colors';
import { blue, green, red, slateDark } from '@radix-ui/colors';
import { GRAPH_HEIGHT, GRAPH_WIDTH } from './Root.js';
export enum LineColor {
Red,
Green,
Blue,
White,
}
const LINE_COLORS = {
[LineColor.Red]: red.red9,
[LineColor.Green]: green.green9,
[LineColor.Blue]: blue.blue9,
[LineColor.White]: slateDark.slate11,
};
export enum LineStyle {
@@ -1,32 +1,34 @@
import { times } from 'lodash';
import { theme } from '../../../stitches.config.js';
import { MarginProps } from './Root.js';
import { MarginInputProps } from './Root.js';
export enum VerticalMarginAlign {
Left,
Right,
export enum MarginOrientation {
Vertical,
Horizontal,
}
export interface VerticalMarginProps extends MarginProps {
export interface MarginProps extends MarginInputProps {
verticalSeparations: number;
align?: VerticalMarginAlign;
orientation?: MarginOrientation;
}
export function VerticalMargin({
export function Margin({
verticalSeparations,
max,
min,
suffix,
align = VerticalMarginAlign.Left,
}: VerticalMarginProps) {
precision = 0,
orientation = MarginOrientation.Vertical,
}: MarginProps) {
const isVertical = orientation === MarginOrientation.Vertical;
return (
<div
style={{
width: 32,
paddingLeft: isVertical ? 0 : 64,
width: isVertical ? 48 : '100%',
display: 'flex',
flexDirection: 'column',
alignItems:
align === VerticalMarginAlign.Right ? 'flex-end' : 'flex-start',
flexDirection: isVertical ? 'column' : 'row-reverse',
justifyContent: 'space-between',
}}
>
@@ -43,7 +45,7 @@ export function VerticalMargin({
((verticalSeparations - (index + 1)) /
(verticalSeparations - 1)) +
min
).toFixed(0)}${suffix ?? ''}`}
).toFixed(precision)}${suffix ?? ''}`}
</span>
))}
</div>
+36 -39
View File
@@ -1,42 +1,55 @@
import { times } from 'lodash';
import { ReactNode } from 'react';
import { theme } from '../../../stitches.config.js';
import { VerticalMargin, VerticalMarginAlign } from './VerticalMargin.js';
import { Margin, MarginOrientation } from './Margin.js';
export const GRAPH_WIDTH = 512;
export const GRAPH_WIDTH = 544;
export const GRAPH_HEIGHT = 320;
export interface MarginProps {
export interface MarginInputProps {
min: number;
max: number;
suffix?: string;
precision?: number;
}
export interface RootProps {
children: ReactNode;
leftVerticalMargin?: MarginProps;
rightVerticalMargin?: MarginProps;
verticalSeparations?: number;
verticalMargin?: MarginInputProps;
horizontalMargin?: MarginInputProps;
separations?: number;
xMinLabel: string;
xMaxLabel: string;
}
export function Root({
children,
leftVerticalMargin,
rightVerticalMargin,
verticalSeparations = 5,
verticalMargin,
horizontalMargin,
separations = 5,
xMinLabel,
xMaxLabel,
}: RootProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div
style={{
paddingLeft: 64,
display: 'flex',
justifyContent: 'space-between',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{xMinLabel}
</span>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{xMaxLabel}
</span>
</div>
<div style={{ display: 'flex', gap: 16 }}>
{leftVerticalMargin && (
<VerticalMargin
{...leftVerticalMargin}
verticalSeparations={verticalSeparations}
/>
{verticalMargin && (
<Margin {...verticalMargin} verticalSeparations={separations} />
)}
<svg
@@ -49,8 +62,8 @@ export function Root({
border: theme.borderStyles.nonInteractive,
}}
>
{times(verticalSeparations - 2, (index) => {
const y = GRAPH_HEIGHT * ((index + 1) / (verticalSeparations - 1));
{times(separations - 2, (index) => {
const y = GRAPH_HEIGHT * ((index + 1) / (separations - 1));
return (
<polyline
@@ -66,31 +79,15 @@ export function Root({
{children}
</svg>
{rightVerticalMargin && (
<VerticalMargin
{...rightVerticalMargin}
verticalSeparations={verticalSeparations}
align={VerticalMarginAlign.Right}
/>
)}
</div>
<div
style={{
paddingLeft: 48,
paddingRight: 48,
display: 'flex',
justifyContent: 'space-between',
}}
>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{xMinLabel}
</span>
<span style={{ fontSize: 16, color: theme.colors.textLowContrast }}>
{xMaxLabel}
</span>
</div>
{horizontalMargin && (
<Margin
{...horizontalMargin}
verticalSeparations={separations}
orientation={MarginOrientation.Horizontal}
/>
)}
</div>
);
}
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './components/Line.js';
export * from './components/Margin.js';
export * from './components/Root.js';
export * from './components/VerticalMargin.js';
+38
View File
@@ -0,0 +1,38 @@
import { ReactNode } from 'react';
import { theme } from '../stitches.config.js';
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: 'space-between' }}>
<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>
);
}
-15
View File
@@ -1,15 +0,0 @@
import { theme } from '../stitches.config.js';
export default function PoweredByBlitzStars() {
return (
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
<img
src="https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png"
style={{ height: 16, width: 16 }}
/>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
Powered by BlitzStars
</span>
</div>
);
}
-15
View File
@@ -1,15 +0,0 @@
import { theme } from '../stitches.config.js';
export default function PoweredByWargaming() {
return (
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
<img
src="https://i.imgur.com/jADGWm0.png"
style={{ height: 16, width: 16 }}
/>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
Powered by Wargaming
</span>
</div>
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import isDev from '../core/process/isDev.js';
import isDev from '../core/node/isDev.js';
import { theme } from '../stitches.config.js';
export interface TitleBarProps {
+3 -15
View File
@@ -1,19 +1,7 @@
export const BLITZ_SERVERS = {
export type BlitzServer = 'com' | 'eu' | 'asia';
export const BLITZ_SERVERS: Record<BlitzServer, string> = {
com: 'North America',
eu: 'Europe',
asia: 'Asia',
};
export const BLITZ_SERVERS_SHORT = {
com: 'NA',
eu: 'EU',
asia: 'ASIA',
};
export const BLITZ_SERVERS_SHORT_INVERSE = {
NA: 'com',
EU: 'eu',
ASIA: 'asia',
};
export type BlitzServer = keyof typeof BLITZ_SERVERS;
@@ -3,7 +3,7 @@ import { AllStats } from '../../types/accountInfo.js';
/**
* Formula from https://web.archive.org/web/20190105064817/http://wiki.wnefficiency.net/pages/WN8
*/
export default function getWN8(expected: AllStats, average: AllStats) {
export default function calculateWN8(expected: AllStats, average: AllStats) {
const avgDmg = average.damage_dealt / average.battles;
const avgSpot = average.spotted / average.battles;
const avgFrag = average.frags / average.battles;
+6 -10
View File
@@ -1,23 +1,19 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { BlitzServer } from '../../constants/servers.js';
import { serverAndIdPattern } from '../options/resolvePlayer.js';
import errorWithCause from '../process/errorWithCause.js';
import listClansPanServer from './listClansPanServer.js';
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand.js';
import throwError from '../node/throwError.js';
import listClans from './listClans.js';
export default async function getBlitzClan(
interaction: ChatInputCommandInteraction<CacheType>,
clan: string,
) {
export default async function getBlitzClan(clan: string) {
if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/');
return { server: server as BlitzServer, id: Number(accountId) };
} else {
const accounts = await listClansPanServer(clan);
const accounts = await listClans(clan);
if (accounts[0]) {
return { server: accounts[0].server, id: accounts[0].clan_id };
} else {
throw errorWithCause(
throw throwError(
'Could not find clan',
`I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
);
+3 -3
View File
@@ -1,7 +1,7 @@
import { BLITZ_SERVERS, BlitzServer } from '../../constants/servers.js';
import { TanksStats } from '../../types/tanksStats.js';
import { WARGAMING_APPLICATION_ID } from '../process/args.js';
import errorWithCause from '../process/errorWithCause.js';
import { WARGAMING_APPLICATION_ID } from '../node/arguments.js';
import throwError from '../node/throwError.js';
import getWargamingResponse from './getWargamingResponse.js';
export default async function getTankStats(server: BlitzServer, id: number) {
@@ -10,7 +10,7 @@ export default async function getTankStats(server: BlitzServer, id: number) {
);
if (tankStats[id] === null) {
throw errorWithCause(
throw throwError(
'No tank stats available',
`Wargaming says there is no tank stats for this account. This account may not have any battles/tanks or exist in the ${BLITZ_SERVERS[server]} server.`,
);
+12 -3
View File
@@ -1,5 +1,14 @@
import { WargamingResponse } from '../../types/wargamingResponse.js';
import errorWithCause from '../process/errorWithCause.js';
import throwError from '../node/throwError.js';
export type WargamingResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
}
| {
status: 'ok';
data: Data;
};
export default async function getWargamingResponse<Data extends object>(
url: string,
@@ -12,7 +21,7 @@ export default async function getWargamingResponse<Data extends object>(
if (parsed.status === 'ok') {
return parsed.data;
} else {
throw errorWithCause(
throw throwError(
`Wargaming response error status:"${parsed.status}"`,
`Message: "${parsed.error.message}"\nURL: "${url}"`,
);
@@ -1,13 +1,13 @@
import { BlitzServer } from '../../constants/servers.js';
import { Clan, ClanList } from '../../types/clanList.js';
import { WARGAMING_APPLICATION_ID } from '../process/args.js';
import { WARGAMING_APPLICATION_ID } from '../node/arguments.js';
import getWargamingResponse from './getWargamingResponse.js';
export type ClanListWithServer = (Clan & {
server: 'com' | 'eu' | 'asia';
})[];
export default async function listClansPanServer(search: string, limit = 9) {
export default async function listClans(search: string, limit = 9) {
const trimmed = search.trim();
if (trimmed.length < 3 && trimmed.length > 100) return [];
const normalizedLimit = Math.round(limit / 3);
@@ -1,6 +1,6 @@
import { BlitzServer } from '../../constants/servers.js';
import { Account, AccountList } from '../../types/accountList.js';
import { WARGAMING_APPLICATION_ID } from '../process/args.js';
import { WARGAMING_APPLICATION_ID } from '../node/arguments.js';
import getWargamingResponse from './getWargamingResponse.js';
export type AccountListWithServer = (Account & {
@@ -10,7 +10,7 @@ export type AccountListWithServer = (Account & {
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
export default async function listAccountsPanServer(search: string, limit = 9) {
export default async function listPlayers(search: string, limit = 9) {
const trimmedSearch = search.trim();
const normalizedLimit = Math.round(limit / 3);
const encodedSearch = encodeURIComponent(trimmedSearch);
+4 -4
View File
@@ -1,15 +1,15 @@
import { go } from 'fuzzysort';
import errorWithCause from '../process/errorWithCause.js';
import throwError from '../node/throwError.js';
import { TANKS, tankopedia } from './tankopedia.js';
export default function resolveTankId(tank: string) {
const number = Number(tank);
const number = parseInt(tank);
if (Number.isNaN(number)) {
const searchResult = go(tank, TANKS, { keys: ['name'], limit: 1 });
if (searchResult.length === 0) {
throw errorWithCause(
throw throwError(
'Tank not found',
`Could not find tank by the name "${tank}".`,
);
@@ -20,7 +20,7 @@ export default function resolveTankId(tank: string) {
if (tankopedia[number]) {
return number;
} else {
throw errorWithCause(
throw throwError(
'Tank not found',
`Could not find tank by the ID "${number}".`,
);
+1 -1
View File
@@ -1,5 +1,5 @@
import { AllStats } from '../../types/accountInfo.js';
import { emptyAllStats } from '../blitzstars/getTankStatsOverTime.js';
import { emptyAllStats } from '../blitzstars/getTankStatsDiffed.js';
export default function sumStats(allStatsCollection: AllStats[]) {
return allStatsCollection.reduce((accumulated, current) => {
+11 -25
View File
@@ -1,4 +1,5 @@
import { WargamingResponse } from '../../types/wargamingResponse.js';
import { readFileSync } from 'fs';
import { join } from 'path';
export type TankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank';
@@ -18,30 +19,15 @@ export interface Tankopedia {
[id: number]: TankopediaEntry;
}
export const tankopedia: Tankopedia = {};
export const TANK_IDS: number[] = [];
export const TANKS: TankopediaEntry[] = [];
export const TANK_NAMES: string[] = [];
console.log('Caching tankopedia...');
fetch('https://www.blitzstars.com/bs-tankopedia.json')
.then(
(response) =>
response.json() as Promise<
WargamingResponse<Tankopedia> & { status: 'ok' }
>,
)
.then((wargamingResponse) => {
Object.assign(tankopedia, wargamingResponse.data);
Object.entries(tankopedia).forEach(([idString, entry]) => {
const id = Number(idString);
TANK_IDS.push(id);
TANKS.push(entry);
TANK_NAMES.push(entry.name);
}),
console.log('Cached tankopedia');
});
export const tankopedia = (
JSON.parse(
readFileSync(join(__dirname, 'tankopedia.json')) as unknown as string,
) as { data: Tankopedia }
).data;
const entries = Object.entries(tankopedia);
export const TANK_IDS = entries.map(([, { tank_id }]) => tank_id as number);
export const TANKS = entries.map(([, entry]) => entry as TankopediaEntry);
export const TANK_NAMES = entries.map(([, { name }]) => name);
export const TANK_ICONS: Record<TankType, string> = {
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
-26
View File
@@ -1,26 +0,0 @@
import {
CacheType,
ChatInputCommandInteraction,
EmbedBuilder,
} from 'discord.js';
import { NEGATIVE_COLOR } from '../../constants/colors.js';
import { handleError } from '../../events/error.js';
import { client } from '../../index.js';
export default async function wargamingError(
interaction: ChatInputCommandInteraction<CacheType>,
error: Error,
command: string,
) {
await interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(NEGATIVE_COLOR)
.setTitle(`${client.user?.username} ran into an issue`)
.setDescription('Wargaming returned an error. Please try again later.'),
],
});
console.log(`Wargaming returned an error`);
handleError(error, command);
}
-28
View File
@@ -1,28 +0,0 @@
import {
CacheType,
ChatInputCommandInteraction,
EmbedBuilder,
} from 'discord.js';
import { NEGATIVE_COLOR } from '../../constants/colors.js';
import { handleError } from '../../events/error.js';
import { client } from '../../index.js';
export default async function blitzStarsError(
interaction: ChatInputCommandInteraction<CacheType>,
error: Error,
command: string,
) {
await interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(NEGATIVE_COLOR)
.setTitle(`${client.user?.username} ran into an issue`)
.setDescription(
'BlitzStars returned an error. Please try again later.',
),
],
});
console.log(`BlitzStars returned an error`);
handleError(error, command);
}
-5
View File
@@ -1,5 +0,0 @@
import { BlitzServer } from '../../constants/servers.js';
export default function blitzStarsLinks(server: BlitzServer, nickname: string) {
return `[View full stats](https://www.blitzstars.com/player/${server}/${nickname}) • [Support BlitzStars](https://www.blitzstars.com/supporters)`;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Period } from '../options/addPeriodSubCommands.js';
import { Period } from '../discord/addPeriodSubCommands.js';
import getTimeDaysAgo from './getTimeDaysAgo.js';
export default function getPeriodStart(period: Exclude<Period, 'custom'>) {
+2 -2
View File
@@ -7,8 +7,8 @@ import {
getHistoriesDefaultOptions,
} from '../../types/histories.js';
import getWargamingResponse from '../blitz/getWargamingResponse.js';
import { WARGAMING_APPLICATION_ID } from '../process/args.js';
import { emptyAllStats } from './getTankStatsOverTime.js';
import { WARGAMING_APPLICATION_ID } from '../node/arguments.js';
import { emptyAllStats } from './getTankStatsDiffed.js';
export interface PlayerHistoryRaw {
clan: { clan_id: number; name: string; tag: string };
+1 -1
View File
@@ -6,7 +6,7 @@ import {
getHistoriesDefaultOptions,
} from '../../types/histories.js';
import getTankStats from '../blitz/getTankStats.js';
import { emptyAllStats } from './getTankStatsOverTime.js';
import { emptyAllStats } from './getTankStatsDiffed.js';
export interface TankHistory extends History {
tank_id: number;
@@ -34,7 +34,7 @@ export const emptyTankHistoryNode: TankHistoryRaw = {
all: emptyAllStats,
};
export default async function getTankStatsOverTime(
export default async function getTankStatsDiffed(
server: BlitzServer,
id: number,
start: number,
+2 -2
View File
@@ -1,4 +1,4 @@
export default function getTimeDaysAgo(period: number) {
export default function getTimeDaysAgo(daysAgo: number) {
const now = new Date();
now.setHours(5, 0, 0, 0);
@@ -6,7 +6,7 @@ export default function getTimeDaysAgo(period: number) {
if (now.getTime() > Date.now()) now.setDate(now.getDate() - 1);
// subtract the period number of days from now
now.setDate(now.getDate() - period);
now.setDate(now.getDate() - daysAgo);
return now.getTime() / 1000;
}
-11
View File
@@ -1,11 +0,0 @@
export default function last5AM() {
const fiveAM = new Date();
fiveAM.setHours(5, 0, 0, 0); // Set the time to 5:00 AM today
if (fiveAM > new Date()) {
// If it's now before 5:00 AM, go back one day
fiveAM.setDate(fiveAM.getDate() - 1);
}
return fiveAM;
}
+1 -1
View File
@@ -1,5 +1,5 @@
import sumStats from '../blitz/sumStats.js';
import { emptyAllStats } from './getTankStatsOverTime.js';
import { emptyAllStats } from './getTankStatsDiffed.js';
import { tankAverages } from './tankAverages.js';
console.log('Calculating player average...');
@@ -1,8 +0,0 @@
import { EmbedBuilder } from 'discord.js';
export default function poweredByBlitzStars(embed: EmbedBuilder) {
return embed.setTimestamp().setFooter({
text: 'Powered by BlitzStars',
iconURL: 'https://www.blitzstars.com/assets/images/TankyMcPewpew.png',
});
}
+6 -10
View File
@@ -1,3 +1,5 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { AllStats } from '../../types/accountInfo.js';
export interface SpecialStats {
@@ -78,14 +80,8 @@ export interface IndividualTankAverage {
last_update: number;
}
export const tankAverages: Record<number, IndividualTankAverage> = {};
export type TankAverages = Record<number, IndividualTankAverage>;
console.log('Caching tank averages...');
fetch('https://www.blitzstars.com/api/tankaverages.json')
.then((response) => response.json() as Promise<IndividualTankAverage[]>)
.then((individualTankAverages) => {
individualTankAverages.forEach((individualTankAverage) => {
tankAverages[individualTankAverage.tank_id] = individualTankAverage;
});
console.log('Tank averages cached');
});
export const tankAverages = JSON.parse(
readFileSync(join(__dirname, 'tankaverages.json')) as unknown as string,
) as TankAverages;
File renamed without changes.
@@ -2,7 +2,7 @@ import {
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { PERIOD_NAMES } from './resolvePeriod.js';
import { PERIOD_NAMES } from '../discord/resolvePeriodFromCommand.js';
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom';
File renamed without changes.
@@ -1,13 +1,15 @@
import { SlashCommandBuilder } from 'discord.js';
import addPeriodSubCommands from './addPeriodSubCommands.js';
import addTankChoices from './addTankChoices.js';
import addUsernameOption from './addUsernameOption.js';
import addUsernameChoices from './addUsernameChoices.js';
export default function addStatsSubCommandGroups(option: SlashCommandBuilder) {
export default function addStatTypeSubCommandGroups(
option: SlashCommandBuilder,
) {
return option
.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameOption),
option.addStringOption(addUsernameChoices),
)
.setName('player')
.setDescription("Player's statistics"),
@@ -16,7 +18,7 @@ export default function addStatsSubCommandGroups(option: SlashCommandBuilder) {
addPeriodSubCommands(option, (option) =>
option
.addStringOption(addTankChoices)
.addStringOption(addUsernameOption),
.addStringOption(addUsernameChoices),
)
.setName('tank')
.setDescription("Tank's statistics"),
File renamed without changes.
@@ -1,6 +1,6 @@
import { SlashCommandStringOption } from 'discord.js';
export default function addUsernameOption(option: SlashCommandStringOption) {
export default function addUsernameChoices(option: SlashCommandStringOption) {
return option
.setName('username')
.setDescription(
@@ -4,14 +4,14 @@ import {
CacheType,
} from 'discord.js';
import { BLITZ_SERVERS } from '../../constants/servers.js';
import listClansPanServer from '../blitz/listClansPanServer.js';
import listClans from '../blitz/listClans.js';
export default async function clanAutocomplete(
export default async function autocompleteClan(
interaction: AutocompleteInteraction<CacheType>,
) {
const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'clan') return;
const players = await listClansPanServer(focusedOption.value);
const players = await listClans(focusedOption.value);
await interaction.respond(
players
@@ -7,7 +7,7 @@ import { go } from 'fuzzysort';
import resolveTankName from '../blitz/resolveTankName.js';
import { TANKS } from '../blitz/tankopedia.js';
export default async function tanksAutocomplete(
export default async function autocompleteTanks(
interaction: AutocompleteInteraction<CacheType>,
) {
const focusedOption = interaction.options.getFocused(true);
@@ -4,14 +4,14 @@ import {
CacheType,
} from 'discord.js';
import { BLITZ_SERVERS } from '../../constants/servers.js';
import listAccountsPanServer from '../blitz/listAccountsPanServer.js';
import listPlayers from '../blitz/listPlayers.js';
export default async function usernameAutocomplete(
export default async function autocompleteUsername(
interaction: AutocompleteInteraction<CacheType>,
) {
const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'username') return;
const players = await listAccountsPanServer(focusedOption.value);
const players = await listPlayers(focusedOption.value);
try {
await interaction.respond(
@@ -1,7 +1,7 @@
import { EmbedBuilder } from 'discord.js';
import { ACCENT_COLOR } from '../../constants/colors.js';
export default function infoEmbed(title: string, description: string) {
export default function embedInfo(title: string, description: string) {
return new EmbedBuilder()
.setColor(ACCENT_COLOR)
.setTitle(title)
@@ -1,7 +1,7 @@
import { EmbedBuilder } from 'discord.js';
import { NEGATIVE_COLOR } from '../../constants/colors.js';
export default function negativeEmbed(title: string, description: string) {
export default function embedNegative(title: string, description: string) {
return new EmbedBuilder()
.setColor(NEGATIVE_COLOR)
.setTitle(title)
@@ -1,7 +1,7 @@
import { EmbedBuilder } from 'discord.js';
import { POSITIVE_COLOR } from '../../constants/colors.js';
export default function positiveEmbed(title: string, description: string) {
export default function embedPositive(title: string, description: string) {
return new EmbedBuilder()
.setColor(POSITIVE_COLOR)
.setTitle(title)
@@ -1,7 +1,7 @@
import { EmbedBuilder } from 'discord.js';
import { WARNING_COLOR } from '../../constants/colors.js';
export default function warningEmbed(title: string, description: string) {
export default function embedWarning(title: string, description: string) {
return new EmbedBuilder()
.setColor(WARNING_COLOR)
.setTitle(title)
+33
View File
@@ -0,0 +1,33 @@
import {
CacheType,
ChatInputCommandInteraction,
CommandInteractionOption,
} from 'discord.js';
export interface Parameters {
[key: string]: CommandInteractionOption['value'] | undefined | null;
}
export default function interactionToURL(
interaction: ChatInputCommandInteraction<CacheType>,
parameters?: Parameters,
) {
const pathname = [
interaction.commandName,
interaction.options.getSubcommandGroup(false),
interaction.options.getSubcommand(false),
]
.filter(Boolean)
.join('/');
const params = [
...interaction.options.data.map(
(option) => [option.name, option.value] as const,
),
...Object.entries(parameters ?? {}),
]
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`)
.join('&');
return `${pathname}${params.length > 0 ? `?${params}` : ''}`;
}
@@ -3,16 +3,15 @@ import {
ButtonStyle,
ComponentEmojiResolvable,
} from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic.js';
export default function linkButton(
id: string,
url: string,
label: string,
emoji?: ComponentEmojiResolvable,
) {
const button = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setURL(`${CYCLIC_API}/${id}`)
.setURL(url)
.setLabel(label);
if (emoji) button.setEmoji(emoji);
@@ -1,7 +1,7 @@
export type TableInputEntry = [string, string | number] | [];
export type TableInput = TableInputEntry[];
export default function cleanTable(input: TableInput) {
export default function markdownTable(input: TableInput) {
return input
.filter((array) => array[1] !== undefined)
.map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`))
@@ -5,7 +5,7 @@ import {
InteractionEditReplyOptions,
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate/index.js';
import render from '../ui/render.js';
import jsxToPng from '../node/jsxToPng.js';
export default async function normalizeInteractionReturnable(
returnable: InteractionReturnable,
@@ -29,7 +29,7 @@ export default async function normalizeInteractionReturnable(
item,
);
} else {
const image = await render(item);
const image = await jsxToPng(item);
if (!reply.files) reply.files = [];
reply.files.push(image);
}
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,9 @@
import { ButtonInteraction, CacheType } from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic.js';
import resolvePeriodFromURL from '../express/resolvePeriodFromURL.js';
export default function resolvePeriodFromButton(
interaction: ButtonInteraction<CacheType>,
) {
return resolvePeriodFromURL(`${CYCLIC_API}/${interaction.customId}`);
}
@@ -1,13 +1,8 @@
import {
ButtonInteraction,
CacheType,
ChatInputCommandInteraction,
} from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic.js';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import getPeriodNow from '../blitzstars/getPeriodNow.js';
import getPeriodStart from '../blitzstars/getPeriodStart.js';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo.js';
import { Period } from './addPeriodSubCommands.js';
import { Period } from '../discord/addPeriodSubCommands.js';
export const PERIOD_NAMES: Record<Period, string> = {
today: "Today's statistics",
@@ -34,25 +29,18 @@ export interface ResolvedPeriod {
end: number;
}
export default function resolvePeriod(
interaction:
| ChatInputCommandInteraction<CacheType>
| ButtonInteraction<CacheType>,
export default function resolvePeriodFromCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
let statsName: string;
let evolutionName: string;
let start: number;
let end: number;
const period = interaction.options.getSubcommand() as Period;
if (period === 'custom') {
const startRaw =
interaction instanceof ChatInputCommandInteraction
? interaction.options.getInteger('start')!
: parseInt(url!.searchParams.get('start')!);
const endRaw =
interaction instanceof ChatInputCommandInteraction
? interaction.options.getInteger('end')!
: parseInt(url!.searchParams.get('end')!);
const startRaw = interaction.options.getInteger('start')!;
const endRaw = interaction.options.getInteger('end')!;
const startMin = Math.min(startRaw, endRaw);
const endMax = Math.max(startRaw, endRaw);
@@ -0,0 +1,9 @@
import { ButtonInteraction, CacheType } from 'discord.js';
import { CYCLIC_API } from '../../constants/cyclic.js';
import resolvePlayerFromURL from '../express/resolvePlayerFromURL.js';
export default async function resolvePlayerFromButton(
interaction: ButtonInteraction<CacheType>,
) {
return resolvePlayerFromURL(`${CYCLIC_API}/${interaction.customId}`);
}
@@ -0,0 +1,52 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { BlitzServer } from '../../constants/servers.js';
import listPlayers, {
usernamePatternWithoutPosition,
} from '../blitz/listPlayers.js';
import throwError from '../node/throwError.js';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export interface ResolvedPlayer {
server: BlitzServer;
id: number;
}
export default async function resolvePlayerFromCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const commandUsername = interaction.options.getString('username');
const displayName = interaction.guild?.members.cache
.get(interaction.user.id)
?.displayName.match(usernamePatternWithoutPosition)?.[0];
const username = commandUsername ?? displayName;
if (username === undefined) {
throw throwError(
'Username could not be inferred',
"You didn't provide me a username in the command nor was I able to infer your Blitz username from your Discord nickname. Try providing a username manually in the command or using the `/verify` command to set your Discord nickname to your Blitz username.",
);
}
if (serverAndIdPattern.test(username)) {
const [server, accountId] = username.split('/');
return {
server: server as BlitzServer,
id: Number(accountId),
} satisfies ResolvedPlayer;
} else {
const accounts = await listPlayers(username);
if (accounts[0]) {
return {
server: accounts[0].server,
id: accounts[0].account_id,
} satisfies ResolvedPlayer;
} else {
throw throwError(
'Could not find user',
`I couldn't find user \`${username}\`. Try providing a valid username manually or using the \`/verify\` command to set your Discord nickname to your Blitz username.`,
);
}
}
}
@@ -1,5 +1,6 @@
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')
@@ -0,0 +1,11 @@
import { RouteReturnable } from '../../server.js';
import jsxToSvg from '../node/jsxToSvg.js';
export default async function normalizeRouteReturnable(
returnable: RouteReturnable,
) {
const awaitedReturnable = await returnable;
const svg = await jsxToSvg(awaitedReturnable);
return svg;
}
@@ -0,0 +1,7 @@
import { Request } from 'express';
import { CYCLIC_API } from '../../constants/cyclic.js';
import resolvePeriodFromURL from './resolvePeriodFromURL.js';
export default function resolvePeriodFromRequest(request: Request) {
return resolvePeriodFromURL(`${CYCLIC_API}${request.path}`);
}
+43
View File
@@ -0,0 +1,43 @@
import getPeriodNow from '../blitzstars/getPeriodNow.js';
import getPeriodStart from '../blitzstars/getPeriodStart.js';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo.js';
import { Period } from '../discord/addPeriodSubCommands.js';
import {
EVOLUTION_PERIOD_NAMES,
PERIOD_NAMES,
ResolvedPeriod,
} from '../discord/resolvePeriodFromCommand.js';
export default function resolvePeriodFromURL(urlString: string) {
let statsName: string;
let evolutionName: string;
let start: number;
let end: number;
const url = new URL(urlString);
const path = url.pathname.split('/').filter(Boolean);
const period = path[path.length - 1] as Period;
if (period === 'custom') {
const startRaw = parseInt(url!.searchParams.get('start')!);
const endRaw = parseInt(url!.searchParams.get('end')!);
const startMin = Math.min(startRaw, endRaw);
const endMax = Math.max(startRaw, endRaw);
statsName = `${startMin} to ${endMax} days' statistics`;
evolutionName = `${startMin} to ${endMax} days' evolution`;
start = getTimeDaysAgo(startMin);
end = getTimeDaysAgo(endMax);
} else {
statsName = PERIOD_NAMES[period];
evolutionName = EVOLUTION_PERIOD_NAMES[period];
start = getPeriodStart(period);
end = getPeriodNow();
}
return {
statsName,
evolutionName,
start,
end,
} satisfies ResolvedPeriod;
}
@@ -0,0 +1,7 @@
import { Request } from 'express';
import { CYCLIC_API } from '../../constants/cyclic.js';
import resolvePlayerFromURL from './resolvePlayerFromURL.js';
export default function resolvePlayerFromRequest(request: Request) {
return resolvePlayerFromURL(`${CYCLIC_API}${request.originalUrl}`);
}
+11
View File
@@ -0,0 +1,11 @@
import { BlitzServer } from '../../constants/servers.js';
import { ResolvedPlayer } from '../discord/resolvePlayerFromCommand.js';
export default function resolvePlayerFromURL(urlString: string) {
const url = new URL(urlString);
return {
id: parseInt(url.searchParams.get('id')!),
server: url.searchParams.get('server') as BlitzServer,
} satisfies ResolvedPlayer;
}
+23
View File
@@ -0,0 +1,23 @@
export default function withAutoRefresh(svg: string, refreshAfter: number) {
return `
<html>
<head>
<script>
setTimeout(() => {
window.location.reload();
}, ${refreshAfter});
</script>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
${svg}
</body>
</html>
`;
}
@@ -1,12 +0,0 @@
import { ButtonBuilder, ButtonStyle } from 'discord.js';
import { BlitzServer } from '../../constants/servers.js';
export default function fullBlitzStarsStats(
server: BlitzServer,
username: string,
) {
return new ButtonBuilder()
.setLabel('View on BlitzStars')
.setURL(`https://www.blitzstars.com/player/${server}/${username}`)
.setStyle(ButtonStyle.Link);
}
-9
View File
@@ -1,9 +0,0 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
export default function generateCustomId(
interaction: ChatInputCommandInteraction<CacheType>,
) {
return `${interaction.commandName}/${
interaction.options.getSubcommandGroup() ?? ''
}/${interaction.options.getSubcommand() ?? ''}`;
}
-20
View File
@@ -1,20 +0,0 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
export default function interactionToURL(
interaction: ChatInputCommandInteraction<CacheType>,
) {
const pathname = [
interaction.commandName,
interaction.options.getSubcommandGroup(),
interaction.options.getSubcommand(),
]
.filter(Boolean)
.join('/');
const urlSearchParams = new URLSearchParams();
interaction.options.data.forEach((option) => {
if (option.value) urlSearchParams.set(option.name, `${option.value}`);
});
console.log(`${urlSearchParams}`);
}
-6
View File
@@ -1,6 +0,0 @@
import negativeEmbed from './negativeEmbed.js';
export const noMember = negativeEmbed(
'User of command is not a member',
'Discord claims that the user of this command does not have an account... strange!',
);
@@ -1,6 +0,0 @@
import negativeEmbed from './negativeEmbed.js';
export const notTrackedByBlitzStars = negativeEmbed(
'No data to display',
'Player is not tracked by BlitzStars.',
);
File renamed without changes.
File renamed without changes.
+22
View File
@@ -0,0 +1,22 @@
const numberRegex = /\d+(\.\d+)?/g;
/**
* Thanks to icl7126!
* https://stackoverflow.com/a/44134328/12294756
*/
export default function hslToHex(hsl: string) {
let [h, s, l] = Array.from(hsl.matchAll(numberRegex)).map((m) =>
parseFloat(m[0]),
);
l /= 100;
const a = (s * Math.min(l, 1 - l)) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
.toString(16)
.padStart(2, '0'); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}`;
}
+3
View File
@@ -0,0 +1,3 @@
export default function isDev() {
return process.env.NODE_ENV === 'development';
}
+3
View File
@@ -0,0 +1,3 @@
export default function isNumber(value: any) {
return typeof value === 'number' && isFinite(value);
}
+9
View File
@@ -0,0 +1,9 @@
import jsxToSvg from './jsxToSvg.js';
import svgToPng from './svgToPng.js';
export default async function jsxToPng(jsx: JSX.Element) {
const svg = await jsxToSvg(jsx);
const png = svgToPng(svg);
return png;
}
@@ -1,7 +1,5 @@
import { Resvg } from '@resvg/resvg-js';
import { readFile } from 'fs';
import { join } from 'path';
import { ReactNode } from 'react';
import satori from 'satori';
import robotoBlackFile from '../../assets/fonts/Roboto-Black.ttf';
import robotoBoldFile from '../../assets/fonts/Roboto-Bold.ttf';
@@ -32,8 +30,8 @@ Promise.all(
console.log('Fonts imported');
});
export default async function render(element: ReactNode) {
const svg = await satori(element, {
export default async function jsxToSvg(jsx: JSX.Element) {
return await satori(jsx, {
width: 640,
fonts: [
{ data: roboto, name: FONT_NAME, weight: 400 },
@@ -41,8 +39,4 @@ export default async function render(element: ReactNode) {
{ data: robotoBlack, name: FONT_NAME, weight: 900 },
],
});
const png = new Resvg(svg).render().asPng();
return png;
}
+5
View File
@@ -0,0 +1,5 @@
import { Resvg } from "@resvg/resvg-js";
export default function svgToPng(svg: string) {
return new Resvg(svg).render().asPng()
}
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more