From e37a7947b4533cecd7232eda8d4583563ef66fa8 Mon Sep 17 00:00:00 2001 From: tresabhi Date: Thu, 25 May 2023 18:20:51 +0000 Subject: [PATCH] feat: dailystats --- .github/workflows/host.yml | 3 - .gitpod.yml | 1 + package.json | 1 + src/commands/stats.ts | 159 ++++++++--------- src/commands/today.ts | 114 ++++++++++++ src/components/Breakdown.ts | 183 ++++++++++++++++++++ src/components/NoBattlesInPeriod.ts | 20 +++ src/components/PoweredByBlitzStars.ts | 26 +++ src/components/{Screenshot.ts => Root.ts} | 6 +- src/components/TitleBar.ts | 8 +- src/core/blitz/getWargamingResponse.ts | 4 +- src/core/blitzstars/getTankStatsOverTime.ts | 122 +++++++++++++ src/core/blitzstars/last5AM.ts | 11 ++ src/core/ui/screenshotHTML.ts | 14 ++ src/types/accountInfo.ts | 2 - src/types/tanksStats.ts | 48 ++--- src/types/wargamingResponse.ts | 6 + yarn.lock | 62 +++---- 18 files changed, 637 insertions(+), 153 deletions(-) create mode 100644 src/commands/today.ts create mode 100644 src/components/Breakdown.ts create mode 100644 src/components/NoBattlesInPeriod.ts create mode 100644 src/components/PoweredByBlitzStars.ts rename src/components/{Screenshot.ts => Root.ts} (80%) create mode 100644 src/core/blitzstars/getTankStatsOverTime.ts create mode 100644 src/core/blitzstars/last5AM.ts create mode 100644 src/core/ui/screenshotHTML.ts diff --git a/.github/workflows/host.yml b/.github/workflows/host.yml index eb13bde6e..c7ec48d94 100644 --- a/.github/workflows/host.yml +++ b/.github/workflows/host.yml @@ -43,8 +43,5 @@ jobs: - name: Install dependencies run: yarn - - name: Install playwright - run: npx playwright install - - name: Host run: yarn run start discord-token=${{ secrets.DISCORD_TOKEN }} wargaming-application-id=${{ secrets.WARGAMING_APPLICATION_ID }} diff --git a/.gitpod.yml b/.gitpod.yml index 58144330d..b9050fcac 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,2 +1,3 @@ tasks: - init: yarn + - command: npx playwright install && npx playwright install-deps diff --git a/package.json b/package.json index bd8cc4066..5b575fd11 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "type": "module", "scripts": { + "install": "playwright install && playwright install-deps", "start": "vite-node src/index.ts --script", "lint": "tsc" }, diff --git a/src/commands/stats.ts b/src/commands/stats.ts index 109006d8e..13cf5f874 100644 --- a/src/commands/stats.ts +++ b/src/commands/stats.ts @@ -1,7 +1,8 @@ import { SlashCommandBuilder } from 'discord.js'; import escapeHTML from 'escape-html'; import GenericStats from '../components/GenericStats/index.js'; -import Screenshot from '../components/Screenshot.js'; +import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js'; +import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js'; import TitleBar from '../components/TitleBar.js'; import Wrapper from '../components/Wrapper.js'; import { BLITZ_SERVERS } from '../constants/servers.js'; @@ -12,8 +13,8 @@ import getBlitzStarsAccount from '../core/blitzstars/getBlitzStarsAccount.js'; import cmdName from '../core/interaction/cmdName.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; import { args } from '../core/process/args.js'; +import screenshotHTML from '../core/ui/screenshotHTML.js'; import { CommandRegistry } from '../events/interactionCreate.js'; -import { browser } from '../index.js'; import { AccountInfo, AllStats } from '../types/accountInfo.js'; import { PlayerClanData } from '../types/playerClanData.js'; import { BlitzStartsComputedPeriodicStatistics } from '../types/statistics.js'; @@ -29,7 +30,7 @@ const periodNames: Record = { export default { inProduction: true, - inDevelopment: true, + inDevelopment: false, inPublic: true, command: new SlashCommandBuilder() @@ -72,8 +73,8 @@ export default { const a2 = accountInfo[id].statistics; const battles = a2.all.battles - a1.all.battles; - function pb(value: (allStats: AllStats) => number) { - return (value(a2.all) - value(a1.all)) / battles; + function max(value: (allStats: AllStats) => number) { + return Math.max(value(a2.all), value(a1.all)); } function diff(value: (allStats: AllStats) => number) { return value(a2.all) - value(a1.all); @@ -82,18 +83,16 @@ export default { stats = { all: { battles, - capture_points: pb((a) => a.capture_points), - damage_dealt: pb((a) => a.damage_dealt), - damage_received: pb((a) => a.damage_received), + capture_points: diff((a) => a.capture_points), + damage_dealt: diff((a) => a.damage_dealt), + damage_received: diff((a) => a.damage_received), dropped_capture_points: diff((a) => a.dropped_capture_points), - frags: pb((a) => a.frags), - frags8p: pb((a) => a.frags8p), + frags: diff((a) => a.frags), + frags8p: diff((a) => a.frags8p), hits: diff((a) => a.hits), losses: diff((a) => a.losses), - max_frags: pb((a) => a.max_frags), - max_frags_tank_id: pb((a) => a.max_frags_tank_id), - max_xp: pb((a) => a.max_xp), - max_xp_tank_id: pb((a) => a.max_xp_tank_id), + max_frags: max((a) => a.max_frags), + max_xp: max((a) => a.max_xp), shots: diff((a) => a.shots), spotted: diff((a) => a.spotted), survived_battles: diff((a) => a.survived_battles), @@ -113,73 +112,77 @@ export default { } if (!stats) return; - const page = await browser.newPage(); - await page.setViewportSize({ width: 640, height: 1 }); - await page.setContent( - Screenshot( - Wrapper( - TitleBar( - escapeHTML(blitzStarsAccount.nickname), - clanData[id]?.clan - ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png` - : 'https://i.imgur.com/uq6pKbh.png', - clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '', - `${periodNames[period]} • ${new Date().toDateString()} • ${ - BLITZ_SERVERS[server] - }`, - ), - GenericStats([ - [ - 'Winrate', - `${(100 * (stats.all.wins / stats.all.battles)).toFixed(2)}%`, - ], - ['WN8', stats.wn8.toFixed(0)], - [ - 'Survival', - `${( - 100 * - (stats.all.survived_battles / stats.all.battles) - ).toFixed(2)}%`, - ], - [ - 'Accuracy', - `${((stats.all.hits / stats.all.shots) * 100).toFixed(2)}%`, - ], - ['Battles', stats.all.battles], - ['Wins', stats.all.wins], - ['Losses', stats.all.losses], - [ - 'Average damage', - (stats.all.damage_dealt / stats.all.battles).toFixed(0), - ], - ['Average XP', (stats.all.xp / stats.all.battles).toFixed(0)], - ['Average shots', (stats.all.shots / stats.all.battles).toFixed(2)], - ['Average hits', (stats.all.hits / stats.all.battles).toFixed(2)], - ['Average kills', (stats.all.frags / stats.all.battles).toFixed(2)], - [ - 'Average spots', - (stats.all.spotted / stats.all.battles).toFixed(2), - ], - ['Average tier', stats.avg_tier.toFixed(2)], - [ - 'Damage ratio', - (stats.all.damage_dealt / stats.all.damage_received).toFixed(2), - ], - [ - 'Kills to death ratio', - ( - stats.all.frags / - (stats.all.battles - stats.all.survived_battles) - ).toFixed(2), - ], - ]), + const screenshot = await screenshotHTML( + Wrapper( + TitleBar( + escapeHTML(blitzStarsAccount.nickname), + clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '', + clanData[id]?.clan + ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png` + : undefined, + `${periodNames[period]} • ${new Date().toDateString()} • ${ + BLITZ_SERVERS[server] + }`, ), + + stats.all.battles === 0 + ? NoBattlesInPeriod() + : GenericStats([ + [ + 'Winrate', + `${(100 * (stats.all.wins / stats.all.battles)).toFixed(2)}%`, + ], + ['WN8', stats.wn8.toFixed(0)], + [ + 'Survival', + `${( + 100 * + (stats.all.survived_battles / stats.all.battles) + ).toFixed(2)}%`, + ], + [ + 'Accuracy', + `${((stats.all.hits / stats.all.shots) * 100).toFixed(2)}%`, + ], + ['Battles', stats.all.battles], + ['Wins', stats.all.wins], + ['Losses', stats.all.losses], + [ + 'Average damage', + (stats.all.damage_dealt / stats.all.battles).toFixed(0), + ], + ['Average XP', (stats.all.xp / stats.all.battles).toFixed(0)], + [ + 'Average shots', + (stats.all.shots / stats.all.battles).toFixed(2), + ], + ['Average hits', (stats.all.hits / stats.all.battles).toFixed(2)], + [ + 'Average kills', + (stats.all.frags / stats.all.battles).toFixed(2), + ], + [ + 'Average spots', + (stats.all.spotted / stats.all.battles).toFixed(2), + ], + ['Average tier', stats.avg_tier.toFixed(2)], + [ + 'Damage ratio', + (stats.all.damage_dealt / stats.all.damage_received).toFixed(2), + ], + [ + 'Kills to death ratio', + ( + stats.all.frags / + (stats.all.battles - stats.all.survived_battles) + ).toFixed(2), + ], + ]), + + PoweredByBlitzStars(), ), ); - const screenshot = await page - .locator('#screenshot') - .screenshot({ omitBackground: true, scale: 'css' }); - await page.close(); + await interaction.editReply({ files: [screenshot] }); console.log(`Showing stats for ${blitzStarsAccount.nickname}`); diff --git a/src/commands/today.ts b/src/commands/today.ts new file mode 100644 index 000000000..153bc5cba --- /dev/null +++ b/src/commands/today.ts @@ -0,0 +1,114 @@ +import { SlashCommandBuilder } from 'discord.js'; +import escapeHTML from 'escape-html'; +import Breakdown from '../components/Breakdown.js'; +import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js'; +import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js'; +import TitleBar from '../components/TitleBar.js'; +import Wrapper from '../components/Wrapper.js'; +import { BLITZ_SERVERS } from '../constants/servers.js'; +import usernameAutocomplete from '../core/autocomplete/username.js'; +import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; +import getWargamingResponse from '../core/blitz/getWargamingResponse.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 addUsernameOption from '../core/options/addUsernameOption.js'; +import { args } from '../core/process/args.js'; +import screenshotHTML from '../core/ui/screenshotHTML.js'; +import { CommandRegistry } from '../events/interactionCreate.js'; +import { AccountInfo, AllStats } from '../types/accountInfo.js'; +import { PlayerClanData } from '../types/playerClanData.js'; +import { TanksStats } from '../types/tanksStats.js'; +import resolveTankName from '../utilities/resolveTankName.js'; + +export default { + inProduction: true, + inDevelopment: true, + inPublic: true, + + command: new SlashCommandBuilder() + .setName(cmdName('today')) + .setDescription('A general daily breakdown of your performance') + .addStringOption(addUsernameOption), + + async execute(interaction) { + const username = interaction.options.getString('username')!; + const blitzAccount = await getBlitzAccount(interaction, username); + if (!blitzAccount) return; + const { id, server } = blitzAccount; + const tankStatsOverTime = await getTankStatsOverTime( + server, + id, + last5AM().getTime() / 1000, + new Date().getTime() / 1000, + ); + if (!tankStatsOverTime) return; + const accountInfo = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`, + ); + if (!accountInfo) return; + const clanData = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${args['wargaming-application-id']}&account_id=${id}&extra=clan`, + ); + if (!clanData) return; + const careerTankStatsRaw = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`, + ); + if (!careerTankStatsRaw) return; + const careerStats: Record = {}; + + Object.entries(careerTankStatsRaw[id]).forEach(([, tankStats]) => { + careerStats[tankStats.tank_id] = tankStats.all; + }); + + const rows = Object.entries(tankStatsOverTime).map( + ([tankId, tankStats]) => { + const career = careerStats[tankId as unknown as number]; + + return Breakdown.Row( + resolveTankName({ + tank_id: tankId as unknown as number, + name: tankopedia.data[tankId as unknown as number].name, + }), + tankStats.wins / tankStats.battles, + career.wins / career.battles, + -Infinity, + -Infinity, + tankStats.damage_dealt / tankStats.battles, + career.damage_dealt / career.battles, + (tankStats.battles - tankStats.survived_battles) / tankStats.battles, + (career.battles - career.survived_battles) / career.battles, + tankStats.battles, + career.battles, + tankopedia.data[tankId as unknown as number].images.normal, + ); + }, + ); + + const screenshot = await screenshotHTML( + Wrapper( + TitleBar( + escapeHTML(accountInfo[id].nickname), + clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '', + clanData[id]?.clan + ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png` + : undefined, + `Today's breakdown • ${new Date().toDateString()} • ${ + BLITZ_SERVERS[server] + }`, + ), + + rows.length === 0 ? NoBattlesInPeriod() : Breakdown.Root(...rows), + + PoweredByBlitzStars(), + ), + ); + + await interaction.editReply({ files: [screenshot] }); + + console.log(`Showing daily breakdown for ${accountInfo[id].nickname}`); + }, + + autocomplete: usernameAutocomplete, +} satisfies CommandRegistry; diff --git a/src/components/Breakdown.ts b/src/components/Breakdown.ts new file mode 100644 index 000000000..452ad8979 --- /dev/null +++ b/src/components/Breakdown.ts @@ -0,0 +1,183 @@ +import Children from './Children.js'; + +const Breakdown = { + Root(...children: string[]) { + return ` + + +
+ ${Children(children)} +
+ `; + }, + + Row( + name: string, + winrate: number, + careerWinrate: number, + wn8: number, + careerWn8: number, + averageDamage: number, + careerAverageDamage: number, + survival: number, + careerSurvival: number, + battles: number, + careerBattles: number, + icon?: string, + ) { + return ` + + +
+ ${this.RowDiscriminator(name, icon)} + ${this.RowStat( + 'Winrate', + `${(winrate * 100).toFixed(2)}%`, + `${(careerWinrate * 100).toFixed(2)}%`, + winrate - careerWinrate, + )} + ${this.RowStat( + 'Damage', + averageDamage.toFixed(2), + careerAverageDamage.toFixed(2), + averageDamage - careerAverageDamage, + )} + ${this.RowStat( + 'Survival', + `${(survival * 100).toFixed(2)}%`, + `${(careerSurvival * 100).toFixed(2)}%`, + survival - careerSurvival, + )} + ${this.RowStat('Battles', battles.toFixed(0), careerBattles.toFixed(0))} +
+ `; + + /* + ${this.RowStat( + 'WN8', + wn8.toFixed(0), + careerWn8.toFixed(0), + wn8 - careerWn8, + )} + */ + }, + + RowDiscriminator(name: string, icon?: string) { + return ` + + +
+ ${ + icon + ? `` + : '' + } + ${name} +
+ `; + }, + + RowStat(name: string, value: string, career: string, delta?: number) { + return ` + + +
+ ${career} + +
+ ${ + delta + ? `` + : '' + } + + ${value} +
+ + ${name} +
+ `; + }, +}; + +export default Breakdown; diff --git a/src/components/NoBattlesInPeriod.ts b/src/components/NoBattlesInPeriod.ts new file mode 100644 index 000000000..b243662db --- /dev/null +++ b/src/components/NoBattlesInPeriod.ts @@ -0,0 +1,20 @@ +export default function NoBattlesInPeriod() { + return ` + + +
+ No battles played in this period +
+ `; +} diff --git a/src/components/PoweredByBlitzStars.ts b/src/components/PoweredByBlitzStars.ts new file mode 100644 index 000000000..581145730 --- /dev/null +++ b/src/components/PoweredByBlitzStars.ts @@ -0,0 +1,26 @@ +export default function PoweredByBlitzStars() { + return ` + + +
+ + Powered by BlitzStars +
+ `; +} diff --git a/src/components/Screenshot.ts b/src/components/Root.ts similarity index 80% rename from src/components/Screenshot.ts rename to src/components/Root.ts index 3d5d40615..23d627caf 100644 --- a/src/components/Screenshot.ts +++ b/src/components/Root.ts @@ -1,6 +1,6 @@ import Children from './Children.js'; -export default function Screenshot(...children: string[]) { +export default function Root(...children: string[]) { return ` -
+
${Children(children)}
`; diff --git a/src/components/TitleBar.ts b/src/components/TitleBar.ts index b99509609..4886d4136 100644 --- a/src/components/TitleBar.ts +++ b/src/components/TitleBar.ts @@ -1,8 +1,8 @@ export default function TitleBar( name: string, - image: string, - nameDiscriminator = '', - description = '', + nameDiscriminator: string, + image?: string, + description?: string, ) { return `