← Warden results

Warden

Gemini 3.8 Flash · full

Initial view of Warden

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 100,961 bytes · SHA-256 03ba28f694f3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Warden's Corrosion</title>
<style>
  :root {
    --gold: #f5c542;
    --gold-glow: rgba(245, 197, 66, 0.4);
    --orange: #e66826;
    --orange-glow: rgba(230, 104, 38, 0.5);
    --slate: #151a24;
    --slate-deep: #0a0d14;
    --slate-panel: rgba(20, 26, 38, 0.94);
    --slate-border: #334155;
    --corrosion: #1c222b;
    --aether: #5eead4;
    --aether-glow: rgba(94, 234, 212, 0.5);
    --ruby: #ef4444;
    --ruby-dim: #7f1d1d;
    --font-serif: "Cinzel", "Trajan Pro", "Palatino Linotype", "Book Antiqua", Georgia, serif;
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  }

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

  body, html {
    width: 100%;
    height: 100%;
    background: var(--slate-deep);
    color: #e2e8f0;
    font-family: var(--font-sans);
    overflow: hidden;
    position: relative;
  }

  #game-container {
    position: relative;
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #000;
  }

  #viewport-wrapper {
    position: relative;
    box-shadow: 0 0 50px rgba(0,0,0,0.8), 0 0 20px rgba(245, 197, 66, 0.1);
    border: 2px solid #2a3447;
    border-radius: 4px;
    overflow: hidden;
  }

  canvas#gameCanvas {
    display: block;
    background: #0c1017;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
  }

  /* Screen effects */
  #vignette {
    position: absolute;
    inset: 0;
    pointer-events: none;
    box-shadow: inset 0 0 100px rgba(0,0,0,0.8);
    transition: opacity 0.3s;
  }

  #damage-flash {
    position: absolute;
    inset: 0;
    background: radial-gradient(circle, transparent 40%, rgba(239, 68, 68, 0.65) 100%);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.08s ease-out;
  }

  #upgrade-halo {
    position: absolute;
    inset: 0;
    border: 6px solid var(--gold);
    box-shadow: inset 0 0 50px var(--gold-glow);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.5s ease-out;
  }

  /* HUD Overlay */
  #hud {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    padding: 16px 22px;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    pointer-events: none;
    z-index: 10;
  }

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

  .zone-title-bar {
    font-family: var(--font-serif);
    font-size: 13px;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: #94a3b8;
    text-shadow: 0 2px 4px rgba(0,0,0,0.8);
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .zone-title-bar .accent {
    color: var(--gold);
    font-weight: bold;
  }

  /* Hearts System */
  .hearts-row {
    display: flex;
    gap: 6px;
    align-items: center;
  }

  .heart-container {
    width: 26px;
    height: 26px;
    position: relative;
    filter: drop-shadow(0 2px 4px rgba(0,0,0,0.7));
  }

  .heart-container svg {
    width: 100%;
    height: 100%;
    display: block;
  }

  .heart-bg {
    fill: #1e2530;
    stroke: #475569;
    stroke-width: 1.5;
  }

  .heart-fill {
    fill: var(--ruby);
    transition: clip-path 0.2s ease-out;
  }

  /* Gold display */
  .gold-pill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    background: rgba(15, 23, 42, 0.85);
    border: 1px solid #334155;
    padding: 5px 12px;
    border-radius: 20px;
    font-family: var(--font-serif);
    font-size: 14px;
    color: var(--gold);
    font-weight: 600;
    box-shadow: 0 4px 10px rgba(0,0,0,0.5);
  }

  .gold-pill svg {
    width: 16px;
    height: 16px;
    fill: var(--gold);
  }

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

  .quick-weapon-status {
    display: flex;
    align-items: center;
    gap: 12px;
    background: rgba(15, 23, 42, 0.85);
    border: 1px solid #334155;
    padding: 6px 14px;
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.5);
  }

  .weapon-icon-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    font-size: 10px;
    color: #94a3b8;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-family: var(--font-serif);
  }

  .weapon-icon-box .slot-label {
    font-size: 9px;
    color: #64748b;
    margin-bottom: 2px;
  }

  .weapon-tag {
    font-size: 12px;
    font-weight: 700;
    color: var(--gold);
  }

  /* Quick Prompts / Controls banner */
  #controls-hint {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(15, 23, 42, 0.75);
    border: 1px solid rgba(255,255,255,0.1);
    padding: 6px 16px;
    border-radius: 20px;
    font-size: 11px;
    letter-spacing: 0.5px;
    color: #cbd5e1;
    pointer-events: none;
    z-index: 10;
    display: flex;
    gap: 14px;
    backdrop-filter: blur(4px);
  }

  .key-chip {
    background: #334155;
    color: #fff;
    padding: 1px 6px;
    border-radius: 4px;
    font-weight: 600;
    font-size: 10px;
    border: 1px solid #475569;
  }

  /* Interaction prompt over game elements */
  #prompt-banner {
    position: absolute;
    top: 75%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: rgba(10, 15, 25, 0.92);
    border: 1px solid var(--gold);
    color: #fff;
    padding: 10px 22px;
    border-radius: 8px;
    font-family: var(--font-serif);
    font-size: 13px;
    letter-spacing: 1px;
    box-shadow: 0 0 25px rgba(245, 197, 66, 0.25);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s, transform 0.2s;
    z-index: 15;
    text-align: center;
  }

  #prompt-banner.active {
    opacity: 1;
    transform: translate(-50%, -60%);
  }

  /* MODALS & PANELS: Inventory, Shop, Dialogues, Beacon End */
  .modal-overlay {
    position: absolute;
    inset: 0;
    background: rgba(6, 9, 15, 0.85);
    backdrop-filter: blur(6px);
    display: none;
    justify-content: center;
    align-items: center;
    z-index: 30;
    pointer-events: auto;
  }

  .modal-overlay.active {
    display: flex;
  }

  .panel-window {
    width: 620px;
    max-width: 90%;
    background: var(--slate-panel);
    border: 2px solid var(--gold);
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.9), 0 0 30px var(--gold-glow);
    border-radius: 8px;
    padding: 24px 28px;
    position: relative;
  }

  .panel-header {
    border-bottom: 1px solid #334155;
    padding-bottom: 12px;
    margin-bottom: 18px;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .panel-title {
    font-family: var(--font-serif);
    font-size: 20px;
    color: var(--gold);
    letter-spacing: 2px;
    text-transform: uppercase;
  }

  .panel-close-hint {
    font-size: 11px;
    color: #94a3b8;
  }

  .panel-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 20px;
  }

  .inventory-card {
    background: rgba(15, 23, 42, 0.6);
    border: 1px solid #334155;
    border-radius: 6px;
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    position: relative;
    transition: border-color 0.2s, transform 0.15s;
  }

  .inventory-card.equipped {
    border-color: var(--aether);
    box-shadow: inset 0 0 10px rgba(94, 234, 212, 0.15);
  }

  .inventory-card.equipped::after {
    content: "EQUIPPED";
    position: absolute;
    top: 8px;
    right: 8px;
    font-size: 9px;
    font-weight: bold;
    color: var(--aether);
    letter-spacing: 1px;
  }

  .item-name {
    font-family: var(--font-serif);
    font-size: 14px;
    font-weight: 700;
    color: #f1f5f9;
  }

  .item-desc {
    font-size: 11px;
    line-height: 1.4;
    color: #94a3b8;
  }

  .item-actions {
    margin-top: 6px;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .btn {
    background: #1e293b;
    color: #f1f5f9;
    border: 1px solid #475569;
    padding: 6px 14px;
    border-radius: 4px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.5px;
    cursor: pointer;
    transition: all 0.15s;
  }

  .btn:hover {
    background: var(--gold);
    color: #000;
    border-color: var(--gold);
  }

  .btn.btn-accent {
    background: #b45309;
    border-color: var(--gold);
    color: #fff;
  }

  .btn.btn-accent:hover {
    background: var(--gold);
    color: #000;
  }

  .btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
    background: #1e293b !important;
    color: #64748b !important;
    border-color: #334155 !important;
  }

  /* Shop Specifics */
  .shop-item-row {
    background: rgba(15, 23, 42, 0.6);
    border: 1px solid #334155;
    border-radius: 6px;
    padding: 12px 16px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
  }

  .shop-item-info h4 {
    font-family: var(--font-serif);
    color: #fff;
    font-size: 14px;
    margin-bottom: 2px;
  }

  .shop-item-info p {
    font-size: 11px;
    color: #94a3b8;
  }

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

  .price-tag {
    color: var(--gold);
    font-weight: bold;
    font-size: 13px;
    display: flex;
    align-items: center;
    gap: 4px;
    font-family: var(--font-serif);
  }

  /* Notification Banner */
  #toast {
    position: absolute;
    bottom: 48px;
    left: 50%;
    transform: translateX(-50%) translateY(20px);
    background: rgba(12, 17, 26, 0.95);
    border: 1px solid var(--gold);
    color: #f1f5f9;
    padding: 10px 24px;
    border-radius: 30px;
    font-family: var(--font-serif);
    font-size: 13px;
    letter-spacing: 1px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.8), 0 0 15px var(--gold-glow);
    opacity: 0;
    pointer-events: none;
    transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    z-index: 40;
    text-align: center;
  }

  #toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  /* Zone title splash */
  #zone-splash {
    position: absolute;
    top: 30%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.8s ease-in-out;
    z-index: 25;
  }

  #zone-splash.active {
    opacity: 1;
  }

  #zone-splash h1 {
    font-family: var(--font-serif);
    font-size: 32px;
    letter-spacing: 6px;
    text-transform: uppercase;
    color: #fff;
    text-shadow: 0 0 20px rgba(245, 197, 66, 0.8);
    margin-bottom: 6px;
  }

  #zone-splash p {
    font-size: 13px;
    letter-spacing: 3px;
    text-transform: uppercase;
    color: var(--gold);
  }

  /* End Game Overlay */
  #victory-modal {
    position: absolute;
    inset: 0;
    background: radial-gradient(circle, #1a2233 0%, #06090e 100%);
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 100;
    text-align: center;
    padding: 30px;
  }

  #victory-modal.active {
    display: flex;
  }

  #victory-modal h1 {
    font-family: var(--font-serif);
    font-size: 40px;
    letter-spacing: 6px;
    color: var(--gold);
    text-shadow: 0 0 30px var(--gold-glow);
    margin-bottom: 16px;
  }

  #victory-modal p {
    max-width: 600px;
    font-size: 15px;
    line-height: 1.7;
    color: #cbd5e1;
    margin-bottom: 24px;
  }

  /* Responsive touch controls on mobile */
  #mobile-controls {
    display: none;
    position: absolute;
    bottom: 20px;
    left: 0;
    right: 0;
    padding: 0 20px;
    justify-content: space-between;
    pointer-events: none;
    z-index: 20;
  }

  @media (max-width: 800px) {
    #mobile-controls { display: flex; }
    #controls-hint { display: none; }
  }

  .touch-dpad, .touch-actions {
    display: flex;
    gap: 8px;
    pointer-events: auto;
  }

  .touch-btn {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: rgba(30, 41, 59, 0.85);
    border: 1px solid #475569;
    color: #fff;
    font-size: 14px;
    font-weight: bold;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* Audio mute toggle */
  #sound-btn {
    position: absolute;
    top: 16px;
    right: 16px;
    background: rgba(15, 23, 42, 0.85);
    border: 1px solid #334155;
    color: #94a3b8;
    padding: 6px 12px;
    border-radius: 4px;
    font-size: 11px;
    cursor: pointer;
    z-index: 15;
    font-family: var(--font-serif);
    letter-spacing: 1px;
  }
  #sound-btn:hover {
    color: var(--gold);
    border-color: var(--gold);
  }
</style>
</head>
<body>

