Back to Napplet Machines V8
SOURCE / PINNED RELEASE

Made of little things.

Napplet Machines V8

Release
b3f614e739f0…
Author-recorded commit
534f19acb4ba…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b5572362d4a

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

src/controls.ts
import {
  createGamepadInput,
  type GamepadBindings,
} from '../docs/examples/gamepad.js';
import { idleInput, type Input } from './physics.js';
import { clamp } from './track.js';

export const racingBindings: GamepadBindings = {
  steer: [{ axis: 0, scale: -1 }, { button: 14 }, { button: 15, scale: -1 }],
  gas: [{ button: 7 }, { button: 0 }],
  serviceBrake: [{ button: 6 }],
  drift: [{ button: 1 }],
  recover: [{ button: 3 }],
  confirm: [{ button: 0 }],
  pause: [{ button: 9 }],
  previousCar: [{ button: 4 }],
  nextCar: [{ button: 5 }],
  previousTrack: [{ button: 12 }],
  nextTrack: [{ button: 13 }],
};

export class ControllerDriving {
  private pad;
  selected: number | null = null;
  constructor(options?: Parameters<typeof createGamepadInput>[1]) {
    this.pad = createGamepadInput(racingBindings, options);
  }
  poll() {
    const frame = this.pad.poll();
    const disconnected =
      this.selected !== null && frame.disconnected.includes(this.selected);
    if (disconnected) this.selected = null;
    // Claim a slot deliberately. Another pad cannot steal the car, and a
    // replacement in the same browser slot must rejoin with a fresh press.
    let joined = false;
    if (this.selected === null) {
      const joining = frame.players.find(
        (p) => p.mapped && p.actions.confirm.pressed,
      );
      if (joining) {
        this.selected = joining.index;
        joined = true;
      }
    }
    const active = frame.players.find((p) => p.index === this.selected);
    const a = active?.actions;
    const drive: Input = {
      throttle: (a?.gas.value || 0) - (a?.serviceBrake.value || 0),
      steer: a?.steer.value || 0,
      brake: a?.drift.down || false,
      reset: false,
    };
    return {
      drive,
      disconnected,
      joined,
      status: frame.status,
      selected: this.selected,
      unmapped: frame.players.some((p) => !p.mapped),
      confirm: !!a?.confirm.pressed,
      back: !!a?.drift.pressed,
      pause: !!a?.pause.pressed,
      recover: !!a?.recover.pressed,
      carStep: Number(!!a?.nextCar.pressed) - Number(!!a?.previousCar.pressed),
      trackStep:
        Number(!!a?.nextTrack.pressed) - Number(!!a?.previousTrack.pressed),
    };
  }
  reset() {
    this.pad.reset();
  }
  dispose() {
    this.pad.dispose();
  }
}

export function mixDriving(...sources: Input[]): Input {
  // Strongest steering wins; opposing equal-strength inputs cancel.
  const signed = (key: 'steer' | 'throttle') => {
    const values = sources.map((s) => s[key]);
    const positive = Math.max(0, ...values),
      negative = Math.min(0, ...values);
    return positive === -negative
      ? 0
      : positive > -negative
        ? positive
        : negative;
  };
  return {
    steer: signed('steer'),
    throttle: signed('throttle'),
    brake: sources.some((s) => s.brake),
    reset: sources.some((s) => s.reset),
  };
}
export function steeringAt(x: number, left: number, width: number) {
  return clamp((left + width / 2 - x) / Math.max(1, width / 2 - 26), -1, 1);
}
export function pedalAt(x: number, y: number, width: number, height: number) {
  if (x < 0 || y < 0 || x > width || y > height) return '';
  return y < height * 0.5 ? 'go' : x < width * 0.5 ? 'stop' : 'drift';
}

