From c2fca42a8624aff20474ec2f926ab60999c66a14 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Tue, 30 May 2023 18:42:34 +0000 Subject: [PATCH 01/10] feat: cleaned up tankopedia --- src/commands/debug.ts | 2 +- src/commands/ownedtanks.ts | 4 +- src/commands/searchtanks.ts | 4 +- src/commands/stats.tsx | 4 +- src/commands/tankstats.tsx | 8 +- src/commands/today.tsx | 8 +- src/core/autocomplete/tanks.ts | 6 +- src/core/blitz/resolveTankId.ts | 4 +- .../blitz}/resolveTankName.ts | 2 +- src/core/blitz/tankopedia.ts | 165 ++++++++++++++++++ src/core/blitzstars/tankopedia.ts | 75 -------- .../fullBlitzStarsStats.ts | 0 .../supportBlitzStars.ts | 0 src/core/ui/URLToBase64.ts | 9 + 14 files changed, 195 insertions(+), 96 deletions(-) rename src/{utilities => core/blitz}/resolveTankName.ts (74%) create mode 100644 src/core/blitz/tankopedia.ts delete mode 100644 src/core/blitzstars/tankopedia.ts rename src/core/{actions => interaction}/fullBlitzStarsStats.ts (100%) rename src/core/{actions => interaction}/supportBlitzStars.ts (100%) create mode 100644 src/core/ui/URLToBase64.ts diff --git a/src/commands/debug.ts b/src/commands/debug.ts index 7b18e4d5a..b18d676fc 100644 --- a/src/commands/debug.ts +++ b/src/commands/debug.ts @@ -1,7 +1,7 @@ import { EmbedBuilder, 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 cmdName from '../core/interaction/cmdName.js'; import getClientId from '../core/process/getClientId.js'; import { CommandRegistry } from '../events/interactionCreate.js'; diff --git a/src/commands/ownedtanks.ts b/src/commands/ownedtanks.ts index c026bf993..069200ea0 100644 --- a/src/commands/ownedtanks.ts +++ b/src/commands/ownedtanks.ts @@ -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 diff --git a/src/commands/searchtanks.ts b/src/commands/searchtanks.ts index bf7d49823..f14361372 100644 --- a/src/commands/searchtanks.ts +++ b/src/commands/searchtanks.ts @@ -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, ); diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index 3dc9bed2c..13e2078bc 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -9,8 +9,6 @@ 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 getWargamingResponse from '../core/blitz/getWargamingResponse.js'; @@ -19,6 +17,8 @@ import getPeriodNow from '../core/blitzstars/getPeriodNow.js'; import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; import getTankStatsOverTime from '../core/blitzstars/getTankStatsOverTime.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, diff --git a/src/commands/tankstats.tsx b/src/commands/tankstats.tsx index bed654baa..61a10c796 100644 --- a/src/commands/tankstats.tsx +++ b/src/commands/tankstats.tsx @@ -9,20 +9,21 @@ 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 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 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,7 +35,6 @@ 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, diff --git a/src/commands/today.tsx b/src/commands/today.tsx index 7f756cc03..ea84232c2 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -9,16 +9,17 @@ 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 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 cmdName from '../core/interaction/cmdName.js'; +import fullBlitzStarsStats from '../core/interaction/fullBlitzStarsStats.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,7 +27,6 @@ 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, diff --git a/src/core/autocomplete/tanks.ts b/src/core/autocomplete/tanks.ts index e92edb117..7d5776f7d 100644 --- a/src/core/autocomplete/tanks.ts +++ b/src/core/autocomplete/tanks.ts @@ -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, @@ -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 diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index fe3d71c94..95507f5d9 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -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, @@ -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({ diff --git a/src/utilities/resolveTankName.ts b/src/core/blitz/resolveTankName.ts similarity index 74% rename from src/utilities/resolveTankName.ts rename to src/core/blitz/resolveTankName.ts index a17599908..2f97fdb7f 100644 --- a/src/utilities/resolveTankName.ts +++ b/src/core/blitz/resolveTankName.ts @@ -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; diff --git a/src/core/blitz/tankopedia.ts b/src/core/blitz/tankopedia.ts new file mode 100644 index 000000000..e4bd8b6ab --- /dev/null +++ b/src/core/blitz/tankopedia.ts @@ -0,0 +1,165 @@ +import { args } from '../process/args.js'; +import isDev from '../process/isDev.js'; +import URLToBase64 from '../ui/URLToBase64.js'; +import getWargamingResponse from './getWargamingResponse.js'; + +export interface Tankopedia { + [id: number]: { + description: string; + engines: number[]; + guns: number[]; + is_premium: boolean; + name: string; + nation: string; + next_tanks: { [id: number]: number }; + prices_xp: { [id: number]: number }; + suspensions: number[]; + tank_id: number; + tier: number; + turrets: number[] | null; + type: string; + cost: { + price_credit: number; + price_gold: number; + }; + default_profile: { + battle_level_range_max: number; + battle_level_range_min: number; + engine_id: number; + firepower: number; + gun_id: number; + hp: number; + hull_hp: number; + hull_weight: number; + is_default: boolean; + maneuverability: number; + max_ammo: number; + max_weight: number; + profile_id: string; + protection: number; + shot_efficiency: number; + signal_range: number; + speed_backward: number; + suspension_id: number; + turret_id: number; + weight: number; + armor: { + hull: { + front: number; + rear: number; + sides: number; + }; + turret: { + front: number; + rear: number; + sides: number; + }; + }; + engine: { + fire_chance: number; + name: string; + power: number; + tier: number; + weight: number; + }; + gun: { + aim_time: number; + caliber: number; + clip_capacity: number; + clip_reload_time: number; + dispersion: number; + fire_rate: number; + move_down_arc: number; + move_up_arc: number; + name: string; + reload_time: number; + tier: number; + traverse_speed: number; + weight: number; + }; + shells: { + damage: number; + penetration: number; + type: string; + }; + suspension: { + load_limit: number; + name: string; + tier: number; + traverse_speed: number; + weight: number; + }; + turret: { + hp: number; + name: string; + tier: number; + traverse_left_arc: number; + traverse_right_arc: number; + traverse_speed: number; + view_range: number; + weight: number; + }; + }; + images: { + normal: string; + preview: string; + }; + modules_tree: { + [id: number]: { + is_default: boolean; + module_id: number; + name: string; + next_modules: number[] | null; + next_tanks: number[] | null; + price_credit: number; + price_xp: number; + type: string; + }; + }; + }; +} + +export interface TankopediaInfo { + achievement_sections: { [name: string]: { name: string; order: number } }; + tanks_updated_at: number; + languages: { [name: string]: string }; + vehicle_types: { [name: string]: string }; + vehicle_nations: { [name: string]: string }; + game_version: string; +} + +console.log('Caching tankopedia...'); +export const tankopedia = await getWargamingResponse( + `https://api.wotblitz.com/wotb/encyclopedia/vehicles/?application_id=${args['wargaming-application-id']}`, +); +console.log('Cached tankopedia'); + +console.log('Caching tankopedia info...'); +export const tankopediaInfo = await getWargamingResponse( + `https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${args['wargaming-application-id']}`, +); +console.log('Cached tankopedia info'); + +/** + * @deprecated + */ +export const TANK_TYPE_EMOJIS: Record = { + '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'); +} diff --git a/src/core/blitzstars/tankopedia.ts b/src/core/blitzstars/tankopedia.ts deleted file mode 100644 index 638cf29d0..000000000 --- a/src/core/blitzstars/tankopedia.ts +++ /dev/null @@ -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 = { - 'AT-SPG': 'Tank Destroyer', - heavyTank: 'Heavy Tank', - mediumTank: 'Medium Tank', - lightTank: 'Light Tank', -}; - -export const TANK_TYPE_EMOJIS: Record = { - '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'); -} diff --git a/src/core/actions/fullBlitzStarsStats.ts b/src/core/interaction/fullBlitzStarsStats.ts similarity index 100% rename from src/core/actions/fullBlitzStarsStats.ts rename to src/core/interaction/fullBlitzStarsStats.ts diff --git a/src/core/actions/supportBlitzStars.ts b/src/core/interaction/supportBlitzStars.ts similarity index 100% rename from src/core/actions/supportBlitzStars.ts rename to src/core/interaction/supportBlitzStars.ts diff --git a/src/core/ui/URLToBase64.ts b/src/core/ui/URLToBase64.ts new file mode 100644 index 000000000..31b7c9954 --- /dev/null +++ b/src/core/ui/URLToBase64.ts @@ -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; +} From a12f969251dcd387405128e7f49e1cdb969102bc Mon Sep 17 00:00:00 2001 From: tresabhi Date: Thu, 1 Jun 2023 18:36:39 +0000 Subject: [PATCH 02/10] feat: wm8 and average tier calculation --- src/commands/debug.ts | 40 ++--- src/commands/stats.tsx | 92 ++++++------ src/commands/tankstats.tsx | 73 +++------ src/commands/today.tsx | 2 +- src/components/GenericAllStats.tsx | 44 ++++++ src/components/GenericStats/index.tsx | 4 +- src/core/blitz/getWN8.ts | 45 ++++++ src/core/blitz/tankopedia.ts | 158 ++++---------------- src/core/blitzstars/getPeriodStart.ts | 10 +- src/core/blitzstars/getPlayerStats.ts | 15 -- src/core/blitzstars/getTankStatsOverTime.ts | 4 +- src/core/blitzstars/tankaverages.ts | 90 +++++++++++ src/types/accountInfo.ts | 13 +- src/types/playerStats.ts | 23 --- src/types/statistics.ts | 30 ---- 15 files changed, 315 insertions(+), 328 deletions(-) create mode 100644 src/components/GenericAllStats.tsx create mode 100644 src/core/blitz/getWN8.ts delete mode 100644 src/core/blitzstars/getPlayerStats.ts create mode 100644 src/core/blitzstars/tankaverages.ts delete mode 100644 src/types/playerStats.ts delete mode 100644 src/types/statistics.ts diff --git a/src/commands/debug.ts b/src/commands/debug.ts index b18d676fc..a33a18f1f 100644 --- a/src/commands/debug.ts +++ b/src/commands/debug.ts @@ -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/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}`], + ]), + ), ], }); diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index 13e2078bc..ecb3c6465 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -3,7 +3,7 @@ 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'; @@ -11,11 +11,14 @@ import Wrapper from '../components/Wrapper.js'; import { BLITZ_SERVERS } from '../constants/servers.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'; @@ -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 && } {stats.battles > 0 && ( - )} diff --git a/src/commands/tankstats.tsx b/src/commands/tankstats.tsx index 61a10c796..3edcf380b 100644 --- a/src/commands/tankstats.tsx +++ b/src/commands/tankstats.tsx @@ -3,7 +3,7 @@ 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'; @@ -12,6 +12,7 @@ import { BLITZ_SERVERS } from '../constants/servers.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'; @@ -21,6 +22,7 @@ import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; import getTankStatsOverTime, { emptyAllStats, } 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'; @@ -38,7 +40,7 @@ import { TanksStats } from '../types/tanksStats.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( } {stats.battles > 0 && ( - // TODO: GenericStats generator for AllStats - )} diff --git a/src/commands/today.tsx b/src/commands/today.tsx index ea84232c2..0fd76e2e6 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -30,7 +30,7 @@ import { TanksStats } from '../types/tanksStats.js'; export default { inProduction: true, - inDevelopment: false, + inDevelopment: true, inPublic: true, command: new SlashCommandBuilder() diff --git a/src/components/GenericAllStats.tsx b/src/components/GenericAllStats.tsx new file mode 100644 index 000000000..62039cf49 --- /dev/null +++ b/src/components/GenericAllStats.tsx @@ -0,0 +1,44 @@ +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 ( + + ); +} diff --git a/src/components/GenericStats/index.tsx b/src/components/GenericStats/index.tsx index 6f55650b0..53e253e57 100644 --- a/src/components/GenericStats/index.tsx +++ b/src/components/GenericStats/index.tsx @@ -11,7 +11,9 @@ export default function GenericStats({ stats, columnCount = 2, }: GenericStatsProps) { - const filteredStats = stats.filter(([, value]) => value !== `${-Infinity}`); + const filteredStats = stats.filter( + ([, value]) => `${value}` !== `${-Infinity}`, + ); const itemsPerRow = Math.ceil(filteredStats.length / columnCount); const columns: Stat[][] = []; diff --git a/src/core/blitz/getWN8.ts b/src/core/blitz/getWN8.ts new file mode 100644 index 000000000..86da436f9 --- /dev/null +++ b/src/core/blitz/getWN8.ts @@ -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; +} diff --git a/src/core/blitz/tankopedia.ts b/src/core/blitz/tankopedia.ts index e4bd8b6ab..19d6c3df3 100644 --- a/src/core/blitz/tankopedia.ts +++ b/src/core/blitz/tankopedia.ts @@ -1,145 +1,49 @@ -import { args } from '../process/args.js'; +import { WargamingResponse } from '../../types/wargamingResponse.js'; import isDev from '../process/isDev.js'; import URLToBase64 from '../ui/URLToBase64.js'; -import getWargamingResponse from './getWargamingResponse.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]: { - description: string; - engines: number[]; - guns: number[]; - is_premium: boolean; name: string; nation: string; - next_tanks: { [id: number]: number }; - prices_xp: { [id: number]: number }; - suspensions: number[]; - tank_id: number; + is_premium: boolean; tier: number; - turrets: number[] | null; + cost: { price_credit: number; price_gold: number }; + images: { preview: string; normal: string }; + tank_id: number; type: string; - cost: { - price_credit: number; - price_gold: number; - }; - default_profile: { - battle_level_range_max: number; - battle_level_range_min: number; - engine_id: number; - firepower: number; - gun_id: number; - hp: number; - hull_hp: number; - hull_weight: number; - is_default: boolean; - maneuverability: number; - max_ammo: number; - max_weight: number; - profile_id: string; - protection: number; - shot_efficiency: number; - signal_range: number; - speed_backward: number; - suspension_id: number; - turret_id: number; - weight: number; - armor: { - hull: { - front: number; - rear: number; - sides: number; - }; - turret: { - front: number; - rear: number; - sides: number; - }; - }; - engine: { - fire_chance: number; - name: string; - power: number; - tier: number; - weight: number; - }; - gun: { - aim_time: number; - caliber: number; - clip_capacity: number; - clip_reload_time: number; - dispersion: number; - fire_rate: number; - move_down_arc: number; - move_up_arc: number; - name: string; - reload_time: number; - tier: number; - traverse_speed: number; - weight: number; - }; - shells: { - damage: number; - penetration: number; - type: string; - }; - suspension: { - load_limit: number; - name: string; - tier: number; - traverse_speed: number; - weight: number; - }; - turret: { - hp: number; - name: string; - tier: number; - traverse_left_arc: number; - traverse_right_arc: number; - traverse_speed: number; - view_range: number; - weight: number; - }; - }; - images: { - normal: string; - preview: string; - }; - modules_tree: { - [id: number]: { - is_default: boolean; - module_id: number; - name: string; - next_modules: number[] | null; - next_tanks: number[] | null; - price_credit: number; - price_xp: number; - type: string; - }; - }; + description: string; }; } -export interface TankopediaInfo { - achievement_sections: { [name: string]: { name: string; order: number } }; - tanks_updated_at: number; - languages: { [name: string]: string }; - vehicle_types: { [name: string]: string }; - vehicle_nations: { [name: string]: string }; - game_version: string; -} - console.log('Caching tankopedia...'); -export const tankopedia = await getWargamingResponse( - `https://api.wotblitz.com/wotb/encyclopedia/vehicles/?application_id=${args['wargaming-application-id']}`, -); +export const tankopedia = ( + (await ( + await fetch('https://www.blitzstars.com/bs-tankopedia.json') + ).json()) as WargamingResponse & { status: 'ok' } +).data; console.log('Cached tankopedia'); -console.log('Caching tankopedia info...'); -export const tankopediaInfo = await getWargamingResponse( - `https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${args['wargaming-application-id']}`, -); -console.log('Cached tankopedia info'); - /** * @deprecated */ diff --git a/src/core/blitzstars/getPeriodStart.ts b/src/core/blitzstars/getPeriodStart.ts index 8cb9c3074..916bcd9f7 100644 --- a/src/core/blitzstars/getPeriodStart.ts +++ b/src/core/blitzstars/getPeriodStart.ts @@ -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); diff --git a/src/core/blitzstars/getPlayerStats.ts b/src/core/blitzstars/getPlayerStats.ts deleted file mode 100644 index 2c9d6904d..000000000 --- a/src/core/blitzstars/getPlayerStats.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CacheType, ChatInputCommandInteraction } from 'discord.js'; -import { PlayerStats } from '../../types/playerStats.js'; - -export default async function getPlayerStats( - interaction: ChatInputCommandInteraction, - 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); - }, - ); -} diff --git a/src/core/blitzstars/getTankStatsOverTime.ts b/src/core/blitzstars/getTankStatsOverTime.ts index 662b41b3b..1230d7c0f 100644 --- a/src/core/blitzstars/getTankStatsOverTime.ts +++ b/src/core/blitzstars/getTankStatsOverTime.ts @@ -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( `https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`, diff --git a/src/core/blitzstars/tankaverages.ts b/src/core/blitzstars/tankaverages.ts new file mode 100644 index 000000000..7d6e5bce2 --- /dev/null +++ b/src/core/blitzstars/tankaverages.ts @@ -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 = {}; + +Object.entries(tankAveragesRaw).forEach(([, tank]) => { + tankAverages[tank.tank_id] = tank; +}); +console.log('Tank averages cached'); diff --git a/src/types/accountInfo.ts b/src/types/accountInfo.ts index f4eceb21d..3e14adc50 100644 --- a/src/types/accountInfo.ts +++ b/src/types/accountInfo.ts @@ -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 { diff --git a/src/types/playerStats.ts b/src/types/playerStats.ts deleted file mode 100644 index 5f9e31662..000000000 --- a/src/types/playerStats.ts +++ /dev/null @@ -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; -}[]; diff --git a/src/types/statistics.ts b/src/types/statistics.ts deleted file mode 100644 index 0f051d96a..000000000 --- a/src/types/statistics.ts +++ /dev/null @@ -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; -} From 1d04513bb100286fb861f81323975d558bdf7b15 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Thu, 1 Jun 2023 18:58:35 +0000 Subject: [PATCH 03/10] fix: 0 passing instead of boolean --- src/components/Breakdown/components/RowStat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Breakdown/components/RowStat.tsx b/src/components/Breakdown/components/RowStat.tsx index 398cad660..f923c0b83 100644 --- a/src/components/Breakdown/components/RowStat.tsx +++ b/src/components/Breakdown/components/RowStat.tsx @@ -31,7 +31,7 @@ export function RowStat({ name, value, career, delta }: RowStatProps) { justifyContent: 'center', }} > - {delta && ( + {delta !== 0 && ( 0 From de857e7263e69e257ce2dd3c697020ec8aa2de59 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Thu, 1 Jun 2023 19:16:29 +0000 Subject: [PATCH 04/10] feat: percentile indicator --- .vscode/settings.json | 1 + src/commands/stats.tsx | 6 +-- src/commands/tankstats.tsx | 2 +- src/components/GenericAllStats.tsx | 9 +++- .../components/GenericStatsRow.tsx | 16 ++++++- src/components/GenericStats/index.tsx | 5 ++- src/components/PercentileIndicator.tsx | 45 +++++++++++++++++++ src/constants/percentiles.ts | 13 ++++++ src/core/blitz/getWN8Percentile.ts | 21 +++++++++ src/types/accountInfo.ts | 2 +- 10 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/components/PercentileIndicator.tsx create mode 100644 src/constants/percentiles.ts create mode 100644 src/core/blitz/getWN8Percentile.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 2b09f08da..862f08758 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "Smri", "strvk", "tankopedia", + "Unicum", "Unregistering", "vickers", "wargaming", diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index ecb3c6465..3a7304f8e 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -85,12 +85,12 @@ export default { return accumulator + tankTier * stats.battles; }, 0) / totalBattles, - wn8: + WN8: entries.reduce((accumulator, [tankIdString, stats]) => { const tankId = Number(tankIdString); - const tankWn8 = getWN8(tankAverages[tankId].all, stats); + const tankWN8 = getWN8(tankAverages[tankId].all, stats); - return accumulator + tankWn8 * stats.battles; + return accumulator + tankWN8 * stats.battles; }, 0) / totalBattles, }; diff --git a/src/commands/tankstats.tsx b/src/commands/tankstats.tsx index 3edcf380b..0e4854ffa 100644 --- a/src/commands/tankstats.tsx +++ b/src/commands/tankstats.tsx @@ -100,7 +100,7 @@ export default { stats={stats} supplementaryStats={{ tier: tankopedia[tankId].tier, - wn8: getWN8(tankAverages[tankId].all, stats), + WN8: getWN8(tankAverages[tankId].all, stats), }} /> )} diff --git a/src/components/GenericAllStats.tsx b/src/components/GenericAllStats.tsx index 62039cf49..24d8b1d06 100644 --- a/src/components/GenericAllStats.tsx +++ b/src/components/GenericAllStats.tsx @@ -1,3 +1,4 @@ +import getWN8Percentile from '../core/blitz/getWN8Percentile.js'; import { AllStats, SupplementaryStats } from '../types/accountInfo.js'; import GenericStats from './GenericStats/index.js'; @@ -14,7 +15,13 @@ export default function GenericAllStats({ {stat[0]} - + + {stat[2] && } {stat[1]} diff --git a/src/components/GenericStats/index.tsx b/src/components/GenericStats/index.tsx index 53e253e57..d85239537 100644 --- a/src/components/GenericStats/index.tsx +++ b/src/components/GenericStats/index.tsx @@ -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] + | [string, string | number, Percentile | undefined]; export interface GenericStatsProps { stats: Stat[]; diff --git a/src/components/PercentileIndicator.tsx b/src/components/PercentileIndicator.tsx new file mode 100644 index 000000000..9a466a562 --- /dev/null +++ b/src/components/PercentileIndicator.tsx @@ -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 ( +
+ ); +} diff --git a/src/constants/percentiles.ts b/src/constants/percentiles.ts new file mode 100644 index 000000000..f0e7dbfec --- /dev/null +++ b/src/constants/percentiles.ts @@ -0,0 +1,13 @@ +// http://www.wotinfo.net/en/wn8scale +export enum Percentile { + VeryBad, + Bad, + BelowAverage, + Average, + AboveAverage, + Good, + VeryGood, + Great, + Unicum, + SuperUnicum, +} diff --git a/src/core/blitz/getWN8Percentile.ts b/src/core/blitz/getWN8Percentile.ts new file mode 100644 index 000000000..870bf83d4 --- /dev/null +++ b/src/core/blitz/getWN8Percentile.ts @@ -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]; +} diff --git a/src/types/accountInfo.ts b/src/types/accountInfo.ts index 3e14adc50..908930892 100644 --- a/src/types/accountInfo.ts +++ b/src/types/accountInfo.ts @@ -19,7 +19,7 @@ export interface AllStats { } export interface SupplementaryStats { - wn8: number; + WN8: number; tier: number; } From 04c835b25e57918b1212e73d27b207cc1873c3cf Mon Sep 17 00:00:00 2001 From: tresabhi Date: Fri, 2 Jun 2023 00:28:34 +0000 Subject: [PATCH 05/10] fix: stricter regex for username search --- src/commands/searchplayers.ts | 16 +++++++++------- src/core/blitz/listAccountsPanServer.ts | 17 ++++++++++------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/commands/searchplayers.ts b/src/commands/searchplayers.ts index 93579dd83..9ea41a53a 100644 --- a/src/commands/searchplayers.ts +++ b/src/commands/searchplayers.ts @@ -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( - `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( + `https://api.wotblitz.${server}/wotb/account/list/?application_id=${ + args['wargaming-application-id'] + }&search=${encodeURIComponent(trimmedSearch)}&limit=${limit}`, + ) + : []; await interaction.editReply({ embeds: [ diff --git a/src/core/blitz/listAccountsPanServer.ts b/src/core/blitz/listAccountsPanServer.ts index 0c021a44a..70643e540 100644 --- a/src/core/blitz/listAccountsPanServer.ts +++ b/src/core/blitz/listAccountsPanServer.ts @@ -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( - `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( - `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( - `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 && From 3a8591af3d99022ef7a7b4fc3dc2c8c723b10317 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Fri, 2 Jun 2023 00:37:08 +0000 Subject: [PATCH 06/10] fix: today stats tank name truncating --- src/components/Breakdown/components/RowDiscriminator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Breakdown/components/RowDiscriminator.tsx b/src/components/Breakdown/components/RowDiscriminator.tsx index 8f3a11713..91bb614f3 100644 --- a/src/components/Breakdown/components/RowDiscriminator.tsx +++ b/src/components/Breakdown/components/RowDiscriminator.tsx @@ -33,7 +33,7 @@ export function RowDiscriminator({ name, icon }: RowDiscriminatorProps) { display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', - textOverflow: 'ellipsis', + textOverflow: 'clip', maxWidth: '100%', }} > From aec110723a1529ac28d278e801561643d71f7264 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Fri, 2 Jun 2023 00:38:30 +0000 Subject: [PATCH 07/10] fix: deltas showing up on battle count --- src/components/Breakdown/components/RowStat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Breakdown/components/RowStat.tsx b/src/components/Breakdown/components/RowStat.tsx index f923c0b83..5668059f5 100644 --- a/src/components/Breakdown/components/RowStat.tsx +++ b/src/components/Breakdown/components/RowStat.tsx @@ -31,7 +31,7 @@ export function RowStat({ name, value, career, delta }: RowStatProps) { justifyContent: 'center', }} > - {delta !== 0 && ( + {delta !== undefined && delta !== 0 && ( 0 From c5d14758d5d7d38646180159d3cd5e5a55e8da65 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Fri, 2 Jun 2023 00:45:23 +0000 Subject: [PATCH 08/10] fix: replaced ellipses in title bar with clipping --- src/components/TitleBar.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 3c8951a89..ead666fa2 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -19,7 +19,7 @@ export default function TitleBar({ style={{ display: 'flex', alignItems: 'center', - gap: 8, + gap: 16, }} >
@@ -37,6 +37,7 @@ export default function TitleBar({ style={{ display: 'flex', gap: 4, + overflow: 'hidden', }} > @@ -58,8 +57,6 @@ export default function TitleBar({ fontSize: 32, color: theme.colors.textLowContrast, fontWeight: 900, - overflow: 'hidden', - textOverflow: 'ellipsis', whiteSpace: 'nowrap', }} > From f801783358bde02e49980888a58afd8196ae8316 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Fri, 2 Jun 2023 01:31:14 +0000 Subject: [PATCH 09/10] feat: added WN8 to /today --- src/commands/debug.ts | 2 +- src/commands/stats.tsx | 2 +- src/commands/tankstats.tsx | 2 +- src/commands/today.tsx | 117 ++++++++++++++---- src/components/Breakdown/components/Row.tsx | 17 ++- .../Breakdown/components/RowStat.tsx | 16 ++- src/components/GenericAllStats.tsx | 4 +- .../components/GenericStatsRow.tsx | 2 +- src/components/GenericStats/index.tsx | 8 +- src/components/Wrapper.tsx | 22 +++- src/core/blitzstars/playerAverage.ts | 12 ++ .../{tankaverages.ts => tankAverages.ts} | 0 src/core/interaction/cleanTable.ts | 2 +- src/core/ui/render.tsx | 8 +- 14 files changed, 163 insertions(+), 51 deletions(-) create mode 100644 src/core/blitzstars/playerAverage.ts rename src/core/blitzstars/{tankaverages.ts => tankAverages.ts} (100%) diff --git a/src/commands/debug.ts b/src/commands/debug.ts index a33a18f1f..e7630f90d 100644 --- a/src/commands/debug.ts +++ b/src/commands/debug.ts @@ -1,7 +1,7 @@ import { SlashCommandBuilder } from 'discord.js'; import packageJSON from '../../package.json' assert { type: 'json' }; import { tankopedia } from '../core/blitz/tankopedia.js'; -import { tankAverages } from '../core/blitzstars/tankaverages.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'; diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index 3a7304f8e..ee6e30b45 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -18,7 +18,7 @@ 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 { 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'; diff --git a/src/commands/tankstats.tsx b/src/commands/tankstats.tsx index 0e4854ffa..17c55a548 100644 --- a/src/commands/tankstats.tsx +++ b/src/commands/tankstats.tsx @@ -22,7 +22,7 @@ import getPeriodicStart from '../core/blitzstars/getPeriodStart.js'; import getTankStatsOverTime, { emptyAllStats, } from '../core/blitzstars/getTankStatsOverTime.js'; -import { tankAverages } from '../core/blitzstars/tankaverages.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'; diff --git a/src/commands/today.tsx b/src/commands/today.tsx index 0fd76e2e6..d9ac2539a 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -7,18 +7,21 @@ 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 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 { 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'; @@ -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>( + (accumulator, [tankIdString, tankStats]) => { + const tankId = Number(tankIdString); - return ( - - ); + return tankId === 0 + ? accumulator + : { + ...accumulator, + [tankId]: getWN8(tankAverages[tankId].all, tankStats), + }; }, + {}, ); + const careerWN8s = tankStatsOverTimeEntries.reduce>( + (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 ( + + ); + }); const image = await render( - + , + WrapperSize.Roomy, ); const actionRow = new ActionRowBuilder().addComponents( @@ -133,6 +189,15 @@ export default { ); await interaction.editReply({ + embeds: + rows.length >= 6 + ? [ + infoEmbed( + 'Very large image!', + 'Zoom in or click "Open in Browser."', + ), + ] + : undefined, files: [image], components: [actionRow], }); diff --git a/src/components/Breakdown/components/Row.tsx b/src/components/Breakdown/components/Row.tsx index 29cfc00ec..0d5f8e125 100644 --- a/src/components/Breakdown/components/Row.tsx +++ b/src/components/Breakdown/components/Row.tsx @@ -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} /> + - {delta !== undefined && delta !== 0 && ( + {percentile === undefined && delta !== undefined && delta !== 0 && ( 0 @@ -41,6 +50,9 @@ export function RowStat({ name, value, career, delta }: RowStatProps) { style={{ width: 12, height: 12 }} /> )} + {percentile !== undefined && ( + + )} - {stat[2] && } + {stat[2] !== undefined && } {stat[1]}
diff --git a/src/components/GenericStats/index.tsx b/src/components/GenericStats/index.tsx index d85239537..94414baad 100644 --- a/src/components/GenericStats/index.tsx +++ b/src/components/GenericStats/index.tsx @@ -2,8 +2,8 @@ import { Percentile } from '../../constants/percentiles.js'; import GenericStatsColumn from './components/GenericStatsColumn.js'; export type Stat = - | [string, string | number] - | [string, string | number, Percentile | undefined]; + | [string, string | number | undefined] + | [string, string | number | undefined, Percentile | undefined]; export interface GenericStatsProps { stats: Stat[]; @@ -14,9 +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[][] = []; diff --git a/src/components/Wrapper.tsx b/src/components/Wrapper.tsx index 15d51f1c2..9689b4bd3 100644 --- a/src/components/Wrapper.tsx +++ b/src/components/Wrapper.tsx @@ -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.Regular]: 640, + [WrapperSize.Roomy]: 800, +}; + +export default function Wrapper({ + children, + size = WrapperSize.Regular, +}: WrapperProps) { return (
{ + return sumStats([accumulator, tankAverage.all]); + }, + emptyAllStats, +); +console.log('Calculated player average'); diff --git a/src/core/blitzstars/tankaverages.ts b/src/core/blitzstars/tankAverages.ts similarity index 100% rename from src/core/blitzstars/tankaverages.ts rename to src/core/blitzstars/tankAverages.ts diff --git a/src/core/interaction/cleanTable.ts b/src/core/interaction/cleanTable.ts index 7939453cc..45961433c 100644 --- a/src/core/interaction/cleanTable.ts +++ b/src/core/interaction/cleanTable.ts @@ -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'); } diff --git a/src/core/ui/render.tsx b/src/core/ui/render.tsx index 10afb44ef..6100c1793 100644 --- a/src/core/ui/render.tsx +++ b/src/core/ui/render.tsx @@ -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 }, From 8d613a42f46354d87672ef6a9493c4c2c5b0fc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 1 Jun 2023 20:33:26 -0500 Subject: [PATCH 10/10] feat: clarified large image message --- src/commands/today.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/today.tsx b/src/commands/today.tsx index d9ac2539a..4eebe41b1 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -194,7 +194,7 @@ export default { ? [ infoEmbed( 'Very large image!', - 'Zoom in or click "Open in Browser."', + "Don't forget to oom in.", ), ] : undefined,