<div id="game-container">
  <div id="viewport-wrapper">
    <canvas id="gameCanvas" width="960" height="600"></canvas>
    <div id="vignette"></div>
    <div id="damage-flash"></div>
    <div id="upgrade-halo"></div>

    <!-- Sound Toggle inside HUD right -->
    <div id="hud">
      <div class="hud-left">
        <div class="zone-title-bar">
          <span id="hud-zone-name">THE ASHEN THRESHOLD</span>
          <span class="accent">•</span>
          <span id="hud-zone-sub">ZONE I</span>
        </div>
        <div class="hearts-row" id="hearts-container">
          <!-- Dynamically populated hearts -->
        </div>
        <div class="gold-pill">
          <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="#f5c542" stroke-width="2" fill="#d97706"/><polygon points="12,5 15,12 12,19 9,12" fill="#fef08a"/></svg>
          <span id="gold-counter">0</span> SIGILS
        </div>
      </div>

      <div class="hud-right">
        <div style="display: flex; gap: 8px; align-items: center;">
          <div class="quick-weapon-status">
            <div class="weapon-icon-box">
              <span class="slot-label">MELEE [J/Z]</span>
              <span class="weapon-tag" id="hud-melee-name">EMBER BLADE</span>
            </div>
            <div class="weapon-icon-box" id="hud-ranged-box" style="opacity: 0.35;">
              <span class="slot-label">RANGED [K/X]</span>
              <span class="weapon-tag" id="hud-ranged-name">LOCKED</span>
            </div>
            <button class="btn" id="btn-open-inventory" style="pointer-events:auto; margin-left:6px;" onclick="toggleInventory()">BAG [I]</button>
          </div>
          <button id="sound-btn" style="pointer-events:auto; position:static;" onclick="toggleAudio()">AUDIO: ON</button>
        </div>
      </div>
    </div>

    <!-- Zone Splash Display -->
    <div id="zone-splash">
      <h1 id="splash-title">THE ASHEN THRESHOLD</h1>
      <p id="splash-subtitle">Aurelion's Sunken Observatory</p>
    </div>

    <!-- Context Interaction Prompt -->
    <div id="prompt-banner">PRESS <span class="key-chip">E</span> TO INTERACT</div>

    <!-- Controls Bar -->
    <div id="controls-hint">
      <span><span class="key-chip">WASD / ARROWS</span> Move</span>
      <span><span class="key-chip">J / Z / SPACE</span> Melee</span>
      <span><span class="key-chip">K / X</span> Aether Shard</span>
      <span><span class="key-chip">E</span> Interact</span>
      <span><span class="key-chip">I / TAB</span> Inventory</span>
    </div>

    <!-- Toast Notification -->
    <div id="toast">Checkpoint Reached</div>

    <!-- INVENTORY MODAL -->
    <div class="modal-overlay" id="inventory-modal">
      <div class="panel-window">
        <div class="panel-header">
          <div class="panel-title">WARDEN'S SATCHEL</div>
          <div class="panel-close-hint">Press <span class="key-chip">I</span> or <span class="key-chip">ESC</span> to return</div>
        </div>

        <div style="margin-bottom: 12px; font-size: 12px; color: #94a3b8;">
          Active Equipment & Consumables. Only one accessory active at a time.
        </div>

        <div class="panel-grid" id="inventory-items-grid">
          <!-- Dynamically populated items -->
        </div>

        <div style="display:flex; justify-content:space-between; align-items:center; border-top:1px solid #334155; padding-top:14px;">
          <div style="font-size:12px; color:#cbd5e1;">
            Equipped: <strong style="color:var(--gold)" id="equipped-accessory-label">None</strong>
          </div>
          <button class="btn" onclick="toggleInventory()">Close</button>
        </div>
      </div>
    </div>

    <!-- SHOP MODAL -->
    <div class="modal-overlay" id="shop-modal">
      <div class="panel-window" style="width: 660px;">
        <div class="panel-header">
          <div class="panel-title">MERCHANT OF THE ARCHIVES</div>
          <div class="price-tag"><span id="shop-gold-display">0</span> SIGILS</div>
        </div>

        <div style="margin-bottom: 16px; font-size: 12px; color: #94a3b8; font-style: italic;">
          "The black-iron corrosion devours all, Warden... Exchange your sigils for surviving antiquities."
        </div>

        <div id="shop-items-container">
          <!-- Dynamically populated shop stock -->
        </div>

        <div style="display:flex; justify-content:flex-end; border-top:1px solid #334155; padding-top:14px; margin-top:12px;">
          <button class="btn" onclick="closeShop()">Depart Altar</button>
        </div>
      </div>
    </div>

    <!-- VICTORY / RESOLUTION MODAL -->
    <div id="victory-modal">
      <h1>THE BEACON REIGNITED</h1>
      <p>
        The celestial gears groan into life. Purifying solar light floods through Aurelion's Sunken Observatory, dissolving the black-iron corrosion that choked its ancient towers.
      </p>
      <p style="color: var(--gold); font-family: var(--font-serif); font-size: 17px;">
        The Last Warden stands vigilant as the storm clouds break above the valley.
      </p>
      <div style="margin-top: 20px; display:flex; gap:12px;">
        <button class="btn btn-accent" onclick="restartGame()">Begin Anew (New Cycle)</button>
      </div>
    </div>

  </div>
</div>

<script>
/* =========================================================================
   THE WARDEN'S CORROSION - GAME ENGINE & SYSTEM IMPLEMENTATION
   ========================================================================= */

// --- AUDIO SYNTHESIS SYSTEM (Web Audio API) ---
class SoundEngine {
  constructor() {
    this.ctx = null;
    this.enabled = true;
  }

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

  playStep(type = 'stone') {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    const filter = this.ctx.createBiquadFilter();

    if (type === 'metal') {
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(320 + Math.random() * 40, t);
      osc.frequency.exponentialRampToValueAtTime(140, t + 0.08);
      filter.type = 'bandpass';
      filter.frequency.value = 600;
      gain.gain.setValueAtTime(0.06, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
    } else if (type === 'water') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(450, t);
      osc.frequency.exponentialRampToValueAtTime(180, t + 0.12);
      filter.type = 'lowpass';
      filter.frequency.value = 800;
      gain.gain.setValueAtTime(0.08, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
    } else {
      // stone / bronze grind
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(160 + Math.random() * 20, t);
      osc.frequency.exponentialRampToValueAtTime(60, t + 0.06);
      filter.type = 'lowpass';
      filter.frequency.value = 400;
      gain.gain.setValueAtTime(0.07, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.06);
    }

    osc.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.12);
  }

  playAttack(upgradeLevel) {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;

    if (upgradeLevel === 0) {
      // Base Ember Blade: Sharp whoosh + crisp impact thud
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(400, t);
      osc.frequency.exponentialRampToValueAtTime(120, t + 0.15);
      gain.gain.setValueAtTime(0.2, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(t);
      osc.stop(t + 0.15);

      // Noise whoosh
      this.playWhiteNoise(0.12, 0.15, 800);
    } else if (upgradeLevel === 1) {
      // Ignited Edge: Whoosh + flame crackle + deeper resonant thud
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(320, t);
      osc.frequency.exponentialRampToValueAtTime(90, t + 0.22);
      gain.gain.setValueAtTime(0.25, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.22);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(t);
      osc.stop(t + 0.22);

      // flame crackle burst
      this.playWhiteNoise(0.2, 0.2, 1600);
    } else {
      // Solar Falchion: Heavy wind-up + resonant clang + low bass rumble
      const bass = this.ctx.createOscillator();
      const bassGain = this.ctx.createGain();
      bass.type = 'triangle';
      bass.frequency.setValueAtTime(140, t);
      bass.frequency.exponentialRampToValueAtTime(40, t + 0.38);
      bassGain.gain.setValueAtTime(0.4, t);
      bassGain.gain.exponentialRampToValueAtTime(0.001, t + 0.38);
      bass.connect(bassGain);
      bassGain.connect(this.ctx.destination);
      bass.start(t);
      bass.stop(t + 0.38);

      // Clang
      const clang = this.ctx.createOscillator();
      const clangGain = this.ctx.createGain();
      clang.type = 'sine';
      clang.frequency.setValueAtTime(880, t);
      clang.frequency.exponentialRampToValueAtTime(220, t + 0.3);
      clangGain.gain.setValueAtTime(0.25, t);
      clangGain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
      clang.connect(clangGain);
      clangGain.connect(this.ctx.destination);
      clang.start(t);
      clang.stop(t + 0.3);
    }
  }

  playAetherShoot() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Sharp crystal twang
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(1100, t);
    osc.frequency.exponentialRampToValueAtTime(320, t + 0.18);
    gain.gain.setValueAtTime(0.25, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.18);
  }

  playAetherHit() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Distant impact chime
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(784, t);
    osc.frequency.setValueAtTime(987, t + 0.05);
    gain.gain.setValueAtTime(0.18, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.35);
  }

  playDamage() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Dull body-thud + brief high-pitch dissonant chime
    const thud = this.ctx.createOscillator();
    const thudGain = this.ctx.createGain();
    thud.type = 'triangle';
    thud.frequency.setValueAtTime(90, t);
    thud.frequency.exponentialRampToValueAtTime(30, t + 0.18);
    thudGain.gain.setValueAtTime(0.35, t);
    thudGain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
    thud.connect(thudGain);
    thudGain.connect(this.ctx.destination);
    thud.start(t);
    thud.stop(t + 0.18);

    const chime = this.ctx.createOscillator();
    const chimeGain = this.ctx.createGain();
    chime.type = 'sawtooth';
    chime.frequency.setValueAtTime(630, t);
    chime.frequency.setValueAtTime(610, t + 0.08); // Dissonant shift
    chimeGain.gain.setValueAtTime(0.15, t);
    chimeGain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
    chime.connect(chimeGain);
    chimeGain.connect(this.ctx.destination);
    chime.start(t);
    chime.stop(t + 0.25);
  }

  playEnemyHit() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(240, t);
    osc.frequency.exponentialRampToValueAtTime(70, t + 0.1);
    gain.gain.setValueAtTime(0.2, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.1);
  }

  playEnemyDeath() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(180, t);
    osc.frequency.exponentialRampToValueAtTime(35, t + 0.3);
    gain.gain.setValueAtTime(0.3, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.3);
    this.playWhiteNoise(0.25, 0.25, 500);
  }

  playCoin() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Metallic clink
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(1320, t);
    osc.frequency.setValueAtTime(1760, t + 0.05);
    gain.gain.setValueAtTime(0.12, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.14);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.14);
  }

  playPickup() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Ascending melodic chime
    [523.25, 659.25, 783.99, 1046.5].forEach((freq, i) => {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(freq, t + i * 0.07);
      gain.gain.setValueAtTime(0.15, t + i * 0.07);
      gain.gain.exponentialRampToValueAtTime(0.001, t + i * 0.07 + 0.22);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(t + i * 0.07);
      osc.stop(t + i * 0.07 + 0.25);
    });
  }

  playRareItem() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Resonant chord
    [440, 554.37, 659.25, 880].forEach(freq => {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(freq, t);
      gain.gain.setValueAtTime(0.12, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.6);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(t);
      osc.stop(t + 0.65);
    });
  }

  playSanctuarySave() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    [329.63, 392.00, 493.88, 587.33, 659.25].forEach((freq, idx) => {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(freq, t + idx * 0.08);
      gain.gain.setValueAtTime(0.15, t + idx * 0.08);
      gain.gain.exponentialRampToValueAtTime(0.001, t + idx * 0.08 + 0.45);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(t + idx * 0.08);
      osc.stop(t + idx * 0.08 + 0.5);
    });
  }

  playAltarUpgrade() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Deep majestic fanfare
    [261.63, 329.63, 392.00, 523.25, 659.25, 783.99].forEach((freq, i) => {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = i < 3 ? 'triangle' : 'sine';
      osc.frequency.setValueAtTime(freq, t + i * 0.09);
      gain.gain.setValueAtTime(0.18, t + i * 0.09);
      gain.gain.exponentialRampToValueAtTime(0.001, t + i * 0.09 + 0.7);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(t + i * 0.09);
      osc.stop(t + i * 0.09 + 0.75);
    });
  }

  playShopBuy() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    // Coin drop + confirmation chime
    this.playCoin();
    setTimeout(() => {
      if (!this.enabled || !this.ctx) return;
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(987.77, this.ctx.currentTime);
      gain.gain.setValueAtTime(0.18, this.ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start(this.ctx.currentTime);
      osc.stop(this.ctx.currentTime + 0.35);
    }, 80);
  }

  playSwitchClick() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'square';
    osc.frequency.setValueAtTime(360, t);
    osc.frequency.exponentialRampToValueAtTime(180, t + 0.09);
    gain.gain.setValueAtTime(0.18, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.09);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.09);
  }

  playWallBreak() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(110, t);
    osc.frequency.exponentialRampToValueAtTime(30, t + 0.28);
    gain.gain.setValueAtTime(0.3, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.28);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.28);
    this.playWhiteNoise(0.2, 0.25, 450);
  }

  playFreeze() {
    if (!this.enabled) return;
    this.init();
    const t = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(1400, t);
    osc.frequency.exponentialRampToValueAtTime(400, t + 0.3);
    gain.gain.setValueAtTime(0.2, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(t);
    osc.stop(t + 0.35);
  }

  playWhiteNoise(duration, vol, freqCutoff = 1000) {
    if (!this.ctx) return;
    const bufferSize = this.ctx.sampleRate * duration;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      data[i] = Math.random() * 2 - 1;
    }
    const noise = this.ctx.createBufferSource();
    noise.buffer = buffer;

    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.value = freqCutoff;

    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(vol, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);

    noise.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    noise.start();
  }
}