export class TouchDriving {
  value = idleInput();
  private steeringPointer: number | null = null;
  private pedalPointer: number | null = null;
  constructor(
    private steering: HTMLElement,
    private pedals: HTMLElement,
  ) {
    const steer = (e: PointerEvent) => {
      const r = steering.getBoundingClientRect();
      this.setSteering(steeringAt(e.clientX, r.left, r.width));
    };
    steering.addEventListener('pointerdown', (e) => {
      if (this.steeringPointer !== null) return;
      e.preventDefault();
      steering.focus({ preventScroll: true });
      this.steeringPointer = e.pointerId;
      steering.setPointerCapture(e.pointerId);
      steer(e);
      steering.classList.add('pressed');
    });
    steering.addEventListener('pointermove', (e) => {
      if (e.pointerId === this.steeringPointer) steer(e);
    });
    const releaseSteer = (e: PointerEvent) => {
      if (e.pointerId !== this.steeringPointer) return;
      this.steeringPointer = null;
      this.setSteering(0);
      steering.classList.remove('pressed');
    };
    for (const event of ['pointerup', 'pointercancel', 'lostpointercapture'])
      steering.addEventListener(event, releaseSteer as EventListener);
    steering.addEventListener('keydown', (e) => {
      if (!['ArrowLeft', 'ArrowRight', 'Home'].includes(e.key)) return;
      e.preventDefault();
      e.stopPropagation();
      this.setSteering(
        e.key === 'Home'
          ? 0
          : clamp(
              this.value.steer + (e.key === 'ArrowLeft' ? 0.15 : -0.15),
              -1,
              1,
            ),
      );
    });
    steering.addEventListener('keyup', (e) => {
      if (['ArrowLeft', 'ArrowRight', 'Home'].includes(e.key)) {
        e.stopPropagation();
        this.setSteering(0);
      }
    });
    steering.addEventListener('blur', () => {
      if (this.steeringPointer === null) this.setSteering(0);
    });
    const pedal = (e: PointerEvent) => {
      const r = pedals.getBoundingClientRect();
      this.setPedal(
        pedalAt(e.clientX - r.left, e.clientY - r.top, r.width, r.height),
      );
    };
    pedals.addEventListener('pointerdown', (e) => {
      if (this.pedalPointer !== null) return;
      e.preventDefault();
      this.pedalPointer = e.pointerId;
      pedals.setPointerCapture(e.pointerId);
      pedal(e);
    });
    pedals.addEventListener('pointermove', (e) => {
      if (e.pointerId === this.pedalPointer) pedal(e);
    });
    const releasePedal = (e: PointerEvent) => {
      if (e.pointerId !== this.pedalPointer) return;
      this.pedalPointer = null;
      this.setPedal('');
    };
    for (const event of ['pointerup', 'pointercancel', 'lostpointercapture'])
      pedals.addEventListener(event, releasePedal as EventListener);
    pedals.querySelectorAll<HTMLButtonElement>('button').forEach((button) => {
      button.addEventListener('keydown', (e) => {
        if (![' ', 'Enter'].includes(e.key)) return;
        e.preventDefault();
        e.stopPropagation();
        this.setPedal(button.dataset.drive!);
      });
      button.addEventListener('keyup', (e) => {
        if ([' ', 'Enter'].includes(e.key)) {
          e.stopPropagation();
          this.setPedal('');
        }
      });
      button.addEventListener('blur', () => {
        if (this.pedalPointer === null) this.setPedal('');
      });
    });
  }
  private setSteering(value: number) {
    this.value.steer = value;
    const travel = Math.max(1, this.steering.clientWidth / 2 - 26);
    this.steering.style.setProperty('--thumb-x', `${-value * travel}px`);
    this.steering.setAttribute(
      'aria-valuenow',
      String(Math.round(value * 100)),
    );
    this.steering.setAttribute(
      'aria-valuetext',
      value === 0
        ? 'Centred'
        : `${Math.round(Math.abs(value) * 100)}% ${value > 0 ? 'left' : 'right'}`,
    );
  }
  private setPedal(mode: string) {
    this.value.throttle =
      mode === 'go' || mode === 'drift' ? 1 : mode === 'stop' ? -1 : 0;
    this.value.brake = mode === 'drift';
    this.pedals.querySelectorAll<HTMLElement>('[data-drive]').forEach((b) => {
      b.classList.toggle('pressed', b.dataset.drive === mode);
      b.setAttribute('aria-pressed', String(b.dataset.drive === mode));
    });
  }
  clear() {
    const steeringId = this.steeringPointer,
      pedalId = this.pedalPointer;
    this.steeringPointer = null;
    this.pedalPointer = null;
    if (steeringId !== null && this.steering.hasPointerCapture(steeringId))
      this.steering.releasePointerCapture(steeringId);
    if (pedalId !== null && this.pedals.hasPointerCapture(pedalId))
      this.pedals.releasePointerCapture(pedalId);
    this.setSteering(0);
    this.setPedal('');
    this.steering.classList.remove('pressed');
  }
}