import { animated as a, useSpring } from "@react-spring/web"; import { q } from "@thorium/context/AppContext"; import { useCardContext } from "@thorium/context/CardContext"; import useAnimationFrame from "@thorium/hooks/useAnimationFrame"; import { useGamepadPress, useGamepadValue } from "@thorium/hooks/useGamepadStore"; import useMeasure from "@thorium/hooks/useMeasure"; import { useStation } from "@thorium/routes/station/useStation"; import Button from "@thorium/ui/Button"; import { cn } from "@thorium/utils/cn"; import { useLiveQuery } from "@thorium/utils/live-query/client"; import type { KilometerPerSecond } from "@thorium/utils/unitTypes"; import { useDrag } from "@use-gesture/react"; import throttle from "lodash.throttle"; import { useEffect, useRef, useState } from "react"; const C_IN_METERS = 299792458; export function formatSpeed(speed: KilometerPerSecond) { if (Math.abs(speed) > C_IN_METERS / 1000 / 2) { return `${(speed / (C_IN_METERS / 1000)).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1, })} C`; } if (Math.abs(speed) > 1) { return `${speed.toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1, })} km/s`; } return `${(speed * 1000).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1, })} m/s`; } export function useForwardVelocity() { const { shipId } = useStation(); const [{ id: impulseId }] = q.pilot.impulseEngines.get.useNetRequest({ shipId, }); const [{ id: warpId, currentWarpFactor }] = q.pilot.warpEngines.get.useNetRequest({ shipId }); const [ship] = q.navigation.ship.useNetRequest({ shipId }); const { interpolate } = useLiveQuery(); return function getForwardVelocity(): [KilometerPerSecond, KilometerPerSecond] { const id = currentWarpFactor ? warpId : impulseId; const { f: forwardVelocity } = interpolate(ship.id) || { f: 0 }; const { x: targetSpeed } = interpolate(id) || { x: 0 }; return [forwardVelocity, targetSpeed] as [KilometerPerSecond, KilometerPerSecond]; }; } const ForwardVelocity = () => { const { cardLoaded } = useCardContext(); const forwardRef = useRef(null); const targetRef = useRef(null); const getForwardVelocity = useForwardVelocity(); useAnimationFrame(() => { const [forwardVelocity, targetVelocity] = getForwardVelocity(); if (targetRef.current) { targetRef.current.textContent = formatSpeed(targetVelocity); } if (forwardRef.current) { forwardRef.current.textContent = formatSpeed(forwardVelocity); } }, cardLoaded); return ( <>
Forward Velocity
{formatSpeed(0)}
Target Velocity

{formatSpeed(0)}

); }; const KNOB_HEIGHT = 44; const BUTTON_OFFSET = 0.8; export const ImpulseControls = ({ cardLoaded = true, onFlightControlInteraction, forwardAutopilot, }: { cardLoaded?: boolean; onFlightControlInteraction?: () => void; forwardAutopilot?: boolean; }) => { const { shipId } = useStation(); const [{ targetSpeed, cruisingSpeed, emergencySpeed, speeds }] = q.pilot.impulseEngines.get.useNetRequest({ shipId }); const [{ currentWarpFactor, speeds: warpSpeeds }] = q.pilot.warpEngines.get.useNetRequest({ shipId, }); const downRef = useRef(false); const [ref, measurement, getMeasurements] = useMeasure(); const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 280, friction: 30 }, })); const callback = useRef( throttle((speed: number) => { void q.pilot.impulseEngines.setSpeed.netSend({ speed: Math.min(emergencySpeed, Math.max(0, speed)), shipId, }); if (speed === 0) { void q.pilot.warpEngines.setWarpFactor.netSend({ factor: 0, shipId }); } }, 100), ); const bind = useDrag( ({ down, first, offset: [, yVal] }) => { if (first) onFlightControlInteraction?.(); downRef.current = down; set({ y: yVal, immediate: down, }); const normalizedValue = Math.min(1, Math.abs(1 - yVal) / (measurement.height - KNOB_HEIGHT)); const speedValue = normalizedValue < 0.005 ? 0 : normalizedValue <= BUTTON_OFFSET ? normalizedValue * (1 / BUTTON_OFFSET) * cruisingSpeed : (normalizedValue - BUTTON_OFFSET) * (1 / (1 - BUTTON_OFFSET)) * (emergencySpeed - cruisingSpeed) + cruisingSpeed; callback.current(speedValue); }, { axis: "y", bounds: { bottom: 0, top: -measurement.height + KNOB_HEIGHT }, from: () => [0, y.get()], }, ); const height = measurement.height; useEffect(() => { if (downRef.current) return; getMeasurements(); const y = (targetSpeed / cruisingSpeed <= 1 ? (targetSpeed / cruisingSpeed) * BUTTON_OFFSET : BUTTON_OFFSET + ((targetSpeed - cruisingSpeed) / (emergencySpeed - cruisingSpeed)) * (1 - BUTTON_OFFSET)) * (-height + KNOB_HEIGHT); set({ y, }); }, [targetSpeed, cruisingSpeed, emergencySpeed, set, height, getMeasurements, cardLoaded]); useGamepadValue("impulse-speed", (value) => { const throttleValue = (value + 1) / 2; callback.current(throttleValue * cruisingSpeed); }); const [impulseAdjust, setImpulseAdjust] = useState(0); const impulseAdjustVelocity = useRef(0); useGamepadValue("impulse-adjust", (value) => { setImpulseAdjust(value); impulseAdjustVelocity.current = 0; }); useAnimationFrame( () => { const adjust = Math.min(Math.abs(impulseAdjustVelocity.current), Math.abs(impulseAdjust)) * Math.sign(impulseAdjust); callback.current(targetSpeed + adjust); impulseAdjustVelocity.current += impulseAdjust / 500; }, impulseAdjust !== 0 && cardLoaded, ); // Warp Gamepad Control const [warpFocus, setWarpFocus] = useState(0); useGamepadValue("warp-focus-set", (value) => { setWarpFocus( Math.max( 0, Math.min(warpSpeeds.length + 1, Math.round(((value + 1) / 2) * (warpSpeeds.length + 1))), ), ); }); useGamepadPress("warp-focus-adjust", { onDown: (val) => { setWarpFocus((focus) => Math.max(0, Math.min(warpSpeeds.length + 1, Math.round(val + focus))), ); }, }); useGamepadPress("warp-engage", { onDown: () => { void q.pilot.warpEngines.setWarpFactor.netSend({ factor: warpFocus, shipId, }); setWarpFocus(0); }, }); useGamepadPress("full-stop", { onDown: () => { callback.current(0); setWarpFocus(0); }, }); return (
{/* TODO: Include heat indicator here eventually. */}
{speeds.map((speed, i) => { const buttonSpeed = i === speeds.length - 1 ? emergencySpeed : cruisingSpeed * ((i + 1) / (speeds.length - 1)); return ( ); })}
`translate3d(0px,${y}px,0)`), }} className="z-10 h-10 w-10 cursor-pointer touch-none rounded-full border-2 border-black/50 bg-gray-500 shadow-md" />
6, })} > {warpSpeeds .slice() .reverse() .map(({ label }, i, arr) => { const warpFactor = arr.length - i; return ( ); })}
); };