Compare commits

..
44 Commits
Author SHA1 Message Date
TrèsAbhi 6a1b786dae fix: [] 2024-05-03 09:19:29 -05:00
TrèsAbhi ea1eeb4366 fix: first time tank deltas 2024-05-03 09:17:09 -05:00
TrèsAbhi 15bec37a33 feat: logs 2024-05-03 09:10:23 -05:00
TrèsAbhi 4bba794bfb feat: disable login 2024-05-03 09:04:35 -05:00
TrèsAbhi 82014845cb feat: add column 2024-05-03 09:02:06 -05:00
TrèsAbhi 74a83d17e7 feat: better layout 2024-05-02 18:27:27 -05:00
TrèsAbhi 26ea06b091 fix: undef window 2024-05-02 18:08:58 -05:00
TrèsAbhi 352cb70830 feat: totals 2024-05-01 22:11:12 -05:00
TrèsAbhi ff92b3dd45 feat: login auth 2024-05-01 21:04:31 -05:00
TrèsAbhi 99ed269723 fix: old code 2024-05-01 20:56:38 -05:00
TrèsAbhi c24639dd0c feat: deltas 2024-05-01 20:54:27 -05:00
TrèsAbhi cceebd1980 fix: redirect to tracker 2024-05-01 20:36:35 -05:00
TrèsAbhi 596018b3b2 feat: ghost login button 2024-05-01 20:27:10 -05:00
TrèsAbhi 8bc3e2c6b9 feat: stat generalzation 2024-05-01 17:55:13 -05:00
TrèsAbhi aafba7b6c8 feat: link 2024-05-01 17:29:40 -05:00
TrèsAbhi d56be26db2 feat: since timer 2024-05-01 17:23:33 -05:00
TrèsAbhi e740274e5a feat: table 2024-05-01 17:20:08 -05:00
TrèsAbhi 0634804914 Merge pull request #116 from tresabhi/main 2024-05-01 16:37:22 -05:00
TrèsAbhi 4d908493f1 feat: experimental 2024-05-01 16:36:34 -05:00
TrèsAbhi 23d43f131b fix: capitalize K 2024-05-01 16:35:28 -05:00
TrèsAbhi 8a1b8673dc feat: updated favicon 2024-05-01 16:34:46 -05:00
TrèsAbhi 8682b2ea78 feat: rearranged navbar 2024-05-01 16:33:22 -05:00
TrèsAbhi 6e32b41b15 feat: convinient login 2024-05-01 13:04:31 -05:00
TrèsAbhi 6223773c71 feat: move health to right 2024-04-30 22:20:31 -05:00
TrèsAbhi 7fd4c931e5 feat: remove flair 2024-04-30 10:01:23 -05:00
TrèsAbhi 1011c76cda feat: add n90 2024-04-30 09:57:37 -05:00
TrèsAbhi 38fa15ab97 feat: cc account tracking 2 2024-04-28 19:16:08 -05:00
TrèsAbhi d2b18360ed feat: cc account tracking 2024-04-28 18:43:12 -05:00
TrèsAbhi 6ae86a7a70 feat: some protobuf parsing 2024-04-28 17:06:31 -05:00
TrèsAbhi 6b175238b2 feat: subtitle 2024-04-27 11:07:58 -05:00
TrèsAbhi 2a2a7a067c feat: map definitions 2024-04-27 10:50:09 -05:00
TrèsAbhi 25788dedf4 feat: merge types 2024-04-26 12:51:35 -05:00
TrèsAbhi 391990676f feat: wn8 and labels 2024-04-26 12:34:59 -05:00
TrèsAbhi 8c16fdf9b5 fix 2024-04-25 14:00:13 -05:00
TrèsAbhi c30358248f fix: use production videos 2024-04-25 14:00:00 -05:00
TrèsAbhi 62b218a44d feat: some more functionality 2024-04-25 13:47:30 -05:00
TrèsAbhi 1f30d1d2c3 feat: mostly complete rating command 2024-04-24 16:22:48 -05:00
TrèsAbhi b1917c73f2 Update Party3.tsx 2024-04-24 08:13:18 -05:00
TrèsAbhi 2d3325f13a fix: dir 2024-04-23 16:08:13 -05:00
TrèsAbhi 983b96fd99 feat: some work 2024-04-23 16:07:59 -05:00
TrèsAbhi 2c105d7a89 feat: some unfinished ideas 2024-04-23 15:51:16 -05:00
TrèsAbhi 28348552f0 fix: watermark 2024-04-23 15:07:21 -05:00
TrèsAbhi 449f2b0677 fix: more updates to docs 2024-04-23 15:06:24 -05:00
TrèsAbhi 5346ab576f fix: realign 2024-04-23 10:33:24 -05:00
56 changed files with 2294 additions and 1182 deletions

No files matched your search