const audio = new SoundEngine();

function toggleAudio() {
  audio.enabled = !audio.enabled;
  document.getElementById('sound-btn').innerText = audio.enabled ? 'AUDIO: ON' : 'AUDIO: MUTED';
}

/* =========================================================================
   GAME STATE & DATA DEFINITIONS
   ========================================================================= */

const ZONES = [
  { id: 0, name: "The Ashen Threshold", sub: "ZONE I • Prologue", theme: "ashen", floorType: "stone" },
  { id: 1, name: "The Rusted Archives", sub: "ZONE II • Early Archives", theme: "bronze", floorType: "metal" },
  { id: 2, name: "The Collapsed Gallery", sub: "ZONE III • Mid-Early", theme: "gallery", floorType: "stone" },
  { id: 3, name: "The Sunken Vault", sub: "ZONE IV • Turning Point", theme: "sunken", floorType: "water" },
  { id: 4, name: "The Twin Spire", sub: "ZONE V • Late Gauntlet", theme: "spire", floorType: "metal" },
  { id: 5, name: "The Core Chamber", sub: "ZONE VI • Climax", theme: "core", floorType: "stone" }
];

// Player State
const player = {
  x: 200,
  y: 300,
  vx: 0,
  vy: 0,
  speed: 3.4,
  radius: 14,
  facing: { x: 1, y: 0 },
  maxContainers: 3, // Target is 5 max. 1 container = 2 HP
  hp: 6,           // Current HP (starts at 3 * 2 = 6)
  gold: 0,
  weaponUpgrade: 0, // 0: Ember Blade, 1: Ignited Edge (Zone 3), 2: Solar Falchion (Zone 5)
  hasRanged: false, // Aether Shard acquired in Zone 4
  rangedAmmo: 20,
  maxRangedAmmo: 30,
  equippedAccessory: null, // "wardenBand", "phantomLantern", "shadowCloak"
  inventory: {
    // Accessories
    wardenBand: false,
    phantomLantern: false,
    shadowCloak: false,
    // Consumables
    vitalStem: 1,     // Restores 2 containers (4 HP)
    bindingSeal: 0    // Freezes one enemy in place ~3 sec
  },
  // Attack state
  isAttacking: false,
  attackTimer: 0,
  attackDuration: 0.22,
  attackCooldown: 0,
  rangedCooldown: 0,
  invulnerableTimer: 0,
  footstepTimer: 0,
  // Animation
  walkCycle: 0,
  moving: false
};

// Checkpoint state
let activeSavePoint = 0; // 0: Not yet saved, 1, 2, 3
let lastCheckpointState = null;

// Game Systems State
let currentZone = 0;
let enemies = [];
let projectiles = [];
let particles = [];
let floatingTexts = [];
let mapEntities = []; // Chests, switches, walls, altars, merchants, etc.
let promptContext = null;
let isShopOpen = false;
let isInventoryOpen = false;
let isGameWon = false;

// Input Keys
const keys = {
  up: false,
  down: false,
  left: false,
  right: false,
  attackMelee: false,
  attackRanged: false,
  interact: false
};

/* =========================================================================
   ZONE LAYOUT & ENTITY POPULATION
   ========================================================================= */

class Wall {
  constructor(x, y, w, h, isBreakable = false, hp = 3, id = null) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.isBreakable = isBreakable;
    this.maxHp = hp;
    this.hp = hp;
    this.id = id;
    this.broken = false;
  }

  damage(amount) {
    if (!this.isBreakable || this.broken) return;
    this.hp -= amount;
    createSparks(this.x + this.w/2, this.y + this.h/2, '#94a3b8', 6);
    audio.playEnemyHit();
    if (this.hp <= 0) {
      this.broken = true;
      audio.playWallBreak();
      createSparks(this.x + this.w/2, this.y + this.h/2, '#cbd5e1', 20);
      showToast("Weakened Barrier Shattered!");
    }
  }

  draw(ctx) {
    if (this.broken) return;
    ctx.save();
    if (this.isBreakable) {
      // Dark discolored wall with hairline cracks
      ctx.fillStyle = '#1e2633';
      ctx.fillRect(this.x, this.y, this.w, this.h);
      ctx.strokeStyle = '#3b4b66';
      ctx.lineWidth = 2;
      ctx.strokeRect(this.x, this.y, this.w, this.h);

      // Cracks pattern
      ctx.strokeStyle = player.equippedAccessory === 'phantomLantern' ? 'rgba(94, 234, 212, 0.9)' : 'rgba(255, 180, 100, 0.6)';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(this.x + 6, this.y + 6);
      ctx.lineTo(this.x + this.w * 0.45, this.y + this.h * 0.5);
      ctx.lineTo(this.x + this.w * 0.35, this.y + this.h - 6);
      ctx.moveTo(this.x + this.w * 0.45, this.y + this.h * 0.5);
      ctx.lineTo(this.x + this.w - 8, this.y + this.h * 0.3);
      ctx.stroke();

      if (player.equippedAccessory === 'phantomLantern') {
        // Phantom lantern soft blue glow revealing hidden cracks
        ctx.fillStyle = 'rgba(94, 234, 212, 0.2)';
        ctx.fillRect(this.x, this.y, this.w, this.h);
      }
    } else {
      // Sturdy ancient bronze/stone boundary
      ctx.fillStyle = '#161c28';
      ctx.fillRect(this.x, this.y, this.w, this.h);
      ctx.strokeStyle = '#2d3748';
      ctx.lineWidth = 2;
      ctx.strokeRect(this.x, this.y, this.w, this.h);

      // Bronze rivets / aesthetic lines
      ctx.fillStyle = '#2b394f';
      ctx.fillRect(this.x + 4, this.y + 4, 3, 3);
      ctx.fillRect(this.x + this.w - 7, this.y + 4, 3, 3);
      ctx.fillRect(this.x + 4, this.y + this.h - 7, 3, 3);
      ctx.fillRect(this.x + this.w - 7, this.y + this.h - 7, 3, 3);
    }
    ctx.restore();
  }
}

class SanctuaryStone {
  constructor(x, y, stoneId) {
    this.x = x;
    this.y = y;
    this.stoneId = stoneId; // 1, 2, 3
    this.radius = 24;
    this.active = false;
    this.glowTimer = 0;
  }

  update(dt) {
    this.glowTimer += dt * 2.5;
  }

  activate() {
    this.active = true;
    activeSavePoint = this.stoneId;
    // Save checkpoint
    saveCheckpoint();
    // Heal player completely
    player.hp = player.maxContainers * 2;
    updateHealthUI();
    audio.playSanctuarySave();
    createSparks(this.x, this.y, '#f5c542', 25);
    showToast(`Sanctuary Stone ${this.stoneId} Activated — Wounds Cleansed & Progress Saved`);
  }

  draw(ctx) {
    ctx.save();
    // Base pedestal
    ctx.fillStyle = '#1e293b';
    ctx.strokeStyle = this.active ? '#f5c542' : '#64748b';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Inner glowing celestial rune
    const pulse = Math.sin(this.glowTimer) * 0.3 + 0.7;
    ctx.fillStyle = this.active ? `rgba(245, 197, 66, ${pulse})` : 'rgba(100, 116, 139, 0.4)';
    ctx.beginPath();
    ctx.arc(this.x, this.y, 11, 0, Math.PI * 2);
    ctx.fill();

    // Rune spikes
    ctx.strokeStyle = this.active ? '#fef08a' : '#94a3b8';
    ctx.lineWidth = 2;
    for (let i = 0; i < 4; i++) {
      const angle = (Math.PI / 2) * i + (this.active ? this.glowTimer * 0.2 : 0);
      ctx.beginPath();
      ctx.moveTo(this.x + Math.cos(angle) * 7, this.y + Math.sin(angle) * 7);
      ctx.lineTo(this.x + Math.cos(angle) * 20, this.y + Math.sin(angle) * 20);
      ctx.stroke();
    }

    // Ambient floating golden motes if active
    if (this.active && Math.random() < 0.15) {
      particles.push(new Particle(
        this.x + (Math.random() - 0.5) * 30,
        this.y + (Math.random() - 0.5) * 30,
        (Math.random() - 0.5) * 10,
        -15 - Math.random() * 15,
        '#f5c542',
        1.2,
        2
      ));
    }
    ctx.restore();
  }
}

class WeaponAltar {
  constructor(x, y, upgradeLevel, title) {
    this.x = x;
    this.y = y;
    this.upgradeLevel = upgradeLevel; // 1 (Ignited Edge) or 2 (Solar Falchion)
    this.title = title;
    this.radius = 24;
    this.claimed = false;
  }

  activate() {
    if (this.claimed) return;
    this.claimed = true;
    player.weaponUpgrade = this.upgradeLevel;
    updateWeaponUI();
    audio.playAltarUpgrade();
    triggerUpgradeHalo();
    createSparks(this.x, this.y, this.upgradeLevel === 1 ? '#e66826' : '#f5c542', 40);
    showToast(`Ancient Altar Consecrated: ${this.title} Acquired!`);
  }

  draw(ctx) {
    ctx.save();
    // Altar dais
    ctx.fillStyle = '#1e2230';
    ctx.strokeStyle = this.claimed ? '#64748b' : (this.upgradeLevel === 1 ? '#e66826' : '#f5c542');
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    if (!this.claimed) {
      // Floating glowing sword icon
      const hover = Math.sin(Date.now() / 250) * 4;
      ctx.save();
      ctx.translate(this.x, this.y + hover);
      ctx.rotate(Math.PI / 4);
      ctx.fillStyle = this.upgradeLevel === 1 ? '#f97316' : '#fbbf24';
      ctx.fillRect(-2, -14, 4, 28); // Blade
      ctx.fillStyle = '#fff';
      ctx.fillRect(-6, 2, 12, 3); // Crossguard
      ctx.restore();
    } else {
      ctx.fillStyle = '#475569';
      ctx.font = '10px serif';
      ctx.textAlign = 'center';
      ctx.fillText('EMPTY', this.x, this.y + 4);
    }
    ctx.restore();
  }
}

class PressurePlate {
  constructor(x, y, w, h, targetDoorId) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.targetDoorId = targetDoorId;
    this.depressed = false;
  }

  update() {
    let pressed = false;
    // Check player
    if (player.x >= this.x && player.x <= this.x + this.w &&
        player.y >= this.y && player.y <= this.y + this.h) {
      pressed = true;
    }
    // Check enemies on plate
    for (let e of enemies) {
      if (e.x >= this.x && e.x <= this.x + this.w &&
          e.y >= this.y && e.y <= this.y + this.h) {
        pressed = true;
        break;
      }
    }

    if (pressed && !this.depressed) {
      this.depressed = true;
      audio.playSwitchClick();
      // Unlock/open target door
      const door = mapEntities.find(ent => ent instanceof Gate && ent.id === this.targetDoorId);
      if (door) {
        door.open = true;
        showToast("Heavy Mechanism Depressed — Pathway Cleared");
      }
    }
  }

  draw(ctx) {
    ctx.save();
    ctx.fillStyle = this.depressed ? '#334155' : '#475569';
    ctx.fillRect(this.x, this.y, this.w, this.h);
    ctx.strokeStyle = this.depressed ? '#5eead4' : '#94a3b8';
    ctx.lineWidth = 2;
    ctx.strokeRect(this.x, this.y, this.w, this.h);

    // Inner bronze grooves
    ctx.strokeStyle = this.depressed ? '#5eead4' : '#334155';
    ctx.strokeRect(this.x + 4, this.y + 4, this.w - 8, this.h - 8);
    ctx.restore();
  }
}

class RangedCrystalSwitch {
  constructor(x, y, targetDoorId) {
    this.x = x;
    this.y = y;
    this.radius = 16;
    this.targetDoorId = targetDoorId;
    this.activated = false;
    this.pulse = 0;
  }

