Spaces:
Running
Running
Download src/input.js from cloudronin/astara: direct link, hf CLI and curl.
- Browser
- Download file 7.36 kB
-
https://huggingface.co/spaces/cloudronin/astara/resolve/main/src/input.js
- Command line
-
hf download hf://spaces/cloudronin/astara/src/input.js
-
curl -L -o input.js https://huggingface.co/spaces/cloudronin/astara/resolve/main/src/input.js
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 }; | |
| } | |