astara / src /input.js
system's picture
system HF Staff
77dd24d: A source check that checks, and a dev server that answers
45cc183 verified
Raw History Blame Contribute Delete
7.36 kB
// 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 };
}