Back to Pursuit PIT
SOURCE / PINNED RELEASE

Made of little things.

Pursuit PIT

Release
565f4981bdc3…
Author-recorded commit
197afdbed44b…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7

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

src/controller-input.ts
import { createGamepadInput, type GamepadSample } from '../docs/examples/gamepad.js';

const bindings = {
  x: [{ axis: 0 }, { button: 14, scale: -1 }, { button: 15 }],
  y: [{ axis: 1 }, { button: 12, scale: -1 }, { button: 13 }],
  gas: [{ button: 7 }], brake: [{ button: 6 }],
  confirm: [{ button: 0 }], back: [{ button: 1 }],
  siren: [{ button: 2 }], radio: [{ button: 3 }],
  previousCar: [{ button: 4 }], nextCar: [{ button: 5 }],
  mute: [{ button: 8 }], pause: [{ button: 9 }],
  joinLeft: [{ button: 10 }], joinRight: [{ button: 11 }],
} as const;
export type ControllerAction = keyof typeof bindings;
export type Direction = 'up' | 'down' | 'left' | 'right';
export type Driving = { gas: number; brake: number; steer: number };
const neutral = (): Driving => ({ gas: 0, brake: 0, steer: 0 });

/** One local driver. Never identifies a player by array position or device name. */
export function createControllerInput(options: {
  read?: () => ArrayLike<GamepadSample | null>;
  active?: () => boolean;
} = {}) {
  const input = createGamepadInput(bindings, options);
  let slot: number | null = null;
  let driving = neutral();
  let needsNeutral = true;
  let navigationReady = false;
  let direction: Direction | null = null;
  let repeatAt = 0;
  function reset() {
    input.reset();
    driving = neutral();
    needsNeutral = true;
    navigationReady = false;
    direction = null;
  }
  return {
    get driving() { return driving; },
    reset,
    dispose: () => input.dispose(),
    poll(now: number) {
      const frame = input.poll();
      const lost = slot !== null && frame.disconnected.includes(slot);
      if (lost) { slot = null; reset(); }
      let joined = false;
      if (slot === null && !lost && frame.status === 'ready') {
        const candidate = frame.players.find((pad) => pad.mapped && Object.values(pad.actions).some((a) => a.pressed));
        if (candidate) { slot = candidate.index; joined = true; reset(); }
      }
      const pad = frame.players.find((p) => p.index === slot);
      const actions = pad?.actions;
      const pressed = new Set<ControllerAction>();
      let navigation: Direction | null = null;
      driving = neutral();
      if (pad && actions && frame.status === 'ready' && !joined && !lost) {
        for (const name of Object.keys(bindings) as ControllerAction[]) {
          if (actions[name].pressed) pressed.add(name);
        }
        const gas = actions.gas.value, brake = actions.brake.value, steer = actions.x.value;
        if (gas === 0 && brake === 0 && steer === 0) needsNeutral = false;
        if (!needsNeutral) driving = { gas, brake, steer };
        const x = actions.x.value, y = actions.y.value;
        const next = Math.max(Math.abs(x), Math.abs(y)) < 0.5 ? null
          : Math.abs(x) > Math.abs(y) ? (x < 0 ? 'left' : 'right') : (y < 0 ? 'up' : 'down');
        if (!next) { navigationReady = true; direction = null; }
        else if (navigationReady && (next !== direction || now >= repeatAt)) {
          navigation = next;
          repeatAt = now + (next !== direction ? 350 : 120);
          direction = next;
        }
      } else {
        needsNeutral = true;
        navigationReady = false;
        direction = null;
      }
      const status = frame.status === 'ready'
        ? slot !== null ? 'Controller connected'
          : frame.players.some((p) => p.mapped) ? 'Press a controller button to join'
          : 'Controller layout unsupported — use keyboard or touch'
        : frame.status === 'blocked' ? 'Controller access blocked — use keyboard or touch'
        : frame.status === 'unavailable' ? 'Controller input unavailable — use keyboard or touch'
        : frame.status === 'inactive' ? 'Focus the game for controller input'
        : 'Connect a controller, then press a button';
      return { pressed, navigation, lost, joined, status };
    },
  };
}

/** Each source keeps its own held state; opposite equal steering signals cancel. */
export function mixDriving(held: ReadonlySet<string>, pad: Driving) {
  const keyboardSteer = Number(held.has('right')) - Number(held.has('left'));
  const steer = keyboardSteer === -pad.steer ? 0
    : Math.abs(keyboardSteer) > Math.abs(pad.steer) ? keyboardSteer : pad.steer;
  return {
    throttle: Math.max(Number(held.has('gas')), pad.gas) - Math.max(Number(held.has('brake')), pad.brake),
    steer,
  };
}