Files
siege-protocol/js/renderer-shop-sections.js
T
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

415 lines
18 KiB
JavaScript

// ═══ renderer-shop-sections.js ═══
// ============================================================
// RENDERER SHOP SECTIONS — armory tab content
// ============================================================
function _shopDrawTowerContent(yOff, cx, cw, H) {
const sy = y => _SH_BODY_Y + y - _shopScrollY;
const vis = (y, h) => sy(y) + h >= _SH_BODY_Y && sy(y) < H;
const stats = [
['HP', G.tower.hp + ' / ' + G.tower.maxHp],
['Armor', G.tower.armor],
['Aim Speed', G.tower.aimSpeed.toFixed(3)],
['Vision', (() => { const ws = (G.weapons||[]).filter(w=>w); return (ws.length > 0 ? Math.max(...ws.map(w=>w.range??0)) : 0) + 'px'; })()],
['Slots', getEquippedWeapons().length + '/' + G.tower.weaponSlots],
['Shield', G.tower.shield ? G.tower.shield.toUpperCase() + ' (' + G.tower.shieldHp + '/' + G.tower.shieldMaxHp + ')' : 'None'],
];
const STAT_W = Math.floor((cw - 5 * 10) / 6);
const STAT_H = 52;
if (vis(yOff, STAT_H)) {
for (let i = 0; i < stats.length; i++) {
const bx = cx + i * (STAT_W + 10);
ctx.fillStyle = '#060e18'; ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.fillRect(bx, sy(yOff), STAT_W, STAT_H);
ctx.strokeRect(bx, sy(yOff), STAT_W, STAT_H);
ctx.font = '9px Orbitron, monospace'; ctx.letterSpacing = '1.5px';
ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillStyle = '#3a6080';
ctx.fillText(stats[i][0], bx + STAT_W / 2, sy(yOff) + 8);
ctx.font = '13px Orbitron, monospace'; ctx.letterSpacing = '0px';
ctx.fillStyle = '#b8d8e8'; ctx.textBaseline = 'bottom';
ctx.fillText(String(stats[i][1]), bx + STAT_W / 2, sy(yOff) + STAT_H - 8);
}
}
yOff += STAT_H + 20;
const categories = {
'Hull': ['hp1','hp2','hp3','hp4','hp5'],
'Armor': ['armor1','armor2','armor3','armor4','armor5'],
'Servo Motors': ['aim1','aim2','aim3','aim4','aim5','aim6'],
'Vision Range': ['range1','range2','range3','range4'],
'Weapon Slots': ['slot2','slot3','slot4','slot5','slot6','slot7','slot8'],
'Shield': ['shield_dome','shield_dir'],
'Utility': ['repair1'],
};
for (const [catName, ids] of Object.entries(categories)) {
const CAT_H = 22;
if (vis(yOff, CAT_H)) {
ctx.font = '9px Orbitron, monospace'; ctx.letterSpacing = '3px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a6080';
ctx.fillText(catName.toUpperCase(), cx, sy(yOff) + CAT_H / 2);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(cx, sy(yOff) + CAT_H); ctx.lineTo(cx + cw, sy(yOff) + CAT_H); ctx.stroke();
ctx.letterSpacing = '0px';
}
yOff += CAT_H + 4;
const available = ids.map(id => TOWER_UPGRADE_TREE.find(u => u.id === id)).filter(Boolean);
let rowW = 0;
for (let i = 0; i < available.length; i++) {
if (i > 0 && available[i].requires.length > 0) rowW += _SH_ARR_W;
rowW += _SH_UPG_W;
}
let nx = cx + Math.max(0, (cw - rowW) / 2);
if (vis(yOff, _SH_UPG_H)) {
for (let i = 0; i < available.length; i++) {
const upg = available[i];
if (i > 0 && upg.requires.length > 0) {
ctx.font = '13px "Share Tech Mono", monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a5060';
ctx.fillText('→', nx + _SH_ARR_W / 2, sy(yOff) + _SH_UPG_H / 2);
nx += _SH_ARR_W;
}
const isBought = G.towerUpgradesBought.includes(upg.id);
const shieldConflict = (upg.id === 'shield_dome' && G.tower.shield === 'dome') ||
(upg.id === 'shield_dir' && G.tower.shield === 'directional');
const effectiveBought = isBought || shieldConflict;
const reqsMet = upg.requires.every(r => G.towerUpgradesBought.includes(r));
const tierLocked = (upg.minTier ?? 0) > (G.difficultyTier || 0);
const canAfford = !tierLocked && spendableCredits() >= upg.cost;
const tierLockLabel = tierLocked ? (DIFFICULTY_TIERS[upg.minTier]?.name ?? '') : null;
const uid = upg.id;
_shopUpgNode(nx, sy(yOff), upg, effectiveBought,
tierLocked || (!reqsMet && !isBought), !canAfford && reqsMet && !isBought && !tierLocked,
(!effectiveBought && reqsMet && canAfford && !tierLocked) ? () => buyTowerUpgrade(uid) : null,
(isBought && !upg.repeatable) ? () => refundTowerUpgrade(uid) : null,
tierLockLabel
);
nx += _SH_UPG_W;
}
}
yOff += _SH_UPG_H + 14;
if (catName === 'Shield' && G.tower.shield) {
const shTree = SHIELD_UPGRADE_TREES[G.tower.shield] || [];
if (shTree.length > 0) {
const SH_H = 18;
if (vis(yOff, SH_H)) {
ctx.font = '9px Orbitron, monospace'; ctx.letterSpacing = '2px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#1a5060';
ctx.fillText(G.tower.shield.toUpperCase() + ' UPGRADES', cx, sy(yOff) + SH_H / 2);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(cx, sy(yOff) + SH_H); ctx.lineTo(cx + cw, sy(yOff) + SH_H); ctx.stroke();
ctx.letterSpacing = '0px';
}
yOff += SH_H + 4;
const shBought = G.shieldUpgradesBought || [];
let shRowW = 0;
for (let i = 0; i < shTree.length; i++) {
if (i > 0 && shTree[i].requires.length > 0) shRowW += _SH_ARR_W;
shRowW += _SH_UPG_W;
}
let shx = cx + Math.max(0, (cw - shRowW) / 2);
if (vis(yOff, _SH_UPG_H)) {
for (let i = 0; i < shTree.length; i++) {
const upg = shTree[i];
if (i > 0 && upg.requires.length > 0) {
ctx.font = '13px "Share Tech Mono", monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a5060';
ctx.fillText('→', shx + _SH_ARR_W / 2, sy(yOff) + _SH_UPG_H / 2);
shx += _SH_ARR_W;
}
const b = shBought.includes(upg.id);
const rm = upg.requires.every(r => shBought.includes(r));
const ca = spendableCredits() >= upg.cost;
const uid = upg.id;
_shopUpgNode(shx, sy(yOff), upg, b, !rm && !b, !ca && rm && !b,
(!b && rm && ca) ? () => buyShieldUpgrade(uid) : null,
(b && !upg.repeatable) ? () => refundShieldUpgrade(uid) : null
);
shx += _SH_UPG_W;
}
}
yOff += _SH_UPG_H + 14;
}
}
}
return yOff;
}
function _shopDrawBuyContent(yOff, cx, cw, H) {
const sy = y => _SH_BODY_Y + y - _shopScrollY;
const vis = (y, h) => sy(y) + h >= _SH_BODY_Y && sy(y) < H;
const equippedWeapons = getEquippedWeapons();
const INFO_H = 36;
if (vis(yOff, INFO_H)) {
ctx.fillStyle = '#060e18'; ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.fillRect(cx, sy(yOff), cw, INFO_H);
ctx.strokeRect(cx, sy(yOff), cw, INFO_H);
ctx.font = '12px "Share Tech Mono", monospace'; ctx.letterSpacing = '0px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a6080';
ctx.fillText('Weapon Slots: ', cx + 12, sy(yOff) + INFO_H / 2);
const lw = ctx.measureText('Weapon Slots: ').width;
ctx.fillStyle = '#b8d8e8';
ctx.fillText(equippedWeapons.length + '/' + G.tower.weaponSlots, cx + 12 + lw, sy(yOff) + INFO_H / 2);
}
yOff += INFO_H + 14;
const CARD_W = 188, CARD_H = 112, CARD_GAP = 12;
const cols = Math.max(1, Math.floor((cw + CARD_GAP) / (CARD_W + CARD_GAP)));
const actW = Math.floor((cw - (cols - 1) * CARD_GAP) / cols);
for (let i = 0; i < WEAPON_DEFS.length; i++) {
const def = WEAPON_DEFS[i];
const col = i % cols;
const row = Math.floor(i / cols);
const cardX = cx + col * (actW + CARD_GAP);
const cardYOff = yOff + row * (CARD_H + CARD_GAP);
if (!vis(cardYOff, CARD_H)) continue;
const owned = countOwnedWeaponType(def.id);
const atCap = owned >= MAX_WEAPONS_PER_TYPE;
const canAfford = spendableCredits() >= def.cost;
const canBuy = canAfford && !atCap;
const csy = sy(cardYOff);
const hov = canBuy && isHovered(cardX, csy, actW, CARD_H);
ctx.save();
if (!canBuy) ctx.globalAlpha = 0.5;
ctx.fillStyle = hov ? '#0c1e30' : '#060e18';
ctx.strokeStyle = hov ? '#ffd700' : (canBuy ? '#1a3048' : '#0e1e28');
ctx.lineWidth = 1;
ctx.fillRect(cardX, csy, actW, CARD_H);
ctx.strokeRect(cardX, csy, actW, CARD_H);
ctx.font = '26px monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#ffffff';
ctx.fillText(def.icon, cardX + 30, csy + 30);
ctx.font = '12px Orbitron, "Share Tech Mono", monospace'; ctx.letterSpacing = '1px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#b8d8e8';
ctx.fillText(def.name, cardX + 54, csy + 10);
ctx.letterSpacing = '0px';
ctx.font = '11px "Share Tech Mono", monospace';
ctx.fillStyle = canAfford ? '#ffd700' : '#ff3355';
ctx.fillText(def.cost + '¢', cardX + 54, csy + 28);
ctx.save();
ctx.beginPath(); ctx.rect(cardX + 8, csy + 50, actW - 16, 30); ctx.clip();
ctx.font = '10px "Share Tech Mono", monospace';
ctx.fillStyle = '#3a6080'; ctx.textBaseline = 'top'; ctx.textAlign = 'left';
const dl = _shopWrapText(def.desc, actW - 20, 2);
for (let li = 0; li < dl.length; li++) ctx.fillText(dl[li], cardX + 8, csy + 52 + li * 14);
ctx.restore();
ctx.font = '10px "Share Tech Mono", monospace';
ctx.fillStyle = atCap ? '#ff3355' : owned > 0 ? '#8ab8d0' : '#3a6080';
ctx.textBaseline = 'top'; ctx.textAlign = 'left';
ctx.fillText('Owned: ' + owned + '/' + MAX_WEAPONS_PER_TYPE + (atCap ? ' (MAX)' : ''), cardX + 8, csy + 88);
ctx.restore();
if (canBuy) { const dId = def.id; addHitRegion(cardX, csy, actW, CARD_H, () => buyWeapon(dId)); }
}
const rows = Math.ceil(WEAPON_DEFS.length / cols);
yOff += rows * (CARD_H + CARD_GAP);
return yOff;
}
function _shopDrawWeaponContent(yOff, cx, cw, H, weapon) {
const sy = y => _SH_BODY_Y + y - _shopScrollY;
const vis = (y, h) => sy(y) + h >= _SH_BODY_Y && sy(y) < H;
const def = getWeaponDef(weapon);
const bought = G.weaponUpgradesBought[weapon.instanceId] || [];
const HDR_H = 80;
if (vis(yOff, HDR_H)) {
ctx.fillStyle = '#060e18'; ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.fillRect(cx, sy(yOff), cw, HDR_H);
ctx.strokeRect(cx, sy(yOff), cw, HDR_H);
ctx.font = '22px monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#ffffff';
ctx.fillText(def?.icon || '', cx + 12, sy(yOff) + 22);
ctx.font = '14px Orbitron, "Share Tech Mono", monospace'; ctx.letterSpacing = '2px';
ctx.fillStyle = '#b8d8e8';
ctx.fillText(def?.name || '', cx + 38, sy(yOff) + 22);
ctx.letterSpacing = '0px';
const statPairs = [
['DMG', weapon.damage],
['RATE', weapon.fireRate + 'f'],
...(weapon.pierce ? [['PIERCE', weapon.pierce]] : []),
...(weapon.critChance ? [['CRIT', Math.round(weapon.critChance * 100) + '%']] : []),
...(weapon.chains ? [['CHAINS', weapon.chains]] : []),
...(weapon.aoeRadius ? [['AOE', weapon.aoeRadius]] : []),
...(weapon.targets ? [['TGT', weapon.targets]] : []),
];
const spw = Math.min(110, Math.floor(cw / Math.max(1, statPairs.length)));
ctx.font = '10px "Share Tech Mono", monospace'; ctx.textBaseline = 'top';
for (let i = 0; i < statPairs.length; i++) {
const bx = cx + 12 + i * spw;
ctx.fillStyle = '#3a6080'; ctx.textAlign = 'left'; ctx.fillText(statPairs[i][0], bx, sy(yOff) + 46);
ctx.fillStyle = '#b8d8e8'; ctx.fillText(String(statPairs[i][1]), bx, sy(yOff) + 60);
}
}
yOff += HDR_H + 14;
const TGT_H = 42;
if (vis(yOff, TGT_H)) {
ctx.fillStyle = '#040a10'; ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.fillRect(cx, sy(yOff), cw, TGT_H);
ctx.strokeRect(cx, sy(yOff), cw, TGT_H);
ctx.font = '10px Orbitron, monospace'; ctx.letterSpacing = '2px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a6080';
ctx.fillText('TARGET:', cx + 12, sy(yOff) + TGT_H / 2);
ctx.letterSpacing = '0px';
const lw = ctx.measureText('TARGET:').width;
const TBTNS = ['nearest','strongest','weakest','fastest','furthest','group'];
const BTN_W = 92, BTN_H = 28;
let bx = cx + 12 + lw + 18;
for (const mode of TBTNS) {
const active = weapon.targeting === mode;
const hov = isHovered(bx, sy(yOff) + 7, BTN_W, BTN_H);
ctx.fillStyle = active ? '#00d4ff' : (hov ? '#0a1e30' : 'transparent');
ctx.strokeStyle = active ? '#00d4ff' : (hov ? '#00d4ff' : '#1a3048');
ctx.lineWidth = 1;
ctx.fillRect(bx, sy(yOff) + 7, BTN_W, BTN_H);
ctx.strokeRect(bx, sy(yOff) + 7, BTN_W, BTN_H);
ctx.font = '10px Orbitron, monospace'; ctx.letterSpacing = '0.5px';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = active ? '#000' : (hov ? '#00d4ff' : '#3a6080');
ctx.fillText(mode.toUpperCase(), bx + BTN_W / 2, sy(yOff) + 7 + BTN_H / 2);
ctx.letterSpacing = '0px';
const mRef = mode, iRef = weapon.instanceId;
addHitRegion(bx, sy(yOff) + 7, BTN_W, BTN_H, () => setWeaponTargeting(iRef, mRef));
bx += BTN_W + 6;
}
}
yOff += TGT_H + 14;
const infuseSlots = weapon.canInfuse3 ? 3 : weapon.canInfuse2 ? 2 : weapon.canInfuse ? 1 : 0;
if (infuseSlots > 0) {
const INF_H = 54;
if (vis(yOff, INF_H)) {
ctx.fillStyle = '#040a10'; ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.fillRect(cx, sy(yOff), cw, INF_H);
ctx.strokeRect(cx, sy(yOff), cw, INF_H);
ctx.font = '10px Orbitron, monospace'; ctx.letterSpacing = '2px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a6080';
ctx.fillText('ELEMENTS:', cx + 12, sy(yOff) + INF_H / 2);
ctx.letterSpacing = '0px';
const EB = 28;
let ex = cx + 130;
for (let i = 0; i < infuseSlots; i++) {
const cur = weapon.elements[i];
const el = ELEMENTS[cur];
ctx.font = '20px monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = el?.color || '#444';
ctx.fillText(el?.icon || '—', ex + EB / 2, sy(yOff) + 16);
ex += EB + 4;
for (const [elId, elDef] of Object.entries(ELEMENTS)) {
if (elId === 'physical') continue;
const active = cur === elId;
const hov2 = isHovered(ex, sy(yOff) + 2, EB, EB);
ctx.fillStyle = active ? elDef.color + '44' : (hov2 ? '#0a1828' : 'transparent');
ctx.strokeStyle = active ? elDef.color : (hov2 ? elDef.color : '#1a2838');
ctx.lineWidth = 1;
ctx.fillRect(ex, sy(yOff) + 2, EB, EB);
ctx.strokeRect(ex, sy(yOff) + 2, EB, EB);
ctx.font = '14px monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = elDef.color;
ctx.fillText(elDef.icon, ex + EB / 2, sy(yOff) + 2 + EB / 2);
const iid2 = weapon.instanceId, slotI = i, eid = elId;
addHitRegion(ex, sy(yOff) + 2, EB, EB, () => setWeaponInfusion(iid2, slotI, eid));
ex += EB + 2;
}
ex += 16;
}
}
yOff += INF_H + 14;
}
const tree = WEAPON_UPGRADE_TREES[def?.id] || [];
if (tree.length === 0) return yOff;
const TREE_HDR_H = 22;
if (vis(yOff, TREE_HDR_H)) {
ctx.font = '9px Orbitron, monospace'; ctx.letterSpacing = '3px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a6080';
ctx.fillText('UPGRADE TREE', cx, sy(yOff) + TREE_HDR_H / 2);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(cx, sy(yOff) + TREE_HDR_H); ctx.lineTo(cx + cw, sy(yOff) + TREE_HDR_H); ctx.stroke();
ctx.letterSpacing = '0px';
}
yOff += TREE_HDR_H + 4;
const HINT_H = 18;
if (vis(yOff, HINT_H)) {
ctx.font = '10px "Share Tech Mono", monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#1a3048';
ctx.fillText('Right-click a purchased upgrade to refund it (cascades dependents)', cx, sy(yOff) + HINT_H / 2);
}
yOff += HINT_H + 8;
const catOrder = [];
const catMap = {};
for (const upg of tree) {
const cat = upg.category || 'General';
if (!catMap[cat]) { catMap[cat] = []; catOrder.push(cat); }
catMap[cat].push(upg);
}
const COL_W = _SH_UPG_W + 16, COL_GAP = 10;
let maxColH = 0;
for (const cat of catOrder) {
let colH = 24;
for (const u of catMap[cat]) { if (u.requires.length > 0) colH += 20; colH += _SH_UPG_H + 4; }
maxColH = Math.max(maxColH, colH);
}
if (vis(yOff, maxColH)) {
let colX = cx;
for (const cat of catOrder) {
const upgrades = catMap[cat];
if (vis(yOff, 24)) {
ctx.font = '10px Orbitron, monospace'; ctx.letterSpacing = '1px';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a6080';
ctx.fillText(cat, colX + COL_W / 2, sy(yOff) + 10);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(colX, sy(yOff) + 20); ctx.lineTo(colX + COL_W, sy(yOff) + 20); ctx.stroke();
ctx.letterSpacing = '0px';
}
let ny = yOff + 24;
for (const upg of upgrades) {
if (upg.requires.length > 0) {
if (vis(ny - 16, 16)) {
ctx.font = '13px "Share Tech Mono", monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a5060';
ctx.fillText('↓', colX + COL_W / 2, sy(ny) - 10);
}
ny += 20;
}
const isBought = bought.includes(upg.id);
const reqsMet = upg.requires.every(r => bought.includes(r));
const tierLocked = (upg.minTier ?? 0) > (G.difficultyTier || 0);
const canAfford = !tierLocked && spendableCredits() >= upg.cost;
const tierLockLabel = tierLocked ? (DIFFICULTY_TIERS[upg.minTier]?.name ?? '') : null;
const uid = upg.id, iid = weapon.instanceId;
_shopUpgNode(colX + (COL_W - _SH_UPG_W) / 2, sy(ny), upg, isBought,
tierLocked || (!reqsMet && !isBought), !canAfford && reqsMet && !isBought && !tierLocked,
(!isBought && reqsMet && canAfford && !tierLocked) ? () => buyWeaponUpgrade(iid, uid) : null,
(isBought && !upg.repeatable) ? () => refundWeaponUpgrade(iid, uid) : null,
tierLockLabel
);
ny += _SH_UPG_H + 4;
}
colX += COL_W + COL_GAP;
}
}
yOff += maxColH + 14;
return yOff;
}