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:
+195
-18
@@ -10,6 +10,93 @@
|
||||
// ── HUD ───────────────────────────────────────────────────────
|
||||
const HUD_H = 64;
|
||||
|
||||
// ── TOOLTIP ───────────────────────────────────────────────────
|
||||
const _TT_DELAY = 700; // ms of continuous hover before tooltip shows
|
||||
let _ttTarget = null; // button key currently hovered
|
||||
let _ttHoverMs = 0; // timestamp when hover on _ttTarget began
|
||||
let _ttBx = 0, _ttBy = 0, _ttBw = 0, _ttBh = 0;
|
||||
|
||||
function _tickTooltip(key, x, y, w, h) {
|
||||
if (isHovered(x, y, w, h)) {
|
||||
if (_ttTarget !== key) { _ttTarget = key; _ttHoverMs = Date.now(); _ttBx = x; _ttBy = y; _ttBw = w; _ttBh = h; }
|
||||
} else if (_ttTarget === key) {
|
||||
_ttTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
function _drawHudTooltip(key, W, H) {
|
||||
// [description, hotkey | null]
|
||||
const tips = {
|
||||
armory: ['Buy and upgrade weapons', 'Space'],
|
||||
command: ['Upgrade tower hull, armor & more', 'C'],
|
||||
threat: ['Change enemy difficulty tier', null],
|
||||
prestige: ['Reset for permanent bonuses', null],
|
||||
inventory: ['Mount and manage weapons', 'I'],
|
||||
rdown: ['Decrease credit reserve', null],
|
||||
rup: ['Increase credit reserve', null],
|
||||
qty: ['Click to cycle qty · scroll adjust', null],
|
||||
};
|
||||
const tip = tips[key];
|
||||
if (!tip) return;
|
||||
const [desc, hotkey] = tip;
|
||||
|
||||
ctx.save();
|
||||
ctx.letterSpacing = '0px';
|
||||
|
||||
ctx.font = '11px "Share Tech Mono", monospace';
|
||||
const descW = ctx.measureText(desc).width;
|
||||
|
||||
const KEY_PAD = 10, KEY_H = 18;
|
||||
let keyBadgeW = 0;
|
||||
if (hotkey) {
|
||||
ctx.font = '10px Orbitron, monospace';
|
||||
keyBadgeW = ctx.measureText(hotkey).width + KEY_PAD * 2;
|
||||
}
|
||||
|
||||
const H_PAD = 12, GAP = hotkey ? 8 : 0;
|
||||
const TW = H_PAD * 2 + descW + GAP + keyBadgeW;
|
||||
const TH = 28;
|
||||
|
||||
const isBottom = _ttBy > H / 2;
|
||||
const tx = Math.min(Math.max(_ttBx + _ttBw / 2 - TW / 2, 8), W - TW - 8);
|
||||
const ty = isBottom ? _ttBy - TH - 6 : _ttBy + _ttBh + 6;
|
||||
|
||||
// Background pill
|
||||
ctx.fillStyle = 'rgba(4,10,18,0.97)';
|
||||
ctx.strokeStyle = '#1a4060';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillRect(tx, ty, TW, TH);
|
||||
ctx.strokeRect(tx, ty, TW, TH);
|
||||
|
||||
// Description
|
||||
ctx.font = '11px "Share Tech Mono", monospace';
|
||||
ctx.fillStyle = '#b8d8e8';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(desc, tx + H_PAD, ty + TH / 2);
|
||||
|
||||
// Keycap badge
|
||||
if (hotkey) {
|
||||
const bx = tx + H_PAD + descW + GAP;
|
||||
const by = ty + (TH - KEY_H) / 2;
|
||||
ctx.fillStyle = '#060e18';
|
||||
ctx.strokeStyle = '#00d4ff66';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillRect(bx, by, keyBadgeW, KEY_H);
|
||||
ctx.strokeRect(bx, by, keyBadgeW, KEY_H);
|
||||
// Inner highlight line at top (keycap bevel)
|
||||
ctx.strokeStyle = '#00d4ff33';
|
||||
ctx.beginPath(); ctx.moveTo(bx + 2, by + 2); ctx.lineTo(bx + keyBadgeW - 2, by + 2); ctx.stroke();
|
||||
ctx.font = '10px Orbitron, monospace';
|
||||
ctx.fillStyle = '#00d4ff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(hotkey, bx + keyBadgeW / 2, by + KEY_H / 2);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Right-section column centers (canvas x coords, 1600px wide)
|
||||
const _HUD_KILLS_CX = 1542;
|
||||
const _HUD_SCORE_CX = 1468;
|
||||
@@ -19,7 +106,7 @@ const _HUD_DIV2_X = 1204;
|
||||
const _HUD_CRED_CX = 1118;
|
||||
|
||||
function drawHUD() {
|
||||
const W = canvas.width;
|
||||
const W = canvas.width, H = canvas.height;
|
||||
const cheapest = cheapestEnemyCost();
|
||||
|
||||
// ── Background strip ─────────────────────────────────────────
|
||||
@@ -44,28 +131,50 @@ function drawHUD() {
|
||||
const titleW = ctx.measureText('SIEGE PROTOCOL').width;
|
||||
ctx.letterSpacing = '0px';
|
||||
|
||||
// ── LEFT: shop button ────────────────────────────────────────
|
||||
const SBX = 20 + titleW + 16;
|
||||
const SBY = 14;
|
||||
const SBH = 36;
|
||||
// ── LEFT: THREAT, PRESTIGE buttons ───────────────────────────
|
||||
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
|
||||
const shopLabel = '⚙ SHOP [Space]';
|
||||
const SBW = ctx.measureText(shopLabel).width + 32;
|
||||
const shopHov = isHovered(SBX, SBY, SBW, SBH);
|
||||
const tierDef = DIFFICULTY_TIERS[G.difficultyTier || 0];
|
||||
const tierName = tierDef?.name ?? 'NORMAL';
|
||||
const prestige = G.prestigeLevel || 0;
|
||||
const tierActive = (G.difficultyTier || 0) > 0;
|
||||
|
||||
ctx.fillStyle = shopHov ? '#00d4ff' : 'transparent';
|
||||
ctx.strokeStyle = '#00d4ff';
|
||||
// THREAT button
|
||||
const threatLabel = '⚠ ' + tierName;
|
||||
const TH_BW = Math.ceil(ctx.measureText(threatLabel).width) + 28;
|
||||
const TH_BX = 20 + titleW + 16;
|
||||
const TH_BY = 14, TH_BH = 36;
|
||||
const threatHov = isHovered(TH_BX, TH_BY, TH_BW, TH_BH);
|
||||
const threatOn = G.threatOpen;
|
||||
ctx.fillStyle = (threatHov || threatOn) ? (tierActive ? '#ff6b35' : '#00d4ff') : 'transparent';
|
||||
ctx.strokeStyle = tierActive ? '#ff6b35' : (threatOn ? '#00d4ff' : '#3a5060');
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.rect(SBX, SBY, SBW, SBH); ctx.fill(); ctx.stroke();
|
||||
|
||||
if (shopHov) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 16; }
|
||||
ctx.fillStyle = shopHov ? '#000000' : '#00d4ff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(shopLabel, SBX + SBW / 2, SBY + SBH / 2);
|
||||
ctx.beginPath(); ctx.rect(TH_BX, TH_BY, TH_BW, TH_BH); ctx.fill(); ctx.stroke();
|
||||
if (threatHov || threatOn) { ctx.shadowColor = tierActive ? '#ff6b35' : '#00d4ff'; ctx.shadowBlur = 12; }
|
||||
ctx.fillStyle = (threatHov || threatOn) ? '#000000' : (tierActive ? '#ff6b35' : '#3a6080');
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText(threatLabel, TH_BX + TH_BW / 2, TH_BY + TH_BH / 2);
|
||||
ctx.shadowBlur = 0;
|
||||
addHitRegion(TH_BX, TH_BY, TH_BW, TH_BH, () => G.threatOpen ? closeThreatPanel() : openThreatPanel());
|
||||
_tickTooltip('threat', TH_BX, TH_BY, TH_BW, TH_BH);
|
||||
|
||||
addHitRegion(SBX, SBY, SBW, SBH, () => G.shopOpen ? closeShop() : openShop());
|
||||
// PRESTIGE button
|
||||
const prestigeLabel = prestige > 0 ? ('✶P' + prestige + ' PRESTIGE') : 'PRESTIGE';
|
||||
const PS_BX = TH_BX + TH_BW + 8;
|
||||
const PS_BW = Math.ceil(ctx.measureText(prestigeLabel).width) + 28;
|
||||
const PS_BY = 14, PS_BH = 36;
|
||||
const prestigeHov = isHovered(PS_BX, PS_BY, PS_BW, PS_BH);
|
||||
const prestigeOn = G.prestigeOpen;
|
||||
ctx.fillStyle = (prestigeHov || prestigeOn) ? '#c77dff' : 'transparent';
|
||||
ctx.strokeStyle = prestige > 0 ? '#c77dff' : (prestigeOn ? '#c77dff' : '#3a4060');
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.rect(PS_BX, PS_BY, PS_BW, PS_BH); ctx.fill(); ctx.stroke();
|
||||
if (prestigeHov || prestigeOn) { ctx.shadowColor = '#c77dff'; ctx.shadowBlur = 12; }
|
||||
ctx.fillStyle = (prestigeHov || prestigeOn) ? '#000000' : (prestige > 0 ? '#c77dff' : '#3a4060');
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText(prestigeLabel, PS_BX + PS_BW / 2, PS_BY + PS_BH / 2);
|
||||
ctx.shadowBlur = 0;
|
||||
addHitRegion(PS_BX, PS_BY, PS_BW, PS_BH, () => G.prestigeOpen ? closePrestigeDialog() : openPrestigeDialog());
|
||||
_tickTooltip('prestige', PS_BX, PS_BY, PS_BW, PS_BH);
|
||||
|
||||
// ── CENTER: HP bar ───────────────────────────────────────────
|
||||
const CX = W / 2;
|
||||
@@ -179,6 +288,8 @@ function drawHUD() {
|
||||
|
||||
if (!rDownDis) addHitRegion(rDownX, RBY, RBW, RBH, () => adjustReserve(-10));
|
||||
if (!rUpDis) addHitRegion(rUpX, RBY, RBW, RBH, () => adjustReserve(10));
|
||||
_tickTooltip('rdown', rDownX, RBY, RBW, RBH);
|
||||
_tickTooltip('rup', rUpX, RBY, RBW, RBH);
|
||||
|
||||
hudDivider(_HUD_DIV2_X);
|
||||
|
||||
@@ -200,9 +311,75 @@ function drawHUD() {
|
||||
ctx.fillText(G.credits + '¢', CRED, HUD_H - 6);
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// ── BOTTOM-LEFT: ARMORY button ─────────────────────────────
|
||||
const BTN_H = 34;
|
||||
const BTN_Y = H - 14 - BTN_H;
|
||||
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
|
||||
const armoryLabel = '⚙ ARMORY';
|
||||
const ARM_BW = Math.ceil(ctx.measureText(armoryLabel).width) + 32;
|
||||
const ARM_BX = 20, ARM_BY = BTN_Y;
|
||||
const armoryHov = isHovered(ARM_BX, ARM_BY, ARM_BW, BTN_H);
|
||||
const armoryOn = G.armoryOpen;
|
||||
ctx.fillStyle = (armoryHov || armoryOn) ? '#00d4ff' : 'transparent';
|
||||
ctx.strokeStyle = '#00d4ff'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.rect(ARM_BX, ARM_BY, ARM_BW, BTN_H); ctx.fill(); ctx.stroke();
|
||||
if (armoryHov || armoryOn) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 16; }
|
||||
ctx.fillStyle = (armoryHov || armoryOn) ? '#000000' : '#00d4ff';
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText(armoryLabel, ARM_BX + ARM_BW / 2, ARM_BY + BTN_H / 2);
|
||||
ctx.shadowBlur = 0;
|
||||
// Don't register when open — overlay is full-screen and its regions must take priority
|
||||
if (!armoryOn) addHitRegion(ARM_BX, ARM_BY, ARM_BW, BTN_H, openArmory);
|
||||
_tickTooltip('armory', ARM_BX, ARM_BY, ARM_BW, BTN_H);
|
||||
|
||||
// ── BOTTOM-LEFT: COMMAND button ────────────────────────────
|
||||
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
|
||||
const cmdLabel = '🏰 COMMAND';
|
||||
const CMD_BW = Math.ceil(ctx.measureText(cmdLabel).width) + 28;
|
||||
const CMD_BX = ARM_BX + ARM_BW + 8, CMD_BY = BTN_Y;
|
||||
const cmdHov = isHovered(CMD_BX, CMD_BY, CMD_BW, BTN_H);
|
||||
const cmdOn = G.commandOpen;
|
||||
ctx.fillStyle = (cmdHov || cmdOn) ? '#00d4ff' : 'transparent';
|
||||
ctx.strokeStyle = cmdOn ? '#00d4ff' : '#1a4060'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.rect(CMD_BX, CMD_BY, CMD_BW, BTN_H); ctx.fill(); ctx.stroke();
|
||||
if (cmdHov || cmdOn) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 16; }
|
||||
ctx.fillStyle = (cmdHov || cmdOn) ? '#000000' : '#3a8090';
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText(cmdLabel, CMD_BX + CMD_BW / 2, CMD_BY + BTN_H / 2);
|
||||
ctx.shadowBlur = 0;
|
||||
if (!cmdOn) addHitRegion(CMD_BX, CMD_BY, CMD_BW, BTN_H, openCommand);
|
||||
_tickTooltip('command', CMD_BX, CMD_BY, CMD_BW, BTN_H);
|
||||
|
||||
// ── BOTTOM-RIGHT: INVENTORY button ─────────────────────────
|
||||
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
|
||||
const invLabel = 'INVENTORY';
|
||||
const INV_BW = Math.ceil(ctx.measureText(invLabel).width) + 28;
|
||||
const INV_BH = BTN_H;
|
||||
const INV_BX = (W - SP_W) - 14 - INV_BW;
|
||||
const INV_BY = BTN_Y;
|
||||
const invOpen = document.body.classList.contains('inventory-open');
|
||||
const invHov = isHovered(INV_BX, INV_BY, INV_BW, INV_BH);
|
||||
ctx.fillStyle = (invHov || invOpen) ? '#00d4ff' : 'transparent';
|
||||
ctx.strokeStyle = invOpen ? '#00d4ff' : '#1a4060'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.rect(INV_BX, INV_BY, INV_BW, INV_BH); ctx.fill(); ctx.stroke();
|
||||
if (invHov || invOpen) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 12; }
|
||||
ctx.fillStyle = (invHov || invOpen) ? '#000000' : '#3a6080';
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText(invLabel, INV_BX + INV_BW / 2, INV_BY + INV_BH / 2);
|
||||
ctx.shadowBlur = 0;
|
||||
if (!invOpen) addHitRegion(INV_BX, INV_BY, INV_BW, INV_BH, () => openWeaponPicker(-1));
|
||||
_tickTooltip('inventory', INV_BX, INV_BY, INV_BW, INV_BH);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Called last in render() so tooltips always appear on top
|
||||
function drawTooltips() {
|
||||
if (_ttTarget && Date.now() - _ttHoverMs >= _TT_DELAY) {
|
||||
_drawHudTooltip(_ttTarget, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// ── BROKE WARNING ─────────────────────────────────────────────
|
||||
function drawBrokeWarning() {
|
||||
const cheapest = cheapestEnemyCost();
|
||||
|
||||
Reference in New Issue
Block a user