// ═══ input.js ═══ // ============================================================ // INPUT.JS — Keyboard hotkeys, mouse interaction // ============================================================ const HOTKEYS = { 'Space': () => G.shopOpen ? closeShop() : openShop(), 'Escape': () => { if (document.body.classList.contains('inventory-open')) closeWeaponPicker(); else if (G.shopOpen) closeShop(); else togglePause(); }, 'KeyP': () => { if (!G.shopOpen && !document.body.classList.contains('inventory-open')) togglePause(); }, 'KeyI': () => { if (!G.shopOpen) document.body.classList.contains('inventory-open') ? closeWeaponPicker() : openWeaponPicker(-1); }, 'Tab': () => { if (G.shopOpen) cycleShopTab(); }, }; // 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.shopOpen && !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; 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; } function canvasPt(e) { const r = canvas.getBoundingClientRect(); return { x: (e.clientX - r.left) * (GAME_W / r.width), y: (e.clientY - r.top) * (GAME_H / r.height), }; } 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; } } }); canvas.addEventListener('mouseup', e => { 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.x - zone.x, dy = pt.y - zone.y; if (dx * dx + dy * dy <= zone.r * zone.r) { 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.shopOpen) { _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)]; } } }, { passive: false }); canvas.addEventListener('contextmenu', e => { e.preventDefault(); if (!G.shopOpen) 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; } } }); } function cycleShopTab() { const weaponTabs = getEquippedWeapons().map(w => w.instanceId); const tabs = ['tower', 'weapons', ...weaponTabs]; const idx = tabs.indexOf(G.shopTab); G.shopTab = tabs[(Math.max(0, idx) + 1) % tabs.length]; _shopScrollY = 0; }