diff --git a/client/src/cards/Pilot/index.tsx b/client/src/cards/Pilot/index.tsx index 1bfe8ac0..6f48ad44 100644 --- a/client/src/cards/Pilot/index.tsx +++ b/client/src/cards/Pilot/index.tsx @@ -1,22 +1,71 @@ import {useDataStream} from "client/src/context/useDataStream"; -import {useNetRequest} from "client/src/context/useNetRequest"; +import {Joystick, LinearJoystick} from "@thorium/ui/Joystick"; +import {ReactNode} from "react"; +import type {Coordinates} from "server/src/utils/unitTypes"; +import {netSend} from "client/src/context/netSend"; +async function rotation({x = 0, y = 0, z = 0}: Partial>) { + await netSend("thrustersSetRotationDelta", {rotation: {x, y, z}}); +} +async function direction({x = 0, y = 0, z = 0}: Partial>) { + await netSend("thrustersSetDirection", {direction: {x, y, z}}); +} + +function UntouchableLabel({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +

+ {children} +

+ ); +} export function Pilot() { useDataStream({systemId: null}); return (
-
+
Impulse controls here
-
Thruster direction here here
+
+ direction({z: -y})} vertical> + Fore + Aft + + direction({y: -y, x: -x})} + > + Down + Up + Starboard + Port + +
-
+
Course controls here
Camera controls here
-
Thruster rotation here
+
+ rotation({z: x, x: y})}> + Pitch Down + Pitch Up + + Starboard Roll + + Port Roll + + rotation({y: -x})}> + Port Yaw + Starboard Yaw +
); diff --git a/client/src/components/ui/Joystick.tsx b/client/src/components/ui/Joystick.tsx index cb0ff5c3..2c89d8d3 100644 --- a/client/src/components/ui/Joystick.tsx +++ b/client/src/components/ui/Joystick.tsx @@ -1 +1,63 @@ -export {}; +import {animated as a} from "@react-spring/web"; +import {useJoystick} from "client/src/hooks/useJoystick"; +import {ReactNode} from "react"; + +export const Joystick = ({ + children, + className, + onDrag, +}: { + onDrag: (dir: {x: number; y: number}) => void; + className?: string; + children?: ReactNode; +}) => { + const [xy, bind, containerRef] = useJoystick({axisSnap: true, onDrag}); + + return ( +
+
+ `translate3d(${x}px,${y}px,0)`)}} + className="z-10 w-10 h-10 rounded-full border-black/50 border-2 bg-gray-500 shadow-md cursor-pointer" + > + {children} +
+
+ ); +}; + +export const LinearJoystick = ({ + className, + onDrag, + children, + vertical, +}: { + className?: string; + onDrag: (dirs: {x: number; y: number}) => void; + children: ReactNode; + vertical?: boolean; +}) => { + const [xy, bind, containerRef] = useJoystick({ + axis: vertical ? "y" : "x", + onDrag, + }); + return ( +
+ `translate3d(${x}px,${y}px,0)`)}} + className="z-10 w-10 h-10 rounded-full border-black/50 border-2 bg-gray-500 shadow-md cursor-pointer" + > + {children} +
+ ); +}; diff --git a/client/src/hooks/useJoystick.ts b/client/src/hooks/useJoystick.ts new file mode 100644 index 00000000..5e477d5b --- /dev/null +++ b/client/src/hooks/useJoystick.ts @@ -0,0 +1,65 @@ +import throttle from "lodash.throttle"; +import {useSpring} from "@react-spring/web"; +import {useDrag} from "@use-gesture/react"; +import {useRef} from "react"; + +function distance(x1: number, y1: number, x2 = 0, y2 = 0) { + const a = x1 - x2; + const b = y1 - y2; + + return Math.sqrt(a * a + b * b); +} +const minDistance = 10; +export function useJoystick({ + axisSnap, + axis, + onDrag = () => {}, + throttleMs = 100, +}: { + axisSnap?: boolean; + axis?: "x" | "y" | undefined; + onDrag?: (values: {x: number; y: number}) => void; + throttleMs?: number; +} = {}) { + const callback = useRef(throttle(onDrag, throttleMs)); + const containerRef = useRef(null); + const maxDistance = useRef(0); + const [{xy}, set] = useSpring(() => ({ + xy: [0, 0], + config: {mass: 1, tension: 280, friction: 30}, + })); + const bind = useDrag( + ({down, first, movement: [x, y]}) => { + if (first) { + maxDistance.current = + (containerRef.current?.getBoundingClientRect()[ + axis === "y" ? "height" : "width" + ] || 0) / 2; + } + const dist = distance(x, y); + if (dist > maxDistance.current) { + const theta = Math.abs(Math.atan(y / x)); + x = maxDistance.current * Math.cos(theta) * (x > 0 ? 1 : -1); + y = maxDistance.current * Math.sin(theta) * (y > 0 ? 1 : -1); + } + if ( + axisSnap && + (Math.abs(x) > minDistance || Math.abs(y) > minDistance) + ) { + if (x < minDistance && x > minDistance * -1) x = 0; + if (y < minDistance && y > minDistance * -1) y = 0; + } + set({ + xy: down ? [x, y] : [0, 0], + immediate: down, + }); + callback.current?.( + down + ? {x: x / maxDistance.current, y: y / maxDistance.current} + : {x: 0, y: 0} + ); + }, + {axis} + ); + return [xy, bind, containerRef] as const; +} diff --git a/server/src/inputs/list.ts b/server/src/inputs/list.ts index 73adf87b..8f9369f1 100644 --- a/server/src/inputs/list.ts +++ b/server/src/inputs/list.ts @@ -17,3 +17,4 @@ export {thrustersPluginInput} from "./plugins/shipSystems/thrusters"; export {impulseEnginesInputs} from "./shipSystems/impulseEngines"; export {warpEnginesInputs} from "./shipSystems/warpEngines"; export {pluginInventoryInputs} from "./plugins/inventory"; +export {thrustersInputs} from "./shipSystems/thrusters"; diff --git a/server/src/inputs/shipSystems/thrusters.ts b/server/src/inputs/shipSystems/thrusters.ts index cebd45ee..4f4b3dc8 100644 --- a/server/src/inputs/shipSystems/thrusters.ts +++ b/server/src/inputs/shipSystems/thrusters.ts @@ -1,4 +1,5 @@ import {DataContext} from "server/src/utils/DataContext"; +import {pubsub} from "server/src/utils/pubsub"; import { Coordinates, MetersPerSecond,