Compare commits

...
14 Commits
22 changed files with 235 additions and 148 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
# Blitzkrieg 1.14.1
# Blitzkrieg 1.14.2
A late night hot fix for my homies
+24
View File
@@ -0,0 +1,24 @@
# Blitzkrieg 1.14.3
Pull your brush out and start polishing.
## Changes
![HisRoyalFatness' career breakdown](https://i.imgur.com/GhgMTnS.png)
- Rows in `/breakdown` are now chunked into `8` per image
- This is true unless the number of pages exceeds `10` in which case the rows are even distributed across all `10` pages
- `10` is the maximum number of images that Discord allows
- Short region names are used in username autocomplete instead of their full names
- Example: "North America" is "NA" now
## Fixes
- Removed reference to username inference in the username option description
- Removed an unnecessary type `asd` that did absolutely nothing used nowhere
## Technical changes
- Image orders are now preserved even if processed in parallel and all completed out of order
- `fetchBlitz` is now throttled to `10` requests per second
- Request overflows are managed by queueing
+1 -1
View File
@@ -27,7 +27,7 @@ readdir('src/workers').then((workers) => {
bundle: true,
sourcemap: true,
minifyIdentifiers: false, // cause goofy errors
minifyIdentifiers: false, // causes goofy errors
minifySyntax: true,
minifyWhitespace: true,
+1 -1
View File
@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import { TreeTypeEnum } from '../../../components/Tanks';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchWargaming';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import {
TankopediaEntry,
+130 -100
View File
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from 'discord.js';
import { chunk } from 'lodash';
import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar';
@@ -35,6 +36,9 @@ import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryRaw } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 10;
async function render(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
@@ -132,38 +136,80 @@ async function render(
0,
) / careerBattles;
return (
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
const children: JSX.Element[] = [];
{filteredOrder.length === 0 && (
<NoData type={NoDataType.BattlesInPeriod} />
)}
{filteredOrder.length > 0 && (
<Breakdown.Root>
if (filteredOrder.length > 0) {
children.push(
<Breakdown.Row
key={id}
type="summary"
title="Total"
stats={[
{
title: 'Battles',
current: currentBattles,
career: careerBattles,
},
{
title: 'Winrate',
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
},
{
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
},
]}
/>,
);
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];
children.push(
<Breakdown.Row
key={id}
type="summary"
title="Total"
type="tank"
tankType={tankopediaEntry?.type}
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
title={await resolveTankName(id)}
stats={[
{
title: 'Battles',
current: currentBattles,
career: careerBattles,
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: 'Winrate',
current: `${(100 * currentWinrate).toFixed(2)}%`,
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: currentWinrate - careerWinrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
current.wins / current.battles - career.wins / career.battles,
},
{
title: 'WN8',
@@ -182,83 +228,67 @@ async function render(
},
{
title: 'Damage',
current: Math.round(currentDamage).toLocaleString(),
career: Math.round(careerDamage).toLocaleString(),
delta: currentDamage - careerDamage,
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>
/>,
);
}),
);
}
{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 (
<Breakdown.Row
key={id}
type="tank"
tankType={tankopediaEntry?.type}
treeType={tankopediaEntry ? await getTreeType(id) : undefined}
title={await resolveTankName(id)}
stats={[
{
title: 'Battles',
current: current.battles.toLocaleString(),
career: career.battles.toLocaleString(),
},
{
title: 'Winrate',
current: `${(
100 *
(current.wins / current.battles)
).toFixed(2)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(
2,
)}%`,
delta:
current.wins / current.battles -
career.wins / career.battles,
},
{
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
{
title: 'Damage',
current: Math.round(
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>
);
}),
)}
</Breakdown.Root>
)}
</Wrapper>
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<Wrapper>
{index === 0 && (
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
)}
<Breakdown.Root>{page}</Breakdown.Root>
</Wrapper>
))
.reverse();
} else {
return [
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
{filteredOrder.length === 0 && (
<NoData type={NoDataType.BattlesInPeriod} />
)}
</Wrapper>,
];
}
}
export const breakdownCommand = new Promise<CommandRegistryRaw>(
@@ -286,11 +316,11 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
...filters,
});
return Promise.all([
render(player, period, filters),
return [
...(await render(player, period, filters)),
buttonPrimary(path, 'Refresh'),
getBlitzStarsLinkButton(player.region, player.id),
]);
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming';
import fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices';
import embedInfo from '../core/discord/embedInfo';
+1 -1
View File
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming';
import fetchBlitz from '../core/blitz/fetchBlitz';
import {
AccountList,
usernamePattern,
+1 -1
View File
@@ -11,7 +11,7 @@ export const REGION_NAMES: Record<Region, string> = {
export const REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA',
eu: 'EU',
asia: 'AS',
asia: 'ASIA',
};
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
+52
View File
@@ -0,0 +1,52 @@
export type BlitzResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
}
| {
status: 'ok';
data: Data;
};
const MAX_CALLS_PER_SECOND = 10;
const queue: { url: string; resolve: (data: object) => void }[] = [];
let inProgress = 0;
async function manageQueue() {
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
inProgress++;
setTimeout(() => {
inProgress--;
manageQueue();
}, 1000);
const { url, resolve } = queue.shift()!;
const data = (await fetch(url)
.then((response) => response.json())
.catch((error) => ({ status: 'error', error }))) as BlitzResponse<
typeof resolve
>;
if (data.status === 'ok') {
resolve(data.data);
} else {
throw new Error(`Wargaming response error status:"${data.status}"`, {
cause: `Message: "${data.error.message}"\nURL: "${url}"`,
});
}
}
}
export default function fetchBlitz<Data extends object>(url: string) {
return new Promise<Data>((resolve) => {
queue.push({
url,
resolve(data) {
resolve(data as Data);
},
});
manageQueue();
});
}
-23
View File
@@ -1,23 +0,0 @@
export type BlitzResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
}
| {
status: 'ok';
data: Data;
};
export default async function fetchBlitz<Data extends object>(url: string) {
const data = (await fetch(url).then((response) =>
response.json(),
)) as BlitzResponse<Data>;
if (data.status === 'ok') {
return data.data;
} else {
throw new Error(`Wargaming response error status:"${data.status}"`, {
cause: `Message: "${data.error.message}"\nURL: "${url}"`,
});
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming';
import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
interface IndividualAccountAchievements {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming';
import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
export interface AllStats {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming';
import fetchBlitz from './fetchBlitz';
import { normalizeExtra } from './normalizeExtra';
import { normalizeIds } from './normalizeIds';
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming';
import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
export interface IndividualClanInfo {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { TanksStats } from '../../types/tanksStats';
import { UserError } from '../blitzkrieg/userError';
import fetchBlitz from './fetchWargaming';
import fetchBlitz from './fetchBlitz';
export default async function getTankStats(region: Region, id: number) {
const tankStats = await fetchBlitz<TanksStats>(
+1 -1
View File
@@ -1,7 +1,7 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { Clan, ClanList } from '../../types/clanList';
import fetchBlitz from './fetchWargaming';
import fetchBlitz from './fetchBlitz';
export type ClanListWithRegion = (Clan & {
region: 'com' | 'eu' | 'asia';
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming';
import fetchBlitz from './fetchBlitz';
interface Account {
nickname: string;
+1 -3
View File
@@ -1,7 +1,6 @@
import { deburr } from 'lodash';
import { TreeTypeString } from '../../components/Tanks';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from '../blitz/fetchWargaming';
import fetchBlitz from '../blitz/fetchBlitz';
import { context } from '../blitzkrieg/context';
export interface TankopediaEntry {
@@ -16,7 +15,6 @@ export interface TankopediaEntry {
type: string;
description: string;
}
type asd = TreeTypeString;
export interface Tankopedia {
[id: number]: TankopediaEntry | undefined;
}
+1 -3
View File
@@ -3,8 +3,6 @@ import { SlashCommandStringOption } from 'discord.js';
export default function addUsernameChoices(option: SlashCommandStringOption) {
return option
.setName('username')
.setDescription(
'The username you use in Blitz (inferred from Discord nickname by default)',
)
.setDescription('The username you use in Blitz')
.setAutocomplete(true);
}
+4 -2
View File
@@ -3,7 +3,7 @@ import {
AutocompleteInteraction,
CacheType,
} from 'discord.js';
import { REGION_NAMES } from '../../constants/regions';
import { REGION_NAMES_SHORT } from '../../constants/regions';
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
export default async function autocompleteUsername(
@@ -19,7 +19,9 @@ export default async function autocompleteUsername(
? players.map(
(player) =>
({
name: `${player.nickname} (${REGION_NAMES[player.region]})`,
name: `${player.nickname} (${
REGION_NAMES_SHORT[player.region]
})`,
value: `${player.region}/${player.account_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>,
)
@@ -11,6 +11,7 @@ import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded';
export default async function normalizeInteractionReturnable(
returnable: InteractionReturnable,
) {
const images: [number, Buffer][] = [];
const reply: InteractionEditReplyOptions = {};
const awaitedReturnable = await returnable;
@@ -19,7 +20,7 @@ export default async function normalizeInteractionReturnable(
: [awaitedReturnable];
await Promise.all(
normalizedReturnable.map(async (item) => {
normalizedReturnable.map(async (item, index) => {
if (typeof item === 'string') {
reply.content = item;
} else if (item instanceof EmbedBuilder) {
@@ -38,11 +39,16 @@ export default async function normalizeInteractionReturnable(
return;
} else {
const image = await jsxToPngThreaded(item);
if (!reply.files) reply.files = [];
reply.files.push(image);
images.push([index, image]);
}
}),
);
if (images.length > 0) {
images.sort(([a], [b]) => b - a);
if (!reply.files) reply.files = [];
images.forEach(([, image]) => reply.files?.push(image));
}
return reply;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { devtools, persist } from 'zustand/middleware';
import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn';
import { Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming';
import fetchBlitz from '../core/blitz/fetchBlitz';
import { NormalizedTankStats, TanksStats } from '../types/tanksStats';
type Session = (