File size: 7,362 Bytes
020d2dc
71eae78
 
 
 
 
 
0b13ced
 
 
 
71eae78
 
 
ec98146
 
 
 
 
 
 
020d2dc
 
71eae78
0b13ced
020d2dc
 
 
 
 
ec98146
 
020d2dc
 
71eae78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2428d78
 
0b13ced
2428d78
71eae78
 
 
 
020d2dc
 
 
 
71eae78
 
 
 
 
 
 
 
0b13ced
71eae78
020d2dc
 
 
 
 
ec98146
 
 
020d2dc
 
 
 
 
 
 
 
 
 
 
71eae78
 
 
 
ec98146
 
71eae78
ec98146
0b13ced
71eae78
 
 
 
020d2dc
 
 
 
 
 
ec98146
 
 
 
 
 
 
 
0b13ced
ec98146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d93bf0
6f7f7b5
7d93bf0
6f7f7b5
 
 
 
 
 
45cc183
 
 
 
7d93bf0
1c99eda
ec98146
6f7f7b5
 
 
 
ec98146
 
020d2dc
 
 
 
 
ec98146
020d2dc
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// Torch is a HOLD, never a toggle. The cost has to be continuous.
//
//   A / Left arrow    walk left
//   D / Right arrow   walk right
//   Space (hold)      torch up, sword away, and he cannot move
//   F                 swing
//   E                 take the object lying at his feet
//   Q (hold)          his loose pages. Raises the torch with it — one button, one cost.
//                     It was called the map until somebody played it and said
//                     "I thought the first page was an inventory index" — which
//                     it should have been, and now is.
//   R                 reset
//   `                 toggle the debug readout (handled in the renderer)
//
// On a touch screen there is no keyboard, so there is an on-screen pad in
// index.html: walk on the left, act on the right. Screen ZONES were tried first
// and could not carry it — the game needs two holds at once (walking and the
// torch) plus two taps, and a zone cannot tell you which one a thumb meant.
//
// Every button is its own element with its own captured pointer, so holding
// "walk right" and stabbing SWING with the other thumb both land.