  hit() {
    if (this.activated) return;
    this.activated = true;
    audio.playSwitchClick();
    audio.playAetherHit();
    createSparks(this.x, this.y, '#5eead4', 24);
    const door = mapEntities.find(ent => ent instanceof Gate && ent.id === this.targetDoorId);
    if (door) {
      door.open = true;
      showToast("Aether Crystal Struck — Gateway Unsealed");
    }
  }

  draw(ctx) {
    this.pulse += 0.05;
    ctx.save();
    ctx.fillStyle = this.activated ? '#5eead4' : '#0f766e';
    ctx.strokeStyle = this.activated ? '#a7f3d0' : '#14b8a6';
    ctx.lineWidth = 3;

    const scale = this.activated ? 1.0 : (1.0 + Math.sin(this.pulse) * 0.1);
    ctx.translate(this.x, this.y);
    ctx.scale(scale, scale);

    // Diamond shape crystal
    ctx.beginPath();
    ctx.moveTo(0, -this.radius);
    ctx.lineTo(this.radius, 0);
    ctx.lineTo(0, this.radius);
    ctx.lineTo(-this.radius, 0);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Core highlight
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(0, 0, 4, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

class Gate {
  constructor(x, y, w, h, id, reqText = "LOCKED GATE") {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.id = id;
    this.open = false;
    this.reqText = reqText;
  }

  draw(ctx) {
    if (this.open) return;
    ctx.save();
    // Iron portcullis bars
    ctx.fillStyle = '#0f172a';
    ctx.fillRect(this.x, this.y, this.w, this.h);
    ctx.strokeStyle = '#64748b';
    ctx.lineWidth = 3;
    ctx.strokeRect(this.x, this.y, this.w, this.h);

    // Vertical bars
    const barCount = Math.max(3, Math.floor(this.w / 12));
    ctx.strokeStyle = '#94a3b8';
    ctx.lineWidth = 2;
    for (let i = 1; i < barCount; i++) {
      const bx = this.x + (this.w / barCount) * i;
      ctx.beginPath();
      ctx.moveTo(bx, this.y);
      ctx.lineTo(bx, this.y + this.h);
      ctx.stroke();
    }
    ctx.restore();
  }
}

class ZoneExit {
  constructor(x, y, w, h, targetZone, spawnX, spawnY) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.targetZone = targetZone;
    this.spawnX = spawnX;
    this.spawnY = spawnY;
    this.glowTimer = 0;
  }

  draw(ctx) {
    this.glowTimer += 0.04;
    ctx.save();
    const glow = Math.sin(this.glowTimer) * 0.25 + 0.75;
    ctx.fillStyle = `rgba(245, 197, 66, ${glow * 0.35})`;
    ctx.fillRect(this.x, this.y, this.w, this.h);

    ctx.strokeStyle = `rgba(245, 197, 66, ${glow})`;
    ctx.lineWidth = 3;
    ctx.strokeRect(this.x, this.y, this.w, this.h);

    // Gateway arch runes
    ctx.fillStyle = '#fef08a';
    ctx.font = 'bold 10px serif';
    ctx.textAlign = 'center';
    ctx.fillText('PORTAL', this.x + this.w / 2, this.y + this.h / 2 + 3);
    ctx.restore();
  }
}

class Chest {
  constructor(x, y, itemType, isGold = false, goldAmount = 0) {
    this.x = x;
    this.y = y;
    this.w = 26;
    this.h = 22;
    this.itemType = itemType; // e.g. "wardenBand", "heartContainer4", "vitalStem", "shadowCloak"
    this.isGold = isGold;
    this.goldAmount = goldAmount;
    this.opened = false;
  }

  open() {
    if (this.opened) return;
    this.opened = true;
    createSparks(this.x + this.w/2, this.y + this.h/2, '#f5c542', 20);

    if (this.isGold) {
      player.gold += this.goldAmount;
      updateGoldUI();
      audio.playRareItem();
      showToast(`Ancient Relic Coffer: Found ${this.goldAmount} Sigils!`);
      return;
    }

    if (this.itemType === 'wardenBand') {
      player.inventory.wardenBand = true;
      if (!player.equippedAccessory) player.equippedAccessory = 'wardenBand';
      audio.playRareItem();
      showToast("Found Relic: Warden's Band! (Equipped on wrist - Increases swing tempo)");
    } else if (this.itemType === 'phantomLantern') {
      player.inventory.phantomLantern = true;
      if (!player.equippedAccessory) player.equippedAccessory = 'phantomLantern';
      audio.playRareItem();
      showToast("Acquired: Phantom Lantern! (Reveals hidden cracks and secret paths)");
    } else if (this.itemType === 'shadowCloak') {
      player.inventory.shadowCloak = true;
      if (!player.equippedAccessory) player.equippedAccessory = 'shadowCloak';
      audio.playRareItem();
      showToast("Acquired: Shadow Cloak! (Reinforces defense against environmental traps)");
    } else if (this.itemType === 'heartContainer4') {
      if (player.maxContainers < 4) {
        player.maxContainers = 4;
        player.hp = Math.min(player.hp + 2, player.maxContainers * 2);
        updateHealthUI();
        audio.playRareItem();
        triggerUpgradeHalo();
        showToast("Heart Container Discovered! Health capacity permanently expanded.");
      }
    } else if (this.itemType === 'heartContainer5') {
      if (player.maxContainers < 5) {
        player.maxContainers = 5;
        player.hp = Math.min(player.hp + 2, player.maxContainers * 2);
        updateHealthUI();
        audio.playRareItem();
        triggerUpgradeHalo();
        showToast("Final Heart Container Discovered! Maximum vitality unlocked.");
      }
    } else if (this.itemType === 'vitalStem') {
      player.inventory.vitalStem++;
      audio.playPickup();
      showToast("Found: Vital Stem (Restores 2 Containers)");
    } else if (this.itemType === 'bindingSeal') {
      player.inventory.bindingSeal++;
      audio.playPickup();
      showToast("Found: Binding Seal (Freezes enemy in place)");
    } else if (this.itemType === 'aetherShard') {
      player.hasRanged = true;
      player.rangedAmmo = player.maxRangedAmmo;
      updateWeaponUI();
      audio.playRareItem();
      triggerUpgradeHalo();
      showToast("THE AETHER SHARD UNLOCKED! Press [K] or [X] to fire distant projectiles.");
    }
  }

  draw(ctx) {
    ctx.save();
    ctx.fillStyle = this.opened ? '#1e293b' : '#78350f';
    ctx.fillRect(this.x, this.y, this.w, this.h);
    ctx.strokeStyle = this.opened ? '#475569' : '#f5c542';
    ctx.lineWidth = 2;
    ctx.strokeRect(this.x, this.y, this.w, this.h);

    if (!this.opened) {
      // Golden lock latch
      ctx.fillStyle = '#fef08a';
      ctx.fillRect(this.x + this.w/2 - 2, this.y + this.h/2 - 2, 4, 6);
    }
    ctx.restore();
  }
}

class MerchantAltar {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.radius = 28;
    this.aura = 0;
  }

  update(dt) {
    this.aura += dt * 1.5;
  }

  interact() {
    openShop();
  }

  draw(ctx) {
    ctx.save();
    // Ambient merchant glow
    const pulse = Math.sin(this.aura) * 0.2 + 0.8;
    ctx.fillStyle = `rgba(147, 51, 234, ${pulse * 0.25})`;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius + 6, 0, Math.PI * 2);
    ctx.fill();

    // Stone Merchant Pedestal
    ctx.fillStyle = '#1e1b4b';
    ctx.strokeStyle = '#c084fc';
    ctx.lineWidth = 2.5;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Floating merchant sigil / lantern
    ctx.fillStyle = '#f5c542';
    ctx.beginPath();
    ctx.arc(this.x, this.y - 4, 8, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = '#fff';
    ctx.font = 'bold 9px serif';
    ctx.textAlign = 'center';
    ctx.fillText('MERCHANT', this.x, this.y + 16);
    ctx.restore();
  }
}

class BeaconMechanism {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.radius = 42;
    this.activated = false;
    this.spin = 0;
  }

  update(dt) {
    this.spin += dt * 1.2;
  }

  activate() {
    if (this.activated) return;
    this.activated = true;
    audio.playAltarUpgrade();
    triggerUpgradeHalo();
    isGameWon = true;
    setTimeout(() => {
      document.getElementById('victory-modal').classList.add('active');
    }, 1200);
  }

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

    // Large sunburst gears
    ctx.rotate(this.spin);
    ctx.strokeStyle = this.activated ? '#f5c542' : '#ca8a04';
    ctx.lineWidth = 4;
    ctx.beginPath();
    ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
    ctx.stroke();

    for (let i = 0; i < 8; i++) {
      const angle = (Math.PI / 4) * i;
      ctx.beginPath();
      ctx.moveTo(Math.cos(angle) * (this.radius - 8), Math.sin(angle) * (this.radius - 8));
      ctx.lineTo(Math.cos(angle) * (this.radius + 12), Math.sin(angle) * (this.radius + 12));
      ctx.stroke();
    }

    // Core sphere
    ctx.rotate(-this.spin * 2);
    ctx.fillStyle = this.activated ? '#fff' : '#f59e0b';
    ctx.beginPath();
    ctx.arc(0, 0, 16, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();
  }
}

// Environmental Spike Traps
class SpikeTrap {
  constructor(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.timer = 0;
    this.active = false;
  }

  update(dt) {
    this.timer += dt;
    this.active = (Math.sin(this.timer * 2.5) > 0.1);

    if (this.active) {
      // Check collision with player
      if (player.x >= this.x && player.x <= this.x + this.w &&
          player.y >= this.y && player.y <= this.y + this.h) {
        // Shadow cloak reduces environmental trap damage
        const dmg = player.equippedAccessory === 'shadowCloak' ? 1 : 2;
        damagePlayer(dmg);
      }
    }
  }

  draw(ctx) {
    ctx.save();
    ctx.fillStyle = '#1e1b24';
    ctx.fillRect(this.x, this.y, this.w, this.h);
    ctx.strokeStyle = '#374151';
    ctx.strokeRect(this.x, this.y, this.w, this.h);

    if (this.active) {
      ctx.fillStyle = '#ef4444';
      // Draw spike teeth
      const cols = Math.floor(this.w / 12);
      for (let i = 0; i < cols; i++) {
        const sx = this.x + i * 12 + 6;
        ctx.beginPath();
        ctx.moveTo(sx - 4, this.y + this.h);
        ctx.lineTo(sx, this.y + 4);
        ctx.lineTo(sx + 4, this.y + this.h);
        ctx.fill();
      }
    }
    ctx.restore();
  }
}

/* =========================================================================
   ENEMY TYPES: CORRUPTED CONSTRUCTS
   ========================================================================= */

class Construct {
  constructor(x, y, type = 'sentinel') {
    this.x = x;
    this.y = y;
    this.type = type; // "sentinel", "rustHulk", "spireGuardian", "archivist"
    this.vx = 0;
    this.vy = 0;
    this.frozenTimer = 0;
    this.attackCooldown = 0;

    if (type === 'sentinel') {
      this.radius = 16;
      this.hp = 3;
      this.maxHp = 3;
      this.speed = 1.6;
      this.damage = 1;
      this.goldDrop = 10;
      this.color = '#78350f';
    } else if (type === 'rustHulk') {
      this.radius = 24;
      this.hp = 7;
      this.maxHp = 7;
      this.speed = 1.0;
      this.damage = 2;
      this.goldDrop = 22;
      this.color = '#451a03';
    } else if (type === 'spireGuardian') {
      this.radius = 28;
      this.hp = 14;
      this.maxHp = 14;
      this.speed = 1.35;
      this.damage = 2;
      this.goldDrop = 55;
      this.color = '#312e81';
    } else if (type === 'archivist') {
      this.radius = 15;
      this.hp = 4;
      this.maxHp = 4;
      this.speed = 1.8;
      this.damage = 1;
      this.goldDrop = 14;
      this.color = '#065f46';
    }
  }

  update(dt) {
    if (this.frozenTimer > 0) {
      this.frozenTimer -= dt;
      return; // Can not move or attack while frozen
    }

    if (this.attackCooldown > 0) this.attackCooldown -= dt;

    // AI: Track player if within aggro distance (360 px)
    const dx = player.x - this.x;
    const dy = player.y - this.y;
    const dist = Math.hypot(dx, dy);

    if (dist < 360 && dist > 18) {
      this.vx = (dx / dist) * this.speed;
      this.vy = (dy / dist) * this.speed;
    } else {
      this.vx *= 0.9;
      this.vy *= 0.9;
    }

    // Move with wall collisions
    const nextX = this.x + this.vx;
    const nextY = this.y + this.vy;

    if (!isSolidAt(nextX, this.y, this.radius)) this.x = nextX;
    if (!isSolidAt(this.x, nextY, this.radius)) this.y = nextY;

    // Attack player on contact
    if (dist < this.radius + player.radius) {
      if (this.attackCooldown <= 0) {
        damagePlayer(this.damage);
        this.attackCooldown = 1.2;
      }
    }
  }

