// ═══ input.js ═══ // ============================================================ // INPUT.JS — Keyboard hotkeys, mouse interaction // ============================================================ let _shiftHeld = false; document.addEventListener('keydown', e => { if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') _shiftHeld = true; }); document.addEventListener('keyup', e => { if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') _shiftHeld = false; }); let _altHeld = false; document.addEventListener('keydown', e => { if (e.code === 'AltLeft' || e.code === 'AltRight') { e.preventDefault(); _altHeld = true; } }); document.addEventListener('keyup', e => { if (e.code === 'AltLeft' || e.code === 'AltRight') _altHeld = false; }); window.addEventListener('blur', () => { _radialSlot = -1; _shiftHeld = false; _altHeld = false; _prestigeHoldMs = -1; }); document.addEventListener('visibilitychange', () => { if (document.hidden) { _radialSlot = -1; _shiftHeld = false; _altHeld = false; _prestigeHoldMs = -1; } }); const HOTKEYS = { 'Space': () => G.armoryOpen ? closeArmory() : openArmory(), 'KeyC': () => G.commandOpen ? closeCommand() : openCommand(), 'Escape': () => { if (G.weaponDetailSlot >= 0) closeWeaponDetail(); else if (typeof _radialSlot !== 'undefined' && _radialSlot >= 0) { _radialSlot = -1; } else if (document.body.classList.contains('inventory-open')) closeWeaponPicker(); else if (G.armoryOpen) closeArmory(); else if (G.commandOpen) closeCommand(); else if (G.threatOpen) closeThreatPanel(); else if (G.prestigeOpen) closePrestigeDialog(); else togglePause(); }, 'KeyP': () => { if (!G.armoryOpen && !G.commandOpen && !document.body.classList.contains('inventory-open')) togglePause(); }, 'KeyI': () => { if (!G.armoryOpen && !G.commandOpen) { document.body.classList.contains('inventory-open') ? closeWeaponPicker() : openWeaponPicker(-1); } }, }; // 1–0 keys for enemy deploy const ENEMY_HOTKEYS = ['Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9','Digit0']; function initInput() { window.addEventListener('wheel', e => { if (e.ctrlKey) e.preventDefault(); }, { passive: false }); document.addEventListener('keydown', e => { // Block all game input when dev console is open if (DEV_MODE && document.getElementById('dev-console').classList.contains('open')) return; // Enemy deploy hotkeys const idx = ENEMY_HOTKEYS.indexOf(e.code); if (idx >= 0 && idx < ENEMY_DEFS.length && !G.armoryOpen && !G.commandOpen && !G.paused) { e.preventDefault(); deployEnemy(ENEMY_DEFS[idx].id, G.sendQuantity); return; } const fn = HOTKEYS[e.code]; if (fn) { e.preventDefault(); fn(); } }); initCanvasMouse(); } // ── CANVAS MOUSE INTERACTION ────────────────────────────────── let _hitRegions = []; let _hoverPt = null; let _dragWeapon = null; let _dragSource = null; let _suppressNextClick = false; let _sellHoldSlot = -1; let _sellHoldMs = 0; const SELL_HOLD_DURATION = 1000; let _prestigeHoldMs = -1; const PRESTIGE_HOLD_DURATION = 1200; function clearHitRegions() { _hitRegions.length = 0; } function addHitRegion(x, y, w, h, action) { _hitRegions.push({ x, y, w, h, action }); } function isHovered(x, y, w, h) { return _hoverPt !== null && _hoverPt.x >= x && _hoverPt.x < x + w && _hoverPt.y >= y && _hoverPt.y < y + h; } // For elements drawn in world space inside the camera transform function isHoveredWorld(x, y, w, h) { if (!_hoverPt) return false; const wx = _hoverPt.worldX ?? _hoverPt.x; const wy = _hoverPt.worldY ?? _hoverPt.y; return wx >= x && wx < x + w && wy >= y && wy < y + h; } // Hit region stored in canvas (screen) space, converting from world space function addHitRegionWorld(x, y, w, h, action) { const zoom = G?.camera?.zoom ?? 1.0; const sx = (x - ARENA_CX) * zoom + ARENA_CX; const sy = (y - ARENA_CY) * zoom + ARENA_CY; _hitRegions.push({ x: sx, y: sy, w: w * zoom, h: h * zoom, action }); } function canvasPt(e) { const r = canvas.getBoundingClientRect(); const zoom = G?.camera?.zoom ?? 1.0; const cx = (e.clientX - r.left) * (GAME_W / r.width); const cy = (e.clientY - r.top) * (GAME_H / r.height); return { x: cx, y: cy, worldX: (cx - ARENA_CX) / zoom + ARENA_CX, worldY: (cy - ARENA_CY) / zoom + ARENA_CY, }; } function initCanvasMouse() { canvas.addEventListener('mousedown', e => { if (e.button !== 0) return; if (DEV_MODE && document.getElementById('dev-console').classList.contains('open')) return; const pt = canvasPt(e); for (const r of _dragRegions) { if (pt.x >= r.x && pt.x < r.x + r.w && pt.y >= r.y && pt.y < r.y + r.h) { _dragWeapon = r.weapon; _dragSource = r.source || null; return; } } // Sell hold detection if (_sellRegion && pt.x >= _sellRegion.x && pt.x < _sellRegion.x + _sellRegion.w && pt.y >= _sellRegion.y && pt.y < _sellRegion.y + _sellRegion.h) { _sellHoldSlot = _sellRegion.slot; _sellHoldMs = Date.now(); } // Prestige hold detection if (_prestigeHoldRegion && pt.x >= _prestigeHoldRegion.x && pt.x < _prestigeHoldRegion.x + _prestigeHoldRegion.w && pt.y >= _prestigeHoldRegion.y && pt.y < _prestigeHoldRegion.y + _prestigeHoldRegion.h) { _prestigeHoldMs = Date.now(); } }); canvas.addEventListener('mouseup', e => { // Cancel sell hold if released too early if (_sellHoldSlot >= 0 && Date.now() - _sellHoldMs < SELL_HOLD_DURATION) { _sellHoldSlot = -1; } // Cancel prestige hold if released too early if (_prestigeHoldMs > 0 && Date.now() - _prestigeHoldMs < PRESTIGE_HOLD_DURATION) { _prestigeHoldMs = -1; } if (!_dragWeapon) return; const pt = canvasPt(e); let dropped = false; if (_dragSource?.type === 'slot') { for (const zone of _bagDropZones) { if (pt.x >= zone.x && pt.x < zone.x + zone.w && pt.y >= zone.y && pt.y < zone.y + zone.h) { removeWeaponFromSlot(_dragSource.slotIndex); dropped = true; break; } } } else { for (const zone of _bagDropZones) { if (pt.x >= zone.x && pt.x < zone.x + zone.w && pt.y >= zone.y && pt.y < zone.y + zone.h) { dropped = true; break; } } } if (!dropped) { for (const zone of _mountDropZones) { const dx = (pt.worldX ?? pt.x) - zone.x, dy = (pt.worldY ?? pt.y) - zone.y; if (dx * dx + dy * dy <= zone.r * zone.r) { // Dropping back onto source slot = click, not drag — let the click handler fire if (_dragSource?.type === 'slot' && zone.slotIndex === _dragSource.slotIndex) break; equipWeaponInstanceToSlot(zone.slotIndex, _dragWeapon.instanceId); dropped = true; break; } } } if (dropped) _suppressNextClick = true; _dragWeapon = _dragSource = null; }); canvas.addEventListener('click', e => { if (_suppressNextClick) { _suppressNextClick = false; return; } if (DEV_MODE && document.getElementById('dev-console').classList.contains('open')) return; const pt = canvasPt(e); for (const r of _hitRegions) { if (pt.x >= r.x && pt.x < r.x + r.w && pt.y >= r.y && pt.y < r.y + r.h) { r.action(); return; } } }); canvas.addEventListener('mousemove', e => { _hoverPt = canvasPt(e); let pointer = false; for (const r of _hitRegions) { if (_hoverPt.x >= r.x && _hoverPt.x < r.x + r.w && _hoverPt.y >= r.y && _hoverPt.y < r.y + r.h) { pointer = true; break; } } if (_dragWeapon) pointer = true; canvas.style.cursor = pointer ? 'pointer' : 'default'; }); canvas.addEventListener('mouseleave', () => { _hoverPt = null; canvas.style.cursor = 'default'; }); canvas.addEventListener('wheel', e => { e.preventDefault(); if (document.body.classList.contains('inventory-open')) { _pickerScrollY = clamp(_pickerScrollY + e.deltaY * 0.5, 0, _pickerScrollMax); } else if (G.weaponDetailSlot >= 0) { _weaponDetailScrollY = clamp(_weaponDetailScrollY + e.deltaY * 0.5, 0, _weaponDetailScrollMax); } else if (G.armoryOpen || G.commandOpen) { _shopScrollY = clamp(_shopScrollY + e.deltaY * 0.5, 0, _shopScrollMax); } else if (_hoverPt && _hoverPt.x >= 1330) { const pt = _hoverPt; if (pt.y >= 112 && pt.y < 722) { // enemy cards area — scroll enemy list _sidePanelScrollY = clamp(_sidePanelScrollY + e.deltaY * 0.5, 0, _sidePanelScrollMax); } else if (pt.y >= 750 && pt.y < 900) { // combat log area _logScrollY = clamp(_logScrollY + e.deltaY * 0.5, 0, _logScrollMax); } else if (pt.y >= 64 && pt.y < 112) { // deploy header — cycle send quantity const steps = [1, 5, 10, 25, 50]; const idx = steps.indexOf(G.sendQuantity); G.sendQuantity = e.deltaY > 0 ? steps[Math.min(idx + 1, steps.length - 1)] : steps[Math.max(idx - 1, 0)]; } } else if (_hoverPt && _hoverPt.x < 1330 && G?.camera) { // arena area — zoom in/out const cam = G.camera; const delta = -e.deltaY * 0.0012; cam.zoom = Math.max(cam.minZoom, Math.min(cam.maxZoom, cam.zoom + delta)); } }, { passive: false }); canvas.addEventListener('contextmenu', e => { e.preventDefault(); if (!G.armoryOpen && !G.commandOpen && G.weaponDetailSlot < 0) return; const pt = canvasPt(e); for (const r of _shopRightClick) { if (pt.x >= r.x && pt.x < r.x + r.w && pt.y >= r.y && pt.y < r.y + r.h) { r.action(); return; } } }); }