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>
263 lines
9.5 KiB
JavaScript
263 lines
9.5 KiB
JavaScript
// ═══ renderer-overlays.js ═══
|
|
// ============================================================
|
|
// RENDERER OVERLAYS — game over, pause, mount drag UI
|
|
// ============================================================
|
|
|
|
// ── GAME OVER / BANKRUPT OVERLAY ─────────────────────────────
|
|
function drawGameOverPanel() {
|
|
const W = canvas.width, H = canvas.height;
|
|
|
|
// Game over takes over hit detection — clear HUD regions
|
|
clearHitRegions();
|
|
|
|
// Full-screen dark tint
|
|
ctx.fillStyle = 'rgba(0,0,0,0.88)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
const BW = 460, BH = 270;
|
|
const BX = (W - BW) / 2, BY = (H - BH) / 2;
|
|
ctx.fillStyle = '#060e16';
|
|
ctx.strokeStyle = '#1a3048';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.rect(BX, BY, BW, BH); ctx.fill(); ctx.stroke();
|
|
|
|
const isBankrupt = G.isBankrupt;
|
|
const titleColor = isBankrupt ? '#ffd700' : '#ff3355';
|
|
const title = isBankrupt ? 'BANKRUPT' : 'TOWER FALLEN';
|
|
const subText = isBankrupt
|
|
? 'You ran out of credits with no way to recover.'
|
|
: 'The defense has been breached.';
|
|
|
|
ctx.save();
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'top';
|
|
ctx.font = '900 30px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.fillStyle = titleColor;
|
|
ctx.shadowColor = titleColor;
|
|
ctx.shadowBlur = 24;
|
|
ctx.fillText(title, W / 2, BY + 28);
|
|
ctx.shadowBlur = 0;
|
|
|
|
ctx.font = '14px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.fillStyle = '#ffd700';
|
|
ctx.fillText(`Score: ${G.score} — Kills: ${G.totalKills}`, W / 2, BY + 78);
|
|
|
|
ctx.font = '12px "Share Tech Mono", monospace';
|
|
if (G._isNewBest) {
|
|
ctx.fillStyle = '#ffd700';
|
|
ctx.fillText('★ NEW BEST!', W / 2, BY + 108);
|
|
} else if (_best) {
|
|
ctx.fillStyle = '#3a6080';
|
|
ctx.fillText(`Best: ${_best.score} — ${_best.kills} kills`, W / 2, BY + 108);
|
|
}
|
|
|
|
ctx.font = '11px "Share Tech Mono", monospace';
|
|
ctx.fillStyle = '#3a6080';
|
|
ctx.fillText(subText, W / 2, BY + 134);
|
|
|
|
// RESTART button
|
|
const RBW = 220, RBH = 44;
|
|
const RBX = W / 2 - RBW / 2;
|
|
const RBY = BY + 192;
|
|
const restHov = isHovered(RBX, RBY, RBW, RBH);
|
|
ctx.fillStyle = restHov ? '#00d4ff' : 'transparent';
|
|
ctx.strokeStyle = '#00d4ff';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.rect(RBX, RBY, RBW, RBH); ctx.fill(); ctx.stroke();
|
|
if (restHov) { ctx.shadowColor = 'rgba(0,212,255,0.35)'; ctx.shadowBlur = 20; }
|
|
ctx.font = '12px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = restHov ? '#000000' : '#00d4ff';
|
|
ctx.fillText('RESTART MISSION', W / 2, RBY + RBH / 2);
|
|
ctx.shadowBlur = 0;
|
|
ctx.restore();
|
|
|
|
addHitRegion(RBX, RBY, RBW, RBH, restartGame);
|
|
}
|
|
|
|
// ── PAUSE OVERLAY ─────────────────────────────────────────────
|
|
function drawPauseOverlay() {
|
|
if (!document.body.classList.contains('paused')) return;
|
|
|
|
const W = canvas.width, H = canvas.height;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.58)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
const BW = 420, BH = 118;
|
|
const BX = (W - BW) / 2, BY = (H - BH) / 2;
|
|
ctx.save();
|
|
ctx.fillStyle = 'rgba(6,14,22,0.94)';
|
|
ctx.strokeStyle = '#1a3048';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.rect(BX, BY, BW, BH); ctx.fill(); ctx.stroke();
|
|
ctx.shadowColor = 'rgba(0,212,255,0.18)';
|
|
ctx.shadowBlur = 34;
|
|
ctx.strokeRect(BX, BY, BW, BH);
|
|
ctx.shadowBlur = 0;
|
|
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'top';
|
|
ctx.font = '900 34px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.fillStyle = '#ffd700';
|
|
ctx.shadowColor = '#ffd700';
|
|
ctx.shadowBlur = 22;
|
|
ctx.fillText('PAUSED', W / 2, BY + 18);
|
|
ctx.shadowBlur = 0;
|
|
|
|
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
|
|
ctx.fillStyle = '#3a6080';
|
|
ctx.fillText('Esc resumes · Space opens armory · I opens inventory', W / 2, BY + 78);
|
|
ctx.restore();
|
|
}
|
|
|
|
// ── MOUNT POINT TOOLTIP ───────────────────────────────────────
|
|
function _drawMountTooltip(mx, my, weapon, socketR) {
|
|
const def = getWeaponDef(weapon);
|
|
if (!def) return;
|
|
const TW = 168, TH = 58;
|
|
let tx = mx + socketR + 10;
|
|
let ty = my - TH / 2;
|
|
if (tx + TW > 1330) tx = mx - socketR - 10 - TW;
|
|
ty = Math.max(68, Math.min(900 - TH - 4, ty));
|
|
|
|
ctx.save();
|
|
ctx.fillStyle = '#050d18';
|
|
ctx.strokeStyle = '#00d4ff';
|
|
ctx.lineWidth = 1;
|
|
ctx.fillRect(tx, ty, TW, TH);
|
|
ctx.strokeRect(tx, ty, TW, TH);
|
|
|
|
ctx.font = '10px Orbitron, monospace';
|
|
ctx.letterSpacing = '1px';
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'top';
|
|
ctx.fillStyle = '#00d4ff';
|
|
ctx.fillText((def.icon || '') + ' ' + def.name, tx + 8, ty + 8);
|
|
ctx.letterSpacing = '0px';
|
|
|
|
const parts = [];
|
|
if (typeof def.damage === 'number') parts.push(`DMG ${def.damage}`);
|
|
if (typeof def.fireRate === 'number') parts.push(`RPM ${Math.round(3600 / def.fireRate)}`);
|
|
const el = weapon.elements?.[0] ? ELEMENTS[weapon.elements[0]] : null;
|
|
if (el) parts.push(el.icon + ' ' + el.name);
|
|
ctx.font = '9px "Share Tech Mono", monospace';
|
|
ctx.fillStyle = '#7aaabb';
|
|
ctx.fillText(parts.join(' '), tx + 8, ty + 26);
|
|
|
|
ctx.fillStyle = '#3a6080';
|
|
ctx.fillText('drag to move or bag', tx + 8, ty + 41);
|
|
ctx.restore();
|
|
}
|
|
|
|
// ── MOUNT POINT INTERACTION (drawn after overlays so it sits on top) ─────
|
|
function drawMountInteraction(cx, cy) {
|
|
if (G.shopOpen) return;
|
|
const invOpen = document.body.classList.contains('inventory-open');
|
|
const totalSlots = Math.max(1, G.tower.weaponSlots);
|
|
const hpRatio = G.tower.hp / G.tower.maxHp;
|
|
const hpColor = hpRatio > 0.5 ? '#00d4ff' : hpRatio > 0.25 ? '#ffd700' : '#ff3355';
|
|
|
|
for (let slotIndex = 0; slotIndex < totalSlots; slotIndex++) {
|
|
const weapon = G.weapons[slotIndex];
|
|
const installed = !!weapon;
|
|
const mountAngle = HARDPOINT_BASE_ANGLE + (slotIndex / totalSlots) * Math.PI * 2;
|
|
|
|
if (invOpen) {
|
|
const actual = getSlotHardpoint(slotIndex, cx, cy, totalSlots);
|
|
const ORBIT = Math.max(78, 64 + totalSlots * 6);
|
|
const mx = cx + Math.cos(mountAngle) * ORBIT;
|
|
const my = cy + Math.sin(mountAngle) * ORBIT;
|
|
const dropR = 20;
|
|
const hov = isHovered(mx - dropR, my - dropR, dropR * 2, dropR * 2);
|
|
const dragging = _dragWeapon !== null;
|
|
|
|
ctx.save();
|
|
ctx.strokeStyle = '#00aaff33';
|
|
ctx.lineWidth = 1;
|
|
ctx.setLineDash([5, 6]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(actual.x, actual.y);
|
|
ctx.lineTo(mx, my);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
ctx.restore();
|
|
|
|
if (dragging) {
|
|
ctx.save();
|
|
ctx.shadowBlur = hov ? 20 : 8;
|
|
ctx.shadowColor = '#ffd700';
|
|
ctx.strokeStyle = hov ? '#ffd700' : '#00aaff55';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(mx, my, dropR + 5, 0, Math.PI * 2); ctx.stroke();
|
|
ctx.shadowBlur = 0;
|
|
ctx.restore();
|
|
}
|
|
|
|
ctx.fillStyle = hov ? '#0c1820' : '#050d16';
|
|
ctx.strokeStyle = installed
|
|
? (hov ? '#ffd700' : hpColor + 'aa')
|
|
: (hov ? '#00aaff' : '#1a3240');
|
|
ctx.lineWidth = hov ? 2.5 : 1.5;
|
|
ctx.beginPath(); ctx.arc(mx, my, dropR, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
|
|
ctx.font = '8px Orbitron, monospace';
|
|
ctx.letterSpacing = '1px';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = '#3a6080';
|
|
ctx.fillText(`S${slotIndex + 1}`, mx, my - (installed ? 8 : 0));
|
|
ctx.letterSpacing = '0px';
|
|
|
|
if (installed) {
|
|
_dragRegions.push({ x: mx - dropR, y: my - dropR, w: dropR * 2, h: dropR * 2, weapon, source: { type: 'slot', slotIndex } });
|
|
const def = getWeaponDef(weapon);
|
|
ctx.font = '14px monospace';
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillText(def?.icon || '?', mx, my + 5);
|
|
} else {
|
|
ctx.font = '14px "Share Tech Mono", monospace';
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = '#1a3240';
|
|
ctx.fillText('+', mx, my);
|
|
}
|
|
|
|
_mountDropZones.push({ x: mx, y: my, r: dropR, slotIndex });
|
|
addHitRegion(mx - dropR, my - dropR, dropR * 2, dropR * 2, () => openWeaponPicker(slotIndex));
|
|
if (hov && installed) _drawMountTooltip(mx, my, weapon, dropR);
|
|
} else {
|
|
// inventory closed: invisible hit region — click opens picker for this slot
|
|
const mount = getSlotHardpoint(slotIndex, cx, cy, totalSlots);
|
|
const r = 10;
|
|
addHitRegion(mount.x - r, mount.y - r, r * 2, r * 2, () => openWeaponPicker(slotIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── DRAG GHOST ────────────────────────────────────────────────
|
|
function drawDragGhost() {
|
|
if (!_dragWeapon || !_hoverPt) return;
|
|
const def = getWeaponDef(_dragWeapon);
|
|
if (!def) return;
|
|
const GW = 90, GH = 68;
|
|
const gx = _hoverPt.x - GW / 2;
|
|
const gy = _hoverPt.y - GH / 2;
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.85;
|
|
ctx.fillStyle = '#060e16';
|
|
ctx.strokeStyle = '#ffd700';
|
|
ctx.lineWidth = 2;
|
|
ctx.fillRect(gx, gy, GW, GH);
|
|
ctx.strokeRect(gx, gy, GW, GH);
|
|
ctx.globalAlpha = 1;
|
|
ctx.font = '22px monospace';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillText(def.icon || '?', gx + GW / 2, gy + GH / 2 - 8);
|
|
ctx.font = '8px Orbitron, monospace';
|
|
ctx.fillStyle = '#b8d8e8';
|
|
ctx.fillText(def.name, gx + GW / 2, gy + GH / 2 + 14);
|
|
ctx.restore();
|
|
}
|