← RPG results

RPG

Gemini 3.7 Flash · full

Initial view of RPG

Browser check: Artifact runtime error. This is a technical check, not a quality review.

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 111,085 bytes · SHA-256 85677dfbf644
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Chronicles of the Sunken Spire - Action RPG</title>
<style>
  :root {
    --bg-dark: #0a0c10;
    --panel-bg: rgba(16, 22, 34, 0.92);
    --border-gold: #c69b3f;
    --border-gold-glow: #e6c66d;
    --accent-red: #d32f2f;
    --accent-cyan: #26c6da;
    --accent-green: #66bb6a;
    --text-gold: #f5dfa2;
    --text-main: #e0e6ed;
    --font-ui: 'Cinzel', 'Trajan Pro', 'Georgia', serif;
    --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  }

  * {
    box-sizing: border-box;
    user-select: none;
    margin: 0;
    padding: 0;
    -webkit-tap-highlight-color: transparent;
  }

  body {
    background-color: var(--bg-dark);
    color: var(--text-main);
    font-family: var(--font-sans);
    overflow: hidden;
    width: 100vw;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  #game-wrapper {
    position: relative;
    width: 100%;
    height: 100%;
    max-width: 1280px;
    max-height: 720px;
    background: #000;
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.9), 0 0 15px rgba(198, 155, 63, 0.2);
    overflow: hidden;
  }

  canvas#gameCanvas {
    display: block;
    width: 100%;
    height: 100%;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
  }

  /* HUD OVERLAY */
  #hud {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 16px 20px;
    font-family: var(--font-ui);
  }

  .hud-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
  }

  .hud-left {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .heart-container-bar {
    display: flex;
    gap: 6px;
    filter: drop-shadow(0 2px 4px rgba(0,0,0,0.8));
    align-items: center;
  }

  .heart-icon {
    width: 28px;
    height: 28px;
    transition: transform 0.15s ease;
  }

  .heart-icon.pulse {
    animation: heartBeat 0.4s ease;
  }

  @keyframes heartBeat {
    0% { transform: scale(1); }
    50% { transform: scale(1.35); }
    100% { transform: scale(1); }
  }

  .stamina-bar-wrap {
    width: 150px;
    height: 10px;
    background: rgba(0, 0, 0, 0.6);
    border: 1.5px solid rgba(255, 255, 255, 0.3);
    border-radius: 4px;
    overflow: hidden;
  }

  .stamina-fill {
    height: 100%;
    width: 100%;
    background: linear-gradient(90deg, #2e7d32, #66bb6a);
    transition: width 0.08s linear;
  }

  .hud-stats {
    display: flex;
    gap: 16px;
    margin-top: 4px;
    font-size: 14px;
    color: var(--text-gold);
    text-shadow: 0 1px 3px #000;
  }

  .stat-badge {
    background: rgba(16, 24, 38, 0.85);
    border: 1px solid var(--border-gold);
    padding: 4px 10px;
    border-radius: 4px;
    display: flex;
    align-items: center;
    gap: 6px;
  }

  .hud-right {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 8px;
  }

  .minimap-card {
    width: 130px;
    height: 100px;
    background: rgba(10, 15, 25, 0.85);
    border: 2px solid var(--border-gold);
    border-radius: 6px;
    position: relative;
    overflow: hidden;
    box-shadow: 0 4px 12px rgba(0,0,0,0.6);
  }

  .minimap-card canvas {
    width: 100%;
    height: 100%;
  }

  .zone-name {
    position: absolute;
    bottom: 2px;
    right: 4px;
    font-size: 9px;
    color: var(--text-gold);
    letter-spacing: 0.5px;
    text-shadow: 0 1px 2px #000;
  }

  .hud-bottom {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
  }

  .hud-weapons {
    display: flex;
    gap: 10px;
  }

  .equip-slot {
    width: 54px;
    height: 54px;
    background: rgba(16, 22, 34, 0.88);
    border: 2px solid var(--border-gold);
    border-radius: 8px;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 10px rgba(0,0,0,0.7);
  }

  .equip-slot.active {
    border-color: #ffd54f;
    box-shadow: 0 0 12px rgba(255, 213, 79, 0.6);
  }

  .equip-slot .key-hint {
    position: absolute;
    top: 2px;
    left: 4px;
    font-size: 10px;
    color: #a0b0c0;
  }

  .equip-slot .ammo-badge {
    position: absolute;
    bottom: 2px;
    right: 4px;
    font-size: 11px;
    color: #fff;
    font-weight: bold;
    text-shadow: 0 1px 3px #000;
  }

  .boss-health-bar-container {
    position: absolute;
    top: 60px;
    left: 50%;
    transform: translateX(-50%);
    width: 450px;
    background: rgba(15, 20, 30, 0.9);
    border: 2px solid #b71c1c;
    border-radius: 6px;
    padding: 6px 12px;
    box-shadow: 0 0 20px rgba(183, 28, 28, 0.5);
    display: none;
    flex-direction: column;
    gap: 4px;
    text-align: center;
  }

  .boss-health-bar-container.active {
    display: flex;
  }

  .boss-name {
    font-size: 14px;
    color: #ff8a80;
    letter-spacing: 1px;
  }

  .boss-hp-bg {
    width: 100%;
    height: 14px;
    background: #2b0b0b;
    border-radius: 3px;
    overflow: hidden;
  }

  .boss-hp-fill {
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, #d32f2f, #ff5252);
    transition: width 0.15s ease-out;
  }

  /* MODALS AND OVERLAYS */
  .modal-overlay {
    position: absolute;
    inset: 0;
    background: rgba(5, 8, 14, 0.82);
    backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
    pointer-events: auto;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease, visibility 0.2s;
  }

  .modal-overlay.open {
    opacity: 1;
    visibility: visible;
  }

  .modal-box {
    background: var(--panel-bg);
    border: 2px solid var(--border-gold);
    border-radius: 12px;
    box-shadow: 0 0 30px rgba(0,0,0,0.9), 0 0 15px rgba(198, 155, 63, 0.25);
    padding: 24px 28px;
    max-width: 600px;
    width: 90%;
    color: var(--text-main);
    font-family: var(--font-sans);
    position: relative;
  }

  .modal-title {
    font-family: var(--font-ui);
    font-size: 22px;
    color: var(--text-gold);
    border-bottom: 1px solid rgba(198, 155, 63, 0.3);
    padding-bottom: 10px;
    margin-bottom: 16px;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .close-btn {
    background: transparent;
    border: none;
    color: #a0b0c0;
    font-size: 22px;
    cursor: pointer;
    line-height: 1;
    transition: color 0.15s;
  }
  .close-btn:hover { color: #fff; }

  /* INVENTORY GRID */
  .inventory-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
    margin-bottom: 20px;
  }

  .inv-item-card {
    background: rgba(26, 35, 52, 0.7);
    border: 1px solid rgba(198, 155, 63, 0.4);
    border-radius: 8px;
    padding: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    cursor: pointer;
    transition: transform 0.15s, border-color 0.15s, background-color 0.15s;
    position: relative;
  }

  .inv-item-card:hover {
    border-color: var(--border-gold-glow);
    background: rgba(36, 48, 70, 0.85);
    transform: translateY(-2px);
  }

  .inv-item-card.equipped {
    border-color: #66bb6a;
    box-shadow: 0 0 10px rgba(102, 187, 106, 0.35);
  }

  .inv-item-card .item-icon {
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .inv-item-card .item-name {
    font-size: 12px;
    text-align: center;
    font-weight: 500;
  }

  .inv-item-card .item-count {
    position: absolute;
    top: 4px;
    right: 6px;
    font-size: 11px;
    color: #ffca28;
    font-weight: bold;
  }

  .item-desc-panel {
    background: rgba(10, 14, 22, 0.6);
    border-radius: 6px;
    padding: 12px 14px;
    font-size: 13px;
    line-height: 1.4;
    min-height: 60px;
    border-left: 3px solid var(--border-gold);
    color: #c9d5e3;
  }

  /* SHOP UI */
  .shop-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-height: 320px;
    overflow-y: auto;
    padding-right: 4px;
    margin-bottom: 16px;
  }

  .shop-item-row {
    background: rgba(22, 30, 46, 0.8);
    border: 1px solid rgba(198, 155, 63, 0.3);
    border-radius: 8px;
    padding: 10px 14px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    transition: background 0.15s, border-color 0.15s;
  }

  .shop-item-row:hover {
    background: rgba(30, 42, 64, 0.95);
    border-color: var(--border-gold);
  }

  .shop-item-info {
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .btn-buy {
    background: linear-gradient(135deg, #b8860b, #d4af37);
    border: 1px solid #ffe082;
    color: #1a1200;
    font-family: var(--font-ui);
    font-weight: bold;
    padding: 6px 14px;
    border-radius: 6px;
    cursor: pointer;
    transition: filter 0.15s, transform 0.1s;
  }

  .btn-buy:hover {
    filter: brightness(1.2);
    transform: scale(1.04);
  }

  .btn-buy:disabled {
    background: #3e4450;
    border-color: #555e6d;
    color: #8892a0;
    cursor: not-allowed;
    transform: none;
  }

  /* DIALOGUE BOX */
  #dialogue-overlay {
    position: absolute;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    width: 85%;
    max-width: 780px;
    background: rgba(14, 19, 29, 0.94);
    border: 2px solid var(--border-gold);
    border-radius: 10px;
    padding: 16px 20px;
    z-index: 40;
    box-shadow: 0 8px 24px rgba(0,0,0,0.8);
    display: none;
    pointer-events: auto;
  }

  #dialogue-overlay.active {
    display: flex;
    gap: 16px;
    animation: dialogSlide 0.2s ease-out;
  }

  @keyframes dialogSlide {
    from { opacity: 0; transform: translate(-50%, 15px); }
    to { opacity: 1; transform: translate(-50%, 0); }
  }

  .dialog-portrait {
    width: 64px;
    height: 64px;
    border: 2px solid var(--border-gold);
    border-radius: 8px;
    background: #1c2638;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .dialog-content {
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }

  .dialog-speaker {
    font-family: var(--font-ui);
    font-size: 15px;
    color: var(--text-gold);
    margin-bottom: 4px;
  }

  .dialog-text {
    font-size: 14px;
    line-height: 1.45;
    color: #e2e8f0;
    min-height: 38px;
  }

  .dialog-hint {
    align-self: flex-end;
    font-size: 11px;
    color: #8fa1b8;
    font-family: var(--font-ui);
    margin-top: 4px;
  }

  /* BANNER NOTIFICATION */
  #banner-notify {
    position: absolute;
    top: 80px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(14, 20, 32, 0.92);
    border: 2px solid var(--border-gold-glow);
    padding: 10px 24px;
    border-radius: 30px;
    color: var(--text-gold);
    font-family: var(--font-ui);
    font-size: 16px;
    letter-spacing: 1px;
    box-shadow: 0 0 20px rgba(230, 198, 109, 0.4);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s, transform 0.3s;
    z-index: 45;
  }

  #banner-notify.show {
    opacity: 1;
    transform: translate(-50%, -8px);
  }

  /* TITLE & GAME OVER SCREENS */
  .screen-overlay {
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at center, #172338 0%, #080c14 100%);
    z-index: 60;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 20px;
  }

  .screen-title {
    font-family: var(--font-ui);
    font-size: 38px;
    color: #f7e7b4;
    text-shadow: 0 0 15px rgba(245, 223, 162, 0.5), 0 4px 8px #000;
    margin-bottom: 8px;
    letter-spacing: 2px;
  }

  .screen-subtitle {
    font-size: 16px;
    color: #94a3b8;
    margin-bottom: 30px;
    max-width: 480px;
    line-height: 1.5;
  }

  .btn-primary {
    background: linear-gradient(135deg, #c69b3f, #e6c66d);
    border: 1px solid #fff;
    color: #1a1200;
    font-family: var(--font-ui);
    font-size: 16px;
    font-weight: bold;
    padding: 12px 32px;
    border-radius: 8px;
    cursor: pointer;
    box-shadow: 0 4px 15px rgba(0,0,0,0.5), 0 0 20px rgba(230, 198, 109, 0.4);
    transition: transform 0.15s, filter 0.15s;
    margin: 6px;
  }

  .btn-primary:hover {
    transform: translateY(-2px) scale(1.03);
    filter: brightness(1.15);
  }

  .controls-help-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 8px 18px;
    background: rgba(10, 15, 24, 0.6);
    border: 1px solid rgba(198, 155, 63, 0.3);
    padding: 14px 20px;
    border-radius: 8px;
    margin-bottom: 24px;
    font-size: 13px;
    text-align: left;
    color: #cbd5e1;
  }

  .controls-help-grid strong {
    color: var(--text-gold);
  }

  /* TOUCH CONTROLS (for mobile / tablet) */
  #touch-controls {
    position: absolute;
    inset: 0;
    pointer-events: none;
    display: none;
    z-index: 30;
  }

  .touch-stick-zone {
    position: absolute;
    bottom: 20px;
    left: 20px;
    width: 130px;
    height: 130px;
    background: rgba(255, 255, 255, 0.08);
    border: 2px dashed rgba(255, 255, 255, 0.2);
    border-radius: 50%;
    pointer-events: auto;
    touch-action: none;
  }

  .touch-stick-knob {
    position: absolute;
    top: 40px;
    left: 40px;
    width: 50px;
    height: 50px;
    background: rgba(230, 198, 109, 0.5);
    border: 2px solid var(--border-gold);
    border-radius: 50%;
    pointer-events: none;
  }

  .touch-buttons-zone {
    position: absolute;
    bottom: 20px;
    right: 20px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    pointer-events: auto;
  }

  .touch-btn-row {
    display: flex;
    gap: 12px;
    justify-content: flex-end;
  }

  .touch-action-btn {
    width: 54px;
    height: 54px;
    border-radius: 50%;
    background: rgba(16, 24, 38, 0.85);
    border: 2px solid var(--border-gold);
    color: var(--text-gold);
    font-family: var(--font-ui);
    font-size: 14px;
    font-weight: bold;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 10px rgba(0,0,0,0.5);
  }

  .touch-action-btn:active {
    background: rgba(230, 198, 109, 0.4);
    transform: scale(0.92);
  }

  @media (hover: none) and (pointer: coarse) {
    #touch-controls { display: block; }
  }
</style>
</head>
<body>

