import { Edges, Line } from "@react-three/drei"; import { useFrame } from "@react-three/fiber"; import { useQueryClient } from "@tanstack/react-query"; import { q } from "@thorium/context/AppContext"; import { useCardContext } from "@thorium/context/CardContext"; import useAnimationFrame from "@thorium/hooks/useAnimationFrame"; import { useStation } from "@thorium/routes/station/useStation"; import Button from "@thorium/ui/Button"; import { Icon } from "@thorium/ui/Icon"; import Slider from "@thorium/ui/Slider"; import { useLiveQuery } from "@thorium/utils/live-query/client"; import { isPointWithinCone } from "@thorium/utils/starmap/isPointWithinCone"; import { degToRad } from "@thorium/utils/unitTypes"; import { useEffect, useMemo, useRef } from "react"; import { DoubleSide, Euler, type Group, Quaternion, Vector3 } from "three"; import type { Line2 } from "three-stdlib"; export function PhaserArcs() { const { shipId } = useStation(); const [phasers] = q.targeting.phasers.list.useNetRequest({ shipId }); return ( <> {phasers.map((phaser) => ( ))} ); } const up = new Vector3(0, 1, 0); const cameraProjection = new Vector3(); const planeVector = new Vector3(0, 1, 0); const directionVector = new Vector3(); export function ConeVisualization({ arc, heading, pitch, maxArc, maxRange, }: { id: number; arc: number; heading: number; pitch: number; maxArc: number; maxRange: number; }) { const [height, radius, angle, rotation] = useMemo(() => { const range = maxRange - maxRange * (arc / (maxArc + 1)); const rotation = new Euler(pitch * (Math.PI / 180), heading * (Math.PI / 180), 0); const angle = arc * (Math.PI / 180); const radius = range * Math.tan(angle / 2); return [range, radius, angle, rotation]; }, [arc, maxArc, pitch, heading, maxRange]); const lineLength = Math.sqrt(height ** 2 + radius ** 2); const coneRef = useRef(null); const groupRef = useRef(null); useFrame(({ camera }) => { cameraProjection .copy(camera.position) .projectOnPlane(planeVector.set(0, 1, 0)) .normalize(); const angle = Math.atan2(cameraProjection.x, cameraProjection.y) + rotation.y; if (coneRef.current) { coneRef.current.rotation.y = angle + Math.PI / 2; } groupRef.current?.quaternion.setFromUnitVectors( up, directionVector.set(0, 0, -1).applyEuler(rotation).normalize().negate(), ); }); return ( ); } export function BeamVisualization() { const { shipId, ship: { currentSystem }, } = useStation(); const [firingPhasers] = q.targeting.phasers.firing.useNetRequest({ systemId: currentSystem, }); const { interpolate } = useLiveQuery(); const lineRef = useRef(null); useFrame(() => { if (!lineRef.current) return; const points: number[] = []; const player = interpolate(shipId); if (!player) return; firingPhasers.forEach((phaser) => { const ship = interpolate(phaser.shipId); const target = interpolate(phaser.targetId); if (!ship || !target) return; points.push(ship.x - player.x, ship.y - player.y, ship.z - player.z); points.push(target.x - player.x, target.y - player.y, target.z - player.z); }); if (points.length === 0) { lineRef.current.visible = false; } else { lineRef.current.visible = true; lineRef.current?.geometry.setPositions(points); lineRef.current.material.linewidth = 5 * Math.max(...firingPhasers.map((p) => p.firePercent)); } }); return ( ); } export function Phasers() { const { shipId } = useStation(); const [phasers] = q.targeting.phasers.list.useNetRequest({ shipId }); return (
{phasers.map((phaser) => ( ))}
); } const targetVector = new Vector3(); const playerVector = new Vector3(); const rotationQuaternion = new Quaternion(); const direction = new Vector3(); function PhaserControl({ id, arc, maxArc, maxRange, nominalHeat, maxSafeHeat, heading, pitch, }: { id: number; arc: number; maxArc: number; maxRange: number; nominalHeat: number; maxSafeHeat: number; heading: number; pitch: number; }) { const { cardLoaded } = useCardContext(); const { shipId } = useStation(); const { interpolate } = useLiveQuery(); const chargeRef = useRef(null); const heatRef = useRef(null); const [targetedContact] = q.targeting.targetedContact.useNetRequest({ shipId, }); const buttonContainerRef = useRef(null); useAnimationFrame(() => { const values = interpolate(id); if (!values) return; const { z: heat, x: charge } = values; if (chargeRef.current) { chargeRef.current.value = charge; } if (heatRef.current) { // Scale the heat value heatRef.current.value = (heat - nominalHeat) / (maxSafeHeat - nominalHeat); } // Check if the target is in range let inCone = false; if (targetedContact) { const target = interpolate(targetedContact.id); const player = interpolate(shipId); if (target && player) { targetVector.set(target.x, target.y, target.z); playerVector.set(player.x, player.y, player.z); const range = maxRange - maxRange * (arc / (maxArc + 1)); rotationQuaternion.set(player.r.x, player.r.y, player.r.z, player.r.w); // Turn the ship rotation quaternion into a vector direction.set(0, 0, 1).applyQuaternion(rotationQuaternion); // Add the Phaser rotation to the ship rotation direction.applyAxisAngle(new Vector3(0, 1, 0), degToRad(heading)); direction.applyAxisAngle(new Vector3(1, 0, 0), degToRad(pitch)); direction.multiplyScalar(range); inCone = isPointWithinCone(targetVector, { apex: playerVector, angle: degToRad(arc), direction, }); } } if (inCone) { buttonContainerRef.current?.childNodes.forEach((child) => { if (child instanceof HTMLButtonElement) { child.disabled = false; child.classList.remove("grayscale"); } }); } else { buttonContainerRef.current?.childNodes.forEach((child) => { if (child instanceof HTMLButtonElement) { child.disabled = true; child.classList.add("grayscale"); } }); } }, cardLoaded); const cache = useQueryClient(); const getFirePhasers = (firePercent: number) => { return async function firePhasers() { await q.targeting.phasers.fire.netSend({ phaserId: id, firePercent }); document.addEventListener( "pointerup", () => { q.targeting.phasers.fire.netSend({ phaserId: id, firePercent: 0 }); }, { once: true }, ); }; }; useEffect(() => { return () => { q.targeting.phasers.fire.netSend({ phaserId: id, firePercent: 0 }); }; }, [id]); return (
{ // Manually update the local cache with the arc value so it looks really smooth cache.setQueryData(q.targeting.phasers.list.getQueryKey({ shipId }), (data: any[]) => { if (!data) return data; return data.map((phaser) => { if (phaser.id === id) { return { ...phaser, arc: val as number }; } return phaser; }); }); q.targeting.phasers.setArc.netSend({ phaserId: id, arc: val as number, }); }} />
); }