File size: 3,404 Bytes
ce91fab
 
 
 
 
 
 
5a3b695
ce91fab
 
 
 
 
 
 
 
5a3b695
ce91fab
5a3b695
 
 
 
 
 
ce91fab
 
 
 
5a3b695
 
ce91fab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5a3b695
 
 
ce91fab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import { useEffect, useRef } from "react";

interface KeyboardControlsArgs {
  selectedFlyId: string | null;
  joystickRef: React.RefObject<{ x: number; y: number }>;
  onToggleFlying: () => void;
  onAltitudeStep: (direction: 1 | -1) => void;
  onFeed: () => void;
}

const ARROW_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);

// Keyboard alternative to Joystick.tsx's pointer drag (arrow keys) and the
// flight switch/altitude slider (Space, +/-) -- writes straight into the same
// joystickRef the pointer joystick writes to, so FlyInstances.tsx needs no
// awareness of where a command came from.
export function useKeyboardControls({ selectedFlyId, joystickRef, onToggleFlying, onAltitudeStep, onFeed }: KeyboardControlsArgs) {
  const heldRef = useRef(new Set<string>());
  // Latest-callback refs: onToggleFlying/onAltitudeStep/onFeed are fresh
  // closures every App render (they're not memoized there), but
  // flying/altitude changes are themselves state updates that would
  // otherwise re-run this effect and clearAll() the joystick mid-press.
  // Keeping only selectedFlyId/joystickRef as real effect deps means
  // listener registration only churns on an actual selection change.
  const onToggleFlyingRef = useRef(onToggleFlying);
  onToggleFlyingRef.current = onToggleFlying;
  const onAltitudeStepRef = useRef(onAltitudeStep);
  onAltitudeStepRef.current = onAltitudeStep;
  const onFeedRef = useRef(onFeed);
  onFeedRef.current = onFeed;

  useEffect(() => {
    const applyFromHeld = () => {
      const held = heldRef.current;
      let x = 0;
      let y = 0;
      if (held.has("ArrowUp")) y += 1;
      if (held.has("ArrowDown")) y -= 1;
      if (held.has("ArrowLeft")) x -= 1;
      if (held.has("ArrowRight")) x += 1;
      const mag = Math.hypot(x, y);
      if (mag > 1) {
        x /= mag;
        y /= mag;
      }
      joystickRef.current.x = x;
      joystickRef.current.y = y;
    };

    const clearAll = () => {
      heldRef.current.clear();
      joystickRef.current.x = 0;
      joystickRef.current.y = 0;
    };

    // Selection changed -- don't let a key held before switching flies stick.
    clearAll();
    if (!selectedFlyId) return;

    const onKeyDown = (e: KeyboardEvent) => {
      if (ARROW_KEYS.has(e.key)) {
        e.preventDefault();
        heldRef.current.add(e.key);
        applyFromHeld();
      } else if (e.code === "Space") {
        e.preventDefault();
        onToggleFlyingRef.current();
      } else if (e.key === "+" || e.key === "=") {
        e.preventDefault();
        onAltitudeStepRef.current(1);
      } else if (e.key === "-" || e.key === "_") {
        e.preventDefault();
        onAltitudeStepRef.current(-1);
      } else if (e.key === "Enter") {
        e.preventDefault();
        onFeedRef.current();
      }
    };

    const onKeyUp = (e: KeyboardEvent) => {
      if (ARROW_KEYS.has(e.key)) {
        e.preventDefault();
        heldRef.current.delete(e.key);
        applyFromHeld();
      }
    };

    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    window.addEventListener("blur", clearAll);
    return () => {
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
      window.removeEventListener("blur", clearAll);
      clearAll();
    };
  }, [selectedFlyId, joystickRef]);
}