import { Card, Flex, Text } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; import { ReactNode, useEffect, useRef } from 'react'; import { Group, Mesh, Vector3 } from 'three'; import { radToDeg } from 'three/src/math/MathUtils'; import { AngledPenetration } from '../icons/AngledPenetration'; import { Block } from '../icons/Block'; import { NominalPenetration } from '../icons/NominalPenetration'; import { Ricochet } from '../icons/Ricochet'; import { Shot, useTankopediaTemporary } from '../stores/tankopedia'; export const SHOT_NAMES: Record = { ricochet: 'Ricochet', penetration: 'Penetration', block: 'Blocked', }; export const SHOT_ICONS: Record = { ricochet: , block: , penetration: , }; const THICKNESS = 0.05; const LENGTH = 4; export function ShotDisplay() { const shot = useTankopediaTemporary((state) => state.shot); const entryWrapper = useRef(null); const entryLine = useRef(null); const circle = useRef(null); const exitWrapper = useRef(null); const exitLine = useRef(null); useFrame(({ clock }) => { const t1 = clock.elapsedTime % 1; const t2 = (clock.elapsedTime + 0.5) % 1; entryLine.current?.position.set(0, 0, LENGTH - t1 * LENGTH); entryLine.current?.scale.set(1, -2 * Math.abs(t1 - 0.5) + 1, 1); exitLine.current?.position.set(0, 0, t2 * LENGTH); exitLine.current?.scale.set(1, -2 * Math.abs(t2 - 0.5) + 1, 1); }); useEffect(() => { if (!shot) return; const point = new Vector3(...shot.point); const shotNormal = new Vector3(...shot.shellNormal); const surfaceNormal = new Vector3(...shot.surfaceNormal); const bounceNormal = shotNormal .clone() .multiplyScalar(-1) .sub( surfaceNormal .clone() .multiplyScalar( 2 * surfaceNormal.dot(shotNormal.clone().multiplyScalar(-1)), ), ); entryWrapper.current?.position.set(...shot.point); entryWrapper.current?.lookAt(point.clone().add(shotNormal)); exitWrapper.current?.position.set(...shot.point); exitWrapper.current?.lookAt(point.clone().add(bounceNormal)); circle.current?.position.set(...shot.point); circle.current?.lookAt(point.clone().add(surfaceNormal)); }, [shot]); if (!shot) return null; return ( <> 1 ? 300 : 200, userSelect: 'none', }} > {SHOT_ICONS[shot.type]} {SHOT_NAMES[shot.type]} at {Math.round(radToDeg(shot.angle))}° 1 ? 8 : undefined, }} > {shot.thicknesses.map((layer, index) => ( {shot.thicknesses.length > 1 && ( {index + 1}. {layer.type[0].toUpperCase()} {layer.type.slice(1)} )} {layer.type !== 'gap' && ( )} {Math.round( layer.type === 'gap' ? layer.gap * 1000 : layer.nominal, )} mm {layer.type !== 'external' && layer.type !== 'gap' && ( {Math.round(layer.angled)}mm )} ))} {shot.type === 'ricochet' && ( )} ); }