Merge branch 'main' into dev-rinth
No files matched your search
@@ -62,4 +62,6 @@ jobs:
|
||||
$env:GH_TOKEN = "${{ secrets.GH_TOKEN }}"
|
||||
$env:DISCORD_TOKEN = "${{ secrets.DISCORD_TOKEN }}"
|
||||
$env:DATABASE_URL = "${{ secrets.DATABASE_URL }}"
|
||||
$env:VIDEOS_SHEET = "${{ secrets.VIDEOS_SHEET }}"
|
||||
$env:GOOGLE_APPLICATION_CREDENTIALS = "${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}"
|
||||
yarn build:assets --all-targets --depot ${{ github.ref == 'refs/heads/main' && '--production' || '' }}
|
||||
@@ -18,4 +18,5 @@ temp/
|
||||
/.VSCodeCounter
|
||||
/.yarn
|
||||
depots
|
||||
public/sitemap*
|
||||
public/sitemap*
|
||||
/firebase.json
|
||||
@@ -165,6 +165,7 @@
|
||||
"xkdkw",
|
||||
"XMLDVPL",
|
||||
"YAMLDVPL",
|
||||
"youtubers",
|
||||
"zustand"
|
||||
],
|
||||
"deno.enable": false,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Blitzkrieg 1.19.5
|
||||
|
||||
The tankopedia, now more helpful than ever! This update only affects the website.
|
||||
|
||||
## Features
|
||||
|
||||
- The Tankopedia pages for tanks now include videos from YouTubers
|
||||
- The selection of tanks that have videos is very small currently but you can help expand it by dropping suggestions on [the official Discord server](https://discord.gg/nDt7AjGJQH)
|
||||
- Added shell normalization and ricochet angles to the Tankopedia and Compare
|
||||
|
||||
## Fixes
|
||||
|
||||
- Removed undefined clipping planes
|
||||
- Added proper keying to all JSX tree nodes, navbar menu items, antagonist bar shells, skill rows and fragments of tank stats in the Tankopedia
|
||||
- The second half of barrels for tanks like the T-100 LT are now properly included
|
||||
|
||||
## Technical Features
|
||||
|
||||
- The list of videos is fetched transpiled from a Google Sheets spreadsheet into a CSV file in the asset repository
|
||||
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 148 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blitzrinth",
|
||||
"version": "1.19.4",
|
||||
"name": "blitzkrieg",
|
||||
"version": "1.19.5",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"main": "src/server.ts",
|
||||
"type": "module",
|
||||
@@ -52,6 +52,7 @@
|
||||
"express": "^4.18.2",
|
||||
"false": "^0.0.4",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"googleapis": "^134.0.0",
|
||||
"immer": "^10.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
google.com, pub-1289406790225052, DIRECT, f08c47fec0942fa0
|
||||
appnexus.com, 14163, RESELLER
|
||||
pubcenter.microsoft.com, 253617120, DIRECT
|
||||
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 921 KiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -38,7 +38,7 @@ import { DATA, POI } from './constants';
|
||||
import { Avatar } from './skillIcons';
|
||||
import { TankParameters } from './tankIcons';
|
||||
|
||||
interface Strings {
|
||||
export interface BlitzStrings {
|
||||
[key: string]: string;
|
||||
}
|
||||
type ShellKind =
|
||||
@@ -334,7 +334,7 @@ const blitzShellKindToBlitzrinth: Record<ShellKind, ShellType> = {
|
||||
const missingStrings: Record<string, string> = {
|
||||
'#artefacts:tungstentip/name': 'Tungsten Shells',
|
||||
};
|
||||
const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
|
||||
export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
|
||||
|
||||
export async function definitions(production: boolean) {
|
||||
console.log('Building definitions...');
|
||||
@@ -361,12 +361,12 @@ export async function definitions(production: boolean) {
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
const tankStringIdMap: Record<string, number> = {};
|
||||
const stringsPreInstalled = await readYAMLDVPL<Strings>(
|
||||
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
|
||||
`${DATA}/${POI.strings}/en.yaml.dvpl`,
|
||||
);
|
||||
const stringsCache = await fetch(POI.cachedStrings)
|
||||
.then((response) => response.text())
|
||||
.then((string) => parseYaml(string) as Strings);
|
||||
.then((string) => parseYaml(string) as BlitzStrings);
|
||||
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
|
||||
`${DATA}/${POI.optionalDevices}.dvpl`,
|
||||
);
|
||||
|
||||
@@ -24,15 +24,17 @@ import { skillIcons } from './skillIcons';
|
||||
import { tankArmors } from './tankArmors';
|
||||
import { tankIcons } from './tankIcons';
|
||||
import { tankModels } from './tankModels';
|
||||
import { videos } from './videos';
|
||||
|
||||
if (!targets && !allTargets) throw new Error('No target(s) specified');
|
||||
|
||||
const methods = [
|
||||
// definitions,
|
||||
// scratchedFlags,
|
||||
// currencies,
|
||||
// skillIcons,
|
||||
// circleFlags,
|
||||
definitions,
|
||||
videos,
|
||||
scratchedFlags,
|
||||
currencies,
|
||||
skillIcons,
|
||||
circleFlags,
|
||||
shellIcons,
|
||||
moduleIcons,
|
||||
equipmentIcons,
|
||||
|
||||
@@ -28,9 +28,6 @@ export async function tankArmors(production: boolean) {
|
||||
if (tankKey.includes('tutorial_bot')) return;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
console.log(`Building armor ${id} @ ${nation}/${tankKey}`);
|
||||
|
||||
const model = await extractArmor(DATA, `${nation}-${tankKey}`);
|
||||
|
||||
changes.push({
|
||||
|
||||
@@ -32,9 +32,6 @@ export async function tankModels(production: boolean) {
|
||||
if (tankKey.includes('tutorial_bot')) return;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
console.log(`Building model ${id} @ ${nation}/${tankKey}`);
|
||||
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/${POI.tankParameters}/${nation}/${tankKey}.yaml.dvpl`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { readdir } from 'fs/promises';
|
||||
import { google } from 'googleapis';
|
||||
import { cloneDeep, uniqBy } from 'lodash';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
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/blitzkrieg/commitAssets';
|
||||
import {
|
||||
VideoDefinitions,
|
||||
videoDefinitions,
|
||||
} from '../../src/core/blitzkrieg/videos';
|
||||
import { DATA, POI } from './constants';
|
||||
import { BlitzStrings, botPattern, VehicleDefinitionList } from './definitions';
|
||||
|
||||
const youtubers = [
|
||||
'UCKBYXp4Xn2I2tL1UL4fpbhw', // droodles
|
||||
'UCrQ-Dy8lVsm11u8pCJ8W7Tw', // fatness
|
||||
];
|
||||
|
||||
export async function videos(production: boolean) {
|
||||
console.log('Building videos...');
|
||||
|
||||
const currentVideos = await videoDefinitions;
|
||||
const auth = await google.auth.getClient({
|
||||
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
|
||||
});
|
||||
const youtube = google.youtube({ version: 'v3', auth });
|
||||
|
||||
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
|
||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
||||
);
|
||||
const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
|
||||
`${DATA}/${POI.strings}/en.yaml.dvpl`,
|
||||
);
|
||||
const stringsCache = await fetch(POI.cachedStrings)
|
||||
.then((response) => response.text())
|
||||
.then((string) => parseYaml(string) as BlitzStrings);
|
||||
const strings = {
|
||||
...stringsPreInstalled,
|
||||
...stringsCache,
|
||||
};
|
||||
const tanks: { id: number; name: string; tier: number }[] = [];
|
||||
await Promise.all(
|
||||
nations.map(async (nation) => {
|
||||
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(tankList.root).map(async ([tankKey, tank]) => {
|
||||
if (botPattern.test(tankKey)) return null;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
const name =
|
||||
(tank.shortUserString
|
||||
? strings[tank.shortUserString]
|
||||
: undefined) ?? strings[tank.userString];
|
||||
const tier = tank.level;
|
||||
|
||||
tanks.push({ id, name, tier });
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const tanksSanitized = tanks
|
||||
.sort((a, b) => b.tier - a.tier)
|
||||
.sort((a, b) => {
|
||||
const lastUpdatedA = currentVideos[a.id]?.lastUpdated ?? 0;
|
||||
const lastUpdatedB = currentVideos[b.id]?.lastUpdated ?? 0;
|
||||
return lastUpdatedA - lastUpdatedB;
|
||||
});
|
||||
const videos: VideoDefinitions = cloneDeep(currentVideos);
|
||||
|
||||
let done = 0;
|
||||
for (const tank of tanksSanitized) {
|
||||
try {
|
||||
const results = await youtube.search.list({
|
||||
part: ['snippet'],
|
||||
q: `World of Tanks Blitz ${tank.name}`,
|
||||
});
|
||||
|
||||
if (!results.data.items || results.data.items.length === 0) continue;
|
||||
|
||||
const items = uniqBy(
|
||||
results.data.items.filter(
|
||||
(item) =>
|
||||
item.snippet?.channelId &&
|
||||
item.id?.videoId &&
|
||||
youtubers.includes(item.snippet.channelId),
|
||||
),
|
||||
(item) => item.snippet!.channelId,
|
||||
);
|
||||
|
||||
videos[tank.id] = {
|
||||
lastUpdated: Math.round(Date.now() / 1000),
|
||||
videos: items.map((item) => item.id!.videoId!),
|
||||
};
|
||||
|
||||
console.log(
|
||||
`${++done} / ${tanks.length} (${Math.round(100 * (done / tanks.length))}%) done; found ${items.length} for ${tank.name}`,
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 / 5));
|
||||
} catch (error) {
|
||||
console.warn('An error occurred, safely exiting prematurely...', error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const content = Object.entries(videos)
|
||||
.map(
|
||||
([id, { videos, lastUpdated }]) =>
|
||||
`${id},${lastUpdated},${videos.join(',')}`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
await commitAssets(
|
||||
'videos',
|
||||
[{ content, encoding: 'utf-8', path: 'definitions/videos.csv' }],
|
||||
production,
|
||||
);
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import { Roboto_Flex } from 'next/font/google';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode, Suspense, useEffect, useState } from 'react';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { Google } from '../components/Google';
|
||||
import { Loader } from '../components/Loader';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Party3 } from '../components/Party3';
|
||||
import isDev from '../core/blitzrinth/isDev';
|
||||
import { isLocalhost } from '../core/blitzrinth/isLocalhost';
|
||||
import { useFullScreen } from '../hooks/useFullScreen';
|
||||
@@ -60,7 +60,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
<meta property="og:site_name" content="Blitzrinth" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
|
||||
<Google />
|
||||
<Party3 />
|
||||
</head>
|
||||
|
||||
<body
|
||||
|
||||
@@ -1233,10 +1233,21 @@ export default function Page() {
|
||||
<Row name="Shells" value="shells" decimals={0} />
|
||||
</>
|
||||
)}
|
||||
<Row name="Caliber" value="caliber" decimals={0} />
|
||||
<Row name="Penetration" value="penetration" decimals={0} />
|
||||
<Row name="Damage" value="damage" decimals={0} />
|
||||
<Row name="Module damage" value="moduleDamage" decimals={0} />
|
||||
<Row name="Caliber" value="caliber" decimals={0} />
|
||||
<Row
|
||||
name="Normalization"
|
||||
value="shellNormalization"
|
||||
decimals={0}
|
||||
/>
|
||||
<Row
|
||||
name="Ricochet"
|
||||
value="shellRicochet"
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={0}
|
||||
/>
|
||||
<Row name="Shell velocity" value="shellVelocity" decimals={0} />
|
||||
<Row
|
||||
name="Aim time"
|
||||
|
||||
@@ -191,6 +191,7 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
|
||||
<Flex>
|
||||
{antagonist.turret.guns.map((gun, index) => (
|
||||
<ModuleButton
|
||||
key={gun.id}
|
||||
rowChild
|
||||
first={index === 0}
|
||||
last={
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash';
|
||||
import { use, useEffect, useRef, useState } from 'react';
|
||||
import { Fragment, use, useEffect, useRef, useState } from 'react';
|
||||
import { lerp } from 'three/src/math/MathUtils';
|
||||
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
|
||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||
@@ -155,7 +155,6 @@ export function Characteristics() {
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Info name="Gun type">{GUN_TYPE_NAMES[gun.type]}</Info>
|
||||
<InfoWithDelta name="DPM" decimals={0} unit="hp / min">
|
||||
{stats.dpm}
|
||||
@@ -189,7 +188,6 @@ export function Characteristics() {
|
||||
{gun.type !== 'regular' && (
|
||||
<InfoWithDelta name="Shells">{stats.shells}</InfoWithDelta>
|
||||
)}
|
||||
|
||||
{gun.type === 'autoReloader' ? (
|
||||
<>
|
||||
<Info indent name="Most optimal">
|
||||
@@ -229,9 +227,6 @@ export function Characteristics() {
|
||||
{stats.intraClip!}
|
||||
</InfoWithDelta>
|
||||
)}
|
||||
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
||||
{stats.caliber}
|
||||
</InfoWithDelta>
|
||||
<InfoWithDelta decimals={0} name="Penetration" unit="mm">
|
||||
{stats.penetration}
|
||||
</InfoWithDelta>
|
||||
@@ -295,6 +290,22 @@ export function Characteristics() {
|
||||
{stats.explosionRadius!}
|
||||
</InfoWithDelta>
|
||||
)}
|
||||
<InfoWithDelta name="Caliber" decimals={0} unit="mm">
|
||||
{stats.caliber}
|
||||
</InfoWithDelta>
|
||||
<InfoWithDelta name="Normalization" decimals={0} unit="°">
|
||||
{stats.shellNormalization}
|
||||
</InfoWithDelta>
|
||||
{shell.type !== 'he' && (
|
||||
<InfoWithDelta
|
||||
name="Ricochet"
|
||||
decimals={0}
|
||||
deltaType="lowerIsBetter"
|
||||
unit="°"
|
||||
>
|
||||
{stats.shellRicochet!}
|
||||
</InfoWithDelta>
|
||||
)}
|
||||
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
|
||||
{stats.shellVelocity}
|
||||
</InfoWithDelta>
|
||||
@@ -516,7 +527,7 @@ export function Characteristics() {
|
||||
const count = member.count ?? 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Fragment key={member.type}>
|
||||
<InfoWithDelta
|
||||
key={`${member.type}-root`}
|
||||
name={`${CREW_MEMBER_NAMES[member.type]}${count > 1 ? ` x ${count}` : ''}`}
|
||||
@@ -545,7 +556,7 @@ export function Characteristics() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
@@ -124,11 +124,15 @@ export const TankModel = memo(() => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
onPointerDown,
|
||||
});
|
||||
return jsxTree(
|
||||
node,
|
||||
{
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
onPointerDown,
|
||||
},
|
||||
node.uuid,
|
||||
);
|
||||
})}
|
||||
|
||||
<group ref={turretContainer}>
|
||||
@@ -189,11 +193,15 @@ export const TankModel = memo(() => {
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
onPointerDown,
|
||||
});
|
||||
return jsxTree(
|
||||
node,
|
||||
{
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
onPointerDown,
|
||||
},
|
||||
node.uuid,
|
||||
);
|
||||
})}
|
||||
|
||||
<group ref={gunContainer}>
|
||||
@@ -255,11 +263,15 @@ export const TankModel = memo(() => {
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
onPointerDown,
|
||||
});
|
||||
return jsxTree(
|
||||
node,
|
||||
{
|
||||
castShadow: true,
|
||||
receiveShadow: true,
|
||||
onPointerDown,
|
||||
},
|
||||
node.uuid,
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { InfoCircledIcon, PlusIcon } from '@radix-ui/react-icons';
|
||||
import { Callout, Card, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { use } from 'react';
|
||||
import { videoDefinitions } from '../../../../../core/blitzkrieg/videos';
|
||||
import { useFullScreen } from '../../../../../hooks/useFullScreen';
|
||||
import { useDuel } from '../../../../../stores/duel';
|
||||
|
||||
export function Videos() {
|
||||
const isFullScreen = useFullScreen();
|
||||
const tank = useDuel((state) => state.protagonist!.tank);
|
||||
const awaitedVideoDefinitions = use(videoDefinitions);
|
||||
const videos = awaitedVideoDefinitions[tank.id]?.videos ?? [];
|
||||
|
||||
if (isFullScreen) return null;
|
||||
|
||||
return (
|
||||
<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
|
||||
key={video}
|
||||
href={`https://www.youtube.com/watch?v=${video}`}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src={`https://i.ytimg.com/vi/${video}/hqdefault.jpg`}
|
||||
style={{
|
||||
aspectRatio: '16 / 9',
|
||||
objectFit: 'cover',
|
||||
height: 128,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<Link href="https://discord.gg/nDt7AjGJQH" target="_blank">
|
||||
<Card
|
||||
style={{
|
||||
aspectRatio: '16 / 9',
|
||||
height: 128,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
gap="1"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Text color="purple">
|
||||
{videos.length === 0 ? 'Suggest' : 'Suggest more'} <PlusIcon />
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Link>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { Provisions } from './components/Characteristics/components/Provisions';
|
||||
import { Skills } from './components/Characteristics/components/Skills';
|
||||
import { TankSandbox } from './components/Model/TankSandbox';
|
||||
import { Title } from './components/Title';
|
||||
import { Videos } from './components/Videos';
|
||||
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const initialId = parseInt(params.id);
|
||||
@@ -83,7 +84,7 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
style={{
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
top: 64,
|
||||
top: 64 + 16,
|
||||
position: wideFormat ? 'sticky' : undefined,
|
||||
}}
|
||||
>
|
||||
@@ -91,7 +92,6 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
<TankSandbox />
|
||||
<AntagonistBar />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="5" wrap="wrap">
|
||||
<Modules />
|
||||
<Provisions />
|
||||
@@ -110,6 +110,8 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
<Characteristics />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Videos />
|
||||
</>
|
||||
)}
|
||||
</PageWrapper>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Flex, Link } from '@radix-ui/themes';
|
||||
import { use } from 'react';
|
||||
import { AdHorizontal } from '../../../../components/Ads/Horizontal';
|
||||
import PageWrapper from '../../../../components/PageWrapper';
|
||||
import { tankDefinitions } from '../../../../core/blitzrinth/tankDefinitions';
|
||||
|
||||
@@ -8,11 +11,17 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<title>Super duper secret map for SEO</title>
|
||||
|
||||
<AdHorizontal />
|
||||
|
||||
<Flex direction="column" align="center">
|
||||
{Object.values(awaitedTankDefinitions)
|
||||
.sort((a, b) => b.tier - a.tier)
|
||||
.map((tank) => (
|
||||
<Link href={`./${tank.id}`}>{tank.name}</Link>
|
||||
<Link href={`./${tank.id}`} key={tank.id}>
|
||||
{tank.name}
|
||||
</Link>
|
||||
))}
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export function AdHorizontal() {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1289406790225052"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<ins
|
||||
className="adsbygoogle"
|
||||
style={{ display: 'block' }}
|
||||
data-ad-client="ca-pub-1289406790225052"
|
||||
data-ad-slot="2391822409"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -153,14 +153,12 @@ export function CoreArmorSceneComponent({
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(node, {
|
||||
renderOrder: 0,
|
||||
material: excludeMaterial,
|
||||
})}
|
||||
{jsxTree(node, {
|
||||
renderOrder: 1,
|
||||
material,
|
||||
})}
|
||||
{jsxTree(
|
||||
node,
|
||||
{ renderOrder: 0, material: excludeMaterial },
|
||||
`${node.uuid}-exclude`,
|
||||
)}
|
||||
{jsxTree(node, { renderOrder: 1, material }, `${node.uuid}-include`)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -259,9 +259,9 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
||||
</group>
|
||||
|
||||
{modelNodes.map((node) => {
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
||||
const isCurrentGun = node.name.startsWith(
|
||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||
);
|
||||
const isVisible = isCurrentGun;
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
@@ -37,8 +37,8 @@ export function SpacedArmorSubExternal({
|
||||
depthTest: true,
|
||||
depthWrite: false,
|
||||
blending: AdditiveBlending,
|
||||
clippingPlanes: clip ? [clip] : undefined,
|
||||
clipping: true,
|
||||
clipping: clip !== undefined,
|
||||
...(clip ? { clippingPlanes: [clip] } : {}),
|
||||
|
||||
uniforms: {
|
||||
thickness: { value: null },
|
||||
@@ -107,26 +107,27 @@ export function SpacedArmorSubExternal({
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(node, {
|
||||
renderOrder: 3,
|
||||
material: new MeshBasicMaterial({
|
||||
colorWrite: false,
|
||||
depthTest: true,
|
||||
depthWrite: true,
|
||||
clippingPlanes: clip ? [clip] : undefined,
|
||||
}),
|
||||
onClick() {},
|
||||
userData: {
|
||||
type: ArmorType.External,
|
||||
thickness,
|
||||
variant,
|
||||
} satisfies ArmorUserData,
|
||||
})}
|
||||
{jsxTree(
|
||||
node,
|
||||
{
|
||||
renderOrder: 3,
|
||||
material: new MeshBasicMaterial({
|
||||
colorWrite: false,
|
||||
depthTest: true,
|
||||
depthWrite: true,
|
||||
...(clip ? { clippingPlanes: [clip] } : {}),
|
||||
}),
|
||||
onClick() {},
|
||||
userData: {
|
||||
type: ArmorType.External,
|
||||
thickness,
|
||||
variant,
|
||||
} satisfies ArmorUserData,
|
||||
},
|
||||
`${node.uuid}-exclude`,
|
||||
)}
|
||||
|
||||
{jsxTree(node, {
|
||||
renderOrder: 4,
|
||||
material,
|
||||
})}
|
||||
{jsxTree(node, { renderOrder: 4, material }, `${node.uuid}-include`)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -113,20 +113,28 @@ export function SpacedArmorSubSpaced({
|
||||
|
||||
return (
|
||||
<>
|
||||
{jsxTree(node, {
|
||||
renderOrder: 2,
|
||||
material,
|
||||
onClick() {},
|
||||
userData: {
|
||||
type: ArmorType.Spaced,
|
||||
thickness,
|
||||
} satisfies ArmorUserData,
|
||||
})}
|
||||
{jsxTree(
|
||||
node,
|
||||
{
|
||||
renderOrder: 2,
|
||||
material,
|
||||
onClick() {},
|
||||
userData: {
|
||||
type: ArmorType.Spaced,
|
||||
thickness,
|
||||
} satisfies ArmorUserData,
|
||||
},
|
||||
`${node.uuid}-exclude`,
|
||||
)}
|
||||
|
||||
{jsxTree(node, {
|
||||
renderOrder: 5,
|
||||
material: depthWriteMaterial,
|
||||
})}
|
||||
{jsxTree(
|
||||
node,
|
||||
{
|
||||
renderOrder: 5,
|
||||
material: depthWriteMaterial,
|
||||
},
|
||||
`${node.uuid}-include`,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -81,251 +81,265 @@ export function SpacedArmorSceneComponent({
|
||||
return (
|
||||
<>
|
||||
{props.type === ArmorType.Core &&
|
||||
jsxTree(node, {
|
||||
renderOrder: 0,
|
||||
material: omitMaterial,
|
||||
userData: {
|
||||
type: ArmorType.Core,
|
||||
thickness,
|
||||
} satisfies ArmorUserData,
|
||||
jsxTree(
|
||||
node,
|
||||
{
|
||||
renderOrder: 0,
|
||||
material: omitMaterial,
|
||||
userData: {
|
||||
type: ArmorType.Core,
|
||||
thickness,
|
||||
} satisfies ArmorUserData,
|
||||
|
||||
async onClick(event) {
|
||||
event.stopPropagation();
|
||||
async onClick(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const duel = useDuel.getState();
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
103,
|
||||
duel.antagonist!.tank.equipment,
|
||||
duel.antagonist!.equipmentMatrix,
|
||||
);
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
110,
|
||||
duel.protagonist!.tank.equipment,
|
||||
duel.protagonist!.equipmentMatrix,
|
||||
);
|
||||
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
||||
const shell = useDuel.getState().antagonist!.shell;
|
||||
const explosive = isExplosive(shell.type);
|
||||
const splashing = canSplash(shell.type);
|
||||
const penetration =
|
||||
resolveNearPenetration(shell.penetration) *
|
||||
(hasCalibratedShells
|
||||
? isExplosive(shell.type)
|
||||
? 1.1
|
||||
: 1.05
|
||||
: 1);
|
||||
const shot: Shot = {
|
||||
layers: [],
|
||||
point: event.point,
|
||||
}; // TODO: type this
|
||||
let remainingPenetration = penetration;
|
||||
|
||||
const shellNormal = camera.position
|
||||
.clone()
|
||||
.sub(event.point)
|
||||
.normalize();
|
||||
|
||||
pushLogs(shellNormal, filterIntersections(event.intersections), 0);
|
||||
|
||||
function filterIntersections(intersectionsRaw: Intersection[]) {
|
||||
const showSpacedArmor =
|
||||
useTankopediaPersistent.getState().model.visual.showSpacedArmor;
|
||||
const intersectionsTyped = intersectionsRaw.filter(
|
||||
({ object }) => object.userData.type !== undefined,
|
||||
) as ArmorMeshIntersection[];
|
||||
const intersectionsFiltered = intersectionsTyped.filter(
|
||||
({ object }) =>
|
||||
showSpacedArmor
|
||||
? true
|
||||
: object.userData.type === ArmorType.Core,
|
||||
const duel = useDuel.getState();
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
103,
|
||||
duel.antagonist!.tank.equipment,
|
||||
duel.antagonist!.equipmentMatrix,
|
||||
);
|
||||
const intersectionsWithDuplicates = intersectionsFiltered.slice(
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
110,
|
||||
duel.protagonist!.tank.equipment,
|
||||
duel.protagonist!.equipmentMatrix,
|
||||
);
|
||||
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
||||
const shell = useDuel.getState().antagonist!.shell;
|
||||
const explosive = isExplosive(shell.type);
|
||||
const splashing = canSplash(shell.type);
|
||||
const penetration =
|
||||
resolveNearPenetration(shell.penetration) *
|
||||
(hasCalibratedShells
|
||||
? isExplosive(shell.type)
|
||||
? 1.1
|
||||
: 1.05
|
||||
: 1);
|
||||
const shot: Shot = {
|
||||
layers: [],
|
||||
point: event.point,
|
||||
}; // TODO: type this
|
||||
let remainingPenetration = penetration;
|
||||
|
||||
const shellNormal = camera.position
|
||||
.clone()
|
||||
.sub(event.point)
|
||||
.normalize();
|
||||
|
||||
pushLogs(
|
||||
shellNormal,
|
||||
filterIntersections(event.intersections),
|
||||
0,
|
||||
intersectionsFiltered.findIndex(
|
||||
({ object }) => object.userData.type === ArmorType.Core,
|
||||
) + 1,
|
||||
);
|
||||
const firstGunIntersectionIndex =
|
||||
intersectionsWithDuplicates.findIndex(
|
||||
|
||||
function filterIntersections(intersectionsRaw: Intersection[]) {
|
||||
const showSpacedArmor =
|
||||
useTankopediaPersistent.getState().model.visual
|
||||
.showSpacedArmor;
|
||||
const intersectionsTyped = intersectionsRaw.filter(
|
||||
({ object }) => object.userData.type !== undefined,
|
||||
) as ArmorMeshIntersection[];
|
||||
const intersectionsFiltered = intersectionsTyped.filter(
|
||||
({ object }) =>
|
||||
object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'gun',
|
||||
showSpacedArmor
|
||||
? true
|
||||
: object.userData.type === ArmorType.Core,
|
||||
);
|
||||
const firstTrackIntersectionIndex =
|
||||
intersectionsWithDuplicates.findIndex(
|
||||
({ object }) =>
|
||||
object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'track',
|
||||
const intersectionsWithDuplicates = intersectionsFiltered.slice(
|
||||
0,
|
||||
intersectionsFiltered.findIndex(
|
||||
({ object }) => object.userData.type === ArmorType.Core,
|
||||
) + 1,
|
||||
);
|
||||
let intersections = intersectionsWithDuplicates;
|
||||
|
||||
if (firstGunIntersectionIndex !== -1) {
|
||||
intersections = intersections.filter(({ object }, index) => {
|
||||
return object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'gun'
|
||||
? index === firstGunIntersectionIndex
|
||||
: true;
|
||||
});
|
||||
}
|
||||
if (firstTrackIntersectionIndex !== -1) {
|
||||
intersections = intersections.filter(({ object }, index) => {
|
||||
return object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'track'
|
||||
? index === firstTrackIntersectionIndex
|
||||
: true;
|
||||
});
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
function pushLogs(
|
||||
shellNormal: Vector3,
|
||||
intersections: ArmorMeshIntersection[],
|
||||
startingIndex: number,
|
||||
) {
|
||||
let layerIndex = startingIndex;
|
||||
let loopIndex = 0;
|
||||
|
||||
if (layerIndex !== 0 && intersections.length > 0) {
|
||||
const distance = (
|
||||
shot.layers.at(-1) as ShotLayerBase
|
||||
).point.distanceTo(intersections[0].point);
|
||||
if (explosive && !splashing)
|
||||
remainingPenetration -= 0.5 * remainingPenetration * distance;
|
||||
const blocked = remainingPenetration < 0;
|
||||
|
||||
shot.layers.push({
|
||||
type: null,
|
||||
status: blocked ? 'blocked' : 'penetration',
|
||||
distance,
|
||||
});
|
||||
|
||||
if (blocked) return;
|
||||
}
|
||||
|
||||
for (const intersection of intersections) {
|
||||
const surfaceNormal = intersection
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
const firstGunIntersectionIndex =
|
||||
intersectionsWithDuplicates.findIndex(
|
||||
({ object }) =>
|
||||
object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'gun',
|
||||
);
|
||||
const thickness =
|
||||
thicknessCoefficient * intersection.object.userData.thickness;
|
||||
const firstTrackIntersectionIndex =
|
||||
intersectionsWithDuplicates.findIndex(
|
||||
({ object }) =>
|
||||
object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'track',
|
||||
);
|
||||
let intersections = intersectionsWithDuplicates;
|
||||
|
||||
if (layerIndex !== 0 && startingIndex === 0) {
|
||||
const previousIntersection = intersections[layerIndex - 1];
|
||||
const distance =
|
||||
intersection.distance - previousIntersection.distance;
|
||||
if (firstGunIntersectionIndex !== -1) {
|
||||
intersections = intersections.filter(({ object }, index) => {
|
||||
return object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'gun'
|
||||
? index === firstGunIntersectionIndex
|
||||
: true;
|
||||
});
|
||||
}
|
||||
if (firstTrackIntersectionIndex !== -1) {
|
||||
intersections = intersections.filter(({ object }, index) => {
|
||||
return object.userData.type === ArmorType.External &&
|
||||
object.userData.variant === 'track'
|
||||
? index === firstTrackIntersectionIndex
|
||||
: true;
|
||||
});
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
function pushLogs(
|
||||
shellNormal: Vector3,
|
||||
intersections: ArmorMeshIntersection[],
|
||||
startingIndex: number,
|
||||
) {
|
||||
let layerIndex = startingIndex;
|
||||
let loopIndex = 0;
|
||||
|
||||
if (layerIndex !== 0 && intersections.length > 0) {
|
||||
const distance = (
|
||||
shot.layers.at(-1) as ShotLayerBase
|
||||
).point.distanceTo(intersections[0].point);
|
||||
if (explosive && !splashing)
|
||||
remainingPenetration -=
|
||||
0.5 * remainingPenetration * distance;
|
||||
const wasted = remainingPenetration < 0;
|
||||
|
||||
shot.layers.push({
|
||||
type: null,
|
||||
status: wasted ? 'blocked' : 'penetration',
|
||||
distance,
|
||||
});
|
||||
|
||||
if (wasted && !splashing) return;
|
||||
}
|
||||
|
||||
if (intersection.object.userData.type === ArmorType.External) {
|
||||
remainingPenetration -= thickness;
|
||||
const blocked = remainingPenetration < 0;
|
||||
|
||||
shot.layers.push({
|
||||
type: ArmorType.External,
|
||||
index: layerIndex,
|
||||
shellNormal,
|
||||
surfaceNormal,
|
||||
point: intersection.point,
|
||||
thickness: intersection.object.userData.thickness,
|
||||
type: null,
|
||||
status: blocked ? 'blocked' : 'penetration',
|
||||
distance,
|
||||
});
|
||||
|
||||
if (splashing) {
|
||||
remainingPenetration = Math.max(0, remainingPenetration);
|
||||
} else if (blocked) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const type = intersection.object.userData.type;
|
||||
const angle = surfaceNormal.angleTo(shellNormal);
|
||||
const ricochet = degToRad(shell.ricochet ?? 90);
|
||||
const normalization = degToRad(shell.normalization ?? 0);
|
||||
const threeCalibersRule =
|
||||
shell.caliber > thickness * 3 || layerIndex > 0;
|
||||
const twoCalibersRule = shell.caliber > thickness * 2;
|
||||
const finalNormalization = twoCalibersRule
|
||||
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
|
||||
: normalization;
|
||||
const finalThickness =
|
||||
thickness === 0
|
||||
? 0
|
||||
: thickness /
|
||||
Math.cos(Math.max(0, angle - finalNormalization));
|
||||
if (blocked) return;
|
||||
}
|
||||
|
||||
for (const intersection of intersections) {
|
||||
const surfaceNormal = intersection
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
);
|
||||
const thickness =
|
||||
thicknessCoefficient *
|
||||
intersection.object.userData.thickness;
|
||||
|
||||
if (layerIndex !== 0 && startingIndex === 0) {
|
||||
const previousIntersection = intersections[layerIndex - 1];
|
||||
const distance =
|
||||
intersection.distance - previousIntersection.distance;
|
||||
if (explosive && !splashing)
|
||||
remainingPenetration -=
|
||||
0.5 * remainingPenetration * distance;
|
||||
const wasted = remainingPenetration < 0;
|
||||
|
||||
if (!threeCalibersRule && angle >= ricochet) {
|
||||
shot.layers.push({
|
||||
type,
|
||||
index: layerIndex,
|
||||
shellNormal,
|
||||
surfaceNormal,
|
||||
point: intersection.point,
|
||||
thickness,
|
||||
thicknessAngled: finalThickness,
|
||||
angle,
|
||||
status: 'ricochet',
|
||||
type: null,
|
||||
status: wasted ? 'blocked' : 'penetration',
|
||||
distance,
|
||||
});
|
||||
|
||||
remainingPenetration *= 0.75;
|
||||
if (wasted && !splashing) return;
|
||||
}
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
const newShellNormal = shellNormal
|
||||
.clone()
|
||||
.reflect(surfaceNormal);
|
||||
const newCameraNormal = newShellNormal
|
||||
.clone()
|
||||
.multiplyScalar(-1);
|
||||
|
||||
raycaster.set(intersection.point, newCameraNormal);
|
||||
pushLogs(
|
||||
newShellNormal,
|
||||
filterIntersections(
|
||||
raycaster.intersectObjects(scene.children, true),
|
||||
),
|
||||
layerIndex + 1,
|
||||
);
|
||||
|
||||
break;
|
||||
} else {
|
||||
remainingPenetration -= finalThickness;
|
||||
if (
|
||||
intersection.object.userData.type === ArmorType.External
|
||||
) {
|
||||
remainingPenetration -= thickness;
|
||||
const blocked = remainingPenetration < 0;
|
||||
|
||||
shot.layers.push({
|
||||
type,
|
||||
type: ArmorType.External,
|
||||
index: layerIndex,
|
||||
shellNormal,
|
||||
surfaceNormal,
|
||||
point: intersection.point,
|
||||
thickness,
|
||||
thicknessAngled: finalThickness,
|
||||
angle,
|
||||
thickness: intersection.object.userData.thickness,
|
||||
status: blocked ? 'blocked' : 'penetration',
|
||||
});
|
||||
|
||||
if (splashing) {
|
||||
remainingPenetration = Math.max(0, remainingPenetration);
|
||||
} else if (blocked) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const type = intersection.object.userData.type;
|
||||
const angle = surfaceNormal.angleTo(shellNormal);
|
||||
const ricochet = degToRad(shell.ricochet ?? 90);
|
||||
const normalization = degToRad(shell.normalization ?? 0);
|
||||
const threeCalibersRule =
|
||||
shell.caliber > thickness * 3 || layerIndex > 0;
|
||||
const twoCalibersRule = shell.caliber > thickness * 2;
|
||||
const finalNormalization = twoCalibersRule
|
||||
? (1.4 * normalization * shell.caliber) /
|
||||
(2.0 * thickness)
|
||||
: normalization;
|
||||
const finalThickness =
|
||||
thickness === 0
|
||||
? 0
|
||||
: thickness /
|
||||
Math.cos(Math.max(0, angle - finalNormalization));
|
||||
|
||||
if (!threeCalibersRule && angle >= ricochet) {
|
||||
shot.layers.push({
|
||||
type,
|
||||
index: layerIndex,
|
||||
shellNormal,
|
||||
surfaceNormal,
|
||||
point: intersection.point,
|
||||
thickness,
|
||||
thicknessAngled: finalThickness,
|
||||
angle,
|
||||
status: 'ricochet',
|
||||
});
|
||||
|
||||
remainingPenetration *= 0.75;
|
||||
|
||||
const raycaster = new Raycaster();
|
||||
const newShellNormal = shellNormal
|
||||
.clone()
|
||||
.reflect(surfaceNormal);
|
||||
const newCameraNormal = newShellNormal
|
||||
.clone()
|
||||
.multiplyScalar(-1);
|
||||
|
||||
raycaster.set(intersection.point, newCameraNormal);
|
||||
pushLogs(
|
||||
newShellNormal,
|
||||
filterIntersections(
|
||||
raycaster.intersectObjects(scene.children, true),
|
||||
),
|
||||
layerIndex + 1,
|
||||
);
|
||||
|
||||
break;
|
||||
} else {
|
||||
remainingPenetration -= finalThickness;
|
||||
const blocked = remainingPenetration < 0;
|
||||
|
||||
shot.layers.push({
|
||||
type,
|
||||
index: layerIndex,
|
||||
shellNormal,
|
||||
surfaceNormal,
|
||||
point: intersection.point,
|
||||
thickness,
|
||||
thicknessAngled: finalThickness,
|
||||
angle,
|
||||
status: blocked ? 'blocked' : 'penetration',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
layerIndex++;
|
||||
loopIndex++;
|
||||
}
|
||||
|
||||
layerIndex++;
|
||||
loopIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
useTankopediaTemporary.setState({ shot });
|
||||
useTankopediaTemporary.setState({ shot });
|
||||
},
|
||||
},
|
||||
})}
|
||||
node.uuid,
|
||||
)}
|
||||
|
||||
{props.type === ArmorType.Spaced && (
|
||||
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
||||
|
||||
@@ -28,6 +28,7 @@ export function CrewSkillManager({
|
||||
|
||||
return (
|
||||
<GenericTankComponentButton
|
||||
key={skill}
|
||||
special={!permanentSkills.includes(skill)}
|
||||
first
|
||||
last
|
||||
|
||||
@@ -131,6 +131,7 @@ export default function Navbar() {
|
||||
|
||||
return (
|
||||
<LinkRadix
|
||||
key={tool.id}
|
||||
tabIndex={-1}
|
||||
href={`/tools/${tool.id}`}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
export function Google() {
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function Party3() {
|
||||
useEffect(() => {
|
||||
(window as any).msAdsQueue = (window as any).msAdsQueue || [];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* analytics */}
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-98NTMRHRP9"
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-D8DT6EWVFJ"
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
@@ -13,7 +19,7 @@ export function Google() {
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-98NTMRHRP9');
|
||||
gtag('config', 'G-D8DT6EWVFJ');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
@@ -24,6 +30,13 @@ export function Google() {
|
||||
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1289406790225052"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
|
||||
{/* pub center */}
|
||||
{/* <script
|
||||
async
|
||||
src="https://adsdk.microsoft.com/pubcenter/sdk.js?siteId=364477&publisherId=253617120"
|
||||
crossOrigin="anonymous"
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -65,5 +65,6 @@ export default async function commitMultipleFiles(
|
||||
repo,
|
||||
ref: `heads/${branch}`,
|
||||
sha: newCommitData.sha,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
@@ -430,8 +430,12 @@ export function tankCharacteristics(
|
||||
const height = size[0];
|
||||
const length = size[1];
|
||||
const volume = width * height * length;
|
||||
const shellNormalization = shell.normalization ?? 0;
|
||||
const shellRicochet = shell.ricochet;
|
||||
|
||||
return {
|
||||
shellNormalization,
|
||||
shellRicochet,
|
||||
dpm,
|
||||
dpmMaximum,
|
||||
dpmEffective,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { asset } from './asset';
|
||||
|
||||
export interface VideoDefinitions {
|
||||
[id: number]: {
|
||||
videos: string[];
|
||||
lastUpdated: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const videoDefinitions = fetch(asset('definitions/videos.csv'))
|
||||
.then((response) => response.text())
|
||||
.then((text) =>
|
||||
text
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const [idString, lastUpdatedString, ...videos] = line.split(',');
|
||||
const id = Number(idString);
|
||||
const lastUpdated = Number(lastUpdatedString);
|
||||
return { id, videos, lastUpdated };
|
||||
})
|
||||
.filter(({ videos }) => videos.length > 0)
|
||||
.reduce<VideoDefinitions>((accumulator, { id, videos, lastUpdated }) => {
|
||||
accumulator[id] = {
|
||||
videos,
|
||||
lastUpdated,
|
||||
};
|
||||
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import { use, useMemo } from 'react';
|
||||
import {
|
||||
TankDefinition,
|
||||
tankDefinitions,
|
||||
} from '../core/blitzkrieg/tankDefinitions';
|
||||
import { useDuel } from '../stores/duel';
|
||||
|
||||
export enum UseEccentricityMode {
|
||||
Tier,
|
||||
Class,
|
||||
}
|
||||
|
||||
export function useEccentricity(mode: UseEccentricityMode) {
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const tank = useDuel((state) => state.protagonist!.tank);
|
||||
const similarTanks = useMemo(
|
||||
() =>
|
||||
Object.values(awaitedTankDefinitions).filter(
|
||||
(tankB) =>
|
||||
tank.tier === tankB.tier &&
|
||||
(mode === UseEccentricityMode.Class
|
||||
? tank.class === tankB.class
|
||||
: true),
|
||||
),
|
||||
[tank.id],
|
||||
);
|
||||
|
||||
function eccentricity(property: (tank: TankDefinition) => number) {
|
||||
const average = sumBy(similarTanks, property) / similarTanks.length;
|
||||
return property(tank) / average;
|
||||
}
|
||||
|
||||
return eccentricity;
|
||||
}
|
||||
@@ -2590,6 +2590,13 @@ acorn@^8.11.3:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
|
||||
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
||||
|
||||
agent-base@^7.0.2:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317"
|
||||
integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
|
||||
aggregate-error@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
|
||||
@@ -2691,7 +2698,7 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
|
||||
integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==
|
||||
|
||||
base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
@@ -2718,6 +2725,11 @@ big.js@^5.2.2:
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bignumber.js@^9.0.0:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c"
|
||||
integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==
|
||||
|
||||
bl@^1.0.0:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7"
|
||||
@@ -2807,6 +2819,11 @@ buffer-crc32@~0.2.3:
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
|
||||
|
||||
[email protected]:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
|
||||
|
||||
buffer-fill@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
||||
@@ -2852,7 +2869,7 @@ cac@^6.7.14:
|
||||
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
|
||||
integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
|
||||
|
||||
call-bind@^1.0.6:
|
||||
call-bind@^1.0.6, call-bind@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
|
||||
@@ -3101,7 +3118,7 @@ [email protected]:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^4.1.0, debug@^4.3.1, debug@^4.3.4:
|
||||
debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
@@ -3273,6 +3290,13 @@ [email protected]:
|
||||
optionalDependencies:
|
||||
wcwidth "^1.0.1"
|
||||
|
||||
[email protected], ecdsa-sig-formatter@^1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
|
||||
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
[email protected]:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
@@ -3450,6 +3474,11 @@ express@^4.18.2:
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
extend@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
||||
"false@^0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/false/-/false-0.0.4.tgz#8f55de5af5c03f50bf68aed57ed80e3d2176a517"
|
||||
@@ -3629,6 +3658,25 @@ fuzzysort@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-2.0.4.tgz#a21d1ce8947eaf2797dc3b7c28c36db9d1165f84"
|
||||
integrity sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw==
|
||||
|
||||
gaxios@^6.0.0, gaxios@^6.0.3, gaxios@^6.1.1:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.5.0.tgz#21bc20e24f21189ce8907079b56205ff9fd2c0d7"
|
||||
integrity sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w==
|
||||
dependencies:
|
||||
extend "^3.0.2"
|
||||
https-proxy-agent "^7.0.1"
|
||||
is-stream "^2.0.0"
|
||||
node-fetch "^2.6.9"
|
||||
uuid "^9.0.1"
|
||||
|
||||
gcp-metadata@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c"
|
||||
integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==
|
||||
dependencies:
|
||||
gaxios "^6.0.0"
|
||||
json-bigint "^1.0.0"
|
||||
|
||||
gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
@@ -3710,6 +3758,38 @@ glsl-noise@^0.0.0:
|
||||
resolved "https://registry.yarnpkg.com/glsl-noise/-/glsl-noise-0.0.0.tgz#367745f3a33382c0eeec4cb54b7e99cfc1d7670b"
|
||||
integrity sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==
|
||||
|
||||
google-auth-library@^9.0.0, google-auth-library@^9.7.0:
|
||||
version "9.8.0"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.8.0.tgz#43ae0f72dd72f49fd6a4a46d8eeffe2236691b20"
|
||||
integrity sha512-TJJXFzMlVGRlIH27gYZ6XXyPf5Y3OItsKFfefsDAafNNywYRTkei83nEO29IrYj8GtdHWU78YnW+YZdaZaXIJA==
|
||||
dependencies:
|
||||
base64-js "^1.3.0"
|
||||
ecdsa-sig-formatter "^1.0.11"
|
||||
gaxios "^6.1.1"
|
||||
gcp-metadata "^6.1.0"
|
||||
gtoken "^7.0.0"
|
||||
jws "^4.0.0"
|
||||
|
||||
googleapis-common@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-7.1.0.tgz#e97e468a7ef2f230df8255c0f66701cf7e28947c"
|
||||
integrity sha512-p3KHiWDBBWJEXk6SYauBEvxw5+UmRy7k2scxGtsNv9eHsTbpopJ3/7If4OrNnzJ9XMLg3IlyQXpVp8YPQsStiw==
|
||||
dependencies:
|
||||
extend "^3.0.2"
|
||||
gaxios "^6.0.3"
|
||||
google-auth-library "^9.7.0"
|
||||
qs "^6.7.0"
|
||||
url-template "^2.0.8"
|
||||
uuid "^9.0.0"
|
||||
|
||||
googleapis@^134.0.0:
|
||||
version "134.0.0"
|
||||
resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-134.0.0.tgz#1cde101a00916c677384b76bcd0eea57cf3f80bd"
|
||||
integrity sha512-o8LhD1754W6MHWtpwAPeP1WUHgNxuMxCnLMDFlMKAA5kCMTNqX9/eaTXnkkAIv6YRfoKMQ6D1vyR6/biXuhE9g==
|
||||
dependencies:
|
||||
google-auth-library "^9.0.0"
|
||||
googleapis-common "^7.0.0"
|
||||
|
||||
gopd@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
|
||||
@@ -3722,6 +3802,14 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
gtoken@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26"
|
||||
integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==
|
||||
dependencies:
|
||||
gaxios "^6.0.0"
|
||||
jws "^4.0.0"
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
@@ -3796,6 +3884,14 @@ [email protected]:
|
||||
statuses "2.0.1"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
https-proxy-agent@^7.0.1:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168"
|
||||
integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==
|
||||
dependencies:
|
||||
agent-base "^7.0.2"
|
||||
debug "4"
|
||||
|
||||
[email protected]:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
@@ -3917,6 +4013,11 @@ is-stream@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
|
||||
|
||||
is-stream@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
|
||||
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
@@ -3980,6 +4081,13 @@ jsesc@^2.5.1:
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
|
||||
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
|
||||
|
||||
json-bigint@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
|
||||
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
|
||||
dependencies:
|
||||
bignumber.js "^9.0.0"
|
||||
|
||||
json-parse-even-better-errors@^2.3.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
|
||||
@@ -4014,6 +4122,23 @@ jsonfile@^6.0.1:
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jwa@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
|
||||
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
|
||||
dependencies:
|
||||
buffer-equal-constant-time "1.0.1"
|
||||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jws@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
|
||||
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
|
||||
dependencies:
|
||||
jwa "^2.0.0"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
knip@^3.4.0:
|
||||
version "3.13.2"
|
||||
resolved "https://registry.yarnpkg.com/knip/-/knip-3.13.2.tgz#34f347a9b3ff1d45f3be7fed6bd3bb1ee5fbbaaa"
|
||||
@@ -4397,6 +4522,13 @@ [email protected]:
|
||||
data-uri-to-buffer "^3.0.1"
|
||||
fetch-blob "^2.1.1"
|
||||
|
||||
node-fetch@^2.6.9:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-releases@^2.0.14:
|
||||
version "2.0.14"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
|
||||
@@ -4772,6 +4904,13 @@ [email protected]:
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
qs@^6.7.0:
|
||||
version "6.12.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a"
|
||||
integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==
|
||||
dependencies:
|
||||
side-channel "^1.0.6"
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
@@ -5011,7 +5150,7 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
[email protected], safe-buffer@^5.1.1, safe-buffer@~5.2.0:
|
||||
[email protected], safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
@@ -5184,6 +5323,16 @@ side-channel@^1.0.4:
|
||||
get-intrinsic "^1.2.4"
|
||||
object-inspect "^1.13.1"
|
||||
|
||||
side-channel@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
|
||||
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
|
||||
dependencies:
|
||||
call-bind "^1.0.7"
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.4"
|
||||
object-inspect "^1.13.1"
|
||||
|
||||
signal-exit@^4.0.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
@@ -5480,6 +5629,11 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
troika-three-text@^0.49.0:
|
||||
version "0.49.0"
|
||||
resolved "https://registry.yarnpkg.com/troika-three-text/-/troika-three-text-0.49.0.tgz#bc2dc1924250c477cd39316cd83585dee12550e0"
|
||||
@@ -5615,6 +5769,11 @@ update-browserslist-db@^1.0.13:
|
||||
escalade "^3.1.1"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
url-template@^2.0.8:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
|
||||
integrity sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==
|
||||
|
||||
use-callback-ref@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0"
|
||||
@@ -5650,7 +5809,7 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@^9.0.1:
|
||||
uuid@^9.0.0, uuid@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
@@ -5787,6 +5946,19 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz#3e1b422b3d87cd3cc77f2602c9db63bc0f6accbd"
|
||||
integrity sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
|
||||