<div id="game-wrapper">
  <!-- Core Rendering Canvas -->
  <canvas id="gameCanvas" width="1280" height="720"></canvas>

  <!-- HUD Layer -->
  <div id="hud">
    <div class="hud-top">
      <div class="hud-left">
        <div class="heart-container-bar" id="heart-bar" title="Health"></div>
        <div class="stamina-bar-wrap" title="Stamina / Dash Energy">
          <div class="stamina-fill" id="stamina-fill"></div>
        </div>
        <div class="hud-stats">
          <div class="stat-badge" title="Glimmer Gold">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="#ffd700"><polygon points="12 2 15 8 22 9 17 14 18 21 12 17 6 21 7 14 2 9 9 8 12 2"/></svg>
            <span id="gold-count">0</span>
          </div>
          <div class="stat-badge" title="Ancient Keys">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="#e0e0e0"><path d="M7 14A5 5 0 1 1 12 9a5 5 0 0 1-5 5zm0-8a3 3 0 1 0 3 3 3 3 0 0 0-3-3zm14 9.5l-6-6V7h-2v2.5l5.5 5.5H19v2h2v-1.5z"/></svg>
            <span id="key-count">0</span>
          </div>
        </div>
      </div>

      <div class="hud-right">
        <div class="minimap-card">
          <canvas id="minimapCanvas" width="130" height="100"></canvas>
          <div class="zone-name" id="zone-display-name">Whispering Grove</div>
        </div>
      </div>
    </div>

    <!-- Boss Health Bar (When active) -->
    <div class="boss-health-bar-container" id="boss-bar-wrap">
      <div class="boss-name" id="boss-name-label">Crypt Warden</div>
      <div class="boss-hp-bg">
        <div class="boss-hp-fill" id="boss-hp-fill"></div>
      </div>
    </div>

    <div class="hud-bottom">
      <div class="hud-weapons">
        <div class="equip-slot active" id="slot-sword" title="Melee Sword [LMB / Space]">
          <span class="key-hint">LMB</span>
          <div id="sword-icon-container"></div>
        </div>
        <div class="equip-slot" id="slot-bow" style="display:none;" title="Mystic Bow [RMB / F]">
          <span class="key-hint">RMB</span>
          <div id="bow-icon-container"></div>
          <span class="ammo-badge" id="arrow-count">∞</span>
        </div>
        <div class="equip-slot" id="slot-bomb" title="Bomb / Urn [Q]">
          <span class="key-hint">Q</span>
          <div id="bomb-icon-container"></div>
          <span class="ammo-badge" id="bomb-count">0</span>
        </div>
        <div class="equip-slot" id="slot-inv" style="cursor:pointer; pointer-events:auto;" onclick="game.toggleInventory()" title="Open Inventory [I / Tab]">
          <span class="key-hint">I</span>
          <svg width="26" height="26" viewBox="0 0 24 24" fill="#c69b3f"><path d="M20 6h-4V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zm-10-2h4v2h-4V4zm10 16H4V8h16v12z"/></svg>
        </div>
      </div>
      <div style="color: #94a3b8; font-size: 11px; text-shadow: 0 1px 3px #000; letter-spacing: 0.5px;">
        [SPACE/LMB] Attack &nbsp;|&nbsp; [SHIFT] Dash &nbsp;|&nbsp; [E] Interact/Save &nbsp;|&nbsp; [I] Bag
      </div>
    </div>
  </div>

  <!-- Notification Banner -->
  <div id="banner-notify">Progress Saved at Ancient Shrine</div>

  <!-- Dialogue Overlay -->
  <div id="dialogue-overlay">
    <div class="dialog-portrait" id="dialog-portrait"></div>
    <div class="dialog-content">
      <div>
        <div class="dialog-speaker" id="dialog-speaker">Elder Orin</div>
        <div class="dialog-text" id="dialog-text">The Spire has awakened... Take this blade and restore the seals.</div>
      </div>
      <div class="dialog-hint">Press [E] or [Space] to continue</div>
    </div>
  </div>

  <!-- Inventory Modal -->
  <div class="modal-overlay" id="inventory-modal">
    <div class="modal-box">
      <div class="modal-title">
        <span>Adventurer's Satchel</span>
        <button class="close-btn" onclick="game.toggleInventory()">&times;</button>
      </div>
      <div class="inventory-grid" id="inventory-grid"></div>
      <div class="item-desc-panel" id="inv-item-description">
        Select an item to view properties, bonuses, and equip status.
      </div>
    </div>
  </div>

  <!-- Shop Modal -->
  <div class="modal-overlay" id="shop-modal">
    <div class="modal-box">
      <div class="modal-title">
        <span>Outpost Merchant</span>
        <button class="close-btn" onclick="game.closeShop()">&times;</button>
      </div>
      <div class="shop-list" id="shop-items-container"></div>
      <div style="font-size: 13px; color: var(--text-gold); display:flex; justify-content:space-between; align-items:center;">
        <span>Your Gold: <strong id="shop-gold-display">0</strong> G</span>
        <span style="color:#94a3b8; font-size:11px;">Restocks whenever you return from exploration</span>
      </div>
    </div>
  </div>

  <!-- Title / Start Screen -->
  <div class="screen-overlay" id="title-screen">
    <div class="screen-title">CHRONICLES OF THE SUNKEN SPIRE</div>
    <div class="screen-subtitle">
      Explore ancient forgotten realms, master blade & bow, conquer guardian bosses, and shatter the seal of the Void Sovereign.
    </div>
    <div class="controls-help-grid">
      <div><strong>WASD / Arrow Keys</strong>: Move Character</div>
      <div><strong>Left Click / Space</strong>: Sword Attack</div>
      <div><strong>Right Click / F</strong>: Ranged Bow</div>
      <div><strong>Space / Shift</strong>: Evasive Dash Roll</div>
      <div><strong>E</strong>: Interact / Talk / Save Shrine</div>
      <div><strong>Q / 1-3</strong>: Use Bomb & Potions</div>
      <div><strong>I / Tab</strong>: Inventory & Equipment</div>
      <div><strong>Audio Synth</strong>: Auto-tuned Dynamic Audio</div>
    </div>
    <button class="btn-primary" id="btn-start-game">BEGIN ADVENTURE</button>
    <button class="btn-primary" id="btn-continue-game" style="display:none; background: #263238; border-color:#546e7a; color:#cfd8dc;">CONTINUE CHECKPOINT</button>
  </div>

  <!-- Game Over Screen -->
  <div class="screen-overlay" id="gameover-screen" style="display:none;">
    <div class="screen-title" style="color: #ef5350;">YOU HAVE FALLEN</div>
    <div class="screen-subtitle">
      The void energy overtook your spirit. Awaken at the last consecrated shrine with renewed determination.
    </div>
    <button class="btn-primary" id="btn-respawn">RESPAWN AT SAVE SHRINE</button>
  </div>

  <!-- Victory / Game Completed Screen -->
  <div class="screen-overlay" id="victory-screen" style="display:none;">
    <div class="screen-title" style="color: #ffd54f;">THE SPIRE IS LIBERATED!</div>
    <div class="screen-subtitle" id="victory-stats-text">
      You conquered all five heart containers, mastered the Stormcaller blade and Mystic bow, and sealed away the Void Sovereign forever!
    </div>
    <button class="btn-primary" onclick="location.reload()">PLAY AGAIN</button>
  </div>

  <!-- Touch Controls -->
  <div id="touch-controls">
    <div class="touch-stick-zone" id="touch-joystick">
      <div class="touch-stick-knob" id="touch-knob"></div>
    </div>
    <div class="touch-buttons-zone">
      <div class="touch-btn-row">
        <div class="touch-action-btn" id="btn-touch-bow" style="display:none;">BOW</div>
        <div class="touch-action-btn" id="btn-touch-bomb">BOMB</div>
        <div class="touch-action-btn" id="btn-touch-interact">E</div>
      </div>
      <div class="touch-btn-row">
        <div class="touch-action-btn" id="btn-touch-dash" style="background:#1e3a5f;">DASH</div>
        <div class="touch-action-btn" id="btn-touch-attack" style="width:64px; height:64px; background:#6b2020;">ATK</div>
      </div>
    </div>
  </div>
</div>

<script>
/* ==========================================================================
   CHRONICLES OF THE SUNKEN SPIRE - ACTION-ADVENTURE ENGINE
   ========================================================================== */

// --- PROCEDURAL AUDIO SYNTHESIZER (WEB AUDIO API) ---
class SoundEngine {
  constructor() {
    this.ctx = null;
    this.masterGain = null;
    this.muted = false;
  }

  init() {
    if (this.ctx) return;
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    this.ctx = new AudioCtx();
    this.masterGain = this.ctx.createGain();
    this.masterGain.gain.value = 0.28;
    this.masterGain.connect(this.ctx.destination);
  }

  ensureState() {
    if (!this.ctx) this.init();
    if (this.ctx && this.ctx.state === 'suspended') {
      this.ctx.resume();
    }
  }

  playStep() {
    this.ensureState();
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    const filter = this.ctx.createBiquadFilter();
    
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(80 + Math.random() * 30, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + 0.05);

    filter.type = 'lowpass';
    filter.frequency.value = 400;

    gain.gain.setValueAtTime(0.04, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.05);

    osc.connect(filter);
    filter.connect(gain);
    gain.connect(this.masterGain);

    osc.start();
    osc.stop(this.ctx.currentTime + 0.06);
  }

  playAttack(tier = 1) {
    this.ensureState();
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    const now = this.ctx.currentTime;

    if (tier === 1) {
      // Rusty Slash
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(320, now);
      osc.frequency.exponentialRampToValueAtTime(80, now + 0.12);
      gain.gain.setValueAtTime(0.18, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + 0.12);
    } else if (tier === 2) {
      // Sunforged Fire Slash
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(540, now);
      osc.frequency.exponentialRampToValueAtTime(140, now + 0.16);
      gain.gain.setValueAtTime(0.24, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + 0.16);
    } else {
      // Stormcaller Lightning Cleave
      osc.type = 'square';
      osc.frequency.setValueAtTime(680, now);
      osc.frequency.exponentialRampToValueAtTime(160, now + 0.22);
      gain.gain.setValueAtTime(0.3, now);
      gain.gain.exponentialRampToValueAtTime(0.01, now + 0.22);
    }

    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start(now);
    osc.stop(now + 0.23);
  }

  playBow() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'sine';
    osc.frequency.setValueAtTime(380, now);
    osc.frequency.exponentialRampToValueAtTime(840, now + 0.08);

    gain.gain.setValueAtTime(0.25, now);
    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);

    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start(now);
    osc.stop(now + 0.16);
  }

  playHit() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'square';
    osc.frequency.setValueAtTime(200, now);
    osc.frequency.exponentialRampToValueAtTime(50, now + 0.09);

    gain.gain.setValueAtTime(0.22, now);
    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.09);

    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start(now);
    osc.stop(now + 0.1);
  }

  playHurt() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(140, now);
    osc.frequency.exponentialRampToValueAtTime(40, now + 0.18);

    gain.gain.setValueAtTime(0.3, now);
    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.18);

    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start(now);
    osc.stop(now + 0.2);
  }

  playCoin() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'sine';
    osc.frequency.setValueAtTime(987, now); // B5
    osc.frequency.setValueAtTime(1318, now + 0.07); // E6

    gain.gain.setValueAtTime(0.18, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);

    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start(now);
    osc.stop(now + 0.26);
  }

  playPowerup() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const notes = [440, 554, 659, 880, 1108];
    notes.forEach((freq, idx) => {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'triangle';
      osc.frequency.value = freq;
      const t = now + idx * 0.06;
      gain.gain.setValueAtTime(0.2, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
      osc.connect(gain);
      gain.connect(this.masterGain);
      osc.start(t);
      osc.stop(t + 0.2);
    });
  }

  playSave() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const chords = [523.25, 659.25, 783.99, 1046.50];
    chords.forEach((freq, idx) => {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.value = freq;
      gain.gain.setValueAtTime(0.15, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6 + idx * 0.1);
      osc.connect(gain);
      gain.connect(this.masterGain);
      osc.start(now);
      osc.stop(now + 0.8);
    });
  }

  playDash() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'sine';
    osc.frequency.setValueAtTime(280, now);
    osc.frequency.exponentialRampToValueAtTime(600, now + 0.1);

    gain.gain.setValueAtTime(0.2, now);
    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);

    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start(now);
    osc.stop(now + 0.16);
  }

  playExplosion() {
    this.ensureState();
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(120, now);
    osc.frequency.exponentialRampToValueAtTime(25, now + 0.35);

    gain.gain.setValueAtTime(0.35, now);
    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);

    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start(now);
    osc.stop(now + 0.42);
  }
}

const sounds = new SoundEngine();

// --- VECTOR & MATH UTILS ---
const dist = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
const clamp = (val, min, max) => Math.max(min, Math.min(max, val));

// --- PARTICLE ENGINE ---
class ParticleSystem {
  constructor() {
    this.particles = [];
  }

  emit(x, y, count = 5, options = {}) {
    for (let i = 0; i < count; i++) {
      const angle = options.angle !== undefined ? options.angle + (Math.random() - 0.5) * (options.spread || 0.8) : Math.random() * Math.PI * 2;
      const speed = options.speed ? options.speed * (0.5 + Math.random() * 0.8) : 1 + Math.random() * 3;
      this.particles.push({
        x,
        y,
        vx: Math.cos(angle) * speed + (options.vx || 0),
        vy: Math.sin(angle) * speed + (options.vy || 0),
        size: options.size || (2 + Math.random() * 3),
        color: options.color || '#ffd54f',
        alpha: 1,
        life: options.life || (0.3 + Math.random() * 0.4),
        maxLife: options.life || (0.3 + Math.random() * 0.4),
        shape: options.shape || 'circle'
      });
    }
  }

  update(dt) {
    for (let i = this.particles.length - 1; i >= 0; i--) {
      const p = this.particles[i];
      p.x += p.vx * 60 * dt;
      p.y += p.vy * 60 * dt;
      p.life -= dt;
      p.alpha = Math.max(0, p.life / p.maxLife);
      if (p.life <= 0) {
        this.particles.splice(i, 1);
      }
    }
  }

  render(ctx, camera) {
    for (const p of this.particles) {
      const sx = p.x - camera.x;
      const sy = p.y - camera.y;
      ctx.save();
      ctx.globalAlpha = p.alpha;
      ctx.fillStyle = p.color;
      if (p.shape === 'circle') {
        ctx.beginPath();
        ctx.arc(sx, sy, p.size, 0, Math.PI * 2);
        ctx.fill();
      } else if (p.shape === 'spark') {
        ctx.fillRect(sx - p.size, sy - p.size * 0.4, p.size * 2, p.size * 0.8);
      } else if (p.shape === 'ring') {
        ctx.strokeStyle = p.color;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(sx, sy, (1 - p.alpha) * 25, 0, Math.PI * 2);
        ctx.stroke();
      }
      ctx.restore();
    }
  }
}

