← RPG results

RPG

Gemini 3.6 Flash · full

Initial view of RPG

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 68,727 bytes · SHA-256 541a3ed64b36
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Chronicles of Aethelgard - Action RPG</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    body, html {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background-color: #0d0f12;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      color: #e2e8f0;
    }
    #game-container {
      position: relative;
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      background: #05070a;
    }
    canvas#gameCanvas {
      background-color: #121820;
      box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
      image-rendering: pixelated;
      cursor: crosshair;
    }

    /* UI Overlay */
    #ui-layer {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding: 20px;
      z-index: 10;
    }

    /* HUD Header */
    .hud-top {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      width: 100%;
    }
    .hud-card {
      background: rgba(15, 23, 42, 0.85);
      border: 2px solid rgba(255, 255, 255, 0.15);
      border-radius: 12px;
      padding: 12px 18px;
      backdrop-filter: blur(8px);
      box-shadow: 0 8px 20px rgba(0,0,0,0.4);
      display: flex;
      align-items: center;
      gap: 15px;
    }

    /* Health Display (Hearts) */
    .health-container {
      display: flex;
      align-items: center;
      gap: 6px;
    }
    .heart-icon {
      width: 28px;
      height: 28px;
      display: inline-block;
      position: relative;
      filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
    }

    /* Bars & Stats */
    .stat-bars {
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .bar-bg {
      width: 140px;
      height: 10px;
      background: rgba(255, 255, 255, 0.1);
      border-radius: 5px;
      overflow: hidden;
      border: 1px solid rgba(255,255,255,0.2);
    }
    .bar-fill-stamina {
      height: 100%;
      width: 100%;
      background: linear-gradient(90deg, #38ef7d, #11998e);
      transition: width 0.1s linear;
    }

    /* Economy & Equipment HUD */
    .hud-right {
      display: flex;
      gap: 12px;
    }
    .currency-badge {
      font-size: 18px;
      font-weight: bold;
      color: #ffd700;
      text-shadow: 0 2px 4px rgba(0,0,0,0.8);
      display: flex;
      align-items: center;
      gap: 6px;
    }
    .weapon-badge {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 13px;
      font-weight: 600;
    }
    .weapon-slot {
      width: 42px;
      height: 42px;
      background: rgba(0,0,0,0.5);
      border: 2px solid #3b82f6;
      border-radius: 8px;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 20px;
      position: relative;
    }
    .weapon-slot.ranged {
      border-color: #10b981;
    }
    .hotkey-tag {
      position: absolute;
      bottom: -4px;
      right: -4px;
      background: #1e293b;
      color: #cbd5e1;
      font-size: 10px;
      padding: 1px 4px;
      border-radius: 4px;
      border: 1px solid #475569;
    }

    /* Notification Banner */
    #banner-notify {
      position: absolute;
      top: 80px;
      left: 50%;
      transform: translateX(-50%) translateY(-20px);
      background: rgba(15, 23, 42, 0.95);
      border: 2px solid #f59e0b;
      color: #fbbf24;
      padding: 12px 28px;
      border-radius: 30px;
      font-size: 16px;
      font-weight: bold;
      letter-spacing: 0.5px;
      box-shadow: 0 10px 25px rgba(0,0,0,0.6);
      opacity: 0;
      transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
      pointer-events: none;
      z-index: 30;
      text-align: center;
    }
    #banner-notify.show {
      opacity: 1;
      transform: translateX(-50%) translateY(0);
    }

    /* Interaction & Quest Prompt */
    #interaction-prompt {
      position: absolute;
      bottom: 90px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(0, 0, 0, 0.8);
      border: 1px solid rgba(255,255,255,0.3);
      padding: 8px 18px;
      border-radius: 20px;
      font-size: 14px;
      color: #fff;
      display: none;
      align-items: center;
      gap: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.5);
    }
    .key-cap {
      background: #334155;
      border: 1px solid #94a3b8;
      border-bottom: 3px solid #64748b;
      padding: 2px 7px;
      border-radius: 5px;
      font-size: 12px;
      font-weight: bold;
    }

    /* Boss Bar */
    #boss-bar-container {
      position: absolute;
      bottom: 30px;
      left: 50%;
      transform: translateX(-50%);
      width: 60%;
      max-width: 500px;
      display: none;
      flex-direction: column;
      align-items: center;
      gap: 6px;
      background: rgba(15, 23, 42, 0.85);
      padding: 10px 16px;
      border-radius: 10px;
      border: 1px solid rgba(239, 68, 68, 0.5);
    }
    .boss-name {
      font-size: 14px;
      font-weight: bold;
      color: #ef4444;
      text-transform: uppercase;
      letter-spacing: 1px;
    }
    .boss-bar-bg {
      width: 100%;
      height: 14px;
      background: #1f2937;
      border-radius: 7px;
      overflow: hidden;
      border: 1px solid #374151;
    }
    .boss-bar-fill {
      width: 100%;
      height: 100%;
      background: linear-gradient(90deg, #dc2626, #f87171);
      transition: width 0.2s ease-out;
    }

    /* Controls Overlay Bar */
    .controls-hint {
      position: absolute;
      bottom: 15px;
      left: 20px;
      font-size: 12px;
      color: #94a3b8;
      background: rgba(15, 23, 42, 0.7);
      padding: 6px 12px;
      border-radius: 6px;
      display: flex;
      gap: 12px;
    }

    /* Modal Screens (Shop, Inventory, Pause, Game Over) */
    .modal-overlay {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(5, 7, 10, 0.85);
      backdrop-filter: blur(6px);
      display: none;
      justify-content: center;
      align-items: center;
      z-index: 50;
      pointer-events: auto;
    }
    .modal-card {
      background: #1e293b;
      border: 2px solid #475569;
      border-radius: 16px;
      width: 90%;
      max-width: 650px;
      padding: 24px;
      box-shadow: 0 20px 40px rgba(0,0,0,0.8);
      color: #f1f5f9;
      display: flex;
      flex-direction: column;
      gap: 18px;
      max-height: 85vh;
      overflow-y: auto;
    }
    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      border-bottom: 2px solid #334155;
      padding-bottom: 12px;
    }
    .modal-title {
      font-size: 22px;
      font-weight: bold;
      color: #f59e0b;
    }
    .close-btn {
      background: #334155;
      border: none;
      color: #cbd5e1;
      width: 32px;
      height: 32px;
      border-radius: 50%;
      cursor: pointer;
      font-size: 16px;
      font-weight: bold;
    }
    .close-btn:hover { background: #475569; color: #fff; }

    /* Inventory Items Grid */
    .grid-container {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
      gap: 12px;
    }
    .shop-item-card, .inventory-item-card {
      background: #0f172a;
      border: 1px solid #334155;
      border-radius: 10px;
      padding: 12px;
      display: flex;
      flex-direction: column;
      align-items: center;
      text-align: center;
      gap: 8px;
      transition: all 0.2s ease;
      position: relative;
    }
    .shop-item-card:hover, .inventory-item-card:hover {
      border-color: #3b82f6;
      transform: translateY(-2px);
      background: #1e293b;
    }
    .item-icon {
      font-size: 32px;
    }
    .item-name {
      font-size: 13px;
      font-weight: bold;
      color: #f8fafc;
    }
    .item-desc {
      font-size: 11px;
      color: #94a3b8;
      line-height: 1.3;
    }
    .item-price {
      font-size: 12px;
      font-weight: bold;
      color: #ffd700;
      margin-top: auto;
    }
    .buy-btn, .use-btn {
      margin-top: 6px;
      width: 100%;
      padding: 6px 10px;
      background: #2563eb;
      border: none;
      border-radius: 6px;
      color: #fff;
      font-size: 12px;
      font-weight: bold;
      cursor: pointer;
      transition: background 0.2s;
    }
    .buy-btn:hover, .use-btn:hover { background: #1d4ed8; }
    .buy-btn:disabled { background: #475569; cursor: not-allowed; opacity: 0.6; }

    /* Touch Controls for Mobile/Tablet */
    #mobile-controls {
      display: none;
      position: absolute;
      bottom: 20px;
      left: 0;
      width: 100%;
      padding: 0 20px;
      justify-content: space-between;
      pointer-events: auto;
      z-index: 20;
    }
    .d-pad {
      width: 120px;
      height: 120px;
      position: relative;
    }
    .d-btn {
      position: absolute;
      width: 40px;
      height: 40px;
      background: rgba(255,255,255,0.2);
      border: 1px solid rgba(255,255,255,0.4);
      border-radius: 8px;
      color: #fff;
      font-size: 18px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .d-btn:active { background: rgba(255,255,255,0.5); }
    .d-up { top: 0; left: 40px; }
    .d-down { bottom: 0; left: 40px; }
    .d-left { top: 40px; left: 0; }
    .d-right { top: 40px; right: 0; }
    .action-btns {
      display: flex;
      gap: 12px;
      align-items: flex-end;
    }
    .act-btn {
      width: 54px;
      height: 54px;
      border-radius: 50%;
      background: rgba(37, 99, 235, 0.7);
      border: 2px solid rgba(255,255,255,0.5);
      color: #fff;
      font-weight: bold;
      font-size: 16px;
      display: flex;
      justify-content: center;
      align-items: center;
      box-shadow: 0 4px 10px rgba(0,0,0,0.5);
    }
    .act-btn:active { transform: scale(0.92); }
    .act-btn.ranged-btn { background: rgba(16, 185, 129, 0.7); }
    .act-btn.dash-btn { background: rgba(245, 158, 11, 0.7); }

    /* Title Screen & Game Over */
    .title-card {
      text-align: center;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 20px;
    }
    .title-card h1 {
      font-size: 38px;
      color: #f59e0b;
      letter-spacing: 2px;
      text-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
    }
    .start-btn {
      padding: 14px 36px;
      font-size: 18px;
      font-weight: bold;
      background: linear-gradient(135deg, #2563eb, #1d4ed8);
      color: #fff;
      border: none;
      border-radius: 30px;
      cursor: pointer;
      box-shadow: 0 6px 20px rgba(37, 99, 235, 0.5);
      transition: transform 0.2s, box-shadow 0.2s;
    }
    .start-btn:hover {
      transform: translateY(-3px);
      box-shadow: 0 10px 25px rgba(37, 99, 235, 0.7);
    }
  </style>
</head>
<body>

  <div id="game-container">
    <canvas id="gameCanvas" width="960" height="640"></canvas>

    <!-- UI Overlay -->
    <div id="ui-layer">
      <!-- HUD Top -->
      <div class="hud-top">
        <div class="hud-card">
          <!-- Hearts Container -->
          <div class="health-container" id="hearts-wrapper">
            <!-- Hearts rendered dynamically -->
          </div>
          <div class="stat-bars">
            <div style="font-size:10px; color:#94a3b8; font-weight:bold;">STAMINA</div>
            <div class="bar-bg">
              <div class="bar-fill-stamina" id="stamina-fill"></div>
            </div>
          </div>
        </div>

        <div class="hud-right">
          <div class="hud-card">
            <div class="currency-badge">
              <span>🪙</span>
              <span id="gold-count">0</span>
            </div>
          </div>
          <div class="hud-card weapon-badge">
            <div class="weapon-slot" id="melee-slot" title="Melee Weapon (Left Click / Space)">
              <span id="melee-icon">🗡️</span>
              <span class="hotkey-tag">L-CLK</span>
            </div>
            <div class="weapon-slot ranged" id="ranged-slot" style="opacity:0.4;" title="Ranged Weapon (Right Click / Shift - Unlocked Mid-Game)">
              <span id="ranged-icon">🔒</span>
              <span class="hotkey-tag">R-CLK</span>
            </div>
          </div>
        </div>
      </div>

      <!-- Controls Hint -->
      <div class="controls-hint">
        <span><b>WASD / Arrows</b> Move</span> |
        <span><b>Space / Left Click</b> Attack</span> |
        <span><b>Shift / Right Click</b> Ranged</span> |
        <span><b>C</b> Dash</span> |
        <span><b>E</b> Interact</span> |
        <span><b>I / Tab</b> Inventory</span>
      </div>

      <!-- Interaction Prompt -->
      <div id="interaction-prompt">
        <span class="key-cap">E</span>
        <span id="prompt-text">Interact</span>
      </div>

      <!-- Boss Bar -->
      <div id="boss-bar-container">
        <div class="boss-name" id="boss-name-text">ANCIENT GOLEM GUARDIAN</div>
        <div class="boss-bar-bg">
          <div class="boss-bar-fill" id="boss-bar-fill"></div>
        </div>
      </div>

      <!-- Notification Banner -->
      <div id="banner-notify">Item Acquired!</div>
    </div>

    <!-- Mobile Touch UI -->
    <div id="mobile-controls">
      <div class="d-pad">
        <div class="d-btn d-up" id="btn-up">▲</div>
        <div class="d-btn d-down" id="btn-down">▼</div>
        <div class="d-btn d-left" id="btn-left">◀</div>
        <div class="d-btn d-right" id="btn-right">▶</div>
      </div>
      <div class="action-btns">
        <div class="act-btn dash-btn" id="btn-dash">⚡</div>
        <div class="act-btn ranged-btn" id="btn-ranged">🏹</div>
        <div class="act-btn" id="btn-attack">⚔️</div>
      </div>
    </div>

    <!-- Title / Start Screen Modal -->
    <div class="modal-overlay" id="modal-title" style="display: flex;">
      <div class="modal-card title-card">
        <h1>CHRONICLES OF AETHELGARD</h1>
        <p style="color: #cbd5e1; max-width: 480px; line-height: 1.6; font-size: 14px;">
          Explore the enchanted realm, battle ancient foes, unlock legendary weapon evolutions, and seek the Sacred Bow to restore peace to the fallen kingdom!
        </p>
        <div style="background: rgba(0,0,0,0.3); padding: 12px 20px; border-radius: 10px; font-size: 13px; text-align: left; width: 100%;">
          <p style="color:#f59e0b; font-weight:bold; margin-bottom: 6px;">🎯 Gameplay & Progression Highlights:</p>
          <ul style="margin-left: 20px; color: #94a3b8; line-height: 1.5;">
            <li><b>Health:</b> Tracked by <b>Hearts</b> (Each Heart = 2 HP). Starts at <b>3 HP</b> (1.5 Hearts), upgradeable to <b>5 HP</b>!</li>
            <li><b>Weapon Evolution:</b> Rusty Dagger ➔ Knight's Broadsword ➔ Rune Flameblade.</li>
            <li><b>Ranged Weapon:</b> Defeat the Sunken Temple Guardian to discover the Gale Bow!</li>
            <li><b>In-World Save Shrines:</b> Save progress and fully restore your health anytime.</li>
          </ul>
        </div>
        <button class="start-btn" id="btn-start-game">BEGIN ADVENTURE</button>
      </div>
    </div>

    <!-- Shop Modal -->
    <div class="modal-overlay" id="modal-shop">
      <div class="modal-card">
        <div class="modal-header">
          <div class="modal-title">🏪 Village Shop & Armory</div>
          <button class="close-btn" id="btn-close-shop">✕</button>
        </div>
        <div class="grid-container" id="shop-items-container">
          <!-- Populated by JS -->
        </div>
      </div>
    </div>

    <!-- Inventory Modal -->
    <div class="modal-overlay" id="modal-inventory">
      <div class="modal-card">
        <div class="modal-header">
          <div class="modal-title">🎒 Adventurer's Satchel</div>
          <button class="close-btn" id="btn-close-inventory">✕</button>
        </div>
        <div style="font-size: 13px; color: #94a3b8; margin-bottom: 8px;">Active Equipment & Accessories:</div>
        <div class="grid-container" id="inventory-items-container">
          <!-- Populated by JS -->
        </div>
      </div>
    </div>

    <!-- Game Over / Victory Modal -->
    <div class="modal-overlay" id="modal-end">
      <div class="modal-card title-card">
        <h1 id="end-title">GAME OVER</h1>
        <p id="end-message" style="color: #cbd5e1; font-size: 15px;"></p>
        <button class="start-btn" id="btn-restart-game">RESPAWN AT SAVE SHRINE</button>
      </div>
    </div>

  </div>

  <script>
    /* ==========================================================================
       1. AUDIO SYNTHESIZER (Web Audio API)
       ========================================================================== */
    class SoundEngine {
      constructor() {
        this.ctx = null;
        this.enabled = true;
      }

      init() {
        if (!this.ctx) {
          const AudioContext = window.AudioContext || window.webkitAudioContext;
          if (AudioContext) {
            this.ctx = new AudioContext();
          }
        }
        if (this.ctx && this.ctx.state === 'suspended') {
          this.ctx.resume();
        }
      }

      play(type) {
        if (!this.enabled) return;
        this.init();
        if (!this.ctx) return;

        const t = this.ctx.currentTime;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.connect(gain);
        gain.connect(this.ctx.destination);

        switch(type) {
          case 'step':
            osc.type = 'triangle';
            osc.frequency.setValueAtTime(120, t);
            osc.frequency.exponentialRampToValueAtTime(40, t + 0.08);
            gain.gain.setValueAtTime(0.08, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.08);
            osc.start(t);
            osc.stop(t + 0.08);
            break;

          case 'swing':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(280, t);
            osc.frequency.exponentialRampToValueAtTime(80, t + 0.15);
            gain.gain.setValueAtTime(0.2, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15);
            osc.start(t);
            osc.stop(t + 0.15);
            break;

          case 'hit':
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(180, t);
            osc.frequency.exponentialRampToValueAtTime(40, t + 0.12);
            gain.gain.setValueAtTime(0.25, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
            osc.start(t);
            osc.stop(t + 0.12);
            break;

          case 'bow':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(400, t);
            osc.frequency.exponentialRampToValueAtTime(900, t + 0.1);
            gain.gain.setValueAtTime(0.18, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
            osc.start(t);
            osc.stop(t + 0.12);
            break;

          case 'pickup':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(523.25, t); // C5
            osc.frequency.setValueAtTime(659.25, t + 0.08); // E5
            osc.frequency.setValueAtTime(783.99, t + 0.16); // G5
            gain.gain.setValueAtTime(0.2, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.28);
            osc.start(t);
            osc.stop(t + 0.28);
            break;

          case 'coin':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(987.77, t); // B5
            osc.frequency.setValueAtTime(1318.51, t + 0.06); // E6
            gain.gain.setValueAtTime(0.15, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.18);
            osc.start(t);
            osc.stop(t + 0.18);
            break;

          case 'shop':
            osc.type = 'triangle';
            osc.frequency.setValueAtTime(440, t);
            osc.frequency.setValueAtTime(554.37, t + 0.1);
            gain.gain.setValueAtTime(0.2, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.22);
            osc.start(t);
            osc.stop(t + 0.22);
            break;

          case 'save':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(329.63, t); // E4
            osc.frequency.setValueAtTime(440, t + 0.12); // A4
            osc.frequency.setValueAtTime(554.37, t + 0.24); // C#5
            osc.frequency.setValueAtTime(659.25, t + 0.36); // E5
            gain.gain.setValueAtTime(0.25, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.5);
            osc.start(t);
            osc.stop(t + 0.5);
            break;

          case 'hurt':
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(120, t);
            osc.frequency.linearRampToValueAtTime(50, t + 0.2);
            gain.gain.setValueAtTime(0.3, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
            osc.start(t);
            osc.stop(t + 0.2);
            break;

          case 'boss_defeat':
            osc.type = 'square';
            osc.frequency.setValueAtTime(200, t);
            osc.frequency.exponentialRampToValueAtTime(800, t + 0.4);
            gain.gain.setValueAtTime(0.3, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.6);
            osc.start(t);
            osc.stop(t + 0.6);
            break;

          case 'dash':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(300, t);
            osc.frequency.exponentialRampToValueAtTime(150, t + 0.1);
            gain.gain.setValueAtTime(0.15, t);
            gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
            osc.start(t);
            osc.stop(t + 0.1);
            break;
        }
      }
    }

    const sound = new SoundEngine();

    /* ==========================================================================
       2. GAME STATE & CONSTANTS
       ========================================================================== */
    const CANVAS_WIDTH = 960;
    const CANVAS_HEIGHT = 640;
    const MAP_SIZE = 2400; // 2400x2400 world

    // Game Worlds / Zones
    const ZONES = {
      VILLAGE: { name: "Oakhaven Village", bg: "#1e293b", floor: "#283548", wall: "#0f172a", boundary: { x: 0, y: 0, w: 1200, h: 1200 } },
      FOREST: { name: "Whispering Woods", bg: "#064e3b", floor: "#065f46", wall: "#022c22", boundary: { x: 1200, y: 0, w: 1200, h: 1200 } },
      TEMPLE: { name: "Sunken Temple", bg: "#1e1b4b", floor: "#312e81", wall: "#0f172a", boundary: { x: 0, y: 1200, w: 1200, h: 1200 } },
      DUNGEON: { name: "Dread Caverns", bg: "#450a0a", floor: "#7f1d1d", wall: "#290606", boundary: { x: 1200, y: 1200, w: 1200, h: 1200 } }
    };

    // Weapon Definitions
    const MELEE_WEAPONS = [
      { id: 'dagger', name: "Rusty Dagger", dmg: 2, range: 45, arc: Math.PI * 0.5, cooldown: 250, color: '#cbd5e1', icon: '🗡️' },
      { id: 'broadsword', name: "Knight's Broadsword", dmg: 4, range: 65, arc: Math.PI * 0.75, cooldown: 350, color: '#38bdf8', icon: '⚔️' },
      { id: 'flameblade', name: "Rune Flameblade", dmg: 7, range: 85, arc: Math.PI * 0.9, cooldown: 280, color: '#f97316', icon: '🔥' }
    ];

    const RANGED_WEAPON = { id: 'gale_bow', name: "Gale Bow", dmg: 3, speed: 12, cooldown: 400, color: '#34d399', icon: '🏹' };

    // Shop Catalog
    const SHOP_ITEMS = [
      { id: 'health_potion', name: "Health Potion", cost: 15, icon: '🧪', desc: "Restores 2 HP immediately.", type: 'consumable' },
      { id: 'broadsword_upgrade', name: "Knight's Broadsword", cost: 40, icon: '⚔️', desc: "Melee Upgrade 1: Increased range, higher cleave damage.", type: 'upgrade_melee', reqLevel: 1 },
      { id: 'flameblade_upgrade', name: "Rune Flameblade", cost: 100, icon: '🔥', desc: "Melee Upgrade 2: High damage flame arcs that burn foes.", type: 'upgrade_melee', reqLevel: 2 },
      { id: 'swiftness_ring', name: "Ring of Swiftness", cost: 35, icon: '💍', desc: "Increases movement speed by 25%.", type: 'accessory' },
      { id: 'amulet_vitality', name: "Amulet of Vitality", cost: 60, icon: '📿', desc: "Permanently adds 1 Max HP Heart Slot (up to 5 HP).", type: 'accessory_hp' },
      { id: 'power_elixir', name: "Elixir of Might", cost: 50, icon: '🍷', desc: "+2 Attack Damage bonus to all attacks.", type: 'accessory_dmg' }
    ];

    /* Game State Object */
    const gameState = {
      started: false,
      paused: false,
      gameOver: false,
      victory: false,
      
      // Player Stats
      hp: 3,             // Current HP (Starts at 3)
      maxHp: 3,          // Max HP (Starts at 3, max 5)
      stamina: 100,
      maxStamina: 100,
      gold: 0,
      
      // Equipment & Progression
      meleeLevel: 0,     // 0: Dagger, 1: Broadsword, 2: Flameblade
      hasRanged: false,  // Discovered halfway through game (Sunken Temple Guardian)
      activeWeaponType: 'melee', // 'melee' or 'ranged'
      
      // Inventory
      inventory: [],
      equippedAccessories: {
        swiftness: false,
        might: false
      },

      // World Save
      lastSavePoint: { x: 300, y: 300, zone: "Oakhaven Village" },

      // Bosses Defeated
      bosses: {
        templeGuardian: false, // Drops Gale Bow
        dreadLord: false       // Final Boss
      }
    };

    /* Controls State */
    const keys = {};
    const mouse = { x: 0, y: 0, worldX: 0, worldY: 0, downLeft: false, downRight: false };

    /* ==========================================================================
       3. PLAYER CLASS & ENTITIES
       ========================================================================== */
    class Player {
      constructor(x, y) {
        this.x = x;
        this.y = y;
        this.radius = 18;
        this.baseSpeed = 3.8;
        this.vx = 0;
        this.vy = 0;
        
        // Combat timers
        this.lastMeleeAttack = 0;
        this.lastRangedAttack = 0;
        this.invulnerableUntil = 0;
        this.isAttackingMelee = false;
        this.meleeAttackAngle = 0;
        this.meleeAttackProgress = 0; // 0 to 1

        // Dash mechanics
        this.isDashing = false;
        this.dashEndTime = 0;
        this.dashCooldown = 0;

        // Visual animation
        this.facingAngle = 0;
        this.walkCycle = 0;
        this.isMoving = false;
      }

      get speed() {
        let spd = this.baseSpeed;
        if (gameState.equippedAccessories.swiftness) spd *= 1.25;
        if (this.isDashing) spd *= 2.5;
        return spd;
      }

      get damageBonus() {
        return gameState.equippedAccessories.might ? 2 : 0;
      }

      update(dt) {
        if (gameState.gameOver) return;

        // Stamina regen
        if (!this.isDashing && gameState.stamina < gameState.maxStamina) {
          gameState.stamina = Math.min(gameState.maxStamina, gameState.stamina + 25 * dt);
        }

        // Handle dash
        if (this.isDashing && Date.now() > this.dashEndTime) {
          this.isDashing = false;
        }

        // Movement input
        let dx = 0;
        let dy = 0;
        if (keys['KeyW'] || keys['ArrowUp'] || keys['up']) dy -= 1;
        if (keys['KeyS'] || keys['ArrowDown'] || keys['down']) dy += 1;
        if (keys['KeyA'] || keys['ArrowLeft'] || keys['left']) dx -= 1;
        if (keys['KeyD'] || keys['ArrowRight'] || keys['right']) dx += 1;

        if (dx !== 0 && dy !== 0) {
          dx *= 0.7071;
          dy *= 0.7071;
        }

        this.isMoving = dx !== 0 || dy !== 0;

        // Dash trigger (C key or Dash button)
        if ((keys['KeyC'] || keys['dash']) && !this.isDashing && gameState.stamina >= 30 && Date.now() > this.dashCooldown) {
          this.isDashing = true;
          this.dashEndTime = Date.now() + 200;
          this.dashCooldown = Date.now() + 600;
          gameState.stamina -= 30;
          sound.play('dash');
          
          // Spawn dash dust particles
          for (let i = 0; i < 8; i++) {
            particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 3, (Math.random() - 0.5) * 3, '#94a3b8', 12, 4));
          }
        }

        // Move player
        const moveDist = this.speed * dt * 60;
        let newX = this.x + dx * moveDist;
        let newY = this.y + dy * moveDist;

        // Collision with world boundaries & obstacles
        if (!checkWorldCollision(newX, this.y, this.radius)) this.x = newX;
        if (!checkWorldCollision(this.x, newY, this.radius)) this.y = newY;

        // Update facing angle towards mouse
        this.facingAngle = Math.atan2(mouse.worldY - this.y, mouse.worldX - this.x);

        // Walking audio feedback & animation
        if (this.isMoving) {
          this.walkCycle += dt * 10;
          if (Math.sin(this.walkCycle) > 0.8 && Math.random() < 0.2) {
            sound.play('step');
          }
        }

        // Handle Attacks
        if (mouse.downLeft || keys['Space'] || keys['attack']) {
          this.performMeleeAttack();
        } else if ((mouse.downRight || keys['ShiftLeft'] || keys['ranged']) && gameState.hasRanged) {
          this.performRangedAttack();
        }

        // Update Melee Swing animation state
        if (this.isAttackingMelee) {
          const w = MELEE_WEAPONS[gameState.meleeLevel];
          const elapsed = Date.now() - this.lastMeleeAttack;
          this.meleeAttackProgress = Math.min(1, elapsed / (w.cooldown * 0.6));
          if (this.meleeAttackProgress >= 1) {
            this.isAttackingMelee = false;
          }
        }
      }

      performMeleeAttack() {
        const weapon = MELEE_WEAPONS[gameState.meleeLevel];
        if (Date.now() - this.lastMeleeAttack < weapon.cooldown) return;

        this.lastMeleeAttack = Date.now();
        this.isAttackingMelee = true;
        this.meleeAttackAngle = this.facingAngle;
        this.meleeAttackProgress = 0;
        sound.play('swing');

        const totalDmg = weapon.dmg + this.damageBonus;

        // Check hits on enemies
        enemies.forEach(enemy => {
          const dist = Math.hypot(enemy.x - this.x, enemy.y - this.y);
          if (dist <= weapon.range + enemy.radius) {
            const angleToEnemy = Math.atan2(enemy.y - this.y, enemy.x - this.x);
            let diff = Math.abs(this.meleeAttackAngle - angleToEnemy);
            while (diff > Math.PI) diff -= Math.PI * 2;
            diff = Math.abs(diff);

            if (diff <= weapon.arc / 2) {
              enemy.takeDamage(totalDmg, this.facingAngle);
              
              // Flameblade burn/fire effect particles
              if (weapon.id === 'flameblade') {
                for (let i = 0; i < 6; i++) {
                  particles.push(new Particle(enemy.x, enemy.y, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 5, '#f97316', 15, 5));
                }
              }
            }
          }
        });

        // Break breakables (pots/chests)
        breakables.forEach(b => {
          const dist = Math.hypot(b.x - this.x, b.y - this.y);
          if (dist <= weapon.range + b.radius) {
            b.destroy();
          }
        });
      }

      performRangedAttack() {
        if (!gameState.hasRanged) return;
        if (Date.now() - this.lastRangedAttack < RANGED_WEAPON.cooldown) return;

        this.lastRangedAttack = Date.now();
        sound.play('bow');

        const angle = this.facingAngle;
        projectiles.push(new Projectile(
          this.x + Math.cos(angle) * 20,
          this.y + Math.sin(angle) * 20,
          Math.cos(angle) * RANGED_WEAPON.speed,
          Math.sin(angle) * RANGED_WEAPON.speed,
          RANGED_WEAPON.dmg + this.damageBonus,
          true
        ));
      }

      takeDamage(amount) {
        if (Date.now() < this.invulnerableUntil || this.isDashing) return;

        gameState.hp = Math.max(0, gameState.hp - amount);
        this.invulnerableUntil = Date.now() + 800; // 0.8s i-frames
        sound.play('hurt');
        screenShake = 12;

        // Hurt Particles
        for (let i = 0; i < 10; i++) {
          particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, '#ef4444', 20, 4));
        }

        updateHUD();

        if (gameState.hp <= 0) {
          triggerGameOver(false);
        }
      }

      draw(ctx) {
        ctx.save();
        ctx.translate(this.x, this.y);

        // Flash during i-frames
        if (Date.now() < this.invulnerableUntil && Math.floor(Date.now() / 80) % 2 === 0) {
          ctx.globalAlpha = 0.5;
        }

        // Shadow
        ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
        ctx.beginPath();
        ctx.ellipse(0, 14, 16, 8, 0, 0, Math.PI * 2);
        ctx.fill();

        // Walking tilt/animation
        const walkOffset = this.isMoving ? Math.sin(this.walkCycle * 2) * 3 : 0;

        // Body (Adventurer Coat)
        ctx.rotate(this.facingAngle);

        // Armor Visual Feedback based on equipped accessories
        ctx.fillStyle = gameState.equippedAccessories.might ? '#b91c1c' : (gameState.equippedAccessories.swiftness ? '#047857' : '#2563eb');
        ctx.beginPath();
        ctx.arc(0, walkOffset, this.radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#1e293b';
        ctx.stroke();

        // Head / Helmet
        ctx.fillStyle = '#fde047';
        ctx.beginPath();
        ctx.arc(4, walkOffset, 8, 0, Math.PI * 2);
        ctx.fill();

        // Visor / Eyes
        ctx.fillStyle = '#0f172a';
        ctx.fillRect(8, walkOffset - 3, 3, 6);

        // Draw Weapon Visual Representation
        const currentWeapon = MELEE_WEAPONS[gameState.meleeLevel];
        ctx.fillStyle = currentWeapon.color;

        if (this.isAttackingMelee) {
          // Swing Arc Effect
          const swingAngle = (this.meleeAttackProgress - 0.5) * currentWeapon.arc;
          ctx.save();
          ctx.rotate(swingAngle);

          ctx.fillStyle = currentWeapon.color;
          ctx.beginPath();
          ctx.moveTo(10, 0);
          ctx.lineTo(currentWeapon.range, -10);
          ctx.lineTo(currentWeapon.range + 10, 0);
          ctx.lineTo(currentWeapon.range, 10);
          ctx.closePath();
          ctx.fill();

          // Arc slash visual streak
          ctx.strokeStyle = currentWeapon.color;
          ctx.lineWidth = 4;
          ctx.beginPath();
          ctx.arc(0, 0, currentWeapon.range, -currentWeapon.arc / 2, currentWeapon.arc / 2);
          ctx.stroke();

          ctx.restore();
        } else {
          // Weapon Holstered / Ready in hand
          ctx.fillRect(12, 10 + walkOffset, currentWeapon.range * 0.4, 4);
        }

        // Ranged Bow Visual if held
        if (gameState.hasRanged) {
          ctx.strokeStyle = '#10b981';
          ctx.lineWidth = 3;
          ctx.beginPath();
          ctx.arc(8, -10 + walkOffset, 12, -Math.PI * 0.4, Math.PI * 0.4);
          ctx.stroke();
        }

        ctx.restore();
      }
    }

    /* ==========================================================================
       4. ENEMY CLASSES & AI
       ========================================================================== */
    class Enemy {
      constructor(x, y, type) {
        this.x = x;
        this.y = y;
        this.type = type; // 'goblin', 'skeleton', 'guardian_boss', 'dread_lord'
        this.isBoss = type.includes('boss') || type.includes('lord');
        
        // Configure enemy stats
        switch(type) {
          case 'goblin':
            this.hp = 6;
            this.maxHp = 6;
            this.radius = 16;
            this.speed = 2.2;
            this.damage = 1;
            this.color = '#22c55e';
            this.goldReward = 8;
            break;
          case 'skeleton':
            this.hp = 12;
            this.maxHp = 12;
            this.radius = 18;
            this.speed = 1.8;
            this.damage = 1;
            this.color = '#cbd5e1';
            this.goldReward = 15;
            break;
          case 'guardian_boss': // Mid-game Boss (Sunken Temple)
            this.hp = 65;
            this.maxHp = 65;
            this.radius = 36;
            this.speed = 1.4;
            this.damage = 2;
            this.color = '#818cf8';
            this.goldReward = 80;
            break;
          case 'dread_lord': // Final Boss (Dread Caverns)
            this.hp = 120;
            this.maxHp = 120;
            this.radius = 42;
            this.speed = 1.6;
            this.damage = 2;
            this.color = '#ef4444';
            this.goldReward = 200;
            break;
        }

        this.vx = 0;
        this.vy = 0;
        this.lastAttackTime = 0;
        this.attackCooldown = this.isBoss ? 1200 : 1000;
        this.hurtTimer = 0;
      }

      update(dt, player) {
        if (this.hurtTimer > 0) this.hurtTimer -= dt;

        const dist = Math.hypot(player.x - this.x, player.y - this.y);

        // Aggro & Chasing AI
        const aggroRange = this.isBoss ? 600 : 320;
        if (dist < aggroRange) {
          const angle = Math.atan2(player.y - this.y, player.x - this.x);
          
          // Move towards player
          if (dist > this.radius + player.radius - 5) {
            let moveX = Math.cos(angle) * this.speed * dt * 60;
            let moveY = Math.sin(angle) * this.speed * dt * 60;

            if (!checkWorldCollision(this.x + moveX, this.y, this.radius)) this.x += moveX;
            if (!checkWorldCollision(this.x, this.y + moveY, this.radius)) this.y += moveY;
          }

          // Melee attack player
          if (dist <= this.radius + player.radius + 8) {
            if (Date.now() - this.lastAttackTime > this.attackCooldown) {
              this.lastAttackTime = Date.now();
              player.takeDamage(this.damage);
            }
          }

          // Boss Ranged Attacks
          if (this.isBoss && Math.random() < 0.015 && Date.now() - this.lastAttackTime > 800) {
            this.lastAttackTime = Date.now();
            projectiles.push(new Projectile(
              this.x, this.y,
              Math.cos(angle) * 6,
              Math.sin(angle) * 6,
              this.damage,
              false
            ));
          }
        }
      }

      takeDamage(amount, knockbackAngle) {
        this.hp -= amount;
        this.hurtTimer = 0.2;
        sound.play('hit');

        // Knockback
        if (!this.isBoss) {
          this.x += Math.cos(knockbackAngle) * 15;
          this.y += Math.sin(knockbackAngle) * 15;
        }

        // Floating Damage Number
        damageNumbers.push(new DamageNumber(this.x, this.y - 15, amount));

        // Impact particles
        for (let i = 0; i < 5; i++) {
          particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, this.color, 12, 3));
        }

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

      die() {
        // Drop Gold
        const coinsCount = Math.floor(this.goldReward / 5);
        for (let i = 0; i < coinsCount; i++) {
          pickups.push(new Pickup(
            this.x + (Math.random() - 0.5) * 30,
            this.y + (Math.random() - 0.5) * 30,
            'gold',
            5
          ));
        }

        // Mid-game Boss Defeated: Unlocks Gale Bow!
        if (this.type === 'guardian_boss') {
          gameState.bosses.templeGuardian = true;
          gameState.hasRanged = true;
          sound.play('boss_defeat');
          showBanner("🔑 GUARDIAN DEFEATED! UNLOCKED: GALE BOW 🏹");
          document.getElementById('ranged-slot').style.opacity = '1';
          document.getElementById('ranged-icon').textContent = RANGED_WEAPON.icon;
          updateHUD();
        }

        // Final Boss Defeated: Game Victory!
        if (this.type === 'dread_lord') {
          gameState.bosses.dreadLord = true;
          sound.play('boss_defeat');
          triggerGameOver(true);
        }

        // Death Explosion particles
        for (let i = 0; i < 16; i++) {
          particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 8, (Math.random() - 0.5) * 8, this.color, 25, 5));
        }
      }

      draw(ctx) {
        ctx.save();
        ctx.translate(this.x, this.y);

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

        // Shadow
        ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
        ctx.beginPath();
        ctx.ellipse(0, this.radius * 0.8, this.radius, this.radius * 0.5, 0, 0, Math.PI * 2);
        ctx.fill();

        // Enemy Body
        ctx.fillStyle = this.hurtTimer > 0 ? '#ffffff' : this.color;
        ctx.beginPath();
        ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.lineWidth = 2;
        ctx.strokeStyle = '#0f172a';
        ctx.stroke();

        // Boss crown/details
        if (this.isBoss) {
          ctx.fillStyle = '#ffd700';
          ctx.beginPath();
          ctx.moveTo(-12, -this.radius);
          ctx.lineTo(-6, -this.radius - 10);
          ctx.lineTo(0, -this.radius - 4);
          ctx.lineTo(6, -this.radius - 10);
          ctx.lineTo(12, -this.radius);
          ctx.closePath();
          ctx.fill();
        }

        // Enemy Health Bar (if damaged)
        if (this.hp < this.maxHp && !this.isBoss) {
          ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
          ctx.fillRect(-20, -this.radius - 12, 40, 6);
          ctx.fillStyle = '#ef4444';
          ctx.fillRect(-20, -this.radius - 12, (this.hp / this.maxHp) * 40, 6);
        }

        ctx.restore();
      }
    }

    /* ==========================================================================
       5. PROJECTILES, PICKUPS & ENVIRONMENT
       ========================================================================== */
    class Projectile {
      constructor(x, y, vx, vy, damage, isPlayer) {
        this.x = x;
        this.y = y;
        this.vx = vx;
        this.vy = vy;
        this.damage = damage;
        this.isPlayer = isPlayer;
        this.radius = isPlayer ? 5 : 7;
        this.life = 120; // 2 seconds lifespan
      }

      update(dt) {
        this.x += this.vx * dt * 60;
        this.y += this.vy * dt * 60;
        this.life--;

        // Collision with walls
        if (checkWorldCollision(this.x, this.y, this.radius)) {
          this.life = 0;
        }

        // Collision with targets
        if (this.isPlayer) {
          enemies.forEach(enemy => {
            if (Math.hypot(enemy.x - this.x, enemy.y - this.y) < enemy.radius + this.radius) {
              enemy.takeDamage(this.damage, Math.atan2(this.vy, this.vx));
              this.life = 0;
            }
          });
        } else {
          if (Math.hypot(player.x - this.x, player.y - this.y) < player.radius + this.radius) {
            player.takeDamage(this.damage);
            this.life = 0;
          }
        }
      }

      draw(ctx) {
        ctx.save();
        ctx.fillStyle = this.isPlayer ? '#34d399' : '#f87171';
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      }
    }

    class Pickup {
      constructor(x, y, type, value = 1) {
        this.x = x;
        this.y = y;
        this.type = type; // 'gold', 'heart'
        this.value = value;
        this.radius = 10;
        this.bobble = Math.random() * Math.PI * 2;
      }

      update(dt, player) {
        this.bobble += dt * 4;
        const dist = Math.hypot(player.x - this.x, player.y - this.y);

        // Magnet attraction to player
        if (dist < 80) {
          this.x += (player.x - this.x) * 0.1;
          this.y += (player.y - this.y) * 0.1;
        }

        if (dist < player.radius + this.radius) {
          if (this.type === 'gold') {
            gameState.gold += this.value;
            sound.play('coin');
          } else if (this.type === 'heart') {
            gameState.hp = Math.min(gameState.maxHp, gameState.hp + 2);
            sound.play('pickup');
          }
          updateHUD();
          return true; // Destroyed
        }
        return false;
      }

      draw(ctx) {
        ctx.save();
        const yOffset = Math.sin(this.bobble) * 4;
        ctx.translate(this.x, this.y + yOffset);

        if (this.type === 'gold') {
          ctx.fillStyle = '#ffd700';
          ctx.beginPath();
          ctx.arc(0, 0, 7, 0, Math.PI * 2);
          ctx.fill();
          ctx.strokeStyle = '#b45309';
          ctx.lineWidth = 2;
          ctx.stroke();
        } else {
          ctx.fillStyle = '#ef4444';
          ctx.font = '14px sans-serif';
          ctx.textAlign = 'center';
          ctx.fillText('❤️', 0, 4);
        }

        ctx.restore();
      }
    }

    class Breakable {
      constructor(x, y) {
        this.x = x;
        this.y = y;
        this.radius = 16;
      }

      destroy() {
        sound.play('hit');
        // Drop gold or heart
        if (Math.random() < 0.7) {
          pickups.push(new Pickup(this.x, this.y, 'gold', 5));
        } else {
          pickups.push(new Pickup(this.x, this.y, 'heart', 2));
        }

        for (let i = 0; i < 8; i++) {
          particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 5, '#b45309', 15, 4));
        }

        const idx = breakables.indexOf(this);
        if (idx > -1) breakables.splice(idx, 1);
      }

      draw(ctx) {
        ctx.save();
        ctx.translate(this.x, this.y);
        ctx.fillStyle = '#78350f';
        ctx.beginPath();
        ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#451a03';
        ctx.stroke();
        ctx.restore();
      }
    }

    class SaveShrine {
      constructor(x, y, name) {
        this.x = x;
        this.y = y;
        this.name = name;
        this.radius = 24;
      }

      activate() {
        gameState.lastSavePoint = { x: this.x, y: this.y, zone: this.name };
        gameState.hp = gameState.maxHp; // Heal to full on save!
        updateHUD();
        sound.play('save');
        showBanner(`✨ SAVED & HEALED AT ${this.name.toUpperCase()} SHRINE ✨`);

        for (let i = 0; i < 20; i++) {
          particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, '#38bdf8', 30, 4));
        }
      }

      draw(ctx) {
        ctx.save();
        ctx.translate(this.x, this.y);

        // Shrine Base
        ctx.fillStyle = '#334155';
        ctx.fillRect(-20, -20, 40, 40);

        // Glowing Orb
        const glow = Math.sin(Date.now() / 300) * 3 + 12;
        ctx.fillStyle = '#38bdf8';
        ctx.beginPath();
        ctx.arc(0, -10, glow, 0, Math.PI * 2);
        ctx.fill();

        ctx.restore();
      }
    }

    /* Particles & Floating Damage Text */
    class Particle {
      constructor(x, y, vx, vy, color, life, size) {
        this.x = x;
        this.y = y;
        this.vx = vx;
        this.vy = vy;
        this.color = color;
        this.maxLife = life;
        this.life = life;
        this.size = size;
      }

      update() {
        this.x += this.vx;
        this.y += this.vy;
        this.life--;
      }

      draw(ctx) {
        ctx.save();
        ctx.globalAlpha = Math.max(0, this.life / this.maxLife);
        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      }
    }

    class DamageNumber {
      constructor(x, y, text) {
        this.x = x;
        this.y = y;
        this.text = text;
        this.life = 40;
      }

      update() {
        this.y -= 0.8;
        this.life--;
      }

      draw(ctx) {
        ctx.save();
        ctx.globalAlpha = Math.max(0, this.life / 40);
        ctx.font = 'bold 16px sans-serif';
        ctx.fillStyle = '#fbbf24';
        ctx.strokeStyle = '#000000';
        ctx.lineWidth = 3;
        ctx.strokeText(this.text, this.x, this.y);
        ctx.fillText(this.text, this.x, this.y);
        ctx.restore();
      }
    }

    /* ==========================================================================
       6. WORLD INSTANTIATION & COLLISION
       ========================================================================== */
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');

    let player = new Player(300, 300);
    let enemies = [];
    let projectiles = [];
    let pickups = [];
    let breakables = [];
    let saveShrines = [];
    let particles = [];
    let damageNumbers = [];
    let worldObstacles = [];
    let screenShake = 0;

    function initWorld() {
      enemies = [];
      projectiles = [];
      pickups = [];
      breakables = [];
      saveShrines = [];
      particles = [];
      damageNumbers = [];
      worldObstacles = [];

      // Save Shrines across regions
      saveShrines.push(new SaveShrine(300, 200, "Village"));
      saveShrines.push(new SaveShrine(1500, 200, "Whispering Forest"));
      saveShrines.push(new SaveShrine(300, 1400, "Sunken Temple"));
      saveShrines.push(new SaveShrine(1500, 1400, "Dread Caverns"));

      // Obstacles / Trees / Rocks
      // Village Fence / River boundaries
      worldObstacles.push({ x: 1180, y: 0, w: 40, h: 2400 }); // Zone divider vertical
      worldObstacles.push({ x: 0, y: 1180, w: 2400, h: 40 }); // Zone divider horizontal

      // Add gaps in zone dividers for doorways/bridges
      worldObstacles.push({ x: 600, y: 400, w: 80, h: 80 }); // Village trees/houses
      worldObstacles.push({ x: 400, y: 800, w: 100, h: 100 });

      // Populate Enemies per Zone
      // Forest (Zone 2)
      for (let i = 0; i < 14; i++) {
        enemies.push(new Enemy(1300 + Math.random() * 900, 100 + Math.random() * 900, 'goblin'));
        breakables.push(new Breakable(1300 + Math.random() * 900, 100 + Math.random() * 900));
      }

      // Temple (Zone 3 - Mid Game)
      for (let i = 0; i < 12; i++) {
        enemies.push(new Enemy(100 + Math.random() * 900, 1300 + Math.random() * 900, 'skeleton'));
        breakables.push(new Breakable(100 + Math.random() * 900, 1300 + Math.random() * 900));
      }
      // Mid-game Boss: Temple Guardian (Guardians Gale Bow)
      enemies.push(new Enemy(600, 1800, 'guardian_boss'));

      // Dungeon (Zone 4 - End Game)
      for (let i = 0; i < 15; i++) {
        enemies.push(new Enemy(1300 + Math.random() * 900, 1300 + Math.random() * 900, Math.random() < 0.5 ? 'goblin' : 'skeleton'));
        breakables.push(new Breakable(1300 + Math.random() * 900, 1300 + Math.random() * 900));
      }
      // Final Boss: Dread Lord
      enemies.push(new Enemy(1800, 1800, 'dread_lord'));
    }

    function checkWorldCollision(x, y, radius) {
      if (x - radius < 0 || x + radius > MAP_SIZE || y - radius < 0 || y + radius > MAP_SIZE) {
        return true;
      }
      for (let obs of worldObstacles) {
        // Doorway passages
        if (obs.x === 1180 && (y > 500 && y < 700 || y > 1700 && y < 1900)) continue;
        if (obs.y === 1180 && (x > 500 && x < 700 || x > 1700 && x < 1900)) continue;

        if (x + radius > obs.x && x - radius < obs.x + obs.w &&
            y + radius > obs.y && y - radius < obs.y + obs.h) {
          return true;
        }
      }
      return false;
    }

    /* ==========================================================================
       7. UI, HUD & SHOP CONTROLLER
       ========================================================================== */
    function updateHUD() {
      // Render Heart Shapes
      // Requirement: Health tracked visually using shapes/symbols (hearts). Each heart = 2 HP.
      // Starts at 3 HP (1.5 hearts), upgradeable to 5 HP (2.5 hearts).
      const heartsContainer = document.getElementById('hearts-wrapper');
      heartsContainer.innerHTML = '';

      const totalHearts = Math.ceil(gameState.maxHp / 2);
      let currentHp = gameState.hp;

      for (let i = 0; i < totalHearts; i++) {
        const heartSvg = document.createElement('div');
        heartSvg.className = 'heart-icon';

        if (currentHp >= 2) {
          // Full Heart (2 points)
          heartSvg.innerHTML = `<svg viewBox="0 0 32 32" fill="#ef4444"><path d="M16 28s-12-7.5-12-16a7 7 0 0 1 12-4.5A7 7 0 0 1 28 12c0 8.5-12 16-12 16z"/></svg>`;
          currentHp -= 2;
        } else if (currentHp === 1) {
          // Half Heart (1 point)
          heartSvg.innerHTML = `<svg viewBox="0 0 32 32"><defs><linearGradient id="half"><stop offset="50%" stop-color="#ef4444"/><stop offset="50%" stop-color="#334155"/></linearGradient></defs><path fill="url(#half)" d="M16 28s-12-7.5-12-16a7 7 0 0 1 12-4.5A7 7 0 0 1 28 12c0 8.5-12 16-12 16z"/></svg>`;
          currentHp -= 1;
        } else {
          // Empty Heart (0 points)
          heartSvg.innerHTML = `<svg viewBox="0 0 32 32" fill="#334155"><path d="M16 28s-12-7.5-12-16a7 7 0 0 1 12-4.5A7 7 0 0 1 28 12c0 8.5-12 16-12 16z"/></svg>`;
        }
        heartsContainer.appendChild(heartSvg);
      }

      // Stamina Bar
      const staminaPct = (gameState.stamina / gameState.maxStamina) * 100;
      document.getElementById('stamina-fill').style.width = `${staminaPct}%`;

      // Gold Count
      document.getElementById('gold-count').textContent = gameState.gold;

      // Melee Weapon Icon
      const currentWeapon = MELEE_WEAPONS[gameState.meleeLevel];
      document.getElementById('melee-icon').textContent = currentWeapon.icon;
    }

    function showBanner(text) {
      const banner = document.getElementById('banner-notify');
      banner.textContent = text;
      banner.classList.add('show');
      setTimeout(() => banner.classList.remove('show'), 3000);
    }

    function openShop() {
      gameState.paused = true;
      sound.play('shop');
      const shopContainer = document.getElementById('shop-items-container');
      shopContainer.innerHTML = '';

      SHOP_ITEMS.forEach(item => {
        const card = document.createElement('div');
        card.className = 'shop-item-card';

        // Check if melee upgrade is already purchased or available
        let canBuy = gameState.gold >= item.cost;
        let isPurchased = false;

        if (item.type === 'upgrade_melee') {
          if (gameState.meleeLevel >= item.reqLevel) {
            isPurchased = true;
            canBuy = false;
          } else if (gameState.meleeLevel < item.reqLevel - 1) {
            canBuy = false; // Must buy previous level first
          }
        } else if (item.type === 'accessory' && gameState.equippedAccessories.swiftness) {
          isPurchased = true;
          canBuy = false;
        } else if (item.type === 'accessory_dmg' && gameState.equippedAccessories.might) {
          isPurchased = true;
          canBuy = false;
        } else if (item.type === 'accessory_hp' && gameState.maxHp >= 5) {
          isPurchased = true;
          canBuy = false;
        }

        card.innerHTML = `
          <div class="item-icon">${item.icon}</div>
          <div class="item-name">${item.name}</div>
          <div class="item-desc">${item.desc}</div>
          <div class="item-price">🪙 ${item.cost} Gold</div>
          <button class="buy-btn" ${!canBuy || isPurchased ? 'disabled' : ''}>
            ${isPurchased ? 'OWNED' : 'BUY'}
          </button>
        `;

        const btn = card.querySelector('.buy-btn');
        if (canBuy && !isPurchased) {
          btn.addEventListener('click', () => {
            gameState.gold -= item.cost;
            sound.play('coin');

            if (item.type === 'consumable') {
              gameState.inventory.push(item);
            } else if (item.type === 'upgrade_melee') {
              gameState.meleeLevel = item.reqLevel;
              showBanner(`🗡️ WEAPON EVOLVED: ${MELEE_WEAPONS[gameState.meleeLevel].name}!`);
            } else if (item.type === 'accessory') {
              gameState.equippedAccessories.swiftness = true;
              gameState.inventory.push(item);
              showBanner(`⚡ EQUIPPED: Ring of Swiftness!`);
            } else if (item.type === 'accessory_dmg') {
              gameState.equippedAccessories.might = true;
              gameState.inventory.push(item);
              showBanner(`🍷 ELIXIR ABSORBED: +2 Attack Power!`);
            } else if (item.type === 'accessory_hp') {
              // Health Expansion: Max HP increased to 5 total points!
              gameState.maxHp = Math.min(5, gameState.maxHp + 2); // Add +2 HP (1 Heart) up to 5 total
              gameState.hp = gameState.maxHp;
              gameState.inventory.push(item);
              showBanner(`💖 MAX HEALTH INCREASED TO ${gameState.maxHp} HP!`);
            }

            updateHUD();
            openShop(); // Refresh shop display
          });
        }

        shopContainer.appendChild(card);
      });

      document.getElementById('modal-shop').style.display = 'flex';
    }

    function openInventory() {
      gameState.paused = true;
      const invContainer = document.getElementById('inventory-items-container');
      invContainer.innerHTML = '';

      if (gameState.inventory.length === 0) {
        invContainer.innerHTML = '<div style="color:#94a3b8; font-size:13px; grid-column: 1/-1;">Your satchel is currently empty. Visit the village shop or explore the world to acquire items!</div>';
      } else {
        gameState.inventory.forEach((item, index) => {
          const card = document.createElement('div');
          card.className = 'inventory-item-card';

          card.innerHTML = `
            <div class="item-icon">${item.icon}</div>
            <div class="item-name">${item.name}</div>
            <div class="item-desc">${item.desc}</div>
            ${item.type === 'consumable' ? `<button class="use-btn">USE</button>` : `<div style="font-size:10px; color:#10b981; font-weight:bold; margin-top: auto;">ACTIVE</div>`}
          `;

          if (item.type === 'consumable') {
            card.querySelector('.use-btn').addEventListener('click', () => {
              gameState.hp = Math.min(gameState.maxHp, gameState.hp + 2);
              gameState.inventory.splice(index, 1);
              sound.play('pickup');
              updateHUD();
              openInventory();
            });
          }

          invContainer.appendChild(card);
        });
      }

      document.getElementById('modal-inventory').style.display = 'flex';
    }

    /* Close Modals */
    document.getElementById('btn-close-shop').addEventListener('click', () => {
      document.getElementById('modal-shop').style.display = 'none';
      gameState.paused = false;
    });

    document.getElementById('btn-close-inventory').addEventListener('click', () => {
      document.getElementById('modal-inventory').style.display = 'none';
      gameState.paused = false;
    });

    /* Game Over / Restart Handler */
    function triggerGameOver(isVictory) {
      gameState.gameOver = true;
      const modal = document.getElementById('modal-end');
      const title = document.getElementById('end-title');
      const msg = document.getElementById('end-message');
      const btn = document.getElementById('btn-restart-game');

      if (isVictory) {
        title.textContent = "🏆 VICTORY ATTAINED!";
        title.style.color = "#38ef7d";
        msg.textContent = "You have vanquished the Dread Lord and cleansed the realm of Aethelgard! Your courage and legend will be remembered for generations.";
        btn.textContent = "PLAY AGAIN";
      } else {
        title.textContent = "💀 YOU HAVE FALLEN";
        title.style.color = "#ef4444";
        msg.textContent = "The darkness overcame your strength. Respawn at your last saved shrine with restored vitality.";
        btn.textContent = "RESPAWN AT SHRINE";
      }

      modal.style.display = 'flex';
    }

    document.getElementById('btn-restart-game').addEventListener('click', () => {
      document.getElementById('modal-end').style.display = 'none';
      gameState.gameOver = false;
      gameState.hp = gameState.maxHp;
      player.x = gameState.lastSavePoint.x;
      player.y = gameState.lastSavePoint.y;
      updateHUD();
    });

    /* ==========================================================================
       8. MAIN GAME LOOP & INPUT LISTENERS
       ========================================================================== */
    let lastTime = performance.now();

    function gameLoop(now) {
      const dt = Math.min((now - lastTime) / 1000, 0.1);
      lastTime = now;

      if (gameState.started && !gameState.paused && !gameState.gameOver) {
        updateGame(dt);
      }

      renderGame();
      requestAnimationFrame(gameLoop);
    }

    function updateGame(dt) {
      player.update(dt);

      // Update Projectiles
      for (let i = projectiles.length - 1; i >= 0; i--) {
        projectiles[i].update(dt);
        if (projectiles[i].life <= 0) projectiles.splice(i, 1);
      }

      // Update Enemies
      let activeBoss = null;
      for (let i = enemies.length - 1; i >= 0; i--) {
        enemies[i].update(dt, player);
        if (enemies[i].isBoss && Math.hypot(enemies[i].x - player.x, enemies[i].y - player.y) < 600) {
          activeBoss = enemies[i];
        }
        if (enemies[i].hp <= 0) enemies.splice(i, 1);
      }

      // Update Boss HUD
      const bossHud = document.getElementById('boss-bar-container');
      if (activeBoss) {
        bossHud.style.display = 'flex';
        document.getElementById('boss-name-text').textContent = activeBoss.type === 'guardian_boss' ? 'TEMPLE GUARDIAN GOLEM' : 'DREAD LORD OF AETHELGARD';
        document.getElementById('boss-bar-fill').style.width = `${(activeBoss.hp / activeBoss.maxHp) * 100}%`;
      } else {
        bossHud.style.display = 'none';
      }

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

      // Update Particles & Floating Text
      for (let i = particles.length - 1; i >= 0; i--) {
        particles[i].update();
        if (particles[i].life <= 0) particles.splice(i, 1);
      }

      for (let i = damageNumbers.length - 1; i >= 0; i--) {
        damageNumbers[i].update();
        if (damageNumbers[i].life <= 0) damageNumbers.splice(i, 1);
      }

      // Interactions (Save Shrine & Shop)
      let interactionAvailable = false;
      let promptText = "";

      // Check distance to Village Shop NPC (X: 300, Y: 400)
      if (Math.hypot(player.x - 300, player.y - 400) < 50) {
        interactionAvailable = true;
        promptText = "Open Village Shop & Armory";
        if (keys['KeyE']) {
          keys['KeyE'] = false;
          openShop();
        }
      }

      // Check distance to Save Shrines
      saveShrines.forEach(shrine => {
        if (Math.hypot(player.x - shrine.x, player.y - shrine.y) < 45) {
          interactionAvailable = true;
          promptText = `Save Game & Heal (${shrine.name})`;
          if (keys['KeyE']) {
            keys['KeyE'] = false;
            shrine.activate();
          }
        }
      });

      const promptEl = document.getElementById('interaction-prompt');
      if (interactionAvailable) {
        promptEl.style.display = 'flex';
        document.getElementById('prompt-text').textContent = promptText;
      } else {
        promptEl.style.display = 'none';
      }

      // Screen Shake dampening
      if (screenShake > 0) screenShake *= 0.85;
    }

    function renderGame() {
      ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

      // Camera Offset tracking Player
      ctx.save();
      let camX = player.x - CANVAS_WIDTH / 2;
      let camY = player.y - CANVAS_HEIGHT / 2;

      // Screen Shake offset
      if (screenShake > 0.5) {
        camX += (Math.random() - 0.5) * screenShake;
        camY += (Math.random() - 0.5) * screenShake;
      }

      ctx.translate(-camX, -camY);

      // Draw World Floor / Zones
      Object.values(ZONES).forEach(zone => {
        const b = zone.boundary;
        ctx.fillStyle = zone.bg;
        ctx.fillRect(b.x, b.y, b.w, b.h);

        // Tile Grid Lines
        ctx.strokeStyle = zone.floor;
        ctx.lineWidth = 2;
        for (let x = b.x; x < b.x + b.w; x += 100) {
          ctx.beginPath();
          ctx.moveTo(x, b.y);
          ctx.lineTo(x, b.y + b.h);
          ctx.stroke();
        }
        for (let y = b.y; y < b.y + b.h; y += 100) {
          ctx.beginPath();
          ctx.moveTo(b.x, y);
          ctx.lineTo(b.x + b.w, y);
          ctx.stroke();
        }
      });

      // Draw Village Shopkeeper NPC
      ctx.fillStyle = '#f59e0b';
      ctx.beginPath();
      ctx.arc(300, 400, 20, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 12px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText("SHOP", 300, 375);

      // Draw Save Shrines
      saveShrines.forEach(s => s.draw(ctx));

      // Draw World Obstacles / Walls
      ctx.fillStyle = '#0f172a';
      worldObstacles.forEach(obs => {
        ctx.fillRect(obs.x, obs.y, obs.w, obs.h);
      });

      // Draw Breakable Containers
      breakables.forEach(b => b.draw(ctx));

      // Draw Pickups
      pickups.forEach(p => p.draw(ctx));

      // Draw Projectiles
      projectiles.forEach(p => p.draw(ctx));

      // Draw Enemies
      enemies.forEach(e => e.draw(ctx));

      // Draw Player
      player.draw(ctx);

      // Draw Particles & Floating Numbers
      particles.forEach(p => p.draw(ctx));
      damageNumbers.forEach(d => d.draw(ctx));

      ctx.restore();
    }

    /* Key & Mouse Event Listeners */
    window.addEventListener('keydown', e => {
      keys[e.code] = true;

      // Inventory Toggle
      if (e.code === 'KeyI' || e.code === 'Tab') {
        e.preventDefault();
        const invModal = document.getElementById('modal-inventory');
        if (invModal.style.display === 'flex') {
          invModal.style.display = 'none';
          gameState.paused = false;
        } else {
          openInventory();
        }
      }
    });

    window.addEventListener('keyup', e => {
      keys[e.code] = false;
    });

    canvas.addEventListener('mousemove', e => {
      const rect = canvas.getBoundingClientRect();
      mouse.x = e.clientX - rect.left;
      mouse.y = e.clientY - rect.top;

      // Convert screen coords to world coords
      const camX = player.x - CANVAS_WIDTH / 2;
      const camY = player.y - CANVAS_HEIGHT / 2;
      mouse.worldX = mouse.x + camX;
      mouse.worldY = mouse.y + camY;
    });

    canvas.addEventListener('mousedown', e => {
      sound.init();
      if (e.button === 0) mouse.downLeft = true;
      if (e.button === 2) mouse.downRight = true;
    });

    canvas.addEventListener('mouseup', e => {
      if (e.button === 0) mouse.downLeft = false;
      if (e.button === 2) mouse.downRight = false;
    });

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

    // Mobile / Touch controls binding
    const setupTouchBtn = (id, keyName) => {
      const btn = document.getElementById(id);
      if (!btn) return;
      btn.addEventListener('touchstart', (e) => { e.preventDefault(); keys[keyName] = true; });
      btn.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyName] = false; });
    };

    setupTouchBtn('btn-up', 'up');
    setupTouchBtn('btn-down', 'down');
    setupTouchBtn('btn-left', 'left');
    setupTouchBtn('btn-right', 'right');
    setupTouchBtn('btn-attack', 'attack');
    setupTouchBtn('btn-ranged', 'ranged');
    setupTouchBtn('btn-dash', 'dash');

    // Start Game Button
    document.getElementById('btn-start-game').addEventListener('click', () => {
      sound.init();
      document.getElementById('modal-title').style.display = 'none';
      gameState.started = true;
      initWorld();
      updateHUD();
    });

    // Start Loop
    requestAnimationFrame(gameLoop);


  </script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.6-flash","provider":"openrouter","persona":"full","sessionId":"70d0e7d4-dd59-4ada-8cd3-7d308c65f166","tokensIn":132561,"tokensOut":27059,"tokensTotal":159620,"turns":6,"toolCalls":5,"failedToolCalls":0,"timestamp":"2026-07-21T19:23:17.537Z"} -->