mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +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",
|
"Smri",
|
||||||
"strvk",
|
"strvk",
|
||||||
"tankopedia",
|
"tankopedia",
|
||||||
|
"Unicum",
|
||||||
"Unregistering",
|
"Unregistering",
|
||||||
"vickers",
|
"vickers",
|
||||||
"wargaming",
|
"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 packageJSON from '../../package.json' assert { type: 'json' };
|
||||||
import { ACCENT_COLOR } from '../constants/colors.js';
|
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||||
import { tankopedia } from '../core/blitzstars/tankopedia.js';
|
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||||
|
import cleanTable from '../core/interaction/cleanTable.js';
|
||||||
import cmdName from '../core/interaction/cmdName.js';
|
import cmdName from '../core/interaction/cmdName.js';
|
||||||
|
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||||
import getClientId from '../core/process/getClientId.js';
|
import getClientId from '../core/process/getClientId.js';
|
||||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||||
import { client } from '../index.js';
|
import { client } from '../index.js';
|
||||||
@@ -24,22 +26,24 @@ export default {
|
|||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
infoEmbed(
|
||||||
.setTitle(`${client.user?.username} debug information`)
|
`${client.user?.username} debug information`,
|
||||||
.setColor(ACCENT_COLOR)
|
cleanTable([
|
||||||
.setDescription(
|
['Version', packageJSON.version],
|
||||||
`**Version**: ${packageJSON.version}\n**Uptime**: ${Math.floor(
|
['Client ID', getClientId()],
|
||||||
uptime / 1000 / 60 / 60 / 24,
|
['Tag', interaction.client.user.tag],
|
||||||
)}d ${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor(
|
[
|
||||||
(uptime / 1000 / 60) % 60,
|
'Uptime',
|
||||||
)}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor(
|
`${Math.floor(uptime / 1000 / 60 / 60 / 24)}d ${Math.floor(
|
||||||
uptime % 1000,
|
(uptime / 1000 / 60 / 60) % 24,
|
||||||
)}ms\n**Tankopedia**: ${
|
)}h ${Math.floor((uptime / 1000 / 60) % 60)}m ${Math.floor(
|
||||||
tankopedia ? 'cached' : 'not cached'
|
(uptime / 1000) % 60,
|
||||||
}\n**Client ID**: ${getClientId()}\n**Tag**: ${
|
)}s ${Math.floor(uptime % 1000)}ms`,
|
||||||
interaction.client.user.tag
|
],
|
||||||
}`,
|
['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 usernameAutocomplete from '../core/autocomplete/username.js';
|
||||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.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 cmdName from '../core/interaction/cmdName.js';
|
||||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||||
import addUsernameOption from '../core/options/addUsernameOption.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 { CommandRegistry } from '../events/interactionCreate.js';
|
||||||
import { AccountInfo } from '../types/accountInfo.js';
|
import { AccountInfo } from '../types/accountInfo.js';
|
||||||
import { TanksStats } from '../types/tanksStats.js';
|
import { TanksStats } from '../types/tanksStats.js';
|
||||||
import resolveTankName from '../utilities/resolveTankName.js';
|
|
||||||
|
|
||||||
const COMP_TANKS = [
|
const COMP_TANKS = [
|
||||||
// light tanks
|
// light tanks
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
|
|||||||
import markdownEscape from 'markdown-escape';
|
import markdownEscape from 'markdown-escape';
|
||||||
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
|
import { BLITZ_SERVERS, BlitzServer } from '../constants/servers.js';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||||
|
import { usernameRegex } from '../core/blitz/listAccountsPanServer.js';
|
||||||
import cmdName from '../core/interaction/cmdName.js';
|
import cmdName from '../core/interaction/cmdName.js';
|
||||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||||
import addServerChoices from '../core/options/addServerChoices.js';
|
import addServerChoices from '../core/options/addServerChoices.js';
|
||||||
@@ -34,13 +35,14 @@ export default {
|
|||||||
const server = interaction.options.getString('server') as BlitzServer;
|
const server = interaction.options.getString('server') as BlitzServer;
|
||||||
const name = interaction.options.getString('username')!;
|
const name = interaction.options.getString('username')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
const limit = interaction.options.getInteger('limit') ?? 25;
|
||||||
const normalizedSearch = encodeURIComponent(name.trim());
|
const trimmedSearch = name.trim();
|
||||||
const players =
|
const players = usernameRegex.test(trimmedSearch)
|
||||||
normalizedSearch.length >= 3 && normalizedSearch.length <= 100
|
? await getWargamingResponse<AccountList>(
|
||||||
? await getWargamingResponse<AccountList>(
|
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${
|
||||||
`https://api.wotblitz.${server}/wotb/account/list/?application_id=${args['wargaming-application-id']}&search=${normalizedSearch}&limit=${limit}`,
|
args['wargaming-application-id']
|
||||||
)
|
}&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`,
|
||||||
: [];
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import markdownEscape from 'markdown-escape';
|
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 cmdName from '../core/interaction/cmdName.js';
|
||||||
import infoEmbed from '../core/interaction/infoEmbed.js';
|
import infoEmbed from '../core/interaction/infoEmbed.js';
|
||||||
import addTankChoices from '../core/options/addTankChoices.js';
|
import addTankChoices from '../core/options/addTankChoices.js';
|
||||||
@@ -27,7 +27,7 @@ export default {
|
|||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
const tank = interaction.options.getString('tank')!;
|
const tank = interaction.options.getString('tank')!;
|
||||||
const limit = interaction.options.getInteger('limit') ?? 25;
|
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,
|
(result) => result.target,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+45
-51
@@ -3,22 +3,25 @@ import {
|
|||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import GenericStats from '../components/GenericStats/index.js';
|
import GenericAllStats from '../components/GenericAllStats.js';
|
||||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||||
import TitleBar from '../components/TitleBar.js';
|
import TitleBar from '../components/TitleBar.js';
|
||||||
import Wrapper from '../components/Wrapper.js';
|
import Wrapper from '../components/Wrapper.js';
|
||||||
import { BLITZ_SERVERS } from '../constants/servers.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 usernameAutocomplete from '../core/autocomplete/username.js';
|
||||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||||
|
import getWN8 from '../core/blitz/getWN8.js';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||||
import sumStats from '../core/blitz/sumStats.js';
|
import sumStats from '../core/blitz/sumStats.js';
|
||||||
|
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||||
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
|
import getPeriodNow from '../core/blitzstars/getPeriodNow.js';
|
||||||
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
|
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
|
||||||
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
|
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
|
||||||
|
import { tankAverages } from '../core/blitzstars/tankAverages.js';
|
||||||
import cmdName from '../core/interaction/cmdName.js';
|
import cmdName from '../core/interaction/cmdName.js';
|
||||||
|
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
|
||||||
|
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
|
||||||
import addStatPeriodChoices, {
|
import addStatPeriodChoices, {
|
||||||
StatPeriod,
|
StatPeriod,
|
||||||
statPeriodNames,
|
statPeriodNames,
|
||||||
@@ -27,12 +30,16 @@ import addUsernameOption from '../core/options/addUsernameOption.js';
|
|||||||
import { args } from '../core/process/args.js';
|
import { args } from '../core/process/args.js';
|
||||||
import render from '../core/ui/render.js';
|
import render from '../core/ui/render.js';
|
||||||
import { CommandRegistry } from '../events/interactionCreate.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';
|
import { PlayerClanData } from '../types/playerClanData.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
inDevelopment: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
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`,
|
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${args['wargaming-application-id']}&account_id=${id}&extra=clan`,
|
||||||
);
|
);
|
||||||
let stats: AllStats;
|
let stats: AllStats;
|
||||||
|
let supplementaryStats: SupplementaryStats | undefined;
|
||||||
|
|
||||||
if (period === 'career') {
|
if (period === 'career') {
|
||||||
stats = accountInfo[id].statistics.all;
|
stats = accountInfo[id].statistics.all;
|
||||||
} else {
|
} else {
|
||||||
stats = sumStats(
|
const tankStatsOverTime = await getTankStatsOverTime(
|
||||||
Object.entries(
|
server,
|
||||||
await getTankStatsOverTime(
|
id,
|
||||||
server,
|
getPeriodicStart(period),
|
||||||
id,
|
getPeriodNow(),
|
||||||
getPeriodicStart(period),
|
|
||||||
getPeriodNow(),
|
|
||||||
),
|
|
||||||
).map(([, stats]) => stats),
|
|
||||||
);
|
);
|
||||||
|
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(
|
const image = await render(
|
||||||
@@ -88,43 +116,9 @@ export default {
|
|||||||
|
|
||||||
{stats.battles === 0 && <NoBattlesInPeriod />}
|
{stats.battles === 0 && <NoBattlesInPeriod />}
|
||||||
{stats.battles > 0 && (
|
{stats.battles > 0 && (
|
||||||
<GenericStats
|
<GenericAllStats
|
||||||
stats={[
|
stats={stats}
|
||||||
[
|
supplementaryStats={supplementaryStats}
|
||||||
'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),
|
|
||||||
],
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+26
-55
@@ -3,26 +3,29 @@ import {
|
|||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import GenericStats from '../components/GenericStats/index.js';
|
import GenericAllStats from '../components/GenericAllStats.js';
|
||||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||||
import TitleBar from '../components/TitleBar.js';
|
import TitleBar from '../components/TitleBar.js';
|
||||||
import Wrapper from '../components/Wrapper.js';
|
import Wrapper from '../components/Wrapper.js';
|
||||||
import { BLITZ_SERVERS } from '../constants/servers.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 tanksAutocomplete from '../core/autocomplete/tanks.js';
|
||||||
import usernameAutocomplete from '../core/autocomplete/username.js';
|
import usernameAutocomplete from '../core/autocomplete/username.js';
|
||||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||||
|
import getWN8 from '../core/blitz/getWN8.js';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||||
import resolveTankId from '../core/blitz/resolveTankId.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 getPeriodNow from '../core/blitzstars/getPeriodNow.js';
|
||||||
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
|
import getPeriodicStart from '../core/blitzstars/getPeriodStart.js';
|
||||||
import getTankStatsOverTime, {
|
import getTankStatsOverTime, {
|
||||||
emptyAllStats,
|
emptyAllStats,
|
||||||
} from '../core/blitzstars/getTankStatsOverTime.js';
|
} 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 cmdName from '../core/interaction/cmdName.js';
|
||||||
|
import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.js';
|
||||||
|
import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js';
|
||||||
import addStatPeriodChoices, {
|
import addStatPeriodChoices, {
|
||||||
StatPeriod,
|
StatPeriod,
|
||||||
statPeriodNames,
|
statPeriodNames,
|
||||||
@@ -34,11 +37,10 @@ import render from '../core/ui/render.js';
|
|||||||
import { CommandRegistry } from '../events/interactionCreate.js';
|
import { CommandRegistry } from '../events/interactionCreate.js';
|
||||||
import { AccountInfo, AllStats } from '../types/accountInfo.js';
|
import { AccountInfo, AllStats } from '../types/accountInfo.js';
|
||||||
import { TanksStats } from '../types/tanksStats.js';
|
import { TanksStats } from '../types/tanksStats.js';
|
||||||
import resolveTankName from '../utilities/resolveTankName.js';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
inDevelopment: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
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}`,
|
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
|
||||||
);
|
);
|
||||||
let stats: AllStats;
|
let stats: AllStats;
|
||||||
|
const tankId = Number(tank);
|
||||||
|
|
||||||
if (period === 'career') {
|
if (period === 'career') {
|
||||||
stats =
|
stats =
|
||||||
tankStats[id].find((stats) => stats.tank_id === Number(tank))?.all ??
|
tankStats[id].find((stats) => stats.tank_id === tankId)?.all ??
|
||||||
emptyAllStats;
|
emptyAllStats;
|
||||||
} else {
|
} else {
|
||||||
stats = (
|
const tankStatsOverTime = await getTankStatsOverTime(
|
||||||
await getTankStatsOverTime(
|
server,
|
||||||
server,
|
id,
|
||||||
id,
|
getPeriodicStart(period),
|
||||||
getPeriodicStart(period),
|
getPeriodNow(),
|
||||||
getPeriodNow(),
|
);
|
||||||
)
|
|
||||||
)[Number(tank)];
|
stats = tankStatsOverTime[tankId] ?? emptyAllStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await render(
|
const image = await render(
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo[id].nickname}
|
||||||
nameDiscriminator={`(${resolveTankName(Number(tank))})`}
|
nameDiscriminator={`(${resolveTankName(tankId)})`}
|
||||||
image={tankopedia[Number(tank)].images.normal}
|
image={tankopedia[tankId].images.normal}
|
||||||
description={`${
|
description={`${
|
||||||
statPeriodNames[period]
|
statPeriodNames[period]
|
||||||
} • ${new Date().toDateString()} • ${BLITZ_SERVERS[server]}`}
|
} • ${new Date().toDateString()} • ${BLITZ_SERVERS[server]}`}
|
||||||
@@ -93,44 +96,12 @@ export default {
|
|||||||
|
|
||||||
{stats.battles === 0 && <NoBattlesInPeriod />}
|
{stats.battles === 0 && <NoBattlesInPeriod />}
|
||||||
{stats.battles > 0 && (
|
{stats.battles > 0 && (
|
||||||
// TODO: GenericStats generator for AllStats
|
<GenericAllStats
|
||||||
<GenericStats
|
stats={stats}
|
||||||
stats={[
|
supplementaryStats={{
|
||||||
[
|
tier: tankopedia[tankId].tier,
|
||||||
'Winrate',
|
WN8: getWN8(tankAverages[tankId].all, stats),
|
||||||
`${(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),
|
|
||||||
],
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+96
-31
@@ -7,18 +7,22 @@ import * as Breakdown from '../components/Breakdown/index.js';
|
|||||||
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
|
||||||
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
|
||||||
import TitleBar from '../components/TitleBar.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 { 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 usernameAutocomplete from '../core/autocomplete/username.js';
|
||||||
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
import getBlitzAccount from '../core/blitz/getBlitzAccount.js';
|
||||||
|
import getWN8 from '../core/blitz/getWN8.js';
|
||||||
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
import getWargamingResponse from '../core/blitz/getWargamingResponse.js';
|
||||||
|
import resolveTankName from '../core/blitz/resolveTankName.js';
|
||||||
import sumStats from '../core/blitz/sumStats.js';
|
import sumStats from '../core/blitz/sumStats.js';
|
||||||
|
import { tankopedia } from '../core/blitz/tankopedia.js';
|
||||||
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
|
import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.js';
|
||||||
import last5AM from '../core/blitzstars/last5AM.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 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 addUsernameOption from '../core/options/addUsernameOption.js';
|
||||||
import { args } from '../core/process/args.js';
|
import { args } from '../core/process/args.js';
|
||||||
import render from '../core/ui/render.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 { AccountInfo, AllStats } from '../types/accountInfo.js';
|
||||||
import { PlayerClanData } from '../types/playerClanData.js';
|
import { PlayerClanData } from '../types/playerClanData.js';
|
||||||
import { TanksStats } from '../types/tanksStats.js';
|
import { TanksStats } from '../types/tanksStats.js';
|
||||||
import resolveTankName from '../utilities/resolveTankName.js';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inProduction: true,
|
inProduction: true,
|
||||||
inDevelopment: false,
|
inDevelopment: true,
|
||||||
inPublic: true,
|
inPublic: true,
|
||||||
|
|
||||||
command: new SlashCommandBuilder()
|
command: new SlashCommandBuilder()
|
||||||
@@ -75,36 +78,88 @@ export default {
|
|||||||
tankStatsOverTime[0] = accumulatedStats;
|
tankStatsOverTime[0] = accumulatedStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = Object.entries(tankStatsOverTime).map(
|
const tankStatsOverTimeEntries = Object.entries(tankStatsOverTime);
|
||||||
([tankId, tankStats]) => {
|
const todayWN8s = tankStatsOverTimeEntries.reduce<Record<number, number>>(
|
||||||
const career = careerStats[tankId as unknown as number];
|
(accumulator, [tankIdString, tankStats]) => {
|
||||||
|
const tankId = Number(tankIdString);
|
||||||
|
|
||||||
return (
|
return tankId === 0
|
||||||
<Breakdown.Row
|
? accumulator
|
||||||
key={tankId}
|
: {
|
||||||
name={tankId === '0' ? 'Today' : resolveTankName(Number(tankId))}
|
...accumulator,
|
||||||
winrate={tankStats.wins / tankStats.battles}
|
[tankId]: getWN8(tankAverages[tankId].all, tankStats),
|
||||||
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
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
|
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(
|
const image = await render(
|
||||||
<Wrapper>
|
<Wrapper size={WrapperSize.Roomy}>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
name={accountInfo[id].nickname}
|
name={accountInfo[id].nickname}
|
||||||
nameDiscriminator={
|
nameDiscriminator={
|
||||||
@@ -125,6 +180,7 @@ export default {
|
|||||||
|
|
||||||
<PoweredByBlitzStars />
|
<PoweredByBlitzStars />
|
||||||
</Wrapper>,
|
</Wrapper>,
|
||||||
|
WrapperSize.Roomy,
|
||||||
);
|
);
|
||||||
|
|
||||||
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||||
@@ -133,6 +189,15 @@ export default {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
|
embeds:
|
||||||
|
rows.length >= 6
|
||||||
|
? [
|
||||||
|
infoEmbed(
|
||||||
|
'Very large image!',
|
||||||
|
"Don't forget to oom in.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
files: [image],
|
files: [image],
|
||||||
components: [actionRow],
|
components: [actionRow],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import getWN8Percentile from '../../../core/blitz/getWN8Percentile.js';
|
||||||
import { RowDiscriminator } from './RowDiscriminator.js';
|
import { RowDiscriminator } from './RowDiscriminator.js';
|
||||||
import { RowStat } from './RowStat.js';
|
import { RowStat } from './RowStat.js';
|
||||||
|
|
||||||
@@ -22,8 +23,8 @@ export function Row({
|
|||||||
careerWinrate,
|
careerWinrate,
|
||||||
WN8,
|
WN8,
|
||||||
careerWN8,
|
careerWN8,
|
||||||
damage: averageDamage,
|
damage,
|
||||||
careerDamage: careerAverageDamage,
|
careerDamage,
|
||||||
survival,
|
survival,
|
||||||
careerSurvival,
|
careerSurvival,
|
||||||
battles,
|
battles,
|
||||||
@@ -39,11 +40,17 @@ export function Row({
|
|||||||
career={`${(careerWinrate * 100).toFixed(2)}%`}
|
career={`${(careerWinrate * 100).toFixed(2)}%`}
|
||||||
delta={winrate - careerWinrate}
|
delta={winrate - careerWinrate}
|
||||||
/>
|
/>
|
||||||
|
<RowStat
|
||||||
|
name="WN8"
|
||||||
|
value={WN8.toFixed(0)}
|
||||||
|
career={careerWN8.toFixed(0)}
|
||||||
|
percentile={getWN8Percentile(WN8)}
|
||||||
|
/>
|
||||||
<RowStat
|
<RowStat
|
||||||
name="Damage"
|
name="Damage"
|
||||||
value={averageDamage.toFixed(2)}
|
value={damage.toFixed(2)}
|
||||||
career={careerAverageDamage.toFixed(2)}
|
career={careerDamage.toFixed(2)}
|
||||||
delta={averageDamage - careerAverageDamage}
|
delta={damage - careerDamage}
|
||||||
/>
|
/>
|
||||||
<RowStat
|
<RowStat
|
||||||
name="Survival"
|
name="Survival"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function RowDiscriminator({ name, icon }: RowDiscriminatorProps) {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'clip',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
import { Percentile } from '../../../constants/percentiles.js';
|
||||||
import { theme } from '../../../stitches.config.js';
|
import { theme } from '../../../stitches.config.js';
|
||||||
|
import PercentileIndicator from '../../PercentileIndicator.js';
|
||||||
|
|
||||||
export interface RowStatProps {
|
export interface RowStatProps {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
career: string;
|
career: string;
|
||||||
delta?: number;
|
delta?: number;
|
||||||
|
percentile?: Percentile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RowStat({ name, value, career, delta }: RowStatProps) {
|
export function RowStat({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
career,
|
||||||
|
delta,
|
||||||
|
percentile,
|
||||||
|
}: RowStatProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -31,7 +40,7 @@ export function RowStat({ name, value, career, delta }: RowStatProps) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{delta && (
|
{percentile === undefined && delta !== undefined && delta !== 0 && (
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
(delta ?? 0) > 0
|
(delta ?? 0) > 0
|
||||||
@@ -41,6 +50,9 @@ export function RowStat({ name, value, career, delta }: RowStatProps) {
|
|||||||
style={{ width: 12, height: 12 }}
|
style={{ width: 12, height: 12 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{percentile !== undefined && (
|
||||||
|
<PercentileIndicator percentile={percentile} />
|
||||||
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
style={{
|
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 { theme } from '../../../stitches.config.js';
|
||||||
import { Stat } from '../index.js';
|
import { Stat } from '../index.js';
|
||||||
|
|
||||||
|
import '../../../core/blitz/getWN8Percentile.js';
|
||||||
|
import PercentileIndicator from '../../PercentileIndicator.js';
|
||||||
|
|
||||||
export interface GenericStatsRowProps {
|
export interface GenericStatsRowProps {
|
||||||
stat: Stat;
|
stat: Stat;
|
||||||
}
|
}
|
||||||
@@ -17,7 +20,18 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
|
|||||||
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
|
||||||
{stat[0]}
|
{stat[0]}
|
||||||
</span>
|
</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]}
|
{stat[1]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { Percentile } from '../../constants/percentiles.js';
|
||||||
import GenericStatsColumn from './components/GenericStatsColumn.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 {
|
export interface GenericStatsProps {
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
@@ -11,7 +14,7 @@ export default function GenericStats({
|
|||||||
stats,
|
stats,
|
||||||
columnCount = 2,
|
columnCount = 2,
|
||||||
}: GenericStatsProps) {
|
}: GenericStatsProps) {
|
||||||
const filteredStats = stats.filter(([, value]) => value !== `${-Infinity}`);
|
const filteredStats = stats.filter(([, value]) => value !== undefined);
|
||||||
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
|
const itemsPerRow = Math.ceil(filteredStats.length / columnCount);
|
||||||
const columns: Stat[][] = [];
|
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={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8,
|
gap: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', gap: 8, flex: 1 }}>
|
<div style={{ display: 'flex', gap: 8, flex: 1 }}>
|
||||||
@@ -37,6 +37,7 @@ export default function TitleBar({
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -44,8 +45,6 @@ export default function TitleBar({
|
|||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
color: theme.colors.textHighContrast,
|
color: theme.colors.textHighContrast,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -58,8 +57,6 @@ export default function TitleBar({
|
|||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
color: theme.colors.textLowContrast,
|
color: theme.colors.textLowContrast,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { theme } from '../stitches.config.js';
|
import { theme } from '../stitches.config.js';
|
||||||
|
|
||||||
export interface WrapperProps {
|
export enum WrapperSize {
|
||||||
children: ReactNode;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -13,7 +27,7 @@ export default function Wrapper({ children }: WrapperProps) {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
gap: 32,
|
gap: 32,
|
||||||
width: 640,
|
width: WRAPPER_SIZE_WIDTHS[size],
|
||||||
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
background: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||||
color: theme.colors.textHighContrast,
|
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,
|
CacheType,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import resolveTankName from '../../utilities/resolveTankName.js';
|
import resolveTankName from '../blitz/resolveTankName.js';
|
||||||
import { tanks } from '../blitzstars/tankopedia.js';
|
import { TANKS } from '../blitz/tankopedia.js';
|
||||||
|
|
||||||
export default async function tanksAutocomplete(
|
export default async function tanksAutocomplete(
|
||||||
interaction: AutocompleteInteraction<CacheType>,
|
interaction: AutocompleteInteraction<CacheType>,
|
||||||
@@ -15,7 +15,7 @@ export default async function tanksAutocomplete(
|
|||||||
|
|
||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
focusedOption.value
|
focusedOption.value
|
||||||
? go(focusedOption.value, tanks, { keys: ['name'], limit: 10 }).map(
|
? go(focusedOption.value, TANKS, { keys: ['name'], limit: 10 }).map(
|
||||||
(item) =>
|
(item) =>
|
||||||
({
|
({
|
||||||
name: item.obj.name
|
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';
|
server: 'com' | 'eu' | 'asia';
|
||||||
})[];
|
})[];
|
||||||
|
|
||||||
export default async function listAccountsPanServer(search: string, limit = 9) {
|
export const usernameRegex = /^[a-zA-Z0-9_]{3,24}$/;
|
||||||
const normalizedSearch = encodeURIComponent(search.trim());
|
|
||||||
const normalizedLimit = Math.round(limit / 3);
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getWargamingResponse<AccountList>(
|
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(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
value &&
|
value &&
|
||||||
@@ -25,7 +28,7 @@ export default async function listAccountsPanServer(search: string, limit = 9) {
|
|||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<AccountList>(
|
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(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
value &&
|
value &&
|
||||||
@@ -35,7 +38,7 @@ export default async function listAccountsPanServer(search: string, limit = 9) {
|
|||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
getWargamingResponse<AccountList>(
|
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(
|
).then(
|
||||||
(value) =>
|
(value) =>
|
||||||
value &&
|
value &&
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||||
import { go } from 'fuzzysort';
|
import { go } from 'fuzzysort';
|
||||||
import { tankopedia, tanks } from '../blitzstars/tankopedia.js';
|
|
||||||
import errorEmbed from '../interaction/errorEmbed.js';
|
import errorEmbed from '../interaction/errorEmbed.js';
|
||||||
|
import { TANKS, tankopedia } from './tankopedia.js';
|
||||||
|
|
||||||
export default async function resolveTankId(
|
export default async function resolveTankId(
|
||||||
interaction: ChatInputCommandInteraction<CacheType>,
|
interaction: ChatInputCommandInteraction<CacheType>,
|
||||||
@@ -10,7 +10,7 @@ export default async function resolveTankId(
|
|||||||
const number = Number(tank);
|
const number = Number(tank);
|
||||||
|
|
||||||
if (Number.isNaN(number)) {
|
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) {
|
if (searchResult.length === 0) {
|
||||||
await interaction.editReply({
|
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) {
|
export default function resolveTankName(tankId: number) {
|
||||||
const tankopediaName = tankopedia[tankId].name;
|
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) {
|
export default function getPeriodicStart(period: StatPeriod) {
|
||||||
if (period === 'career') {
|
if (period === 'career') {
|
||||||
return -Infinity;
|
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 {
|
} else {
|
||||||
const daysAgo = period === 'today' ? 0 : Number(period) - 1;
|
const daysAgo = Number(period) - 1;
|
||||||
const periodStart = new Date();
|
const periodStart = new Date();
|
||||||
|
|
||||||
periodStart.setDate(periodStart.getDate() - daysAgo);
|
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(
|
export default async function getTankStatsOverTime(
|
||||||
server: BlitzServer,
|
server: BlitzServer,
|
||||||
id: number,
|
id: number,
|
||||||
start = -Infinity,
|
start: number,
|
||||||
end = Infinity,
|
end: number,
|
||||||
) {
|
) {
|
||||||
const latestTankStatsRaw = await getWargamingResponse<TanksStats>(
|
const latestTankStatsRaw = await getWargamingResponse<TanksStats>(
|
||||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
|
`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) {
|
export default function cleanTable(input: TableInput) {
|
||||||
return input
|
return input
|
||||||
.filter((array) => array[1] !== `${-Infinity}`)
|
.filter((array) => array[1] !== undefined)
|
||||||
.map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`))
|
.map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`))
|
||||||
.join('\n');
|
.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 { readFile } from 'fs';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import satori from 'satori';
|
import satori from 'satori';
|
||||||
|
import { WRAPPER_SIZE_WIDTHS, WrapperSize } from '../../components/Wrapper.js';
|
||||||
|
|
||||||
const FONT_NAME = 'Roboto';
|
const FONT_NAME = 'Roboto';
|
||||||
const FONT_FILES = ['Roboto', 'Roboto-Bold', 'Roboto-Black'];
|
const FONT_FILES = ['Roboto', 'Roboto-Bold', 'Roboto-Black'];
|
||||||
@@ -20,9 +21,12 @@ const [robotoFlex, robotoFlexBold, robotoFlexBlack] = await Promise.all(
|
|||||||
);
|
);
|
||||||
console.log('Fonts imported');
|
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, {
|
const svg = await satori(element, {
|
||||||
width: 640,
|
width: WRAPPER_SIZE_WIDTHS[size],
|
||||||
fonts: [
|
fonts: [
|
||||||
{ data: robotoFlex, name: FONT_NAME, weight: 400 },
|
{ data: robotoFlex, name: FONT_NAME, weight: 400 },
|
||||||
{ data: robotoFlexBold, name: FONT_NAME, weight: 700 },
|
{ data: robotoFlexBold, name: FONT_NAME, weight: 700 },
|
||||||
|
|||||||
@@ -18,16 +18,9 @@ export interface AllStats {
|
|||||||
dropped_capture_points: number;
|
dropped_capture_points: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialStats {
|
export interface SupplementaryStats {
|
||||||
winrate: number;
|
WN8: number;
|
||||||
damageRatio: number;
|
tier: number;
|
||||||
kdr: number;
|
|
||||||
dpb: number;
|
|
||||||
kpb: number;
|
|
||||||
hpb: number;
|
|
||||||
spb: number;
|
|
||||||
hitRate: number;
|
|
||||||
survivalRate: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClanStats {
|
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