// --- ITEM & INVENTORY DATABASE ---
const ITEM_DB = {
  // Consumables & Upgrades
  potion_heal: {
    id: 'potion_heal',
    name: 'Elixir of Vitality',
    type: 'consumable',
    desc: 'Instantly restores 4 points of Health (2 full hearts).',
    price: 35,
    icon: '🧪',
    use: (player) => {
      if (player.hp >= player.maxHp) return false;
      player.hp = Math.min(player.maxHp, player.hp + 4);
      sounds.playPowerup();
      game.notify('Restored 4 Health points!');
      return true;
    }
  },
  bomb_pack: {
    id: 'bomb_pack',
    name: 'Satchel of Fire Urns',
    type: 'consumable',
    desc: 'Explosive throwables that crack sealed walls & devastate armored foes.',
    price: 30,
    icon: '💣',
    use: (player) => {
      player.bombs += 3;
      sounds.playPowerup();
      game.notify('+3 Fire Urns added to Satchel!');
      return true;
    }
  },
  heart_container: {
    id: 'heart_container',
    name: 'Heart Vessel of Valor',
    type: 'upgrade',
    desc: 'Permanently increases maximum health by 1 full Heart Container (2 HP).',
    price: 150,
    icon: '💖',
    use: (player) => {
      if (player.heartContainers >= 5) {
        game.notify('Maximum Heart Containers (5) reached!');
        return false;
      }
      player.heartContainers++;
      player.maxHp = player.heartContainers * 2;
      player.hp = player.maxHp;
      sounds.playPowerup();
      game.notify(`Heart Capacity Expanded to ${player.heartContainers} Containers!`);
      return true;
    }
  },

  // 5 Accessories & Artifacts
  swift_boots: {
    id: 'swift_boots',
    name: 'Swiftwind Boots',
    type: 'accessory',
    desc: 'Enchanted leather boots. Increases movement speed by 30% and reduces dash cooldown.',
    price: 80,
    icon: '👢',
    bonus: { speedMult: 1.3, dashCooldownMult: 0.7 }
  },
  amulet_thorns: {
    id: 'amulet_thorns',
    name: 'Amulet of Thorns',
    type: 'accessory',
    desc: 'Spiked brass talisman. Reflects 50% melee damage back onto attackers.',
    price: 110,
    icon: '🧿',
    bonus: { reflectDmg: true }
  },
  gold_ring: {
    id: 'gold_ring',
    name: 'Ring of the Alchemist',
    type: 'accessory',
    desc: 'Doubles currency drops from fallen foes and grass, with wider pickup vacuum.',
    price: 90,
    icon: '💍',
    bonus: { goldMult: 2.0, magnetRadius: 90 }
  },
  iron_buckler: {
    id: 'iron_buckler',
    name: 'Iron Aegis Shield',
    type: 'accessory',
    desc: 'Sturdy buckler. Absorbs 1 damage point from all incoming frontal attacks.',
    price: 120,
    icon: '🛡️',
    bonus: { defense: 1 }
  },
  berserk_talisman: {
    id: 'berserk_talisman',
    name: 'Bloodstone Talisman',
    type: 'accessory',
    desc: 'Infuses blade with ferocious fury. Increases weapon damage by +1 when HP is below 50%.',
    price: 140,
    icon: '🩸',
    bonus: { berserk: true }
  }
};

// --- WEAPON EVOLUTION TIERS ---
const WEAPONS = {
  SWORD: [
    {
      tier: 1,
      name: 'Rusty Broadsword',
      damage: 1.0,
      range: 48,
      arc: Math.PI * 0.75,
      speed: 0.28,
      color: '#b0bec5',
      trailColor: 'rgba(200, 215, 230, 0.4)',
      desc: 'An aged iron blade. Basic reach and dependable balance.'
    },
    {
      tier: 2,
      name: 'Sunforged Broadsword',
      damage: 2.0,
      range: 58,
      arc: Math.PI * 0.95,
      speed: 0.22,
      color: '#ff9800',
      trailColor: 'rgba(255, 152, 0, 0.55)',
      desc: 'Tempered in solar magma. Emits fiery sweeping slashes that ignite brush & monsters.'
    },
    {
      tier: 3,
      name: 'Stormcaller Greatblade',
      damage: 3.5,
      range: 72,
      arc: Math.PI * 1.25,
      speed: 0.18,
      color: '#00e5ff',
      trailColor: 'rgba(0, 229, 255, 0.65)',
      desc: 'Crackles with celestial lightning. Devastating wide sweep and unleashed energy arcs!'
    }
  ],
  BOW: {
    name: 'Mystic Quiver Bow',
    damage: 1.5,
    projectileSpeed: 520,
    range: 450,
    color: '#64ffda',
    desc: 'Uncovered in the Sunken Crypts. Launches crystalline arrows that hit distant switches & snipers.'
  }
};

// --- MAP ZONES DEFINITIONS ---
const ZONES = {
  VILLAGE: 0,
  OVERWORLD: 1,
  CRYPT: 2,
  SPIRE: 3,
  SANCTUM: 4
};

const ZONE_CONFIGS = [
  {
    id: ZONES.VILLAGE,
    name: 'Whispering Grove (Town)',
    ambientColor: '#102214',
    musicScale: 'peaceful',
    width: 28,
    height: 20
  },
  {
    id: ZONES.OVERWORLD,
    name: 'Verdant Wilds & Ancient Gates',
    ambientColor: '#0b1c1b',
    musicScale: 'adventure',
    width: 36,
    height: 26
  },
  {
    id: ZONES.CRYPT,
    name: 'Sunken Crypts (Mid-Game)',
    ambientColor: '#0d111d',
    musicScale: 'dungeon',
    width: 34,
    height: 24
  },
  {
    id: ZONES.SPIRE,
    name: 'Storm Peaks & Spire Ascent',
    ambientColor: '#170f26',
    musicScale: 'storm',
    width: 36,
    height: 28
  },
  {
    id: ZONES.SANCTUM,
    name: 'Sanctum of the Void Sovereign',
    ambientColor: '#1d0a14',
    musicScale: 'boss',
    width: 30,
    height: 22
  }
];

// --- WORLD TILEMAP & ENTITY ENGINE ---
const TILE_SIZE = 40;

const TILES = {
  GRASS: 0,
  DIRT: 1,
  STONE_FLOOR: 2,
  WALL: 3,
  WATER: 4,
  DOOR: 5,
  CRACKED_WALL: 6,
  SHRINE: 7,
  SHOP_CARPET: 8,
  LAVA: 9,
  VOID_FLOOR: 10
};

class World {
  constructor(game) {
    this.game = game;
    this.currentZoneId = ZONES.VILLAGE;
    this.zonesData = {};
    this.initAllZones();
  }

  initAllZones() {
    ZONE_CONFIGS.forEach(cfg => {
      this.zonesData[cfg.id] = this.generateZone(cfg);
    });
  }

  generateZone(cfg) {
    const W = cfg.width;
    const H = cfg.height;
    const map = [];
    for (let y = 0; y < H; y++) {
      const row = [];
      for (let x = 0; x < W; x++) {
        // Border walls
        if (x === 0 || x === W - 1 || y === 0 || y === H - 1) {
          row.push(TILES.WALL);
        } else {
          if (cfg.id === ZONES.VILLAGE) {
            row.push(Math.random() < 0.2 ? TILES.DIRT : TILES.GRASS);
          } else if (cfg.id === ZONES.CRYPT || cfg.id === ZONES.SANCTUM) {
            row.push(TILES.STONE_FLOOR);
          } else if (cfg.id === ZONES.SPIRE) {
            row.push(Math.random() < 0.15 ? TILES.LAVA : TILES.STONE_FLOOR);
          } else {
            row.push(Math.random() < 0.25 ? TILES.DIRT : TILES.GRASS);
          }
        }
      }
      map.push(row);
    }

    const props = [];
    const enemies = [];
    const npcs = [];
    const interactables = [];

    // Customize per zone
    if (cfg.id === ZONES.VILLAGE) {
      // Village Save Shrine
      map[7][7] = TILES.SHRINE;
      interactables.push({
        type: 'shrine',
        x: 7 * TILE_SIZE + 20,
        y: 7 * TILE_SIZE + 20,
        name: 'Grove Haven Shrine'
      });

      // Shop area
      for (let sx = 18; sx <= 22; sx++) {
        for (let sy = 5; sy <= 8; sy++) {
          map[sy][sx] = TILES.SHOP_CARPET;
        }
      }
      npcs.push({
        x: 20 * TILE_SIZE + 20,
        y: 6 * TILE_SIZE + 20,
        name: 'Merchant Barnaby',
        type: 'merchant',
        portrait: '🧙‍♂️',
        dialog: "Welcome, brave traveler! I stock vital elixirs, satchels, and heart blessings in exchange for shiny Glimmer gold."
      });

      // Elder NPC
      npcs.push({
        x: 10 * TILE_SIZE + 20,
        y: 6 * TILE_SIZE + 20,
        name: 'Elder Orin',
        type: 'npc',
        portrait: '👴',
        dialog: "The Void Sovereign stirs atop the Sunken Spire. Seek the Sunforged Blade in the Wilds, master the Mystic Bow in the Crypts, and claim 5 Heart Containers to prevail!"
      });

      // Training Dummy
      enemies.push(new Enemy(14 * TILE_SIZE, 12 * TILE_SIZE, 'dummy'));

      // Bushes & Grass pots
      for (let i = 0; i < 22; i++) {
        const rx = 3 + Math.floor(Math.random() * (W - 6));
        const ry = 3 + Math.floor(Math.random() * (H - 6));
        if (map[ry][rx] === TILES.GRASS) {
          props.push({ type: 'bush', x: rx * TILE_SIZE + 20, y: ry * TILE_SIZE + 20, hp: 1 });
        }
      }

      // Exit to Overworld (East Gate)
      map[10][W - 1] = TILES.DOOR;
      map[11][W - 1] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: (W - 1) * TILE_SIZE,
        y: 10.5 * TILE_SIZE,
        targetZone: ZONES.OVERWORLD,
        targetX: 2.5 * TILE_SIZE,
        targetY: 10.5 * TILE_SIZE
      });

    } else if (cfg.id === ZONES.OVERWORLD) {
      // Overworld: Gates, Slimes, Goblins, Chests, Cracked Wall for Sunforged Blade (Weapon Upgrade 1)
      map[10][0] = TILES.DOOR;
      map[11][0] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: 0,
        y: 10.5 * TILE_SIZE,
        targetZone: ZONES.VILLAGE,
        targetX: (ZONE_CONFIGS[0].width - 2.5) * TILE_SIZE,
        targetY: 10.5 * TILE_SIZE
      });

      // Exit to Crypts (East)
      map[14][W - 1] = TILES.DOOR;
      map[15][W - 1] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: (W - 1) * TILE_SIZE,
        y: 14.5 * TILE_SIZE,
        targetZone: ZONES.CRYPT,
        targetX: 2.5 * TILE_SIZE,
        targetY: 12.5 * TILE_SIZE
      });

      // Shrine
      map[5][18] = TILES.SHRINE;
      interactables.push({
        type: 'shrine',
        x: 18 * TILE_SIZE + 20,
        y: 5 * TILE_SIZE + 20,
        name: 'Wilds Outpost Shrine'
      });

      // Internal ruins walls & Cracked wall secret room
      for (let y = 16; y <= 22; y++) {
        map[y][10] = TILES.WALL;
        map[y][18] = TILES.WALL;
      }
      for (let x = 10; x <= 18; x++) {
        map[22][x] = TILES.WALL;
      }
      map[16][14] = TILES.CRACKED_WALL; // Needs Bomb or heavy hit

      // Weapon Upgrade 1 in secret chamber
      interactables.push({
        type: 'pedestal_weapon1',
        x: 14 * TILE_SIZE + 20,
        y: 19 * TILE_SIZE + 20,
        name: 'Sunforged Broadsword'
      });

      // Chest with Heart Piece / Accessory
      interactables.push({
        type: 'chest',
        x: 28 * TILE_SIZE + 20,
        y: 6 * TILE_SIZE + 20,
        item: 'swift_boots',
        opened: false
      });

      // Enemies
      for (let i = 0; i < 7; i++) {
        enemies.push(new Enemy((6 + Math.random() * 22) * TILE_SIZE, (4 + Math.random() * 16) * TILE_SIZE, 'slime'));
      }
      for (let i = 0; i < 5; i++) {
        enemies.push(new Enemy((12 + Math.random() * 18) * TILE_SIZE, (6 + Math.random() * 14) * TILE_SIZE, 'goblin'));
      }
      for (let i = 0; i < 20; i++) {
        props.push({ type: 'bush', x: (4 + Math.random() * (W - 8)) * TILE_SIZE, y: (3 + Math.random() * (H - 6)) * TILE_SIZE, hp: 1 });
      }

    } else if (cfg.id === ZONES.CRYPT) {
      // Sunken Crypts (Mid-Game Dungeon)
      // Entrance from Overworld (West)
      map[12][0] = TILES.DOOR;
      map[13][0] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: 0,
        y: 12.5 * TILE_SIZE,
        targetZone: ZONES.OVERWORLD,
        targetX: (ZONE_CONFIGS[1].width - 2.5) * TILE_SIZE,
        targetY: 14.5 * TILE_SIZE
      });

      // Exit to Spire (North gate locked by Crypt Boss)
      map[0][16] = TILES.DOOR;
      map[0][17] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: 16.5 * TILE_SIZE,
        y: 0,
        targetZone: ZONES.SPIRE,
        targetX: 16.5 * TILE_SIZE,
        targetY: (ZONE_CONFIGS[3].height - 2.5) * TILE_SIZE,
        requiresCryptBoss: true
      });

      // Shrine
      map[18][6] = TILES.SHRINE;
      interactables.push({
        type: 'shrine',
        x: 6 * TILE_SIZE + 20,
        y: 18 * TILE_SIZE + 20,
        name: 'Crypt Sanctum Shrine'
      });

      // Crypt Boss: Crypt Warden (Unlocks Mystic Bow upon defeat!)
      enemies.push(new BossCryptWarden(24 * TILE_SIZE, 12 * TILE_SIZE));

      // Skeletons & Bat minions
      for (let i = 0; i < 6; i++) {
        enemies.push(new Enemy((10 + Math.random() * 12) * TILE_SIZE, (6 + Math.random() * 12) * TILE_SIZE, 'skeleton'));
      }
      for (let i = 0; i < 5; i++) {
        enemies.push(new Enemy((16 + Math.random() * 10) * TILE_SIZE, (4 + Math.random() * 14) * TILE_SIZE, 'bat'));
      }

      // Chest with Amulet of Thorns
      interactables.push({
        type: 'chest',
        x: 8 * TILE_SIZE + 20,
        y: 6 * TILE_SIZE + 20,
        item: 'amulet_thorns',
        opened: false
      });

    } else if (cfg.id === ZONES.SPIRE) {
      // Storm Peaks & Spire Ascent
      // Entrance from Crypts (South)
      map[H - 1][16] = TILES.DOOR;
      map[H - 1][17] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: 16.5 * TILE_SIZE,
        y: (H - 1) * TILE_SIZE,
        targetZone: ZONES.CRYPT,
        targetX: 16.5 * TILE_SIZE,
        targetY: 2.5 * TILE_SIZE
      });

      // Exit to Final Sanctum (North)
      map[0][16] = TILES.DOOR;
      map[0][17] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: 16.5 * TILE_SIZE,
        y: 0,
        targetZone: ZONES.SANCTUM,
        targetX: 14.5 * TILE_SIZE,
        targetY: (ZONE_CONFIGS[4].height - 2.5) * TILE_SIZE
      });

      // Shrine
      map[20][8] = TILES.SHRINE;
      interactables.push({
        type: 'shrine',
        x: 8 * TILE_SIZE + 20,
        y: 20 * TILE_SIZE + 20,
        name: 'Storm Peak Summit Shrine'
      });

      // Stormcaller Greatblade (Weapon Upgrade 2) Pedestal guarded by Elite Knights
      interactables.push({
        type: 'pedestal_weapon2',
        x: 28 * TILE_SIZE + 20,
        y: 12 * TILE_SIZE + 20,
        name: 'Stormcaller Greatblade'
      });

      // Switch Puzzle for Mystic Bow (Shoot switch to open gate / bridge)
      interactables.push({
        type: 'ranged_switch',
        x: 22 * TILE_SIZE + 20,
        y: 6 * TILE_SIZE + 20,
        activated: false
      });

      // Chest with Bloodstone Talisman or Heart Vessel
      interactables.push({
        type: 'chest',
        x: 8 * TILE_SIZE + 20,
        y: 8 * TILE_SIZE + 20,
        item: 'berserk_talisman',
        opened: false
      });

      for (let i = 0; i < 5; i++) {
        enemies.push(new Enemy((12 + Math.random() * 18) * TILE_SIZE, (8 + Math.random() * 14) * TILE_SIZE, 'knight'));
      }
      for (let i = 0; i < 5; i++) {
        enemies.push(new Enemy((10 + Math.random() * 20) * TILE_SIZE, (6 + Math.random() * 16) * TILE_SIZE, 'bat'));
      }

    } else if (cfg.id === ZONES.SANCTUM) {
      // Sanctum of the Void Sovereign (Final Boss)
      map[H - 1][14] = TILES.DOOR;
      map[H - 1][15] = TILES.DOOR;
      interactables.push({
        type: 'gate',
        x: 14.5 * TILE_SIZE,
        y: (H - 1) * TILE_SIZE,
        targetZone: ZONES.SPIRE,
        targetX: 16.5 * TILE_SIZE,
        targetY: 2.5 * TILE_SIZE
      });

      // Save Shrine right before the Sovereign
      map[18][6] = TILES.SHRINE;
      interactables.push({
        type: 'shrine',
        x: 6 * TILE_SIZE + 20,
        y: 18 * TILE_SIZE + 20,
        name: 'Sanctum Threshold Shrine'
      });

      // Final Boss: Void Sovereign
      enemies.push(new BossVoidSovereign(15 * TILE_SIZE, 8 * TILE_SIZE));
    }

    return {
      map,
      props,
      enemies,
      npcs,
      interactables,
      width: W,
      height: H,
      name: cfg.name
    };
  }

  getCurrentZone() {
    return this.zonesData[this.currentZoneId];
  }

  isSolid(x, y) {
    const tx = Math.floor(x / TILE_SIZE);
    const ty = Math.floor(y / TILE_SIZE);
    const zone = this.getCurrentZone();
    if (tx < 0 || tx >= zone.width || ty < 0 || ty >= zone.height) return true;
    const tile = zone.map[ty][tx];
    return tile === TILES.WALL || tile === TILES.CRACKED_WALL || tile === TILES.WATER;
  }
}

