mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
fix: don't update armor highlighting on protagonist shell switch
This commit is contained in:
1 parent
d81eddba44
commit
da767213cb
3 files changed
+353
-344
No files matched your search
+2
-1
@@ -15,4 +15,5 @@ servers.csv
|
|||||||
.next/
|
.next/
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
temp/
|
temp/
|
||||||
/.yarn
|
/.yarn
|
||||||
|
/.VSCodeCounter
|
||||||
+8
-12
@@ -80,16 +80,6 @@ export function ArmorHighlighting() {
|
|||||||
turretContainer.current?.position.copy(turretPosition);
|
turretContainer.current?.position.copy(turretPosition);
|
||||||
turretContainer.current?.rotation.copy(turretRotation);
|
turretContainer.current?.rotation.copy(turretRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModelTransform(useTankopediaTemporary.getState().model.pose);
|
|
||||||
modelTransformEvent.on(handleModelTransform);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
modelTransformEvent.off(handleModelTransform);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = useTankopediaTemporary.subscribe(
|
const unsubscribe = useTankopediaTemporary.subscribe(
|
||||||
(state) => state.mode,
|
(state) => state.mode,
|
||||||
(mode) => {
|
(mode) => {
|
||||||
@@ -97,8 +87,14 @@ export function ArmorHighlighting() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return unsubscribe;
|
handleModelTransform(useTankopediaTemporary.getState().model.pose);
|
||||||
});
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modelTransformEvent.off(handleModelTransform);
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const armorGltf = useArmor(protagonist.tank.id);
|
const armorGltf = useArmor(protagonist.tank.id);
|
||||||
const armorNodes = Object.values(armorGltf.nodes);
|
const armorNodes = Object.values(armorGltf.nodes);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MeshProps, useFrame, useThree } from '@react-three/fiber';
|
import { MeshProps, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { useEffect, useRef } from 'react';
|
import { memo, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
IUniform,
|
IUniform,
|
||||||
Mesh,
|
Mesh,
|
||||||
@@ -39,364 +39,375 @@ export type ArmorMeshUserData = {
|
|||||||
thickness: number;
|
thickness: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ArmorMesh({
|
export const ArmorMesh = memo<ArmorMeshProps>(
|
||||||
thickness,
|
({ thickness, maxThickness, ...props }) => {
|
||||||
maxThickness,
|
const camera = useThree((state) => state.camera);
|
||||||
...props
|
const initialShell = useDuel.getState().antagonist!.shell;
|
||||||
}: ArmorMeshProps) {
|
const initialTankopedia = useTankopediaPersistent.getState();
|
||||||
const camera = useThree((state) => state.camera);
|
const material = useRef<ShaderMaterial>(null);
|
||||||
const initialShell = useDuel.getState().antagonist!.shell;
|
const explosionCapable = isExplosive(initialShell.type);
|
||||||
const initialTankopedia = useTankopediaPersistent.getState();
|
const ricochetCapable = canRicochet(initialShell.type);
|
||||||
const material = useRef<ShaderMaterial>(null);
|
const cameraNormal = new Vector3();
|
||||||
const explosionCapable = isExplosive(initialShell.type);
|
const shellNormal = new Vector3();
|
||||||
const ricochetCapable = canRicochet(initialShell.type);
|
const surfaceNormal = new Vector3();
|
||||||
const cameraNormal = new Vector3();
|
const mesh = useRef<Mesh>(null);
|
||||||
const shellNormal = new Vector3();
|
const scene = useThree((state) => state.scene);
|
||||||
const surfaceNormal = new Vector3();
|
const hasCalibratedShells = useAwait(hasEquipment(103));
|
||||||
const mesh = useRef<Mesh>(null);
|
const hasEnhancedArmor = useAwait(hasEquipment(110));
|
||||||
const scene = useThree((state) => state.scene);
|
const wireframe = initialTankopedia.model.visual.wireframe;
|
||||||
const hasCalibratedShells = useAwait(hasEquipment(103));
|
|
||||||
const hasEnhancedArmor = useAwait(hasEquipment(110));
|
|
||||||
const wireframe = initialTankopedia.model.visual.wireframe;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function updateQuickEquipments() {
|
async function updateQuickEquipments() {
|
||||||
const shell = useDuel.getState().antagonist!.shell;
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
const nearPenetration = resolveNearPenetration(shell.penetration);
|
const nearPenetration = resolveNearPenetration(shell.penetration);
|
||||||
const hasCalibratedShells = await hasEquipment(103);
|
const hasCalibratedShells = await hasEquipment(103);
|
||||||
const hasEnhancedArmor = await hasEquipment(110);
|
const hasEnhancedArmor = await hasEquipment(110);
|
||||||
|
|
||||||
if (material.current) {
|
if (material.current) {
|
||||||
material.current.uniforms.thickness.value =
|
material.current.uniforms.thickness.value =
|
||||||
thickness * (hasEnhancedArmor ? 1.04 : 1);
|
thickness * (hasEnhancedArmor ? 1.04 : 1);
|
||||||
material.current.uniforms.penetration.value =
|
material.current.uniforms.penetration.value =
|
||||||
nearPenetration *
|
nearPenetration *
|
||||||
(hasCalibratedShells ? (isExplosive(shell.type) ? 1.1 : 1.05) : 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateShellProperties() {
|
|
||||||
const shell = useDuel.getState().antagonist!.shell;
|
|
||||||
if (material.current) {
|
|
||||||
material.current.uniforms.canRicochet.value = canRicochet(shell.type);
|
|
||||||
material.current.uniforms.isExplosive.value = isExplosive(shell.type);
|
|
||||||
material.current.uniforms.canSplash.value = canSplash(shell.type);
|
|
||||||
material.current.uniforms.caliber.value = shell.caliber;
|
|
||||||
material.current.uniforms.ricochetAngle.value = degToRad(
|
|
||||||
shell.ricochet ?? 90,
|
|
||||||
);
|
|
||||||
material.current.uniforms.damage.value = shell.damage.armor;
|
|
||||||
material.current.uniforms.explosionRadius.value =
|
|
||||||
shell.explosionRadius ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQuickEquipments();
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribes = [
|
|
||||||
useTankopediaTemporary.subscribe(
|
|
||||||
(state) => state.equipmentMatrix,
|
|
||||||
updateQuickEquipments,
|
|
||||||
),
|
|
||||||
useDuel.subscribe(
|
|
||||||
(state) => state.antagonist!.shell,
|
|
||||||
updateShellProperties,
|
|
||||||
),
|
|
||||||
useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.model.visual.greenPenetration,
|
|
||||||
(greenPenetration) => {
|
|
||||||
if (material.current) {
|
|
||||||
material.current.uniforms.greenPenetration.value = greenPenetration;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.model.visual.wireframe,
|
|
||||||
(wireframe) => {
|
|
||||||
if (material.current) {
|
|
||||||
material.current.wireframe = wireframe;
|
|
||||||
material.current.uniforms.wireframe.value = wireframe;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useFrame(({ gl, camera }) => {
|
|
||||||
if (material.current) {
|
|
||||||
gl.getSize(material.current.uniforms.resolution.value);
|
|
||||||
|
|
||||||
(
|
|
||||||
material.current.uniforms.resolution as IUniform<Vector2>
|
|
||||||
).value.multiplyScalar(gl.getPixelRatio());
|
|
||||||
|
|
||||||
material.current.uniforms.externalModuleMask.value =
|
|
||||||
externalModuleMaskRenderTarget.texture;
|
|
||||||
material.current.uniforms.spacedArmorDepth.value =
|
|
||||||
spacedArmorDepthRenderTarget.depthTexture;
|
|
||||||
material.current.uniforms.spacedArmorMask.value =
|
|
||||||
spacedArmorDepthRenderTarget.texture;
|
|
||||||
material.current.uniforms.projectionMatrixInverse.value =
|
|
||||||
camera.projectionMatrixInverse;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<mesh {...props} renderOrder={0}>
|
|
||||||
<meshBasicMaterial colorWrite={false} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<mesh
|
|
||||||
{...props}
|
|
||||||
renderOrder={1}
|
|
||||||
userData={
|
|
||||||
{
|
|
||||||
type: 'coreArmor',
|
|
||||||
thickness,
|
|
||||||
} satisfies ArmorMeshUserData
|
|
||||||
}
|
|
||||||
ref={mesh}
|
|
||||||
onClick={async (event) => {
|
|
||||||
// if (useTankopediaTemporary.getState().mode !== 'armor') return;
|
|
||||||
|
|
||||||
shellNormal.copy(
|
|
||||||
cameraNormal.copy(camera.position).sub(event.point).normalize(),
|
|
||||||
);
|
|
||||||
surfaceNormal
|
|
||||||
.copy(event.normal!)
|
|
||||||
.applyQuaternion(event.object.getWorldQuaternion(new Quaternion()));
|
|
||||||
const angle = Math.acos(surfaceNormal.dot(shellNormal));
|
|
||||||
const shell = useDuel.getState().antagonist!.shell;
|
|
||||||
const hasCalibratedShells = await hasEquipment(103);
|
|
||||||
const resolvedPenetration =
|
|
||||||
resolveNearPenetration(shell.penetration) *
|
|
||||||
(hasCalibratedShells ? (isExplosive(shell.type) ? 1.1 : 1.05) : 1);
|
(hasCalibratedShells ? (isExplosive(shell.type) ? 1.1 : 1.05) : 1);
|
||||||
const indexedObjects = new Set<ArmorMeshUserData['type']>();
|
}
|
||||||
const intersections = event.intersections
|
}
|
||||||
.filter(({ object }) => Boolean(object.userData.type))
|
|
||||||
.map((event) => {
|
|
||||||
const typedEvent = event as typeof event & {
|
|
||||||
object: (typeof event)['object'] & {
|
|
||||||
userData: ArmorMeshUserData;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return typedEvent;
|
function updateShellProperties() {
|
||||||
})
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
.filter(({ object }) => {
|
|
||||||
if (indexedObjects.has(object.userData.type)) return false;
|
if (material.current) {
|
||||||
indexedObjects.add(object.userData.type);
|
material.current.uniforms.canRicochet.value = canRicochet(shell.type);
|
||||||
return true;
|
material.current.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||||
});
|
material.current.uniforms.canSplash.value = canSplash(shell.type);
|
||||||
const intersectionsTillFirstCoreArmor = intersections.slice(
|
material.current.uniforms.caliber.value = shell.caliber;
|
||||||
0,
|
material.current.uniforms.ricochetAngle.value = degToRad(
|
||||||
intersections.findIndex(
|
shell.ricochet ?? 90,
|
||||||
({ object }) => object.userData.type === 'coreArmor',
|
|
||||||
) + 1,
|
|
||||||
);
|
);
|
||||||
const coreArmorMesh = intersectionsTillFirstCoreArmor.at(-1)!.object;
|
material.current.uniforms.damage.value = shell.damage.armor;
|
||||||
const hasEnhancedArmor = await hasEquipment(110);
|
material.current.uniforms.explosionRadius.value =
|
||||||
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
shell.explosionRadius ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
|
updateQuickEquipments();
|
||||||
|
}
|
||||||
|
|
||||||
let hasRicochet = false;
|
const unsubscribes = [
|
||||||
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
|
useTankopediaTemporary.subscribe(
|
||||||
.map((event, index) => {
|
(state) => state.equipmentMatrix,
|
||||||
if (hasRicochet) return null;
|
updateQuickEquipments,
|
||||||
|
),
|
||||||
switch (event.object.userData.type) {
|
useDuel.subscribe(
|
||||||
case 'externalModule': {
|
(state) => state.antagonist!.shell,
|
||||||
return {
|
updateShellProperties,
|
||||||
nominal:
|
),
|
||||||
event.object.userData.thickness * thicknessCoefficient,
|
useTankopediaPersistent.subscribe(
|
||||||
angled:
|
(state) => state.model.visual.greenPenetration,
|
||||||
event.object.userData.thickness * thicknessCoefficient,
|
(greenPenetration) => {
|
||||||
type: 'external',
|
if (material.current) {
|
||||||
distance: event.distance,
|
material.current.uniforms.greenPenetration.value =
|
||||||
ricochet: false,
|
greenPenetration;
|
||||||
} satisfies ArmorPiercingLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'spacedArmor':
|
|
||||||
case 'coreArmor': {
|
|
||||||
const threeCalibersRule =
|
|
||||||
shell.caliber >
|
|
||||||
3 *
|
|
||||||
(event.object.userData.thickness * thicknessCoefficient);
|
|
||||||
|
|
||||||
if (
|
|
||||||
index === 0 &&
|
|
||||||
canRicochet(shell.type) &&
|
|
||||||
!threeCalibersRule &&
|
|
||||||
angle >= degToRad(shell.ricochet!)
|
|
||||||
) {
|
|
||||||
hasRicochet = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const twoCalibersRule =
|
|
||||||
shell.caliber >
|
|
||||||
2 *
|
|
||||||
(event.object.userData.thickness * thicknessCoefficient);
|
|
||||||
const normalization =
|
|
||||||
twoCalibersRule && event.object.userData.thickness > 0
|
|
||||||
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
|
|
||||||
(2 *
|
|
||||||
event.object.userData.thickness *
|
|
||||||
thicknessCoefficient)
|
|
||||||
: shell.normalization ?? 0;
|
|
||||||
const angled =
|
|
||||||
(event.object.userData.thickness * thicknessCoefficient) /
|
|
||||||
Math.cos(angle - degToRad(normalization));
|
|
||||||
|
|
||||||
return {
|
|
||||||
ricochet: hasRicochet,
|
|
||||||
nominal:
|
|
||||||
event.object.userData.thickness * thicknessCoefficient,
|
|
||||||
angled,
|
|
||||||
type:
|
|
||||||
event.object.userData.type === 'coreArmor'
|
|
||||||
? 'core'
|
|
||||||
: 'spaced',
|
|
||||||
distance: event.distance,
|
|
||||||
} satisfies ArmorPiercingLayer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => {
|
|
||||||
if (aIndex === armorPiercingLayersRaw.length - 1) return null;
|
|
||||||
const layerB = armorPiercingLayersRaw[aIndex + 1];
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'gap',
|
|
||||||
gap: Math.abs(layerB.distance - layerA.distance),
|
|
||||||
} satisfies ArmorPiercingLayer;
|
|
||||||
});
|
|
||||||
const armorPiercingLayers: ArmorPiercingLayer[] = [];
|
|
||||||
|
|
||||||
armorPiercingLayersRaw.forEach((layer, index) => {
|
|
||||||
const gap = gaps[index];
|
|
||||||
|
|
||||||
armorPiercingLayers.push(layer);
|
|
||||||
if (gap) armorPiercingLayers.push(gap);
|
|
||||||
});
|
|
||||||
|
|
||||||
let remainingPenetration = resolvedPenetration;
|
|
||||||
const explosiveCapable = isExplosive(shell.type);
|
|
||||||
|
|
||||||
armorPiercingLayers.forEach((layer) => {
|
|
||||||
if (layer.type !== 'gap') {
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
useTankopediaPersistent.subscribe(
|
||||||
|
(state) => state.model.visual.wireframe,
|
||||||
|
(wireframe) => {
|
||||||
|
if (material.current) {
|
||||||
|
material.current.wireframe = wireframe;
|
||||||
|
material.current.uniforms.wireframe.value = wireframe;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
remainingPenetration = Math.max(0, remainingPenetration);
|
return () => {
|
||||||
});
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const hasPenetrated = remainingPenetration > 0;
|
useFrame(({ gl, camera }) => {
|
||||||
let ricochet: Shot['ricochet'] = undefined;
|
if (material.current) {
|
||||||
|
gl.getSize(material.current.uniforms.resolution.value);
|
||||||
|
|
||||||
if (hasRicochet) {
|
(
|
||||||
const raycaster = new Raycaster();
|
material.current.uniforms.resolution as IUniform<Vector2>
|
||||||
const bounceNormal = shellNormal
|
).value.multiplyScalar(gl.getPixelRatio());
|
||||||
.clone()
|
|
||||||
.multiplyScalar(-1)
|
material.current.uniforms.externalModuleMask.value =
|
||||||
.sub(
|
externalModuleMaskRenderTarget.texture;
|
||||||
surfaceNormal
|
material.current.uniforms.spacedArmorDepth.value =
|
||||||
.clone()
|
spacedArmorDepthRenderTarget.depthTexture;
|
||||||
.multiplyScalar(
|
material.current.uniforms.spacedArmorMask.value =
|
||||||
2 *
|
spacedArmorDepthRenderTarget.texture;
|
||||||
surfaceNormal.dot(shellNormal.clone().multiplyScalar(-1)),
|
material.current.uniforms.projectionMatrixInverse.value =
|
||||||
),
|
camera.projectionMatrixInverse;
|
||||||
);
|
}
|
||||||
raycaster.set(event.point, bounceNormal);
|
});
|
||||||
const intersections = raycaster
|
|
||||||
.intersectObjects(scene.children, true)
|
return (
|
||||||
.filter(
|
<>
|
||||||
(intersection) =>
|
<mesh {...props} renderOrder={0}>
|
||||||
typeof intersection.object.userData?.type === 'string',
|
<meshBasicMaterial colorWrite={false} />
|
||||||
);
|
</mesh>
|
||||||
const indexOfCoreArmor = intersections.findIndex(
|
|
||||||
({ object }) => object.userData.type === 'coreArmor',
|
<mesh
|
||||||
|
{...props}
|
||||||
|
renderOrder={1}
|
||||||
|
userData={
|
||||||
|
{
|
||||||
|
type: 'coreArmor',
|
||||||
|
thickness,
|
||||||
|
} satisfies ArmorMeshUserData
|
||||||
|
}
|
||||||
|
ref={mesh}
|
||||||
|
onClick={async (event) => {
|
||||||
|
// if (useTankopediaTemporary.getState().mode !== 'armor') return;
|
||||||
|
|
||||||
|
shellNormal.copy(
|
||||||
|
cameraNormal.copy(camera.position).sub(event.point).normalize(),
|
||||||
);
|
);
|
||||||
|
surfaceNormal
|
||||||
|
.copy(event.normal!)
|
||||||
|
.applyQuaternion(
|
||||||
|
event.object.getWorldQuaternion(new Quaternion()),
|
||||||
|
);
|
||||||
|
const angle = Math.acos(surfaceNormal.dot(shellNormal));
|
||||||
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
|
const hasCalibratedShells = await hasEquipment(103);
|
||||||
|
const resolvedPenetration =
|
||||||
|
resolveNearPenetration(shell.penetration) *
|
||||||
|
(hasCalibratedShells
|
||||||
|
? isExplosive(shell.type)
|
||||||
|
? 1.1
|
||||||
|
: 1.05
|
||||||
|
: 1);
|
||||||
|
const indexedObjects = new Set<ArmorMeshUserData['type']>();
|
||||||
|
const intersections = event.intersections
|
||||||
|
.filter(({ object }) => Boolean(object.userData.type))
|
||||||
|
.map((event) => {
|
||||||
|
const typedEvent = event as typeof event & {
|
||||||
|
object: (typeof event)['object'] & {
|
||||||
|
userData: ArmorMeshUserData;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return typedEvent;
|
||||||
|
})
|
||||||
|
.filter(({ object }) => {
|
||||||
|
if (indexedObjects.has(object.userData.type)) return false;
|
||||||
|
indexedObjects.add(object.userData.type);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
const intersectionsTillFirstCoreArmor = intersections.slice(
|
const intersectionsTillFirstCoreArmor = intersections.slice(
|
||||||
0,
|
0,
|
||||||
indexOfCoreArmor + 1,
|
intersections.findIndex(
|
||||||
|
({ object }) => object.userData.type === 'coreArmor',
|
||||||
|
) + 1,
|
||||||
);
|
);
|
||||||
|
const coreArmorMesh =
|
||||||
|
intersectionsTillFirstCoreArmor.at(-1)!.object;
|
||||||
|
const hasEnhancedArmor = await hasEquipment(110);
|
||||||
|
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
||||||
|
|
||||||
if (intersectionsTillFirstCoreArmor.length > 0) {
|
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
|
||||||
intersectionsTillFirstCoreArmor.forEach((intersection) => {
|
|
||||||
switch (intersection.object.userData.type) {
|
let hasRicochet = false;
|
||||||
|
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
|
||||||
|
.map((event, index) => {
|
||||||
|
if (hasRicochet) return null;
|
||||||
|
|
||||||
|
switch (event.object.userData.type) {
|
||||||
case 'externalModule': {
|
case 'externalModule': {
|
||||||
remainingPenetration -=
|
return {
|
||||||
intersection.object.userData.thickness;
|
nominal:
|
||||||
break;
|
event.object.userData.thickness * thicknessCoefficient,
|
||||||
|
angled:
|
||||||
|
event.object.userData.thickness * thicknessCoefficient,
|
||||||
|
type: 'external',
|
||||||
|
distance: event.distance,
|
||||||
|
ricochet: false,
|
||||||
|
} satisfies ArmorPiercingLayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'spacedArmor':
|
case 'spacedArmor':
|
||||||
case 'coreArmor': {
|
case 'coreArmor': {
|
||||||
const angle = bounceNormal.angleTo(
|
const threeCalibersRule =
|
||||||
intersection.object.position.clone().sub(event.point),
|
shell.caliber >
|
||||||
);
|
3 *
|
||||||
|
(event.object.userData.thickness *
|
||||||
|
thicknessCoefficient);
|
||||||
|
|
||||||
|
if (
|
||||||
|
index === 0 &&
|
||||||
|
canRicochet(shell.type) &&
|
||||||
|
!threeCalibersRule &&
|
||||||
|
angle >= degToRad(shell.ricochet!)
|
||||||
|
) {
|
||||||
|
hasRicochet = true;
|
||||||
|
}
|
||||||
|
|
||||||
const twoCalibersRule =
|
const twoCalibersRule =
|
||||||
shell.caliber >
|
shell.caliber >
|
||||||
2 *
|
2 *
|
||||||
(intersection.object.userData.thickness *
|
(event.object.userData.thickness *
|
||||||
thicknessCoefficient);
|
thicknessCoefficient);
|
||||||
const normalization = twoCalibersRule
|
const normalization =
|
||||||
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
|
twoCalibersRule && event.object.userData.thickness > 0
|
||||||
(2 *
|
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
|
||||||
intersection.object.userData.thickness *
|
(2 *
|
||||||
thicknessCoefficient)
|
event.object.userData.thickness *
|
||||||
: shell.normalization ?? 0;
|
thicknessCoefficient)
|
||||||
|
: shell.normalization ?? 0;
|
||||||
const angled =
|
const angled =
|
||||||
(intersection.object.userData.thickness *
|
(event.object.userData.thickness * thicknessCoefficient) /
|
||||||
thicknessCoefficient) /
|
|
||||||
Math.cos(angle - degToRad(normalization));
|
Math.cos(angle - degToRad(normalization));
|
||||||
|
|
||||||
remainingPenetration -= angled;
|
return {
|
||||||
|
ricochet: hasRicochet,
|
||||||
|
nominal:
|
||||||
|
event.object.userData.thickness * thicknessCoefficient,
|
||||||
|
angled,
|
||||||
|
type:
|
||||||
|
event.object.userData.type === 'coreArmor'
|
||||||
|
? 'core'
|
||||||
|
: 'spaced',
|
||||||
|
distance: event.distance,
|
||||||
|
} satisfies ArmorPiercingLayer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
const gaps = armorPiercingLayersRaw.map((layerA, aIndex) => {
|
||||||
|
if (aIndex === armorPiercingLayersRaw.length - 1) return null;
|
||||||
|
const layerB = armorPiercingLayersRaw[aIndex + 1];
|
||||||
|
|
||||||
ricochet = {
|
return {
|
||||||
point: intersectionsTillFirstCoreArmor.at(-1)!.point.toArray(),
|
type: 'gap',
|
||||||
distance: intersectionsTillFirstCoreArmor.at(-1)!.distance,
|
gap: Math.abs(layerB.distance - layerA.distance),
|
||||||
penetration: remainingPenetration > 0,
|
} satisfies ArmorPiercingLayer;
|
||||||
};
|
});
|
||||||
|
const armorPiercingLayers: ArmorPiercingLayer[] = [];
|
||||||
|
|
||||||
|
armorPiercingLayersRaw.forEach((layer, index) => {
|
||||||
|
const gap = gaps[index];
|
||||||
|
|
||||||
|
armorPiercingLayers.push(layer);
|
||||||
|
if (gap) armorPiercingLayers.push(gap);
|
||||||
|
});
|
||||||
|
|
||||||
|
let remainingPenetration = resolvedPenetration;
|
||||||
|
const explosiveCapable = isExplosive(shell.type);
|
||||||
|
|
||||||
|
armorPiercingLayers.forEach((layer) => {
|
||||||
|
if (layer.type !== 'gap') {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingPenetration = Math.max(0, remainingPenetration);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = {
|
||||||
type: hasRicochet
|
type: hasRicochet
|
||||||
? 'ricochet'
|
? 'ricochet'
|
||||||
: hasPenetrated
|
: hasPenetrated
|
||||||
? 'penetration'
|
? 'penetration'
|
||||||
: 'block',
|
: 'block',
|
||||||
point: event.point.toArray(),
|
point: event.point.toArray(),
|
||||||
thicknesses: armorPiercingLayers,
|
thicknesses: armorPiercingLayers,
|
||||||
shellNormal: shellNormal.toArray(),
|
shellNormal: shellNormal.toArray(),
|
||||||
surfaceNormal: surfaceNormal.toArray(),
|
surfaceNormal: surfaceNormal.toArray(),
|
||||||
angle,
|
angle,
|
||||||
ricochet,
|
ricochet,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{true && (
|
|
||||||
<shaderMaterial
|
<shaderMaterial
|
||||||
wireframe={wireframe}
|
wireframe={wireframe}
|
||||||
ref={material}
|
ref={material}
|
||||||
@@ -437,8 +448,9 @@ export function ArmorMesh({
|
|||||||
wireframe: { value: wireframe },
|
wireframe: { value: wireframe },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</mesh>
|
||||||
</mesh>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
},
|
||||||
}
|
() => true,
|
||||||
|
);
|
||||||
Reference in new issue
Block a user