File size: 3,238 Bytes
a1c7c3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// Keyboard and touch input, translated into game intents.

const KEY_MAP = {
  ArrowLeft: 'left',
  KeyA: 'left',
  ArrowRight: 'right',
  KeyD: 'right',
  ArrowUp: 'jump',
  KeyW: 'jump',
  Space: 'jump',
  ShiftLeft: 'boost',
  ShiftRight: 'boost',
  KeyE: 'edge',
  Enter: 'confirm',
  NumpadEnter: 'confirm',
  KeyP: 'pause',
  Escape: 'pause',
  KeyM: 'mute',
};

export class Input {
  constructor(target = window) {
    this.target = target;
    this.pressed = new Set();
    this.held = new Set();
    this.muted = false;
    this.onFirstGesture = null;

    this.keydown = (e) => {
      const action = KEY_MAP[e.code];
      if (!action) return;
      // Stop the page scrolling under the game.
      if (e.code === 'Space' || e.code.startsWith('Arrow')) e.preventDefault();
      if (!this.held.has(action)) this.pressed.add(action);
      this.held.add(action);
      this.#gesture();
    };
    this.keyup = (e) => {
      const action = KEY_MAP[e.code];
      if (action) this.held.delete(action);
    };
    this.blur = () => this.held.clear();

    this.touchstart = (e) => {
      const t = e.changedTouches[0];
      this.touchOrigin = { x: t.clientX, y: t.clientY, time: performance.now() };
      this.#gesture();
    };
    this.touchend = (e) => {
      if (!this.touchOrigin) return;
      const t = e.changedTouches[0];
      const dx = t.clientX - this.touchOrigin.x;
      const dy = t.clientY - this.touchOrigin.y;
      const dt = performance.now() - this.touchOrigin.time;
      const THRESHOLD = 34;
      if (Math.abs(dx) < THRESHOLD && Math.abs(dy) < THRESHOLD) {
        if (dt < 400) this.pressed.add('jump');
      } else if (Math.abs(dx) > Math.abs(dy)) {
        this.pressed.add(dx > 0 ? 'right' : 'left');
      } else if (dy < 0) {
        this.pressed.add('jump');
      } else {
        this.pressed.add('edge');
      }
      this.touchOrigin = null;
    };

    this.pointerdown = () => this.#gesture();

    target.addEventListener('keydown', this.keydown);
    target.addEventListener('keyup', this.keyup);
    target.addEventListener('blur', this.blur);
    target.addEventListener('touchstart', this.touchstart, { passive: true });
    target.addEventListener('touchend', this.touchend, { passive: true });
    target.addEventListener('pointerdown', this.pointerdown);
  }

  #gesture() {
    if (!this.gestured) {
      this.gestured = true;
      this.onFirstGesture?.();
    }
  }

  // True once per press.
  consume(action) {
    if (!this.pressed.has(action)) return false;
    this.pressed.delete(action);
    return true;
  }

  isHeld(action) {
    return this.held.has(action);
  }

  // Drop queued presses that were not handled this frame so they cannot fire
  // late, for example a jump buffered while the game over screen was up.
  endFrame() {
    this.pressed.clear();
  }

  destroy() {
    this.target.removeEventListener('keydown', this.keydown);
    this.target.removeEventListener('keyup', this.keyup);
    this.target.removeEventListener('blur', this.blur);
    this.target.removeEventListener('touchstart', this.touchstart);
    this.target.removeEventListener('touchend', this.touchend);
    this.target.removeEventListener('pointerdown', this.pointerdown);
  }
}