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

+1
View File
@@ -85,6 +85,7 @@
"Pyogenics", "Pyogenics",
"ratingsleaderboard", "ratingsleaderboard",
"ratingsplot", "ratingsplot",
"raycaster",
"researchable", "researchable",
"Resvg", "Resvg",
"roboto", "roboto",
@@ -36,6 +36,7 @@ export function Consumables() {
return ( return (
<ModuleButton <ModuleButton
key={consumable.id}
first={index === 0} first={index === 0}
last={index === consumablesList.length - 1} last={index === consumablesList.length - 1}
rowChild rowChild
@@ -35,6 +35,7 @@ export function Provisions() {
return ( return (
<ModuleButton <ModuleButton
key={provision.id}
first={index === 0} first={index === 0}
last={index === provisionsList.length - 1} last={index === provisionsList.length - 1}
rowChild rowChild
@@ -127,6 +127,7 @@ export function Characteristics() {
{gun.type === 'autoReloader' ? ( {gun.type === 'autoReloader' ? (
gun.reload.map((reload, index) => ( gun.reload.map((reload, index) => (
<Info <Info
key={index}
indent={index > 0} indent={index > 0}
name={index > 0 ? `Shell ${index + 1}` : 'Reload on shell 1'} name={index > 0 ? `Shell ${index + 1}` : 'Reload on shell 1'}
unit="s" unit="s"
@@ -144,25 +145,25 @@ export function Characteristics() {
</Info> </Info>
<Info name="Penetration" unit="mm" /> <Info name="Penetration" unit="mm" />
{gun.shells.map((shell) => ( {gun.shells.map((shell) => (
<Info indent name={SHELL_NAMES[shell.type]}> <Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
{resolveNearPenetration(shell.penetration)} {resolveNearPenetration(shell.penetration)}
</Info> </Info>
))} ))}
<Info name="Damage" unit="hp" /> <Info name="Damage" unit="hp" />
{gun.shells.map((shell) => ( {gun.shells.map((shell) => (
<Info indent name={SHELL_NAMES[shell.type]}> <Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
{shell.damage.armor} {shell.damage.armor}
</Info> </Info>
))} ))}
<Info name="Module damage" unit="hp" /> <Info name="Module damage" unit="hp" />
{gun.shells.map((shell, index) => ( {gun.shells.map((shell, index) => (
<Info indent name={SHELL_NAMES[shell.type]}> <Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
{shell.damage.module} {shell.damage.module}
</Info> </Info>
))} ))}
<Info name="Shell velocity" unit="m/s" /> <Info name="Shell velocity" unit="m/s" />
{gun.shells.map((shell, index) => ( {gun.shells.map((shell, index) => (
<Info indent name={SHELL_NAMES[shell.type]}> <Info key={shell.type} indent name={SHELL_NAMES[shell.type]}>
{shell.speed} {shell.speed}
</Info> </Info>
))} ))}
@@ -4,6 +4,7 @@ import {
IUniform, IUniform,
Mesh, Mesh,
Quaternion, Quaternion,
Raycaster,
ShaderMaterial, ShaderMaterial,
Vector2, Vector2,
Vector3, Vector3,
@@ -16,6 +17,7 @@ import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetr
import { useDuel } from '../../../../stores/duel'; import { useDuel } from '../../../../stores/duel';
import { import {
ArmorPiercingLayer, ArmorPiercingLayer,
Shot,
mutateTankopediaTemporary, mutateTankopediaTemporary,
useTankopediaPersistent, useTankopediaPersistent,
} from '../../../../stores/tankopedia'; } from '../../../../stores/tankopedia';
@@ -49,6 +51,7 @@ export function ArmorMesh({
const shellNormal = new Vector3(); const shellNormal = new Vector3();
const surfaceNormal = new Vector3(); const surfaceNormal = new Vector3();
const mesh = useRef<Mesh>(null); const mesh = useRef<Mesh>(null);
const scene = useThree((state) => state.scene);
useEffect(() => { useEffect(() => {
function updateQuickEquipments() { function updateQuickEquipments() {
@@ -189,8 +192,11 @@ export function ArmorMesh({
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return; if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor.map( let hasRicochet = false;
(event, index) => { const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
.map((event, index) => {
if (hasRicochet) return null;
switch (event.object.userData.type) { switch (event.object.userData.type) {
case 'externalModule': { case 'externalModule': {
return { return {
@@ -198,9 +204,9 @@ export function ArmorMesh({
event.object.userData.thickness * thicknessCoefficient, event.object.userData.thickness * thicknessCoefficient,
angled: angled:
event.object.userData.thickness * thicknessCoefficient, event.object.userData.thickness * thicknessCoefficient,
ricochet: false,
type: 'external', type: 'external',
distance: event.distance, distance: event.distance,
ricochet: false,
} satisfies ArmorPiercingLayer; } satisfies ArmorPiercingLayer;
} }
@@ -210,7 +216,6 @@ export function ArmorMesh({
shell.caliber > shell.caliber >
3 * 3 *
(event.object.userData.thickness * thicknessCoefficient); (event.object.userData.thickness * thicknessCoefficient);
let ricochet = false;
if ( if (
index === 0 && index === 0 &&
@@ -218,7 +223,7 @@ export function ArmorMesh({
!threeCalibersRule && !threeCalibersRule &&
angle >= degToRad(shell.ricochet!) angle >= degToRad(shell.ricochet!)
) { ) {
ricochet = true; hasRicochet = true;
} }
const twoCalibersRule = const twoCalibersRule =
@@ -236,10 +241,10 @@ export function ArmorMesh({
Math.cos(angle - degToRad(normalization)); Math.cos(angle - degToRad(normalization));
return { return {
ricochet: hasRicochet,
nominal: nominal:
event.object.userData.thickness * thicknessCoefficient, event.object.userData.thickness * thicknessCoefficient,
angled, angled,
ricochet,
type: type:
event.object.userData.type === 'coreArmor' event.object.userData.type === 'coreArmor'
? 'core' ? 'core'
@@ -248,8 +253,8 @@ export function ArmorMesh({
} satisfies ArmorPiercingLayer; } satisfies ArmorPiercingLayer;
} }
} }
}, })
); .filter(Boolean);
const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => { const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => {
if (aIndex === armorPiercingLayersRaw.length - 1) return null; if (aIndex === armorPiercingLayersRaw.length - 1) return null;
const layerB = armorPiercingLayersRaw[aIndex + 1]; const layerB = armorPiercingLayersRaw[aIndex + 1];
@@ -273,7 +278,11 @@ export function ArmorMesh({
armorPiercingLayers.forEach((layer) => { armorPiercingLayers.forEach((layer) => {
if (layer.type !== 'gap') { if (layer.type !== 'gap') {
remainingPenetration -= layer.angled; if (layer.ricochet) {
remainingPenetration *= 0.75;
} else {
remainingPenetration -= layer.angled;
}
} else if (explosiveCapable) { } else if (explosiveCapable) {
// there is a 50% penetration loss per meter for HE based shells // there is a 50% penetration loss per meter for HE based shells
remainingPenetration -= 0.5 * remainingPenetration * layer.gap; remainingPenetration -= 0.5 * remainingPenetration * layer.gap;
@@ -282,10 +291,79 @@ export function ArmorMesh({
remainingPenetration = Math.max(0, remainingPenetration); remainingPenetration = Math.max(0, remainingPenetration);
}); });
const hasRicochet = armorPiercingLayers.some(
(layer) => layer.type !== 'gap' && layer.ricochet,
);
const hasPenetrated = remainingPenetration > 0; 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) => { mutateTankopediaTemporary((draft) => {
draft.shot = { draft.shot = {
@@ -299,6 +377,7 @@ export function ArmorMesh({
shellNormal: shellNormal.toArray(), shellNormal: shellNormal.toArray(),
surfaceNormal: surfaceNormal.toArray(), surfaceNormal: surfaceNormal.toArray(),
angle, angle,
ricochet,
}; };
}); });
}} }}
+55 -13
View File
@@ -4,10 +4,12 @@ import { useFrame } from '@react-three/fiber';
import { ReactNode, useEffect, useRef } from 'react'; import { ReactNode, useEffect, useRef } from 'react';
import { Group, Mesh, Vector3 } from 'three'; import { Group, Mesh, Vector3 } from 'three';
import { radToDeg } from 'three/src/math/MathUtils'; import { radToDeg } from 'three/src/math/MathUtils';
import { isExplosive } from '../core/blitz/isExplosive';
import { AngledPenetration } from '../icons/AngledPenetration'; import { AngledPenetration } from '../icons/AngledPenetration';
import { Block } from '../icons/Block'; import { Block } from '../icons/Block';
import { NominalPenetration } from '../icons/NominalPenetration'; import { NominalPenetration } from '../icons/NominalPenetration';
import { Ricochet } from '../icons/Ricochet'; import { Ricochet } from '../icons/Ricochet';
import { useDuel } from '../stores/duel';
import { Shot, useTankopediaTemporary } from '../stores/tankopedia'; import { Shot, useTankopediaTemporary } from '../stores/tankopedia';
export const SHOT_NAMES: Record<Shot['type'], string> = { export const SHOT_NAMES: Record<Shot['type'], string> = {
@@ -30,8 +32,12 @@ export function ShotDisplay() {
const entryWrapper = useRef<Group>(null); const entryWrapper = useRef<Group>(null);
const entryLine = useRef<Mesh>(null); const entryLine = useRef<Mesh>(null);
const circle = useRef<Mesh>(null); const circle = useRef<Mesh>(null);
const ricochetCircle = useRef<Mesh>(null);
const exitWrapper = useRef<Group>(null); const exitWrapper = useRef<Group>(null);
const exitLine = useRef<Mesh>(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 }) => { useFrame(({ clock }) => {
const t1 = clock.elapsedTime % 1; const t1 = clock.elapsedTime % 1;
@@ -39,7 +45,7 @@ export function ShotDisplay() {
entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH); entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH);
entryLine.current?.scale.set(1, -2 * Math.abs(t1 - 0.5) + 1, 1); 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); exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1);
}); });
@@ -47,21 +53,21 @@ export function ShotDisplay() {
if (!shot) return; if (!shot) return;
const point = new Vector3(...shot.point); 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 surfaceNormal = new Vector3(...shot.surfaceNormal);
const bounceNormal = shotNormal const bounceNormal = shellNormal
.clone() .clone()
.multiplyScalar(-1) .multiplyScalar(-1)
.sub( .sub(
surfaceNormal surfaceNormal
.clone() .clone()
.multiplyScalar( .multiplyScalar(
2 * surfaceNormal.dot(shotNormal.clone().multiplyScalar(-1)), 2 * surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
), ),
); );
entryWrapper.current?.position.set(...shot.point); 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?.position.set(...shot.point);
exitWrapper.current?.lookAt(point.clone().add(bounceNormal)); exitWrapper.current?.lookAt(point.clone().add(bounceNormal));
circle.current?.position.set(...shot.point); circle.current?.position.set(...shot.point);
@@ -89,7 +95,7 @@ export function ShotDisplay() {
> >
<Card <Card
style={{ style={{
width: shot.thicknesses.length > 1 ? 300 : 200, width: number ? 300 : 200,
userSelect: 'none', userSelect: 'none',
}} }}
> >
@@ -104,12 +110,12 @@ export function ShotDisplay() {
<Flex <Flex
direction="column" direction="column"
style={{ style={{
paddingLeft: shot.thicknesses.length > 1 ? 8 : undefined, paddingLeft: number ? 8 : undefined,
}} }}
> >
{shot.thicknesses.map((layer, index) => ( {shot.thicknesses.map((layer, index) => (
<Flex gap="2"> <Flex gap="2" key={index}>
{shot.thicknesses.length > 1 && ( {number && (
<Text> <Text>
{index + 1}. {layer.type[0].toUpperCase()} {index + 1}. {layer.type[0].toUpperCase()}
{layer.type.slice(1)} {layer.type.slice(1)}
@@ -127,6 +133,9 @@ export function ShotDisplay() {
: layer.nominal, : layer.nominal,
)} )}
mm mm
{layer.type === 'gap' && isExplosive(shell.type)
? ` (${Math.round(Math.min(100, 0.5 * layer.gap * 100))}% pen. loss)`
: ''}
</Text> </Text>
</Flex> </Flex>
{layer.type !== 'external' && layer.type !== 'gap' && ( {layer.type !== 'external' && layer.type !== 'gap' && (
@@ -137,6 +146,26 @@ export function ShotDisplay() {
)} )}
</Flex> </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>
</Flex> </Flex>
</Card> </Card>
@@ -161,21 +190,34 @@ export function ShotDisplay() {
<group ref={exitWrapper}> <group ref={exitWrapper}>
<mesh <mesh
renderOrder={1} renderOrder={1}
position={[0, 0, LENGTH / 2]} position={[0, 0, secondLineLength / 2]}
rotation={[Math.PI / 2, 0, 0]} rotation={[Math.PI / 2, 0, 0]}
ref={exitLine} ref={exitLine}
> >
<cylinderGeometry <cylinderGeometry
args={[THICKNESS / 2, THICKNESS / 2, LENGTH, 8]} args={[THICKNESS / 2, THICKNESS / 2, secondLineLength, 8]}
/> />
<meshBasicMaterial color="#ff8080" /> <meshBasicMaterial color="#ff8080" />
</mesh> </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 <cylinderGeometry
args={[THICKNESS / 4, THICKNESS / 4, LENGTH, 8]} args={[THICKNESS / 4, THICKNESS / 4, secondLineLength, 8]}
/> />
<meshBasicMaterial color="#ffffff" depthTest={false} /> <meshBasicMaterial color="#ffffff" depthTest={false} />
</mesh> </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> </group>
)} )}
</> </>
+2 -2
View File
@@ -15,8 +15,8 @@ export function AngledPenetration(props: ComponentProps<'svg'>) {
fill="currentColor" fill="currentColor"
/> />
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M28 8H12L22 28H38L28 8ZM42 36H26L36 56H52L42 36Z" d="M28 8H12L22 28H38L28 8ZM42 36H26L36 56H52L42 36Z"
fill="currentColor" fill="currentColor"
/> />
+7 -2
View File
@@ -69,14 +69,19 @@ interface TankopediaTemporary {
shot?: Shot; shot?: Shot;
} }
export type Shot = { export interface Shot {
point: Vector3Tuple; point: Vector3Tuple;
surfaceNormal: Vector3Tuple; surfaceNormal: Vector3Tuple;
shellNormal: Vector3Tuple; shellNormal: Vector3Tuple;
thicknesses: ArmorPiercingLayer[]; thicknesses: ArmorPiercingLayer[];
type: 'ricochet' | 'penetration' | 'block'; type: 'ricochet' | 'penetration' | 'block';
angle: number; angle: number;
}; ricochet?: {
distance: number;
point: Vector3Tuple;
penetration: boolean;
};
}
export type ArmorPiercingLayer = export type ArmorPiercingLayer =
| { | {