mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Merge pull request #1 from tresabhi/1.5.0
This commit is contained in:
39 files changed
+663
-359
No files matched your search
Vendored
+1
@@ -9,6 +9,7 @@
|
||||
"Smri",
|
||||
"strvk",
|
||||
"tankopedia",
|
||||
"Unicum",
|
||||
"Unregistering",
|
||||
"vickers",
|
||||
"wargaming",
|
||||
|
||||
+23
-19
@@ -1,8 +1,10 @@
|
||||
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||
import { ACCENT_COLOR } from '../constants/colors.js';
|
||||
import { tankopedia } from '../core/blitzstars/tankopedia.js';
|
||||
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||
import cleanTable from '../core/interaction/cleanTable.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||
import getClientId from '../core/process/getClientId.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||
import { client } from '../index.js';
|
||||
@@ -24,22 +26,24 @@ export default {
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setTitle(`${client.user?.username} debug information`)
|
||||
.setColor(ACCENT_COLOR)
|
||||
.setDescription(
|
||||
`**Version**: ${packageJSON.version}\n**Uptime**: ${Math.floor(
|
||||
uptime / 1000 / 60 / 60 / 24,
|
||||
)}d ${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
|
||||
(uptime / 1000 / 60) % 60,
|
||||
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
|
||||
uptime % 1000,
|
||||
)}ms\n**Tankopedia**: ${
|
||||
tankopedia ? 'cached' : 'not cached'
|
||||
}\n**Client ID**: ${getClientId()}\n**Tag**: ${
|
||||
interaction.client.user.tag
|
||||
}`,
|
||||
),
|
||||
infoEmbed(
|
||||
`${client.user?.username} debug information`,
|
||||
cleanTable([
|
||||
['Version', packageJSON.version],
|
||||
['Client ID', getClientId()],
|
||||
['Tag', interaction.client.user.tag],
|
||||
[
|
||||
'Uptime',
|
||||
`${Math.floor(uptime / 1000 / 60 / 60 / 24)}d ${Math.floor(
|
||||
(uptime / 1000 / 60 / 60) % 24,
|
||||
)}h ${Math.floor((uptime / 1000 / 60) % 60)}m ${Math.floor(
|
||||
(uptime / 1000) % 60,
|
||||
)}s ${Math.floor(uptime % 1000)}ms`,
|
||||
],
|
||||
['Tankopedia cached', `${tankopedia !== undefined}`],
|
||||
['Tank averages cached', `${tankAverages !== undefined}`],
|
||||
]),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import markdownEscape from 'markdown-escape';
|
||||
import usernameAutocomplete from '../core/autocomplete/username.js';
|
||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import { TANK_TYPE_EMOJIS, tankopedia } from '../core/blitzstars/tankopedia.js';
|
||||
import resolveTankName from '../core/blitz/resolveTankName.js';
|
||||
import { TANK_TYPE_EMOJIS, tankopedia } from '../core/blitz/tankopedia.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||
import addUsernameOption from '../core/options/addUsernameOption.js';
|
||||
@@ -11,7 +12,6 @@ import { args } from '../core/process/args.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||
import { AccountInfo } from '../types/accountInfo.js';
|
||||
import { TanksStats } from '../types/tanksStats.js';
|
||||
import resolveTankName from '../utilities/resolveTankName.js';
|
||||
|
||||
const COMP_TANKS = [
|
||||
// light tanks
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 { usernameRegex } from '../core/blitz/listAccountsPanServer.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||
import addServerChoices from '../core/options/addServerChoices.js';
|
||||
@@ -34,13 +35,14 @@ export default {
|
||||
const server = interaction.options.getString('server') as BlitzServer;
|
||||
const name = interaction.options.getString('username')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const normalizedSearch = encodeURIComponent(name.trim());
|
||||
const players =
|
||||
normalizedSearch.length >= 3 && normalizedSearch.length <= 100
|
||||
? await getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${normalizedSearch}&limit=${limit}`,
|
||||
)
|
||||
: [];
|
||||
const trimmedSearch = name.trim();
|
||||
const players = usernameRegex.test(trimmedSearch)
|
||||
? await getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${
|
||||
args['wargaming-application-id']
|
||||
}&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`,
|
||||
)
|
||||
: [];
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import { tankNames } from '../core/blitzstars/tankopedia.js';
|
||||
import { TANK_NAMES } from '../core/blitz/tankopedia.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||
import addTankChoices from '../core/options/addTankChoices.js';
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
async execute(interaction) {
|
||||
const tank = interaction.options.getString('tank')!;
|
||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||
const results = go(tank, tankNames, { limit }).map(
|
||||
const results = go(tank, TANK_NAMES, { limit }).map(
|
||||
(result) => result.target,
|
||||
);
|
||||
|
||||
|
||||
+45
-51
@@ -3,22 +3,25 @@ import {
|
||||
ButtonBuilder,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import GenericStats from '../components/GenericStats/index.js';
|
||||
import GenericAllStats from '../components/GenericAllStats.js';
|
||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||
import TitleBar from '../components/TitleBar.js';
|
||||
import Wrapper from '../components/Wrapper.js';
|
||||
import { BLITZ_SERVERS } from '../constants/servers.js';
|
||||
import fullBlitzStarsStats from '../core/actions/fullBlitzStarsStats.js';
|
||||
import { supportBlitzStars } from '../core/actions/supportBlitzStars.js';
|
||||
import usernameAutocomplete from '../core/autocomplete/username.js';
|
||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||
import getWN8 from '../core/blitz/getWN8.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 getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
|
||||
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
|
||||
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
|
||||
import addStatPeriodChoices, {
|
||||
StatPeriod,
|
||||
statPeriodNames,
|
||||
@@ -27,12 +30,16 @@ import addUsernameOption from '../core/options/addUsernameOption.js';
|
||||
import { args } from '../core/process/args.js';
|
||||
import render from '../core/ui/render.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||
import { AccountInfo, AllStats } from '../types/accountInfo.js';
|
||||
import {
|
||||
AccountInfo,
|
||||
AllStats,
|
||||
SupplementaryStats,
|
||||
} from '../types/accountInfo.js';
|
||||
import { PlayerClanData } from '../types/playerClanData.js';
|
||||
|
||||
export default {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -53,20 +60,41 @@ export default {
|
||||
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${args['wargaming-application-id']}&account_id=${id}&extra=clan`,
|
||||
);
|
||||
let stats: AllStats;
|
||||
let supplementaryStats: SupplementaryStats | undefined;
|
||||
|
||||
if (period === 'career') {
|
||||
stats = accountInfo[id].statistics.all;
|
||||
} else {
|
||||
stats = sumStats(
|
||||
Object.entries(
|
||||
await getTankStatsOverTime(
|
||||
server,
|
||||
id,
|
||||
getPeriodicStart(period),
|
||||
getPeriodNow(),
|
||||
),
|
||||
).map(([, stats]) => stats),
|
||||
const tankStatsOverTime = await getTankStatsOverTime(
|
||||
server,
|
||||
id,
|
||||
getPeriodicStart(period),
|
||||
getPeriodNow(),
|
||||
);
|
||||
const entries = Object.entries(tankStatsOverTime);
|
||||
const totalBattles = entries.reduce(
|
||||
(accumulator, [, stats]) => accumulator + stats.battles,
|
||||
0,
|
||||
);
|
||||
|
||||
supplementaryStats = {
|
||||
tier:
|
||||
entries.reduce((accumulator, [tankIdString, stats]) => {
|
||||
const tankId = Number(tankIdString);
|
||||
const tankTier = tankopedia[tankId].tier;
|
||||
|
||||
return accumulator + tankTier * stats.battles;
|
||||
}, 0) / totalBattles,
|
||||
WN8:
|
||||
entries.reduce((accumulator, [tankIdString, stats]) => {
|
||||
const tankId = Number(tankIdString);
|
||||
const tankWN8 = getWN8(tankAverages[tankId].all, stats);
|
||||
|
||||
return accumulator + tankWN8 * stats.battles;
|
||||
}, 0) / totalBattles,
|
||||
};
|
||||
|
||||
stats = sumStats(entries.map(([, stats]) => stats));
|
||||
}
|
||||
|
||||
const image = await render(
|
||||
@@ -88,43 +116,9 @@ export default {
|
||||
|
||||
{stats.battles === 0 && <NoBattlesInPeriod />}
|
||||
{stats.battles > 0 && (
|
||||
<GenericStats
|
||||
stats={[
|
||||
[
|
||||
'Winrate',
|
||||
`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
|
||||
],
|
||||
[
|
||||
'Survival',
|
||||
`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
],
|
||||
['Accuracy', `${((stats.hits / stats.shots) * 100).toFixed(2)}%`],
|
||||
['Battles', stats.battles],
|
||||
['Wins', stats.wins],
|
||||
['Losses', stats.losses],
|
||||
[
|
||||
'Average damage',
|
||||
(stats.damage_dealt / stats.battles).toFixed(0),
|
||||
],
|
||||
['Average XP', (stats.xp / stats.battles).toFixed(0)],
|
||||
['Average shots', (stats.shots / stats.battles).toFixed(2)],
|
||||
['Average hits', (stats.hits / stats.battles).toFixed(2)],
|
||||
['Average kills', (stats.frags / stats.battles).toFixed(2)],
|
||||
['Average spots', (stats.spotted / stats.battles).toFixed(2)],
|
||||
[
|
||||
'Damage ratio',
|
||||
(stats.damage_dealt / stats.damage_received).toFixed(2),
|
||||
],
|
||||
[
|
||||
'Kills to death ratio',
|
||||
(
|
||||
stats.frags /
|
||||
(stats.battles - stats.survived_battles)
|
||||
).toFixed(2),
|
||||
],
|
||||
]}
|
||||
<GenericAllStats
|
||||
stats={stats}
|
||||
supplementaryStats={supplementaryStats}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+26
-55
@@ -3,26 +3,29 @@ import {
|
||||
ButtonBuilder,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import GenericStats from '../components/GenericStats/index.js';
|
||||
import GenericAllStats from '../components/GenericAllStats.js';
|
||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||
import TitleBar from '../components/TitleBar.js';
|
||||
import Wrapper from '../components/Wrapper.js';
|
||||
import { BLITZ_SERVERS } from '../constants/servers.js';
|
||||
import fullBlitzStarsStats from '../core/actions/fullBlitzStarsStats.js';
|
||||
import { supportBlitzStars } from '../core/actions/supportBlitzStars.js';
|
||||
import tanksAutocomplete from '../core/autocomplete/tanks.js';
|
||||
import usernameAutocomplete from '../core/autocomplete/username.js';
|
||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||
import getWN8 from '../core/blitz/getWN8.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import resolveTankId from '../core/blitz/resolveTankId.js';
|
||||
import resolveTankName from '../core/blitz/resolveTankName.js';
|
||||
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
|
||||
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
|
||||
import getTankStatsOverTime, {
|
||||
emptyAllStats,
|
||||
} from '../core/blitzstars/getTankStatsOverTime.js';
|
||||
import { tankopedia } from '../core/blitzstars/tankopedia.js';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
|
||||
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
|
||||
import addStatPeriodChoices, {
|
||||
StatPeriod,
|
||||
statPeriodNames,
|
||||
@@ -34,11 +37,10 @@ import render from '../core/ui/render.js';
|
||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||
import { AccountInfo, AllStats } from '../types/accountInfo.js';
|
||||
import { TanksStats } from '../types/tanksStats.js';
|
||||
import resolveTankName from '../utilities/resolveTankName.js';
|
||||
|
||||
export default {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -64,28 +66,29 @@ export default {
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
|
||||
);
|
||||
let stats: AllStats;
|
||||
const tankId = Number(tank);
|
||||
|
||||
if (period === 'career') {
|
||||
stats =
|
||||
tankStats[id].find((stats) => stats.tank_id === Number(tank))?.all ??
|
||||
tankStats[id].find((stats) => stats.tank_id === tankId)?.all ??
|
||||
emptyAllStats;
|
||||
} else {
|
||||
stats = (
|
||||
await getTankStatsOverTime(
|
||||
server,
|
||||
id,
|
||||
getPeriodicStart(period),
|
||||
getPeriodNow(),
|
||||
)
|
||||
)[Number(tank)];
|
||||
const tankStatsOverTime = await getTankStatsOverTime(
|
||||
server,
|
||||
id,
|
||||
getPeriodicStart(period),
|
||||
getPeriodNow(),
|
||||
);
|
||||
|
||||
stats = tankStatsOverTime[tankId] ?? emptyAllStats;
|
||||
}
|
||||
|
||||
const image = await render(
|
||||
<Wrapper>
|
||||
<TitleBar
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={`(${resolveTankName(Number(tank))})`}
|
||||
image={tankopedia[Number(tank)].images.normal}
|
||||
nameDiscriminator={`(${resolveTankName(tankId)})`}
|
||||
image={tankopedia[tankId].images.normal}
|
||||
description={`${
|
||||
statPeriodNames[period]
|
||||
} • ${new Date().toDateString()} • ${BLITZ_SERVERS[server]}`}
|
||||
@@ -93,44 +96,12 @@ export default {
|
||||
|
||||
{stats.battles === 0 && <NoBattlesInPeriod />}
|
||||
{stats.battles > 0 && (
|
||||
// TODO: GenericStats generator for AllStats
|
||||
<GenericStats
|
||||
stats={[
|
||||
[
|
||||
'Winrate',
|
||||
`${(100 * (stats.wins / stats.battles)).toFixed(2)}%`,
|
||||
],
|
||||
[
|
||||
'Survival',
|
||||
`${(100 * (stats.survived_battles / stats.battles)).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
],
|
||||
['Accuracy', `${((stats.hits / stats.shots) * 100).toFixed(2)}%`],
|
||||
['Battles', stats.battles],
|
||||
['Wins', stats.wins],
|
||||
['Losses', stats.losses],
|
||||
[
|
||||
'Average damage',
|
||||
(stats.damage_dealt / stats.battles).toFixed(0),
|
||||
],
|
||||
['Average XP', (stats.xp / stats.battles).toFixed(0)],
|
||||
['Average shots', (stats.shots / stats.battles).toFixed(2)],
|
||||
['Average hits', (stats.hits / stats.battles).toFixed(2)],
|
||||
['Average kills', (stats.frags / stats.battles).toFixed(2)],
|
||||
['Average spots', (stats.spotted / stats.battles).toFixed(2)],
|
||||
[
|
||||
'Damage ratio',
|
||||
(stats.damage_dealt / stats.damage_received).toFixed(2),
|
||||
],
|
||||
[
|
||||
'Kills to death ratio',
|
||||
(
|
||||
stats.frags /
|
||||
(stats.battles - stats.survived_battles)
|
||||
).toFixed(2),
|
||||
],
|
||||
]}
|
||||
<GenericAllStats
|
||||
stats={stats}
|
||||
supplementaryStats={{
|
||||
tier: tankopedia[tankId].tier,
|
||||
WN8: getWN8(tankAverages[tankId].all, stats),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+96
-31
@@ -7,18 +7,22 @@ import * as Breakdown from '../components/Breakdown/index.js';
|
||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||
import TitleBar from '../components/TitleBar.js';
|
||||
import Wrapper from '../components/Wrapper.js';
|
||||
import Wrapper, { WrapperSize } from '../components/Wrapper.js';
|
||||
import { BLITZ_SERVERS } from '../constants/servers.js';
|
||||
import fullBlitzStarsStats from '../core/actions/fullBlitzStarsStats.js';
|
||||
import { supportBlitzStars } from '../core/actions/supportBlitzStars.js';
|
||||
import usernameAutocomplete from '../core/autocomplete/username.js';
|
||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||
import getWN8 from '../core/blitz/getWN8.js';
|
||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||
import resolveTankName from '../core/blitz/resolveTankName.js';
|
||||
import sumStats from '../core/blitz/sumStats.js';
|
||||
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
|
||||
import last5AM from '../core/blitzstars/last5AM.js';
|
||||
import { tankopedia } from '../core/blitzstars/tankopedia.js';
|
||||
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||
import cmdName from '../core/interaction/cmdName.js';
|
||||
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
|
||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
|
||||
import addUsernameOption from '../core/options/addUsernameOption.js';
|
||||
import { args } from '../core/process/args.js';
|
||||
import render from '../core/ui/render.js';
|
||||
@@ -26,11 +30,10 @@ import { CommandRegistry } from '../events/interactionCreate.js';
|
||||
import { AccountInfo, AllStats } from '../types/accountInfo.js';
|
||||
import { PlayerClanData } from '../types/playerClanData.js';
|
||||
import { TanksStats } from '../types/tanksStats.js';
|
||||
import resolveTankName from '../utilities/resolveTankName.js';
|
||||
|
||||
export default {
|
||||
inProduction: true,
|
||||
inDevelopment: false,
|
||||
inDevelopment: true,
|
||||
inPublic: true,
|
||||
|
||||
command: new SlashCommandBuilder()
|
||||
@@ -75,36 +78,88 @@ export default {
|
||||
tankStatsOverTime[0] = accumulatedStats;
|
||||
}
|
||||
|
||||
const rows = Object.entries(tankStatsOverTime).map(
|
||||
([tankId, tankStats]) => {
|
||||
const career = careerStats[tankId as unknown as number];
|
||||
const tankStatsOverTimeEntries = Object.entries(tankStatsOverTime);
|
||||
const todayWN8s = tankStatsOverTimeEntries.reduce<Record<number, number>>(
|
||||
(accumulator, [tankIdString, tankStats]) => {
|
||||
const tankId = Number(tankIdString);
|
||||
|
||||
return (
|
||||
<Breakdown.Row
|
||||
key={tankId}
|
||||
name={tankId === '0' ? 'Today' : resolveTankName(Number(tankId))}
|
||||
winrate={tankStats.wins / tankStats.battles}
|
||||
careerWinrate={career.wins / career.battles}
|
||||
WN8={-Infinity}
|
||||
careerWN8={-Infinity}
|
||||
damage={tankStats.damage_dealt / tankStats.battles}
|
||||
careerDamage={career.damage_dealt / career.battles}
|
||||
survival={tankStats.survived_battles / tankStats.battles}
|
||||
careerSurvival={career.survived_battles / career.battles}
|
||||
battles={tankStats.battles}
|
||||
careerBattles={career.battles}
|
||||
icon={
|
||||
tankId === '0'
|
||||
? undefined
|
||||
: tankopedia[tankId as unknown as number].images.normal
|
||||
}
|
||||
/>
|
||||
);
|
||||
return tankId === 0
|
||||
? accumulator
|
||||
: {
|
||||
...accumulator,
|
||||
[tankId]: getWN8(tankAverages[tankId].all, tankStats),
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
const careerWN8s = tankStatsOverTimeEntries.reduce<Record<number, number>>(
|
||||
(accumulator, [tankIdString, tankStats]) => {
|
||||
const tankId = Number(tankIdString);
|
||||
|
||||
return tankId === 0
|
||||
? accumulator
|
||||
: {
|
||||
...accumulator,
|
||||
[tankId]: getWN8(tankAverages[tankId].all, careerStats[tankId]),
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
const todayWN8sEntries = Object.entries(todayWN8s);
|
||||
const careerWN8sEntries = Object.entries(careerWN8s);
|
||||
|
||||
todayWN8s[0] =
|
||||
todayWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, wn8]) =>
|
||||
accumulator + wn8 * tankStatsOverTime[Number(tankIdString)].battles,
|
||||
0,
|
||||
) /
|
||||
todayWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString]) =>
|
||||
accumulator + tankStatsOverTime[Number(tankIdString)].battles,
|
||||
0,
|
||||
);
|
||||
careerWN8s[0] =
|
||||
careerWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString, wn8]) =>
|
||||
accumulator + wn8 * careerStats[Number(tankIdString)].battles,
|
||||
0,
|
||||
) /
|
||||
careerWN8sEntries.reduce(
|
||||
(accumulator, [tankIdString]) =>
|
||||
accumulator + careerStats[Number(tankIdString)].battles,
|
||||
0,
|
||||
);
|
||||
|
||||
const rows = tankStatsOverTimeEntries.map(([tankIdString, tankStats]) => {
|
||||
const tankId = Number(tankIdString);
|
||||
const career = careerStats[tankId];
|
||||
|
||||
return (
|
||||
<Breakdown.Row
|
||||
key={tankId}
|
||||
name={tankId === 0 ? 'Total' : resolveTankName(Number(tankIdString))}
|
||||
winrate={tankStats.wins / tankStats.battles}
|
||||
careerWinrate={career.wins / career.battles}
|
||||
WN8={todayWN8s[tankId]}
|
||||
careerWN8={careerWN8s[tankId]}
|
||||
damage={tankStats.damage_dealt / tankStats.battles}
|
||||
careerDamage={career.damage_dealt / career.battles}
|
||||
survival={tankStats.survived_battles / tankStats.battles}
|
||||
careerSurvival={career.survived_battles / career.battles}
|
||||
battles={tankStats.battles}
|
||||
careerBattles={career.battles}
|
||||
icon={
|
||||
tankId === 0
|
||||
? undefined
|
||||
: tankopedia[tankIdString as unknown as number].images.normal
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const image = await render(
|
||||
<Wrapper>
|
||||
<Wrapper size={WrapperSize.Roomy}>
|
||||
<TitleBar
|
||||
name={accountInfo[id].nickname}
|
||||
nameDiscriminator={
|
||||
@@ -125,6 +180,7 @@ export default {
|
||||
|
||||
<PoweredByBlitzStars />
|
||||
</Wrapper>,
|
||||
WrapperSize.Roomy,
|
||||
);
|
||||
|
||||
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
@@ -133,6 +189,15 @@ export default {
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds:
|
||||
rows.length >= 6
|
||||
? [
|
||||
infoEmbed(
|
||||
'Very large image!',
|
||||
"Don't forget to oom in.",
|
||||
),
|
||||
]
|
||||
: undefined,
|
||||
files: [image],
|
||||
components: [actionRow],
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import getWN8Percentile from '../../../core/blitz/getWN8Percentile.js';
|
||||
import { RowDiscriminator } from './RowDiscriminator.js';
|
||||
import { RowStat } from './RowStat.js';
|
||||
|
||||
@@ -22,8 +23,8 @@ export function Row({
|
||||
careerWinrate,
|
||||
WN8,
|
||||
careerWN8,
|
||||
damage: averageDamage,
|
||||
careerDamage: careerAverageDamage,
|
||||
damage,
|
||||
careerDamage,
|
||||
survival,
|
||||
careerSurvival,
|
||||
battles,
|
||||
@@ -39,11 +40,17 @@ export function Row({
|
||||
career={`${(careerWinrate * 100).toFixed(2)}%`}
|
||||
delta={winrate - careerWinrate}
|
||||
/>
|
||||
<RowStat
|
||||
name="WN8"
|
||||
value={WN8.toFixed(0)}
|
||||
career={careerWN8.toFixed(0)}
|
||||
percentile={getWN8Percentile(WN8)}
|
||||
/>
|
||||
<RowStat
|
||||
name="Damage"
|
||||
value={averageDamage.toFixed(2)}
|
||||
career={careerAverageDamage.toFixed(2)}
|
||||
delta={averageDamage - careerAverageDamage}
|
||||
value={damage.toFixed(2)}
|
||||
career={careerDamage.toFixed(2)}
|
||||
delta={damage - careerDamage}
|
||||
/>
|
||||
<RowStat
|
||||
name="Survival"
|
||||
|
||||
@@ -33,7 +33,7 @@ export function RowDiscriminator({ name, icon }: RowDiscriminatorProps) {
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
textOverflow: 'clip',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { Percentile } from '../../../constants/percentiles.js';
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
import PercentileIndicator from '../../PercentileIndicator.js';
|
||||
|
||||
export interface RowStatProps {
|
||||
name: string;
|
||||
value: string;
|
||||
career: string;
|
||||
delta?: number;
|
||||
percentile?: Percentile;
|
||||
}
|
||||
|
||||
export function RowStat({ name, value, career, delta }: RowStatProps) {
|
||||
export function RowStat({
|
||||
name,
|
||||
value,
|
||||
career,
|
||||
delta,
|
||||
percentile,
|
||||
}: RowStatProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -31,7 +40,7 @@ export function RowStat({ name, value, career, delta }: RowStatProps) {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{delta && (
|
||||
{percentile === undefined && delta !== undefined && delta !== 0 && (
|
||||
<img
|
||||
src={
|
||||
(delta ?? 0) > 0
|
||||
@@ -41,6 +50,9 @@ export function RowStat({ name, value, career, delta }: RowStatProps) {
|
||||
style={{ width: 12, height: 12 }}
|
||||
/>
|
||||
)}
|
||||
{percentile !== undefined && (
|
||||
<PercentileIndicator percentile={percentile} />
|
||||
)}
|
||||
|
||||
<span
|
||||
style={{
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import getWN8Percentile from '../core/blitz/getWN8Percentile.js';
|
||||
import { AllStats, SupplementaryStats } from '../types/accountInfo.js';
|
||||
import GenericStats from './GenericStats/index.js';
|
||||
|
||||
export interface GenericAllStatsProps {
|
||||
stats: AllStats;
|
||||
supplementaryStats?: SupplementaryStats;
|
||||
}
|
||||
|
||||
export default function GenericAllStats({
|
||||
stats,
|
||||
supplementaryStats,
|
||||
}: GenericAllStatsProps) {
|
||||
return (
|
||||
<GenericStats
|
||||
stats={[
|
||||
['Winrate', `${(100 * (stats.wins / stats.battles)).toFixed(2)}%`],
|
||||
[
|
||||
'WN8',
|
||||
supplementaryStats?.WN8.toFixed(0),
|
||||
supplementaryStats
|
||||
? getWN8Percentile(supplementaryStats.WN8)
|
||||
: undefined,
|
||||
],
|
||||
[
|
||||
'Survival',
|
||||
`${(100 * (stats.survived_battles / stats.battles)).toFixed(2)}%`,
|
||||
],
|
||||
['Accuracy', `${((stats.hits / stats.shots) * 100).toFixed(2)}%`],
|
||||
['Battles', stats.battles],
|
||||
['Wins', stats.wins],
|
||||
['Losses', stats.losses],
|
||||
['Average damage', (stats.damage_dealt / stats.battles).toFixed(0)],
|
||||
['Average tier', supplementaryStats?.tier.toFixed(2)],
|
||||
['Average XP', (stats.xp / stats.battles).toFixed(0)],
|
||||
['Average shots', (stats.shots / stats.battles).toFixed(2)],
|
||||
['Average hits', (stats.hits / stats.battles).toFixed(2)],
|
||||
['Average kills', (stats.frags / stats.battles).toFixed(2)],
|
||||
['Average spots', (stats.spotted / stats.battles).toFixed(2)],
|
||||
[
|
||||
'Damage ratio',
|
||||
(stats.damage_dealt / stats.damage_received).toFixed(2),
|
||||
],
|
||||
[
|
||||
'Kills to death ratio',
|
||||
(stats.frags / (stats.battles - stats.survived_battles)).toFixed(2),
|
||||
],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { theme } from '../../../stitches.config.js';
|
||||
import { Stat } from '../index.js';
|
||||
|
||||
import '../../../core/blitz/getWN8Percentile.js';
|
||||
import PercentileIndicator from '../../PercentileIndicator.js';
|
||||
|
||||
export interface GenericStatsRowProps {
|
||||
stat: Stat;
|
||||
}
|
||||
@@ -17,7 +20,18 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||
{stat[0]}
|
||||
</span>
|
||||
<span style={{ color: theme.colors.textHighContrast, fontWeight: 'bold', fontSize: 16 }}>
|
||||
<span
|
||||
style={{
|
||||
color: theme.colors.textHighContrast,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{stat[2] !== undefined && <PercentileIndicator percentile={stat[2]} />}
|
||||
{stat[1]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Percentile } from '../../constants/percentiles.js';
|
||||
import GenericStatsColumn from './components/GenericStatsColumn.js';
|
||||
|
||||
export type Stat = [string, string | number];
|
||||
export type Stat =
|
||||
| [string, string | number | undefined]
|
||||
| [string, string | number | undefined, Percentile | undefined];
|
||||
|
||||
export interface GenericStatsProps {
|
||||
stats: Stat[];
|
||||
@@ -11,7 +14,7 @@ export default function GenericStats({
|
||||
stats,
|
||||
columnCount = 2,
|
||||
}: GenericStatsProps) {
|
||||
const filteredStats = stats.filter(([, value]) => value !== `${-Infinity}`);
|
||||
const filteredStats = stats.filter(([, value]) => value !== undefined);
|
||||
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
|
||||
const columns: Stat[][] = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
blue,
|
||||
green,
|
||||
lime,
|
||||
orange,
|
||||
pink,
|
||||
purple,
|
||||
red,
|
||||
slate,
|
||||
teal,
|
||||
yellow,
|
||||
} from '@radix-ui/colors';
|
||||
import { Percentile } from '../constants/percentiles.js';
|
||||
|
||||
export interface PercentileIndicatorProps {
|
||||
percentile: Percentile;
|
||||
}
|
||||
|
||||
export const PERCENTILE_COLORS = {
|
||||
[Percentile.VeryBad]: slate.slate9,
|
||||
[Percentile.Bad]: red.red9,
|
||||
[Percentile.BelowAverage]: orange.orange9,
|
||||
[Percentile.Average]: yellow.yellow9,
|
||||
[Percentile.AboveAverage]: lime.lime9,
|
||||
[Percentile.Good]: green.green9,
|
||||
[Percentile.VeryGood]: teal.teal9,
|
||||
[Percentile.Great]: blue.blue9,
|
||||
[Percentile.Unicum]: pink.pink9,
|
||||
[Percentile.SuperUnicum]: purple.purple9,
|
||||
};
|
||||
|
||||
export default function PercentileIndicator({
|
||||
percentile,
|
||||
}: PercentileIndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: PERCENTILE_COLORS[percentile],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default function TitleBar({
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8, flex: 1 }}>
|
||||
@@ -37,6 +37,7 @@ export default function TitleBar({
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@@ -44,8 +45,6 @@ export default function TitleBar({
|
||||
fontSize: 32,
|
||||
color: theme.colors.textHighContrast,
|
||||
fontWeight: 900,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
@@ -58,8 +57,6 @@ export default function TitleBar({
|
||||
fontSize: 32,
|
||||
color: theme.colors.textLowContrast,
|
||||
fontWeight: 900,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { theme } from '../stitches.config.js';
|
||||
|
||||
export interface WrapperProps {
|
||||
children: ReactNode;
|
||||
export enum WrapperSize {
|
||||
Regular,
|
||||
Roomy,
|
||||
}
|
||||
|
||||
export default function Wrapper({ children }: WrapperProps) {
|
||||
export interface WrapperProps {
|
||||
children: ReactNode;
|
||||
size?: WrapperSize;
|
||||
}
|
||||
|
||||
export const WRAPPER_SIZE_WIDTHS: Record<WrapperSize, number> = {
|
||||
[WrapperSize.Regular]: 640,
|
||||
[WrapperSize.Roomy]: 800,
|
||||
};
|
||||
|
||||
export default function Wrapper({
|
||||
children,
|
||||
size = WrapperSize.Regular,
|
||||
}: WrapperProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -13,7 +27,7 @@ export default function Wrapper({ children }: WrapperProps) {
|
||||
flexDirection: 'column',
|
||||
padding: 16,
|
||||
gap: 32,
|
||||
width: 640,
|
||||
width: WRAPPER_SIZE_WIDTHS[size],
|
||||
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||
color: theme.colors.textHighContrast,
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// http://www.wotinfo.net/en/wn8scale
|
||||
export enum Percentile {
|
||||
VeryBad,
|
||||
Bad,
|
||||
BelowAverage,
|
||||
Average,
|
||||
AboveAverage,
|
||||
Good,
|
||||
VeryGood,
|
||||
Great,
|
||||
Unicum,
|
||||
SuperUnicum,
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import resolveTankName from '../../utilities/resolveTankName.js';
|
||||
import { tanks } from '../blitzstars/tankopedia.js';
|
||||
import resolveTankName from '../blitz/resolveTankName.js';
|
||||
import { TANKS } from '../blitz/tankopedia.js';
|
||||
|
||||
export default async function tanksAutocomplete(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
@@ -15,7 +15,7 @@ export default async function tanksAutocomplete(
|
||||
|
||||
await interaction.respond(
|
||||
focusedOption.value
|
||||
? go(focusedOption.value, tanks, { keys: ['name'], limit: 10 }).map(
|
||||
? go(focusedOption.value, TANKS, { keys: ['name'], limit: 10 }).map(
|
||||
(item) =>
|
||||
({
|
||||
name: item.obj.name
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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) {
|
||||
const avgDmg = average.damage_dealt / average.battles;
|
||||
const avgSpot = average.spotted / average.battles;
|
||||
const avgFrag = average.frags / average.battles;
|
||||
const avgDef = average.dropped_capture_points / average.battles;
|
||||
const avgWinRate = average.wins / average.battles;
|
||||
|
||||
const expDmg = expected.damage_dealt / expected.battles;
|
||||
const expSpot = expected.spotted / expected.battles;
|
||||
const expFrag = expected.frags / expected.battles;
|
||||
const expDef = expected.dropped_capture_points / expected.battles;
|
||||
const expWinRate = expected.wins / expected.battles;
|
||||
|
||||
const rDAMAGE = avgDmg / expDmg;
|
||||
const rSPOT = avgSpot / expSpot;
|
||||
const rFRAG = avgFrag / expFrag;
|
||||
const rDEF = avgDef / expDef;
|
||||
const rWIN = avgWinRate / expWinRate;
|
||||
|
||||
const rWINc = Math.max(0, (rWIN - 0.71) / (1 - 0.71));
|
||||
const rDAMAGEc = Math.max(0, (rDAMAGE - 0.22) / (1 - 0.22));
|
||||
const rFRAGc = Math.max(
|
||||
0,
|
||||
Math.min(rDAMAGEc + 0.2, (rFRAG - 0.12) / (1 - 0.12)),
|
||||
);
|
||||
const rSPOTc = Math.max(
|
||||
0,
|
||||
Math.min(rDAMAGEc + 0.1, (rSPOT - 0.38) / (1 - 0.38)),
|
||||
);
|
||||
const rDEFc = Math.max(0, Math.min(rDAMAGEc + 0.1, (rDEF - 0.1) / (1 - 0.1)));
|
||||
|
||||
const WN8 =
|
||||
980 * rDAMAGEc +
|
||||
210 * rDAMAGEc * rFRAGc +
|
||||
155 * rFRAGc * rSPOTc +
|
||||
75 * rDEFc * rFRAGc +
|
||||
145 * Math.min(1.8, rWINc);
|
||||
|
||||
return WN8;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Percentile } from '../../constants/percentiles.js';
|
||||
|
||||
const WN8_PERCENTILES: [number, Percentile][] = [
|
||||
[0, Percentile.VeryBad],
|
||||
[300, Percentile.Bad],
|
||||
[450, Percentile.BelowAverage],
|
||||
[650, Percentile.Average],
|
||||
[900, Percentile.AboveAverage],
|
||||
[1200, Percentile.Good],
|
||||
[1600, Percentile.VeryGood],
|
||||
[2000, Percentile.Great],
|
||||
[2450, Percentile.Unicum],
|
||||
[2900, Percentile.SuperUnicum],
|
||||
];
|
||||
|
||||
export default function getWN8Percentile(WN8: number) {
|
||||
const lastIndex = WN8_PERCENTILES.findLastIndex(
|
||||
([WN8Listing]) => WN8Listing <= WN8,
|
||||
);
|
||||
return WN8_PERCENTILES[lastIndex === -1 ? 0 : lastIndex][1];
|
||||
}
|
||||
@@ -7,15 +7,18 @@ export type AccountListWithServer = (Account & {
|
||||
server: 'com' | 'eu' | 'asia';
|
||||
})[];
|
||||
|
||||
export default async function listAccountsPanServer(search: string, limit = 9) {
|
||||
const normalizedSearch = encodeURIComponent(search.trim());
|
||||
const normalizedLimit = Math.round(limit / 3);
|
||||
export const usernameRegex = /^[a-zA-Z0-9_]{3,24}$/;
|
||||
|
||||
if (normalizedSearch.length >= 3 && normalizedSearch.length <= 100) {
|
||||
export default async function listAccountsPanServer(search: string, limit = 9) {
|
||||
const trimmedSearch = search.trim();
|
||||
const normalizedLimit = Math.round(limit / 3);
|
||||
const encodedSearch = encodeURIComponent(trimmedSearch);
|
||||
|
||||
if (usernameRegex.test(trimmedSearch)) {
|
||||
return (
|
||||
await Promise.all([
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.com/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.com/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
@@ -25,7 +28,7 @@ export default async function listAccountsPanServer(search: string, limit = 9) {
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.eu/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.eu/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
@@ -35,7 +38,7 @@ export default async function listAccountsPanServer(search: string, limit = 9) {
|
||||
})),
|
||||
),
|
||||
getWargamingResponse<AccountList>(
|
||||
`https://api.wotblitz.asia/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${search}&limit=${normalizedLimit}`,
|
||||
`https://api.wotblitz.asia/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${encodedSearch}&limit=${normalizedLimit}`,
|
||||
).then(
|
||||
(value) =>
|
||||
value &&
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import { tankopedia, tanks } from '../blitzstars/tankopedia.js';
|
||||
import errorEmbed from '../interaction/errorEmbed.js';
|
||||
import { TANKS, tankopedia } from './tankopedia.js';
|
||||
|
||||
export default async function resolveTankId(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
@@ -10,7 +10,7 @@ export default async function resolveTankId(
|
||||
const number = Number(tank);
|
||||
|
||||
if (Number.isNaN(number)) {
|
||||
const searchResult = go(tank, tanks, { keys: ['name'], limit: 1 });
|
||||
const searchResult = go(tank, TANKS, { keys: ['name'], limit: 1 });
|
||||
|
||||
if (searchResult.length === 0) {
|
||||
await interaction.editReply({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { tankopedia } from '../core/blitzstars/tankopedia.js';
|
||||
import { tankopedia } from './tankopedia.js';
|
||||
|
||||
export default function resolveTankName(tankId: number) {
|
||||
const tankopediaName = tankopedia[tankId].name;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { WargamingResponse } from '../../types/wargamingResponse.js';
|
||||
import isDev from '../process/isDev.js';
|
||||
import URLToBase64 from '../ui/URLToBase64.js';
|
||||
|
||||
/*
|
||||
"1": {
|
||||
"name": "T-34",
|
||||
"nation": "ussr",
|
||||
"is_premium": false,
|
||||
"tier": 5,
|
||||
"cost": {
|
||||
"price_credit": 400000,
|
||||
"price_gold": 0
|
||||
},
|
||||
"images": {
|
||||
"preview": "/api/bs-cache/tank-icons/https%3A%2F%2Fglossary-eu-static.gcdn.co%2Ficons%2Fwotb%2Fcurrent%2Fuploaded%2Fvehicles%2Fhd_thumbnail%2FT-34.png",
|
||||
"normal": "http://glossary-eu-static.gcdn.co/icons/wotb/current/uploaded/vehicles/hd/T-34.png"
|
||||
},
|
||||
"tank_id": 1,
|
||||
"type": "mediumTank",
|
||||
"description": "The legend of the Soviet armored forces and the most widely-produced Soviet tank of World War II, with a total of 33,805 vehicles manufactured. Three variants of this model were produced at several Soviet factories from 1940 through 1944."
|
||||
},
|
||||
*/
|
||||
|
||||
export interface Tankopedia {
|
||||
[id: number]: {
|
||||
name: string;
|
||||
nation: string;
|
||||
is_premium: boolean;
|
||||
tier: number;
|
||||
cost: { price_credit: number; price_gold: number };
|
||||
images: { preview: string; normal: string };
|
||||
tank_id: number;
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Caching tankopedia...');
|
||||
export const tankopedia = (
|
||||
(await (
|
||||
await fetch('https://www.blitzstars.com/bs-tankopedia.json')
|
||||
).json()) as WargamingResponse<Tankopedia> & { status: 'ok' }
|
||||
).data;
|
||||
console.log('Cached tankopedia');
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const TANK_TYPE_EMOJIS: Record<string, string> = {
|
||||
'AT-SPG': '🔽',
|
||||
heavyTank: '🇭',
|
||||
mediumTank: '🇲',
|
||||
lightTank: '🇱',
|
||||
};
|
||||
|
||||
export const TANK_IDS = Object.keys(tankopedia).map((id) => Number(id));
|
||||
export const TANKS = TANK_IDS.map((id) => tankopedia[id]);
|
||||
export const TANK_NAMES = TANKS.map((tank) => tank.name);
|
||||
|
||||
if (!isDev()) {
|
||||
console.log('Encoding tank images...');
|
||||
await Promise.all(
|
||||
Object.entries(tankopedia).map(async ([, tank]) => {
|
||||
tank.images.normal = await URLToBase64(tank.images.normal);
|
||||
}),
|
||||
);
|
||||
console.log('Encoded tank images');
|
||||
}
|
||||
@@ -3,8 +3,16 @@ import { StatPeriod } from '../options/addStatPeriodChoices.js';
|
||||
export default function getPeriodicStart(period: StatPeriod) {
|
||||
if (period === 'career') {
|
||||
return -Infinity;
|
||||
} else if (period === 'today') {
|
||||
const now = new Date();
|
||||
now.setHours(5, 0, 0, 0);
|
||||
|
||||
// If it's after 5:00 AM today, subtract one day
|
||||
if (now.getTime() > Date.now()) now.setDate(now.getDate() - 1);
|
||||
|
||||
return now.getTime() / 1000;
|
||||
} else {
|
||||
const daysAgo = period === 'today' ? 0 : Number(period) - 1;
|
||||
const daysAgo = Number(period) - 1;
|
||||
const periodStart = new Date();
|
||||
|
||||
periodStart.setDate(periodStart.getDate() - daysAgo);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
import { PlayerStats } from '../../types/playerStats.js';
|
||||
|
||||
export default async function getPlayerStats(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
command: string,
|
||||
id: number,
|
||||
callback: (playerStats: PlayerStats) => void,
|
||||
) {
|
||||
fetch(`https://www.blitzstars.com/api/playerstats/${id}`).then(
|
||||
async (response) => {
|
||||
callback((await response.json()) as PlayerStats);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -47,8 +47,8 @@ export type TankHistory = TankHistoryNode[];
|
||||
export default async function getTankStatsOverTime(
|
||||
server: BlitzServer,
|
||||
id: number,
|
||||
start = -Infinity,
|
||||
end = Infinity,
|
||||
start: number,
|
||||
end: number,
|
||||
) {
|
||||
const latestTankStatsRaw = await getWargamingResponse<TanksStats>(
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import sumStats from '../blitz/sumStats.js';
|
||||
import { emptyAllStats } from './getTankStatsOverTime.js';
|
||||
import { tankAverages } from './tankAverages.js';
|
||||
|
||||
console.log('Calculating player average...');
|
||||
export const playerAverage = Object.entries(tankAverages).reduce(
|
||||
(accumulator, [, tankAverage]) => {
|
||||
return sumStats([accumulator, tankAverage.all]);
|
||||
},
|
||||
emptyAllStats,
|
||||
);
|
||||
console.log('Calculated player average');
|
||||
@@ -0,0 +1,90 @@
|
||||
import { AllStats } from '../../types/accountInfo.js';
|
||||
|
||||
export interface SpecialStats {
|
||||
winrate: number;
|
||||
damageRatio: number;
|
||||
kdr: number;
|
||||
damagePerBattle: number;
|
||||
killsPerBattle: number;
|
||||
hitsPerBattle: number;
|
||||
spotsPerBattle: number;
|
||||
wpm: number;
|
||||
dpm: number;
|
||||
kpm: number;
|
||||
hitRate: number;
|
||||
survivalRate: number;
|
||||
}
|
||||
|
||||
export type Percentiles = [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
|
||||
export interface SpecialPercentiles {
|
||||
winRate: Percentiles;
|
||||
damageRatio: Percentiles;
|
||||
kdr: Percentiles;
|
||||
damagePerBattle: Percentiles;
|
||||
killsPerBattle: Percentiles;
|
||||
hitsPerBattle: Percentiles;
|
||||
spotsPerBattle: Percentiles;
|
||||
wpm: Percentiles;
|
||||
dpm: Percentiles;
|
||||
kpm: Percentiles;
|
||||
hitRate: Percentiles;
|
||||
survivalRate: Percentiles;
|
||||
}
|
||||
|
||||
export interface MeanSd {
|
||||
winrateMean: number;
|
||||
winrateSd: number;
|
||||
hitRateMean: number;
|
||||
hitRateSd: number;
|
||||
survivalRateMean: number;
|
||||
survivalRateSd: number;
|
||||
damageRatioMean: number;
|
||||
damageRatioSd: number;
|
||||
kdrMean: number;
|
||||
kdrSd: number;
|
||||
dpbMean: number;
|
||||
dpbSd: number;
|
||||
kpbMean: number;
|
||||
kpbSd: number;
|
||||
hpbMean: number;
|
||||
hpbSd: number;
|
||||
spbMean: number;
|
||||
spbSd: number;
|
||||
}
|
||||
|
||||
export interface IndividualTankAverages {
|
||||
_id: number;
|
||||
battle_life_time: number;
|
||||
total_battle_life_time: number;
|
||||
number_of_players: number;
|
||||
tank_id: number;
|
||||
all: AllStats;
|
||||
special: SpecialStats;
|
||||
percentiles: SpecialPercentiles;
|
||||
percentiles_player_count: number;
|
||||
meanSd: MeanSd;
|
||||
shotEff: number;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
console.log('Caching tank averages...');
|
||||
const tankAveragesRaw = (await (
|
||||
await fetch('https://www.blitzstars.com/api/tankaverages.json')
|
||||
).json()) as IndividualTankAverages[];
|
||||
export const tankAverages: Record<number, IndividualTankAverages> = {};
|
||||
|
||||
Object.entries(tankAveragesRaw).forEach(([, tank]) => {
|
||||
tankAverages[tank.tank_id] = tank;
|
||||
});
|
||||
console.log('Tank averages cached');
|
||||
@@ -1,75 +0,0 @@
|
||||
import isDev from '../process/isDev.js';
|
||||
|
||||
export type TankType = 'heavyTank' | 'AT-SPG' | 'mediumTank' | 'lightTank';
|
||||
|
||||
export interface Tankopedia {
|
||||
status: 'ok'; // always will be ok since it is cached
|
||||
meta: {
|
||||
count: number;
|
||||
};
|
||||
data: {
|
||||
[tankId: number]: {
|
||||
name?: string;
|
||||
nation: string;
|
||||
is_premium: boolean;
|
||||
tier: number;
|
||||
cost: {
|
||||
price_credit: number;
|
||||
price_gold: number;
|
||||
};
|
||||
images: {
|
||||
preview: string;
|
||||
normal?: string;
|
||||
};
|
||||
tank_id: number;
|
||||
type: TankType;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Caching tankopedia...');
|
||||
export const tankopedia = (
|
||||
(await (
|
||||
await fetch('https://www.blitzstars.com/bs-tankopedia.json')
|
||||
).json()) as Tankopedia
|
||||
).data;
|
||||
console.log('Cached tankopedia');
|
||||
|
||||
export const tankIds = Object.keys(tankopedia) as unknown as number[];
|
||||
|
||||
export const tankNames = tankIds.map((id) => tankopedia[id].name);
|
||||
|
||||
export const tanks = tankIds.map((id) => tankopedia[id]);
|
||||
|
||||
export const TANK_TYPE_NAMES: Record<TankType, string> = {
|
||||
'AT-SPG': 'Tank Destroyer',
|
||||
heavyTank: 'Heavy Tank',
|
||||
mediumTank: 'Medium Tank',
|
||||
lightTank: 'Light Tank',
|
||||
};
|
||||
|
||||
export const TANK_TYPE_EMOJIS: Record<TankType, string> = {
|
||||
'AT-SPG': '🔽',
|
||||
heavyTank: '🇭',
|
||||
mediumTank: '🇲',
|
||||
lightTank: '🇱',
|
||||
};
|
||||
|
||||
if (!isDev()) {
|
||||
console.log('Encoding tank images...');
|
||||
await Promise.all(
|
||||
Object.entries(tankopedia).map(async ([, tank]) => {
|
||||
if (tank.images.normal) {
|
||||
const response = await fetch(tank.images.normal);
|
||||
const blob = await response.arrayBuffer();
|
||||
const base64 = `data:${response.headers.get(
|
||||
'content-type',
|
||||
)};base64,${Buffer.from(blob).toString('base64')}`;
|
||||
|
||||
tank.images.normal = base64;
|
||||
}
|
||||
}),
|
||||
);
|
||||
console.log('Encoded tank images');
|
||||
}
|
||||
@@ -3,7 +3,7 @@ export type TableInput = TableInputEntry[];
|
||||
|
||||
export default function cleanTable(input: TableInput) {
|
||||
return input
|
||||
.filter((array) => array[1] !== `${-Infinity}`)
|
||||
.filter((array) => array[1] !== undefined)
|
||||
.map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`))
|
||||
.join('\n');
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,9 @@
|
||||
export default async function URLToBase64(URL: string) {
|
||||
const response = await fetch(URL);
|
||||
const blob = await response.arrayBuffer();
|
||||
const base64 = `data:${response.headers.get(
|
||||
'content-type',
|
||||
)};base64,${Buffer.from(blob).toString('base64')}`;
|
||||
|
||||
return base64;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Resvg } from '@resvg/resvg-js';
|
||||
import { readFile } from 'fs';
|
||||
import { ReactNode } from 'react';
|
||||
import satori from 'satori';
|
||||
import { WRAPPER_SIZE_WIDTHS, WrapperSize } from '../../components/Wrapper.js';
|
||||
|
||||
const FONT_NAME = 'Roboto';
|
||||
const FONT_FILES = ['Roboto', 'Roboto-Bold', 'Roboto-Black'];
|
||||
@@ -20,9 +21,12 @@ const [robotoFlex, robotoFlexBold, robotoFlexBlack] = await Promise.all(
|
||||
);
|
||||
console.log('Fonts imported');
|
||||
|
||||
export default async function render(element: ReactNode) {
|
||||
export default async function render(
|
||||
element: ReactNode,
|
||||
size: WrapperSize = WrapperSize.Regular,
|
||||
) {
|
||||
const svg = await satori(element, {
|
||||
width: 640,
|
||||
width: WRAPPER_SIZE_WIDTHS[size],
|
||||
fonts: [
|
||||
{ data: robotoFlex, name: FONT_NAME, weight: 400 },
|
||||
{ data: robotoFlexBold, name: FONT_NAME, weight: 700 },
|
||||
|
||||
@@ -18,16 +18,9 @@ export interface AllStats {
|
||||
dropped_capture_points: number;
|
||||
}
|
||||
|
||||
export interface SpecialStats {
|
||||
winrate: number;
|
||||
damageRatio: number;
|
||||
kdr: number;
|
||||
dpb: number;
|
||||
kpb: number;
|
||||
hpb: number;
|
||||
spb: number;
|
||||
hitRate: number;
|
||||
survivalRate: number;
|
||||
export interface SupplementaryStats {
|
||||
WN8: number;
|
||||
tier: number;
|
||||
}
|
||||
|
||||
export interface ClanStats {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { BlitzStartsComputedPeriodicStatistics } from './statistics.js';
|
||||
|
||||
export type PlayerStats = {
|
||||
clan: {
|
||||
clan_id: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
};
|
||||
statistics: BlitzStartsComputedPeriodicStatistics;
|
||||
_id: string;
|
||||
last_battle_time: number;
|
||||
nickname: string;
|
||||
account_id: number;
|
||||
achievements: {
|
||||
account_id: string;
|
||||
medalLafayettePool: number;
|
||||
medalRadleyWalters: number;
|
||||
warrior: number;
|
||||
jointVictoryCount: number;
|
||||
};
|
||||
region: string;
|
||||
__v: number;
|
||||
}[];
|
||||
@@ -1,30 +0,0 @@
|
||||
import { AllStats, SpecialStats } from './accountInfo.js';
|
||||
|
||||
export interface BlitzStartsComputedPeriodicStatistics {
|
||||
all: AllStats;
|
||||
|
||||
wn7: number;
|
||||
wn8: number;
|
||||
avg_tier: number;
|
||||
}
|
||||
|
||||
export interface PeriodStatistics
|
||||
extends BlitzStartsComputedPeriodicStatistics {
|
||||
special: SpecialStats;
|
||||
pwp: number;
|
||||
}
|
||||
|
||||
export interface PlayerPeriodicStatsCollection {
|
||||
statistics: PeriodStatistics;
|
||||
period30d: PeriodStatistics;
|
||||
period90d: PeriodStatistics;
|
||||
|
||||
_id: string;
|
||||
nickname: string;
|
||||
nickname_lowercase: string;
|
||||
region: string;
|
||||
last_battle_time: number;
|
||||
__v: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
Reference in new issue
Block a user