export function createInput(target, { onReset } = {}) {
  // `attack` and `pickup` are one-shots: the scene consumes them and clears them.
  const state = { torch: false, attack: false, pickup: false, sheaf: false, left: false, right: false };

  // Track the two hold sources separately so releasing the mouse does not cancel
  // a key that is still held, and vice versa.
  let keyHeld = false;
  let pointerHeld = false;
  let padHeld = false;
  const sync = () => { state.torch = keyHeld || pointerHeld || padHeld; };

  const onKeyDown = (e) => {
    switch (e.code) {
      case 'Space':
        if (e.repeat) return;
        e.preventDefault();
        keyHeld = true;
        sync();
        break;
      case 'KeyA': case 'ArrowLeft':
        e.preventDefault();
        state.left = true;
        break;
      case 'KeyD': case 'ArrowRight':
        e.preventDefault();
        state.right = true;
        break;
      case 'KeyF':
        if (!e.repeat) state.attack = true;
        break;
      case 'KeyE':
        if (!e.repeat) state.pickup = true;
        break;
      case 'KeyQ':
        e.preventDefault();
        state.sheaf = true;
        break;
      case 'KeyR':
        onReset?.();
        break;
      default:
    }
  };

  const onKeyUp = (e) => {
    switch (e.code) {
      case 'Space':
        e.preventDefault();
        keyHeld = false;
        sync();
        break;
      case 'KeyA': case 'ArrowLeft': state.left = false; break;
      case 'KeyD': case 'ArrowRight': state.right = false; break;
      case 'KeyQ': state.sheaf = false; break;
      default:
    }
  };

  const onPointerDown = (e) => {
    if (e.button !== undefined && e.button !== 0) return;
    // A finger on the scene does nothing; the pad owns touch. Otherwise a thumb
    // resting anywhere would silently hold the torch up.
    if (e.pointerType === 'touch') return;
    pointerHeld = true;
    sync();
    // Capture keeps a drag outside the canvas counted as still held. It throws
    // for a pointer id the browser does not recognise, which never matters to
    // the demo, so it is not worth failing the handler over.
    try { target.setPointerCapture?.(e.pointerId); } catch { /* not capturable */ }
  };

  const onPointerUp = () => { pointerHeld = false; sync(); };

  // Losing focus mid-hold would otherwise strand the torch in the up position,
  // or leave him walking forever, because the keyup never arrives.
  const releaseAll = () => {
    keyHeld = false;
    pointerHeld = false;
    padHeld = false;
    for (const b of document.querySelectorAll('#pad button.on')) b.classList.remove('on');
    state.attack = false;
    state.pickup = false;
    state.sheaf = false;
    state.left = false;
    state.right = false;
    sync();
  };

  window.addEventListener('keydown', onKeyDown);
  window.addEventListener('keyup', onKeyUp);
  window.addEventListener('blur', releaseAll);
  document.addEventListener('visibilitychange', () => { if (document.hidden) releaseAll(); });

  // --- the touch pad ----------------------------------------------------
  const pad = document.getElementById('pad');
  if (pad) {
    const press = (act, down) => {
      switch (act) {
        case 'left':  state.left = down; break;
        case 'right': state.right = down; break;
        case 'torch': padHeld = down; sync(); break;
        case 'sheaf': state.sheaf = down; break;
        // One-shots. Fire on the way down so the swing is not waiting for a
        // thumb to lift, which reads as lag on a control you are mashing.
        case 'attack': if (down) state.attack = true; break;
        case 'pickup': if (down) state.pickup = true; break;
        default:
      }
    };
    for (const btn of pad.querySelectorAll('button')) {
      const act = btn.dataset.act;
      btn.addEventListener('pointerdown', (e) => {
        e.preventDefault();
        btn.classList.add('on');
        // Capture, so sliding a thumb off the button still counts as held and
        // still delivers the pointerup. Without it a walk can strand itself on.
        try { btn.setPointerCapture?.(e.pointerId); } catch { /* not capturable */ }
        press(act, true);
      });
      const release = (e) => {
        if (e) e.preventDefault();
        btn.classList.remove('on');
        press(act, false);
      };
      btn.addEventListener('pointerup', release);
      btn.addEventListener('pointercancel', release);
      // Belt and braces for engines that drop a capture on scroll or a call.
      btn.addEventListener('lostpointercapture', release);
      btn.addEventListener('contextmenu', (e) => e.preventDefault());
    }

    const show = () => { pad.hidden = false; };
    if (window.matchMedia?.('(hover: none) and (pointer: coarse)')?.matches) show();
    // Some devices report a fine pointer and still only have a finger.
    window.addEventListener('touchstart', show, { once: true, passive: true });
  }

  /**
   * Lights TAKE only when there is something to do with it.
   *
   * ONE BUTTON, ONE SLOT, NO LABEL CHANGE. It used to be two: TAKE on the left
   * wheel and a second MARK button over by the walk arrows, because pinning a
   * page needed PAGES held and TAKE pressed and one thumb cannot do both.
   *
   * The top page of the pile is now what he is holding, so TAKE acts on it
   * wherever he stands and the second button is deleted. Whether this press
   * pins a page, takes one back down, takes a key or picks something off the
   * ground is decided by where he is standing — which is what TAKE has meant
   * everywhere else in this game since the first object. Letters are not on
   * that list: they stay on the door.
   */
  const takeBtn = pad?.querySelector('.wheel [data-act="pickup"]');
  let takeLive = false;
  const setTakeAvailable = (yes) => {
    if (!takeBtn || yes === takeLive) return;
    takeLive = yes;
    takeBtn.classList.toggle('live', yes);
  };

  target.addEventListener('pointerdown', onPointerDown);
  window.addEventListener('pointerup', onPointerUp);
  window.addEventListener('pointercancel', onPointerUp);
  target.addEventListener('contextmenu', (e) => e.preventDefault());

  return { state, setTakeAvailable };
}