mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 03:29:51 +02:00
Merge branch 'main' into dev-rinth
This commit is contained in:
commit
13a901c839
38 files changed
+876
-308
No files matched your search
+2
-2
@@ -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;
|
||||
|
||||
+22
-21
@@ -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`)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+21
-13
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user