fix: don't update armor highlighting on protagonist shell switch

This commit is contained in:
TrèsAbhi committed 2024-02-22 13:07:13 -06:00
1 parent d81eddba44
commit da767213cb
3 files changed
+352 -343

No files matched your search

+1
View File
@@ -16,3 +16,4 @@ servers.csv
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
temp/ temp/
/.yarn /.yarn
/.VSCodeCounter
@@ -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,
);