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([ ['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() 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 } }