  takeDamage(amt, knockbackX = 0, knockbackY = 0) {
    this.hp -= amt;
    this.x += knockbackX * 8;
    this.y += knockbackY * 8;
    audio.playEnemyHit();

    // Hit particle feedback
    if (player.weaponUpgrade === 1) {
      // Burn flinch & orange sparks
      createSparks(this.x, this.y, '#e66826', 10);
      floatingTexts.push(new FloatingText(this.x, this.y - 12, `${amt} BURN`, '#f97316'));
    } else if (player.weaponUpgrade === 2) {
      // Solar light impact
      createSparks(this.x, this.y, '#f5c542', 15);
      floatingTexts.push(new FloatingText(this.x, this.y - 12, `${amt} SOLAR`, '#fde047'));
    } else {
      createSparks(this.x, this.y, '#f87171', 8);
      floatingTexts.push(new FloatingText(this.x, this.y - 12, `${amt}`, '#fff'));
    }

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

  die() {
    audio.playEnemyDeath();
    createSparks(this.x, this.y, '#94a3b8', 25);
    // Sigils / Money drop
    const drop = this.goldDrop + Math.floor(Math.random() * 6);
    player.gold += drop;
    updateGoldUI();
    floatingTexts.push(new FloatingText(this.x, this.y - 20, `+${drop} SIGILS`, '#f5c542'));
    audio.playCoin();

    // Chance to drop Vital Stem or Binding Seal
    if (Math.random() < 0.2) {
      player.inventory.vitalStem++;
      floatingTexts.push(new FloatingText(this.x + 10, this.y - 32, `+1 VITAL STEM`, '#4ade80'));
    }

    // Specific boss drops
    if (this.type === 'spireGuardian' && !player.inventory.shadowCloak) {
      player.inventory.shadowCloak = true;
      if (!player.equippedAccessory) player.equippedAccessory = 'shadowCloak';
      showToast("Spire Guardian Defeated: Acquired Shadow Cloak!");
      audio.playRareItem();
    }

    // Remove from enemy array
    const idx = enemies.indexOf(this);
    if (idx !== -1) enemies.splice(idx, 1);
  }

  draw(ctx) {
    ctx.save();

    // Frozen visual indication
    if (this.frozenTimer > 0) {
      ctx.fillStyle = 'rgba(94, 234, 212, 0.4)';
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.radius + 6, 0, Math.PI * 2);
      ctx.fill();
    }

    // Construct Body
    ctx.fillStyle = this.color;
    ctx.strokeStyle = '#1e293b';
    ctx.lineWidth = 2.5;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Black-iron corrosion creep lines
    ctx.strokeStyle = '#020617';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(this.x - this.radius * 0.5, this.y - this.radius * 0.3);
    ctx.lineTo(this.x + this.radius * 0.2, this.y + this.radius * 0.4);
    ctx.moveTo(this.x - this.radius * 0.2, this.y + this.radius * 0.6);
    ctx.lineTo(this.x + this.radius * 0.5, this.y - this.radius * 0.2);
    ctx.stroke();

    // Glowing corrupted optical eye
    ctx.fillStyle = '#ef4444';
    ctx.beginPath();
    ctx.arc(this.x, this.y - 2, 4, 0, Math.PI * 2);
    ctx.fill();

    // Health bar above construct
    if (this.hp < this.maxHp) {
      const barW = this.radius * 2;
      const barH = 4;
      const hpPct = Math.max(0, this.hp / this.maxHp);
      ctx.fillStyle = '#1e293b';
      ctx.fillRect(this.x - barW/2, this.y - this.radius - 10, barW, barH);
      ctx.fillStyle = '#ef4444';
      ctx.fillRect(this.x - barW/2, this.y - this.radius - 10, barW * hpPct, barH);
    }

    ctx.restore();
  }
}

/* =========================================================================
   PROJECTILES & PARTICLES
   ========================================================================= */

class AetherProjectile {
  constructor(x, y, vx, vy) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.radius = 5;
    this.alive = true;
    this.life = 1.4;
  }

  update(dt) {
    this.life -= dt;
    if (this.life <= 0) {
      this.alive = false;
      return;
    }

    this.x += this.vx * dt * 60;
    this.y += this.vy * dt * 60;

    // Trail particle
    if (Math.random() < 0.6) {
      particles.push(new Particle(this.x, this.y, (Math.random()-0.5)*15, (Math.random()-0.5)*15, '#5eead4', 0.4, 2));
    }

    // Check hit switches
    for (let ent of mapEntities) {
      if (ent instanceof RangedCrystalSwitch) {
        const d = Math.hypot(this.x - ent.x, this.y - ent.y);
        if (d < this.radius + ent.radius) {
          ent.hit();
          this.alive = false;
          return;
        }
      }
      // Check breakable walls
      if (ent instanceof Wall && ent.isBreakable && !ent.broken) {
        if (this.x >= ent.x && this.x <= ent.x + ent.w &&
            this.y >= ent.y && this.y <= ent.y + ent.h) {
          ent.damage(1);
          this.alive = false;
          return;
        }
      }
      // Check solid gates/walls
      if (ent instanceof Wall && !ent.broken && !ent.isBreakable) {
        if (this.x >= ent.x && this.x <= ent.x + ent.w &&
            this.y >= ent.y && this.y <= ent.y + ent.h) {
          audio.playAetherHit();
          createSparks(this.x, this.y, '#5eead4', 6);
          this.alive = false;
          return;
        }
      }
      if (ent instanceof Gate && !ent.open) {
        if (this.x >= ent.x && this.x <= ent.x + ent.w &&
            this.y >= ent.y && this.y <= ent.y + ent.h) {
          audio.playAetherHit();
          this.alive = false;
          return;
        }
      }
    }

    // Check hit enemies
    for (let e of enemies) {
      const d = Math.hypot(this.x - e.x, this.y - e.y);
      if (d < this.radius + e.radius) {
        e.takeDamage(2, this.vx > 0 ? 1 : -1, this.vy > 0 ? 1 : -1);
        audio.playAetherHit();
        this.alive = false;
        return;
      }
    }
  }

  draw(ctx) {
    ctx.save();
    ctx.fillStyle = '#a7f3d0';
    ctx.strokeStyle = '#2dd4bf';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
    ctx.restore();
  }
}

class Particle {
  constructor(x, y, vx, vy, color, life, size = 3) {
    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(dt) {
    this.life -= dt;
    this.x += this.vx * dt;
    this.y += this.vy * dt;
  }

  draw(ctx) {
    const alpha = Math.max(0, this.life / this.maxLife);
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.size, this.size);
    ctx.restore();
  }
}

function createSparks(x, y, color, count = 12) {
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = 25 + Math.random() * 95;
    particles.push(new Particle(
      x, y,
      Math.cos(angle) * speed,
      Math.sin(angle) * speed,
      color,
      0.35 + Math.random() * 0.35,
      2 + Math.random() * 2
    ));
  }
}

class FloatingText {
  constructor(x, y, text, color) {
    this.x = x;
    this.y = y;
    this.text = text;
    this.color = color;
    this.life = 1.0;
  }

  update(dt) {
    this.life -= dt;
    this.y -= 25 * dt;
  }

  draw(ctx) {
    ctx.save();
    ctx.globalAlpha = Math.max(0, this.life);
    ctx.fillStyle = this.color;
    ctx.font = 'bold 12px "Cinzel", Georgia, serif';
    ctx.textAlign = 'center';
    ctx.fillText(this.text, this.x, this.y);
    ctx.restore();
  }
}

/* =========================================================================
   ZONE SETUP & POPULATION BUILDER
   ========================================================================= */

