feat: ricochet repenetration

This commit is contained in:
TrèsAbhi committed 2024-02-06 17:57:20 -06:00
1 parent 8bb5032ee4
commit 901f1bfe42
8 files changed
+163 -33

No files matched your search

@@ -4,6 +4,7 @@ import {
IUniform,
Mesh,
Quaternion,
Raycaster,
ShaderMaterial,
Vector2,
Vector3,
@@ -16,6 +17,7 @@ import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetr
import { useDuel } from '../../../../stores/duel';
import {
ArmorPiercingLayer,
Shot,
mutateTankopediaTemporary,
useTankopediaPersistent,
} from '../../../../stores/tankopedia';
@@ -49,6 +51,7 @@ export function ArmorMesh({
const shellNormal = new Vector3();
const surfaceNormal = new Vector3();
const mesh = useRef<Mesh>(null);
const scene = useThree((state) => state.scene);
useEffect(() => {
function updateQuickEquipments() {
@@ -189,8 +192,11 @@ export function ArmorMesh({
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor.map(
(event, index) => {
let hasRicochet = false;
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
.map((event, index) => {
if (hasRicochet) return null;
switch (event.object.userData.type) {
case 'externalModule': {
return {
@@ -198,9 +204,9 @@ export function ArmorMesh({
event.object.userData.thickness * thicknessCoefficient,
angled:
event.object.userData.thickness * thicknessCoefficient,
ricochet: false,
type: 'external',
distance: event.distance,
ricochet: false,
} satisfies ArmorPiercingLayer;
}
@@ -210,7 +216,6 @@ export function ArmorMesh({
shell.caliber >
3 *
(event.object.userData.thickness * thicknessCoefficient);
let ricochet = false;
if (
index === 0 &&
@@ -218,7 +223,7 @@ export function ArmorMesh({
!threeCalibersRule &&
angle >= degToRad(shell.ricochet!)
) {
ricochet = true;
hasRicochet = true;
}
const twoCalibersRule =
@@ -236,10 +241,10 @@ export function ArmorMesh({
Math.cos(angle - degToRad(normalization));
return {
ricochet: hasRicochet,
nominal:
event.object.userData.thickness * thicknessCoefficient,
angled,
ricochet,
type:
event.object.userData.type === 'coreArmor'
? 'core'
@@ -248,8 +253,8 @@ export function ArmorMesh({
} satisfies ArmorPiercingLayer;
}
}
},
);
})
.filter(Boolean);
const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => {
if (aIndex === armorPiercingLayersRaw.length - 1) return null;
const layerB = armorPiercingLayersRaw[aIndex + 1];
@@ -273,7 +278,11 @@ export function ArmorMesh({
armorPiercingLayers.forEach((layer) => {
if (layer.type !== 'gap') {
remainingPenetration -= layer.angled;
if (layer.ricochet) {
remainingPenetration *= 0.75;
} else {
remainingPenetration -= layer.angled;
}
} else if (explosiveCapable) {
// there is a 50% penetration loss per meter for HE based shells
remainingPenetration -= 0.5 * remainingPenetration * layer.gap;
@@ -282,10 +291,79 @@ export function ArmorMesh({
remainingPenetration = Math.max(0, remainingPenetration);
});
const hasRicochet = armorPiercingLayers.some(
(layer) => layer.type !== 'gap' && layer.ricochet,
);
const hasPenetrated = remainingPenetration > 0;
let ricochet: Shot['ricochet'] = undefined;
if (hasRicochet) {
const raycaster = new Raycaster();
const bounceNormal = shellNormal
.clone()
.multiplyScalar(-1)
.sub(
surfaceNormal
.clone()
.multiplyScalar(
2 *
surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
),
);
raycaster.set(event.point, bounceNormal);
const intersections = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersection) =>
typeof intersection.object.userData?.type === 'string',
);
const indexOfCoreArmor = intersections.findIndex(
({ object }) => object.userData.type === 'coreArmor',
);
const intersectionsTillFirstCoreArmor = intersections.slice(
0,
indexOfCoreArmor + 1,
);
if (intersectionsTillFirstCoreArmor.length > 0) {
intersectionsTillFirstCoreArmor.forEach((intersection) => {
switch (intersection.object.userData.type) {
case 'externalModule': {
remainingPenetration -=
intersection.object.userData.thickness;
break;
}
case 'spacedArmor':
case 'coreArmor': {
const angle = bounceNormal.angleTo(
intersection.object.position.clone().sub(event.point),
);
const twoCalibersRule =
shell.caliber >
2 *
(intersection.object.userData.thickness *
thicknessCoefficient);
const normalization = twoCalibersRule
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
(2 *
intersection.object.userData.thickness *
thicknessCoefficient)
: shell.normalization ?? 0;
const angled =
(intersection.object.userData.thickness *
thicknessCoefficient) /
Math.cos(angle - degToRad(normalization));
remainingPenetration -= angled;
}
}
});
ricochet = {
point: intersectionsTillFirstCoreArmor.at(-1)!.point.toArray(),
distance: intersectionsTillFirstCoreArmor.at(-1)!.distance,
penetration: remainingPenetration > 0,
};
}
}
mutateTankopediaTemporary((draft) => {
draft.shot = {
@@ -299,6 +377,7 @@ export function ArmorMesh({
shellNormal: shellNormal.toArray(),
surfaceNormal: surfaceNormal.toArray(),
angle,
ricochet,
};
});
}}
+55 -13
View File
@@ -4,10 +4,12 @@ import { useFrame } from '@react-three/fiber';
import { ReactNode, useEffect, useRef } from 'react';
import { Group, Mesh, Vector3 } from 'three';
import { radToDeg } from 'three/src/math/MathUtils';
import { isExplosive } from '../core/blitz/isExplosive';
import { AngledPenetration } from '../icons/AngledPenetration';
import { Block } from '../icons/Block';
import { NominalPenetration } from '../icons/NominalPenetration';
import { Ricochet } from '../icons/Ricochet';
import { useDuel } from '../stores/duel';
import { Shot, useTankopediaTemporary } from '../stores/tankopedia';
export const SHOT_NAMES: Record<Shot['type'], string> = {
@@ -30,8 +32,12 @@ export function ShotDisplay() {
const entryWrapper = useRef<Group>(null);
const entryLine = useRef<Mesh>(null);
const circle = useRef<Mesh>(null);
const ricochetCircle = useRef<Mesh>(null);
const exitWrapper = useRef<Group>(null);
const exitLine = useRef<Mesh>(null);
const secondLineLength = shot?.ricochet ? shot?.ricochet.distance : LENGTH;
const number = shot && (shot.thicknesses.length > 1 || shot.ricochet);
const shell = useDuel((state) => state.antagonist!.shell);
useFrame(({ clock }) => {
const t1 = clock.elapsedTime % 1;
@@ -39,7 +45,7 @@ export function ShotDisplay() {
entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH);
entryLine.current?.scale.set(1, -2 * Math.abs(t1 - 0.5) + 1, 1);
exitLine.current?.position.set(0, 0, t2 * LENGTH);
exitLine.current?.position.set(0, 0, t2 * secondLineLength);
exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1);
});
@@ -47,21 +53,21 @@ export function ShotDisplay() {
if (!shot) return;
const point = new Vector3(...shot.point);
const shotNormal = new Vector3(...shot.shellNormal);
const shellNormal = new Vector3(...shot.shellNormal);
const surfaceNormal = new Vector3(...shot.surfaceNormal);
const bounceNormal = shotNormal
const bounceNormal = shellNormal
.clone()
.multiplyScalar(-1)
.sub(
surfaceNormal
.clone()
.multiplyScalar(
2 * surfaceNormal.dot(shotNormal.clone().multiplyScalar(-1)),
2 * surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
),
);
entryWrapper.current?.position.set(...shot.point);
entryWrapper.current?.lookAt(point.clone().add(shotNormal));
entryWrapper.current?.lookAt(point.clone().add(shellNormal));
exitWrapper.current?.position.set(...shot.point);
exitWrapper.current?.lookAt(point.clone().add(bounceNormal));
circle.current?.position.set(...shot.point);
@@ -89,7 +95,7 @@ export function ShotDisplay() {
>
<Card
style={{
width: shot.thicknesses.length > 1 ? 300 : 200,
width: number ? 300 : 200,
userSelect: 'none',
}}
>
@@ -104,12 +110,12 @@ export function ShotDisplay() {
<Flex
direction="column"
style={{
paddingLeft: shot.thicknesses.length > 1 ? 8 : undefined,
paddingLeft: number ? 8 : undefined,
}}
>
{shot.thicknesses.map((layer, index) => (
<Flex gap="2">
{shot.thicknesses.length > 1 && (
<Flex gap="2" key={index}>
{number && (
<Text>
{index + 1}. {layer.type[0].toUpperCase()}
{layer.type.slice(1)}
@@ -127,6 +133,9 @@ export function ShotDisplay() {
: layer.nominal,
)}
mm
{layer.type === 'gap' && isExplosive(shell.type)
? ` (${Math.round(Math.min(100, 0.5 * layer.gap * 100))}% pen. loss)`
: ''}
</Text>
</Flex>
{layer.type !== 'external' && layer.type !== 'gap' && (
@@ -137,6 +146,26 @@ export function ShotDisplay() {
)}
</Flex>
))}
{shot.ricochet && (
<>
<Flex gap="1">
<Text>{shot.thicknesses.length + 1}. Ricochet</Text>
<Ricochet width={24} height={24} />
</Flex>
{shot.ricochet.penetration ? (
<Flex gap="1">
<Text>{shot.thicknesses.length + 2}. Penetration</Text>
<NominalPenetration width={24} height={24} />
</Flex>
) : (
<Flex gap="1">
<Text>{shot.thicknesses.length + 2}. Blocked</Text>
<Block width={24} height={24} />
</Flex>
)}
</>
)}
</Flex>
</Flex>
</Card>
@@ -161,21 +190,34 @@ export function ShotDisplay() {
<group ref={exitWrapper}>
<mesh
renderOrder={1}
position={[0, 0, LENGTH / 2]}
position={[0, 0, secondLineLength / 2]}
rotation={[Math.PI / 2, 0, 0]}
ref={exitLine}
>
<cylinderGeometry
args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]}
args={[THICKNESS / 2, THICKNESS / 2, secondLineLength, 8]}
/>
<meshBasicMaterial color="#ff8080" />
</mesh>
<mesh position={[0, 0, LENGTH / 2]} rotation={[Math.PI / 2, 0, 0]}>
<mesh
position={[0, 0, secondLineLength / 2]}
rotation={[Math.PI / 2, 0, 0]}
>
<cylinderGeometry
args={[THICKNESS / 4, THICKNESS / 4, LENGTH, 8]}
args={[THICKNESS / 4, THICKNESS / 4, secondLineLength, 8]}
/>
<meshBasicMaterial color="#ffffff" depthTest={false} />
</mesh>
{shot.ricochet && (
<mesh ref={ricochetCircle} position={[0, 0, secondLineLength]}>
<torusGeometry args={[0.05, 0.0125, 4, 16]} />
<meshBasicMaterial
color={shot.ricochet.penetration ? 'green' : 'red'}
depthTest={false}
/>
</mesh>
)}
</group>
)}
</>