// ═══ 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; }