function loadZone(zoneIdx, spawnX = 200, spawnY = 300) {
  currentZone = zoneIdx;
  player.x = spawnX;
  player.y = spawnY;
  player.vx = 0;
  player.vy = 0;

  enemies = [];
  projectiles = [];
  particles = [];
  floatingTexts = [];
  mapEntities = [];

  const zoneInfo = ZONES[zoneIdx];
  document.getElementById('hud-zone-name').innerText = zoneInfo.name.toUpperCase();
  document.getElementById('hud-zone-sub').innerText = zoneInfo.sub.toUpperCase();

  // Show splash notification
  triggerZoneSplash(zoneInfo.name, zoneInfo.sub);

  // Outer Room Boundaries for all 960x600 canvas
  const W = 960;
  const H = 600;
  const T = 24; // Wall thickness

  mapEntities.push(new Wall(0, 0, W, T));       // Top
  mapEntities.push(new Wall(0, H - T, W, T));   // Bottom
  mapEntities.push(new Wall(0, 0, T, H));       // Left
  mapEntities.push(new Wall(W - T, 0, T, H));   // Right

  /* -------------------------------------------------------------
     ZONE SPECIFIC MAP POPULATION
     ------------------------------------------------------------- */
  if (zoneIdx === 0) {
    // ZONE 1: The Ashen Threshold (Prologue)
    // Sanctuary Stone 1 right at entrance
    mapEntities.push(new SanctuaryStone(140, 300, 1));

    // Dividing columns / barriers
    mapEntities.push(new Wall(320, 24, 28, 200));
    mapEntities.push(new Wall(320, 360, 28, 216));

    // Breakable cracked wall hiding a small starter cache
    mapEntities.push(new Wall(320, 224, 28, 136, true, 2, "z1_crack"));
    mapEntities.push(new Chest(240, 100, "vitalStem"));

    // Introductory Corrupted Constructs
    enemies.push(new Construct(540, 240, 'sentinel'));
    enemies.push(new Construct(620, 360, 'sentinel'));
    enemies.push(new Construct(740, 280, 'sentinel'));

    // Gateway to Zone 2 at the right
    mapEntities.push(new ZoneExit(W - 40, 260, 30, 80, 1, 60, 300));

  } else if (zoneIdx === 1) {
    // ZONE 2: The Rusted Archives (Early Archives)
    // Sanctuary Stone 2 at center
    mapEntities.push(new SanctuaryStone(480, 300, 2));

    // Merchant Altar unlocked here
    mapEntities.push(new MerchantAltar(560, 160));

    // Library bookshelf walls
    mapEntities.push(new Wall(180, 120, 160, 24));
    mapEntities.push(new Wall(180, 440, 160, 24));
    mapEntities.push(new Wall(640, 120, 160, 24));
    mapEntities.push(new Wall(640, 440, 160, 24));

    // Hidden chest for Warden's Band
    mapEntities.push(new Chest(840, 80, "wardenBand"));

    // Secret breakable wall hiding gold coffer
    mapEntities.push(new Wall(760, 24, 24, 96, true, 3));
    mapEntities.push(new Chest(880, 200, null, true, 80));

    // Archivist & Sentinel constructs
    enemies.push(new Construct(340, 240, 'sentinel'));
    enemies.push(new Construct(340, 380, 'archivist'));
    enemies.push(new Construct(700, 260, 'archivist'));
    enemies.push(new Construct(760, 360, 'rustHulk'));

    // Return to Zone 1
    mapEntities.push(new ZoneExit(10, 260, 25, 80, 0, W - 80, 300));
    // Advance to Zone 3
    mapEntities.push(new ZoneExit(W - 40, 260, 30, 80, 2, 60, 300));

  } else if (zoneIdx === 2) {
    // ZONE 3: The Collapsed Gallery (Mid-Early)
    // Weapon Upgrade 1 Altar: Ignited Edge
    mapEntities.push(new WeaponAltar(480, 160, 1, "Ignited Edge"));

    // Environmental Pressure Plate puzzle: Opens gate to Zone 4
    mapEntities.push(new PressurePlate(460, 440, 40, 40, "gallery_gate"));
    mapEntities.push(new Gate(W - 45, 240, 25, 120, "gallery_gate", "PLATE CONTROLLED GATE"));

    // Crack-marked breakable wall hiding Heart Container 4
    mapEntities.push(new Wall(660, 24, 24, 180, true, 4, "hc4_wall"));
    mapEntities.push(new Chest(780, 90, "heartContainer4"));

    // Mini-boss Hulk guarding the altar
    enemies.push(new Construct(480, 280, 'rustHulk'));
    enemies.push(new Construct(260, 200, 'sentinel'));
    enemies.push(new Construct(260, 380, 'sentinel'));
    enemies.push(new Construct(700, 380, 'archivist'));

    // Spikes along side passage
    mapEntities.push(new SpikeTrap(280, 460, 140, 20));

    // Return to Zone 2
    mapEntities.push(new ZoneExit(10, 260, 25, 80, 1, W - 80, 300));
    // Advance to Zone 4
    mapEntities.push(new ZoneExit(W - 40, 260, 30, 80, 3, 60, 300));

  } else if (zoneIdx === 3) {
    // ZONE 4: The Sunken Vault (Mid-Game / Halfway Turning Point)
    // Discovery: The Aether Shard (Ranged Weapon) behind cracked wall!
    mapEntities.push(new Wall(340, 340, 28, 140, true, 3, "shard_wall"));
    mapEntities.push(new Chest(220, 420, "aetherShard"));

    // Reward for solving light reflection / switch: Phantom Lantern!
    mapEntities.push(new RangedCrystalSwitch(820, 100, "lantern_vault"));
    mapEntities.push(new Gate(700, 380, 24, 100, "lantern_vault"));
    mapEntities.push(new Chest(780, 420, "phantomLantern"));

    // Another switch to open main exit
    mapEntities.push(new RangedCrystalSwitch(820, 480, "vault_exit_gate"));
    mapEntities.push(new Gate(W - 45, 240, 25, 120, "vault_exit_gate"));

    // Water barriers & constructs
    mapEntities.push(new Wall(480, 120, 28, 220));
    enemies.push(new Construct(380, 220, 'rustHulk'));
    enemies.push(new Construct(580, 200, 'archivist'));
    enemies.push(new Construct(580, 380, 'sentinel'));
    enemies.push(new Construct(720, 260, 'archivist'));

    // Return to Zone 3
    mapEntities.push(new ZoneExit(10, 260, 25, 80, 2, W - 80, 300));
    // Advance to Zone 5
    mapEntities.push(new ZoneExit(W - 40, 260, 30, 80, 4, 60, 300));

  } else if (zoneIdx === 4) {
    // ZONE 5: The Twin Spire (Late Gauntlet)
    // Sanctuary Stone 3 at entrance
    mapEntities.push(new SanctuaryStone(140, 300, 3));

    // Weapon Upgrade 2 Altar: Solar Falchion
    mapEntities.push(new WeaponAltar(500, 140, 2, "Solar Falchion"));

    // Ranged switch over a chasm/spike row for Heart Container 5
    mapEntities.push(new SpikeTrap(320, 380, 340, 26));
    mapEntities.push(new RangedCrystalSwitch(840, 440, "hc5_spire_gate"));
    mapEntities.push(new Gate(800, 40, 25, 100, "hc5_spire_gate"));
    mapEntities.push(new Chest(880, 80, "heartContainer5"));

    // Large bonus gold chest & broken jar with Binding Seal
    mapEntities.push(new Chest(300, 100, null, true, 120));
    mapEntities.push(new Chest(680, 480, "bindingSeal"));

    // Tough combat gauntlet: Spire Guardian (mini-boss) + Constructs
    enemies.push(new Construct(520, 300, 'spireGuardian'));
    enemies.push(new Construct(380, 220, 'rustHulk'));
    enemies.push(new Construct(660, 240, 'archivist'));
    enemies.push(new Construct(740, 360, 'sentinel'));

    // Return to Zone 4
    mapEntities.push(new ZoneExit(10, 260, 25, 80, 3, W - 80, 300));
    // Advance to Zone 6 (Climax)
    mapEntities.push(new ZoneExit(W - 40, 260, 30, 80, 5, 60, 300));

  } else if (zoneIdx === 5) {
    // ZONE 6: The Core Chamber (Climax)
    // Final confrontation & The Core Beacon
    const beacon = new BeaconMechanism(480, 300);
    mapEntities.push(beacon);

    // Gate protecting the beacon until side crystal switches are activated
    mapEntities.push(new RangedCrystalSwitch(160, 120, "beacon_gate_left"));
    mapEntities.push(new RangedCrystalSwitch(800, 120, "beacon_gate_right"));

    mapEntities.push(new Gate(430, 200, 100, 20, "beacon_gate_left"));
    mapEntities.push(new Gate(430, 380, 100, 20, "beacon_gate_right"));

    // Elite Guardian constructs guarding the Core
    enemies.push(new Construct(340, 300, 'spireGuardian'));
    enemies.push(new Construct(620, 300, 'spireGuardian'));
    enemies.push(new Construct(480, 140, 'rustHulk'));
    enemies.push(new Construct(480, 460, 'rustHulk'));

    // Return to Zone 5
    mapEntities.push(new ZoneExit(10, 260, 25, 80, 4, W - 80, 300));
  }
}

function isSolidAt(x, y, radius = 10) {
  for (let ent of mapEntities) {
    if (ent instanceof Wall && !ent.broken) {
      if (x + radius > ent.x && x - radius < ent.x + ent.w &&
          y + radius > ent.y && y - radius < ent.y + ent.h) {
        return true;
      }
    }
    if (ent instanceof Gate && !ent.open) {
      if (x + radius > ent.x && x - radius < ent.x + ent.w &&
          y + radius > ent.y && y - radius < ent.y + ent.h) {
        return true;
      }
    }
  }
  return false;
}

/* =========================================================================
   CHECKPOINT & PERSISTENCE
   ========================================================================= */

function saveCheckpoint() {
  lastCheckpointState = {
    zone: currentZone,
    x: player.x,
    y: player.y,
    maxContainers: player.maxContainers,
    gold: player.gold,
    weaponUpgrade: player.weaponUpgrade,
    hasRanged: player.hasRanged,
    inventory: JSON.parse(JSON.stringify(player.inventory)),
    equippedAccessory: player.equippedAccessory,
    activeSavePoint: activeSavePoint
  };
}

function restoreCheckpoint() {
  if (!lastCheckpointState) {
    // Respawn in current zone at entrance with half health
    player.hp = player.maxContainers * 2;
    player.x = 200;
    player.y = 300;
    loadZone(currentZone, 200, 300);
    return;
  }
  player.maxContainers = lastCheckpointState.maxContainers;
  player.hp = player.maxContainers * 2;
  player.gold = lastCheckpointState.gold;
  player.weaponUpgrade = lastCheckpointState.weaponUpgrade;
  player.hasRanged = lastCheckpointState.hasRanged;
  player.inventory = JSON.parse(JSON.stringify(lastCheckpointState.inventory));
  player.equippedAccessory = lastCheckpointState.equippedAccessory;
  activeSavePoint = lastCheckpointState.activeSavePoint;

  loadZone(lastCheckpointState.zone, lastCheckpointState.x, lastCheckpointState.y);
  updateHealthUI();
  updateGoldUI();
  updateWeaponUI();
  showToast("Restored at Sanctuary Stone");
}

/* =========================================================================
   COMBAT & DAMAGE HANDLING
   ========================================================================= */

function damagePlayer(amt) {
  if (player.invulnerableTimer > 0 || isGameWon) return;

  player.hp = Math.max(0, player.hp - amt);
  player.invulnerableTimer = 1.0;
  audio.playDamage();

  // Screen-edge damage red flash
  const flash = document.getElementById('damage-flash');
  flash.style.opacity = '1';
  setTimeout(() => { flash.style.opacity = '0'; }, 150);

  // Red particle burst centered on player
  createSparks(player.x, player.y, '#ef4444', 16);
  floatingTexts.push(new FloatingText(player.x, player.y - 16, `-${amt} HP`, '#ef4444'));

  updateHealthUI();

  if (player.hp <= 0) {
    showToast("The Warden collapsed under the corrosion... Restoring from sanctuary.");
    setTimeout(() => {
      restoreCheckpoint();
    }, 1000);
  }
}

function performMeleeAttack() {
  if (player.attackCooldown > 0) return;

  const speedBonus = (player.equippedAccessory === 'wardenBand') ? 0.06 : 0.0;
  // Upgrade 1 (Ignited Edge) increases attack speed noticeably
  const baseDuration = (player.weaponUpgrade === 1) ? 0.16 : 0.22;
  player.attackDuration = Math.max(0.12, baseDuration - speedBonus);

  player.isAttacking = true;
  player.attackTimer = player.attackDuration;
  player.attackCooldown = player.attackDuration + 0.12;

  audio.playAttack(player.weaponUpgrade);

  // Arc range calculation
  // Upgrade 2 (Solar Falchion): wider arc sweep / visual range extension
  let range = 42;
  let sweepAngle = Math.PI * 0.65;
  let dmg = 1;

  if (player.weaponUpgrade === 1) {
    range = 48;
    dmg = 2; // Fire/ember damage
  } else if (player.weaponUpgrade === 2) {
    range = 62;
    sweepAngle = Math.PI * 0.95;
    dmg = 4; // Solar/light heavy impact
  }

  const baseAngle = Math.atan2(player.facing.y, player.facing.x);

  // Check hits against breakable walls
  for (let ent of mapEntities) {
    if (ent instanceof Wall && ent.isBreakable && !ent.broken) {
      const cx = ent.x + ent.w / 2;
      const cy = ent.y + ent.h / 2;
      const dist = Math.hypot(cx - player.x, cy - player.y);
      if (dist < range + Math.max(ent.w, ent.h) / 2) {
        // Solar Falchion breaks them faster
        const wallDmg = (player.weaponUpgrade === 2) ? 3 : 1;
        ent.damage(wallDmg);
      }
    }
  }

  // Check hits against enemies
  for (let e of enemies) {
    const dx = e.x - player.x;
    const dy = e.y - player.y;
    const dist = Math.hypot(dx, dy);

    if (dist <= range + e.radius) {
      const enemyAngle = Math.atan2(dy, dx);
      let diff = enemyAngle - baseAngle;
      while (diff < -Math.PI) diff += Math.PI * 2;
      while (diff > Math.PI) diff -= Math.PI * 2;

      if (Math.abs(diff) <= sweepAngle / 2) {
        e.takeDamage(dmg, player.facing.x, player.facing.y);
      }
    }
  }
}

function performRangedAttack() {
  if (!player.hasRanged || player.rangedCooldown > 0) return;
  if (player.rangedAmmo <= 0) {
    showToast("Aether Shard Depleted! Restock at the Merchant Altar.");
    return;
  }

  player.rangedAmmo--;
  player.rangedCooldown = 0.35;
  audio.playAetherShoot();

  // Fire crystal projectile in facing direction
  const speed = 9.5;
  const vx = player.facing.x * speed;
  const vy = player.facing.y * speed;
  const px = player.x + player.facing.x * 16;
  const py = player.y + player.facing.y * 16;

  projectiles.push(new AetherProjectile(px, py, vx, vy));
  updateWeaponUI();
}

/* =========================================================================
   CONSUMABLES & INVENTORY LOGIC
   ========================================================================= */

function useVitalStem() {
  if (player.inventory.vitalStem <= 0) return;
  if (player.hp >= player.maxContainers * 2) {
    showToast("Vitality is already at full capacity.");
    return;
  }

  player.inventory.vitalStem--;
  // Restores 2 full containers (4 HP)
  player.hp = Math.min(player.hp + 4, player.maxContainers * 2);
  audio.playPickup();
  triggerUpgradeHalo();
  createSparks(player.x, player.y, '#22c55e', 20);
  updateHealthUI();
  updateInventoryModal();
  showToast("Used Vital Stem: Restored 2 full heart containers (4 HP)!");
}

function useBindingSeal() {
  if (player.inventory.bindingSeal <= 0) return;

  // Freeze closest enemy in place for ~3 seconds
  let closest = null;
  let minDist = 300;
  for (let e of enemies) {
    const d = Math.hypot(e.x - player.x, e.y - player.y);
    if (d < minDist) {
      minDist = d;
      closest = e;
    }
  }

  if (!closest) {
    showToast("No enemies nearby to bind!");
    return;
  }

  player.inventory.bindingSeal--;
  closest.frozenTimer = 3.2;
  audio.playFreeze();
  createSparks(closest.x, closest.y, '#38bdf8', 22);
  floatingTexts.push(new FloatingText(closest.x, closest.y - 18, "FROZEN", "#38bdf8"));
  updateInventoryModal();
  showToast("Binding Seal Released: Construct frozen in black ice!");
}

function equipAccessory(accName) {
  if (player.equippedAccessory === accName) {
    player.equippedAccessory = null; // Toggle off
    showToast(`Unequipped accessory.`);
  } else {
    player.equippedAccessory = accName;
    audio.playRareItem();
    showToast(`Equipped: ${formatItemName(accName)}`);
  }
  updateInventoryModal();
}

