626879ed0c
- Add enemy freshness tracking (novelty bonus for repeated deploys) - Add freshness bar to sidepanel enemy cards with penalty indicator - Major overhaul of renderer-overlays.js (790+ lines for UI polish) - Enhanced combat log, shop overlays, and inventory UI - Improved weapon/upgrade display with partial ownership colors - Added element icons and weakness/resistance indicators to cards - Enhanced radial menu and tooltip system - Add "stale/%" penalty text when freshness depleted - Update play link to ffazeshift.net in index.html
275 lines
9.7 KiB
JavaScript
275 lines
9.7 KiB
JavaScript
// ═══ 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;
|
||
}
|
||
}
|
||
});
|
||
}
|