Skip to content

src/input.ts

102 lines · 3.0 KB · TypeScript
Raw
import * as THREE from 'three'

type InputOptions = {
  element: HTMLElement
  toWorld: (clientX: number, clientY: number) => THREE.Vector2 | null
  getShipPosition: () => THREE.Vector2
  clampTarget: (target: THREE.Vector2) => THREE.Vector2
}

const TOUCH_SENSITIVITY = 1.4

const movementKeys = new Map<string, [x: number, y: number]>([
  ['ArrowLeft', [-1, 0]],
  ['KeyA', [-1, 0]],
  ['ArrowRight', [1, 0]],
  ['KeyD', [1, 0]],
  ['ArrowUp', [0, 1]],
  ['KeyW', [0, 1]],
  ['ArrowDown', [0, -1]],
  ['KeyS', [0, -1]],
])

/**
 * Keyboard moves the ship directly. The mouse sets an absolute target to fly
 * toward. Touch uses relative dragging so the player's thumb never covers the
 * ship.
 */
export class Input {
  private readonly pressed = new Set<string>()
  private target: THREE.Vector2 | null = null
  private drag: { pointerId: number; last: THREE.Vector2 } | null = null

  constructor(private readonly options: InputOptions) {
    window.addEventListener('keydown', this.handleKeyDown)
    window.addEventListener('keyup', this.handleKeyUp)
    window.addEventListener('blur', () => this.pressed.clear())

    const { element } = options
    element.addEventListener('pointerdown', this.handlePointerDown)
    element.addEventListener('pointermove', this.handlePointerMove)
    element.addEventListener('pointerup', this.handlePointerUp)
    element.addEventListener('pointercancel', this.handlePointerUp)
  }

  keyAxis() {
    const axis = new THREE.Vector2()
    for (const code of this.pressed) {
      const [x, y] = movementKeys.get(code) ?? [0, 0]
      axis.x += x
      axis.y += y
    }
    return axis
  }

  getTarget() {
    return this.target
  }

  reset() {
    this.target = null
    this.drag = null
  }

  private handleKeyDown = (event: KeyboardEvent) => {
    if (!movementKeys.has(event.code)) return
    event.preventDefault()
    this.pressed.add(event.code)
    this.target = null
  }

  private handleKeyUp = (event: KeyboardEvent) => {
    this.pressed.delete(event.code)
  }

  private handlePointerDown = (event: PointerEvent) => {
    if (event.pointerType === 'mouse') return
    const world = this.options.toWorld(event.clientX, event.clientY)
    if (!world) return
    this.drag = { pointerId: event.pointerId, last: world }
    this.target = this.options.getShipPosition().clone()
  }

  private handlePointerMove = (event: PointerEvent) => {
    const world = this.options.toWorld(event.clientX, event.clientY)
    if (!world) return

    if (event.pointerType === 'mouse') {
      this.target = this.options.clampTarget(world)
      return
    }

    if (this.drag?.pointerId !== event.pointerId) return
    const delta = world.clone().sub(this.drag.last).multiplyScalar(TOUCH_SENSITIVITY)
    this.drag.last = world
    const base = this.target ?? this.options.getShipPosition().clone()
    this.target = this.options.clampTarget(base.add(delta))
  }

  private handlePointerUp = (event: PointerEvent) => {
    if (this.drag?.pointerId === event.pointerId) this.drag = null
  }
}