function formatItemName(id) {
  switch(id) {
    case 'wardenBand': return "Warden's Band";
    case 'phantomLantern': return "Phantom Lantern";
    case 'shadowCloak': return "Shadow Cloak";
    default: return "None";
  }
}

/* =========================================================================
   SHOP SYSTEM (Merchant Altar in Zone 2)
   ========================================================================= */

const shopStock = {
  vitalStem: { name: "Vital Stem", desc: "Restores 2 full containers (4 HP).", price: 50, remaining: 3 },
  bindingSeal: { name: "Binding Seal", desc: "Freezes one enemy in place for ~3s.", price: 75, remaining: 2 },
  heartContainer5: { name: "Heart Container Upgrade #5", desc: "Permanently grants 5th Heart Container (+2 Max HP).", price: 300, remaining: 1 },
  aetherRestock: { name: "Aether Shard Restock", desc: "Adds +10 projectile charges for the Aether Shard.", price: 10, remaining: 99 }
};

function openShop() {
  isShopOpen = true;
  document.getElementById('shop-modal').classList.add('active');
  renderShop();
}

function closeShop() {
  isShopOpen = false;
  document.getElementById('shop-modal').classList.remove('active');
}

function renderShop() {
  document.getElementById('shop-gold-display').innerText = player.gold;
  const container = document.getElementById('shop-items-container');
  container.innerHTML = '';

  for (let key in shopStock) {
    const item = shopStock[key];
    // Hide ammo if ranged shard not yet unlocked
    if (key === 'aetherRestock' && !player.hasRanged) continue;
    // Hide heart container if already bought or max reached
    if (key === 'heartContainer5' && player.maxContainers >= 5) continue;

    const row = document.createElement('div');
    row.className = 'shop-item-row';
    const canAfford = player.gold >= item.price && item.remaining > 0;

    row.innerHTML = `
      <div class="shop-item-info">
        <h4>${item.name}</h4>
        <p>${item.desc} (Stock: ${item.remaining})</p>
      </div>
      <div class="shop-item-action">
        <div class="price-tag">${item.price} SIGILS</div>
        <button class="btn btn-accent" ${canAfford ? '' : 'disabled'} onclick="buyShopItem('${key}')">PURCHASE</button>
      </div>
    `;
    container.appendChild(row);
  }
}

function buyShopItem(itemKey) {
  const item = shopStock[itemKey];
  if (!item || player.gold < item.price || item.remaining <= 0) return;

  player.gold -= item.price;
  item.remaining--;
  audio.playShopBuy();

  if (itemKey === 'vitalStem') {
    player.inventory.vitalStem++;
    showToast("Purchased Vital Stem!");
  } else if (itemKey === 'bindingSeal') {
    player.inventory.bindingSeal++;
    showToast("Purchased Binding Seal!");
  } else if (itemKey === 'heartContainer5') {
    player.maxContainers = 5;
    player.hp = Math.min(player.hp + 2, player.maxContainers * 2);
    updateHealthUI();
    triggerUpgradeHalo();
    showToast("Purchased Final Heart Container! Maximum 5 containers unlocked.");
  } else if (itemKey === 'aetherRestock') {
    player.rangedAmmo = Math.min(player.maxRangedAmmo, player.rangedAmmo + 10);
    updateWeaponUI();
    showToast("Replenished 10 Aether Shard charges.");
  }

  updateGoldUI();
  renderShop();
}

/* =========================================================================
   UI & OVERLAYS MANAGEMENT
   ========================================================================= */

function updateHealthUI() {
  const container = document.getElementById('hearts-container');
  container.innerHTML = '';

  for (let i = 0; i < player.maxContainers; i++) {
    const heartDiv = document.createElement('div');
    heartDiv.className = 'heart-container';

    // 1 container = 2 HP
    const hpInThisHeart = Math.max(0, Math.min(2, player.hp - i * 2));
    const fillPercent = (hpInThisHeart / 2) * 100;

    heartDiv.innerHTML = `
      <svg viewBox="0 0 32 32">
        <path class="heart-bg" d="M16 29s-12-7.5-12-16a7 7 0 0 1 12-4.5 7 7 0 0 1 12 4.5c0 8.5-12 16-12 16z"/>
        <path class="heart-fill" style="clip-path: inset(${100 - fillPercent}% 0 0 0);" d="M16 29s-12-7.5-12-16a7 7 0 0 1 12-4.5 7 7 0 0 1 12 4.5c0 8.5-12 16-12 16z"/>
      </svg>
    `;
    container.appendChild(heartDiv);
  }
}

function updateGoldUI() {
  document.getElementById('gold-counter').innerText = player.gold;
}

function updateWeaponUI() {
  const meleeName = document.getElementById('hud-melee-name');
  if (player.weaponUpgrade === 0) {
    meleeName.innerText = "EMBER BLADE";
    meleeName.style.color = "#f5c542";
  } else if (player.weaponUpgrade === 1) {
    meleeName.innerText = "IGNITED EDGE";
    meleeName.style.color = "#e66826";
  } else if (player.weaponUpgrade === 2) {
    meleeName.innerText = "SOLAR FALCHION";
    meleeName.style.color = "#fde047";
  }

  const rangedBox = document.getElementById('hud-ranged-box');
  const rangedName = document.getElementById('hud-ranged-name');
  if (player.hasRanged) {
    rangedBox.style.opacity = "1";
    rangedName.innerText = `AETHER SHARD [${player.rangedAmmo}]`;
    rangedName.style.color = "#5eead4";
  } else {
    rangedBox.style.opacity = "0.35";
    rangedName.innerText = "LOCKED";
    rangedName.style.color = "#94a3b8";
  }
}

function toggleInventory() {
  if (isShopOpen) return;
  isInventoryOpen = !isInventoryOpen;
  const modal = document.getElementById('inventory-modal');
  if (isInventoryOpen) {
    modal.classList.add('active');
    updateInventoryModal();
  } else {
    modal.classList.remove('active');
  }
}

function updateInventoryModal() {
  const grid = document.getElementById('inventory-items-grid');
  grid.innerHTML = '';

  document.getElementById('equipped-accessory-label').innerText = formatItemName(player.equippedAccessory);

  // Accessories
  const accList = [
    { id: 'wardenBand', name: "Warden's Band", type: 'Accessory', desc: "Equipped on wrist: increases weapon swing cadence.", owned: player.inventory.wardenBand },
    { id: 'phantomLantern', name: "Phantom Lantern", type: 'Accessory', desc: "Emits faint ethereal light: illuminates hairline cracks & hidden passages.", owned: player.inventory.phantomLantern },
    { id: 'shadowCloak', name: "Shadow Cloak", type: 'Accessory', desc: "Woven from dusk silk: halves damage taken from environmental floor spike traps.", owned: player.inventory.shadowCloak }
  ];

  accList.forEach(acc => {
    if (!acc.owned) return;
    const card = document.createElement('div');
    const isEquipped = player.equippedAccessory === acc.id;
    card.className = `inventory-card ${isEquipped ? 'equipped' : ''}`;
    card.innerHTML = `
      <div class="item-name">${acc.name}</div>
      <div class="item-desc">${acc.desc}</div>
      <div class="item-actions">
        <span style="font-size:10px; color:#64748b;">${acc.type}</span>
        <button class="btn" onclick="equipAccessory('${acc.id}')">${isEquipped ? 'UNEQUIP' : 'EQUIP'}</button>
      </div>
    `;
    grid.appendChild(card);
  });

  // Consumables
  if (player.inventory.vitalStem > 0) {
    const stemCard = document.createElement('div');
    stemCard.className = 'inventory-card';
    stemCard.innerHTML = `
      <div class="item-name">Vital Stem (x${player.inventory.vitalStem})</div>
      <div class="item-desc">Purifying glowing bulb. Restores 2 full heart containers (4 HP).</div>
      <div class="item-actions">
        <span style="font-size:10px; color:#22c55e;">CONSUMABLE</span>
        <button class="btn btn-accent" onclick="useVitalStem()">USE</button>
      </div>
    `;
    grid.appendChild(stemCard);
  }

  if (player.inventory.bindingSeal > 0) {
    const sealCard = document.createElement('div');
    sealCard.className = 'inventory-card';
    sealCard.innerHTML = `
      <div class="item-name">Binding Seal (x${player.inventory.bindingSeal})</div>
      <div class="item-desc">Runic ice seal. Freezes the nearest construct in place for 3 seconds.</div>
      <div class="item-actions">
        <span style="font-size:10px; color:#38bdf8;">CONSUMABLE</span>
        <button class="btn btn-accent" onclick="useBindingSeal()">DISCHARGE</button>
      </div>
    `;
    grid.appendChild(sealCard);
  }

  if (grid.children.length === 0) {
    grid.innerHTML = `<div style="grid-column: 1 / -1; color:#64748b; font-style:italic; padding:20px; text-align:center;">Satchel is empty. Explore chests and shrines to gather artifacts.</div>`;
  }
}

let toastTimer = null;
function showToast(msg) {
  const toast = document.getElementById('toast');
  toast.innerText = msg;
  toast.classList.add('show');
  if (toastTimer) clearTimeout(toastTimer);
  toastTimer = setTimeout(() => {
    toast.classList.remove('show');
  }, 2600);
}

function triggerUpgradeHalo() {
  const halo = document.getElementById('upgrade-halo');
  halo.style.opacity = '1';
  setTimeout(() => {
    halo.style.opacity = '0';
  }, 1800);
}

function triggerZoneSplash(title, subtitle) {
  const splash = document.getElementById('zone-splash');
  document.getElementById('splash-title').innerText = title;
  document.getElementById('splash-subtitle').innerText = subtitle;
  splash.classList.add('active');
  setTimeout(() => {
    splash.classList.remove('active');
  }, 2200);
}

/* =========================================================================
   MAIN GAME LOOP & UPDATE
   ========================================================================= */

let lastTime = 0;
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

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

  update(dt);
  render();

  requestAnimationFrame(gameLoop);
}

