622a9fd170
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>
256 lines
8.9 KiB
JavaScript
256 lines
8.9 KiB
JavaScript
// ═══ renderer-inventory.js ═══
|
|
// ============================================================
|
|
// RENDERER INVENTORY — bottom weapon drawer
|
|
// ============================================================
|
|
|
|
const _PK_X = 16, _PK_Y = 642, _PK_W = 1298, _PK_H = 244;
|
|
const _PK_PAD = 16;
|
|
const _PK_TITLE_H = 24;
|
|
const _PK_CLOSE_H = 30;
|
|
const _PK_BODY_Y = _PK_Y + _PK_PAD + _PK_TITLE_H;
|
|
const _PK_BODY_H = _PK_H - _PK_PAD * 2 - _PK_TITLE_H - _PK_CLOSE_H;
|
|
const _PK_DETAIL_W = 246;
|
|
const _PK_GAP = 12;
|
|
const _PK_LIST_X = _PK_X + _PK_PAD;
|
|
const _PK_LIST_W = _PK_W - _PK_PAD * 2 - _PK_GAP - _PK_DETAIL_W;
|
|
const _PK_DET_X = _PK_LIST_X + _PK_LIST_W + _PK_GAP;
|
|
const _PK_CARD_W = 92, _PK_CARD_H = 70, _PK_CARD_GAP = 8;
|
|
const _PK_SECT_H = 18, _PK_DROP_H = 32;
|
|
const _PK_CPR = Math.floor((_PK_LIST_W + _PK_CARD_GAP) / (_PK_CARD_W + _PK_CARD_GAP));
|
|
|
|
let _pickerHoverWeapon = null;
|
|
let _pickerHoverDef = null;
|
|
|
|
function drawInventoryOverlay() {
|
|
if (!document.body.classList.contains('inventory-open')) return;
|
|
|
|
const W = canvas.width, H = canvas.height;
|
|
const equipMode = _pickerSlot >= 0;
|
|
|
|
ctx.fillStyle = 'rgba(0,0,0,0.42)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
ctx.fillStyle = '#060e16';
|
|
ctx.strokeStyle = '#1a3048';
|
|
ctx.lineWidth = 1;
|
|
ctx.fillRect(_PK_X, _PK_Y, _PK_W, _PK_H);
|
|
ctx.strokeRect(_PK_X, _PK_Y, _PK_W, _PK_H);
|
|
|
|
ctx.save();
|
|
|
|
const titleSuffix = equipMode ? `- SLOT ${_pickerSlot + 1}` : '- BAG';
|
|
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.letterSpacing = '3px';
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'top';
|
|
ctx.fillStyle = '#00d4ff';
|
|
ctx.fillText('WEAPON INVENTORY ' + titleSuffix, _PK_LIST_X, _PK_Y + _PK_PAD);
|
|
ctx.letterSpacing = '0px';
|
|
|
|
const invCount = (G.weaponInventory || []).length;
|
|
ctx.font = '10px "Share Tech Mono", monospace';
|
|
ctx.fillStyle = '#3a6080';
|
|
ctx.fillText(invCount + ' in bag', _PK_LIST_X + 270, _PK_Y + _PK_PAD + 1);
|
|
|
|
ctx.strokeStyle = '#1a3048';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(_PK_X, _PK_BODY_Y - 8);
|
|
ctx.lineTo(_PK_X + _PK_W, _PK_BODY_Y - 8);
|
|
ctx.stroke();
|
|
|
|
const CB_W = 110, CB_H = 26;
|
|
const CB_X = _PK_X + _PK_W - _PK_PAD - CB_W;
|
|
const CB_Y = _PK_Y + 7;
|
|
const cbHov = isHovered(CB_X, CB_Y, CB_W, CB_H);
|
|
ctx.fillStyle = cbHov ? '#1a0808' : 'transparent';
|
|
ctx.strokeStyle = cbHov ? '#ff4444' : '#3a6080';
|
|
ctx.lineWidth = 1;
|
|
ctx.fillRect(CB_X, CB_Y, CB_W, CB_H);
|
|
ctx.strokeRect(CB_X, CB_Y, CB_W, CB_H);
|
|
ctx.font = '10px Orbitron, monospace';
|
|
ctx.letterSpacing = '2px';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = cbHov ? '#ff4444' : '#3a6080';
|
|
ctx.fillText('CLOSE', CB_X + CB_W / 2, CB_Y + CB_H / 2);
|
|
ctx.letterSpacing = '0px';
|
|
addHitRegion(CB_X, CB_Y, CB_W, CB_H, closeWeaponPicker);
|
|
_bagDropZones.push({ x: _PK_X, y: _PK_Y, w: _PK_W, h: _PK_H });
|
|
|
|
let yOff = 0, col = 0;
|
|
_pickerHoverWeapon = null;
|
|
_pickerHoverDef = null;
|
|
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(_PK_LIST_X, _PK_BODY_Y, _PK_LIST_W, _PK_BODY_H);
|
|
ctx.clip();
|
|
|
|
function flushRow() {
|
|
if (col > 0) { yOff += _PK_CARD_H + _PK_CARD_GAP; col = 0; }
|
|
}
|
|
|
|
function drawCard(weapon, def, opts) {
|
|
const { isRemove, isEquipped, cantAfford, isBuy, location, action, source } = opts || {};
|
|
const cx = _PK_LIST_X + col * (_PK_CARD_W + _PK_CARD_GAP);
|
|
const cy = _PK_BODY_Y + yOff - _pickerScrollY;
|
|
col++;
|
|
if (col >= _PK_CPR) { yOff += _PK_CARD_H + _PK_CARD_GAP; col = 0; }
|
|
|
|
const visible = cy + _PK_CARD_H > _PK_BODY_Y && cy < _PK_BODY_Y + _PK_BODY_H;
|
|
if (!visible) return;
|
|
|
|
const hov = isHovered(cx, cy, _PK_CARD_W, _PK_CARD_H);
|
|
if (hov && !cantAfford) { _pickerHoverWeapon = weapon; _pickerHoverDef = def; }
|
|
|
|
const border = isRemove ? '#ff4444' : isEquipped ? '#00d4ff99' : (hov && !cantAfford) ? '#ffd700' : '#1a3048';
|
|
const bg = isRemove ? '#150808' : isEquipped ? '#07101a' : (hov && !cantAfford) ? '#0c1820' : '#080f18';
|
|
|
|
ctx.save();
|
|
if (cantAfford) ctx.globalAlpha = 0.4;
|
|
else if (isEquipped) ctx.globalAlpha = 0.72;
|
|
ctx.fillStyle = bg; ctx.strokeStyle = border; ctx.lineWidth = 1;
|
|
ctx.fillRect(cx, cy, _PK_CARD_W, _PK_CARD_H);
|
|
ctx.strokeRect(cx, cy, _PK_CARD_W, _PK_CARD_H);
|
|
|
|
ctx.font = '22px monospace';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = isRemove ? '#ff4444' : '#ffffff';
|
|
ctx.fillText(def?.icon || (isRemove ? 'X' : '?'), cx + _PK_CARD_W / 2, cy + 21);
|
|
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.rect(cx + 4, cy + 36, _PK_CARD_W - 8, 16); ctx.clip();
|
|
ctx.font = '9px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.letterSpacing = '1px';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = isRemove ? '#ff4444' : '#b8d8e8';
|
|
ctx.fillText(isRemove ? 'REMOVE' : (def?.name || ''), cx + _PK_CARD_W / 2, cy + 44);
|
|
ctx.restore();
|
|
|
|
if (isEquipped) {
|
|
ctx.save();
|
|
ctx.fillStyle = 'rgba(0,212,255,0.18)';
|
|
ctx.fillRect(cx + 4, cy + 4, _PK_CARD_W - 8, 14);
|
|
ctx.font = '8px Orbitron, monospace';
|
|
ctx.letterSpacing = '1px';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = '#7ecfff';
|
|
ctx.fillText('EQUIPPED', cx + _PK_CARD_W / 2, cy + 11);
|
|
ctx.restore();
|
|
}
|
|
|
|
const sub = isRemove ? 'free'
|
|
: isBuy ? (cantAfford ? `Need ${def.cost - spendableCredits()}c` : `${def.cost}c`)
|
|
: (location || '');
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.rect(cx + 2, cy + 54, _PK_CARD_W - 4, 14); ctx.clip();
|
|
ctx.font = '9px "Share Tech Mono", monospace';
|
|
ctx.letterSpacing = '0px';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = isBuy ? '#ffd700' : '#3a6080';
|
|
ctx.fillText(sub, cx + _PK_CARD_W / 2, cy + 61);
|
|
ctx.restore();
|
|
ctx.restore();
|
|
|
|
if (!cantAfford && action) addHitRegion(cx, cy, _PK_CARD_W, _PK_CARD_H, action);
|
|
if (!cantAfford && weapon && !isRemove && !isBuy)
|
|
_dragRegions.push({ x: cx, y: cy, w: _PK_CARD_W, h: _PK_CARD_H, weapon, source: source || null });
|
|
}
|
|
|
|
for (let i = 0; i < G.tower.weaponSlots; i++) {
|
|
const w = G.weapons[i];
|
|
if (!w) continue;
|
|
const ii = i;
|
|
drawCard(w, getWeaponDef(w), {
|
|
location: `SOCKET ${i + 1}`,
|
|
isEquipped: true,
|
|
source: { type: 'slot', slotIndex: i },
|
|
action: (equipMode && i !== _pickerSlot)
|
|
? () => { equipWeaponInstanceToSlot(_pickerSlot, G.weapons[ii].instanceId); }
|
|
: null
|
|
});
|
|
}
|
|
|
|
for (const w of (G.weaponInventory || [])) {
|
|
const wRef = w;
|
|
drawCard(w, getWeaponDef(w), {
|
|
location: 'BAG',
|
|
source: { type: 'bag' },
|
|
action: equipMode
|
|
? () => { equipWeaponInstanceToSlot(_pickerSlot, wRef.instanceId); }
|
|
: null
|
|
});
|
|
}
|
|
flushRow();
|
|
|
|
if (equipMode) {
|
|
for (const def of WEAPON_DEFS) {
|
|
const ownedCount = countOwnedWeaponType(def.id);
|
|
if (ownedCount >= MAX_WEAPONS_PER_TYPE) continue;
|
|
const cantAfford = spendableCredits() < def.cost;
|
|
const dRef = def;
|
|
drawCard(null, def, {
|
|
location: 'BUY', isBuy: true, cantAfford, ownedCount,
|
|
action: cantAfford ? null : () => { buyAndEquipWeapon(_pickerSlot, dRef.id); }
|
|
});
|
|
}
|
|
flushRow();
|
|
}
|
|
|
|
_pickerScrollMax = Math.max(0, yOff - _PK_BODY_H + 20);
|
|
|
|
ctx.restore();
|
|
|
|
const DX = _PK_DET_X, DY = _PK_BODY_Y, DH = _PK_BODY_H;
|
|
ctx.fillStyle = '#050a10';
|
|
ctx.strokeStyle = '#122030';
|
|
ctx.lineWidth = 1;
|
|
ctx.fillRect(DX, DY, _PK_DETAIL_W, DH);
|
|
ctx.strokeRect(DX, DY, _PK_DETAIL_W, DH);
|
|
|
|
if (_pickerHoverWeapon || _pickerHoverDef) {
|
|
const hw = _pickerHoverWeapon, hd = _pickerHoverDef;
|
|
let dy = DY + 10;
|
|
ctx.font = '12px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.letterSpacing = '2px';
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'top';
|
|
ctx.fillStyle = '#00d4ff';
|
|
ctx.fillText((hd?.icon || '') + ' ' + (hd?.name || ''), DX + 10, dy);
|
|
ctx.letterSpacing = '0px';
|
|
dy += 28;
|
|
ctx.font = '10px "Share Tech Mono", monospace';
|
|
const elLabel = hw ? weaponElementLabel(hw) : '';
|
|
if (elLabel) {
|
|
ctx.fillStyle = '#3a6080'; ctx.textAlign = 'left';
|
|
ctx.fillText('ELEMENTS', DX + 10, dy);
|
|
ctx.fillStyle = '#b8d8e8'; ctx.textAlign = 'right';
|
|
ctx.fillText(elLabel, DX + _PK_DETAIL_W - 10, dy);
|
|
ctx.textAlign = 'left'; dy += 18;
|
|
}
|
|
for (const [k, v] of (hw ? weaponStatRows(hw) : [])) {
|
|
if (dy > DY + DH - 18) break;
|
|
ctx.strokeStyle = '#102030'; ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(DX + 8, dy + 16);
|
|
ctx.lineTo(DX + _PK_DETAIL_W - 8, dy + 16);
|
|
ctx.stroke();
|
|
ctx.fillStyle = '#3a6080'; ctx.textAlign = 'left'; ctx.fillText(k, DX + 10, dy);
|
|
ctx.fillStyle = '#b8d8e8'; ctx.textAlign = 'right'; ctx.fillText(String(v), DX + _PK_DETAIL_W - 10, dy);
|
|
dy += 18;
|
|
}
|
|
} else {
|
|
ctx.font = '11px Orbitron, monospace';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = '#1a3048';
|
|
ctx.fillText('Drag weapons into sockets', DX + _PK_DETAIL_W / 2, DY + DH / 2 - 10);
|
|
ctx.fillText('or back into the bag', DX + _PK_DETAIL_W / 2, DY + DH / 2 + 10);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|