+9
View File
@@ -11,7 +11,9 @@
"applypatches",
"artefacts",
"atgm",
"Avearges",
"AYUV",
"battleresults",
"bbox",
"bepaint",
"betakrieg",
@@ -37,6 +39,7 @@
"CUBETEXCOORD",
"customperiod",
"datasource",
"dbid",
"Ddpf",
"Ddsd",
"debugflags",
@@ -80,6 +83,7 @@
"loadouts",
"loddist",
"lods",
"lossrate",
"mantlets",
"metallicness",
"minitelrose",
@@ -107,6 +111,8 @@
"POSITIVEY",
"POSITIVEZ",
"Promisable",
"protobuf",
"protos",
"Pyogenics",
"ratingleaderboard",
"raycaster",
@@ -150,6 +156,7 @@
"Unicum",
"UNLOCALIZED",
"UNORM",
"unpickled",
"Unregistering",
"vickers",
"vitepress",
@@ -162,6 +169,8 @@
"Woah",
"WOTB",
"wotblitz",
"wotbreplay",
"WOTINSPECTOR",
"xffn",
"xkdkw",
"XMLDVPL",
+3 -3
View File
@@ -8,16 +8,16 @@ Tools for everything World of Tanks Blitz
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
![BlitzKit application directory](https://i.imgur.com/JlEA0J1.png)
![BlitzKit application directory](https://i.imgur.com/YWCtOuJ.png)
## Visit the website
### https://blitzkit.vercel.app/
![BlitzKit website](https://i.imgur.com/oLVIx2n.png)
![BlitzKit website](https://i.imgur.com/bdYMUCL.png)
## Visit the documentation website
### https://tresabhi.github.io/blitzkit/
![BlitzKit documentation website](https://i.imgur.com/TOt5fMx.png)
![BlitzKit documentation website](https://i.imgur.com/hm3KTsF.png)
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.20.0
**IMPORTANT FOR STREAMERS:** This version removed the session tracker embed. Embeds will be reintroduced in the future.
## Features
- Reworked session tracker
- Played tanks are now displayed as a clean table that fits the theme of the website
- Tank images are now displayed along with them being linked to their respective tankopedia pages
- The UI has been simplified
- You can also change what columns are shown in the table but this is a work in progress feature
- Added the `/replay` command
- This command uploads your replays to WOTINSPECTOR and displays the battle results
## Fixes
- Fixed website logo to be "BK"
- Fixed the logo of the website to have a capital "K"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+7 -2
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkit",
"version": "1.19.6",
"version": "1.20.0",
"description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts",
"type": "module",
@@ -13,6 +13,7 @@
"build:ratingleaderboard": "vite-node --script scripts/buildRatingLeaderboard",
"build:assets": "vite-node --max_old_space_size=16384 --script scripts/buildAssets/index",
"test": "vite-node --script scripts/test",
"test2": "vite-node --script scripts/test2",
"dev:bot": "vite-node --script scripts/build && yarn start:bot",
"dev:docs": "vitepress dev docs",
"dev:website": "next dev",
@@ -44,6 +45,7 @@
"@resvg/resvg-js": "^2.4.1",
"@types/three": "^0.160.0",
"@vanilla-extract/css": "^1.13.0",
"adm-zip": "^0.5.12",
"bepaint": "^0.1.0-beta.3",
"decompress": "^4.2.1",
"discord.js": "^14.13.0",
@@ -60,14 +62,16 @@
"markdown-escape": "^2.0.0",
"next": "^14.1.0",
"parse-dds": "^1.2.1",
"pickleparser": "^0.2.1",
"postprocessing": "^6.34.1",
"protobufjs": "^7.2.6",
"react": "^18.2.0",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-fuzzysort": "^1.0.1",
"react-promise-suspense": "^0.3.4",
"satori": "^0.10.13",
"sharp": "0.33.1",
"sharp": "^0.33.3",
"three": "0.159.0",
"three-stdlib": "^2.29.4",
"vite-node": "^0.34.5",
@@ -78,6 +82,7 @@
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
"@types/adm-zip": "^0.5.5",
"@types/decompress": "^4.2.7",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.18",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ import { parse as parseYaml } from 'yaml';
import { readStringDVPL } from '../src/core/blitz/readStringDVPL';
import { writeDVPL } from '../src/core/blitz/writeDVPL';
import { secrets } from '../src/core/blitzkit/secrets';
import { dvp } from '../submodules/closedkrieg/src/dvp';
import { dvp } from '../submodules/blitzkit-closed/src/dvp';
import { DATA } from './buildAssets/constants';
const versionTextFile = await readStringDVPL(`${DATA}/version.txt.dvpl`);
+40 -1
View File
@@ -18,6 +18,7 @@ import {
EquipmentRow,
} from '../../src/core/blitzkit/equipmentDefinitions';
import { GameDefinitions } from '../../src/core/blitzkit/gameDefinitions';
import { MapDefinitions } from '../../src/core/blitzkit/mapDefinitions';
import {
ModelArmor,
ModelDefinitions,
@@ -325,6 +326,31 @@ type ConsumablesVehicleFilter =
| { name: string }
| { extendedTags: string };
export interface Maps {
maps: {
[key: string]: {
id: number;
tags?: string;
localName: string;
avaliableInTrainingRoom: boolean; // lol wg typo
spriteFrame: number;
supremacyPointsThreshold?: number;
availableModes: number[];
shadowMapsAvailable?: boolean;
assaultRespawnPoints: {
allies: MapAlly[];
enemies: MapAlly[];
};
levels?: number[];
};
};
}
export interface MapAlly {
respawnNumber: number;
points: Array<number[]>;
}
const blitzShellKindToBlitzkit: Record<ShellKind, ShellType> = {
ARMOR_PIERCING: 'ap',
ARMOR_PIERCING_CR: 'ap_cr',
@@ -348,6 +374,7 @@ export async function definitions(production: boolean) {
};
const tankDefinitions: TankDefinitions = {};
const modelDefinitions: ModelDefinitions = {};
const mapDefinitions: MapDefinitions = {};
const equipmentDefinitions: EquipmentDefinitions = {
presets: {},
equipments: {},
@@ -386,7 +413,7 @@ export async function definitions(production: boolean) {
const avatar = await readXMLDVPL<{ root: Avatar }>(
`${DATA}/XML/item_defs/tankmen/avatar.xml.dvpl`,
);
const maps = await readYAMLDVPL<Maps>(`${DATA}/maps.yaml.dvpl`);
const tankXps = new Map<number, number>();
await Promise.all(
@@ -1122,6 +1149,13 @@ export async function definitions(production: boolean) {
skillDefinitions.classes[tankClass as TankClass] = skills.split(' ');
});
Object.entries(maps.maps).forEach(([key, map]) => {
mapDefinitions[map.id] = {
id: map.id,
name: strings[`#maps:${key}:${map.localName}`],
};
});
await commitAssets(
'definitions',
[
@@ -1160,6 +1194,11 @@ export async function definitions(production: boolean) {
encoding: 'base64',
path: 'definitions/skills.cdon.lz4',
},
{
content: superCompress(mapDefinitions),
encoding: 'base64',
path: 'definitions/maps.cdon.lz4',
},
],
production,
);
+23 -5
View File
@@ -6,22 +6,40 @@ import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
import { commitAssets } from '../../src/core/blitzkit/commitAssets';
import {
VideoDefinitions,
videoDefinitions,
} from '../../src/core/blitzkit/videos';
import { VideoDefinitions } from '../../src/core/blitzkit/videos';
import { DATA, POI } from './constants';
import { BlitzStrings, botPattern, VehicleDefinitionList } from './definitions';
const youtubers = [
'UCKBYXp4Xn2I2tL1UL4fpbhw', // droodles
'UCrQ-Dy8lVsm11u8pCJ8W7Tw', // fatness
'UCxRpZqMElssTdPYDK3x7wyg', // n90
];
export async function videos(production: boolean) {
console.log('Building videos...');
const currentVideos = await videoDefinitions;
const currentVideos = await fetch(
'https://raw.githubusercontent.com/tresabhi/blitzkit-assets/main/definitions/videos.csv',
)
.then((response) => response.text())
.then((lines) => {
return lines
.split('\n')
.map((line) => {
const [idString, lastUpdatedString, ...videos] = line.split(',');
const id = Number(idString);
const lastUpdated = Number(lastUpdatedString);
return { id, lastUpdated, videos };
})
.reduce<VideoDefinitions>(
(accumulator, { id, lastUpdated, videos }) => {
accumulator[id] = { lastUpdated, videos };
return accumulator;
},
{},
);
});
const auth = await google.auth.getClient({
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
});
+36
View File
@@ -0,0 +1,36 @@
import AdmZip from 'adm-zip';
import { readFile } from 'fs/promises';
import { BattleResultsReadStream } from '../src/core/streams/battleResults';
interface WoTBReplayMeta {
version: string;
title: string;
dbid: string;
playerName: string;
battleStartTime: string;
playerVehicleName: string;
mapName: string;
arenaUniqueId: string;
battleDuration: number;
vehicleCompDescriptor: number;
camouflageId: number;
mapId: number;
arenaBonusType: number;
}
const file = await readFile('temp/test.wotbreplay');
const zip = new AdmZip(file);
const entries = zip.getEntries();
const metaRaw = entries.find((entry) => entry.name === 'meta.json');
const battleResultsRaw = entries.find(
(entry) => entry.name === 'battle_results.dat',
);
if (!metaRaw) throw new Error('No meta.json found');
if (!battleResultsRaw) throw new Error('No battle_results.dat found');
const meta = JSON.parse(metaRaw.getData().toString()) as WoTBReplayMeta;
const battleResults = new BattleResultsReadStream(
battleResultsRaw.getData().buffer,
).battleResults();
+62
View File
@@ -0,0 +1,62 @@
'use client';
import { Flex, Heading, Text } from '@radix-ui/themes';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { mutateApp } from '../../../stores/app';
export default function Page({
params,
searchParams,
}: {
params: { provider: string };
searchParams: { [key: string]: string };
}) {
const router = useRouter();
useEffect(() => {
switch (params.provider) {
case 'wargaming': {
if (
!searchParams.expires_at ||
!searchParams.access_token ||
!searchParams.account_id
) {
break;
}
mutateApp((draft) => {
draft.login = {
expiresAt: Number(searchParams.expires_at),
id: Number(searchParams.account_id),
token: searchParams.access_token,
};
});
break;
}
default: {
console.error(`Unknown provider: ${params.provider}`);
}
}
router.push(searchParams.return ? searchParams.return : '/');
});
return (
<PageWrapper>
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="2"
>
<Heading>Verifying you...</Heading>
<Text color="gray">You'll be on your way shortly.</Text>
</Flex>
</PageWrapper>
);
}
-338
View File
@@ -1,338 +0,0 @@
'use client';
import { ContextMenu } from '@radix-ui/themes';
import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { resetSession } from '../../../core/blitzkit/resetSession';
import {
TankDefinition,
tankDefinitions,
} from '../../../core/blitzkit/tankDefinitions';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import calculateWN8 from '../../../core/statistics/calculateWN8';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
import mutateSession, { useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
import { Menu } from '../../tools/session/components/Menu';
const REFRESH_RATE = 1 / 5;
interface SessionPageProps {
naked?: boolean;
}
export default function SessionPage({ naked = true }: SessionPageProps) {
const session = useSession();
const since = session.isTracking ? new Date(session.time) : undefined;
const [diff, setDiff] = useState<
| {
careerWN8: number;
list: {
stats: IndividualTankStats;
tankDefinitions?: TankDefinition;
career: IndividualTankStats;
currentWN8?: number;
careerWN8?: number;
}[];
}
| undefined
>(undefined);
const sessionBattles = diff?.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.battles,
0,
);
const tankStatsArray = session.isTracking
? (Object.values(session.tankStats) as IndividualTankStats[])
: [];
const sessionWinrate = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.wins,
0,
) / sessionBattles!
: 0;
const careerBattles = tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.battles,
0,
);
const careerWinrate =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.wins,
0,
) / careerBattles;
const currentDamage = diff
? diff.list.reduce(
(accumulator, { stats }) => accumulator + stats.all.damage_dealt,
0,
) / sessionBattles!
: 0;
const careerDamage =
tankStatsArray.reduce(
(accumulator, stats) => accumulator + stats.all.damage_dealt,
0,
) / careerBattles;
useEffect(() => {
if (session.title === 'Untitled session') {
mutateSession((draft) => {
if (draft.isTracking) draft.title = draft.nickname;
});
}
}, [session.title]);
useEffect(() => {
async function recalculateDiff() {
if (session.isTracking) {
const { id, region } = session;
const careerRaw = await fetchBlitz<TanksStats>(region, 'tanks/stats', {
account_id: id,
});
const career = careerRaw[id]!.reduce<
Record<number, IndividualTankStats>
>(
(accumulator, curr) => ({
...accumulator,
[curr.tank_id]: curr,
}),
{},
);
const awaitedTankDefinitions = await tankDefinitions;
const awaitedTankAverages = await tankAverages;
const careerWN8 =
careerRaw[id]!.reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles *
calculateWN8(awaitedTankAverages[tank_id].all, all)
: 0),
0,
) /
careerRaw[id]!.reduce(
(accumulator, { tank_id, all }) =>
accumulator +
(awaitedTankAverages[tank_id] && all.battles > 0
? all.battles
: 0),
0,
);
setDiff({
careerWN8,
list: (
Object.values(
deltaTankStats(session.tankStats, career, session.time / 1000),
) as IndividualTankStats[]
).map((stats) => {
return {
stats,
tankDefinitions: awaitedTankDefinitions[stats.tank_id],
career: career[stats.tank_id],
currentWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
stats.all,
)
: undefined,
careerWN8: awaitedTankAverages[stats.tank_id]
? calculateWN8(
awaitedTankAverages[stats.tank_id].all,
career[stats.tank_id].all,
)
: undefined,
};
}),
});
}
}
recalculateDiff();
const interval = setInterval(recalculateDiff, 1000 / REFRESH_RATE);
return () => clearInterval(interval);
}, [session]);
const totalRowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: sessionBattles?.toLocaleString(),
career: tankStatsArray
.reduce((accumulator, stats) => accumulator + stats.all.battles, 0)
.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: sessionBattles ? `${(100 * sessionWinrate).toFixed(2)}%` : '--',
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
},
wn8: (() => {
if (!diff) return undefined;
const battlesWithWN8 = diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
(currentWN8 === undefined ? 0 : stats.all.battles) + accumulator,
0,
);
const currentWN8 =
diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
stats.all.battles * (currentWN8 ?? 0) + accumulator,
0,
) / battlesWithWN8!;
return {
title: 'WN8',
current: battlesWithWN8
? Math.round(currentWN8!).toLocaleString()
: '--',
percentile: battlesWithWN8 ? getWN8Percentile(currentWN8!) : undefined,
career: Math.round(diff.careerWN8).toLocaleString(),
};
})(),
damage: {
title: 'Damage',
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
current: sessionBattles
? Math.round(currentDamage).toLocaleString()
: '--',
career: Math.round(careerDamage).toLocaleString(),
},
none: undefined,
};
return (
<ContextMenu.Root>
<ContextMenu.Trigger>
<div>
<Breakdown.Root>
{session.showTotal &&
session.isTracking &&
since !== undefined &&
diff !== undefined && (
<Breakdown.Row
naked={naked}
color={session.color}
minimized={!session.showCareer}
title={session.title}
stats={session.customColumns.map((customColumn) => {
const rowStat = totalRowStats[customColumn.display];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
)}
{diff?.list
.sort(
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
)
.map(
({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => {
const rowStats: Record<
CustomColumnDisplay,
Breakdown.RowStatItem | undefined
> = {
battles: {
title: 'Battles',
current: stats.all.battles.toLocaleString(),
career: career.all.battles.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: `${(
100 *
(stats.all.wins / stats.all.battles)
).toFixed(2)}%`,
career: `${(
100 *
(career.all.wins / career.all.battles)
).toFixed(2)}%`,
delta:
stats.all.wins / stats.all.battles -
career.all.wins / career.all.battles,
},
wn8: {
title: 'WN8',
current:
currentWN8 === undefined
? undefined
: Math.round(currentWN8).toLocaleString(),
career:
careerWN8 === undefined
? undefined
: Math.round(careerWN8).toLocaleString(),
percentile:
currentWN8 === undefined
? undefined
: getWN8Percentile(currentWN8),
},
damage: {
title: 'Damage',
current: Math.round(
stats.all.damage_dealt / stats.all.battles,
).toLocaleString(),
career: Math.round(
career.all.damage_dealt / career.all.battles,
).toLocaleString(),
delta:
stats.all.damage_dealt / stats.all.battles -
career.all.damage_dealt / career.all.battles,
},
none: undefined,
};
return (
<Breakdown.Row
naked={naked}
color={session.color}
key={stats.tank_id}
minimized={!session.showCareer}
title={
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
}
type="tank"
tankType={tankDefinitions?.class}
treeType={tankDefinitions?.treeType}
stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta &&
rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/>
);
},
)}
</Breakdown.Root>
</div>
</ContextMenu.Trigger>
<Menu reset={resetSession} Builder={ContextMenu} />
</ContextMenu.Root>
);
}
+1 -5
View File
@@ -15,7 +15,7 @@ export default function Page() {
<Heading size="5">Appearance</Heading>
<Flex align="center" gap="2">
<Text>Dark mode</Text>
<Text>Dark mode (experimental)</Text>
<Switch
checked={darkMode}
onCheckedChange={(checked) =>
@@ -38,10 +38,6 @@ export default function Page() {
/>
</Flex>
</Flex>
<Text color="gray">
<i>Not a lot here, eh?</i>
</Text>
</Flex>
</PageWrapper>
);
@@ -1,61 +0,0 @@
import { ContextMenu, DropdownMenu } from '@radix-ui/themes';
import mutateSession, { useSession } from '../../../../stores/session';
export type CustomColumnDisplay =
| 'battles'
| 'winrate'
| 'wn8'
| 'damage'
| 'none';
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
interface CustomColumn {
Builder: typeof DropdownMenu | typeof ContextMenu;
column: number;
}
export function CustomColumn({ column, Builder }: CustomColumn) {
const customColumn = useSession((state) => state.customColumns[column]);
return (
<Builder.Sub>
<Builder.SubTrigger>Column {column + 1}</Builder.SubTrigger>
<Builder.SubContent>
<Builder.RadioGroup
value={customColumn.display}
onValueChange={(event) =>
mutateSession((draft) => {
draft.customColumns[column].display =
event as CustomColumnDisplay;
})
}
>
<Builder.RadioItem value="battles">Battles</Builder.RadioItem>
<Builder.RadioItem value="winrate">Winrate</Builder.RadioItem>
<Builder.RadioItem value="wn8">WN8</Builder.RadioItem>
<Builder.RadioItem value="damage">Damage</Builder.RadioItem>
<Builder.RadioItem value="none">None</Builder.RadioItem>
</Builder.RadioGroup>
{!NO_DELTA.includes(customColumn.display) && (
<>
<Builder.Separator />
<Builder.CheckboxItem
checked={customColumn.showDelta}
onCheckedChange={(checked) =>
mutateSession((draft) => {
draft.customColumns[column].showDelta = checked;
})
}
>
Show delta
</Builder.CheckboxItem>
</>
)}
</Builder.SubContent>
</Builder.Sub>
);
}
-135
View File
@@ -1,135 +0,0 @@
import {
Button,
ContextMenu,
Dialog,
DropdownMenu,
Flex,
Heading,
TextField,
} from '@radix-ui/themes';
import { createColors } from 'bepaint';
import { useRef, useState } from 'react';
import {
ACCENT_COLORS,
AccentColor,
GRAY_COLORS,
GrayColor,
PALETTES,
} from '../../../../constants/radixColors';
import { useSession } from '../../../../stores/session';
import { CustomColumn } from './CustomColumn';
interface ContextMenuProps {
Builder: typeof DropdownMenu | typeof ContextMenu;
reset: () => void;
}
export function Menu({ Builder, reset }: ContextMenuProps) {
const session = useSession();
const [showTitleEditor, setShowTitleEditor] = useState(false);
const titleInput = useRef<HTMLInputElement>(null);
return (
<>
<Dialog.Root open={showTitleEditor} onOpenChange={setShowTitleEditor}>
<Dialog.Content>
<Flex direction="column" gap="4">
<Heading size="4">Edit title</Heading>
<Flex gap="2" style={{ width: '100%' }}>
<TextField.Root
style={{ flex: 1 }}
ref={titleInput}
defaultValue={session.title}
placeholder="Enter title..."
onKeyDown={(event) => {
if (event.key === 'Enter' && titleInput.current) {
setShowTitleEditor(false);
useSession.setState({ title: titleInput.current!.value });
}
}}
/>
<Dialog.Close>
<Button
onClick={() =>
useSession.setState({ title: titleInput.current!.value })
}
>
Set
</Button>
</Dialog.Close>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
<Builder.Content>
<Builder.Sub>
<Builder.SubTrigger>Accent color</Builder.SubTrigger>
<Builder.SubContent>
<Builder.RadioGroup
value={session.color}
onValueChange={(value) =>
useSession.setState({ color: value as AccentColor | GrayColor })
}
>
{[...ACCENT_COLORS, ...GRAY_COLORS].map((color) => (
<Builder.RadioItem value={color} key={color}>
<Flex gap="2" align="center">
<div
style={{
backgroundColor: createColors(PALETTES[color])
.solidBackground,
width: 16,
height: 16,
borderRadius: '100%',
}}
/>
{`${color.charAt(0).toUpperCase()}${color.slice(1)}`}
</Flex>
</Builder.RadioItem>
))}
</Builder.RadioGroup>
</Builder.SubContent>
</Builder.Sub>
<Builder.Separator />
<Builder.Item onClick={() => setShowTitleEditor(true)}>
Edit title...
</Builder.Item>
<CustomColumn Builder={Builder} column={0} />
<CustomColumn Builder={Builder} column={1} />
<CustomColumn Builder={Builder} column={2} />
<CustomColumn Builder={Builder} column={3} />
<Builder.Separator />
<Builder.CheckboxItem
checked={session.showTotal}
onCheckedChange={(checked) =>
useSession.setState({ showTotal: checked })
}
>
Show total
</Builder.CheckboxItem>
<Builder.CheckboxItem
checked={session.showCareer}
onCheckedChange={(checked) =>
useSession.setState({ showCareer: checked })
}
>
Show career
</Builder.CheckboxItem>
<Builder.Separator />
<Builder.Item color="red" onClick={reset}>
Reset
</Builder.Item>
</Builder.Content>
</>
);
}
-40
View File
@@ -1,40 +0,0 @@
import { style } from '@vanilla-extract/css';
import { theme } from '../../../stitches.config';
export const toolBar = style({
'@media': {
'screen and (min-width: 640px)': {
flexDirection: 'row',
},
},
flexDirection: 'column',
display: 'flex',
gap: 8,
});
export const toolbarButton = style({
'@media': {
'screen and (min-width: 640px)': {
flex: 0,
},
},
flex: 1,
});
export const searchButton = style({
border: 'none',
background: 'none',
fontSize: 16,
display: 'flex',
padding: 8,
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer',
':hover': {
backgroundColor: theme.colors.componentInteractiveHover,
borderRadius: 4,
},
});
+443 -210
View File
@@ -1,267 +1,500 @@
'use client';
import {
CaretDownIcon,
CopyIcon,
PersonIcon,
ReloadIcon,
ArrowDownIcon,
MagnifyingGlassIcon,
PlusIcon,
} from '@radix-ui/react-icons';
import {
AlertDialog,
Button,
Checkbox,
Card,
DropdownMenu,
Flex,
Heading,
IconButton,
Link,
Select,
Table,
Text,
TextField,
} from '@radix-ui/themes';
import { debounce } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { use, useEffect, useMemo, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
import { UNLOCALIZED_REGION_NAMES_SHORT } from '../../../constants/regions';
import {
generateStats,
prettifyStats,
Stat,
STAT_KEYS,
STAT_NAMES,
sumStats,
} from '../../../core/blitz/generateStats';
import {
getAccountInfo,
IndividualAccountInfo,
} from '../../../core/blitz/getAccountInfo';
import getTankStats from '../../../core/blitz/getTankStats';
import { idToRegion } from '../../../core/blitz/idToRegion';
import searchPlayersAcrossRegions, {
AccountListWithServer,
} from '../../../core/blitz/searchPlayersAcrossRegions';
import { resetSession } from '../../../core/blitzkit/resetSession';
import { setSession } from '../../../core/blitzkit/setSession';
import { useSession } from '../../../stores/session';
import SessionPage from '../../embeds/session/page';
import { Menu } from './components/Menu';
import * as styles from './page.css';
import { tankDefinitions } from '../../../core/blitzkit/tankDefinitions';
import { tankIcon } from '../../../core/blitzkit/tankIcon';
import { tankAverages } from '../../../core/blitzstars/tankAverages';
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
import { useWideFormat } from '../../../hooks/useWideFormat';
import { theme } from '../../../stitches.config';
import mutateSession, {
SessionTracking,
useSession,
} from '../../../stores/session';
import { IndividualTankStats } from '../../../types/tanksStats';
export default function Page() {
const wideFormat = useWideFormat(640);
const awaitedTankDefinitions = use(tankDefinitions);
const awaitedTankAvearges = use(tankAverages);
const [showSearch, setShowSearch] = useState(false);
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState<AccountListWithServer>([]);
const [showCCInaccessibilityPrompt, setShowCCInaccessibilityPrompt] =
useState(false);
const input = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState<
AccountListWithServer | undefined
>(undefined);
const [showSearchResults, setShowSearchResults] = useState(false);
const handleChange = debounce(
async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.value) {
setSearchResults(await searchPlayersAcrossRegions(event.target.value));
} else {
setSearchResults(undefined);
setShowSearchResults(false);
}
},
500,
);
const session = useSession();
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
null,
);
const accountInfoPromise = useMemo(
() =>
new Promise<IndividualAccountInfo | null>(async (resolve) => {
resolve(
session.tracking
? await getAccountInfo(session.player.region, session.player.id)
: null,
);
}),
[session.tracking && session.player.id],
);
const accountInfo = use(accountInfoPromise);
const delta = useMemo(
() =>
session.tracking && tankStatsB
? deltaTankStats(session.player.stats, tankStatsB)
.sort((a, b) => b.last_battle_time - a.last_battle_time)
.map((entry) => {
const tank = awaitedTankDefinitions[entry.tank_id];
const average = awaitedTankAvearges[tank.id];
const stats = generateStats(entry.all, average?.all);
const statsPretty = prettifyStats(stats);
return {
tank,
stats,
statsPretty,
};
})
: undefined,
[session.tracking && session.player, tankStatsB],
);
const total = useMemo(
() =>
delta
? prettifyStats(sumStats(delta.map(({ stats }) => stats)))
: undefined,
[delta],
);
const search = debounce(async () => {
if (!input.current) return;
setSearchResults(await searchPlayersAcrossRegions(input.current.value, 9));
setSearching(false);
}, 500);
useEffect(() => {
if (session.isTracking) input.current!.value = session.nickname;
}, [session]);
async function update() {
if (!session.tracking) return;
const tankStats = await getTankStats(
idToRegion(session.player.id),
session.player.id,
);
setTankStatsB(tankStats);
}
update();
const interval = setInterval(update, 5 * 1000);
return () => clearInterval(interval);
}, [session.tracking && session.player.id]);
return (
<PageWrapper color="blue">
<div className={styles.toolBar}>
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
<PageWrapper>
<AlertDialog.Root
open={showCCInaccessibilityPrompt}
onOpenChange={setShowCCInaccessibilityPrompt}
>
<AlertDialog.Content>
<AlertDialog.Title>
You're attempting to track a CC account
</AlertDialog.Title>
<AlertDialog.Description>
CC (community contributor) accounts do not have public tank
statistics. We are in the process of adding embeds for CC accounts,
stay tuned.
</AlertDialog.Description>
<Flex mt="4" justify="end">
<AlertDialog.Action>
<Button variant="solid">Alright</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
{!session.tracking && (
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="3"
>
<Flex gap="2" align="center">
<Text color="gray">Look up a player to get started</Text>
<ArrowDownIcon />
</Flex>
<TextField.Root
defaultValue={session.isTracking ? session.nickname : undefined}
size="3"
style={{
position: 'relative',
width: '75vw',
maxWidth: 480,
}}
ref={input}
onChange={(event) => {
event.stopPropagation();
placeholder="Search players..."
onChange={() => {
if (!input.current) return;
if (event.target.value) {
setShowSearchResults(true);
setSearchResults(undefined);
const sanitized = input.current.value.trim();
if (sanitized.length > 0) {
setShowSearch(true);
setSearching(true);
search();
} else {
setShowSearchResults(false);
}
handleChange(event);
}}
onBlur={(event) => {
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 (https://github.com/radix-ui/primitives/issues/1969) is fixed
if (showSearchResults) event.target.focus();
}}
onKeyDown={(event) => {
if (event.key === 'Escape') {
setShowSearchResults(false);
input.current?.blur();
setShowSearch(false);
}
}}
placeholder="Search for a player..."
>
<TextField.Slot>
<PersonIcon height="16" width="16" />
<MagnifyingGlassIcon />
</TextField.Slot>
{showSearch && (
<Card
mt="2"
style={{
position: 'absolute',
top: '100%',
left: 0,
width: '100%',
}}
>
{searching && (
<Flex justify="center">
<Text color="gray">Searching...</Text>
</Flex>
)}
{!searching && searchResults.length === 0 && (
<Flex justify="center">
<Text color="gray">No players found</Text>
</Flex>
)}
{!searching && searchResults.length > 0 && (
<Flex direction="column" gap="2">
{searchResults.map((player) => (
<Button
key={player.account_id}
variant="ghost"
onClick={async () => {
const stats = await getTankStats(
player.region,
player.account_id,
);
if (stats === null) {
setShowCCInaccessibilityPrompt(true);
return;
}
setShowSearch(false);
mutateSession((draft) => {
if (!input.current) return;
draft.tracking = true;
(draft as SessionTracking).player = {
id: player.account_id,
region: player.region,
since: Date.now(),
stats,
};
input.current.value = player.nickname;
});
}}
>
{player.nickname} (
{UNLOCALIZED_REGION_NAMES_SHORT[player.region]})
</Button>
))}
</Flex>
)}
</Card>
)}
</TextField.Root>
</Flex>
)}
<DropdownMenu.Root open={showSearchResults} modal={false}>
<DropdownMenu.Trigger>
<div />
</DropdownMenu.Trigger>
{session.tracking && (
<Flex
mt="2"
mb="2"
direction={wideFormat ? 'row' : 'column'}
align="center"
justify="between"
gap="2"
>
<Flex mt="2" mb="2" direction="column" gap="2">
<Heading size="5">Tracking {accountInfo?.nickname}</Heading>
<Text color="gray">
Since {new Date(session.player.since).toLocaleString()}
</Text>
</Flex>
<DropdownMenu.Content>
{searchResults === undefined ? (
<DropdownMenu.Item disabled>Searching...</DropdownMenu.Item>
) : searchResults.length === 0 ? (
<DropdownMenu.Item disabled>No results</DropdownMenu.Item>
) : (
searchResults?.map(
({ account_id: id, nickname, region }, index) => (
<DropdownMenu.Item
key={id}
onClick={() => {
setShowSearchResults(false);
setSession(region, id, nickname);
}}
shortcut={UNLOCALIZED_REGION_NAMES[region]}
>
{nickname}
</DropdownMenu.Item>
),
)
)}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<Flex gap="2" align="center" justify="center">
<Button
color="red"
onClick={async () => {
const stats = await getTankStats(
session.player.region,
session.player.id,
);
<div style={{ display: 'flex', gap: 8 }}>
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button
variant="soft"
onClick={(event) => {
if (!session.resetPrompt) {
event.preventDefault();
resetSession();
}
}}
className={styles.toolbarButton}
color="red"
>
<ReloadIcon width="16" height="16" /> Reset
</Button>
</AlertDialog.Trigger>
if (stats === null) {
setShowCCInaccessibilityPrompt(true);
return;
}
<AlertDialog.Content>
<Flex direction="column" gap="4">
<Flex direction="column" gap="0">
<AlertDialog.Title>
Are you sure you want to reset?
</AlertDialog.Title>
<AlertDialog.Description>
This'll discard your current session and start tracking from
scratch again.
</AlertDialog.Description>
</Flex>
mutateSession((draft) => {
(draft as SessionTracking).player.stats = stats;
});
}}
>
Reset
</Button>
<Button
onClick={() => {
mutateSession((draft) => {
draft.tracking = false;
});
}}
>
Change
</Button>
</Flex>
</Flex>
)}
<Text size="2">
<Checkbox
mr="1"
onCheckedChange={(checked) =>
useSession.setState({
resetPrompt: !checked,
})
}
/>
Don't ask me again
</Text>
</Flex>
<Flex gap="3" mt="4">
<AlertDialog.Cancel>
<Button variant="soft" color="gray">
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={resetSession}>
Reset
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button
variant="soft"
className={styles.toolbarButton}
onClick={(event) => {
if (!session.embedPrompt) {
event.preventDefault();
resetSession();
}
navigator.clipboard.writeText(
`${location.origin}/embeds/session`,
);
}}
>
<CopyIcon width="16" height="16" /> Embed
</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Title>
Copied link to your clipboard
</AlertDialog.Title>
<AlertDialog.Description>
Instructions for streamers:
<iframe
{session.tracking && delta && delta.length > 0 && (
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Tank</Table.ColumnHeaderCell>
{session.columns.map((column, index) => (
<Table.ColumnHeaderCell
width="0"
key={column}
style={{
width: '100%',
aspectRatio: '16 / 9',
border: 'none',
position: 'relative',
}}
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</AlertDialog.Description>
>
{index === 0 && session.columns.length < 4 && (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton
size="1"
variant="surface"
style={{
position: 'absolute',
left: 0,
top: '50%',
transform: 'translate(-100%, -50%)',
zIndex: 1,
}}
>
<PlusIcon />
</IconButton>
</DropdownMenu.Trigger>
<Text size="2">
<Checkbox
mr="1"
onCheckedChange={(checked) =>
useSession.setState({
embedPrompt: !checked,
})
}
/>
Don't show this again
</Text>
<DropdownMenu.Content>
{STAT_KEYS.filter(
(item) => !session.columns.includes(item),
).map((key) => (
<DropdownMenu.Item
key={key}
onClick={() => {
mutateSession((draft) => {
(draft as SessionTracking).columns.unshift(
key as Stat,
);
});
}}
>
{STAT_NAMES[key]}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
)}
<Flex gap="3" mt="4">
<AlertDialog.Cancel>
<Button variant="soft">Got it!</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<Select.Root
value={column}
onValueChange={(value) => {
mutateSession((draft) => {
if (value === 'remove') {
(draft as SessionTracking).columns.splice(index, 1);
} else {
(draft as SessionTracking).columns[index] =
value as Stat;
}
});
}}
>
<Select.Trigger variant="ghost" />
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="soft" className={styles.toolbarButton}>
Options
<CaretDownIcon />
</Button>
</DropdownMenu.Trigger>
<Select.Content>
{STAT_KEYS.filter(
(item) =>
!session.columns.includes(item) || item === column,
).map((key) => (
<Select.Item key={key} value={key}>
{STAT_NAMES[key]}
</Select.Item>
))}
<Menu Builder={DropdownMenu} reset={resetSession} />
</DropdownMenu.Root>
</div>
</div>
<Select.Separator />
<SessionPage naked={false} />
<Select.Item value="remove">Remove</Select.Item>
</Select.Content>
</Select.Root>
</Table.ColumnHeaderCell>
))}
</Table.Row>
</Table.Header>
{!session.isTracking && (
<Table.Body>
<Table.Row
style={{
overflow: 'hidden',
}}
>
<Table.RowHeaderCell
style={{
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
Total
</Table.RowHeaderCell>
{session.columns.map((column) => (
<Table.Cell key={column}>{total![column]}</Table.Cell>
))}
</Table.Row>
{delta.map(({ statsPretty, tank }) => {
return (
<Table.Row
key={tank.id}
style={{
overflow: 'hidden',
}}
>
<Table.RowHeaderCell
style={{
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
}}
>
<Link href={`/tools/tankopedia/${tank.id}`}>
<img
draggable={false}
src={tankIcon(tank.id)}
style={{
position: 'absolute',
width: 128 + 32,
height: '200%',
top: '-50%',
left: 0,
objectFit: 'contain',
objectPosition: '50% 50%',
overflow: 'hidden',
}}
/>
<div
style={{
backgroundColor: 'red',
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: 128,
background:
'linear-gradient(90deg, #00000080, #00000000)',
}}
/>
<Text
style={{
color: theme.colors.textHighContrast,
position: 'relative',
textWrap: 'nowrap',
textShadow: 'black 0 0 4px',
}}
>
{tank.name}
</Text>
</Link>
</Table.RowHeaderCell>
{session.columns.map((column) => (
<Table.Cell key={column}>{statsPretty[column]}</Table.Cell>
))}
</Table.Row>
);
})}
</Table.Body>
</Table.Root>
)}
{session.tracking && delta && delta.length === 0 && (
<Flex
align="center"
justify="center"
direction="column"
style={{ flex: 1 }}
style={{
flex: 1,
}}
>
<Heading color="gray">No player selected</Heading>
<Text color="gray">
Search for a <PersonIcon /> Player to get started
</Text>
<Heading color="gray">No battles</Heading>
<Text color="gray">Go ahead and play a game</Text>
</Flex>
)}
</PageWrapper>
@@ -183,7 +183,7 @@ export function TankSandbox() {
style={{
position: 'absolute',
left: '50%',
top: 16,
top: 10,
}}
>
<BlitzkitWide height={32} />
@@ -1,5 +1,5 @@
import { InfoCircledIcon, PlusIcon } from '@radix-ui/react-icons';
import { Callout, Card, Flex, Heading, Link, Text } from '@radix-ui/themes';
import { PlusIcon } from '@radix-ui/react-icons';
import { Card, Flex, Heading, Link, Text } from '@radix-ui/themes';
import { use } from 'react';
import { videoDefinitions } from '../../../../../core/blitzkit/videos';
import { useFullScreen } from '../../../../../hooks/useFullScreen';
@@ -17,16 +17,6 @@ export function Videos() {
<Flex direction="column" gap="4" mt="4" align="start">
<Heading size="6">Review videos</Heading>
<Callout.Root>
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>
Some YouTubers may not have videos on the {tank.name} leading to
off-topic videos.
</Callout.Text>
</Callout.Root>
<Flex gap="3" wrap="wrap">
{videos.map((video) => (
<Link
+2 -2
View File
@@ -11,7 +11,7 @@ import { getRatingNeighbors } from '../core/blitz/getRatingNeighbors';
import { normalizeLeagueIcon } from '../core/blitz/normalizeLeagueIcon';
import { emblemURL } from '../core/blitzkit/emblemURL';
import getArchivedRatingMidnightLeaderboard from '../core/blitzkit/getArchivedRatingMidnightLeaderboard';
import { webpToPng } from '../core/blitzkit/iconPng';
import { iconPng } from '../core/blitzkit/iconPng';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
@@ -295,7 +295,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
height: 64,
objectFit: 'cover',
}}
src={await webpToPng(
src={await iconPng(
reward.type === 'vehicle'
? reward.vehicle.preview_image_url
: reward.stuff.image_url,
+564
View File
@@ -0,0 +1,564 @@
import { clamp } from 'lodash';
import CommandWrapper from '../components/CommandWrapper';
import TitleBar from '../components/TitleBar';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { idToRegion } from '../core/blitz/idToRegion';
import { emblemURL } from '../core/blitzkit/emblemURL';
import { iconPng } from '../core/blitzkit/iconPng';
import { mapDefinitions } from '../core/blitzkit/mapDefinitions';
import { RenderConfiguration } from '../core/blitzkit/renderConfiguration';
import { tankDefinitions } from '../core/blitzkit/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../core/blitzkit/tankDefinitions/constants';
import { tankIcon } from '../core/blitzkit/tankIcon';
import { tankAverages } from '../core/blitzstars/tankAverages';
import buttonLink from '../core/discord/buttonLink';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import calculateWN8 from '../core/statistics/calculateWN8';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
type WotInspectorReplaySubmission =
| { status: 'error'; data: unknown; error: { message: string } }
| {
status: 'ok';
data: {
download_url: string;
view_url: string;
summary: {
battle_start_time: string;
map_name: string;
battle_start_timestamp: number;
battle_duration: number;
title: string;
arena_unique_id: string;
allies: number[];
enemies: number[];
protagonist: number;
player_name: string;
vehicle: string;
exp_base: number;
exp_total: number;
credits_base: number;
credits_total: number;
room_type: number;
battle_type: number;
mastery_badge: number;
battle_result: number;
vehicle_tier: number;
vehicle_type: number;
protagonist_team: number;
winner_team: number;
details: {
achievements: unknown[];
base_capture_points: number;
base_defend_points: number;
chassis_id: unknown | null;
clan_tag: string;
clanid: number;
credits: number;
damage_assisted: number;
damage_assisted_track: number;
damage_blocked: unknown | null;
damage_made: number;
damage_received: number;
dbid: number;
death_reason: number;
distance_travelled: number;
enemies_damaged: number;
enemies_destroyed: number;
enemies_spotted: number;
entity_id: number;
exp: number;
exp_for_assist: number;
exp_for_damage: number;
exp_team_bonus: number;
gun_id: unknown | null;
hero_bonus_credits: number;
hero_bonus_exp: number;
hitpoints_left: number;
hits_bounced: number;
hits_pen: number;
hits_received: number;
hits_splash: number;
killed_by: number;
name: string;
shots_hit: number;
shots_made: number;
shots_pen: number;
shots_splash: number;
squad_index: number;
team: number;
time_alive: number;
turret_id: unknown | null;
vehicle_descr: number;
wp_points_earned: number;
wp_points_stolen: number;
};
};
};
error: {};
};
interface WotInspectorReplay {
id: string;
map_id: number;
battle_duration: number;
title: unknown | null;
player_name: string;
protagonist: number;
vehicle_descr: number;
mastery_badge: number;
exp_base: number;
enemies_spotted: number;
enemies_destroyed: number;
damage_assisted: number;
damage_made: number;
details_url: string;
download_url: string;
game_version: {
name: string;
package: string;
};
arena_unique_id: string;
download_count: number;
data_version: number;
private: boolean;
private_clan: boolean;
battle_start_time: Date;
upload_time: Date;
allies: number[];
enemies: number[];
protagonist_clan: number;
protagonist_team: number;
battle_result: number;
credits_base: number;
tags: number[];
battle_type: number;
room_type: number;
last_accessed_time: Date;
winner_team: number;
finish_reason: number;
players_data: WotInspectorReplayPlayerData[];
exp_total: number;
credits_total: number;
repair_cost: number;
exp_free: number;
exp_free_base: number;
exp_penalty: number;
credits_penalty: number;
credits_contribution_in: number;
credits_contribution_out: number;
camouflage_id: number;
}
export interface WotInspectorReplayPlayerData {
team: number;
name: string;
entity_id: number;
dbid: number;
clanid: number | null;
clan_tag: null | string;
hitpoints_left: number;
credits: number;
exp: number;
shots_made: number;
shots_hit: number;
shots_splash: number;
shots_pen: number;
damage_made: number;
damage_received: number;
damage_assisted: number;
damage_assisted_track: number;
hits_received: number;
hits_bounced: number;
hits_splash: number;
hits_pen: number;
enemies_spotted: number;
enemies_damaged: number;
enemies_destroyed: number;
time_alive: number;
distance_travelled: number;
killed_by: number;
base_capture_points: number;
base_defend_points: number;
exp_for_damage: number;
exp_for_assist: number;
exp_team_bonus: number;
wp_points_earned: number;
wp_points_stolen: number;
hero_bonus_credits: number;
hero_bonus_exp: number;
death_reason: number;
achievements: any[];
vehicle_descr: number;
turret_id: number;
gun_id: number;
chassis_id: number;
squad_index: number;
damage_blocked: number;
}
function stat(value: string | number, flex = 2) {
return (
<div
style={{
flex,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16 }}>{value}</span>
</div>
);
}
async function playerListing(
player: WotInspectorReplayPlayerData,
protagonistTeam: boolean,
winningTeam: number,
recordingPlayer = false,
) {
const awaitedTankAverages = await tankAverages;
const awaitedTankDefinitions = await tankDefinitions;
const tank = awaitedTankDefinitions[player.vehicle_descr];
const blockAccent = recordingPlayer ? '_amber' : '';
const healthAccent = protagonistTeam
? recordingPlayer
? '_amber'
: '_green'
: '_red';
// our turret ids are always off by 1 or 2 lol so imma just grab the closest
// TODO: 💀
const turret = tank.turrets.sort(
(a, b) =>
Math.abs(a.id - player.turret_id) - Math.abs(b.id - player.turret_id),
)[0];
const totalHealth = turret.health + tank.health;
const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1);
const tankAveragesEntry = awaitedTankAverages[player.vehicle_descr];
const wn8 =
tankAveragesEntry === undefined
? -1
: calculateWN8(tankAveragesEntry.all, {
battles: 1,
damage_dealt: player.damage_made,
dropped_capture_points: player.base_defend_points,
frags: player.enemies_destroyed,
spotted: player.enemies_spotted,
wins: winningTeam === player.team ? 1 : 0,
});
return (
<div
key={player.entity_id}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
backgroundColor: theme.colors[`appBackground2${blockAccent}`],
padding: 8,
}}
>
<div
style={{
display: 'flex',
gap: 8,
flex: 1,
overflow: 'hidden',
alignItems: 'center',
}}
>
<img
src={await iconPng(tankIcon(tank.id))}
style={{
width: 32,
height: 32,
objectFit: 'contain',
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
// align
}}
>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
}}
>
<span
style={{
color: theme.colors[`textLowContrast${blockAccent}`],
fontSize: 12,
}}
>
{TIER_ROMAN_NUMERALS[tank.tier]}
</span>
<span
style={{
color: theme.colors[`textHighContrast${blockAccent}`],
fontSize: 16,
}}
>
{tank.name}
</span>
</div>
<span
style={{
fontSize: 16,
color: theme.colors[`textLowContrast${blockAccent}`],
whiteSpace: 'nowrap',
}}
>
{player.name}
{player.clan_tag ? ` [${player.clan_tag}]` : ''}
</span>
</div>
</div>
<div
style={{
display: 'flex',
flex: 1,
}}
>
{stat(player.damage_made.toLocaleString())}
{stat(wn8 === -1 ? '--' : Math.round(wn8).toLocaleString())}
{stat(player.exp.toLocaleString())}
{stat(player.enemies_destroyed, 1)}
</div>
<div
style={{
borderRadius: 2,
backgroundColor: theme.colors[`componentInteractive${healthAccent}`],
display: 'flex',
width: 4,
height: '100%',
transform: 'scaleY(-1)',
}}
>
<div
style={{
width: '100%',
height: `${healthLeft * 100}%`,
borderRadius: 2,
backgroundColor: theme.colors[`solidBackground${healthAccent}`],
}}
/>
</div>
</div>
);
}
function Header() {
return (
<div
style={{
padding: 8,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ flex: 1 }} />
<div
style={{
display: 'flex',
flex: 1,
}}
>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Damage
</span>
</div>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
WN8
</span>
</div>
<div
style={{
flex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
XP
</span>
</div>
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontSize: 16,
color: theme.colors.textLowContrast,
}}
>
Kills
</span>
</div>
</div>
</div>
);
}
export const replayCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
inProduction: true,
inPublic: true,
command: createLocalizedCommand('replay').addAttachmentOption((option) =>
option.setName('file').setDescription('Replay file').setRequired(true),
),
async handler(interaction) {
const awaitedMapDefinitions = await mapDefinitions;
const file = interaction.options.getAttachment('file')!;
// const file = {
// url: 'https://cdn.discordapp.com/ephemeral-attachments/1232434891652861972/1232781496016834581/20240328_1530__TresAbhi_M60_1157057448155406708.wotbreplay?ex=662ab4bc&is=6629633c&hm=c784cd10e80232350ddaf291fa987b879c4d0a8083b78298c9eee4fa0d507f6c&',
// };
const submissionData = (await fetch(
`https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent(
file.url,
)}`,
).then((response) => response.json())) as WotInspectorReplaySubmission;
if (submissionData.status === 'error') {
return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`;
}
const replayId = submissionData.data.download_url.split('/').at(-1)!;
const replayData = (await fetch(
`https://api.wotinspector.com/v2/blitz/replays/${replayId}/`,
).then((response) => response.json())) as WotInspectorReplay;
const antagonistTeamId = 3 - replayData.protagonist_team;
const players = replayData.players_data.sort(
(a, b) => b.damage_made - a.damage_made,
);
const clan = (
await getClanAccountInfo(
idToRegion(replayData.protagonist),
replayData.protagonist,
['clan'],
)
)?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
return [
new RenderConfiguration({ width: 960 }),
<CommandWrapper>
<TitleBar
title={replayData.player_name}
image={clanImage}
description={`${
replayData.protagonist_team === replayData.winner_team
? 'Victory'
: 'Defeat'
} • ${awaitedMapDefinitions[replayData.map_id].name}`}
/>
<div
style={{
display: 'flex',
gap: 8,
}}
>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter(
(player) => player.team === replayData.protagonist_team,
)
.map((player) =>
playerListing(
player,
true,
replayData.winner_team,
player.dbid === replayData.protagonist,
),
),
)}
</div>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
}}
>
<Header />
{await Promise.all(
players
.filter((player) => player.team === antagonistTeamId)
.map((player) =>
playerListing(player, false, replayData.winner_team),
),
)}
</div>
</div>
</CommandWrapper>,
buttonLink(submissionData.data.view_url, 'WoT Inspector'),
buttonLink(submissionData.data.download_url, 'Download'),
];
},
});
});
+8 -10
View File
@@ -9,7 +9,7 @@ import resolveTankId from '../core/blitz/resolveTankId';
import { asset } from '../core/blitzkit/asset';
import { buildTechTreeLine } from '../core/blitzkit/buildTechTreeLine';
import { emblemURL } from '../core/blitzkit/emblemURL';
import { webpToPng } from '../core/blitzkit/iconPng';
import { iconPng } from '../core/blitzkit/iconPng';
import { resolveAncestry } from '../core/blitzkit/resolveAncestry';
import { tankDefinitions } from '../core/blitzkit/tankDefinitions';
import {
@@ -280,7 +280,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(asset('icons/currencies/xp.webp'))}
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
@@ -305,7 +305,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
@@ -330,7 +330,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(asset('icons/currencies/silver.webp'))}
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
@@ -447,9 +447,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
asset('icons/currencies/xp.webp'),
)}
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
@@ -476,7 +474,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
src={await iconPng(
asset('icons/currencies/free-xp.webp'),
)}
width={16}
@@ -503,7 +501,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
@@ -531,7 +529,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
<img
src={await webpToPng(
src={await iconPng(
asset('icons/currencies/silver.webp'),
)}
width={16}
+1 -1
View File
@@ -14,7 +14,7 @@ export default function CommandWrapper({ children, fat }: CommandWrapperProps) {
flexDirection: 'column',
padding: `16px ${fat ? 0 : 16}px`,
gap: 16,
width: 480,
width: '100%',
color: theme.colors.textHighContrast,
position: 'relative',
background: theme.colors.appBackground1,
@@ -0,0 +1,26 @@
import { ExitIcon } from '@radix-ui/react-icons';
import { Flex, IconButton, Text } from '@radix-ui/themes';
import { use } from 'react';
import { getAccountInfo } from '../../../core/blitz/getAccountInfo';
import { idToRegion } from '../../../core/blitz/idToRegion';
import { logout } from '../../../core/blitz/logout';
import { useApp } from '../../../stores/app';
export function LoggedIn() {
const login = useApp((state) => state.login!);
const accountInfo = use(
getAccountInfo(idToRegion(login.id), login.id, [], {
access_token: login.token,
}),
);
return (
<Flex gap="3" align="center">
<Text>Hello, {accountInfo.nickname}</Text>
<IconButton variant="ghost" color="red" onClick={logout}>
<ExitIcon />
</IconButton>
</Flex>
);
}
+148 -74
View File
@@ -2,28 +2,83 @@ import {
DiscordLogoIcon,
GearIcon,
HamburgerMenuIcon,
PersonIcon,
} from '@radix-ui/react-icons';
import {
Button,
Dialog,
Flex,
Heading,
IconButton,
Link as LinkRadix,
Popover,
Spinner,
} from '@radix-ui/themes';
import Link from 'next/link';
import { Suspense, useEffect } from 'react';
import { REGIONS, UNLOCALIZED_REGION_NAMES } from '../../constants/regions';
import { TOOLS } from '../../constants/tools';
import { authURL } from '../../core/blitz/authURL';
import { extendAuth } from '../../core/blitz/extendAuth';
import { logout } from '../../core/blitz/logout';
import { useFullScreen } from '../../hooks/useFullScreen';
import { useWideFormat } from '../../hooks/useWideFormat';
import { BlitzkitWide } from '../../icons/BlitzkitWide';
import { PatreonIcon } from '../../icons/Patreon';
import { theme } from '../../stitches.config';
import { useApp } from '../../stores/app';
import { LoggedIn } from './components/LoggedIn';
export default function Navbar() {
const isFullScreen = useFullScreen();
const wideFormat = useWideFormat(600);
const wideFormat = useWideFormat(480);
const login = useApp((state) => state.login);
useEffect(() => {
if (!login) return;
const expiresIn = login.expiresAt - Date.now() / 1000;
const expiresInDays = expiresIn / 60 / 60 / 24;
if (expiresInDays < 0) {
logout();
} else if (expiresInDays < 7) {
extendAuth();
}
});
if (isFullScreen) return null;
const plugs = (
<>
<Link
href="https://discord.gg/nDt7AjGJQH"
target="_blank"
style={{
color: 'inherit',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<DiscordLogoIcon width={16} height={16} />
</Link>
<Link
href="https://www.patreon.com/tresabhi"
target="_blank"
style={{
color: 'inherit',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PatreonIcon width={14} height={14} />
</Link>
</>
);
return (
<div
style={{
@@ -52,45 +107,49 @@ export default function Navbar() {
style={{ maxWidth: 800, width: '100%', padding: '0 16px' }}
gap="4"
>
{/* {wideFormat ? (
<EverythingSearch style={{ flex: 1 }} />
) : (
<div style={{ flex: 1 }} />
)}
{!wideFormat && (
<IconButton variant="ghost" color="gray">
<MagnifyingGlassIcon />
</IconButton>
)} */}
<Popover.Root>
<Popover.Trigger>
<IconButton variant="ghost" color="gray">
<HamburgerMenuIcon />
</IconButton>
</Popover.Trigger>
<Link
href="https://discord.gg/nDt7AjGJQH"
target="_blank"
style={{
color: 'inherit',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<DiscordLogoIcon width={16} height={16} />
</Link>
<Popover.Content>
<Flex direction="column" gap="4">
{!wideFormat && <Flex gap="2">{plugs}</Flex>}
<Link
href="https://www.patreon.com/tresabhi"
target="_blank"
style={{
color: 'inherit',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PatreonIcon width={14} height={14} />
</Link>
<Flex direction="column" gap="2">
<Heading size="4">Tools</Heading>
<div style={{ flex: 1 }} />
<Flex direction="column" gap="1" pl="4">
{TOOLS.map((tool, index) => {
if (tool.href || tool.disabled) return null;
return (
<LinkRadix
key={tool.id}
tabIndex={-1}
href={`/tools/${tool.id}`}
onClick={(event) => event.preventDefault()}
>
<Link
href={`/tools/${tool.id}`}
key={index}
style={{
textDecoration: 'none',
color: 'inherit',
}}
>
{tool.title}
</Link>
</LinkRadix>
);
})}
</Flex>
</Flex>
</Flex>
</Popover.Content>
</Popover.Root>
<Link
href="/"
@@ -106,6 +165,8 @@ export default function Navbar() {
<div style={{ flex: 1 }} />
{wideFormat && plugs}
<Link href="/settings">
<Flex style={{ width: '100%', height: '100%' }} justify="center">
<IconButton variant="ghost" color="gray">
@@ -114,45 +175,58 @@ export default function Navbar() {
</Flex>
</Link>
<Popover.Root>
<Popover.Trigger>
<IconButton variant="ghost" color="gray">
<HamburgerMenuIcon />
</IconButton>
</Popover.Trigger>
{login && (
<Popover.Root>
<Popover.Trigger>
<IconButton variant="ghost" color="gray">
<PersonIcon />
</IconButton>
</Popover.Trigger>
<Popover.Content align="center">
<Flex direction="column" gap="2">
<Heading size="4">Tools</Heading>
<Flex direction="column" gap="1" pl="4">
{TOOLS.map((tool, index) => {
if (tool.href || tool.disabled) return null;
return (
<LinkRadix
key={tool.id}
tabIndex={-1}
href={`/tools/${tool.id}`}
onClick={(event) => event.preventDefault()}
>
<Link
href={`/tools/${tool.id}`}
key={index}
style={{
textDecoration: 'none',
color: 'inherit',
}}
>
{tool.title}
</Link>
</LinkRadix>
);
})}
<Popover.Content align="center">
<Flex direction="column" gap="2">
<Suspense fallback={<Spinner />}>
<LoggedIn />
</Suspense>
</Flex>
</Flex>
</Popover.Content>
</Popover.Root>
</Popover.Content>
</Popover.Root>
)}
{/* TODO: re-enable in the future */}
{!login && false && (
<Dialog.Root>
<Dialog.Trigger>
<Button variant="ghost">Log in</Button>
</Dialog.Trigger>
<Dialog.Content
style={{
maxWidth: 360,
}}
>
<Flex direction="column" gap="2" align="center">
<Heading size="3">Choose your region</Heading>
<Flex gap="4" align="center" justify="center" wrap="wrap">
{REGIONS.map((region) => (
<LinkRadix
key={region}
href={authURL(
region,
typeof window === 'undefined'
? undefined
: location.href,
)}
>
{UNLOCALIZED_REGION_NAMES[region]}
</LinkRadix>
))}
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
)}
</Flex>
</Flex>
</div>
+1 -1
View File
@@ -10,7 +10,7 @@ export function Party3() {
{/* analytics */}
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-D8DT6EWVFJ"
src="https://www.googletagmanager.com/gtag/js?id=G-F901X282X7"
/>
<script
dangerouslySetInnerHTML={{
+9
View File
@@ -11,3 +11,12 @@ export const UNLOCALIZED_REGION_NAMES: Record<Region, string> = {
asia: 'Asia',
eu: 'Europe',
};
/**
* @deprecated
*/
export const UNLOCALIZED_REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA',
asia: 'APAC',
eu: 'EU',
};
+10
View File
@@ -0,0 +1,10 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
export function authURL(region: Region, callback?: string) {
if (typeof window === 'undefined') throw new Error('window is undefined');
return `https://api.worldoftanks.${region}/wot/auth/login/?application_id=${WARGAMING_APPLICATION_ID}&redirect_uri=${encodeURIComponent(
`${window.location.origin}/callback/wargaming/${callback ? `?return=${location.href}` : ''}`,
)}`;
}
+59
View File
@@ -0,0 +1,59 @@
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { mutateApp, useApp } from '../../stores/app';
import { idToRegion } from './idToRegion';
interface Extension {
data: {
access_token: string;
account_id: number;
expires_at: number;
};
}
export async function extendAuth() {
const { login } = useApp.getState();
if (!login) return;
// error handling? what's that?
/*
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠙⠛⠛⠛⠿⠻⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⡀⠠⠤⠒⢂⣉⣉⣉⣑⣒⣒⠒⠒⠒⠒⠒⠒⠒⠀⠀⠐⠒⠚⠻⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⡠⠔⠉⣀⠔⠒⠉⣀⣀⠀⠀⠀⣀⡀⠈⠉⠑⠒⠒⠒⠒⠒⠈⠉⠉⠉⠁⠂⠀⠈⠙⢿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀⠔⠁⠠⠖⠡⠔⠊⠀⠀⠀⠀⠀⠀⠀⠐⡄⠀⠀⠀⠀⠀⠀⡄⠀⠀⠀⠀⠉⠲⢄⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⠊⠀⢀⣀⣤⣤⣤⣤⣀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠜⠀⠀⠀⠀⣀⡀⠀⠈⠃⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⡿⠥⠐⠂⠀⠀⠀⠀⡄⠀⠰⢺⣿⣿⣿⣿⣿⣟⠀⠈⠐⢤⠀⠀⠀⠀⠀⠀⢀⣠⣶⣾⣯⠀⠀⠉⠂⠀⠠⠤⢄⣀⠙⢿⣿⣿⣿⣿
⣿⡿⠋⠡⠐⠈⣉⠭⠤⠤⢄⡀⠈⠀⠈⠁⠉⠁⡠⠀⠀⠀⠉⠐⠠⠔⠀⠀⠀⠀⠀⠲⣿⠿⠛⠛⠓⠒⠂⠀⠀⠀⠀⠀⠀⠠⡉⢢⠙⣿⣿⣿
⣿⠀⢀⠁⠀⠊⠀⠀⠀⠀⠀⠈⠁⠒⠂⠀⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⢀⣀⡠⠔⠒⠒⠂⠀⠈⠀⡇⣿⣿⣿⣿⣿
⣿⠀⢸⠀⠀⠀⢀⣀⡠⠋⠓⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⠀⠈⠢⠤⡀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⡠⠀⡇⣿⣿⣿⣿⣿
⣿⡀⠘⠀⠀⠀⠀⠀⠘⡄⠀⠀⠀⠈⠑⡦⢄⣀⠀⠀⠐⠒⠁⢸⠀⠀⠠⠒⠄⠀⠀⠀⠀⠀⢀⠇⠀⣀⡀⠀⠀⢀⢾⡆⠀⠈⡀⠎⣸⣿⣿⣿
⣿⣿⣄⡈⠢⠀⠀⠀⠀⠘⣶⣄⡀⠀⠀⡇⠀⠀⠈⠉⠒⠢⡤⣀⡀⠀⠀⠀⠀⠀⠐⠦⠤⠒⠁⠀⠀⠀⠀⣀⢴⠁⠀⢷⠀⠀⠀⢰⣿⣿⣿⣿
⣿⣿⣿⣿⣇⠂⠀⠀⠀⠀⠈⢂⠀⠈⠹⡧⣀⠀⠀⠀⠀⠀⡇⠀⠀⠉⠉⠉⢱⠒⠒⠒⠒⢖⠒⠒⠂⠙⠏⠀⠘⡀⠀⢸⠀⠀⠀⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠑⠄⠰⠀⠀⠁⠐⠲⣤⣴⣄⡀⠀⠀⠀⠀⢸⠀⠀⠀⠀⢸⠀⠀⠀⠀⢠⠀⣠⣷⣶⣿⠀⠀⢰⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠁⢀⠀⠀⠀⠀⠀⡙⠋⠙⠓⠲⢤⣤⣷⣤⣤⣤⣤⣾⣦⣤⣤⣶⣿⣿⣿⣿⡟⢹⠀⠀⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠑⠀⢄⠀⡰⠁⠀⠀⠀⠀⠀⠈⠉⠁⠈⠉⠻⠋⠉⠛⢛⠉⠉⢹⠁⢀⢇⠎⠀⠀⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⠈⠢⢄⡉⠂⠄⡀⠀⠈⠒⠢⠄⠀⢀⣀⣀⣰⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⢀⣎⠀⠼⠊⠀⠀⠀⠘⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⡀⠉⠢⢄⡈⠑⠢⢄⡀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⢀⠀⠀⠀⠀⠀⢻⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣀⡈⠑⠢⢄⡀⠈⠑⠒⠤⠄⣀⣀⠀⠉⠉⠉⠉⠀⠀⠀⣀⡀⠤⠂⠁⠀⢀⠆⠀⠀⢸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⡀⠁⠉⠒⠂⠤⠤⣀⣀⣉⡉⠉⠉⠉⠉⢀⣀⣀⡠⠤⠒⠈⠀⠀⠀⠀⣸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣶⣤⣤⣤⣤⣀⣀⣤⣤⣤⣶⣾⣿⣿⣿⣿⣿
*/
const response = await fetch(
`https://api.worldoftanks.${idToRegion(
login.id,
)}/wot/auth/prolongate/?application_id=${WARGAMING_APPLICATION_ID}&access_token=${
login.token
}`,
);
const json = (await response.json()) as Extension;
mutateApp((draft) => {
draft.login = {
id: login.id,
token: json.data.access_token,
expiresAt: json.data.expires_at,
};
});
}
+3 -1
View File
@@ -60,10 +60,12 @@ async function manageQueue() {
}
}
export type FetchBlitzParams = Record<string, string | number | undefined>;
export default function fetchBlitz<Data extends object>(
region: Region,
path: string,
params: Record<string, string | number | undefined> = {},
params: FetchBlitzParams = {},
) {
return new Promise<Data>((resolve) => {
queue.push({
+58
View File
@@ -0,0 +1,58 @@
import calculateWN8 from '../statistics/calculateWN8';
import { AllStats } from './getAccountInfo';
export type Stat = keyof ReturnType<typeof generateStats>;
export const STAT_NAMES: Record<Stat, string> = {
battles: 'Battles',
winrate: 'Winrate',
wn8: 'WN8',
averageDamage: 'Damage',
damage: 'Accumulated damage',
wins: 'Wins',
};
export const STAT_KEYS = Object.keys(STAT_NAMES) as Stat[];
export function generateStats(s: AllStats, e?: AllStats) {
return {
battles: s.battles,
wins: s.wins,
winrate: s.wins / s.battles,
wn8: e ? calculateWN8(e, s) : null,
damage: s.damage_dealt,
averageDamage: s.damage_dealt / s.battles,
};
}
export function prettifyStats(s: ReturnType<typeof generateStats>) {
return {
battles: `${s.battles}`,
wins: `${s.wins}`,
winrate: `${(s.winrate * 100).toFixed(0)}%`,
wn8: s.wn8 ? s.wn8.toFixed(0) : '--',
damage: `${s.damage}`,
averageDamage: s.averageDamage.toFixed(0),
} satisfies Record<Stat, string>;
}
export function sumStats(s: ReturnType<typeof generateStats>[]) {
function sum(slice: (stat: ReturnType<typeof generateStats>) => number) {
return s.reduce((a, b) => a + slice(b), 0);
}
const battles = sum((s) => s.battles);
const wins = sum((s) => s.wins);
const damage = sum((s) => s.damage);
return {
battles,
wins,
winrate: wins / battles,
damage,
averageDamage: damage / battles,
wn8:
sum((s) => (s.wn8 ? s.wn8 * s.battles : 0)) /
sum((s) => (s.wn8 ? s.battles : 0)),
} satisfies ReturnType<typeof generateStats>;
}
+9 -3
View File
@@ -1,5 +1,5 @@
import { Region } from '../../constants/regions';
import fetchBlitz from './fetchBlitz';
import fetchBlitz, { FetchBlitzParams } from './fetchBlitz';
import { normalizeIds } from './normalizeIds';
export interface AllStats {
@@ -71,7 +71,7 @@ export interface RatingStats {
dropped_capture_points: number;
}
interface IndividualAccountInfo {
export interface IndividualAccountInfo {
account_id: number;
created_at: number;
updated_at: number;
@@ -95,10 +95,16 @@ export async function getAccountInfo<
ReturnType = Ids extends number
? IndividualAccountInfo
: IndividualAccountInfo[],
>(region: Region, ids: Ids, extra: string[] = []) {
>(
region: Region,
ids: Ids,
extra: string[] = [],
params: FetchBlitzParams = {},
) {
const object = await fetchBlitz<AccountInfo>(region, 'account/info', {
account_id: normalizeIds(ids),
extra: extra.length === 0 ? undefined : extra.join(','),
...params,
});
if (typeof ids === 'number') {
+7 -2
View File
@@ -1,10 +1,15 @@
import { Region } from '../../constants/regions';
import { TanksStats } from '../../types/tanksStats';
import fetchBlitz from './fetchBlitz';
import fetchBlitz, { FetchBlitzParams } from './fetchBlitz';
export default async function getTankStats(region: Region, id: number) {
export default async function getTankStats(
region: Region,
id: number,
params?: FetchBlitzParams,
) {
const tankStats = await fetchBlitz<TanksStats>(region, 'tanks/stats', {
account_id: id,
...params,
});
return tankStats[id];
+7
View File
@@ -0,0 +1,7 @@
import { mutateApp } from '../../stores/app';
export function logout() {
mutateApp((draft) => {
draft.login = undefined;
});
}
+1 -1
View File
@@ -2,7 +2,7 @@ import sharp from 'sharp';
const cache: Record<string, Buffer> = {};
export async function webpToPng(url: string) {
export async function iconPng(url: string) {
if (!url.endsWith('.webp')) return url;
if (!cache[url]) {
+4
View File
@@ -1,3 +1,7 @@
import { config } from 'dotenv';
config();
const devHostStartNames = [
'blitzkrieg-',
'localhost',
+6 -3
View File
@@ -1,9 +1,12 @@
import jsxToSvg from './jsxToSvg';
import { RenderConfiguration } from './renderConfiguration';
import { svgToPngThreaded } from './svgToPngThreaded';
export default async function jsxToPngThreaded(jsx: JSX.Element) {
const svg = await jsxToSvg(jsx);
export default async function jsxToPngThreaded(
jsx: JSX.Element,
renderConfiguration: RenderConfiguration,
) {
const svg = await jsxToSvg(jsx, renderConfiguration);
const png = await svgToPngThreaded(svg);
return png;
}
+6 -2
View File
@@ -1,5 +1,6 @@
import { readFileSync } from 'fs';
import satori from 'satori';
import { RenderConfiguration } from '../renderConfiguration';
import robotoBlack from './Roboto-Black.ttf';
import robotoBold from './Roboto-Bold.ttf';
import roboto from './Roboto.ttf';
@@ -10,9 +11,12 @@ const fonts = [
{ file: robotoBlack, weight: 900 as const },
];
export default async function jsxToSvg(jsx: JSX.Element) {
export default async function jsxToSvg(
jsx: JSX.Element,
renderConfiguration = new RenderConfiguration(),
) {
return await satori(jsx, {
width: 480,
width: renderConfiguration.width,
fonts: fonts.map(({ file, weight }) => ({
weight,
name: 'Roboto',
+13
View File
@@ -0,0 +1,13 @@
import { asset } from './asset';
import { fetchCdonLz4 } from './fetchCdonLz4';
export interface MapDefinitions {
[id: number]: {
id: number;
name: string;
};
}
export const mapDefinitions = fetchCdonLz4<MapDefinitions>(
asset('definitions/maps.cdon.lz4'),
);
+11
View File
@@ -0,0 +1,11 @@
export interface RenderConfiguration {
width: number;
}
export class RenderConfiguration implements RenderConfiguration {
width = 480;
constructor(configuration?: Partial<RenderConfiguration>) {
Object.assign(this, configuration);
}
}
-8
View File
@@ -1,8 +0,0 @@
import { useSession } from '../../stores/session';
import { setSession } from './setSession';
export function resetSession() {
const session = useSession.getState();
if (!session.isTracking) return;
setSession(session.region, session.id, session.nickname);
}
-27
View File
@@ -1,27 +0,0 @@
import { Region } from '../../constants/regions';
import { useSession } from '../../stores/session';
import { NormalizedTankStats, TanksStats } from '../../types/tanksStats';
import fetchBlitz from '../blitz/fetchBlitz';
export async function setSession(region: Region, id: number, nickname: string) {
// TODO: generalize this function and also check for more of these kind
const rawTankStats = (
await fetchBlitz<TanksStats>(region, 'tanks/stats', { account_id: id })
)[id];
const tankStats = rawTankStats!.reduce<NormalizedTankStats>(
(accumulator, tank) => ({
...accumulator,
[tank.tank_id]: tank,
}),
{},
);
useSession.setState({
isTracking: true,
id,
region,
nickname,
tankStats,
time: Date.now(),
});
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { webpToPng } from './iconPng';
import { iconPng } from './iconPng';
import { tankIcon } from './tankIcon';
export function tankIconPng(id: number) {
return webpToPng(tankIcon(id));
return iconPng(tankIcon(id));
}
@@ -9,6 +9,7 @@ import {
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate';
import jsxToPngThreaded from '../blitzkit/jsxToPngThreaded';
import { RenderConfiguration } from '../blitzkit/renderConfiguration';
export default async function normalizeInteractionReturnable(
returnable: InteractionReturnable,
@@ -17,11 +18,11 @@ export default async function normalizeInteractionReturnable(
const reply: InteractionEditReplyOptions &
InteractionReplyOptions &
MessageEditOptions = {};
const awaitedReturnable = await returnable;
const normalizedReturnable = Array.isArray(awaitedReturnable)
? awaitedReturnable
: [awaitedReturnable];
let renderConfiguration = new RenderConfiguration();
await Promise.all(
normalizedReturnable.map(async (item, index) => {
@@ -39,10 +40,12 @@ export default async function normalizeInteractionReturnable(
} else if (item instanceof AttachmentBuilder) {
if (!reply.files) reply.files = [];
reply.files.push(item);
} else if (item instanceof RenderConfiguration) {
renderConfiguration = item;
} else if (item === null) {
return;
} else {
const image = await jsxToPngThreaded(item);
const image = await jsxToPngThreaded(item, renderConfiguration);
images.push([index, image]);
}
}),
+11 -1
View File
@@ -6,7 +6,17 @@ import { AllStats } from '../blitz/getAccountInfo';
* @param expected what all players on average perform like
* @param average what the player in question performs like
*/
export default function calculateWN8(expected: AllStats, average: AllStats) {
export default function calculateWN8(
expected: AllStats,
average: {
damage_dealt: number;
battles: number;
spotted: number;
frags: number;
dropped_capture_points: number;
wins: number;
},
) {
const avgDmg = average.damage_dealt / average.battles;
const avgSpot = average.spotted / average.battles;
const avgFrag = average.frags / average.battles;
+41 -59
View File
@@ -1,68 +1,50 @@
import {
IndividualTankStats,
NormalizedTankStats,
emptyIndividualTankStats,
IndividualTankStats,
} from '../../types/tanksStats';
export function deltaTankStats(
aRaw: NormalizedTankStats,
bRaw: NormalizedTankStats,
time?: number,
tanksA: IndividualTankStats[],
tanksB: IndividualTankStats[],
) {
const bTanks = Object.keys(bRaw);
const changedTanks = bTanks.filter((id) => {
return (
aRaw[id as unknown as number]?.all.battles !==
bRaw[id as unknown as number].all.battles &&
(time === undefined
? true
: bRaw[id as unknown as number].last_battle_time > time)
);
});
const diff = changedTanks.map((id) => {
const a = aRaw[id as unknown as number] ?? emptyIndividualTankStats;
const b = bRaw[id as unknown as number];
return tanksB
.map((b) => {
const a =
tanksA.find((a) => a.tank_id === b.tank_id) ?? emptyIndividualTankStats;
const tankDiff = {
account_id: b.account_id,
battle_life_time: b.battle_life_time,
frags: (b.frags ?? a.frags ?? 0) - (a.frags ?? 0),
max_frags: Math.max(a.max_frags, b.max_frags),
in_garage: b.in_garage,
max_xp: Math.max(a.max_xp, b.max_xp),
in_garage_updated: b.in_garage_updated,
last_battle_time: Math.max(b.last_battle_time, a.last_battle_time),
mark_of_mastery: b.mark_of_mastery,
tank_id: b.tank_id,
return {
account_id: b.account_id,
battle_life_time: b.battle_life_time - a.battle_life_time,
frags: b.frags === null || a.frags === null ? null : b.frags - a.frags,
in_garage: b.in_garage,
in_garage_updated: b.in_garage_updated,
last_battle_time: b.last_battle_time,
mark_of_mastery: b.mark_of_mastery,
max_frags: b.max_frags,
max_xp: b.max_xp,
tank_id: b.tank_id,
all: {
battles: b.all.battles - a.all.battles,
wins: b.all.wins - a.all.wins,
capture_points: b.all.capture_points - a.all.capture_points,
damage_dealt: b.all.damage_dealt - a.all.damage_dealt,
damage_received: b.all.damage_received - a.all.damage_received,
dropped_capture_points:
b.all.dropped_capture_points - a.all.dropped_capture_points,
frags: b.all.frags - a.all.frags,
frags8p: b.all.frags8p - a.all.frags8p,
hits: b.all.hits - a.all.hits,
losses: b.all.losses - a.all.losses,
max_frags: Math.max(a.max_frags, b.max_frags),
max_xp: Math.max(a.max_xp, b.max_xp),
shots: b.all.shots - a.all.shots,
spotted: b.all.spotted - a.all.spotted,
survived_battles: b.all.survived_battles - a.all.survived_battles,
win_and_survived: b.all.win_and_survived - a.all.win_and_survived,
xp: b.all.xp - a.all.xp,
},
} satisfies IndividualTankStats;
return tankDiff;
});
const normalized = diff.reduce<NormalizedTankStats>(
(accumulator, diff) => ({ ...accumulator, [diff.tank_id]: diff }),
{},
);
return normalized;
all: {
battles: b.all.battles - a.all.battles,
capture_points: b.all.capture_points - a.all.capture_points,
damage_dealt: b.all.damage_dealt - a.all.damage_dealt,
damage_received: b.all.damage_received - a.all.damage_received,
dropped_capture_points:
b.all.dropped_capture_points - a.all.dropped_capture_points,
frags: b.all.frags - a.all.frags,
frags8p: b.all.frags8p - a.all.frags8p,
hits: b.all.hits - a.all.hits,
losses: b.all.losses - a.all.losses,
max_frags: b.all.max_frags,
max_xp: b.all.max_xp,
shots: b.all.shots - a.all.shots,
spotted: b.all.spotted - a.all.spotted,
survived_battles: b.all.survived_battles - a.all.survived_battles,
xp: b.all.xp - a.all.xp,
win_and_survived: b.all.win_and_survived - a.all.win_and_survived,
wins: b.all.wins - a.all.wins,
},
} satisfies IndividualTankStats;
})
.filter((a) => a !== null && a.all.battles > 0) as IndividualTankStats[];
}
+59
View File
@@ -0,0 +1,59 @@
import AdmZip from 'adm-zip';
import { readFile, writeFile } from 'fs/promises';
import { Parser } from 'pickleparser';
import protobuf from 'protobufjs';
import { ReadStream } from './buffer';
interface WotbReplayMetaJson {
version: string;
title: string;
dbid: string;
playerName: string;
battleStartTime: string;
playerVehicleName: string;
mapName: string;
arenaUniqueId: string;
battleDuration: number;
vehicleCompDescriptor: number;
camouflageId: number;
mapId: number;
arenaBonusType: number;
}
export class WotbReplayReadStream extends ReadStream {
async wotbReplay() {
const pickleParser = new Parser();
const zip = new AdmZip(Buffer.from(this.buffer));
const metaJsonBuffer = zip.getEntry('meta.json')?.getData();
const battleResultsDat = zip.getEntry('battle_results.dat')?.getData();
if (!metaJsonBuffer || !battleResultsDat)
throw new SyntaxError('meta.json not found');
const metaJson = JSON.parse(
metaJsonBuffer.toString(),
) as WotbReplayMetaJson;
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
const [, battleResultsString] = unpickledBattleResults as [number, string];
const root = await protobuf.load(
`${__dirname}/../../protos/BattleResultsClient.proto`,
);
const BattleResults = root.lookupType(
'BattleResultsGenerated.BattleResultsClient',
);
writeFile('test.dat', Buffer.from(battleResultsString));
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
// const decodedBattleResults = BattleResults.decodeDelimited(
// Buffer.from(battleResultsString),
// );
// console.log(decodedBattleResults.toJSON());
}
}
const replay = await readFile('temp/test3.wotbreplay');
const stream = new WotbReplayReadStream(replay.buffer);
stream.wotbReplay();
+4
View File
@@ -27,6 +27,7 @@ import { pingCommand } from '../../commands/ping';
import { playerInfoCommand } from '../../commands/playerInfo';
import { ratingCommand } from '../../commands/rating';
import { ratingLeaderboardCommand } from '../../commands/ratingLeaderboard';
import { replayCommand } from '../../commands/replay';
import { researchCommand } from '../../commands/research';
import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers';
@@ -34,6 +35,7 @@ import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today';
import getClientId from '../../core/blitzkit/getClientId';
import isDev from '../../core/blitzkit/isDev';
import { RenderConfiguration } from '../../core/blitzkit/renderConfiguration';
import { secrets } from '../../core/blitzkit/secrets';
import handleAutocomplete from './handlers/autocomplete';
import handleButton from './handlers/button';
@@ -44,6 +46,7 @@ export type InteractionRawReturnable =
| EmbedBuilder
| ButtonBuilder
| AttachmentBuilder
| RenderConfiguration
| JSX.Element
| null;
export type InteractionIterableReturnable =
@@ -97,6 +100,7 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
todayCommand,
researchCommand,
ratingCommand,
replayCommand,
];
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
+4 -4
View File
@@ -16,21 +16,21 @@ export function BlitzkitButtonWatermark(props: ComponentProps<'svg'>) {
fill="#8D8D8D"
/>
<path
d="M197.392 97.0371C194.907 95.398 192.778 94.4555 191.007 94.2094C188.967 93.9067 187.181 94.302 185.649 95.3953C184.212 96.4202 183.623 97.5038 183.883 98.6459C184.157 99.7783 185.218 101.003 187.066 102.321C188.804 103.561 190.494 104.279 192.136 104.474C193.779 104.669 195.325 104.25 196.776 103.215C198.158 102.229 198.815 101.077 198.746 99.7593C198.687 98.8011 198.235 97.8936 197.392 97.0371Z"
d="M195.007 94.2094C196.778 94.4555 198.907 95.398 201.392 97.0371C202.235 97.8936 202.687 98.8011 202.746 99.7593C202.815 101.077 202.158 102.229 200.776 103.215C199.325 104.25 197.779 104.669 196.136 104.474C194.494 104.279 192.804 103.561 191.066 102.321C189.218 101.003 188.157 99.7783 187.883 98.6459C187.623 97.5038 188.212 96.4202 189.649 95.3953C191.181 94.302 192.967 93.9067 195.007 94.2094Z"
fill="white"
/>
<path
d="M220.559 280.9L243.11 264.885C249.975 260.009 253.298 255.223 253.077 250.527C252.857 245.747 249.802 241.265 243.911 237.081C238.079 232.94 231.827 230.812 225.155 230.697C218.483 230.498 211.715 232.837 204.849 237.713L182.299 253.728L220.559 280.9Z"
d="M221.456 275.38L242.999 260.08C249.557 255.422 252.732 250.85 252.521 246.364C252.31 241.797 249.392 237.516 243.764 233.519C238.193 229.562 232.22 227.529 225.846 227.42C219.473 227.23 213.007 229.465 206.448 234.123L184.905 249.422L221.456 275.38Z"
fill="white"
/>
<path
d="M164.361 240.989L184.426 226.74C191.232 221.906 194.672 217.371 194.745 213.135C194.759 208.858 192.086 204.816 186.726 201.009C181.424 197.244 175.821 195.408 169.915 195.502C164.01 195.596 157.655 198.059 150.848 202.893L130.784 217.143L164.361 240.989Z"
d="M167.77 237.252L186.937 223.64C193.44 219.022 196.726 214.69 196.796 210.643C196.809 206.557 194.256 202.696 189.135 199.059C184.07 195.462 178.717 193.708 173.076 193.798C167.435 193.887 161.363 196.241 154.861 200.859L135.693 214.472L167.77 237.252Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M416.628 180.603L235.625 52.6262C223.13 43.7913 202.87 43.7913 190.375 52.6262L9.37172 180.603C-3.12391 189.438 -3.12391 203.762 9.37173 212.597L190.375 340.574C202.87 349.409 223.13 349.409 235.625 340.574L416.628 212.597C429.124 203.762 429.124 189.438 416.628 180.603ZM214.47 76.4488L228.267 101.781L235 96.9786L220.094 71.0312L215.844 68L194.721 83.0665L199.957 86.8006L214.47 76.4488ZM176 99.6413C176.042 102.326 177.602 104.766 180.681 106.963C183.679 109.101 186.888 110.238 190.31 110.375C193.731 110.512 196.852 109.575 199.671 107.564C202.38 105.631 203.434 103.493 202.832 101.151C202.794 101.004 202.753 100.859 202.707 100.715C204.879 102.845 205.886 104.918 205.727 106.934C205.494 109.697 203.537 112.391 199.856 115.017L199.24 115.456L204.208 119L204.886 118.517C210.606 114.3 213.459 110.059 213.445 105.793C213.431 101.507 210.92 97.578 205.91 94.005L205.11 93.4339C200.484 90.1343 196.097 88.5088 191.95 88.5574C187.804 88.606 184.143 89.7627 180.968 92.0273C177.629 94.409 175.973 96.947 176 99.6413ZM314.916 235.433L217 168.61L238.183 153.593L289.031 187.048L287.043 151.614L310.477 135L312.251 176.987L386.628 184.595L361.645 202.306L313.099 197.085L313.405 204.328L336.099 220.416L314.916 235.433ZM84 222.631L212.655 314L263.704 277.746C277.613 267.868 285.559 257.622 287.543 247.01C289.586 236.355 284.776 226.886 273.112 218.602C264.688 212.62 255.931 209.217 246.841 208.393C237.809 207.528 229.48 208.59 221.853 211.58L221.5 211.329C224.585 206.711 225.488 201.636 224.207 196.102C222.927 190.485 218.723 185.146 211.595 180.084C200.108 171.926 187.601 168.718 174.073 170.46C160.546 172.202 146.709 178.096 132.563 188.142L84 222.631Z"
d="M416.628 180.603L235.625 52.6262C223.13 43.7913 202.87 43.7913 190.375 52.6262L9.37172 180.603C-3.12391 189.438 -3.12391 203.762 9.37173 212.597L190.375 340.574C202.87 349.409 223.13 349.409 235.625 340.574L416.628 212.597C429.124 203.762 429.124 189.438 416.628 180.603ZM218.47 76.4488L232.267 101.781L239 96.9786L224.094 71.0312L219.844 68L198.721 83.0665L203.957 86.8006L218.47 76.4488ZM184.681 106.963C181.602 104.766 180.042 102.326 180 99.6413C179.973 96.947 181.629 94.409 184.968 92.0273C188.143 89.7627 191.804 88.6061 195.95 88.5574C200.097 88.5088 204.484 90.1343 209.11 93.4339L209.91 94.005C214.92 97.578 217.431 101.507 217.445 105.793C217.459 110.059 214.606 114.3 208.886 118.517L208.208 119L203.24 115.456L203.856 115.017C207.537 112.391 209.494 109.697 209.727 106.934C209.886 104.918 208.879 102.845 206.707 100.715C206.753 100.859 206.794 101.004 206.832 101.151C207.434 103.493 206.38 105.631 203.671 107.564C200.852 109.575 197.731 110.512 194.31 110.375C190.888 110.238 187.679 109.101 184.681 106.963ZM311.596 231.945L218.056 168.108L238.292 153.762L286.867 185.722L284.968 151.871L307.355 136L309.049 176.111L380.102 183.378L356.236 200.298L309.86 195.31L310.153 202.23L331.832 217.599L311.596 231.945ZM91 219.714L213.905 307L262.672 272.366C275.96 262.929 283.551 253.142 285.446 243.003C287.398 232.825 282.803 223.779 271.66 215.866C263.613 210.151 255.247 206.9 246.563 206.113C237.935 205.286 229.978 206.301 222.692 209.158L222.354 208.918C225.302 204.506 226.165 199.658 224.941 194.372C223.718 189.006 219.702 183.905 212.893 179.069C201.919 171.275 189.971 168.211 177.048 169.875C164.124 171.539 150.906 177.17 137.393 186.767L91 219.714Z"
fill="white"
/>
</g>
+4 -4
View File
@@ -3,16 +3,16 @@ import { ComponentProps } from 'react';
export function BlitzkitWide(props: ComponentProps<'svg'>) {
return (
<svg
width="60"
width="62"
height="15"
viewBox="0 0 60 15"
viewBox="0 0 62 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M0.204102 14V1.20312H5.45996C6.9541 1.20312 8.11719 1.48145 8.94922 2.03809C9.78125 2.59473 10.1973 3.43848 10.1973 4.56934C10.1973 5.27246 10.0449 5.85547 9.74023 6.31836C9.43555 6.78125 9.06055 7.0918 8.61523 7.25V7.28516C9.16602 7.44336 9.62891 7.77734 10.0039 8.28711C10.3848 8.79688 10.5752 9.47656 10.5752 10.3262C10.5752 11.4863 10.127 12.3887 9.23047 13.0332C8.33398 13.6777 7.15625 14 5.69727 14H0.204102ZM3.64062 11.6973H5.48633C6.03711 11.6973 6.44141 11.5508 6.69922 11.2578C6.96289 10.9648 7.09473 10.5664 7.09473 10.0625C7.09473 9.57031 6.96289 9.17773 6.69922 8.88477C6.44141 8.5918 6.03711 8.44531 5.48633 8.44531H3.64062V11.6973ZM3.64062 6.35352H5.2666C5.80566 6.35352 6.19238 6.23047 6.42676 5.98438C6.66699 5.73828 6.78711 5.39258 6.78711 4.94727C6.78711 4.50195 6.66699 4.15625 6.42676 3.91016C6.19238 3.66406 5.80566 3.54102 5.2666 3.54102H3.64062V6.35352ZM12.1924 14V0.631836H15.5674V14H12.1924ZM17.5449 14V4.82422H20.9375V14H17.5449ZM17.3164 2.37207C17.3164 1.88574 17.4834 1.47852 17.8174 1.15039C18.1514 0.816406 18.626 0.649414 19.2412 0.649414C19.8623 0.649414 20.3369 0.816406 20.665 1.15039C20.999 1.47852 21.166 1.88574 21.166 2.37207C21.166 2.86426 20.999 3.27734 20.665 3.61133C20.3311 3.94531 19.8535 4.1123 19.2324 4.1123C18.6172 4.1123 18.1426 3.94531 17.8086 3.61133C17.4805 3.27734 17.3164 2.86426 17.3164 2.37207ZM21.9395 7.22363V4.82422H28.0215V7.22363H21.9395ZM23.126 10.9502V5.89648L23.1523 5.65918V2.67969H26.4746V10.7568C26.4746 11.1904 26.5479 11.4834 26.6943 11.6357C26.8467 11.7822 27.04 11.8555 27.2744 11.8555C27.3916 11.8555 27.5117 11.8408 27.6348 11.8115C27.7578 11.7822 27.8691 11.7441 27.9688 11.6973V13.9736C27.8398 14.0439 27.6406 14.1055 27.3711 14.1582C27.1016 14.2168 26.7998 14.2461 26.4658 14.2461C25.458 14.2461 24.6494 13.9824 24.04 13.4551C23.4307 12.9219 23.126 12.0869 23.126 10.9502ZM28.8125 14V11.9609L32.4688 7.29395H28.8916V4.82422H36.9424V6.81934L33.2334 11.5566H37.2324V14H28.8125ZM38.5947 14V0.631836H41.9521V7.44336L44.3428 4.81543H48.0254L41.9521 11.3105V14H38.5947ZM44.4219 14L41.5654 10.0449L43.71 7.64551L48.377 14H44.4219ZM49.0449 14V4.82422H52.4375V14H49.0449ZM48.8164 2.37207C48.8164 1.88574 48.9834 1.47852 49.3174 1.15039C49.6514 0.816406 50.126 0.649414 50.7412 0.649414C51.3623 0.649414 51.8369 0.816406 52.165 1.15039C52.499 1.47852 52.666 1.88574 52.666 2.37207C52.666 2.86426 52.499 3.27734 52.165 3.61133C51.8311 3.94531 51.3535 4.1123 50.7324 4.1123C50.1172 4.1123 49.6426 3.94531 49.3086 3.61133C48.9805 3.27734 48.8164 2.86426 48.8164 2.37207ZM53.4395 7.22363V4.82422H59.5215V7.22363H53.4395ZM54.626 10.9502V5.89648L54.6523 5.65918V2.67969H57.9746V10.7568C57.9746 11.1904 58.0479 11.4834 58.1943 11.6357C58.3467 11.7822 58.54 11.8555 58.7744 11.8555C58.8916 11.8555 59.0117 11.8408 59.1348 11.8115C59.2578 11.7822 59.3691 11.7441 59.4688 11.6973V13.9736C59.3398 14.0439 59.1406 14.1055 58.8711 14.1582C58.6016 14.2168 58.2998 14.2461 57.9658 14.2461C56.958 14.2461 56.1494 13.9824 55.54 13.4551C54.9307 12.9219 54.626 12.0869 54.626 10.9502Z"
fill="currentColor"
d="M0.204102 14V1.20312H5.45996C6.9541 1.20312 8.11719 1.48145 8.94922 2.03809C9.78125 2.59473 10.1973 3.43848 10.1973 4.56934C10.1973 5.27246 10.0449 5.85547 9.74023 6.31836C9.43555 6.78125 9.06055 7.0918 8.61523 7.25V7.28516C9.16602 7.44336 9.62891 7.77734 10.0039 8.28711C10.3848 8.79688 10.5752 9.47656 10.5752 10.3262C10.5752 11.4863 10.127 12.3887 9.23047 13.0332C8.33398 13.6777 7.15625 14 5.69727 14H0.204102ZM3.64062 11.6973H5.48633C6.03711 11.6973 6.44141 11.5508 6.69922 11.2578C6.96289 10.9648 7.09473 10.5664 7.09473 10.0625C7.09473 9.57031 6.96289 9.17773 6.69922 8.88477C6.44141 8.5918 6.03711 8.44531 5.48633 8.44531H3.64062V11.6973ZM3.64062 6.35352H5.2666C5.80566 6.35352 6.19238 6.23047 6.42676 5.98438C6.66699 5.73828 6.78711 5.39258 6.78711 4.94727C6.78711 4.50195 6.66699 4.15625 6.42676 3.91016C6.19238 3.66406 5.80566 3.54102 5.2666 3.54102H3.64062V6.35352ZM12.1924 14V0.631836H15.5674V14H12.1924ZM17.5449 14V4.82422H20.9375V14H17.5449ZM17.3164 2.37207C17.3164 1.88574 17.4834 1.47852 17.8174 1.15039C18.1514 0.816406 18.626 0.649414 19.2412 0.649414C19.8623 0.649414 20.3369 0.816406 20.665 1.15039C20.999 1.47852 21.166 1.88574 21.166 2.37207C21.166 2.86426 20.999 3.27734 20.665 3.61133C20.3311 3.94531 19.8535 4.1123 19.2324 4.1123C18.6172 4.1123 18.1426 3.94531 17.8086 3.61133C17.4805 3.27734 17.3164 2.86426 17.3164 2.37207ZM21.9395 7.22363V4.82422H28.0215V7.22363H21.9395ZM23.126 10.9502V5.89648L23.1523 5.65918V2.67969H26.4746V10.7568C26.4746 11.1904 26.5479 11.4834 26.6943 11.6357C26.8467 11.7822 27.04 11.8555 27.2744 11.8555C27.3916 11.8555 27.5117 11.8408 27.6348 11.8115C27.7578 11.7822 27.8691 11.7441 27.9688 11.6973V13.9736C27.8398 14.0439 27.6406 14.1055 27.3711 14.1582C27.1016 14.2168 26.7998 14.2461 26.4658 14.2461C25.458 14.2461 24.6494 13.9824 24.04 13.4551C23.4307 12.9219 23.126 12.0869 23.126 10.9502ZM28.8125 14V11.9609L32.4688 7.29395H28.8916V4.82422H36.9424V6.81934L33.2334 11.5566H37.2324V14H28.8125ZM38.8057 14V1.20312H42.2773V5.72949L46.1094 1.20312H49.7305L42.2773 9.68457V14H38.8057ZM46.373 14L42.2861 8.17285L44.4834 5.65918L50.5391 14H46.373ZM50.9961 14V4.82422H54.3887V14H50.9961ZM50.7676 2.37207C50.7676 1.88574 50.9346 1.47852 51.2686 1.15039C51.6025 0.816406 52.0771 0.649414 52.6924 0.649414C53.3135 0.649414 53.7881 0.816406 54.1162 1.15039C54.4502 1.47852 54.6172 1.88574 54.6172 2.37207C54.6172 2.86426 54.4502 3.27734 54.1162 3.61133C53.7822 3.94531 53.3047 4.1123 52.6836 4.1123C52.0684 4.1123 51.5938 3.94531 51.2598 3.61133C50.9316 3.27734 50.7676 2.86426 50.7676 2.37207ZM55.3906 7.22363V4.82422H61.4727V7.22363H55.3906ZM56.5771 10.9502V5.89648L56.6035 5.65918V2.67969H59.9258V10.7568C59.9258 11.1904 59.999 11.4834 60.1455 11.6357C60.2979 11.7822 60.4912 11.8555 60.7256 11.8555C60.8428 11.8555 60.9629 11.8408 61.0859 11.8115C61.209 11.7822 61.3203 11.7441 61.4199 11.6973V13.9736C61.291 14.0439 61.0918 14.1055 60.8223 14.1582C60.5527 14.2168 60.251 14.2461 59.917 14.2461C58.9092 14.2461 58.1006 13.9824 57.4912 13.4551C56.8818 12.9219 56.5771 12.0869 56.5771 10.9502Z"
fill="white"
/>
</svg>
);
+5
View File
@@ -208,6 +208,11 @@
},
"commands": {
"replay": {
"$": "replay",
"description": "Display replay results"
},
"research": {
"$": "research",
"description": "Lists the expenses in researching a line",
+298
View File
@@ -0,0 +1,298 @@
syntax = "proto3";
package BattleResultsGenerated;
import "PlayerAvatarImage.proto";
message Int32List {
repeated int32 values = 1;
}
message PlayerInfo {
message AvatarEntry {
int32 key = 1;
.PlayerAvatarInfo value = 2;
}
string name = 1;
int32 prebattleID = 2;
int32 team = 3;
int64 clanID = 4;
string clanTag = 5;
repeated int32 chatMessageCount = 6;
repeated .BattleResultsGenerated.PlayerInfo.AvatarEntry avatar = 7;
int32 profileBackground = 8;
int32 vehicleRank = 9;
}
message TeamInfo {
string title = 1;
string icon = 2;
}
message VehiclePublicResults {
message ExtVehAchievementsEntry {
int32 key = 1;
int32 value = 2;
}
message ExtAccAchievementsEntry {
int32 key = 1;
int32 value = 2;
}
int32 health = 1;
int32 credits = 2;
int32 xp = 3;
int32 shots = 4;
int32 directHits = 5;
int32 explosionHits = 6;
int32 piercings = 7;
int32 damageDealt = 8;
int32 damageAssistedRadio = 9;
int32 damageAssistedTrack = 10;
int32 damageReceived = 11;
int32 directHitsReceived = 12;
int32 noDamageDirectHitsReceived = 13;
int32 explosionHitsReceived = 14;
int32 piercingsReceived = 15;
int32 spotted = 16;
int32 damaged = 17;
int32 kills = 18;
int32 tdamageDealt = 19;
int32 tkills = 20;
int32 capturePoints = 21;
int32 droppedCapturePoints = 22;
int32 mileage = 23;
int32 lifeTime = 24;
int32 killerID = 25;
repeated int32 achievements = 26;
repeated .BattleResultsGenerated.VehiclePublicResults.ExtVehAchievementsEntry extVehAchievements = 27;
repeated .BattleResultsGenerated.VehiclePublicResults.ExtAccAchievementsEntry extAccAchievements = 28;
int32 xpDamage = 29;
int32 xpAssist = 30;
int32 xpTeamBonus = 31;
int32 wpEarned = 32;
int32 wpStolen = 33;
int32 heroCreditsBonus = 34;
int32 heroXpBonus = 35;
int64 accountDBID = 101;
int32 team = 102;
int32 serializedVehType = 103;
int32 gold = 104;
int32 deathReason = 105;
int32 camouflage = 106;
float mmr = 107;
float mmrDiff = 108;
int32 calibrationBattlesLeft = 109;
int32 healedHP = 114;
int32 wasHealedFor = 115;
int32 xpActivity = 36;
int32 customization = 116;
int32 potentialEnemyDamage = 117;
int32 rammingDamage = 118;
int32 helpedToKill = 119;
}
message BattleResultsFinalXp {
int32 xpDamagePure = 1;
int32 xpDamagePrem = 2;
int32 xpAssistPure = 3;
int32 xpAssistPrem = 4;
int32 xpActivityPure = 5;
int32 xpActivityPrem = 6;
int32 xpTeamBonusPure = 7;
int32 xpTeamBonusPrem = 8;
int32 healingXpPure = 9;
int32 healingXpPrem = 10;
int32 heroXpBonusPure = 11;
int32 heroXpBonusPrem = 12;
int32 earnedFreeXpPure = 13;
int32 earnedFreeXpPrem = 14;
int32 earnedAvatarXpPure = 15;
int32 earnedAvatarXpPrem = 16;
int32 xpNoModifiers = 17;
int32 xpPure = 18;
int32 xpPrem = 19;
int32 totalFreeXpPure = 20;
int32 totalFreeXpPrem = 21;
int32 totalAvatarXpPure = 22;
int32 totalAvatarXpPrem = 23;
}
message Ribbons {
int32 id = 1;
int32 grade = 2;
int32 amount = 3;
int32 value = 4;
}
message BattleResultsUpdate {
message ExcellentBattleDataEntry {
string key = 1;
int32 value = 2;
}
message StuffEntry {
int32 key = 1;
int32 value = 2;
}
message RatingBattleStuffBonusEntry {
int32 key = 1;
int32 value = 2;
}
int32 avatarXP = 1;
int32 eventCredits = 2;
int32 eventGold = 3;
int32 eventXP = 4;
int32 eventFreeXP = 5;
int32 eventAvatarXP = 6;
int32 autoRepairCost = 7;
repeated int32 autoLoadCost = 8;
repeated int32 autoEquipCost = 9;
repeated int32 autoProvCost = 10;
bool isPremium = 12;
int32 premiumXPFactor10 = 13;
int32 premiumCreditsFactor10 = 14;
int32 xpFactor10 = 15;
int32 creditsFactor10 = 16;
int32 aogasFactor10 = 17;
int32 markOfMastery = 18;
repeated .BattleResultsGenerated.BattleResultsUpdate.ExcellentBattleDataEntry excellentBattleData = 19;
bytes vehDossier = 20;
bytes accDossier = 21;
bytes vehDossierPrev = 22;
bytes accDossierPrev = 23;
int32 autoRepairDiscount = 24;
repeated int32 autoEquipDiscount = 25;
repeated int32 autoProvDiscount = 26;
repeated .BattleResultsGenerated.BattleResultsUpdate.StuffEntry stuff = 27;
int32 boosterCredits = 29;
int32 boosterXp = 30;
int32 boosterFreeXp = 31;
int32 boosterAvatarXp = 32;
int32 complimentsLimit = 33;
repeated int32 autoAbilsCost = 34;
repeated int32 autoAbilsDiscount = 35;
bool hasHealingReward = 36;
int32 healingCredits = 37;
int32 healingXp = 38;
int32 healingFreeXp = 39;
int32 healingAvatarXp = 40;
float mmr = 107;
float mmrDiff = 108;
int32 calibrationBattlesLeft = 109;
int32 ratingBattleXp = 110;
int32 ratingBattleFreeXp = 111;
int32 ratingBattleAvatarXp = 112;
repeated .BattleResultsGenerated.BattleResultsUpdate.RatingBattleStuffBonusEntry ratingBattleStuffBonus = 113;
repeated .BattleResultsGenerated.Ribbons ribbons = 114;
}
message DropData {
message StuffEntry {
int32 key = 1;
int32 value = 2;
}
int32 credits = 1;
int32 gold = 2;
int32 xp = 3;
int32 freeXP = 4;
int32 avatarXP = 5;
repeated .BattleResultsGenerated.DropData.StuffEntry stuff = 27;
}
message VehicleInteractionDetails {
message DamagedModulesEntry {
int32 key = 1;
int32 value = 2;
}
message DestroyedModulesEntry {
int32 key = 1;
int32 value = 2;
}
bool spotted = 1;
int32 deathReason = 2;
int32 directHits = 3;
int32 directHitWithoutDamage = 4;
int32 explosionHits = 5;
int32 piercings = 6;
int32 damageDealt = 7;
int32 damageAssistedTrack = 8;
int32 damageAssistedRadio = 9;
int32 fire = 10;
repeated .BattleResultsGenerated.VehicleInteractionDetails.DamagedModulesEntry damagedModules = 11;
repeated .BattleResultsGenerated.VehicleInteractionDetails.DestroyedModulesEntry destroyedModules = 12;
int32 ricochetHitWithoutDamage = 13;
}
message InteractionDetails {
message VehiclesEntry {
int32 key = 1;
.BattleResultsGenerated.VehicleInteractionDetails value = 2;
}
repeated .BattleResultsGenerated.InteractionDetails.VehiclesEntry vehicles = 1;
}
message PrebattleStats {
int32 freeXp = 1;
int32 vehicleXp = 2;
}
message BattleResultsClient {
message QuestsProgressEntry {
string key = 1;
.BattleResultsGenerated.Int32List value = 2;
}
message DropCategoriesEntry {
string key = 1;
.BattleResultsGenerated.DropData value = 2;
}
message PlayersEntry {
int64 key = 1;
.BattleResultsGenerated.PlayerInfo value = 2;
}
message VehiclesEntry {
int32 key = 1;
.BattleResultsGenerated.VehiclePublicResults value = 2;
}
int32 arenaTypeID = 1;
int32 arenaCreateTime = 2;
int32 winnerTeam = 3;
int32 finishReason = 4;
int32 duration = 5;
int32 bonusType = 6;
int32 vehLockMode = 7;
.BattleResultsGenerated.VehiclePublicResults public = 8;
int32 gameModeID = 9;
int32 observerMode = 10;
.BattleResultsGenerated.BattleResultsFinalXp finalXp = 11;
int32 repair = 136;
int32 freeXP = 137;
int32 xpPenalty = 145;
int32 creditsPenalty = 146;
int32 creditsContributionIn = 147;
int32 creditsContributionOut = 148;
.BattleResultsGenerated.BattleResultsUpdate update = 150;
int32 originalXP = 181;
int32 originalFreeXP = 182;
int32 originalCredits = 183;
repeated .BattleResultsGenerated.BattleResultsClient.QuestsProgressEntry questsProgress = 184;
repeated .BattleResultsGenerated.BattleResultsClient.DropCategoriesEntry dropCategories = 185;
.BattleResultsGenerated.TeamInfo teamInfo = 186;
repeated .BattleResultsGenerated.BattleResultsClient.PlayersEntry players = 201;
repeated .BattleResultsGenerated.BattleResultsClient.VehiclesEntry vehicles = 301;
.BattleResultsGenerated.InteractionDetails interactionDetails = 302;
.BattleResultsGenerated.PrebattleStats prebattleStats = 303;
}
+8
View File
@@ -0,0 +1,8 @@
syntax = "proto3";
message PlayerAvatarInfo {
uint32 id = 1;
string image = 2;
string image_retina = 3;
string grade = 4;
}
+12
View File
@@ -1,11 +1,19 @@
import { produce } from 'immer';
import { merge } from 'lodash';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface Login {
id: number;
token: string;
expiresAt: number;
}
export interface App {
devBuildAgreementTime: number;
developerMode: boolean;
darkMode: boolean;
login?: Login;
}
export const useApp = create<App>()(
@@ -18,3 +26,7 @@ export const useApp = create<App>()(
{ name: 'app', merge: (a, b) => merge(b, a) },
),
);
export function mutateApp(recipe: (draft: App) => void) {
useApp.setState(produce(recipe));
}
+23 -42
View File
@@ -2,56 +2,37 @@ import { produce } from 'immer';
import { merge } from 'lodash';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn';
import { AccentColor, GrayColor } from '../constants/radixColors';
import { Region } from '../constants/regions';
import { NormalizedTankStats } from '../types/tanksStats';
import { Stat } from '../core/blitz/generateStats';
import { IndividualTankStats } from '../types/tanksStats';
type Session = (
| {
isTracking: false;
}
| {
isTracking: true;
interface SessionBase {
columns: Stat[];
}
region: Region;
id: number;
nickname: string;
tankStats: NormalizedTankStats;
time: number;
}
) & {
title: string;
resetPrompt: boolean;
embedPrompt: boolean;
customColumns: {
display: CustomColumnDisplay;
showDelta: boolean;
}[] & { length: 4 };
showTotal: boolean;
showCareer: boolean;
color: AccentColor | GrayColor;
};
export interface SessionTracking extends SessionBase {
tracking: true;
player: {
id: number;
region: Region;
since: number;
stats: IndividualTankStats[];
};
}
interface SessionNotTracking extends SessionBase {
tracking: false;
}
type Session = SessionTracking | SessionNotTracking;
export const useSession = create<Session>()(
persist(
(set) => ({
title: 'Untitled session',
isTracking: false,
resetPrompt: true,
showTotal: true,
showCareer: true,
embedPrompt: true,
customColumns: [
{ display: 'battles', showDelta: false },
{ display: 'winrate', showDelta: true },
{ display: 'wn8', showDelta: false },
{ display: 'damage', showDelta: true },
],
color: 'slate',
columns: ['battles', 'winrate', 'wn8', 'averageDamage'],
tracking: false,
}),
{ name: 'session', merge: (a, b) => merge(b, a) },
{ name: 'session-2', merge: (a, b) => merge(b, a) },
),
);
+219 -119
View File
@@ -427,10 +427,10 @@
"@docsearch/css" "3.5.2"
algoliasearch "^4.19.1"
"@emnapi/runtime@^0.44.0":
version "0.44.0"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-0.44.0.tgz#1ef702f846cfcd559d28eb7673919087ba5b63e3"
integrity sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==
"@emnapi/runtime@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.1.1.tgz#697d02276ca6f49bafe6fd01c9df0034818afa98"
integrity sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==
dependencies:
tslib "^2.4.0"
@@ -618,118 +618,118 @@
dependencies:
property-graph "^1.3.1"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz#9d3cb0e4899b10b003608a018877b45b6db02861"
integrity sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz#2bbf676be830c5a9ae7d9294f201c9151535badd"
integrity sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.0.0"
"@img/sharp-libvips-darwin-arm64" "1.0.2"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz#3ad6b275dba0ed9995ce4403fc2c59b0475a0162"
integrity sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz#c59567b141eb676e884066f76091a2673120c3f5"
integrity sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==
optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.0.0"
"@img/sharp-libvips-darwin-x64" "1.0.2"
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz#8baf27b01dabba524e885c30287e1916ab978de7"
integrity sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz#b69f49fecbe9572378675769b189410721b0fa53"
integrity sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz#509113f649d3ea0e99b76d41437fc0049d8ba2f9"
integrity sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz#5665da7360d8e5ed7bee314491c8fe736b6a3c39"
integrity sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz#9e131cc95f9f8aa36da9addee89efead21ad9993"
integrity sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz#8a05e5e9e9b760ff46561e32f19bd5e035fa881c"
integrity sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz#a529f68a28ab1d219907071c41ace029121d1c4f"
integrity sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz#0fd33b9bf3221948ce0ca7a5a725942626577a03"
integrity sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz#c26aaa9ee58b47ff901bb5f93f29c4ab3f03caf7"
integrity sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz#4b89150ec91b256ee2cbb5bb125321bf029a4770"
integrity sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz#7a04795fbf2668b9dd7c74bf8326cb779131d809"
integrity sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz#947ccc22ca5bc8c8cfe921b39a5fdaebc5e39f3f"
integrity sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz#600e7322faa5ce124d3ae2cbf63808ea47678591"
integrity sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz#821d58ce774f0f8bed065b69913a62f65d512f2f"
integrity sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==
"@img/[email protected].0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz#af33a04e75f192c5396c6a41df8b7b7bf15a8006"
integrity sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==
"@img/[email protected].2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz#4309474bd8b728a61af0b3b4fad0c476b5f3ccbe"
integrity sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz#3f419eb6f9ace924c511903a4d9034fb847de06b"
integrity sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz#a1f788ddf49ed63509dd37d4b01e571fe7f189d5"
integrity sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.0.0"
"@img/sharp-libvips-linux-arm64" "1.0.2"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz#87e157716c55fa274dd652e71bf9a5b7c260f8ab"
integrity sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz#661b0671ed7f740fd06821ce15050ba23f1d0523"
integrity sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==
optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.0.0"
"@img/sharp-libvips-linux-arm" "1.0.2"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz#c554567bb211cb1e916562752e70ef65df0cdcb7"
integrity sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz#8719341d3931a297df1a956c02ee003736fa8fac"
integrity sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==
optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.0.0"
"@img/sharp-libvips-linux-s390x" "1.0.2"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz#c8623c743e2a68b460b63bd2e225b1b5a485d144"
integrity sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz#dbd860b4aa16e7e25727c7e05b411132b58d017d"
integrity sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.0.0"
"@img/sharp-libvips-linux-x64" "1.0.2"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz#1b17f9950246108cb22cf98f1822fbe4a5b0dc9e"
integrity sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz#25b3fbfe9b6fa32d773422d878d8d84f3f6afceb"
integrity sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.0.0"
"@img/sharp-libvips-linuxmusl-arm64" "1.0.2"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz#69e2ab197c6d6e7a09748e0f2d03244d2c2afed7"
integrity sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz#1e533e44abf2e2d427428ed49294ddba4eb11456"
integrity sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.0.0"
"@img/sharp-libvips-linuxmusl-x64" "1.0.2"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz#aa6f33a8535e6bd4a66c59aeb569499db9d30043"
integrity sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz#340006047a77df0744db84477768bbca6327b4b4"
integrity sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==
dependencies:
"@emnapi/runtime" "^0.44.0"
"@emnapi/runtime" "^1.1.0"
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz#8df522d79b3c08f59e65fbe773849daa353c4f12"
integrity sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz#0fdc49ab094ed0151ec8347afac7917aa5fc5145"
integrity sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==
"@img/[email protected].1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz#8b56e97dc9d987b070d7530a357161d0f057c5af"
integrity sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg==
"@img/[email protected].3":
version "0.33.3"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz#a94e1028f180666f97fd51e35c4ad092d7704ef0"
integrity sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
@@ -1156,6 +1156,59 @@
dependencies:
"@prisma/debug" "5.9.1"
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
"@protobufjs/base64@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
"@protobufjs/codegen@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
"@protobufjs/eventemitter@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
"@protobufjs/fetch@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
dependencies:
"@protobufjs/aspromise" "^1.1.1"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/float@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
"@protobufjs/inquire@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==
"@protobufjs/path@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
"@protobufjs/pool@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
"@protobufjs/utf8@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@radix-ui/colors@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-3.0.0.tgz#e8a591a303c44e503bd1212cacf40a09511165e0"
@@ -2103,6 +2156,13 @@
resolved "https://registry.yarnpkg.com/@total-typescript/ts-reset/-/ts-reset-0.5.1.tgz#93b0535d00faa588518bcfb0db30182e63e4f7af"
integrity sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==
"@types/adm-zip@^0.5.5":
version "0.5.5"
resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.5.tgz#4588042726aa5f351d7ea88232e4a952f60e7c1a"
integrity sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==
dependencies:
"@types/node" "*"
"@types/body-parser@*":
version "1.19.5"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
@@ -2227,6 +2287,13 @@
dependencies:
undici-types "~5.26.4"
"@types/node@>=13.7.0":
version "20.12.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384"
integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==
dependencies:
undici-types "~5.26.4"
"@types/offscreencanvas@^2019.6.4":
version "2019.7.3"
resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz#90267db13f64d6e9ccb5ae3eac92786a7c77a516"
@@ -2590,6 +2657,11 @@ acorn@^8.11.3:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
adm-zip@^0.5.12:
version "0.5.12"
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.12.tgz#87786328e91d54b37358d8a50f954c4cd73ba60b"
integrity sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==
agent-base@^7.0.2:
version "7.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317"
@@ -3231,10 +3303,10 @@ detect-gpu@^5.0.28:
dependencies:
webgl-constants "^1.1.1"
detect-libc@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d"
integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==
detect-libc@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
detect-node-es@^1.1.0:
version "1.1.0"
@@ -4224,6 +4296,11 @@ lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
long@^5.0.0:
version "5.2.3"
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -4754,6 +4831,11 @@ perfect-debounce@^1.0.0:
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
pickleparser@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/pickleparser/-/pickleparser-0.2.1.tgz#7a03f1e9204e91ec9b8efbd3ba2f1eb5955b994d"
integrity sha512-kMzY3uFYcR6OjOqr7nV2nkaXaBsUEOafu3zgPxeD6s/2ueMfVQH8lrymcDWBPGx0OkVxGMikxQit6jgByXjwBg==
[email protected], picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@@ -4889,6 +4971,24 @@ property-graph@^1.3.1:
resolved "https://registry.yarnpkg.com/property-graph/-/property-graph-1.3.1.tgz#25fb2c8040ba9b15f04cda90a443f3f844505293"
integrity sha512-gei3N/bHWJdCItJ4blnlGWd9iauEZI+JZYj/A0D177XSI01+QhiJGAVscYBhe3Yywow3A2QJzVtsO2P+UgrRRQ==
protobufjs@^7.2.6:
version "7.2.6"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.6.tgz#4a0ccd79eb292717aacf07530a07e0ed20278215"
integrity sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.4"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/node" ">=13.7.0"
long "^5.0.0"
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -5207,7 +5307,7 @@ semver@^6.1.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.5, semver@^7.4.0, semver@^7.5.3, semver@^7.5.4:
semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.5, semver@^7.4.0, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
@@ -5260,34 +5360,34 @@ [email protected]:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
[email protected].1:
version "0.33.1"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.1.tgz#81e8778b9f5e2b195666cf56b2e5a110c2399d70"
integrity sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==
sharp@^0.33.3:
version "0.33.3"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.3.tgz#3342fe0aa5ed45a363e6578fa575c7af366216c2"
integrity sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==
dependencies:
color "^4.2.3"
detect-libc "^2.0.2"
semver "^7.5.4"
detect-libc "^2.0.3"
semver "^7.6.0"
optionalDependencies:
"@img/sharp-darwin-arm64" "0.33.1"
"@img/sharp-darwin-x64" "0.33.1"
"@img/sharp-libvips-darwin-arm64" "1.0.0"
"@img/sharp-libvips-darwin-x64" "1.0.0"
"@img/sharp-libvips-linux-arm" "1.0.0"
"@img/sharp-libvips-linux-arm64" "1.0.0"
"@img/sharp-libvips-linux-s390x" "1.0.0"
"@img/sharp-libvips-linux-x64" "1.0.0"
"@img/sharp-libvips-linuxmusl-arm64" "1.0.0"
"@img/sharp-libvips-linuxmusl-x64" "1.0.0"
"@img/sharp-linux-arm" "0.33.1"
"@img/sharp-linux-arm64" "0.33.1"
"@img/sharp-linux-s390x" "0.33.1"
"@img/sharp-linux-x64" "0.33.1"
"@img/sharp-linuxmusl-arm64" "0.33.1"
"@img/sharp-linuxmusl-x64" "0.33.1"
"@img/sharp-wasm32" "0.33.1"
"@img/sharp-win32-ia32" "0.33.1"
"@img/sharp-win32-x64" "0.33.1"
"@img/sharp-darwin-arm64" "0.33.3"
"@img/sharp-darwin-x64" "0.33.3"
"@img/sharp-libvips-darwin-arm64" "1.0.2"
"@img/sharp-libvips-darwin-x64" "1.0.2"
"@img/sharp-libvips-linux-arm" "1.0.2"
"@img/sharp-libvips-linux-arm64" "1.0.2"
"@img/sharp-libvips-linux-s390x" "1.0.2"
"@img/sharp-libvips-linux-x64" "1.0.2"
"@img/sharp-libvips-linuxmusl-arm64" "1.0.2"
"@img/sharp-libvips-linuxmusl-x64" "1.0.2"
"@img/sharp-linux-arm" "0.33.3"
"@img/sharp-linux-arm64" "0.33.3"
"@img/sharp-linux-s390x" "0.33.3"
"@img/sharp-linux-x64" "0.33.3"
"@img/sharp-linuxmusl-arm64" "0.33.3"
"@img/sharp-linuxmusl-x64" "0.33.3"
"@img/sharp-wasm32" "0.33.3"
"@img/sharp-win32-ia32" "0.33.3"
"@img/sharp-win32-x64" "0.33.3"
shebang-command@^2.0.0:
version "2.0.0"