From 7af1fdcf6b4d5bc58e8999ca5c5cc108a50487dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 29 Feb 2024 13:17:41 -0600 Subject: [PATCH] feat: animation --- .../Armor/components/ShotDisplay.tsx | 345 +++++++++--------- src/core/math/normalToEuler.ts | 14 + 2 files changed, 185 insertions(+), 174 deletions(-) create mode 100644 src/core/math/normalToEuler.ts diff --git a/src/components/Armor/components/ShotDisplay.tsx b/src/components/Armor/components/ShotDisplay.tsx index cf2ae9ef5..3d6bc4611 100644 --- a/src/components/Armor/components/ShotDisplay.tsx +++ b/src/components/Armor/components/ShotDisplay.tsx @@ -1,10 +1,12 @@ import { Card, Flex, Inset, Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; -import { Euler, Quaternion, Vector3 } from 'three'; +import { useFrame } from '@react-three/fiber'; +import { useRef } from 'react'; +import { Mesh } from 'three'; import { radToDeg } from 'three/src/math/MathUtils'; -import { J_HAT } from '../../../constants/axis'; import { isExplosive } from '../../../core/blitz/isExplosive'; import { NaNFallback } from '../../../core/math/NaNFallback'; +import { normalToEuler } from '../../../core/math/normalToEuler'; import { AngledPenetration } from '../../../icons/AngledPenetration'; import { Block } from '../../../icons/Block'; import { NominalPenetration } from '../../../icons/NominalPenetration'; @@ -19,18 +21,6 @@ import { } from '../../../stores/tankopedia'; import { ArmorType } from './SpacedArmorScene'; -// export const SHOT_NAMES: Record = { -// ricochet: 'Ricochet', -// penetration: 'Penetration', -// block: 'Blocked', -// }; - -// export const SHOT_ICONS: Record = { -// ricochet: , -// block: , -// penetration: , -// }; - const layerTypeNames: Record = { [ArmorType.Core]: 'Core', [ArmorType.Spaced]: 'Spaced', @@ -38,30 +28,111 @@ const layerTypeNames: Record = { null: 'Gap', }; -// const THICKNESS = 0.05; -// const LENGTH = 4; - const LENGTH_INFINITY = 4; +const TRACER_THIN = 1 / 128; +const TRACER_THICK = 1 / 64; export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); + const preMidPointTracer = useRef(null); + const postMidPointTracer = useRef(null); const hasMultipleLayers = shot?.layers.some( (layer, index) => index !== 0 && layer.type !== null, ); const { shell } = useDuel.getState().antagonist!; const explosive = isExplosive(shell.type); + const midPointLayer = (shot?.layers.findLast( + (layer) => layer.type !== null && layer.status === 'ricochet', + ) ?? shot?.layers.findLast((layer) => layer.type !== null)) as ShotLayerBase; + const firstLayer = shot?.layers[0] as Exclude; + const lastLayer = shot?.layers.at(-1)!; + const distanceFromSpacedArmor = shot + ? firstLayer.point.distanceTo(shot.point) + : undefined; + const preMidPointLayers = shot?.layers.slice( + 0, + shot.layers.indexOf(midPointLayer as ShotLayer), + ); + const preMidPointGap = shot + ? preMidPointLayers!.reduce((accumulator, layer) => { + if (layer.type === null) return accumulator + layer.distance; + return accumulator; + }, 0) + LENGTH_INFINITY + : undefined; + + useFrame(({ clock }) => { + if (!shot || !preMidPointTracer.current) return; + + const t1 = clock.elapsedTime % 1; + const t2 = (clock.elapsedTime + 0.5) % 1; + + preMidPointTracer.current.position.set(0, (1 - t1) * preMidPointGap!, 0); + preMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t1 - 0.5), 1); + + if (!postMidPointTracer.current) return; + + postMidPointTracer.current.position.set(0, t2 * preMidPointGap!, 0); + postMidPointTracer.current.scale.set(1, 1 - 2 * Math.abs(t2 - 0.5), 1); + }); + + if (!shot) return null; return ( <> - {shot?.layers.map((layer, index) => { + + + + + + + + + + + + + {midPointLayer.status === 'ricochet' && ( + + + + + + + + + + + )} + + {shot.layers.map((layer, index) => { if (layer.type === null) return; - const shellRotation = new Euler().setFromQuaternion( - new Quaternion().setFromAxisAngle( - new Vector3().crossVectors(J_HAT, layer.shellNormal).normalize(), - J_HAT.angleTo(layer.shellNormal), - ), - ); + const shellRotation = normalToEuler(layer.shellNormal); const nextLayer = shot.layers[index + 1] as ShotLayerGap | undefined; const trueStatus = nextLayer?.status === 'blocked' ? 'blocked' : layer.status; @@ -107,171 +178,97 @@ export function ShotDisplay() { - - {(() => { - let length = LENGTH_INFINITY; - - if (layer.index !== 0) { - const gap = shot.layers[index - 1] as ShotLayerGap; - length = gap.distance; - } - - return ( - - - - - ); - })()} - - {(() => { - if ( - layer.status !== 'ricochet' || - index !== shot.layers.length - 1 - ) - return null; - const ricochetShellNormal = layer.shellNormal - .clone() - .reflect(layer.surfaceNormal) - .multiplyScalar(-1); - const ricochetShellRotation = new Euler().setFromQuaternion( - new Quaternion().setFromAxisAngle( - new Vector3() - .crossVectors(J_HAT, ricochetShellNormal) - .normalize(), - J_HAT.angleTo(ricochetShellNormal), - ), - ); - - return ( - - - - - - - ); - })()} ); })} - {shot && - (() => { - const firstLayer = shot.layers[0] as Exclude; - const lastLayer = shot.layers.at(-1)!; - const midPointLayer = (shot.layers.findLast( - (layer) => layer.type !== null && layer.status === 'ricochet', - ) ?? - shot.layers.findLast( - (layer) => layer.type !== null, - )) as ShotLayerBase; - const distanceFromSpacedArmor = firstLayer.point.distanceTo( - shot.point, - ); + + + + + {lastLayer.status[0].toUpperCase()} + {lastLayer.status.slice(1)} + {lastLayer.status === 'penetration' && + `: ${Math.round(shell.damage.armor)}hp`} + {lastLayer.status === 'blocked' && + shell.type === 'he' && + `: ${NaNFallback( + Math.max( + 0, + Math.round( + 0.5 * + shell.damage.armor * + (1 - + distanceFromSpacedArmor! / shell.explosionRadius!) - + 1.1 * + (midPointLayer as ShotLayerNonExternal) + .thicknessAngled, + ), + ), + 0, + )}hp`} + - console.log(distanceFromSpacedArmor); + {shot.layers.map((layer) => { + if (layer.type === null && !explosive) return null; - return ( - - - - - {lastLayer.status[0].toUpperCase()} - {lastLayer.status.slice(1)} - {lastLayer.status === 'penetration' && - `: ${Math.round(shell.damage.armor)}hp`} - {lastLayer.status === 'blocked' && - shell.type === 'he' && - `: ${NaNFallback( - Math.max( - 0, - Math.round( - 0.5 * - shell.damage.armor * - (1 - - distanceFromSpacedArmor / - shell.explosionRadius!) - - 1.1 * - (midPointLayer as ShotLayerNonExternal) - .thicknessAngled, - ), - ), - 0, - )}hp`} - + return ( + + + {hasMultipleLayers && ( + + {layer.type === null ? null : `${layer.index + 1}.`} + + )} + {layerTypeNames[`${layer.type}`]} + - {shot.layers.map((layer) => { - if (layer.type === null && !explosive) return null; + {layer.type !== null && ( + <> + + + {Math.round(layer.thickness)}mm + - return ( - - - {hasMultipleLayers && ( - - {layer.type === null - ? null - : `${layer.index + 1}.`} - + {layer.type !== ArmorType.External && ( + + {layer.status === 'ricochet' ? ( + + ) : layer.status === 'blocked' ? ( + + ) : ( + )} - {layerTypeNames[`${layer.type}`]} + {Math.round(layer.thicknessAngled)}mm ( + {Math.round(radToDeg(layer.angle))}°) + )} + + )} - {layer.type !== null && ( - <> - - - - {Math.round(layer.thickness)}mm - - - - {layer.type !== ArmorType.External && ( - - {layer.status === 'ricochet' ? ( - - ) : layer.status === 'blocked' ? ( - - ) : ( - - )} - - {Math.round(layer.thicknessAngled)}mm ( - {Math.round(radToDeg(layer.angle))}°) - - - )} - - )} - - {layer.type === null && ( - - {/* why is it x50? i can't pretend to understand this shit */} - {Math.round(layer.distance * 1000)}mm (- - {Math.round(Math.min(100, layer.distance * 50))}% - penetration) - - )} - - ); - })} + {layer.type === null && ( + + {Math.round(layer.distance * 1000)}mm (- + {Math.round(Math.min(100, layer.distance * 50))}% + penetration) + + )} - - - ); - })()} + ); + })} + + + ); } diff --git a/src/core/math/normalToEuler.ts b/src/core/math/normalToEuler.ts new file mode 100644 index 000000000..b3b005e96 --- /dev/null +++ b/src/core/math/normalToEuler.ts @@ -0,0 +1,14 @@ +import { Euler, Quaternion, Vector3 } from 'three'; +import { J_HAT } from '../../constants/axis'; + +const quaternion = new Quaternion(); +const axis = new Vector3(); + +export function normalToEuler(normal: Vector3, up = J_HAT) { + return new Euler().setFromQuaternion( + quaternion.setFromAxisAngle( + axis.crossVectors(up, normal).normalize(), + up.angleTo(normal), + ), + ); +}