diff --git a/.gitignore b/.gitignore index 38cfabf71..6dc41db35 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ package-lock.json dist/ yarn-error.log dist.zip -commands.txt \ No newline at end of file +commands.txt +/test.* \ No newline at end of file diff --git a/package.json b/package.json index dc2934a1b..22972cbae 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "src/index.ts", "type": "module", "scripts": { - "dev": "yarn build:raw --dev && yarn start", + "dev": "yarn build:raw --dev && yarn start --dev", "start": "node --enable-source-maps --trace-warnings dist/index.cjs", "zip": "zip -r dist.zip dist", "build:raw": "node scripts/build", diff --git a/src/commands/evolution.tsx b/src/commands/evolution.tsx index 27a6599d0..6a46cddbc 100644 --- a/src/commands/evolution.tsx +++ b/src/commands/evolution.tsx @@ -12,6 +12,7 @@ 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 getPlayerHistories from '../core/blitzstars/getPlayerHistories.js'; import getTankHistories from '../core/blitzstars/getTankHistories.js'; import { supportBlitzStars } from '../core/interaction/supportBlitzStars.js'; import { @@ -23,6 +24,7 @@ import getPeriodDataFromSubcommand from '../core/options/getPeriodDataFromSubcom import { WARGAMING_APPLICATION_ID } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; import { AccountInfo } from '../types/accountInfo.js'; +import { Histories } from '../types/histories.js'; import { PlayerClanData } from '../types/playerClanData.js'; export default { @@ -61,36 +63,40 @@ export default { image = tankopedia[tankId].images.normal; } + let histories: Histories; + const { evolutionName, start, end } = getPeriodDataFromSubcommand(interaction); - let careerWinrate: Graph.Plot; - let careerBattles: Graph.Plot; - if (commandGroup === 'player') { + histories = await getPlayerHistories(server, id, { + start, + end, + includeLatestHistories: true, + includePreviousHistories: true, + }); } else { const tankId = resolveTankId(tankIdRaw); - const histories = await getTankHistories(server, id, { + histories = await getTankHistories(server, id, { tankId, start, end, includeLatestHistories: true, includePreviousHistories: true, }); - - careerWinrate = histories.map( - (history) => - [ - history.last_battle_time, - (history.all.wins / history.all.battles) * 100, - ] as Graph.PlotItem, - ); - careerBattles = histories.map( - (history) => - [history.last_battle_time, history.all.battles] as Graph.PlotItem, - ); } + const careerWinrate = histories.map( + (history) => + [ + history.last_battle_time, + (history.all.wins / history.all.battles) * 100, + ] as Graph.PlotItem, + ); + const careerBattles = histories.map( + (history) => + [history.last_battle_time, history.all.battles] as Graph.PlotItem, + ); const winrateYs = careerWinrate.map(([, y]) => y); const maxWinrate = Math.max(...winrateYs); const minWinrate = Math.min(...winrateYs); @@ -121,7 +127,7 @@ export default { interaction.options.getSubcommand() as Period ] } - xMaxLabel="Today" + xMaxLabel="Latest recorded battle" leftVerticalMargin={{ min: minWinrate, max: maxWinrate, diff --git a/src/components/GenericStats/components/GenericStatsRow.tsx b/src/components/GenericStats/components/GenericStatsRow.tsx index 449e9d5e1..1630cccb8 100644 --- a/src/components/GenericStats/components/GenericStatsRow.tsx +++ b/src/components/GenericStats/components/GenericStatsRow.tsx @@ -1,8 +1,6 @@ import { theme } from '../../../stitches.config.js'; -import { Stat } from '../index.js'; - -import '../../../core/blitz/getWN8Percentile.js'; import PercentileIndicator from '../../PercentileIndicator.js'; +import { Stat } from '../index.js'; export interface GenericStatsRowProps { stat: Stat; diff --git a/src/core/blitzstars/getPlayerHistories.ts b/src/core/blitzstars/getPlayerHistories.ts new file mode 100644 index 000000000..d52cfd151 --- /dev/null +++ b/src/core/blitzstars/getPlayerHistories.ts @@ -0,0 +1,92 @@ +import { BlitzServer } from '../../constants/servers.js'; +import { AccountInfo, AllStats } from '../../types/accountInfo.js'; +import { + GetHistoriesOptions, + Histories, + History, + getHistoriesDefaultOptions, +} from '../../types/histories.js'; +import getWargamingResponse from '../blitz/getWargamingResponse.js'; +import { WARGAMING_APPLICATION_ID } from '../process/args.js'; +import { emptyAllStats } from './getTankStatsOverTime.js'; + +export interface PlayerHistoryRaw { + clan: { clan_id: number; name: string; tag: string }; + statistics: { + all: AllStats; + wn8: number; + wn7: number; + average_tier: number; + }; + last_battle_time: number; + nickname: string; + account_id: number; + achievements: { + account_id: number; + [key: string]: number; + }; + region: BlitzServer; +} + +export type PlayerHistoriesRaw = PlayerHistoryRaw[]; + +export default async function getPlayerHistories( + server: BlitzServer, + id: number, + options?: Partial, +) { + const mergedOptions = { ...getHistoriesDefaultOptions, ...options }; + const response = await fetch( + `https://www.blitzstars.com/api/playerstats/${id}`, + ); + const playerHistories = ((await response.json()) as PlayerHistoriesRaw).map( + (history) => + ({ + all: history.statistics.all, + last_battle_time: history.last_battle_time, + } satisfies History), + ); + let lastNonCompliantIndex = -1; + let hasCompliedOnce = false; + const filtered = playerHistories.filter(({ last_battle_time }, index) => { + const complies = + last_battle_time >= mergedOptions.start && + last_battle_time <= mergedOptions.end; + + if (complies && !hasCompliedOnce) hasCompliedOnce = true; + if (!hasCompliedOnce) lastNonCompliantIndex = index; + + return complies; + }); + + const latestNodes: Histories = []; + + if (mergedOptions.includeLatestHistories) { + const { + last_battle_time, + statistics: { all }, + } = ( + await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, + ) + )[id]; + + latestNodes.push({ all, last_battle_time }); + } + + const previousNodes: Histories = []; + + if (mergedOptions.includePreviousHistories) { + if (lastNonCompliantIndex !== -1) { + previousNodes.push( + playerHistories[lastNonCompliantIndex] ?? + ({ + all: emptyAllStats, + last_battle_time: -Infinity, + } satisfies History), + ); + } + } + + return [...latestNodes, ...filtered, ...previousNodes] satisfies Histories; +} diff --git a/src/core/blitzstars/getTankHistories.ts b/src/core/blitzstars/getTankHistories.ts index 751d7b55f..6f4bca99c 100644 --- a/src/core/blitzstars/getTankHistories.ts +++ b/src/core/blitzstars/getTankHistories.ts @@ -1,41 +1,34 @@ import { BlitzServer } from '../../constants/servers.js'; -import { AllStats } from '../../types/accountInfo.js'; +import { + GetHistoriesOptions, + Histories, + History, + getHistoriesDefaultOptions, +} from '../../types/histories.js'; import getTankStats from '../blitz/getTankStats.js'; import { emptyAllStats } from './getTankStatsOverTime.js'; -export interface TankHistory { - all: AllStats; - last_battle_time: number; - mark_of_mastery: number; - battle_life_time: number; - account_id: number; +export interface TankHistory extends History { tank_id: number; } -export const emptyTankHistory: TankHistory = { - all: emptyAllStats, - last_battle_time: -Infinity, - mark_of_mastery: 0, - battle_life_time: 0, - account_id: 0, - tank_id: 0, -}; - export type TankHistories = TankHistory[]; -export interface GetTankHistoriesOptions { - start: number; - end: number; - includeLatestHistories: boolean; - includePreviousHistories: boolean; +export interface TankHistoryRaw extends TankHistory { + mark_of_mastery: number; + battle_life_time: number; + account_id: number; +} + +export type TankHistoriesRaw = TankHistoryRaw[]; + +export interface GetTankHistoriesOptions extends GetHistoriesOptions { tankId?: number; } export const getTankHistoriesDefaultOptions: GetTankHistoriesOptions = { - start: -Infinity, - end: Infinity, - includeLatestHistories: false, - includePreviousHistories: false, + ...getHistoriesDefaultOptions, + tankId: undefined, }; export default async function getTankHistories( @@ -47,7 +40,16 @@ export default async function getTankHistories( const tankHistoriesResponse = await fetch( `https://www.blitzstars.com/api/tankhistories/for/${id}/`, ); - const tankHistories = (await tankHistoriesResponse.json()) as TankHistories; + const tankHistories = ( + (await tankHistoriesResponse.json()) as TankHistoriesRaw + ).map( + (history) => + ({ + all: history.all, + last_battle_time: history.last_battle_time, + tank_id: history.tank_id, + } satisfies TankHistory), + ); const compliantTanks: number[] = []; const lastBattles: Record = {}; const lastNonCompliantIndex: Record = {}; @@ -96,15 +98,16 @@ export default async function getTankHistories( compliantTanks.forEach((tankId) => { if (tankId in lastNonCompliantIndex) { previousNodes.push( - tankHistories[lastNonCompliantIndex[tankId]] ?? { - ...emptyTankHistory, - account_id: id, - tank_id: tankId, - }, + tankHistories[lastNonCompliantIndex[tankId]] ?? + ({ + all: emptyAllStats, + last_battle_time: -Infinity, + tank_id: tankId, + } satisfies TankHistory), ); } }); } - return [...previousNodes, ...filtered, ...latestNodes]; + return [...latestNodes, ...filtered, ...previousNodes] satisfies Histories; } diff --git a/src/core/blitzstars/getTankStatsOverTime.ts b/src/core/blitzstars/getTankStatsOverTime.ts index 526f1f451..d69fc4f80 100644 --- a/src/core/blitzstars/getTankStatsOverTime.ts +++ b/src/core/blitzstars/getTankStatsOverTime.ts @@ -1,8 +1,8 @@ import { BlitzServer } from '../../constants/servers.js'; import { AllStats } from '../../types/accountInfo.js'; import getTankHistories, { - TankHistories, - TankHistory, + TankHistoriesRaw, + TankHistoryRaw, } from './getTankHistories.js'; export const emptyAllStats: AllStats = { @@ -25,7 +25,7 @@ export const emptyAllStats: AllStats = { wins: 0, }; -export const emptyTankHistoryNode: TankHistory = { +export const emptyTankHistoryNode: TankHistoryRaw = { account_id: 0, battle_life_time: 0, last_battle_time: 0, @@ -45,7 +45,7 @@ export default async function getTankStatsOverTime( }); // sort them even though most won't be used - const tankSortedHistory: Record = {}; + const tankSortedHistory: Record = {}; const tankIds: number[] = []; history.forEach((node) => { if (!tankSortedHistory[node.tank_id]) { diff --git a/src/core/options/addPeriodSubCommands.ts b/src/core/options/addPeriodSubCommands.ts index ee949a6b0..b250c61fa 100644 --- a/src/core/options/addPeriodSubCommands.ts +++ b/src/core/options/addPeriodSubCommands.ts @@ -11,7 +11,7 @@ export const RELATIVE_PERIOD_NAMES: Record = { '30': '30 days ago', '60': '60 days ago', '90': '90 days ago', - career: 'Start of career', + career: 'Extent of data', custom: 'Custom period', }; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 3b0dd2a75..fb46359bf 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -124,20 +124,18 @@ try { }); console.log(`Refreshing ${guildCommands.length} guild command(s).`); - Promise.all([ - rest.put( - Routes.applicationGuildCommands(getClientId(), discord.sklld_guild_id), - { body: guildCommands }, - ), - rest.put( + rest + .put( Routes.applicationGuildCommands(getClientId(), discord.tres_guild_id), { body: guildCommands }, - ), - ]).then((guildData) => { - console.log( - `Successfully refreshed ${guildData.flat().length / 2} guild command(s).`, - ); - }); + ) + .then((guildData) => { + console.log( + `Successfully refreshed ${ + (guildData as unknown[]).length + } guild command(s).`, + ); + }); } catch (error) { console.error(error); } diff --git a/src/types/histories.ts b/src/types/histories.ts new file mode 100644 index 000000000..9258831f7 --- /dev/null +++ b/src/types/histories.ts @@ -0,0 +1,24 @@ +// a cleaner simplification of Blitz Stars' histories because it's a mess there + +import { AllStats } from './accountInfo.js'; + +export interface History { + all: AllStats; + last_battle_time: number; +} + +export type Histories = History[]; + +export interface GetHistoriesOptions { + start: number; + end: number; + includeLatestHistories: boolean; + includePreviousHistories: boolean; +} + +export const getHistoriesDefaultOptions: GetHistoriesOptions = { + start: -Infinity, + end: Infinity, + includeLatestHistories: false, + includePreviousHistories: false, +};