import { J_HAT, K_HAT } from "@blitzkit/core"; import { Box, Flex, Text } from "@radix-ui/themes"; import { Html } from "@react-three/drei"; import { useFrame } from "@react-three/fiber"; import { clamp } from "lodash-es"; import { useEffect, useRef, type ComponentProps, type RefObject } from "react"; import { BufferGeometry, DoubleSide, Euler, Group, Line, LineDashedMaterial, Mesh, MeshBasicMaterial, Path, Quaternion, } from "three"; import { isHalloween } from "../core/blitzkit/isHalloween"; import { LocaleProvider, useLocale } from "../hooks/useLocale"; import { Tankopedia, type ShotLayerNonExternal, type ShotStatus, } from "../stores/tankopedia"; import { ShotDisplayCard } from "./ShotDisplayCard"; export const shotStatusColors: Record< ShotStatus, ComponentProps["color"] > = { blocked: "red", penetration: "green", ricochet: "yellow", splash: "orange", }; const SURFACE_POINT_SIZE = 12; export const SHOT_DISPLAY_LENGTH_INFINITY = 4; const TRACER_THICK = 1 / 32; const TRACER_THIN = TRACER_THICK / 2; export function ShotDisplay() { const shot = Tankopedia.use((state) => state.shot); const inTracer = useRef(null); const outTracer = useRef(null); const splashRadiusWrapper = useRef(null); const splashRadiusMaterial = useRef(null); const { locale } = useLocale(); useEffect(() => { if (shot?.splashRadius !== undefined) { const outlineGeometry = new BufferGeometry().setFromPoints( new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50), ); const outlineMaterial = new LineDashedMaterial({ color: "orange", dashSize: 0.05, gapSize: 0.025, }); const outline = new Line( outlineGeometry, outlineMaterial, ).computeLineDistances(); splashRadiusWrapper.current?.add(outline); return () => { splashRadiusWrapper.current?.remove(outline); }; } }); useEffect(() => { if ( isHalloween() && shot?.splashRadius !== undefined && shot.in.status === "penetration" ) { const audio = new Audio("/assets/audio/lotta-damage.mp3"); audio.currentTime = 0.7; audio.volume = 0.25; audio.play(); } }, [shot]); if (!shot) return null; const inLength = shot.in.layers.reduce((accumulator, layer) => { if (layer.type === null) return accumulator + layer.distance; return accumulator; }, 0) + SHOT_DISPLAY_LENGTH_INFINITY; const outLength = shot.out && shot.out.layers.length > 0 ? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo( (shot.out.layers.at(-1) as ShotLayerNonExternal).point, ) : SHOT_DISPLAY_LENGTH_INFINITY; const inLast = shot.in.layers.findLast( (layer) => layer.type !== null, ) as ShotLayerNonExternal; const outTitleColor = shot.out ? shotStatusColors[shot.out.status] : undefined; const inTitleColor = shotStatusColors[shot.in.status]; const tracerGoingUp = shot.in.surfaceNormal.y + (shot.out?.surfaceNormal.y ?? 0) > 0; return ( {shot.splashRadius && ( )} {[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => { if (layer.type === null) return null; const shotStatusColor = shot.in.status === "splash" ? shotStatusColors.splash : shotStatusColors[layer.status]; return (
); })} {shot.out && ( )} ); } interface AnimatorProps { inTracer: RefObject; outTracer: RefObject; splashRadiusWrapper: RefObject; splashRadiusMaterial: RefObject; inLength: number; outLength: number; } function Animator({ inTracer, outTracer, splashRadiusWrapper, splashRadiusMaterial, inLength, outLength, }: AnimatorProps) { const shot = Tankopedia.use((state) => state.shot!); let animationStartTime: number | null = null; useFrame(({ clock, invalidate }) => { const t = clock.elapsedTime; const tracerT1 = t % 1; const tracerT2 = (t + 0.5) % 1; if (inTracer.current) { inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1); inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0); if (outTracer.current) { outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1); outTracer.current.position.set(0, tracerT2 * outLength, 0); } invalidate(); } if ( shot.splashRadius !== undefined && splashRadiusWrapper.current && splashRadiusMaterial.current ) { if (animationStartTime === null) animationStartTime = t; const scale = clamp(4 * (t - animationStartTime), 0, 1) * shot.splashRadius; const opacity = 1 - clamp(2 * (t - animationStartTime), 0, 1); splashRadiusWrapper.current.scale.set(scale, scale, scale); splashRadiusMaterial.current.opacity = opacity; invalidate(); } }); return null; }