// Gamepad input. // ---------- Gamepad ---------- const PAD = { prev: [], active: false, menuT: 0 }; function padPressed(gp, b) { const v = gp.buttons[b] && gp.buttons[b].pressed; const was = PAD.prev[b]; return v && !was; } function pollPad() { const gps = navigator.getGamepads ? navigator.getGamepads() : []; let gp = null; for (const g of gps) if (g && g.connected) { gp = g; break; } if (!gp) return { mx: 0, my: 0 }; const dz = SET.deadzone; const ax = v => Math.abs(v) < dz ? 0 : (v - Math.sign(v) * dz) / (1 - dz); const lx = ax(gp.axes[0] || 0), ly = ax(gp.axes[1] || 0), rx = ax(gp.axes[2] || 0), ry = ax(gp.axes[3] || 0); const pressedAny = gp.buttons.some(b => b && b.pressed) || lx || ly || rx || ry; if (pressedAny) GAME.usingPad = true; const modal = topModal(); if (modal || !GAME.running) { const now = performance.now(); const up = padPressed(gp, 12) || (ly < -0.6 && now > PAD.menuT), down = padPressed(gp, 13) || (ly > 0.6 && now > PAD.menuT); const left = padPressed(gp, 14) || (lx < -0.6 && now > PAD.menuT), right = padPressed(gp, 15) || (lx > 0.6 && now > PAD.menuT); if (up || down || left || right) PAD.menuT = now + 220; const root = modal ? $(modal) : $('title'); const focusables = [...root.querySelectorAll('button:not([disabled]), select, input')].filter(e => e.offsetParent !== null); const cur = document.activeElement; let idx = focusables.indexOf(cur); if (up || down) { idx = idx < 0 ? 0 : (idx + (down ? 1 : -1) + focusables.length) % focusables.length; focusables[idx] && focusables[idx].focus(); } if ((left || right) && cur) { if (cur.type === 'range') { const st = parseFloat(cur.step) || 1; cur.value = Math.max(+cur.min, Math.min(+cur.max, parseFloat(cur.value) + (right ? st : -st))); cur.dispatchEvent(new Event('input')); } else if (cur.tagName === 'SELECT') { cur.selectedIndex = Math.max(0, Math.min(cur.options.length - 1, cur.selectedIndex + (right ? 1 : -1))); cur.dispatchEvent(new Event('change')); } else if (cur.closest('.tabs')) { const tb = [...cur.parentNode.children]; const k = tb.indexOf(cur); const n = tb[(k + (right ? 1 : -1) + tb.length) % tb.length]; n.click(); setTimeout(() => { const t2 = root.querySelector('.tabs .on'); t2 && t2.focus(); }, 0); } } if (padPressed(gp, 0) && cur && focusables.includes(cur)) cur.click(); if (padPressed(gp, 1) && modal) closeModal(modal); if (padPressed(gp, 9) && modal === 'm-pause') closeModal(modal); PAD.prev = gp.buttons.map(b => b && b.pressed); return { mx: 0, my: 0 }; } const mag = Math.hypot(rx, ry); const reachNow = toolId() === 'drill' ? drillReach(GAME.drillW) : REACH; if (mag > 0.05) { GAME.padAim.x = rx / Math.max(1, mag) * reachNow * SET.aimSpeed * Math.min(1, mag); GAME.padAim.y = ry / Math.max(1, mag) * reachNow * SET.aimSpeed * Math.min(1, mag); } if (padPressed(gp, 4)) selectTool((GAME.tool + TOOLS.length - 1) % TOOLS.length); if (padPressed(gp, 5)) selectTool((GAME.tool + 1) % TOOLS.length); if (padPressed(gp, 14)) cycleItem(-1); if (padPressed(gp, 15)) cycleItem(1); if (padPressed(gp, 12)) setBrush(1); if (padPressed(gp, 13)) setBrush(-1); if (padPressed(gp, 0)) doInteract(); if (padPressed(gp, 2)) rotate(); if (padPressed(gp, 3)) toggleLens(); if (padPressed(gp, 1)) { if (GAME.selDrag) { GAME.selDrag = null; hudDirty = true; } else if (GAME.drag) GAME.drag = null; else if (GAME.paste) { GAME.paste = false; hudDirty = true; } else if (toolId() === 'vacuum' && GAME.blow.size) { GAME.blow.clear(); lastBagKey = ''; hudDirty = true; playSfx('ui'); } } if (padPressed(gp, 10)) bot.lamp = !bot.lamp; if (padPressed(gp, 11)) { SET.zoom = SET.zoom >= 10 ? 2 : SET.zoom + 2; saveSettings(); resizeRender(); R.snapNext = true; } if (padPressed(gp, 8)) openNotebook(); if (padPressed(gp, 9)) openPause(); const rt = gp.buttons[7] && (gp.buttons[7].pressed || gp.buttons[7].value > 0.4), lt = gp.buttons[6] && (gp.buttons[6].pressed || gp.buttons[6].value > 0.4); if (GAME.usingPad) { if (rt && !GAME.lmb) { GAME.lmb = true; primaryPress(false); } else if (!rt && GAME.lmb) { primaryRelease(false); GAME.lmb = false; } if (lt && !GAME.rmb) { GAME.rmb = true; secondaryPress(); } else if (!lt && GAME.rmb) { GAME.rmb = false; endRmbRecord(); } } PAD.prev = gp.buttons.map(b => b && b.pressed); return { mx: lx, my: ly }; }