mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: dailystats
This commit is contained in:
1 parent
f729aee3de
commit
e37a7947b4
18 files changed
+637
-153
No files matched your search
+81
-78
@@ -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<Period, string> = {
|
||||
|
||||
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}`);
|
||||
|
||||
@@ -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<AccountInfo>(
|
||||
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
|
||||
);
|
||||
if (!accountInfo) return;
|
||||
const clanData = await getWargamingResponse<PlayerClanData>(
|
||||
`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<TanksStats>(
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
|
||||
);
|
||||
if (!careerTankStatsRaw) return;
|
||||
const careerStats: Record<number, AllStats> = {};
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,183 @@
|
||||
import Children from './Children.js';
|
||||
|
||||
const Breakdown = {
|
||||
Root(...children: string[]) {
|
||||
return `
|
||||
<style>
|
||||
.breakdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown">
|
||||
${Children(children)}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
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 `
|
||||
<style>
|
||||
.breakdown-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown-row">
|
||||
${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))}
|
||||
</div>
|
||||
`;
|
||||
|
||||
/*
|
||||
${this.RowStat(
|
||||
'WN8',
|
||||
wn8.toFixed(0),
|
||||
careerWn8.toFixed(0),
|
||||
wn8 - careerWn8,
|
||||
)}
|
||||
*/
|
||||
},
|
||||
|
||||
RowDiscriminator(name: string, icon?: string) {
|
||||
return `
|
||||
<style>
|
||||
.breakdown-row-discriminator {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
width: 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.breakdown-row-discriminator-icon {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.breakdown-row-discriminator-name {
|
||||
color: #A0A0A0;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown-row-discriminator">
|
||||
${
|
||||
icon
|
||||
? `<img src="${icon}" class="breakdown-row-discriminator-icon" />`
|
||||
: ''
|
||||
}
|
||||
<span class="breakdown-row-discriminator-name">${name}</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
RowStat(name: string, value: string, career: string, delta?: number) {
|
||||
return `
|
||||
<style>
|
||||
.breakdown-row-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-career {
|
||||
color: #A0A0A0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-name {
|
||||
color: #A0A0A0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-value {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-value-delta {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.breakdown-row-stat-value-text {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="breakdown-row-stat">
|
||||
<span class="breakdown-row-stat-career">${career}</span>
|
||||
|
||||
<div class="breakdown-row-stat-value">
|
||||
${
|
||||
delta
|
||||
? `<img src="${
|
||||
(delta ?? 0) > 0
|
||||
? 'https://i.imgur.com/qbjiXa1.png'
|
||||
: 'https://i.imgur.com/3uyNhun.png'
|
||||
}" class="breakdown-row-stat-value-delta" />`
|
||||
: ''
|
||||
}
|
||||
|
||||
<span class="breakdown-row-stat-value-text">${value}</span>
|
||||
</div>
|
||||
|
||||
<span class="breakdown-row-stat-name">${name}</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
export default Breakdown;
|
||||
@@ -0,0 +1,20 @@
|
||||
export default function NoBattlesInPeriod() {
|
||||
return `
|
||||
<style>
|
||||
.no-battles-in-period {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-battles-in-period-text {
|
||||
font-size: 16px;
|
||||
color: #A0A0A0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="no-battles-in-period">
|
||||
<span class="no-battles-in-period-text">No battles played in this period</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export default function PoweredByBlitzStars() {
|
||||
return `
|
||||
<style>
|
||||
.powered-by-blitz-stars {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.powered-by-blitz-stars-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.powered-by-blitz-stars-text {
|
||||
font-size: 16px;
|
||||
color: #A0A0A0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="powered-by-blitz-stars">
|
||||
<img class="powered-by-blitz-stars-icon" src="https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png" />
|
||||
<span class="powered-by-blitz-stars-text">Powered by BlitzStars</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Children from './Children.js';
|
||||
|
||||
export default function Screenshot(...children: string[]) {
|
||||
export default function Root(...children: string[]) {
|
||||
return `
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:[email protected]');
|
||||
@@ -14,12 +14,12 @@ export default function Screenshot(...children: string[]) {
|
||||
font-family: 'Roboto Flex', sans-serif;
|
||||
}
|
||||
|
||||
#screenshot {
|
||||
#root {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="screenshot">
|
||||
<div id="root">
|
||||
${Children(children)}
|
||||
</div>
|
||||
`;
|
||||
@@ -1,8 +1,8 @@
|
||||
export default function TitleBar(
|
||||
name: string,
|
||||
image: string,
|
||||
nameDiscriminator = '',
|
||||
description = '',
|
||||
nameDiscriminator: string,
|
||||
image?: string,
|
||||
description?: string,
|
||||
) {
|
||||
return `
|
||||
<style>
|
||||
@@ -51,7 +51,7 @@ export default function TitleBar(
|
||||
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-info">
|
||||
<img class="title-bar-avatar" src="${image}" />
|
||||
${image ? `<img class="title-bar-avatar" src="${image}" />` : ''}
|
||||
|
||||
<div class="title-bar-text">
|
||||
<span class="title-bar-name">
|
||||
|
||||
@@ -12,7 +12,9 @@ export default async function getWargamingResponse<Data extends object>(
|
||||
if (parsed.status === 'ok') {
|
||||
return parsed.data;
|
||||
} else {
|
||||
console.error(`Wargaming response status ${parsed.status}`);
|
||||
console.error(
|
||||
`Wargaming response status ${parsed.status} ${parsed.error.message}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import fetch from 'node-fetch';
|
||||
import { BlitzServer } from '../../constants/servers.js';
|
||||
import { AllStats } from '../../types/accountInfo.js';
|
||||
import { TanksStats } from '../../types/tanksStats.js';
|
||||
import getWargamingResponse from '../blitz/getWargamingResponse.js';
|
||||
import { args } from '../process/args.js';
|
||||
|
||||
export interface TankHistoryNode {
|
||||
all: AllStats;
|
||||
last_battle_time: number;
|
||||
mark_of_mastery: number;
|
||||
battle_life_time: number;
|
||||
account_id: number;
|
||||
tank_id: number;
|
||||
}
|
||||
|
||||
export const emptyTankHistoryNode: TankHistoryNode = {
|
||||
account_id: 0,
|
||||
battle_life_time: 0,
|
||||
last_battle_time: 0,
|
||||
mark_of_mastery: 0,
|
||||
tank_id: 0,
|
||||
all: {
|
||||
battles: 0,
|
||||
capture_points: 0,
|
||||
damage_dealt: 0,
|
||||
damage_received: 0,
|
||||
dropped_capture_points: 0,
|
||||
frags: 0,
|
||||
frags8p: 0,
|
||||
hits: 0,
|
||||
losses: 0,
|
||||
max_frags: 0,
|
||||
max_xp: 0,
|
||||
shots: 0,
|
||||
spotted: 0,
|
||||
survived_battles: 0,
|
||||
xp: 0,
|
||||
win_and_survived: 0,
|
||||
wins: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export type TankHistory = TankHistoryNode[];
|
||||
|
||||
export default async function getTankStatsOverTime(
|
||||
server: BlitzServer,
|
||||
id: number,
|
||||
start = -Infinity,
|
||||
end = Infinity,
|
||||
) {
|
||||
const latestTankStatsRaw = await getWargamingResponse<TanksStats>(
|
||||
`https://api.wotblitz.${server}/wotb/tanks/stats/?application_id=${args['wargaming-application-id']}&account_id=${id}`,
|
||||
);
|
||||
if (!latestTankStatsRaw) return;
|
||||
const tankHistoriesResponse = await fetch(
|
||||
`https://www.blitzstars.com/api/tankhistories/for/${id}/`,
|
||||
);
|
||||
const tankHistories = (await tankHistoriesResponse.json()) as TankHistory;
|
||||
const history = [...tankHistories, ...latestTankStatsRaw[id]];
|
||||
|
||||
// sort them even though most won't be used
|
||||
const tankSortedHistory: Record<number, TankHistory> = {};
|
||||
const tankIds: number[] = [];
|
||||
history.forEach((node) => {
|
||||
if (!tankSortedHistory[node.tank_id]) {
|
||||
tankSortedHistory[node.tank_id] = [node];
|
||||
tankIds.push(node.tank_id);
|
||||
} else {
|
||||
tankSortedHistory[node.tank_id].push(node);
|
||||
}
|
||||
});
|
||||
|
||||
// find the ones played within the range
|
||||
const tankIdsInRange = tankIds.filter((tankId) =>
|
||||
tankSortedHistory[tankId].some(
|
||||
(node) => node.last_battle_time > start && node.last_battle_time < end,
|
||||
),
|
||||
);
|
||||
|
||||
// fetch history node right before "start" and right before "end"
|
||||
const tanksBeforeAndAfter: Record<number, AllStats> = {};
|
||||
tankIdsInRange.forEach((tankId) => {
|
||||
const sortedHistory = tankSortedHistory[tankId];
|
||||
const previousIndex =
|
||||
sortedHistory.findIndex((node) => node.last_battle_time > start) - 1;
|
||||
const latestIndex =
|
||||
sortedHistory.findIndex((node) => node.last_battle_time > end) - 1;
|
||||
const previous = sortedHistory[previousIndex] ?? emptyTankHistoryNode;
|
||||
const latest =
|
||||
sortedHistory[latestIndex] ?? sortedHistory[sortedHistory.length - 1];
|
||||
|
||||
function diff(get: (allStats: AllStats) => number) {
|
||||
return get(latest.all) - get(previous.all);
|
||||
}
|
||||
function max(get: (allStats: AllStats) => number) {
|
||||
return Math.max(get(latest.all), get(previous.all));
|
||||
}
|
||||
|
||||
tanksBeforeAndAfter[tankId] = {
|
||||
battles: diff((a) => a.battles),
|
||||
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: diff((a) => a.frags),
|
||||
frags8p: diff((a) => a.frags8p),
|
||||
hits: diff((a) => a.hits),
|
||||
losses: diff((a) => a.losses),
|
||||
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),
|
||||
win_and_survived: diff((a) => a.win_and_survived),
|
||||
wins: diff((a) => a.wins),
|
||||
xp: diff((a) => a.xp),
|
||||
};
|
||||
});
|
||||
|
||||
return tanksBeforeAndAfter;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function last5AM() {
|
||||
const fiveAM = new Date();
|
||||
fiveAM.setHours(5, 0, 0, 0); // Set the time to 5:00 AM today
|
||||
|
||||
if (fiveAM > new Date()) {
|
||||
// If it's now before 5:00 AM, go back one day
|
||||
fiveAM.setDate(fiveAM.getDate() - 1);
|
||||
}
|
||||
|
||||
return fiveAM;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Root from '../../components/Root.js';
|
||||
import { browser } from '../../index.js';
|
||||
|
||||
export default async function screenshotHTML(children: string) {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewportSize({ width: 640, height: 1 });
|
||||
await page.setContent(Root(children));
|
||||
const screenshot = await page
|
||||
.locator('#root')
|
||||
.screenshot({ omitBackground: true, scale: 'css' });
|
||||
await page.close();
|
||||
|
||||
return screenshot;
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
export interface AllStats {
|
||||
spotted: number;
|
||||
max_frags_tank_id: number;
|
||||
hits: number;
|
||||
frags: number;
|
||||
max_xp: number;
|
||||
max_xp_tank_id: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
capture_points: number;
|
||||
|
||||
+17
-31
@@ -1,33 +1,19 @@
|
||||
import { AllStats } from './accountInfo.js';
|
||||
|
||||
export interface IndividualTankStats {
|
||||
all: AllStats;
|
||||
last_battle_time: number;
|
||||
account_id: number;
|
||||
max_xp: number;
|
||||
in_garage_updated: number;
|
||||
max_frags: number;
|
||||
frags: number | null;
|
||||
mark_of_mastery: number;
|
||||
battle_life_time: number;
|
||||
in_garage: number | null;
|
||||
tank_id: number;
|
||||
}
|
||||
|
||||
export type TanksStats = {
|
||||
[accountId: number]: {
|
||||
all: {
|
||||
spotted: number;
|
||||
hits: number;
|
||||
frags: number;
|
||||
max_xp: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
capture_points: number;
|
||||
battles: number;
|
||||
damage_dealt: number;
|
||||
damage_received: number;
|
||||
max_frags: number;
|
||||
shots: number;
|
||||
frags8p: number;
|
||||
xp: number;
|
||||
win_and_survived: number;
|
||||
survived_battles: number;
|
||||
dropped_capture_points: number;
|
||||
};
|
||||
last_battle_time: number;
|
||||
account_id: number;
|
||||
max_xp: number;
|
||||
in_garage_updated: number;
|
||||
max_frags: number;
|
||||
frags: number | null;
|
||||
mark_of_mastery: number;
|
||||
battle_life_time: number;
|
||||
in_garage: number | null;
|
||||
tank_id: number;
|
||||
}[];
|
||||
[accountId: number]: IndividualTankStats[];
|
||||
};
|
||||
@@ -1,6 +1,12 @@
|
||||
export type WargamingResponse<Data extends object> =
|
||||
| {
|
||||
status: 'error';
|
||||
error: {
|
||||
field: string;
|
||||
message: string;
|
||||
code: 402;
|
||||
value: null;
|
||||
};
|
||||
}
|
||||
| {
|
||||
status: 'ok';
|
||||
|
||||
Reference in new issue
Block a user