// --- PROJECTILE CLASS ---
class Projectile {
  constructor(x, y, vx, vy, options = {}) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.damage = options.damage || 1;
    this.range = options.range || 400;
    this.distTravelled = 0;
    this.color = options.color || '#64ffda';
    this.isPlayer = options.isPlayer !== undefined ? options.isPlayer : true;
    this.radius = options.radius || 4;
    this.dead = false;
  }

  update(dt, world, enemies, player) {
    const moveStep = Math.hypot(this.vx, this.vy) * dt;
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.distTravelled += moveStep;

    if (this.distTravelled >= this.range) {
      this.dead = true;
      return;
    }

    // Check wall collision
    if (world.isSolid(this.x, this.y)) {
      const tx = Math.floor(this.x / TILE_SIZE);
      const ty = Math.floor(this.y / TILE_SIZE);
      const zone = world.getCurrentZone();
      if (zone.map[ty] && zone.map[ty][tx] === TILES.CRACKED_WALL) {
        zone.map[ty][tx] = TILES.STONE_FLOOR;
        sounds.playExplosion();
        game.particles.emit(this.x, this.y, 20, { color: '#8d6e63', size: 4 });
        game.notify('Secret passage blasted open!');
      }
      this.dead = true;
      game.particles.emit(this.x, this.y, 4, { color: this.color });
      return;
    }

    // Hit enemies
    if (this.isPlayer) {
      // Check ranged switches
      const zone = world.getCurrentZone();
      zone.interactables.forEach(item => {
        if (item.type === 'ranged_switch' && !item.activated && dist(this.x, this.y, item.x, item.y) < 24) {
          item.activated = true;
          this.dead = true;
          sounds.playPowerup();
          game.notify('Ranged Sigil Activated! Ancient barrier lowered.');
          game.particles.emit(item.x, item.y, 25, { color: '#64ffda', size: 4 });
        }
      });

      enemies.forEach(e => {
        if (!e.dead && dist(this.x, this.y, e.x, e.y) < e.radius + this.radius) {
          e.takeDamage(this.damage, this.vx, this.vy);
          this.dead = true;
          game.particles.emit(this.x, this.y, 6, { color: this.color });
        }
      });
    } else {
      // Enemy projectile hit player
      if (dist(this.x, this.y, player.x, player.y) < player.radius + this.radius) {
        player.takeDamage(this.damage, this.x, this.y);
        this.dead = true;
        game.particles.emit(this.x, this.y, 8, { color: '#ff5252' });
      }
    }
  }

  render(ctx, camera) {
    const sx = this.x - camera.x;
    const sy = this.y - camera.y;
    ctx.save();
    ctx.fillStyle = this.color;
    ctx.shadowColor = this.color;
    ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(sx, sy, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

// --- BOMB EXPLOSIVE CLASS ---
class Bomb {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.timer = 1.4;
    this.dead = false;
    this.radius = 12;
    this.blastRadius = 90;
  }

  update(dt, world, enemies, player) {
    this.timer -= dt;
    if (this.timer <= 0) {
      this.explode(world, enemies, player);
      this.dead = true;
    }
  }

  explode(world, enemies, player) {
    sounds.playExplosion();
    game.particles.emit(this.x, this.y, 40, { color: '#ff7043', size: 6, speed: 5 });
    game.particles.emit(this.x, this.y, 25, { color: '#ffca28', size: 4, speed: 4 });
    game.particles.emit(this.x, this.y, 10, { shape: 'ring', color: '#ffab00' });

    // Break cracked walls within blast
    const zone = world.getCurrentZone();
    const cx = Math.floor(this.x / TILE_SIZE);
    const cy = Math.floor(this.y / TILE_SIZE);
    for (let dy = -2; dy <= 2; dy++) {
      for (let dx = -2; dx <= 2; dx++) {
        const tx = cx + dx;
        const ty = cy + dy;
        if (zone.map[ty] && zone.map[ty][tx] === TILES.CRACKED_WALL) {
          zone.map[ty][tx] = TILES.STONE_FLOOR;
          game.notify('Wall blown to rubble!');
        }
      }
    }

    // Damage enemies in blast
    enemies.forEach(e => {
      if (!e.dead && dist(this.x, this.y, e.x, e.y) < this.blastRadius) {
        const d = Math.max(1, dist(this.x, this.y, e.x, e.y));
        const pushX = (e.x - this.x) / d * 400;
        const pushY = (e.y - this.y) / d * 400;
        e.takeDamage(4.0, pushX, pushY);
      }
    });

    // Damage props (bushes)
    zone.props.forEach(p => {
      if (dist(this.x, this.y, p.x, p.y) < this.blastRadius) {
        p.hp = 0;
      }
    });

    // Hurt player if standing too close
    if (dist(this.x, this.y, player.x, player.y) < this.blastRadius * 0.75) {
      player.takeDamage(1, this.x, this.y);
    }
  }

  render(ctx, camera) {
    const sx = this.x - camera.x;
    const sy = this.y - camera.y;
    const pulse = Math.sin(Date.now() * 0.02) * 2;
    ctx.save();
    ctx.fillStyle = '#212121';
    ctx.beginPath();
    ctx.arc(sx, sy, this.radius + pulse, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#d32f2f';
    ctx.lineWidth = 2;
    ctx.stroke();

    // Fuse spark
    ctx.fillStyle = '#ffeb3b';
    ctx.beginPath();
    ctx.arc(sx + 6, sy - 8, 3, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

// --- PICKUP ENTITY (GOLD, HEARTS, BOMBS) ---
class Pickup {
  constructor(x, y, type = 'gold', amount = 1) {
    this.x = x;
    this.y = y;
    this.type = type; // 'gold', 'heart', 'bomb'
    this.amount = amount;
    this.dead = false;
    this.bob = Math.random() * Math.PI * 2;
  }

  update(dt, player) {
    this.bob += dt * 4;
    const d = dist(this.x, this.y, player.x, player.y);
    const magnetDist = player.getMagnetRadius();

    if (d < magnetDist) {
      // Pull towards player
      const speed = 260 * dt;
      this.x += ((player.x - this.x) / d) * speed;
      this.y += ((player.y - this.y) / d) * speed;
    }

    if (d < player.radius + 12) {
      this.collect(player);
      this.dead = true;
    }
  }

  collect(player) {
    if (this.type === 'gold') {
      const mult = player.hasEquipped('gold_ring') ? 2 : 1;
      const total = this.amount * mult;
      player.gold += total;
      sounds.playCoin();
      game.particles.emit(this.x, this.y, 8, { color: '#ffd54f', size: 3 });
    } else if (this.type === 'heart') {
      player.hp = Math.min(player.maxHp, player.hp + 2);
      sounds.playPowerup();
      game.particles.emit(this.x, this.y, 10, { color: '#ff5252', size: 3 });
    } else if (this.type === 'bomb') {
      player.bombs += 1;
      sounds.playPowerup();
      game.particles.emit(this.x, this.y, 6, { color: '#ff9800', size: 3 });
    }
  }

  render(ctx, camera) {
    const sx = this.x - camera.x;
    const sy = this.y - camera.y + Math.sin(this.bob) * 3;
    ctx.save();
    if (this.type === 'gold') {
      ctx.fillStyle = '#ffd700';
      ctx.strokeStyle = '#b8860b';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(sx, sy, 6, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
    } else if (this.type === 'heart') {
      ctx.fillStyle = '#ef5350';
      ctx.beginPath();
      ctx.arc(sx - 3, sy - 2, 4, 0, Math.PI * 2);
      ctx.arc(sx + 3, sy - 2, 4, 0, Math.PI * 2);
      ctx.lineTo(sx, sy + 6);
      ctx.fill();
    } else if (this.type === 'bomb') {
      ctx.fillStyle = '#37474f';
      ctx.beginPath();
      ctx.arc(sx, sy, 5, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }
}

// --- BASE ENEMY CLASS ---
class Enemy {
  constructor(x, y, type = 'slime') {
    this.x = x;
    this.y = y;
    this.type = type;
    this.dead = false;
    this.vx = 0;
    this.vy = 0;
    this.radius = 16;
    this.flashTimer = 0;
    this.attackCooldown = 0;
    this.animTimer = Math.random() * 10;

    // Type setups
    if (type === 'slime') {
      this.maxHp = 2.0;
      this.hp = 2.0;
      this.speed = 55;
      this.damage = 1;
      this.color = '#4caf50';
      this.radius = 14;
    } else if (type === 'goblin') {
      this.maxHp = 4.0;
      this.hp = 4.0;
      this.speed = 85;
      this.damage = 1;
      this.color = '#d84315';
      this.radius = 16;
    } else if (type === 'skeleton') {
      this.maxHp = 5.0;
      this.hp = 5.0;
      this.speed = 70;
      this.damage = 1.5;
      this.color = '#e0e0e0';
      this.radius = 16;
    } else if (type === 'bat') {
      this.maxHp = 2.0;
      this.hp = 2.0;
      this.speed = 110;
      this.damage = 1;
      this.color = '#7e57c2';
      this.radius = 12;
    } else if (type === 'knight') {
      this.maxHp = 9.0;
      this.hp = 9.0;
      this.speed = 60;
      this.damage = 2;
      this.color = '#455a64';
      this.radius = 18;
    } else if (type === 'dummy') {
      this.maxHp = 9999;
      this.hp = 9999;
      this.speed = 0;
      this.damage = 0;
      this.color = '#a1887f';
      this.radius = 18;
    }
  }

  takeDamage(amount, pushX = 0, pushY = 0) {
    if (this.type === 'dummy') {
      sounds.playHit();
      this.flashTimer = 0.15;
      game.particles.emit(this.x, this.y, 8, { color: '#fff', size: 3 });
      return;
    }

    this.hp -= amount;
    this.flashTimer = 0.18;
    sounds.playHit();
    game.particles.emit(this.x, this.y, 10, { color: '#ff1744', size: 3 });

    // Knockback
    const pDist = Math.hypot(pushX, pushY) || 1;
    this.vx += (pushX / pDist) * 160;
    this.vy += (pushY / pDist) * 160;

    if (this.hp <= 0) {
      this.die();
    }
  }

  die() {
    this.dead = true;
    sounds.playExplosion();
    game.particles.emit(this.x, this.y, 25, { color: this.color, size: 5, speed: 4 });
    
    // Drop loot
    const dropChance = Math.random();
    if (dropChance < 0.7) {
      const goldAmt = this.type === 'knight' ? 15 : (this.type === 'goblin' ? 8 : 4);
      game.pickups.push(new Pickup(this.x, this.y, 'gold', goldAmt));
    }
    if (Math.random() < 0.3) {
      game.pickups.push(new Pickup(this.x + 8, this.y, 'heart', 1));
    }
    if (Math.random() < 0.2) {
      game.pickups.push(new Pickup(this.x - 8, this.y, 'bomb', 1));
    }
  }

  update(dt, world, player) {
    if (this.dead) return;
    this.animTimer += dt;
    if (this.flashTimer > 0) this.flashTimer -= dt;
    if (this.attackCooldown > 0) this.attackCooldown -= dt;

    // Apply knockback decay
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.vx *= 0.85;
    this.vy *= 0.85;

    if (this.type === 'dummy') return;

    const d = dist(this.x, this.y, player.x, player.y);
    const aggroRange = this.type === 'bat' ? 260 : 200;

    if (d < aggroRange && d > 10) {
      const nx = (player.x - this.x) / d;
      const ny = (player.y - this.y) / d;
      const nextX = this.x + nx * this.speed * dt;
      const nextY = this.y + ny * this.speed * dt;

      if (!world.isSolid(nextX, this.y)) this.x = nextX;
      if (!world.isSolid(this.x, nextY)) this.y = nextY;

      // Skeleton shoots projectile at range
      if (this.type === 'skeleton' && d > 80 && this.attackCooldown <= 0) {
        this.attackCooldown = 2.2;
        game.projectiles.push(new Projectile(this.x, this.y, nx * 240, ny * 240, {
          isPlayer: false,
          color: '#e0e0e0',
          damage: 1,
          radius: 5
        }));
      }

      // Melee attack player
      if (d < this.radius + player.radius && this.attackCooldown <= 0) {
        this.attackCooldown = 0.9;
        player.takeDamage(this.damage, this.x, this.y);

        // Amulet of thorns retaliation
        if (player.hasEquipped('amulet_thorns')) {
          this.takeDamage(this.damage * 0.5, -nx * 100, -ny * 100);
        }
      }
    }
  }

  render(ctx, camera) {
    if (this.dead) return;
    const sx = this.x - camera.x;
    const sy = this.y - camera.y;

    ctx.save();
    if (this.flashTimer > 0) {
      ctx.fillStyle = '#ffffff';
    } else {
      ctx.fillStyle = this.color;
    }

    if (this.type === 'slime') {
      const squish = Math.sin(this.animTimer * 6) * 3;
      ctx.beginPath();
      ctx.ellipse(sx, sy, this.radius + squish, this.radius - squish, 0, 0, Math.PI * 2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#000';
      ctx.beginPath();
      ctx.arc(sx - 4, sy - 2, 2, 0, Math.PI * 2);
      ctx.arc(sx + 4, sy - 2, 2, 0, Math.PI * 2);
      ctx.fill();
    } else if (this.type === 'goblin') {
      ctx.beginPath();
      ctx.arc(sx, sy, this.radius, 0, Math.PI * 2);
      ctx.fill();
      // Goblin ears
      ctx.fillStyle = '#bf360c';
      ctx.beginPath();
      ctx.moveTo(sx - this.radius, sy);
      ctx.lineTo(sx - this.radius - 8, sy - 6);
      ctx.lineTo(sx - this.radius + 2, sy + 4);
      ctx.moveTo(sx + this.radius, sy);
      ctx.lineTo(sx + this.radius + 8, sy - 6);
      ctx.lineTo(sx + this.radius - 2, sy + 4);
      ctx.fill();
    } else if (this.type === 'skeleton') {
      ctx.beginPath();
      ctx.arc(sx, sy, this.radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#212121';
      ctx.fillRect(sx - 6, sy - 4, 3, 5);
      ctx.fillRect(sx + 3, sy - 4, 3, 5);
    } else if (this.type === 'bat') {
      const wing = Math.sin(this.animTimer * 14) * 8;
      ctx.beginPath();
      ctx.arc(sx, sy, this.radius * 0.7, 0, Math.PI * 2);
      ctx.fill();
      // Wings
      ctx.beginPath();
      ctx.moveTo(sx, sy);
      ctx.lineTo(sx - 14, sy - wing);
      ctx.lineTo(sx - 6, sy + 4);
      ctx.moveTo(sx, sy);
      ctx.lineTo(sx + 14, sy - wing);
      ctx.lineTo(sx + 6, sy + 4);
      ctx.fill();
    } else if (this.type === 'knight') {
      ctx.beginPath();
      ctx.rect(sx - this.radius, sy - this.radius, this.radius * 2, this.radius * 2);
      ctx.fill();
      ctx.strokeStyle = '#ffd54f';
      ctx.lineWidth = 2;
      ctx.stroke();
    } else if (this.type === 'dummy') {
      ctx.beginPath();
      ctx.arc(sx, sy, this.radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#5d4037';
      ctx.fillRect(sx - 3, sy - this.radius, 6, this.radius * 2);
    }

    // Health bar overhead for tough enemies
    if (this.maxHp > 3 && this.type !== 'dummy' && this.hp < this.maxHp) {
      const barW = 28;
      ctx.fillStyle = 'rgba(0,0,0,0.6)';
      ctx.fillRect(sx - barW / 2, sy - this.radius - 10, barW, 4);
      ctx.fillStyle = '#ef5350';
      ctx.fillRect(sx - barW / 2, sy - this.radius - 10, barW * (this.hp / this.maxHp), 4);
    }

    ctx.restore();
  }
}

// --- MID-GAME BOSS: CRYPT WARDEN ---
class BossCryptWarden extends Enemy {
  constructor(x, y) {
    super(x, y, 'boss_crypt');
    this.name = 'Crypt Warden Morvath';
    this.maxHp = 22.0;
    this.hp = 22.0;
    this.radius = 28;
    this.speed = 65;
    this.color = '#7c4dff';
    this.phase = 1;
    this.slamTimer = 3.0;
    this.isBoss = true;
  }

  update(dt, world, player) {
    if (this.dead) return;
    super.update(dt, world, player);

    // Update Boss UI
    game.updateBossBar(this.name, this.hp, this.maxHp);

    this.slamTimer -= dt;
    if (this.slamTimer <= 0) {
      this.slamTimer = 3.2;
      // Ground slam radial shockwave
      sounds.playExplosion();
      game.particles.emit(this.x, this.y, 25, { shape: 'ring', color: '#b388ff' });
      for (let a = 0; a < Math.PI * 2; a += Math.PI / 4) {
        game.projectiles.push(new Projectile(this.x, this.y, Math.cos(a) * 200, Math.sin(a) * 200, {
          isPlayer: false,
          color: '#b388ff',
          damage: 1.5,
          radius: 6
        }));
      }
    }
  }

  die() {
    super.die();
    game.cryptBossDefeated = true;
    game.hideBossBar();
    sounds.playPowerup();
    game.notify('Crypt Warden Defeated! Ranged Mystic Bow Discovered!');

    // UNLOCK RANGED BOW (MID-GAME DISCOVERY)
    game.player.unlockBow();
    game.pickups.push(new Pickup(this.x, this.y, 'heart', 1));
    game.pickups.push(new Pickup(this.x + 16, this.y, 'gold', 50));
  }

  render(ctx, camera) {
    if (this.dead) return;
    const sx = this.x - camera.x;
    const sy = this.y - camera.y;

    ctx.save();
    ctx.fillStyle = this.flashTimer > 0 ? '#fff' : this.color;
    ctx.beginPath();
    ctx.arc(sx, sy, this.radius, 0, Math.PI * 2);
    ctx.fill();

    // Horns / Crown
    ctx.fillStyle = '#ffd700';
    ctx.beginPath();
    ctx.moveTo(sx - 18, sy - 18);
    ctx.lineTo(sx - 24, sy - 36);
    ctx.lineTo(sx - 10, sy - 24);
    ctx.lineTo(sx, sy - 38);
    ctx.lineTo(sx + 10, sy - 24);
    ctx.lineTo(sx + 24, sy - 36);
    ctx.lineTo(sx + 18, sy - 18);
    ctx.fill();

    // Glowing eyes
    ctx.fillStyle = '#ff1744';
    ctx.beginPath();
    ctx.arc(sx - 8, sy - 4, 3, 0, Math.PI * 2);
    ctx.arc(sx + 8, sy - 4, 3, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

// --- FINAL BOSS: VOID SOVEREIGN ---
class BossVoidSovereign extends Enemy {
  constructor(x, y) {
    super(x, y, 'boss_void');
    this.name = 'Void Sovereign Malakor';
    this.maxHp = 45.0;
    this.hp = 45.0;
    this.radius = 34;
    this.speed = 50;
    this.color = '#c2185b';
    this.phase = 1;
    this.patternTimer = 2.0;
    this.isBoss = true;
  }

  update(dt, world, player) {
    if (this.dead) return;
    super.update(dt, world, player);

    game.updateBossBar(this.name, this.hp, this.maxHp);

    if (this.hp < this.maxHp * 0.5) {
      this.phase = 2;
      this.speed = 70;
    }

    this.patternTimer -= dt;
    if (this.patternTimer <= 0) {
      this.patternTimer = this.phase === 1 ? 2.4 : 1.6;

      // Bullet hell spiral attack
      sounds.playBow();
      const count = this.phase === 1 ? 10 : 16;
      for (let i = 0; i < count; i++) {
        const angle = (Math.PI * 2 / count) * i + (Date.now() * 0.002);
        game.projectiles.push(new Projectile(this.x, this.y, Math.cos(angle) * 230, Math.sin(angle) * 230, {
          isPlayer: false,
          color: '#e91e63',
          damage: 1.5,
          radius: 5
        }));
      }
    }
  }

  die() {
    super.die();
    game.hideBossBar();
    game.triggerVictory();
  }

  render(ctx, camera) {
    if (this.dead) return;
    const sx = this.x - camera.x;
    const sy = this.y - camera.y;
    const auraPulse = Math.sin(Date.now() * 0.006) * 6;

    ctx.save();
    // Void aura
    ctx.fillStyle = 'rgba(194, 24, 91, 0.3)';
    ctx.beginPath();
    ctx.arc(sx, sy, this.radius + 12 + auraPulse, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = this.flashTimer > 0 ? '#fff' : this.color;
    ctx.beginPath();
    ctx.arc(sx, sy, this.radius, 0, Math.PI * 2);
    ctx.fill();

    // Void Core
    ctx.fillStyle = '#880e4f';
    ctx.beginPath();
    ctx.arc(sx, sy, 14, 0, Math.PI * 2);
    ctx.fill();

    // Eyes
    ctx.fillStyle = '#ff80ab';
    ctx.beginPath();
    ctx.arc(sx - 10, sy - 6, 4, 0, Math.PI * 2);
    ctx.arc(sx + 10, sy - 6, 4, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();
  }
}

// --- PLAYER CHARACTER CLASS ---
class Player {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = 0;
    this.vy = 0;
    this.radius = 16;
    this.baseSpeed = 160;
    this.facingAngle = 0; // Radians

    // Health System: 3 containers (6 HP) starting, up to 5 containers (10 HP)
    this.heartContainers = 3;
    this.maxHp = this.heartContainers * 2; // Each heart = 2 HP
    this.hp = this.maxHp;

    // Stamina / Dash
    this.stamina = 100;
    this.maxStamina = 100;
    this.isDashing = false;
    this.dashTimer = 0;
    this.dashCooldown = 0;

    // Combat & Weapon Progression
    this.swordTier = 1; // Tier 1: Rusty -> Tier 2: Sunforged -> Tier 3: Stormcaller
    this.hasBow = false; // Halfway mid-game discovery
    this.activeWeapon = 'sword'; // 'sword', 'bow'
    this.attackTimer = 0;
    this.attackDuration = 0.25;

    // Currency & Inventory
    this.gold = 30;
    this.keys = 1;
    this.bombs = 3;
    this.inventory = ['potion_heal', 'potion_heal'];
    this.equippedAccessories = new Set(); // holds item IDs

    // Visual feedback
    this.hurtFlash = 0;
    this.walkAnimTimer = 0;
    this.isMoving = false;
    this.stepAudioTimer = 0;

    // Checkpoint
    this.lastSafeCheckpoint = { zoneId: ZONES.VILLAGE, x: 7 * TILE_SIZE + 20, y: 8 * TILE_SIZE + 20 };
  }

  getSpeed() {
    let s = this.baseSpeed;
    if (this.hasEquipped('swift_boots')) s *= 1.3;
    return s;
  }

  getMagnetRadius() {
    return this.hasEquipped('gold_ring') ? 110 : 45;
  }

  hasEquipped(itemId) {
    return this.equippedAccessories.has(itemId);
  }

  unlockBow() {
    this.hasBow = true;
    document.getElementById('slot-bow').style.display = 'flex';
    document.getElementById('btn-touch-bow').style.display = 'flex';
  }

  upgradeSword(tier) {
    if (tier > this.swordTier) {
      this.swordTier = tier;
      sounds.playPowerup();
      const weaponInfo = WEAPONS.SWORD[tier - 1];
      game.notify(`Acquired ${weaponInfo.name}! (Tier ${tier})`);
      game.updateWeaponIcons();
    }
  }

  takeDamage(amount, fromX = 0, fromY = 0) {
    if (this.hurtFlash > 0 || this.isDashing) return; // Invincible during dash/flash

    let finalDmg = amount;
    if (this.hasEquipped('iron_buckler')) {
      finalDmg = Math.max(0.5, finalDmg - 0.5);
    }

    this.hp = Math.max(0, this.hp - finalDmg);
    this.hurtFlash = 0.5;
    sounds.playHurt();

    // Knockback recoil
    if (fromX || fromY) {
      const d = Math.hypot(this.x - fromX, this.y - fromY) || 1;
      this.vx = ((this.x - fromX) / d) * 220;
      this.vy = ((this.y - fromY) / d) * 220;
    }

    game.particles.emit(this.x, this.y, 14, { color: '#ef5350', size: 3 });

    if (this.hp <= 0) {
      game.triggerGameOver();
    }
  }

  dash(inputX, inputY) {
    if (this.stamina < 30 || this.dashCooldown > 0 || this.isDashing) return;
    this.stamina -= 30;
    this.isDashing = true;
    this.dashTimer = 0.2;
    this.dashCooldown = this.hasEquipped('swift_boots') ? 0.35 : 0.5;

    const angle = (inputX !== 0 || inputY !== 0) ? Math.atan2(inputY, inputX) : this.facingAngle;
    this.vx = Math.cos(angle) * 420;
    this.vy = Math.sin(angle) * 420;

    sounds.playDash();
    game.particles.emit(this.x, this.y, 10, { color: '#80d8ff', shape: 'ring' });
  }

  performAttack(targetX, targetY, world, enemies) {
    if (this.attackTimer > 0) return;

    this.facingAngle = Math.atan2(targetY - this.y, targetX - this.x);

    if (this.activeWeapon === 'sword') {
      const curSword = WEAPONS.SWORD[this.swordTier - 1];
      this.attackTimer = curSword.speed;
      sounds.playAttack(this.swordTier);

      // Sword slash hitbox calculation
      const slashArc = curSword.arc;
      const slashRange = curSword.range;
      let dmg = curSword.damage;

      // Berserk bonus check
      if (this.hasEquipped('berserk_talisman') && this.hp <= this.maxHp * 0.5) {
        dmg += 1.0;
      }

      // Hit enemies within arc
      enemies.forEach(e => {
        if (e.dead) return;
        const d = dist(this.x, this.y, e.x, e.y);
        if (d < slashRange + e.radius) {
          const angleToEnemy = Math.atan2(e.y - this.y, e.x - this.x);
          let angleDiff = Math.abs(angleToEnemy - this.facingAngle);
          if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff;

          if (angleDiff <= slashArc / 2) {
            const pushX = Math.cos(this.facingAngle) * 260;
            const pushY = Math.sin(this.facingAngle) * 260;
            e.takeDamage(dmg, pushX, pushY);
          }
        }
      });

      // Clear grass/bushes
      const zone = world.getCurrentZone();
      zone.props.forEach(p => {
        if (p.hp > 0 && dist(this.x, this.y, p.x, p.y) < slashRange + 10) {
          p.hp = 0;
          sounds.playStep();
          game.particles.emit(p.x, p.y, 8, { color: '#66bb6a', size: 3 });
          if (Math.random() < 0.4) {
            game.pickups.push(new Pickup(p.x, p.y, 'gold', 1));
          }
        }
      });

      // Tier 3 Sword wave projectile (Stormcaller beam)
      if (this.swordTier === 3) {
        game.projectiles.push(new Projectile(
          this.x + Math.cos(this.facingAngle) * 20,
          this.y + Math.sin(this.facingAngle) * 20,
          Math.cos(this.facingAngle) * 380,
          Math.sin(this.facingAngle) * 380,
          { color: '#00e5ff', damage: 1.5, range: 260, radius: 8 }
        ));
      }

    } else if (this.activeWeapon === 'bow' && this.hasBow) {
      this.attackTimer = 0.35;
      sounds.playBow();
      const bow = WEAPONS.BOW;
      game.projectiles.push(new Projectile(
        this.x + Math.cos(this.facingAngle) * 20,
        this.y + Math.sin(this.facingAngle) * 20,
        Math.cos(this.facingAngle) * bow.projectileSpeed,
        Math.sin(this.facingAngle) * bow.projectileSpeed,
        { color: bow.color, damage: bow.damage, range: bow.range, radius: 4 }
      ));
    }
  }

  dropBomb() {
    if (this.bombs <= 0) {
      game.notify('No Fire Urns / Bombs left!');
      return;
    }
    this.bombs--;
    game.bombs.push(new Bomb(this.x, this.y));
    sounds.playStep();
  }

  update(dt, input, world, enemies) {
    if (this.hurtFlash > 0) this.hurtFlash -= dt;
    if (this.attackTimer > 0) this.attackTimer -= dt;
    if (this.dashCooldown > 0) this.dashCooldown -= dt;

    // Dash state logic
    if (this.isDashing) {
      this.dashTimer -= dt;
      game.particles.emit(this.x, this.y, 2, { color: '#b3e5fc', size: 2 });
      if (this.dashTimer <= 0) {
        this.isDashing = false;
      }
    } else {
      // Regenerate Stamina
      this.stamina = Math.min(this.maxStamina, this.stamina + 35 * dt);

      // Normal movement input
      let moveX = 0;
      let moveY = 0;
      if (input.up) moveY -= 1;
      if (input.down) moveY += 1;
      if (input.left) moveX -= 1;
      if (input.right) moveX += 1;

      if (moveX !== 0 && moveY !== 0) {
        moveX *= 0.7071;
        moveY *= 0.7071;
      }

      this.isMoving = moveX !== 0 || moveY !== 0;

      if (this.isMoving) {
        const spd = this.getSpeed();
        this.vx = moveX * spd;
        this.vy = moveY * spd;
        this.walkAnimTimer += dt * 8;
        this.stepAudioTimer += dt;
        if (this.stepAudioTimer > 0.32) {
          sounds.playStep();
          this.stepAudioTimer = 0;
        }
      } else {
        this.vx *= 0.7;
        this.vy *= 0.7;
      }
    }

    // World collision & movement resolution
    const nextX = this.x + this.vx * dt;
    const nextY = this.y + this.vy * dt;

    if (!world.isSolid(nextX, this.y)) {
      this.x = nextX;
    } else {
      this.vx = 0;
    }

    if (!world.isSolid(this.x, nextY)) {
      this.y = nextY;
    } else {
      this.vy = 0;
    }
  }

  render(ctx, camera) {
    const sx = this.x - camera.x;
    const sy = this.y - camera.y;

    ctx.save();

    // Damage flash or transparency
    if (this.hurtFlash > 0) {
      ctx.globalAlpha = Math.sin(Date.now() * 0.05) > 0 ? 0.4 : 1.0;
    }

    // Dash trail effect
    if (this.isDashing) {
      ctx.shadowColor = '#00e5ff';
      ctx.shadowBlur = 12;
    }

    // Shadow
    ctx.fillStyle = 'rgba(0, 0, 0, 0.35)';
    ctx.beginPath();
    ctx.ellipse(sx, sy + 12, 14, 6, 0, 0, Math.PI * 2);
    ctx.fill();

    // Player Body (Tunic / Armor based on gear)
    const bob = this.isMoving ? Math.sin(this.walkAnimTimer) * 2 : 0;
    const curSword = WEAPONS.SWORD[this.swordTier - 1];

    // Armor cloak color changes with upgraded sword / equipment
    let cloakColor = '#2e7d32'; // Forest Green Tier 1
    if (this.swordTier === 2) cloakColor = '#c67100'; // Solar Amber Tier 2
    if (this.swordTier === 3) cloakColor = '#1565c0'; // Storm Azure Tier 3

    ctx.fillStyle = cloakColor;
    ctx.beginPath();
    ctx.arc(sx, sy - 2 + bob, 12, 0, Math.PI * 2);
    ctx.fill();

    // Head / Hair
    ctx.fillStyle = '#ffd54f';
    ctx.beginPath();
    ctx.arc(sx, sy - 8 + bob, 8, 0, Math.PI * 2);
    ctx.fill();

    // Face / Eye direction
    const eyeX = sx + Math.cos(this.facingAngle) * 4;
    const eyeY = sy - 8 + Math.sin(this.facingAngle) * 4 + bob;
    ctx.fillStyle = '#263238';
    ctx.beginPath();
    ctx.arc(eyeX, eyeY, 2, 0, Math.PI * 2);
    ctx.fill();

    // Render Equipped Weapon (Sword / Bow)
    const weaponDist = 18;
    const wx = sx + Math.cos(this.facingAngle) * weaponDist;
    const wy = sy + Math.sin(this.facingAngle) * weaponDist + bob;

    if (this.activeWeapon === 'sword') {
      ctx.save();
      ctx.translate(wx, wy);
      ctx.rotate(this.facingAngle + Math.PI / 4);

      // Sword Blade
      ctx.fillStyle = curSword.color;
      ctx.strokeStyle = '#37474f';
      ctx.lineWidth = 1.5;
      ctx.fillRect(-2, -14, 4, 16);
      ctx.strokeRect(-2, -14, 4, 16);

      // Crossguard
      ctx.fillStyle = '#ffd700';
      ctx.fillRect(-5, 0, 10, 3);
      ctx.restore();

      // Active Slash Arc Rendering
      if (this.attackTimer > 0) {
        ctx.save();
        ctx.strokeStyle = curSword.trailColor;
        ctx.lineWidth = 8;
        ctx.lineCap = 'round';
        ctx.beginPath();
        const startA = this.facingAngle - curSword.arc / 2;
        const endA = this.facingAngle + curSword.arc / 2;
        ctx.arc(sx, sy, curSword.range, startA, endA);
        ctx.stroke();
        ctx.restore();
      }
    } else if (this.activeWeapon === 'bow') {
      ctx.save();
      ctx.translate(wx, wy);
      ctx.rotate(this.facingAngle);
      ctx.strokeStyle = '#8d6e63';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.arc(0, 0, 10, -Math.PI / 2, Math.PI / 2);
      ctx.stroke();
      ctx.restore();
    }

    ctx.restore();
  }
}

// --- MAIN GAME MANAGER ---
class GameManager {
  constructor() {
    this.canvas = document.getElementById('gameCanvas');
    this.ctx = this.canvas.getContext('2d');
    this.minimapCanvas = document.getElementById('minimapCanvas');
    this.mmCtx = this.minimapCanvas.getContext('2d');

    this.particles = new ParticleSystem();
    this.projectiles = [];
    this.bombs = [];
    this.pickups = [];

    this.world = new World(this);
    this.player = new Player(7 * TILE_SIZE + 20, 8 * TILE_SIZE + 20);

    this.camera = { x: 0, y: 0, targetX: 0, targetY: 0 };
    this.input = { up: false, down: false, left: false, right: false, mouseX: 0, mouseY: 0 };

    this.gameState = 'TITLE'; // 'TITLE', 'PLAYING', 'DIALOG', 'INVENTORY', 'SHOP', 'GAMEOVER', 'VICTORY'
    this.cryptBossDefeated = false;
    this.lastTime = performance.now();

    this.initEventListeners();
    this.renderHUD();
  }

  initEventListeners() {
    // Keyboard
    window.addEventListener('keydown', e => {
      if (this.gameState === 'TITLE' || this.gameState === 'GAMEOVER') return;
      sounds.ensureState();

      if (e.key === 'w' || e.key === 'ArrowUp') this.input.up = true;
      if (e.key === 's' || e.key === 'ArrowDown') this.input.down = true;
      if (e.key === 'a' || e.key === 'ArrowLeft') this.input.left = true;
      if (e.key === 'd' || e.key === 'ArrowRight') this.input.right = true;

      if (e.code === 'Space') {
        if (this.dialogueActive) {
          this.advanceDialogue();
        } else {
          this.player.performAttack(this.camera.x + this.input.mouseX, this.camera.y + this.input.mouseY, this.world, this.world.getCurrentZone().enemies);
        }
      }

      if (e.key === 'Shift') {
        let ix = 0, iy = 0;
        if (this.input.up) iy -= 1;
        if (this.input.down) iy += 1;
        if (this.input.left) ix -= 1;
        if (this.input.right) ix += 1;
        this.player.dash(ix, iy);
      }

      if (e.key === 'e' || e.key === 'E') {
        if (this.dialogueActive) {
          this.advanceDialogue();
        } else {
          this.interact();
        }
      }

      if (e.key === 'q' || e.key === 'Q') {
        this.player.dropBomb();
      }

      if (e.key === 'f' || e.key === 'F') {
        if (this.player.hasBow) {
          this.player.activeWeapon = this.player.activeWeapon === 'bow' ? 'sword' : 'bow';
          this.updateWeaponIcons();
        }
      }

      if (e.key === 'i' || e.key === 'I' || e.key === 'Tab') {
        e.preventDefault();
        this.toggleInventory();
      }

      // Quick-potions
      if (e.key === '1') this.useQuickPotion();
    });

    window.addEventListener('keyup', e => {
      if (e.key === 'w' || e.key === 'ArrowUp') this.input.up = false;
      if (e.key === 's' || e.key === 'ArrowDown') this.input.down = false;
      if (e.key === 'a' || e.key === 'ArrowLeft') this.input.left = false;
      if (e.key === 'd' || e.key === 'ArrowRight') this.input.right = false;
    });

    // Mouse & Aim
    this.canvas.addEventListener('mousemove', e => {
      const rect = this.canvas.getBoundingClientRect();
      const scaleX = this.canvas.width / rect.width;
      const scaleY = this.canvas.height / rect.height;
      this.input.mouseX = (e.clientX - rect.left) * scaleX;
      this.input.mouseY = (e.clientY - rect.top) * scaleY;

      // Update player facing angle
      const worldMouseX = this.camera.x + this.input.mouseX;
      const worldMouseY = this.camera.y + this.input.mouseY;
      this.player.facingAngle = Math.atan2(worldMouseY - this.player.y, worldMouseX - this.player.x);
    });

    this.canvas.addEventListener('mousedown', e => {
      sounds.ensureState();
      if (this.gameState !== 'PLAYING') return;

      const worldMouseX = this.camera.x + this.input.mouseX;
      const worldMouseY = this.camera.y + this.input.mouseY;

      if (e.button === 0) {
        // LMB: Sword or current weapon
        this.player.performAttack(worldMouseX, worldMouseY, this.world, this.world.getCurrentZone().enemies);
      } else if (e.button === 2) {
        // RMB: Quick bow shot if available
        if (this.player.hasBow) {
          const oldW = this.player.activeWeapon;
          this.player.activeWeapon = 'bow';
          this.player.performAttack(worldMouseX, worldMouseY, this.world, this.world.getCurrentZone().enemies);
          this.player.activeWeapon = oldW;
        }
      }
    });

    this.canvas.addEventListener('contextmenu', e => e.preventDefault());

    // Title screen buttons
    document.getElementById('btn-start-game').addEventListener('click', () => {
      sounds.init();
      sounds.playPowerup();
      document.getElementById('title-screen').style.display = 'none';
      this.gameState = 'PLAYING';
      this.notify('Entered the Whispering Grove. Speak to Elder Orin.');
    });

    const savedState = localStorage.getItem('sunken_spire_save');
    if (savedState) {
      const btnCont = document.getElementById('btn-continue-game');
      btnCont.style.display = 'inline-block';
      btnCont.addEventListener('click', () => {
        sounds.init();
        this.loadSavedGame();
        document.getElementById('title-screen').style.display = 'none';
        this.gameState = 'PLAYING';
      });
    }

    document.getElementById('btn-respawn').addEventListener('click', () => {
      document.getElementById('gameover-screen').style.display = 'none';
      this.respawnAtSave();
    });

    // Touch controls setup
    this.setupTouchControls();
  }

  setupTouchControls() {
    const joystick = document.getElementById('touch-joystick');
    const knob = document.getElementById('touch-knob');
    let touchId = null;

    joystick.addEventListener('touchstart', e => {
      const touch = e.changedTouches[0];
      touchId = touch.identifier;
      handleJoystick(touch);
    });

    joystick.addEventListener('touchmove', e => {
      for (let i = 0; i < e.changedTouches.length; i++) {
        if (e.changedTouches[i].identifier === touchId) {
          handleJoystick(e.changedTouches[i]);
          break;
        }
      }
    });

    const resetJoystick = () => {
      touchId = null;
      knob.style.transform = 'translate(0px, 0px)';
      this.input.up = false;
      this.input.down = false;
      this.input.left = false;
      this.input.right = false;
    };

    joystick.addEventListener('touchend', resetJoystick);
    joystick.addEventListener('touchcancel', resetJoystick);

    const handleJoystick = (touch) => {
      const rect = joystick.getBoundingClientRect();
      const centerX = rect.left + rect.width / 2;
      const centerY = rect.top + rect.height / 2;
      const dx = touch.clientX - centerX;
      const dy = touch.clientY - centerY;
      const d = Math.hypot(dx, dy);
      const maxR = 40;
      const clampedX = (dx / (d || 1)) * Math.min(d, maxR);
      const clampedY = (dy / (d || 1)) * Math.min(d, maxR);
      knob.style.transform = `translate(${clampedX}px, ${clampedY}px)`;

      this.input.left = dx < -12;
      this.input.right = dx > 12;
      this.input.up = dy < -12;
      this.input.down = dy > 12;
    };

    document.getElementById('btn-touch-attack').addEventListener('touchstart', (e) => {
      e.preventDefault();
      sounds.ensureState();
      const targetX = this.player.x + Math.cos(this.player.facingAngle) * 50;
      const targetY = this.player.y + Math.sin(this.player.facingAngle) * 50;
      this.player.performAttack(targetX, targetY, this.world, this.world.getCurrentZone().enemies);
    });

    document.getElementById('btn-touch-dash').addEventListener('touchstart', (e) => {
      e.preventDefault();
      let ix = 0, iy = 0;
      if (this.input.up) iy -= 1;
      if (this.input.down) iy += 1;
      if (this.input.left) ix -= 1;
      if (this.input.right) ix += 1;
      this.player.dash(ix, iy);
    });

    document.getElementById('btn-touch-bow').addEventListener('touchstart', (e) => {
      e.preventDefault();
      if (this.player.hasBow) {
        this.player.activeWeapon = this.player.activeWeapon === 'bow' ? 'sword' : 'bow';
        this.updateWeaponIcons();
      }
    });

    document.getElementById('btn-touch-bomb').addEventListener('touchstart', (e) => {
      e.preventDefault();
      this.player.dropBomb();
    });

    document.getElementById('btn-touch-interact').addEventListener('touchstart', (e) => {
      e.preventDefault();
      if (this.dialogueActive) {
        this.advanceDialogue();
      } else {
        this.interact();
      }
    });
  }

  useQuickPotion() {
    const idx = this.player.inventory.findIndex(id => id === 'potion_heal');
    if (idx !== -1) {
      const item = ITEM_DB.potion_heal;
      if (item.use(this.player)) {
        this.player.inventory.splice(idx, 1);
        this.renderHUD();
      }
    } else {
      this.notify('No Elixirs of Vitality in satchel!');
    }
  }

  interact() {
    const zone = this.world.getCurrentZone();
    const px = this.player.x;
    const py = this.player.y;

    // Check NPCs
    for (const npc of zone.npcs) {
      if (dist(px, py, npc.x, npc.y) < 50) {
        if (npc.type === 'merchant') {
          this.openShop();
        } else {
          this.showDialogue(npc.name, npc.dialog, npc.portrait);
        }
        return;
      }
    }

    // Check Interactables (Shrines, Pedestals, Gates, Chests)
    for (const item of zone.interactables) {
      const d = dist(px, py, item.x, item.y);
      if (d < 50) {
        if (item.type === 'shrine') {
          this.saveAtShrine(item);
          return;
        } else if (item.type === 'pedestal_weapon1') {
          if (this.player.swordTier < 2) {
            this.player.upgradeSword(2);
            zone.interactables = zone.interactables.filter(i => i !== item);
          }
          return;
        } else if (item.type === 'pedestal_weapon2') {
          if (this.player.swordTier < 3) {
            this.player.upgradeSword(3);
            zone.interactables = zone.interactables.filter(i => i !== item);
          }
          return;
        } else if (item.type === 'chest' && !item.opened) {
          item.opened = true;
          sounds.playPowerup();
          this.player.inventory.push(item.item);
          const meta = ITEM_DB[item.item];
          this.notify(`Discovered chest item: ${meta.name}!`);
          this.particles.emit(item.x, item.y, 25, { color: '#ffd54f', size: 4 });
          this.renderHUD();
          return;
        } else if (item.type === 'gate') {
          if (item.requiresCryptBoss && !this.cryptBossDefeated) {
            this.notify('The gate is locked by the Crypt Warden’s dark seal!');
            return;
          }
          this.changeZone(item.targetZone, item.targetX, item.targetY);
          return;
        }
      }
    }
  }

  saveAtShrine(shrine) {
    this.player.lastSafeCheckpoint = {
      zoneId: this.world.currentZoneId,
      x: shrine.x,
      y: shrine.y
    };
    // Restore full HP
    this.player.hp = this.player.maxHp;
    sounds.playSave();
    this.notify(`Restored HP & Progress Saved at ${shrine.name}!`);
    this.particles.emit(shrine.x, shrine.y, 35, { shape: 'ring', color: '#64ffda' });
    this.saveToStorage();
    this.renderHUD();
  }

  saveToStorage() {
    const data = {
      hp: this.player.hp,
      maxHp: this.player.maxHp,
      heartContainers: this.player.heartContainers,
      swordTier: this.player.swordTier,
      hasBow: this.player.hasBow,
      gold: this.player.gold,
      bombs: this.player.bombs,
      inventory: this.player.inventory,
      equipped: Array.from(this.player.equippedAccessories),
      checkpoint: this.player.lastSafeCheckpoint,
      cryptBossDefeated: this.cryptBossDefeated
    };
    localStorage.setItem('sunken_spire_save', JSON.stringify(data));
  }

  loadSavedGame() {
    try {
      const data = JSON.parse(localStorage.getItem('sunken_spire_save'));
      if (!data) return;
      this.player.heartContainers = data.heartContainers || 3;
      this.player.maxHp = this.player.heartContainers * 2;
      this.player.hp = data.hp || this.player.maxHp;
      this.player.swordTier = data.swordTier || 1;
      this.player.hasBow = !!data.hasBow;
      this.player.gold = data.gold || 0;
      this.player.bombs = data.bombs || 0;
      this.player.inventory = data.inventory || [];
      this.player.equippedAccessories = new Set(data.equipped || []);
      this.cryptBossDefeated = !!data.cryptBossDefeated;

      if (this.player.hasBow) this.player.unlockBow();
      this.updateWeaponIcons();

      if (data.checkpoint) {
        this.world.currentZoneId = data.checkpoint.zoneId;
        this.player.x = data.checkpoint.x;
        this.player.y = data.checkpoint.y;
      }
      this.renderHUD();
      this.notify('Resumed from Last Shrine Checkpoint!');
    } catch (e) {
      console.error(e);
    }
  }

  changeZone(targetZoneId, spawnX, spawnY) {
    this.world.currentZoneId = targetZoneId;
    this.player.x = spawnX;
    this.player.y = spawnY;
    this.projectiles = [];
    this.bombs = [];
    this.pickups = [];

    const zone = this.world.getCurrentZone();
    document.getElementById('zone-display-name').textContent = zone.name;
    this.notify(`Entered ${zone.name}`);
    sounds.playStep();
    this.renderHUD();
  }

  showDialogue(speaker, text, portrait = '👤') {
    this.dialogueActive = true;
    this.dialogueQueue = [{ speaker, text, portrait }];
    const overlay = document.getElementById('dialogue-overlay');
    document.getElementById('dialog-speaker').textContent = speaker;
    document.getElementById('dialog-text').textContent = text;
    document.getElementById('dialog-portrait').textContent = portrait;
    overlay.classList.add('active');
  }

  advanceDialogue() {
    this.dialogueActive = false;
    document.getElementById('dialogue-overlay').classList.remove('active');
  }

  notify(msg) {
    const el = document.getElementById('banner-notify');
    el.textContent = msg;
    el.classList.add('show');
    clearTimeout(this.bannerTimer);
    this.bannerTimer = setTimeout(() => {
      el.classList.remove('show');
    }, 2800);
  }

  toggleInventory() {
    const modal = document.getElementById('inventory-modal');
    if (modal.classList.contains('open')) {
      modal.classList.remove('open');
      this.gameState = 'PLAYING';
    } else {
      this.populateInventoryUI();
      modal.classList.add('open');
      this.gameState = 'INVENTORY';
    }
  }

  populateInventoryUI() {
    const grid = document.getElementById('inventory-grid');
    grid.innerHTML = '';
    const descEl = document.getElementById('inv-item-description');

    // Count item occurrences
    const counts = {};
    this.player.inventory.forEach(id => {
      counts[id] = (counts[id] || 0) + 1;
    });

    const uniqueItems = Array.from(new Set(this.player.inventory));

    if (uniqueItems.length === 0) {
      grid.innerHTML = '<div style="grid-column: span 4; color: #8fa1b8; text-align: center; padding: 20px;">Satchel is currently empty. Explore zones or purchase items!</div>';
      descEl.textContent = 'Select an item to view properties.';
      return;
    }

    uniqueItems.forEach(id => {
      const item = ITEM_DB[id];
      if (!item) return;

      const card = document.createElement('div');
      card.className = 'inv-item-card' + (this.player.hasEquipped(id) ? ' equipped' : '');
      card.innerHTML = `
        <div class="item-count">${counts[id] > 1 ? 'x' + counts[id] : ''}</div>
        <div class="item-icon" style="font-size: 24px;">${item.icon}</div>
        <div class="item-name">${item.name}</div>
      `;

      card.addEventListener('click', () => {
        descEl.innerHTML = `
          <strong>${item.name}</strong> [${item.type.toUpperCase()}]<br>
          ${item.desc}<br><br>
          <button class="btn-buy" id="btn-use-item">${item.type === 'accessory' ? (this.player.hasEquipped(id) ? 'Unequip' : 'Equip') : 'Use Item'}</button>
        `;

        document.getElementById('btn-use-item').addEventListener('click', () => {
          if (item.type === 'consumable' || item.type === 'upgrade') {
            if (item.use(this.player)) {
              const idx = this.player.inventory.indexOf(id);
              if (idx !== -1) this.player.inventory.splice(idx, 1);
              this.populateInventoryUI();
              this.renderHUD();
            }
          } else if (item.type === 'accessory') {
            if (this.player.hasEquipped(id)) {
              this.player.equippedAccessories.delete(id);
              this.notify(`Unequipped ${item.name}`);
            } else {
              this.player.equippedAccessories.add(id);
              this.notify(`Equipped ${item.name}!`);
            }
            sounds.playPowerup();
            this.populateInventoryUI();
            this.renderHUD();
          }
        });
      });

      grid.appendChild(card);
    });
  }

  openShop() {
    this.gameState = 'SHOP';
    const modal = document.getElementById('shop-modal');
    modal.classList.add('open');
    this.populateShopUI();
  }

  closeShop() {
    document.getElementById('shop-modal').classList.remove('open');
    this.gameState = 'PLAYING';
  }

  populateShopUI() {
    const list = document.getElementById('shop-items-container');
    list.innerHTML = '';
    document.getElementById('shop-gold-display').textContent = this.player.gold;

    const shopCatalog = ['potion_heal', 'bomb_pack', 'heart_container', 'gold_ring', 'iron_buckler', 'swift_boots'];

    shopCatalog.forEach(id => {
      const item = ITEM_DB[id];
      const row = document.createElement('div');
      row.className = 'shop-item-row';
      const canAfford = this.player.gold >= item.price;
      const isMaxHeart = id === 'heart_container' && this.player.heartContainers >= 5;

      row.innerHTML = `
        <div class="shop-item-info">
          <div style="font-size: 26px;">${item.icon}</div>
          <div>
            <div style="font-weight: bold; color: var(--text-gold); font-size: 14px;">${item.name}</div>
            <div style="font-size: 11px; color: #a0b0c0;">${item.desc}</div>
          </div>
        </div>
        <button class="btn-buy" ${(!canAfford || isMaxHeart) ? 'disabled' : ''}>
          ${isMaxHeart ? 'MAX (5)' : item.price + ' G'}
        </button>
      `;

      row.querySelector('.btn-buy').addEventListener('click', () => {
        if (this.player.gold >= item.price && !isMaxHeart) {
          this.player.gold -= item.price;
          sounds.playCoin();
          if (item.type === 'upgrade') {
            item.use(this.player);
          } else {
            this.player.inventory.push(id);
            this.notify(`Purchased ${item.name}!`);
          }
          this.populateShopUI();
          this.renderHUD();
        }
      });

      list.appendChild(row);
    });
  }

  updateBossBar(name, hp, maxHp) {
    const wrap = document.getElementById('boss-bar-wrap');
    wrap.classList.add('active');
    document.getElementById('boss-name-label').textContent = name;
    const pct = Math.max(0, Math.min(100, (hp / maxHp) * 100));
    document.getElementById('boss-hp-fill').style.width = pct + '%';
  }

  hideBossBar() {
    document.getElementById('boss-bar-wrap').classList.remove('active');
  }

  triggerGameOver() {
    this.gameState = 'GAMEOVER';
    sounds.playHurt();
    document.getElementById('gameover-screen').style.display = 'flex';
  }

  respawnAtSave() {
    const cp = this.player.lastSafeCheckpoint;
    this.world.currentZoneId = cp.zoneId;
    this.player.x = cp.x;
    this.player.y = cp.y;
    this.player.hp = this.player.maxHp;
    this.projectiles = [];
    this.bombs = [];
    this.gameState = 'PLAYING';
    this.notify('Awakened with renewed resolve at safe shrine.');
    this.renderHUD();
  }

  triggerVictory() {
    this.gameState = 'VICTORY';
    sounds.playSave();
    document.getElementById('victory-screen').style.display = 'flex';
    document.getElementById('victory-stats-text').textContent = `
      You achieved all ${this.player.heartContainers} Heart Containers, mastered the ${WEAPONS.SWORD[this.player.swordTier-1].name} and Mystic Bow, collected ${this.player.gold} Glimmer, and vanquished the Void Sovereign!
    `;
  }

  updateWeaponIcons() {
    const curSword = WEAPONS.SWORD[this.player.swordTier - 1];
    const swordSlot = document.getElementById('slot-sword');
    const bowSlot = document.getElementById('slot-bow');

    swordSlot.classList.toggle('active', this.player.activeWeapon === 'sword');
    bowSlot.classList.toggle('active', this.player.activeWeapon === 'bow');

    document.getElementById('sword-icon-container').innerHTML = `
      <svg width="28" height="28" viewBox="0 0 24 24" fill="${curSword.color}">
        <path d="M19.7 4.3c-0.4-0.4-1-0.4-1.4 0L12 10.6l-2.1-2.1c-0.4-0.4-1-0.4-1.4 0s-0.4 1 0 1.4L10.6 12 4.3 18.3c-0.4 0.4-0.4 1 0 1.4 0.2 0.2 0.5 0.3 0.7 0.3s0.5-0.1 0.7-0.3L12 13.4l2.1 2.1c0.2 0.2 0.5 0.3 0.7 0.3s0.5-0.1 0.7-0.3c0.4-0.4 0.4-1 0-1.4L13.4 12l6.3-6.3c0.4-0.4 0.4-1 0-1.4z"/>
      </svg>
    `;

    document.getElementById('bow-icon-container').innerHTML = `
      <svg width="26" height="26" viewBox="0 0 24 24" fill="#64ffda">
        <path d="M18 2h-2c-4.4 0-8 3.6-8 8 0 1.4 0.4 2.8 1 4L2 21l1 1 7-7c1.2 0.6 2.6 1 4 1 4.4 0 8-3.6 8-8V2h-4zm2 8c0 3.3-2.7 6-6 6-1 0-1.9-0.2-2.7-0.7l6-6V4h-5.3c-0.5 0.8-0.7 1.7-0.7 2.7 0 3.3 2.7 6 6 6h2v-2.7z"/>
      </svg>
    `;

    document.getElementById('bomb-icon-container').innerHTML = `
      <svg width="24" height="24" viewBox="0 0 24 24" fill="#ff7043">
        <path d="M12 2c1.1 0 2 0.9 2 2 0 0.4-0.1 0.7-0.3 1h3.3c1.7 0 3 1.3 3 3v10c0 1.7-1.3 3-3 3H7c-1.7 0-3-1.3-3-3V8c0-1.7 1.3-3 3-3h3.3c-0.2-0.3-0.3-0.6-0.3-1 0-1.1 0.9-2 2-2z"/>
      </svg>
    `;
  }

  renderHUD() {
    // Heart containers: Each heart container holds 2 HP.
    const heartBar = document.getElementById('heart-bar');
    heartBar.innerHTML = '';
    const totalContainers = this.player.heartContainers; // 3 to 5
    const currentHp = this.player.hp; // each container is 2 hp

    for (let i = 0; i < totalContainers; i++) {
      const containerHp = clamp(currentHp - (i * 2), 0, 2);
      const heart = document.createElement('div');
      heart.className = 'heart-icon';

      // SVG Heart (Full, Half, or Empty)
      if (containerHp === 2) {
        heart.innerHTML = `<svg viewBox="0 0 24 24" fill="#ef5350" width="28" height="28"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>`;
      } else if (containerHp === 1) {
        heart.innerHTML = `<svg viewBox="0 0 24 24" width="28" height="28"><defs><linearGradient id="halfGrad"><stop offset="50%" stop-color="#ef5350"/><stop offset="50%" stop-color="#424242"/></linearGradient></defs><path fill="url(#halfGrad)" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>`;
      } else {
        heart.innerHTML = `<svg viewBox="0 0 24 24" fill="#37474f" width="28" height="28"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>`;
      }
      heartBar.appendChild(heart);
    }

    // Stamina Bar
    const stamPct = (this.player.stamina / this.player.maxStamina) * 100;
    document.getElementById('stamina-fill').style.width = stamPct + '%';

    // Gold & Bombs & Keys
    document.getElementById('gold-count').textContent = this.player.gold;
    document.getElementById('key-count').textContent = this.player.keys;
    document.getElementById('bomb-count').textContent = this.player.bombs;

    this.updateWeaponIcons();
  }

  renderMinimap() {
    const zone = this.world.getCurrentZone();
    const mmW = this.minimapCanvas.width;
    const mmH = this.minimapCanvas.height;
    this.mmCtx.fillStyle = 'rgba(10, 16, 26, 0.95)';
    this.mmCtx.fillRect(0, 0, mmW, mmH);

    const cellW = mmW / zone.width;
    const cellH = mmH / zone.height;

    // Draw mini tiles
    for (let y = 0; y < zone.height; y++) {
      for (let x = 0; x < zone.width; x++) {
        const t = zone.map[y][x];
        if (t === TILES.WALL || t === TILES.CRACKED_WALL) {
          this.mmCtx.fillStyle = '#37474f';
          this.mmCtx.fillRect(x * cellW, y * cellH, cellW, cellH);
        } else if (t === TILES.SHRINE) {
          this.mmCtx.fillStyle = '#64ffda';
          this.mmCtx.fillRect(x * cellW, y * cellH, cellW, cellH);
        } else if (t === TILES.DOOR) {
          this.mmCtx.fillStyle = '#ffd54f';
          this.mmCtx.fillRect(x * cellW, y * cellH, cellW, cellH);
        }
      }
    }

    // Player blip
    const pmmX = (this.player.x / (zone.width * TILE_SIZE)) * mmW;
    const pmmY = (this.player.y / (zone.height * TILE_SIZE)) * mmH;
    this.mmCtx.fillStyle = '#ff1744';
    this.mmCtx.beginPath();
    this.mmCtx.arc(pmmX, pmmY, 3, 0, Math.PI * 2);
    this.mmCtx.fill();
  }

  update(dt) {
    if (this.gameState !== 'PLAYING') return;

    const zone = this.world.getCurrentZone();

    // Update Player
    this.player.update(dt, this.input, this.world, zone.enemies);

    // Update Camera (Smooth lerp centering on player)
    this.camera.targetX = this.player.x - this.canvas.width / 2;
    this.camera.targetY = this.player.y - this.canvas.height / 2;

    const mapMaxX = zone.width * TILE_SIZE - this.canvas.width;
    const mapMaxY = zone.height * TILE_SIZE - this.canvas.height;
    this.camera.targetX = clamp(this.camera.targetX, 0, Math.max(0, mapMaxX));
    this.camera.targetY = clamp(this.camera.targetY, 0, Math.max(0, mapMaxY));

    this.camera.x += (this.camera.targetX - this.camera.x) * 8 * dt;
    this.camera.y += (this.camera.targetY - this.camera.y) * 8 * dt;

    // Update Enemies
    zone.enemies.forEach(e => e.update(dt, this.world, this.player));

    // Update Projectiles
    for (let i = this.projectiles.length - 1; i >= 0; i--) {
      const p = this.projectiles[i];
      p.update(dt, this.world, zone.enemies, this.player);
      if (p.dead) this.projectiles.splice(i, 1);
    }

    // Update Bombs
    for (let i = this.bombs.length - 1; i >= 0; i--) {
      const b = this.bombs[i];
      b.update(dt, this.world, zone.enemies, this.player);
      if (b.dead) this.bombs.splice(i, 1);
    }

    // Update Pickups
    for (let i = this.pickups.length - 1; i >= 0; i--) {
      const p = this.pickups[i];
      p.update(dt, this.player);
      if (p.dead) this.pickups.splice(i, 1);
    }

    // Update Particles
    this.particles.update(dt);

    this.renderHUD();
  }

  render() {
    const ctx = this.ctx;
    const zone = this.world.getCurrentZone();
    const cfg = ZONE_CONFIGS[this.world.currentZoneId];

    // Clear background with ambient tone
    ctx.fillStyle = cfg.ambientColor;
    ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

    // Draw Map Tiles
    const startCol = Math.max(0, Math.floor(this.camera.x / TILE_SIZE));
    const endCol = Math.min(zone.width - 1, Math.ceil((this.camera.x + this.canvas.width) / TILE_SIZE));
    const startRow = Math.max(0, Math.floor(this.camera.y / TILE_SIZE));
    const endRow = Math.min(zone.height - 1, Math.ceil((this.camera.y + this.canvas.height) / TILE_SIZE));

    for (let y = startRow; y <= endRow; y++) {
      for (let x = startCol; x <= endCol; x++) {
        const tile = zone.map[y][x];
        const sx = x * TILE_SIZE - this.camera.x;
        const sy = y * TILE_SIZE - this.camera.y;

        if (tile === TILES.GRASS) {
          ctx.fillStyle = '#1e3d1a';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#2d5a27';
          ctx.fillRect(sx + 1, sy + 1, TILE_SIZE - 2, TILE_SIZE - 2);
          ctx.fillStyle = '#3e7b36';
          ctx.fillRect(sx + 8, sy + 12, 4, 8);
          ctx.fillRect(sx + 24, sy + 20, 5, 6);
        } else if (tile === TILES.DIRT) {
          ctx.fillStyle = '#4e342e';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#6d4c41';
          ctx.fillRect(sx + 2, sy + 2, TILE_SIZE - 4, TILE_SIZE - 4);
          ctx.fillStyle = '#3e2723';
          ctx.fillRect(sx + 10, sy + 14, 6, 4);
          ctx.fillRect(sx + 22, sy + 8, 4, 4);
        } else if (tile === TILES.STONE_FLOOR) {
          ctx.fillStyle = '#1e282d';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#2c3940';
          ctx.fillRect(sx + 1, sy + 1, TILE_SIZE - 2, TILE_SIZE - 2);
          ctx.strokeStyle = '#182024';
          ctx.lineWidth = 1;
          ctx.strokeRect(sx + 2, sy + 2, TILE_SIZE - 4, TILE_SIZE - 4);
        } else if (tile === TILES.WALL) {
          ctx.fillStyle = '#11171d';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#212d38';
          ctx.fillRect(sx + 2, sy + 2, TILE_SIZE - 4, TILE_SIZE - 12);
          ctx.fillStyle = '#182028';
          ctx.fillRect(sx, sy + TILE_SIZE - 8, TILE_SIZE, 8);
          ctx.strokeStyle = '#2d3d4c';
          ctx.lineWidth = 1;
          ctx.strokeRect(sx + 2, sy + 2, TILE_SIZE - 4, TILE_SIZE - 12);
        } else if (tile === TILES.CRACKED_WALL) {
          ctx.fillStyle = '#261815';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#3e2723';
          ctx.fillRect(sx + 2, sy + 2, TILE_SIZE - 4, TILE_SIZE - 10);
          ctx.strokeStyle = '#ff9800';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(sx + 6, sy + 6);
          ctx.lineTo(sx + 20, sy + 20);
          ctx.lineTo(sx + 34, sy + 10);
          ctx.stroke();
        } else if (tile === TILES.DOOR) {
          ctx.fillStyle = '#1c1708';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#c69b3f';
          ctx.fillRect(sx + 4, sy + 4, TILE_SIZE - 8, TILE_SIZE - 8);
          ctx.strokeStyle = '#ffe082';
          ctx.lineWidth = 2;
          ctx.strokeRect(sx + 4, sy + 4, TILE_SIZE - 8, TILE_SIZE - 8);
        } else if (tile === TILES.SHRINE) {
          ctx.fillStyle = '#0a231f';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#00695c';
          ctx.beginPath();
          ctx.arc(sx + 20, sy + 20, 15, 0, Math.PI * 2);
          ctx.fill();
        } else if (tile === TILES.SHOP_CARPET) {
          ctx.fillStyle = '#4a148c';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.strokeStyle = '#ab47bc';
          ctx.lineWidth = 1.5;
          ctx.strokeRect(sx + 2, sy + 2, TILE_SIZE - 4, TILE_SIZE - 4);
        } else if (tile === TILES.LAVA) {
          ctx.fillStyle = '#bf360c';
          ctx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
          ctx.fillStyle = '#ff6f00';
          ctx.fillRect(sx + 6, sy + 6, TILE_SIZE - 12, TILE_SIZE - 12);
        }
      }
    }

    // Render Interactables (Shrines, Pedestals, Chests, Gates)
    zone.interactables.forEach(item => {
      const sx = item.x - this.camera.x;
      const sy = item.y - this.camera.y;

      if (item.type === 'shrine') {
        const glow = Math.sin(Date.now() * 0.004) * 4;
        ctx.fillStyle = '#26a69a';
        ctx.beginPath();
        ctx.arc(sx, sy, 16 + glow, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#e0f2f1';
        ctx.beginPath();
        ctx.arc(sx, sy, 8, 0, Math.PI * 2);
        ctx.fill();
      } else if (item.type === 'pedestal_weapon1') {
        ctx.fillStyle = '#ffa726';
        ctx.beginPath();
        ctx.arc(sx, sy, 14, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#fff';
        ctx.fillText('🗡️', sx - 8, sy + 6);
      } else if (item.type === 'pedestal_weapon2') {
        ctx.fillStyle = '#00e5ff';
        ctx.beginPath();
        ctx.arc(sx, sy, 16, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#fff';
        ctx.fillText('⚡', sx - 6, sy + 6);
      } else if (item.type === 'chest') {
        ctx.fillStyle = item.opened ? '#5d4037' : '#ffd54f';
        ctx.fillRect(sx - 12, sy - 10, 24, 20);
        ctx.strokeStyle = '#3e2723';
        ctx.strokeRect(sx - 12, sy - 10, 24, 20);
      } else if (item.type === 'ranged_switch') {
        ctx.fillStyle = item.activated ? '#64ffda' : '#ef5350';
        ctx.beginPath();
        ctx.arc(sx, sy, 12, 0, Math.PI * 2);
        ctx.fill();
      }
    });

    // Render Props (Bushes)
    zone.props.forEach(p => {
      if (p.hp > 0) {
        const sx = p.x - this.camera.x;
        const sy = p.y - this.camera.y;
        ctx.fillStyle = '#1b5e20';
        ctx.beginPath();
        ctx.arc(sx, sy, 14, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#2e7d32';
        ctx.beginPath();
        ctx.arc(sx - 3, sy - 3, 9, 0, Math.PI * 2);
        ctx.fill();
      }
    });

    // Render NPCs
    zone.npcs.forEach(npc => {
      const sx = npc.x - this.camera.x;
      const sy = npc.y - this.camera.y;
      ctx.fillStyle = '#3949ab';
      ctx.beginPath();
      ctx.arc(sx, sy, 16, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#ffca28';
      ctx.font = '16px serif';
      ctx.fillText('💬', sx - 8, sy - 18);
    });

    // Render Pickups
    this.pickups.forEach(p => p.render(ctx, this.camera));

    // Render Bombs
    this.bombs.forEach(b => b.render(ctx, this.camera));

    // Render Enemies & Bosses
    zone.enemies.forEach(e => e.render(ctx, this.camera));

    // Render Player
    this.player.render(ctx, this.camera);

    // Render Projectiles
    this.projectiles.forEach(p => p.render(ctx, this.camera));

    // Render Particles
    this.particles.render(ctx, this.camera);

    // Minimap
    this.renderMinimap();
  }

  loop(timestamp) {
    const dt = Math.min(0.05, (timestamp - this.lastTime) / 1000);
    this.lastTime = timestamp;

    this.update(dt);
    this.render();

    requestAnimationFrame(t => this.loop(t));
  }

  start() {
    this.loop(performance.now());
  }
}

// Instantiate and start engine
const game = new GameManager();
game.start();
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.7-flash","provider":"openrouter","persona":"full","sessionId":"b9d0162a-7713-4d66-b326-8e4617aebf0d","tokensIn":584597,"tokensOut":46015,"tokensTotal":630612,"cost":0.24213405000000002,"turns":12,"toolCalls":11,"failedToolCalls":0,"timestamp":"2026-08-15T15:20:25.284Z"} -->