function update(dt) {
  if (isShopOpen || isInventoryOpen || isGameWon) return;

  // Player cooldowns
  if (player.attackCooldown > 0) player.attackCooldown -= dt;
  if (player.rangedCooldown > 0) player.rangedCooldown -= dt;
  if (player.invulnerableTimer > 0) player.invulnerableTimer -= dt;

  if (player.isAttacking) {
    player.attackTimer -= dt;
    if (player.attackTimer <= 0) {
      player.isAttacking = false;
    }
  }

  // Player Movement handling
  let moveX = 0;
  let moveY = 0;
  if (keys.up) moveY -= 1;
  if (keys.down) moveY += 1;
  if (keys.left) moveX -= 1;
  if (keys.right) moveX += 1;

  if (moveX !== 0 || moveY !== 0) {
    const len = Math.hypot(moveX, moveY);
    player.vx = (moveX / len) * player.speed;
    player.vy = (moveY / len) * player.speed;
    player.facing = { x: moveX / len, y: moveY / len };
    player.moving = true;
    player.walkCycle += dt * 8;

    // Footstep audio feedback based on zone flooring
    player.footstepTimer += dt;
    if (player.footstepTimer > 0.32) {
      player.footstepTimer = 0;
      audio.playStep(ZONES[currentZone].floorType);
    }
  } else {
    player.vx *= 0.5;
    player.vy *= 0.5;
    player.moving = false;
  }

  // Next position collision check with walls/gates
  const newX = player.x + player.vx;
  const newY = player.y + player.vy;

  if (!isSolidAt(newX, player.y, player.radius)) player.x = newX;
  if (!isSolidAt(player.x, newY, player.radius)) player.y = newY;

  // Ember trail if Upgrade 1 or 2
  if (player.weaponUpgrade >= 1 && player.moving && Math.random() < 0.25) {
    particles.push(new Particle(
      player.x - player.facing.x * 10,
      player.y - player.facing.y * 10,
      (Math.random() - 0.5) * 8,
      (Math.random() - 0.5) * 8,
      player.weaponUpgrade === 1 ? '#ea580c' : '#f5c542',
      0.4,
      2
    ));
  }

  // Update Map Entities (Pressure plates, spikes, merchant, beacon, etc.)
  promptContext = null;
  for (let ent of mapEntities) {
    if (ent.update) ent.update(dt);

    // Check interaction prompt proximity
    if (ent instanceof SanctuaryStone) {
      const d = Math.hypot(player.x - ent.x, player.y - ent.y);
      if (d < 38) {
        promptContext = { text: "SANCTUARY STONE [E]", action: () => ent.activate() };
      }
    } else if (ent instanceof WeaponAltar && !ent.claimed) {
      const d = Math.hypot(player.x - ent.x, player.y - ent.y);
      if (d < 38) {
        promptContext = { text: `CONSECRATE ${ent.title.toUpperCase()} [E]`, action: () => ent.activate() };
      }
    } else if (ent instanceof Chest && !ent.opened) {
      const d = Math.hypot(player.x - (ent.x + ent.w/2), player.y - (ent.y + ent.h/2));
      if (d < 38) {
        promptContext = { text: "OPEN COFFER [E]", action: () => ent.open() };
      }
    } else if (ent instanceof MerchantAltar) {
      const d = Math.hypot(player.x - ent.x, player.y - ent.y);
      if (d < 46) {
        promptContext = { text: "MERCHANT OF THE ARCHIVES [E]", action: () => ent.interact() };
      }
    } else if (ent instanceof BeaconMechanism && !ent.activated) {
      const d = Math.hypot(player.x - ent.x, player.y - ent.y);
      if (d < 50) {
        promptContext = { text: "REIGNITE THE CORE BEACON [E]", action: () => ent.activate() };
      }
    } else if (ent instanceof ZoneExit) {
      // Step into portal
      if (player.x >= ent.x && player.x <= ent.x + ent.w &&
          player.y >= ent.y && player.y <= ent.y + ent.h) {
        loadZone(ent.targetZone, ent.spawnX, ent.spawnY);
        return;
      }
    }
  }

  // Update prompt banner UI
  const banner = document.getElementById('prompt-banner');
  if (promptContext) {
    banner.innerText = promptContext.text;
    banner.classList.add('active');
  } else {
    banner.classList.remove('active');
  }

  // Update Enemies
  for (let e of enemies) {
    e.update(dt);
  }

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

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

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

/* =========================================================================
   RENDERING ENGINE
   ========================================================================= */

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 1. Draw Zone Environment Floor
  drawEnvironmentFloor();

  // 2. Draw Map Entities (plates, walls, altars, chests)
  for (let ent of mapEntities) {
    ent.draw(ctx);
  }

  // 3. Draw Enemies
  for (let e of enemies) {
    e.draw(ctx);
  }

  // 4. Draw Projectiles
  for (let p of projectiles) {
    p.draw(ctx);
  }

  // 5. Draw Player Avatar with Gear Progression
  drawPlayer();

  // 6. Draw Particles & Damage Numbers
  for (let part of particles) {
    part.draw(ctx);
  }
  for (let ft of floatingTexts) {
    ft.draw(ctx);
  }
}

function drawEnvironmentFloor() {
  // Atmospheric floor tiles reflecting zone theme
  const theme = ZONES[currentZone].theme;

  if (theme === 'ashen') {
    ctx.fillStyle = '#0f141d';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    // Subtle ashen grid
    ctx.strokeStyle = '#1a2230';
    ctx.lineWidth = 1;
    for (let x = 0; x < canvas.width; x += 48) {
      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
    }
    for (let y = 0; y < canvas.height; y += 48) {
      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
    }
  } else if (theme === 'bronze') {
    ctx.fillStyle = '#141217';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = '#29201a';
    ctx.lineWidth = 1.5;
    for (let x = 0; x < canvas.width; x += 60) {
      for (let y = 0; y < canvas.height; y += 60) {
        ctx.strokeRect(x, y, 60, 60);
      }
    }
  } else if (theme === 'sunken') {
    // Water reflections
    ctx.fillStyle = '#0a131c';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = 'rgba(45, 212, 191, 0.08)';
    ctx.lineWidth = 2;
    for (let y = 10; y < canvas.height; y += 36) {
      ctx.beginPath();
      ctx.moveTo(0, y + Math.sin(Date.now() / 800 + y) * 4);
      ctx.lineTo(canvas.width, y + Math.sin(Date.now() / 800 + y) * 4);
      ctx.stroke();
    }
  } else {
    // Spire & Core
    ctx.fillStyle = '#0d1017';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = '#202838';
    ctx.lineWidth = 1;
    for (let x = 0; x < canvas.width; x += 50) {
      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
    }
  }
}

function drawPlayer() {
  ctx.save();
  ctx.translate(player.x, player.y);

  // Invulnerability flicker
  if (player.invulnerableTimer > 0 && Math.floor(Date.now() / 60) % 2 === 0) {
    ctx.globalAlpha = 0.4;
  }

  // Shadow
  ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
  ctx.beginPath();
  ctx.ellipse(0, 10, 12, 6, 0, 0, Math.PI * 2);
  ctx.fill();

  const angle = Math.atan2(player.facing.y, player.facing.x);
  const bob = player.moving ? Math.sin(player.walkCycle) * 2 : 0;

  // --- Visual Character Progression ---

  // 1. Cloak & Base Stance
  // If Upgrade 2: wider stance; cloak edges shimmer
  const stanceWidth = (player.weaponUpgrade === 2) ? 14 : 10;
  ctx.fillStyle = '#1e293b'; // Warden cloak body
  ctx.beginPath();
  ctx.ellipse(0, bob, stanceWidth, 12, 0, 0, Math.PI * 2);
  ctx.fill();

  // Cloak hem shimmer for Upgrade 2
  if (player.weaponUpgrade === 2) {
    ctx.strokeStyle = '#f5c542';
    ctx.lineWidth = 1.5;
    ctx.stroke();
  }

  // 2. Equipped Accessories Visual Tokens on Model
  if (player.equippedAccessory === 'wardenBand') {
    // Glowing silver ring on wrist
    ctx.fillStyle = '#e2e8f0';
    ctx.strokeStyle = '#94a3b8';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.arc(player.facing.x * 10, player.facing.y * 10 + bob, 3, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
  } else if (player.equippedAccessory === 'phantomLantern') {
    // Soft blue light halo
    ctx.fillStyle = 'rgba(94, 234, 212, 0.2)';
    ctx.beginPath();
    ctx.arc(0, 0, 32, 0, Math.PI * 2);
    ctx.fill();
  } else if (player.equippedAccessory === 'shadowCloak') {
    // Dark cloth trim
    ctx.strokeStyle = '#020617';
    ctx.lineWidth = 3;
    ctx.strokeRect(-8, bob - 8, 16, 16);
  }

  // 3. Off-hand: Aether Shard crystal if discovered
  if (player.hasRanged) {
    // Balanced dual-ready posture with off-hand pulsing crystal
    const offAngle = angle - Math.PI / 2;
    const crystalX = Math.cos(offAngle) * 11;
    const crystalY = Math.sin(offAngle) * 11 + bob;

    ctx.fillStyle = '#5eead4';
    ctx.beginPath();
    ctx.arc(crystalX, crystalY, 3.5, 0, Math.PI * 2);
    ctx.fill();

    // Crystal pulse
    ctx.strokeStyle = 'rgba(94, 234, 212, 0.6)';
    ctx.stroke();
  }

  // 4. Primary Weapon: The Ember Blade / Upgrades
  // Draw blade in hand
  ctx.save();
  ctx.rotate(angle);

  let bladeColor = '#cbd5e1';
  let bladeGlow = null;
  let bladeLength = 16;
  let bladeWidth = 3;

  if (player.weaponUpgrade === 1) {
    // Ignited Edge: Blade glows orange
    bladeColor = '#ea580c';
    bladeGlow = 'rgba(234, 88, 12, 0.5)';
  } else if (player.weaponUpgrade === 2) {
    // Solar Falchion: Blade emits golden light; larger sword
    bladeColor = '#f5c542';
    bladeGlow = 'rgba(245, 197, 66, 0.6)';
    bladeLength = 22;
    bladeWidth = 4.5;
  }

  if (bladeGlow) {
    ctx.shadowColor = bladeGlow;
    ctx.shadowBlur = 8;
  }

  // Draw Sword
  ctx.fillStyle = bladeColor;
  ctx.fillRect(8, -bladeWidth / 2 + bob, bladeLength, bladeWidth);

  // Sword Crossguard & Hilt
  ctx.fillStyle = '#78350f';
  ctx.fillRect(7, -5 + bob, 2.5, 10);

  ctx.restore();

  // 5. Melee Attack Swing Arc Effect
  if (player.isAttacking) {
    const progress = 1 - (player.attackTimer / player.attackDuration);
    const sweep = (player.weaponUpgrade === 2) ? Math.PI * 0.95 : Math.PI * 0.65;
    const startA = angle - sweep / 2;
    const currentA = startA + sweep * progress;
    const arcRadius = (player.weaponUpgrade === 2) ? 56 : ((player.weaponUpgrade === 1) ? 44 : 36);

    ctx.save();
    ctx.strokeStyle = (player.weaponUpgrade === 2) ? '#fef08a' : ((player.weaponUpgrade === 1) ? '#ea580c' : '#e2e8f0');
    ctx.lineWidth = (player.weaponUpgrade === 2) ? 5 : 3;
    ctx.beginPath();
    ctx.arc(0, bob, arcRadius, startA, currentA);
    ctx.stroke();
    ctx.restore();
  }

  // Head/Helmet
  ctx.fillStyle = '#334155';
  ctx.beginPath();
  ctx.arc(0, -2 + bob, 6, 0, Math.PI * 2);
  ctx.fill();

  // Golden Warden Visor
  ctx.fillStyle = '#f5c542';
  ctx.fillRect(player.facing.x * 3 - 2, player.facing.y * 3 - 3 + bob, 4, 2);

  ctx.restore();
}

/* =========================================================================
   INPUT EVENT LISTENERS
   ========================================================================= */

window.addEventListener('keydown', (e) => {
  // Resume Web Audio context upon first keypress
  audio.init();

  if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.up = true;
  if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.down = true;
  if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = true;
  if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = true;

  // Melee attack
  if (e.code === 'KeyJ' || e.code === 'KeyZ' || e.code === 'Space') {
    if (!isShopOpen && !isInventoryOpen) performMeleeAttack();
  }

  // Ranged attack
  if (e.code === 'KeyK' || e.code === 'KeyX') {
    if (!isShopOpen && !isInventoryOpen) performRangedAttack();
  }

  // Interaction
  if (e.code === 'KeyE') {
    if (promptContext && promptContext.action) {
      promptContext.action();
    }
  }

  // Inventory toggle
  if (e.code === 'KeyI' || e.code === 'Tab') {
    e.preventDefault();
    toggleInventory();
  }

  // Escape key closes modals
  if (e.code === 'Escape') {
    if (isInventoryOpen) toggleInventory();
    if (isShopOpen) closeShop();
  }
});

window.addEventListener('keyup', (e) => {
  if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.up = false;
  if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.down = false;
  if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = false;
  if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = false;
});

// Canvas mouse/pointer click fallback for attacks
canvas.addEventListener('mousedown', (e) => {
  audio.init();
  if (isShopOpen || isInventoryOpen) return;
  if (e.button === 0) performMeleeAttack();
  if (e.button === 2) performRangedAttack();
});
canvas.addEventListener('contextmenu', e => e.preventDefault());

function restartGame() {
  player.maxContainers = 3;
  player.hp = 6;
  player.gold = 0;
  player.weaponUpgrade = 0;
  player.hasRanged = false;
  player.rangedAmmo = 20;
  player.equippedAccessory = null;
  player.inventory = {
    wardenBand: false,
    phantomLantern: false,
    shadowCloak: false,
    vitalStem: 1,
    bindingSeal: 0
  };
  activeSavePoint = 0;
  lastCheckpointState = null;
  isGameWon = false;
  document.getElementById('victory-modal').classList.remove('active');

  loadZone(0, 200, 300);
  updateHealthUI();
  updateGoldUI();
  updateWeaponUI();
  showToast("Cycle Reborn at The Ashen Threshold.");
}

/* =========================================================================
   INITIALIZATION
   ========================================================================= */

window.addEventListener('DOMContentLoaded', () => {
  loadZone(0, 200, 300);
  updateHealthUI();
  updateGoldUI();
  updateWeaponUI();
  requestAnimationFrame(gameLoop);
});
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.8-flash","provider":"openrouter","persona":"full","sessionId":"ac34e708-0fb0-494d-9655-11debcf92961","tokensIn":1779071,"tokensOut":42052,"tokensTotal":1821123,"cost":0.5489651999999999,"turns":30,"toolCalls":29,"failedToolCalls":1,"timestamp":"2026-09-03T13:55:42.810Z"} -->