diff --git a/src/commands/breakdown.tsx b/src/commands/breakdown.tsx new file mode 100644 index 000000000..170900c77 --- /dev/null +++ b/src/commands/breakdown.tsx @@ -0,0 +1,310 @@ +import { SlashCommandBuilder } from 'discord.js'; +import * as Breakdown from '../components/Breakdown'; +import NoData, { NoDataType } from '../components/NoData'; +import TitleBar from '../components/TitleBar'; +import Wrapper from '../components/Wrapper'; +import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo'; +import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; +import getTankStats from '../core/blitz/getTankStats'; +import getTreeType from '../core/blitz/getTreeType'; +import resolveTankName from '../core/blitz/resolveTankName'; +import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription'; +import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; +import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; +import { tankAverages } from '../core/blitzstars/tankAverages'; +import { tankopedia } from '../core/blitzstars/tankopedia'; +import addFilterOptions from '../core/discord/addFilterOptions'; +import addUsernameChoices from '../core/discord/addUsernameChoices'; +import autocompleteTanks from '../core/discord/autocompleteTanks'; +import autocompleteUsername from '../core/discord/autocompleteUsername'; +import buttonPrimary from '../core/discord/buttonPrimary'; +import commandToURL from '../core/discord/commandToURL'; +import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams'; +import { getFiltersFromButton } from '../core/discord/getFiltersFromButton'; +import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand'; +import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton'; +import resolvePeriodFromCommand, { + ResolvedPeriod, +} from '../core/discord/resolvePeriodFromCommand'; +import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; +import resolvePlayerFromCommand, { + ResolvedPlayer, +} from '../core/discord/resolvePlayerFromCommand'; +import calculateWN8 from '../core/statistics/calculateWN8'; +import { StatFilters, filterStats } from '../core/statistics/filterStats'; +import getWN8Percentile from '../core/statistics/getWN8Percentile'; +import { CommandRegistryRaw } from '../events/interactionCreate'; + +async function render( + { region, id }: ResolvedPlayer, + { start, end, name }: ResolvedPeriod, + filters: StatFilters, +) { + const awaitedTankopedia = await tankopedia; + const awaitedTankAverages = await tankAverages; + const statsInPeriod = await getStatsInPeriod(region, id, start, end); + const { filteredOrder } = await filterStats(statsInPeriod, filters); + const accountInfo = await getAccountInfo(region, id); + const clanData = await getClanAccountInfo(region, id); + const tankStats = await getTankStats(region, id); + const filterDescriptions = await filtersToDescription(filters); + const orderedCurrentStats: AllStats[] = []; + const orderedCareerStats: AllStats[] = []; + const orderedCurrentWN8: (number | undefined)[] = []; + const orderedCareerWN8: (number | undefined)[] = []; + const orderedCareerWN8Full: (number | undefined)[] = []; + + filteredOrder.forEach((id) => { + const tankAveragesEntry = awaitedTankAverages[id]; + const current = statsInPeriod.diff[id]; + const career = tankStats.find((tank) => tank.tank_id === id)!.all; + const currentWN8 = tankAveragesEntry + ? calculateWN8(tankAveragesEntry.all, current) + : undefined; + const careerWN8 = tankAveragesEntry + ? calculateWN8(tankAveragesEntry.all, career) + : undefined; + + orderedCurrentStats.push(current); + orderedCareerStats.push(career); + orderedCurrentWN8.push(currentWN8); + orderedCareerWN8.push(careerWN8); + }); + tankStats.forEach((tank) => { + const tankAveragesEntry = awaitedTankAverages[tank.tank_id]; + + orderedCareerWN8Full.push( + tankAveragesEntry + ? calculateWN8(tankAveragesEntry.all, tank.all) + : undefined, + ); + }); + + const currentBattles = orderedCurrentStats.reduce( + (accumulator, current) => accumulator + current.battles, + 0, + ); + const careerBattles = tankStats.reduce( + (accumulator, current) => accumulator + current.all.battles, + 0, + ); + const currentWinrate = + orderedCurrentStats.reduce( + (accumulator, current) => accumulator + current.wins, + 0, + ) / currentBattles; + const careerWinrate = + tankStats.reduce( + (accumulator, current) => accumulator + current.all.wins, + 0, + ) / careerBattles; + const currentWN8 = + orderedCurrentWN8.reduce( + (accumulator, current, index) => + accumulator + (current ?? 0) * orderedCurrentStats[index].battles, + 0, + ) / + orderedCurrentStats.reduce( + (accumulator, current, index) => + accumulator + + (orderedCurrentWN8[index] === undefined ? 0 : current.battles), + 0, + ); + const careerWN8 = + orderedCareerWN8Full.reduce( + (accumulator, current) => accumulator + (current ?? 0), + 0, + ) / + tankStats.reduce( + (accumulator, current, index) => + accumulator + + (orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles), + 0, + ); + const currentDamage = + orderedCurrentStats.reduce( + (accumulator, current) => accumulator + current.damage_dealt, + 0, + ) / currentBattles; + const careerDamage = + tankStats.reduce( + (accumulator, current) => accumulator + current.all.damage_dealt, + 0, + ) / careerBattles; + + return ( + + + + {filteredOrder.length === 0 && ( + + )} + {filteredOrder.length > 0 && ( + + + + {await Promise.all( + filteredOrder.map(async (id, index) => { + const tankopediaEntry = awaitedTankopedia[id]; + const current = orderedCurrentStats[index]; + const career = orderedCareerStats[index]; + const currentWN8 = orderedCurrentWN8[index]; + const careerWN8 = orderedCareerWN8[index]; + + return ( + + ); + }), + )} + + )} + + ); +} + +export const breakdownCommand = new Promise( + async (resolve) => { + const command = await addFilterOptions( + new SlashCommandBuilder() + .setName('breakdown') + .setDescription("A period's breakdown by tanks played"), + (option) => option.addStringOption(addUsernameChoices), + ); + + resolve({ + inProduction: true, + inPublic: true, + + command, + + async handler(interaction) { + const player = await resolvePlayerFromCommand(interaction); + const period = resolvePeriodFromCommand(player.region, interaction); + const filters = await getFiltersFromCommand(interaction); + const path = commandToURL(interaction, { + ...player, + ...getCustomPeriodParams(interaction), + ...filters, + }); + + return Promise.all([ + render(player, period, filters), + buttonPrimary(path, 'Refresh'), + getBlitzStarsLinkButton(player.region, player.id), + ]); + }, + + autocomplete: (interaction) => { + autocompleteUsername(interaction); + autocompleteTanks(interaction); + }, + + async button(interaction) { + const player = await resolvePlayerFromButton(interaction); + const period = resolvePeriodFromButton(player.region, interaction); + const filters = getFiltersFromButton(interaction); + + return await render(player, period, filters); + }, + }); + }, +); diff --git a/src/commands/today.tsx b/src/commands/today.tsx index c549c4845..3b3e5e348 100644 --- a/src/commands/today.tsx +++ b/src/commands/today.tsx @@ -1,277 +1,18 @@ import { SlashCommandBuilder } from 'discord.js'; -import * as Breakdown from '../components/Breakdown'; -import NoData, { NoDataType } from '../components/NoData'; -import TitleBar from '../components/TitleBar'; -import Wrapper from '../components/Wrapper'; -import { AllStats, getAccountInfo } from '../core/blitz/getAccountInfo'; -import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; -import getTankStats from '../core/blitz/getTankStats'; -import getTreeType from '../core/blitz/getTreeType'; -import resolveTankName from '../core/blitz/resolveTankName'; -import getPeriodNow from '../core/blitzkrieg/getPeriodNow'; -import getTimeDaysAgo from '../core/blitzkrieg/getTimeDaysAgo'; -import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; -import { tankAverages } from '../core/blitzstars/tankAverages'; -import { tankopedia } from '../core/blitzstars/tankopedia'; -import addUsernameChoices from '../core/discord/addUsernameChoices'; -import autocompleteUsername from '../core/discord/autocompleteUsername'; -import buttonLink from '../core/discord/buttonLink'; -import buttonPrimary from '../core/discord/buttonPrimary'; -import commandToURL from '../core/discord/commandToURL'; -import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; -import resolvePlayerFromCommand, { - ResolvedPlayer, -} from '../core/discord/resolvePlayerFromCommand'; -import calculateWN8 from '../core/statistics/calculateWN8'; -import getWN8Percentile from '../core/statistics/getWN8Percentile'; -import sumStats from '../core/statistics/sumStats'; import { CommandRegistry } from '../events/interactionCreate'; -import { PossiblyPromise } from '../types/possiblyPromise'; - -async function render( - { region: server, id }: ResolvedPlayer, - cutoff = Infinity, - maximized = 4, - showTotal = true, - naked?: boolean, -) { - const { diff: diffed, order } = await getStatsInPeriod( - server, - id, - getTimeDaysAgo(server, 1), - getPeriodNow(), - ); - const accountInfo = await getAccountInfo(server, id); - const clanData = await getClanAccountInfo(server, id); - const careerTankStatsRaw = await getTankStats(server, id); - const careerStats: Record = showTotal - ? { - 0: accountInfo.statistics.all, - } - : {}; - const allStatsToAccumulate: AllStats[] = []; - - Object.entries(diffed).forEach(([, tankStats]) => { - allStatsToAccumulate.push(tankStats); - }); - Object.entries(careerTankStatsRaw).forEach(([, tankStats]) => { - careerStats[tankStats.tank_id] = tankStats.all; - }); - - const accumulatedStats = sumStats(allStatsToAccumulate); - - if (showTotal && Object.keys(diffed).length > 0) { - diffed[0] = accumulatedStats; - } - - const tankStatsOverTimeEntries = Object.entries(diffed); - const todayWN8s = await tankStatsOverTimeEntries.reduce< - PossiblyPromise> - >(async (accumulator, [tankIdString, tankStats]) => { - const tankId = parseInt(tankIdString); - - return (showTotal && tankId === 0) || !(await tankAverages)[tankId] - ? accumulator - : { - ...(await accumulator), - [tankId]: calculateWN8((await tankAverages)[tankId].all, tankStats), - }; - }, {}); - - const careerWN8s = await careerTankStatsRaw.reduce< - PossiblyPromise> - >(async (accumulator, { tank_id }) => { - return (showTotal && tank_id === 0) || - (await tankAverages)[tank_id] === undefined - ? accumulator - : { - ...(await accumulator), - [tank_id]: calculateWN8( - (await tankAverages)[tank_id].all, - careerStats[tank_id], - ), - }; - }, {}); - const todayWN8sEntries = Object.entries(todayWN8s); - const careerWN8sEntries = Object.entries(careerWN8s); - - if (showTotal) { - todayWN8s[0] = - todayWN8sEntries.reduce( - (accumulator, [tankIdString, wn8]) => - accumulator + wn8 * diffed[Number(tankIdString)].battles, - 0, - ) / - todayWN8sEntries.reduce( - (accumulator, [tankIdString]) => - accumulator + diffed[Number(tankIdString)].battles, - 0, - ); - careerWN8s[0] = - careerWN8sEntries.reduce( - (accumulator, [tankIdString, WN8]) => - isNaN(WN8) - ? accumulator - : accumulator + WN8 * careerStats[Number(tankIdString)].battles, - 0, - ) / - careerWN8sEntries.reduce( - (accumulator, [tankIdString, WN8]) => - isNaN(WN8) - ? accumulator - : accumulator + careerStats[Number(tankIdString)].battles, - 0, - ); - } - - const rows = await Promise.all( - [ - // this code unreadable on god will forget tomorrow no cap - ...(order.length === 0 || !showTotal ? [] : [0]), - ...(isFinite(cutoff) ? order.slice(0, cutoff) : order), - ].map(async (id, index) => { - const tankStats = diffed[id]; - const career = careerStats[id]; - const tankopediaEntry = (await tankopedia)[id]; - - return ( - maximized : index + 1 > maximized} - stats={[ - { - title: 'Battles', - current: tankStats.battles.toLocaleString(), - career: career.battles.toLocaleString(), - }, - { - title: 'Winrate', - current: `${(100 * (tankStats.wins / tankStats.battles)).toFixed( - 2, - )}%`, - career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`, - delta: - tankStats.wins / tankStats.battles - - career.wins / career.battles, - }, - { - title: 'WN8', - current: isNaN(todayWN8s[id]) - ? undefined - : Math.round(todayWN8s[id]).toLocaleString(), - career: isNaN(careerWN8s[id]) - ? undefined - : Math.round(careerWN8s[id]).toLocaleString(), - percentile: isNaN(todayWN8s[id]) - ? undefined - : getWN8Percentile(todayWN8s[id]), - }, - { - title: 'Damage', - current: Math.round( - tankStats.damage_dealt / tankStats.battles, - ).toLocaleString(), - career: Math.round( - career.damage_dealt / career.battles, - ).toLocaleString(), - delta: - tankStats.damage_dealt / tankStats.battles - - career.damage_dealt / career.battles, - }, - ]} - /> - ); - }), - ); - - return naked ? ( - - {rows} - - ) : ( - - - - {rows.length === 0 && } - {rows.length > 0 && {rows}} - - ); -} +/** + * TODO: @deprecated + */ export const todayCommand: CommandRegistry = { inProduction: true, inPublic: true, command: new SlashCommandBuilder() .setName('today') - .setDescription('A general daily breakdown of your performance') - .addStringOption(addUsernameChoices) - .addIntegerOption((option) => - option - .setName('cutoff') - .setDescription( - 'The maximum number of tanks to display (default: Infinity)', - ) - .setRequired(false) - .setMinValue(1), - ) - .addIntegerOption((option) => - option - .setName('maximized') - .setDescription( - 'The number of rows with full stats after which all are collapsed (default: 4)', - ) - .setMinValue(0) - .setRequired(false), - ) - .addBooleanOption((option) => - option - .setName('show-total') - .setDescription('Show the total stats at the top (default: true)') - .setRequired(false), - ), + .setDescription('This command has been renamed to `/breakdown today`'), - async handler(interaction) { - const player = await resolvePlayerFromCommand(interaction); - const cutoff = interaction.options.getInteger('cutoff') ?? undefined; - const maximized = interaction.options.getInteger('maximized') ?? undefined; - const showTotal = interaction.options.getBoolean('show-total') ?? undefined; - const { nickname } = await getAccountInfo(player.region, player.id); - const path = commandToURL(interaction, { - ...player, - cutoff, - maximized, - 'show-total': showTotal, - }); - - return [ - await render(player, cutoff, maximized, showTotal, false), - buttonPrimary(path, 'Refresh'), - // linkButton(`https://example.com/${path}`, 'Embed'), - buttonLink( - `https://www.blitzstars.com/player/${player.region}/${nickname}`, - 'BlitzStars', - ), - ]; - }, - - autocomplete: autocompleteUsername, - - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - - return await render(player); + handler() { + return '# This command has been renamed to `/breakdown today`\n`/today` now comes with filters, periods and more! Use `/breakdown today` for same old functionality but also do checkout the other options.'; }, }; diff --git a/src/components/Breakdown/components/RowStat.tsx b/src/components/Breakdown/components/RowStat.tsx index 3c0cda7c0..17b1cd6a2 100644 --- a/src/components/Breakdown/components/RowStat.tsx +++ b/src/components/Breakdown/components/RowStat.tsx @@ -15,7 +15,7 @@ export function RowStat({ value, delta, percentile, - minimized, + minimized = false, }: RowStatProps) { return (