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
temp/
/.yarn
/.VSCodeCounter
@@ -80,16 +80,6 @@ export function ArmorHighlighting() {
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
handleModelTransform(useTankopediaTemporary.getState().model.pose);
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
});
useEffect(() => {
const unsubscribe = useTankopediaTemporary.subscribe(
(state) => state.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 armorNodes = Object.values(armorGltf.nodes);
@@ -1,5 +1,5 @@
import { MeshProps, useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
import { memo, useEffect, useRef } from 'react';
import {
IUniform,
Mesh,
@@ -39,364 +39,375 @@ export type ArmorMeshUserData = {
thickness: number;
};
export function ArmorMesh({
thickness,
maxThickness,
...props
}: ArmorMeshProps) {
const camera = useThree((state) => state.camera);
const initialShell = useDuel.getState().antagonist!.shell;
const initialTankopedia = useTankopediaPersistent.getState();
const material = useRef<ShaderMaterial>(null);
const explosionCapable = isExplosive(initialShell.type);
const ricochetCapable = canRicochet(initialShell.type);
const cameraNormal = new Vector3();
const shellNormal = new Vector3();
const surfaceNormal = new Vector3();
const mesh = useRef<Mesh>(null);
const scene = useThree((state) => state.scene);
const hasCalibratedShells = useAwait(hasEquipment(103));
const hasEnhancedArmor = useAwait(hasEquipment(110));
const wireframe = initialTankopedia.model.visual.wireframe;
export const ArmorMesh = memo<ArmorMeshProps>(
({ thickness, maxThickness, ...props }) => {
const camera = useThree((state) => state.camera);
const initialShell = useDuel.getState().antagonist!.shell;
const initialTankopedia = useTankopediaPersistent.getState();
const material = useRef<ShaderMaterial>(null);
const explosionCapable = isExplosive(initialShell.type);
const ricochetCapable = canRicochet(initialShell.type);
const cameraNormal = new Vector3();
const shellNormal = new Vector3();
const surfaceNormal = new Vector3();
const mesh = useRef<Mesh>(null);
const scene = useThree((state) => state.scene);
const hasCalibratedShells = useAwait(hasEquipment(103));
const hasEnhancedArmor = useAwait(hasEquipment(110));
const wireframe = initialTankopedia.model.visual.wireframe;
useEffect(() => {
async function updateQuickEquipments() {
const shell = useDuel.getState().antagonist!.shell;
const nearPenetration = resolveNearPenetration(shell.penetration);
const hasCalibratedShells = await hasEquipment(103);
const hasEnhancedArmor = await hasEquipment(110);
useEffect(() => {
async function updateQuickEquipments() {
const shell = useDuel.getState().antagonist!.shell;
const nearPenetration = resolveNearPenetration(shell.penetration);
const hasCalibratedShells = await hasEquipment(103);
const hasEnhancedArmor = await hasEquipment(110);
if (material.current) {
material.current.uniforms.thickness.value =
thickness * (hasEnhancedArmor ? 1.04 : 1);
material.current.uniforms.penetration.value =
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) *
if (material.current) {
material.current.uniforms.thickness.value =
thickness * (hasEnhancedArmor ? 1.04 : 1);
material.current.uniforms.penetration.value =
nearPenetration *
(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(
0,
intersections.findIndex(
({ object }) => object.userData.type === 'coreArmor',
) + 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,
);
const coreArmorMesh = intersectionsTillFirstCoreArmor.at(-1)!.object;
const hasEnhancedArmor = await hasEquipment(110);
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
material.current.uniforms.damage.value = shell.damage.armor;
material.current.uniforms.explosionRadius.value =
shell.explosionRadius ?? 0;
}
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
updateQuickEquipments();
}
let hasRicochet = false;
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
.map((event, index) => {
if (hasRicochet) return null;
switch (event.object.userData.type) {
case 'externalModule': {
return {
nominal:
event.object.userData.thickness * thicknessCoefficient,
angled:
event.object.userData.thickness * thicknessCoefficient,
type: 'external',
distance: event.distance,
ricochet: false,
} 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;
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;
}
},
),
];
remainingPenetration = Math.max(0, remainingPenetration);
});
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
}, []);
const hasPenetrated = remainingPenetration > 0;
let ricochet: Shot['ricochet'] = undefined;
useFrame(({ gl, camera }) => {
if (material.current) {
gl.getSize(material.current.uniforms.resolution.value);
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',
(
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);
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(
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) {
intersectionsTillFirstCoreArmor.forEach((intersection) => {
switch (intersection.object.userData.type) {
if (mesh.current !== (coreArmorMesh as unknown as Mesh)) return;
let hasRicochet = false;
const armorPiercingLayersRaw = intersectionsTillFirstCoreArmor
.map((event, index) => {
if (hasRicochet) return null;
switch (event.object.userData.type) {
case 'externalModule': {
remainingPenetration -=
intersection.object.userData.thickness;
break;
return {
nominal:
event.object.userData.thickness * thicknessCoefficient,
angled:
event.object.userData.thickness * thicknessCoefficient,
type: 'external',
distance: event.distance,
ricochet: false,
} satisfies ArmorPiercingLayer;
}
case 'spacedArmor':
case 'coreArmor': {
const angle = bounceNormal.angleTo(
intersection.object.position.clone().sub(event.point),
);
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 *
(intersection.object.userData.thickness *
(event.object.userData.thickness *
thicknessCoefficient);
const normalization = twoCalibersRule
? ((shell.normalization ?? 0) * 1.4 * shell.caliber) /
(2 *
intersection.object.userData.thickness *
thicknessCoefficient)
: shell.normalization ?? 0;
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 =
(intersection.object.userData.thickness *
thicknessCoefficient) /
(event.object.userData.thickness * thicknessCoefficient) /
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 = {
point: intersectionsTillFirstCoreArmor.at(-1)!.point.toArray(),
distance: intersectionsTillFirstCoreArmor.at(-1)!.distance,
penetration: remainingPenetration > 0,
};
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;
}
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) => {
draft.shot = {
type: hasRicochet
? 'ricochet'
: hasPenetrated
? 'penetration'
: 'block',
point: event.point.toArray(),
thicknesses: armorPiercingLayers,
shellNormal: shellNormal.toArray(),
surfaceNormal: surfaceNormal.toArray(),
angle,
ricochet,
};
});
}}
>
{true && (
mutateTankopediaTemporary((draft) => {
draft.shot = {
type: hasRicochet
? 'ricochet'
: hasPenetrated
? 'penetration'
: 'block',
point: event.point.toArray(),
thicknesses: armorPiercingLayers,
shellNormal: shellNormal.toArray(),
surfaceNormal: surfaceNormal.toArray(),
angle,
ricochet,
};
});
}}
>
<shaderMaterial
wireframe={wireframe}
ref={material}
@@ -437,8 +448,9 @@ export function ArmorMesh({
wireframe: { value: wireframe },
}}
/>
)}
</mesh>
</>
);
}
</mesh>
</>
);
},
() => true,
);