// 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 }; }