From da767213cb1418caaae7b7b71086cb2c2cd9d7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:07:13 -0600 Subject: [PATCH] fix: don't update armor highlighting on protagonist shell switch --- .gitignore | 3 +- .../components/ArmorHighlighting.tsx | 20 +- .../ArmorMesh/components/Armor/index.tsx | 674 +++++++++--------- 3 files changed, 353 insertions(+), 344 deletions(-) diff --git a/.gitignore b/.gitignore index 21d412b5b..c1ae6834a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ servers.csv .next/ tsconfig.tsbuildinfo temp/ -/.yarn \ No newline at end of file +/.yarn +/.VSCodeCounter \ No newline at end of file diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx index 73408cdaf..4b490ede6 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx @@ -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); diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx index 2f51b9f78..28285eba3 100644 --- a/src/components/ArmorMesh/components/Armor/index.tsx +++ b/src/components/ArmorMesh/components/Armor/index.tsx @@ -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(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(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( + ({ thickness, maxThickness, ...props }) => { + const camera = useThree((state) => state.camera); + const initialShell = useDuel.getState().antagonist!.shell; + const initialTankopedia = useTankopediaPersistent.getState(); + const material = useRef(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(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 - ).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 ( - <> - - - - - { - // 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(); - 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 + ).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 ( + <> + + + + + { + // 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(); + 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, + }; + }); + }} + > - )} - - - ); -} + + + ); + }, + () => true, +);