Back to Chase'n Those Narly Waves
SOURCE / PINNED RELEASE

Made of little things.

Chase'n Those Narly Waves

Release
0c7dde9d4921…
Author-recorded commit
88e58516be17…
License
LICENSE
Author’s source reference
nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-836af5a08b9

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

src/input.ts
// Keyboard and touch input for the game.
// W A S D are "held" keys (steer and paddle). Arrow keys are single presses
// that get collected and turned into surf tricks by the game.

export type Arrow = 'up' | 'down' | 'left' | 'right';
export type MoveKey = 'w' | 'a' | 's' | 'd';

const MOVE_KEYS: Record<string, MoveKey> = {
  KeyW: 'w',
  KeyA: 'a',
  KeyS: 's',
  KeyD: 'd',
};

const ARROW_KEYS: Record<string, Arrow> = {
  ArrowUp: 'up',
  ArrowDown: 'down',
  ArrowLeft: 'left',
  ArrowRight: 'right',
};

export class Input {
  readonly held = new Set<MoveKey>();
  private arrows: Arrow[] = [];
  /** Keys that were just pressed down (not held from before). */
  private tapped = new Set<MoveKey>();

  press(key: MoveKey): void {
    if (!this.held.has(key)) this.tapped.add(key);
    this.held.add(key);
  }

  /** Was this key tapped since last frame? (Holding it down only counts once.) */
  takeTap(key: MoveKey): boolean {
    const tapped = this.tapped.has(key);
    this.tapped.delete(key);
    return tapped;
  }

  release(key: MoveKey): void {
    this.held.delete(key);
  }

  pressArrow(arrow: Arrow): void {
    this.arrows.push(arrow);
  }

  /** Hand the arrow presses since last frame to the game, then forget them. */
  takeArrows(): Arrow[] {
    const taken = this.arrows;
    this.arrows = [];
    return taken;
  }

  /** Let go of everything (used when the game loses focus or pauses). */
  clear(): void {
    this.held.clear();
    this.tapped.clear();
    this.arrows = [];
  }

  /** -1 = steer left, 1 = steer right, 0 = straight. */
  get steer(): number {
    return (this.held.has('d') ? 1 : 0) - (this.held.has('a') ? 1 : 0);
  }

  get paddling(): boolean {
    return this.held.has('w');
  }

  get braking(): boolean {
    return this.held.has('s');
  }
}

/**
 * Listen for game keys while the napplet has keyboard focus.
 * `isPlaying` stops game keys from doing anything on menus.
 */
export function attachKeyboard(input: Input, isPlaying: () => boolean): void {
  window.addEventListener('keydown', (event) => {
    const move = MOVE_KEYS[event.code];
    const arrow = ARROW_KEYS[event.code];
    if (!move && !arrow) return;
    if (!isPlaying()) return;
    event.preventDefault(); // stop arrows from scrolling the page
    if (move) input.press(move);
    if (arrow && !event.repeat) input.pressArrow(arrow);
  });

  window.addEventListener('keyup', (event) => {
    const move = MOVE_KEYS[event.code];
    if (move) input.release(move);
  });

  window.addEventListener('blur', () => input.clear());
}

/** On-screen buttons for phones and tablets. */
export function attachTouchButtons(root: HTMLElement, input: Input): void {
  for (const button of root.querySelectorAll<HTMLButtonElement>('[data-hold]')) {
    const key = button.dataset.hold as MoveKey;
    const up = (): void => input.release(key);
    button.addEventListener('pointerdown', (event) => {
      event.preventDefault();
      button.setPointerCapture(event.pointerId);
      input.press(key);
    });
    button.addEventListener('pointerup', up);
    button.addEventListener('pointercancel', up);
    button.addEventListener('lostpointercapture', up);
  }

  for (const button of root.querySelectorAll<HTMLButtonElement>('[data-arrow]')) {
    const arrow = button.dataset.arrow as Arrow;
    button.addEventListener('pointerdown', (event) => {
      event.preventDefault();
      input.pressArrow(arrow);
    });
  }
}