Files
44r0n7 626879ed0c Add freshness bar, enhance overlays and renderers
- 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
2026-06-17 11:58:17 -04:00

275 lines
9.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ═══ 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);
}
},
};
// 10 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;
}
}
});
}