Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
372375d18e3f…
Author-recorded commit
6fbf09f88a0a…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f

Archive hash verified: 76e4eb30c67a6f45…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/game/input.ts
// Director keyPressed() emulation. The Lingo uses Mac key codes for the
// arrows (123 left, 124 right, 125 down, 126 up) and Escape (53), and
// characters for the rest (" ", "s", "1").
export type DirectorKey = 123 | 124 | 125 | 126 | 53 | ' ' | 's' | '1';

const CODE_TO_KEY: Record<string, DirectorKey> = {
  ArrowLeft: 123,
  ArrowRight: 124,
  ArrowDown: 125,
  ArrowUp: 126,
  KeyA: 123,
  KeyD: 124,
  KeyS: 125,
  KeyW: 126,
  Escape: 53,
  Space: ' ',
  KeyM: 's', // sound toggle; "s" is taken by WASD steering
  Digit1: '1',
};

export class Input {
  private readonly held = new Set<DirectorKey>();
  /** Virtual (touch/gamepad) sources merged in each poll. */
  private readonly virtual = new Set<DirectorKey>();
  /** Short pulses from shell-triggered NAP-KEYS actions. */
  private readonly actions = new Set<DirectorKey>();
  private readonly actionTimers = new Map<DirectorKey, ReturnType<typeof setTimeout>>();

  keyPressed(key: DirectorKey): boolean {
    return this.held.has(key) || this.virtual.has(key) || this.actions.has(key);
  }

  setVirtual(key: DirectorKey, down: boolean): void {
    if (down) this.virtual.add(key);
    else this.virtual.delete(key);
  }

  /** Turn a shell-triggered action into a brief control pulse. Key repeat extends it. */
  pulseAction(key: DirectorKey, durationMs = 160): void {
    const previous = this.actionTimers.get(key);
    if (previous !== undefined) clearTimeout(previous);
    this.actions.add(key);
    this.actionTimers.set(
      key,
      setTimeout(() => {
        this.actions.delete(key);
        this.actionTimers.delete(key);
      }, durationMs),
    );
  }

  clear(): void {
    this.held.clear();
    this.virtual.clear();
    this.actions.clear();
    for (const timer of this.actionTimers.values()) clearTimeout(timer);
    this.actionTimers.clear();
  }

  attach(target: Window): () => void {
    const down = (e: KeyboardEvent) => {
      // Typing in a text field (the pack link) is not driving.
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
      const key = CODE_TO_KEY[e.code];
      if (key === undefined) return;
      this.held.add(key);
      if (e.code.startsWith('Arrow') || e.code === 'Space') e.preventDefault();
    };
    const up = (e: KeyboardEvent) => {
      const key = CODE_TO_KEY[e.code];
      if (key !== undefined) this.held.delete(key);
    };
    const blur = () => this.held.clear();
    target.addEventListener('keydown', down);
    target.addEventListener('keyup', up);
    target.addEventListener('blur', blur);
    return () => {
      target.removeEventListener('keydown', down);
      target.removeEventListener('keyup', up);
      target.removeEventListener('blur', blur);
    };
  }
}