Initial commit: Siege Protocol
Inverted tower-defense browser game — deploy enemies yourself, tower auto-kills them, pocket credits, upgrade weapons. HTML + Canvas + vanilla JS, no build step. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
// ═══ 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', G.tower.range + '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 canAfford = spendableCredits() >= upg.cost;
|
||||
const uid = upg.id;
|
||||
_shopUpgNode(nx, sy(yOff), upg, effectiveBought,
|
||||
!reqsMet && !isBought, !canAfford && reqsMet && !isBought,
|
||||
(!effectiveBought && reqsMet && canAfford) ? () => buyTowerUpgrade(uid) : null,
|
||||
(isBought && !upg.repeatable) ? () => refundTowerUpgrade(uid) : null
|
||||
);
|
||||
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' : '#1a3048';
|
||||
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 canAfford = spendableCredits() >= upg.cost;
|
||||
const uid = upg.id, iid = weapon.instanceId;
|
||||
_shopUpgNode(colX + (COL_W - _SH_UPG_W) / 2, sy(ny), upg, isBought,
|
||||
!reqsMet && !isBought, !canAfford && reqsMet && !isBought,
|
||||
(!isBought && reqsMet && canAfford) ? () => buyWeaponUpgrade(iid, uid) : null,
|
||||
(isBought && !upg.repeatable) ? () => refundWeaponUpgrade(iid, uid) : null
|
||||
);
|
||||
ny += _SH_UPG_H + 4;
|
||||
}
|
||||
colX += COL_W + COL_GAP;
|
||||
}
|
||||
}
|
||||
yOff += maxColH + 14;
|
||||
return yOff;
|
||||
}
|
||||
Reference in New Issue
Block a user