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
This commit is contained in:
2026-06-17 11:58:17 -04:00
parent 6a710c3f03
commit 626879ed0c
21 changed files with 1884 additions and 312 deletions
+99 -20
View File
@@ -3,16 +3,48 @@
// 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.shopOpen ? closeShop() : openShop(),
'Space': () => G.armoryOpen ? closeArmory() : openArmory(),
'KeyC': () => G.commandOpen ? closeCommand() : openCommand(),
'Escape': () => {
if (document.body.classList.contains('inventory-open')) closeWeaponPicker();
else if (G.shopOpen) closeShop();
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.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(); },
'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
@@ -27,7 +59,7 @@ function initInput() {
// Enemy deploy hotkeys
const idx = ENEMY_HOTKEYS.indexOf(e.code);
if (idx >= 0 && idx < ENEMY_DEFS.length && !G.shopOpen && !G.paused) {
if (idx >= 0 && idx < ENEMY_DEFS.length && !G.armoryOpen && !G.commandOpen && !G.paused) {
e.preventDefault();
deployEnemy(ENEMY_DEFS[idx].id, G.sendQuantity);
return;
@@ -46,6 +78,12 @@ 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; }
@@ -59,11 +97,32 @@ function isHovered(x, y, w, h) {
_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: (e.clientX - r.left) * (GAME_W / r.width),
y: (e.clientY - r.top) * (GAME_H / r.height),
x: cx,
y: cy,
worldX: (cx - ARENA_CX) / zoom + ARENA_CX,
worldY: (cy - ARENA_CY) / zoom + ARENA_CY,
};
}
@@ -79,9 +138,28 @@ function initCanvasMouse() {
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;
@@ -105,8 +183,10 @@ function initCanvasMouse() {
if (!dropped) {
for (const zone of _mountDropZones) {
const dx = pt.x - zone.x, dy = pt.y - zone.y;
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;
@@ -153,7 +233,9 @@ function initCanvasMouse() {
e.preventDefault();
if (document.body.classList.contains('inventory-open')) {
_pickerScrollY = clamp(_pickerScrollY + e.deltaY * 0.5, 0, _pickerScrollMax);
} else if (G.shopOpen) {
} 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;
@@ -171,12 +253,17 @@ function initCanvasMouse() {
? 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.shopOpen) return;
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) {
@@ -185,11 +272,3 @@ function initCanvasMouse() {
}
});
}
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;
}