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
+59 -15
View File
@@ -50,10 +50,11 @@ function drawSidePanel() {
ctx.font = '13px Orbitron, monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#ffd700';
ctx.fillText('×' + G.sendQuantity, QB_X + QB_W / 2, QB_Y + QB_H / 2);
addHitRegion(QB_X, QB_Y, QB_W, QB_H, () => {
if (!G.armoryOpen && !G.commandOpen) addHitRegion(QB_X, QB_Y, QB_W, QB_H, () => {
const idx = SP_QTY_STEPS.indexOf(G.sendQuantity);
G.sendQuantity = SP_QTY_STEPS[(idx + 1) % SP_QTY_STEPS.length];
});
_tickTooltip('qty', QB_X, QB_Y, QB_W, QB_H);
ctx.font = '9px "Share Tech Mono", monospace'; ctx.letterSpacing = '2px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#1a3048';
@@ -66,9 +67,15 @@ function drawSidePanel() {
const qty = G.sendQuantity;
const bonusMult = qty >= 50 ? 1.3 : qty >= 25 ? 1.2 : qty >= 10 ? 1.12 : qty >= 5 ? 1.05 : 1.0;
const tierDef = DIFFICULTY_TIERS[G.difficultyTier || 0];
const tierRewardMult = tierDef?.rewardMult ?? 1;
const visibleDefs = ENEMY_DEFS.filter(d =>
(d.minTier ?? 0) <= (G.difficultyTier || 0) &&
(d.minPrestige ?? 0) <= (G.prestigeLevel || 0)
);
for (let i = 0; i < ENEMY_DEFS.length; i++) {
const def = ENEMY_DEFS[i];
for (let i = 0; i < visibleDefs.length; i++) {
const def = visibleDefs[i];
const totalCost = def.cost * qty;
const canDeploy = G.credits >= totalCost && !G.gameOver;
const cardY = ENEMY_Y + i * (SP_ENEMY_CARD_H + SP_ENEMY_CARD_GAP) - _sidePanelScrollY;
@@ -91,7 +98,7 @@ function drawSidePanel() {
ctx.fillStyle = elColor;
ctx.fillRect(CX, cardY + 5, 3, SP_ENEMY_CARD_H - 10);
// Freshness bar (5px strip at card top)
// Freshness bar (5px strip at card top) — shows novelty bonus remaining
const fresh = G.enemyFreshness[def.id] || 0;
const freshPct = Math.max(0, 1 - fresh / 17);
if (freshPct > 0) {
@@ -100,9 +107,24 @@ function drawSidePanel() {
ctx.fillStyle = fbG;
ctx.fillRect(CX, cardY, CW * freshPct, 5);
}
if (freshPct < 1) {
// Red fill for depleted portion — always visible
ctx.fillStyle = '#ff335555';
ctx.fillRect(CX + CW * freshPct, cardY, CW * (1 - freshPct), 5);
// Penalty text inside the bar (only when meaningfully stale)
if (fresh > 3) {
const penalty = Math.round((1 - freshPct) * 35);
ctx.font = 'bold 9px "Share Tech Mono", monospace';
ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
ctx.fillStyle = '#ff3355dd';
ctx.fillText('-' + penalty + '%', CX + CW - 3, cardY + 2.5);
ctx.textAlign = 'left';
}
}
// Hotkey
const hotkey = i < 9 ? String(i + 1) : i === 9 ? '0' : '';
// Hotkey — base 10 enemies keep 1-0, new enemies have no hotkey
const baseIndex = ENEMY_DEFS.findIndex(d => d.id === def.id);
const hotkey = baseIndex < 9 ? String(baseIndex + 1) : baseIndex === 9 ? '0' : '';
ctx.font = '10px Orbitron, monospace'; ctx.letterSpacing = '0px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#3a6080';
ctx.fillText('[' + hotkey + ']', CX + 6, cardY + 8);
@@ -126,34 +148,55 @@ function drawSidePanel() {
const elIcon = def.element ? (ELEMENTS[def.element]?.icon || '') : '';
if (elIcon) statParts.push(elIcon);
ctx.save();
ctx.beginPath(); ctx.rect(CX + 6, cardY + 26, CW - 12, 16); ctx.clip();
ctx.beginPath(); ctx.rect(CX + 6, cardY + 26, CW - 70, 16); ctx.clip();
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#3a6080';
ctx.fillText(statParts.join(' · '), CX + 6, cardY + 26);
ctx.restore();
// Reward + profit
const rewardPerUnit = Math.round(def.reward * bonusMult);
// Elemental weakness/resistance icons (right side of stats row)
ctx.font = '11px monospace'; ctx.textBaseline = 'top';
let iconX = CX + CW - 6;
const weakEntries = Object.entries(def.weaknesses || {}).filter(([,m]) => m > 1);
const resEntries = Object.entries(def.resistances || {}).filter(([,m]) => m < 1);
for (const [elId] of weakEntries) {
const elD = ELEMENTS[elId]; if (!elD) continue;
iconX -= ctx.measureText(elD.icon).width + 2;
ctx.fillStyle = '#ff6b35'; ctx.textAlign = 'left';
ctx.fillText(elD.icon, iconX, cardY + 26);
}
for (const [elId] of resEntries) {
const elD = ELEMENTS[elId]; if (!elD) continue;
iconX -= ctx.measureText(elD.icon).width + 2;
ctx.fillStyle = '#1a4060'; ctx.textAlign = 'left';
ctx.fillText(elD.icon, iconX, cardY + 26);
}
// Reward + profit (tier-scaled)
const rewardPerUnit = Math.round(def.reward * bonusMult * tierRewardMult);
const profit = rewardPerUnit - def.cost;
const profitStr = (profit >= 0 ? '+' : '') + profit + '¢';
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'bottom'; ctx.fillStyle = '#00ff88';
ctx.fillText('↑ ' + rewardPerUnit + '¢', CX + 6, cardY + SP_ENEMY_CARD_H - 6);
ctx.fillStyle = profit >= 0 ? '#00ff88' : '#ff3355';
ctx.fillText('(' + profitStr + ')', CX + 60, cardY + SP_ENEMY_CARD_H - 6);
if (bonusMult > 1) {
ctx.fillText('(' + profitStr + ')', CX + 62, cardY + SP_ENEMY_CARD_H - 6);
if (tierRewardMult > 1) {
ctx.fillStyle = '#ff6b35'; ctx.font = '9px Orbitron, monospace';
ctx.fillText('×' + tierRewardMult.toFixed(1), CX + 124, cardY + SP_ENEMY_CARD_H - 6);
} else if (bonusMult > 1) {
ctx.fillStyle = '#ffd700'; ctx.font = '9px Orbitron, monospace';
ctx.fillText('+' + Math.round((bonusMult - 1) * 100) + '%', CX + 114, cardY + SP_ENEMY_CARD_H - 6);
ctx.fillText('+' + Math.round((bonusMult - 1) * 100) + '%', CX + 124, cardY + SP_ENEMY_CARD_H - 6);
}
ctx.restore();
if (canDeploy) {
if (canDeploy && !G.armoryOpen && !G.commandOpen) {
const dId = def.id;
addHitRegion(CX, cardY, CW, SP_ENEMY_CARD_H, () => deployEnemy(dId, G.sendQuantity));
}
}
const totalCardH = ENEMY_DEFS.length * (SP_ENEMY_CARD_H + SP_ENEMY_CARD_GAP);
const totalCardH = visibleDefs.length * (SP_ENEMY_CARD_H + SP_ENEMY_CARD_GAP);
_sidePanelScrollMax = Math.max(0, totalCardH - ENEMY_AREA_H + 4);
ctx.restore(); // end enemy clip
@@ -183,7 +226,8 @@ function drawSidePanel() {
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
ctx.fillStyle = logColorMap[logLines[i].type] || '#3a6080';
ctx.fillText(' ' + logLines[i].text, PX + 10, ly + 2);
const cnt = logLines[i].count || 1;
ctx.fillText(' ' + logLines[i].text + (cnt > 1 ? ` ×${cnt}` : ''), PX + 10, ly + 2);
ctx.restore();
}