mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a1b786dae | ||
|
|
ea1eeb4366 | ||
|
|
15bec37a33 | ||
|
|
4bba794bfb | ||
|
|
82014845cb | ||
|
|
74a83d17e7 | ||
|
|
26ea06b091 | ||
|
|
352cb70830 | ||
|
|
ff92b3dd45 | ||
|
|
99ed269723 | ||
|
|
c24639dd0c | ||
|
|
cceebd1980 | ||
|
|
596018b3b2 | ||
|
|
8bc3e2c6b9 | ||
|
|
aafba7b6c8 | ||
|
|
d56be26db2 | ||
|
|
e740274e5a | ||
|
|
0634804914 | ||
|
|
4d908493f1 | ||
|
|
23d43f131b | ||
|
|
8a1b8673dc | ||
|
|
8682b2ea78 | ||
|
|
6e32b41b15 | ||
|
|
6223773c71 | ||
|
|
7fd4c931e5 | ||
|
|
1011c76cda | ||
|
|
38fa15ab97 | ||
|
|
d2b18360ed | ||
|
|
6ae86a7a70 | ||
|
|
6b175238b2 | ||
|
|
2a2a7a067c | ||
|
|
25788dedf4 | ||
|
|
391990676f | ||
|
|
8c16fdf9b5 | ||
|
|
c30358248f | ||
|
|
62b218a44d | ||
|
|
1f30d1d2c3 | ||
|
|
b1917c73f2 | ||
|
|
2d3325f13a | ||
|
|
983b96fd99 | ||
|
|
2c105d7a89 | ||
|
|
28348552f0 | ||
|
|
449f2b0677 | ||
|
|
5346ab576f |
No files matched your search
Vendored
+9
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
## Visit the website
|
||||
|
||||
### https://blitzkit.vercel.app/
|
||||
|
||||

|
||||

|
||||
|
||||
## Visit the documentation website
|
||||
|
||||
### https://tresabhi.github.io/blitzkit/
|
||||
|
||||

|
||||

|
||||
@@ -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
@@ -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 |
@@ -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`);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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'],
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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}` : ''}`,
|
||||
)}`;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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') {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { mutateApp } from '../../stores/app';
|
||||
|
||||
export function logout() {
|
||||
mutateApp((draft) => {
|
||||
draft.login = undefined;
|
||||
});
|
||||
}
|
||||
@@ -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]) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { config } from 'dotenv';
|
||||
|
||||
config();
|
||||
|
||||
const devHostStartNames = [
|
||||
'blitzkrieg-',
|
||||
'localhost',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface RenderConfiguration {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export class RenderConfiguration implements RenderConfiguration {
|
||||
width = 480;
|
||||
|
||||
constructor(configuration?: Partial<RenderConfiguration>) {
|
||||
Object.assign(this, configuration);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -208,6 +208,11 @@
|
||||
},
|
||||
|
||||
"commands": {
|
||||
"replay": {
|
||||
"$": "replay",
|
||||
"description": "Display replay results"
|
||||
},
|
||||
|
||||
"research": {
|
||||
"$": "research",
|
||||
"description": "Lists the expenses in researching a line",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message PlayerAvatarInfo {
|
||||
uint32 id = 1;
|
||||
string image = 2;
|
||||
string image_retina = 3;
|
||||
string grade = 4;
|
||||
}
|
||||
@@ -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
@@ -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) },
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user