← RPG results

RPG

muse spark 1.2 · full

Initial view of RPG

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

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 53,218 bytes · SHA-256 17d876bfc63a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AURELIA — Echoes of the Ember</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700;800&family=Cinzel+Decorative:wght@700&family=JetBrains+Mono:wght@600&family=Inter:wght@600;700;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0f1410;--panel:#1a2219;--line:#2b3a28;--accent:#e8b84b;--accent2:#ff6b35;--green:#6bc46b;--heart:#ff4757}
body{background:#0a0e0a;color:#e8ecd8;font-family:'Inter',system-ui,sans-serif;overflow-x:hidden}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:#2b3a28;border-radius:99px}
/* header */
.topbar{height:56px;background:linear-gradient(180deg,#1b281b,#121c12);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 18px;position:sticky;top:0;z-index:20}
.brand{display:flex;align-items:center;gap:12px}
.brand-mark{width:36px;height:36px;border-radius:10px;background:radial-gradient(circle at 30% 30%,#ffe8a0,#e8b84b 55%,#8a5a10);display:grid;place-items:center;font-family:'Cinzel Decorative',serif;font-size:18px;color:#1a1200;border:1px solid #f0d27a;box-shadow:0 4px 12px rgba(232,184,75,.35)}
.brand h1{font-family:'Cinzel',serif;font-weight:800;letter-spacing:.12em;font-size:16px;color:#f5e6b8}
.brand h1 span{color:var(--accent)}
.topnav{display:flex;gap:8px;align-items:center}
.pill{font-size:11px;letter-spacing:.12em;font-weight:800;padding:7px 12px;border-radius:99px;border:1px solid #2b3a28;background:#0f1a0f;color:#9bb89b}
.pill.active{background:var(--accent);color:#1a1200;border-color:#f0d27a}
.iconbtn{width:36px;height:36px;border-radius:10px;border:1px solid #2b3a28;background:#0f1a0f;display:grid;place-items:center;color:#9bb89b;cursor:pointer}
/* layout */
.shell{max-width:1380px;margin:16px auto;padding:0 16px;display:grid;grid-template-columns:280px 1fr 300px;gap:16px}
@media(max-width:1100px){.shell{grid-template-columns:1fr}.topbar{height:auto;padding:10px 12px;flex-wrap:wrap;gap:8px}}
.panel{background:linear-gradient(180deg,#182218,#131d13);border:1px solid var(--line);border-radius:18px;overflow:hidden;box-shadow:0 12px 30px rgba(0,0,0,.35)}
.panel-head{padding:14px 16px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}
.panel-head h3{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.18em;color:#a9c0a9}
.badge{font-size:11px;font-weight:800;padding:5px 9px;border-radius:99px;background:#1f2f1f;border:1px solid #2b3a28;color:#cfe0cf}
.hearts{display:flex;gap:6px;flex-wrap:wrap;margin:12px 0 8px}
.heart{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-size:15px;border:1px solid #3a1e1e;background:#1e1414;position:relative;overflow:hidden}
.heart.full{background:radial-gradient(circle at 30% 30%,#ff7a8a,#ff2e44 60%,#8a0a1a);border-color:#ff4757;color:#fff;box-shadow:0 2px 10px rgba(255,71,87,.4)}
.heart.half{background:linear-gradient(90deg,#ff4757 50%,#1e1414 50%);color:#fff;border-color:#ff4757}
.heart.empty{background:#0f1410;color:#3a2a2a}
.statrow{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px dashed #253425;font-size:13px}
.statrow b{font-family:'JetBrains Mono',monospace}
.coin{width:18px;height:18px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#fff2a0,#e8b84b 60%,#8a5a10);border:1px solid #f0d27a;display:inline-grid;place-items:center;font-size:10px}
.prog{height:8px;background:#0a100a;border:1px solid #2b3a28;border-radius:99px;overflow:hidden}
.prog>i{display:block;height:100%;background:linear-gradient(90deg,#e8b84b,#ff6b35);width:30%;transition:width .4s}
.weapon-card{margin:12px;padding:12px;border-radius:14px;background:linear-gradient(180deg,#1f2d1f,#131f13);border:1px solid #2b3a28;display:flex;gap:12px;align-items:center}
.wicon{width:54px;height:54px;border-radius:14px;display:grid;place-items:center;font-size:24px;border:1px solid #3a3520;background:radial-gradient(circle at 30% 30%,#2a2410,#18140a)}
.wmeta h4{font-family:'Cinzel',serif;font-size:13px;letter-spacing:.06em}
.wmeta p{font-size:11px;color:#9bb89b;line-height:1.3}
.inv-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:12px}
.slot{aspect-ratio:1;border-radius:12px;border:1px solid #2b3a28;background:#0d140d;display:grid;place-items:center;font-size:18px;position:relative;cursor:pointer}
.slot.has{background:linear-gradient(180deg,#1b2a1b,#0f1a0f);border-color:#3a5a35;box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
.slot .count{position:absolute;right:5px;bottom:4px;font-size:10px;font-weight:800;background:#e8b84b;color:#1a1200;padding:1px 5px;border-radius:99px}
.canvas-wrap{position:relative;background:radial-gradient(1200px 600px at 50% 0%,#243324 0%,#0f1a0f 55%,#080c08 100%);border-radius:18px;overflow:hidden;border:1px solid var(--line)}
#game{width:100%;height:520px;display:block;image-rendering:pixelated;background:transparent}
.hud-top{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:space-between;pointer-events:none;gap:8px}
.hud-chip{pointer-events:auto;background:rgba(12,18,12,.9);border:1px solid #2b3a28;border-radius:99px;padding:6px 10px;font-size:11px;font-weight:800;letter-spacing:.08em;display:flex;align-items:center;gap:8px;backdrop-filter:blur(8px)}
.hud-chip b{color:var(--accent)}
.controls{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);display:flex;gap:6px;flex-wrap:wrap;justify-content:center}
.kbd{font-family:'JetBrains Mono',monospace;font-size:11px;padding:6px 8px;border-radius:8px;background:rgba(16,24,16,.95);border:1px solid #2b3a28;color:#cfe0cf}
.kbd em{color:var(--accent);font-style:normal;font-weight:800}
.log{max-height:170px;overflow:auto;padding:10px 14px;font-size:12px;line-height:1.5;color:#b9cca9}
.log p{margin:6px 0;padding:8px 10px;border-radius:10px;background:#0f1a0f;border:1px solid #1e2e1e}
.log p.hi{background:linear-gradient(90deg,rgba(232,184,75,.18),rgba(232,184,75,.04));border-color:#3a3520;color:#ffe8a0}
.btn{appearance:none;border:none;cursor:pointer;font-weight:800;letter-spacing:.08em;border-radius:12px;padding:10px 14px;transition:.15s}
.btn:active{transform:translateY(1px)}
.btn-accent{background:linear-gradient(180deg,#f0d27a,#e8b84b);color:#1a1200;box-shadow:0 8px 18px rgba(232,184,75,.35);border:1px solid #ffe8a0}
.btn-ghost{background:#0f1a0f;color:#cfe0cf;border:1px solid #2b3a28}
.btn-sm{padding:8px 10px;font-size:12px;border-radius:10px}
.shop-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.shop-card{border:1px solid #2b3a28;border-radius:14px;background:linear-gradient(180deg,#162016,#0f1a0f);padding:10px;display:flex;flex-direction:column;gap:8px}
.shop-card h5{font-size:12px;letter-spacing:.06em}
.shop-card p{font-size:11px;color:#9bb89b;line-height:1.35;min-height:28px}
.price{font-family:'JetBrains Mono',monospace;font-weight:800;color:var(--accent)}
.quest{display:flex;gap:10px;padding:10px;border-radius:12px;background:#0f1a0f;border:1px solid #1e2e1e;align-items:center}
.quest.done{border-color:#2f5a2f;background:linear-gradient(90deg,rgba(107,196,107,.15),transparent)}
.dot{width:22px;height:22px;border-radius:50%;border:2px solid #3a5a35;display:grid;place-items:center;font-size:11px;flex:0 0 22px}
.dot.done{background:var(--green);border-color:var(--green);color:#0a120a}
.modal{position:fixed;inset:0;background:rgba(4,8,4,.72);backdrop-filter:blur(8px);display:none;place-items:center;z-index:50;padding:16px}
.modal.open{display:grid}
.modal-card{width:min(720px,100%);background:linear-gradient(180deg,#1a261a,#0e150e);border:1px solid #2b3a28;border-radius:18px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,.6)}
.modal-head{padding:16px 18px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #2b3a28}
.modal-head h3{font-family:'Cinzel',serif;letter-spacing:.08em}
.toast{position:absolute;top:58px;left:50%;transform:translateX(-50%);background:#0f1a0f;border:1px solid #2b3a28;color:#e8ecd8;padding:8px 12px;border-radius:99px;font-size:12px;font-weight:700;opacity:0;pointer-events:none;transition:.25s}
.toast.show{opacity:1;transform:translateX(-50%) translateY(4px)}
</style>
</head>
<body>
<div class="topbar">
  <div class="brand">
    <div class="brand-mark">◈</div>
    <div>
      <h1>AURELIA <span>— ECHOES OF THE EMBER</span></h1>
      <div style="font-size:11px;letter-spacing:.14em;color:#8fb08f;font-weight:800">ACTION-ADVENTURE • ONE-HOUR ODYSSEY</div>
    </div>
  </div>
  <div class="topnav">
    <span class="pill active" id="locPill">VILLAGE GREEN • SAFE</span>
    <button class="iconbtn" id="soundBtn" title="Toggle sound">🔊</button>
    <button class="iconbtn" id="helpBtn" title="Help">?</button>
  </div>
</div>

<div class="shell">
  <!-- LEFT -->
  <div class="panel">
    <div class="panel-head"><h3>PLAYER STATE</h3><span class="badge" id="areaBadge">Lv.1 Explorer</span></div>
    <div style="padding:14px 16px">
      <div style="font-size:11px;letter-spacing:.16em;font-weight:800;color:#8fb08f">HEALTH — 2 HP PER HEART</div>
      <div class="hearts" id="hearts"></div>
      <div class="statrow"><span>💛 Coins</span><b><span id="coinVal">45</span> <span class="coin">◉</span></b></div>
      <div class="statrow"><span>⚔️ Weapon</span><b id="weaponName">Rusty Blade</b></div>
      <div class="statrow"><span>🏹 Ranged</span><b id="rangedName">— Locked —</b></div>
      <div style="margin-top:10px">
        <div style="display:flex;justify-content:space-between;font-size:11px;font-weight:800;letter-spacing:.08em;color:#9bb89b"><span>PROGRESSION</span><span id="progLabel">32%</span></div>
        <div class="prog" style="margin-top:6px"><i id="progBar"></i></div>
        <div style="font-size:11px;color:#7aa07a;margin-top:6px" id="progHint">Reach Forest Shrine to awaken Iron Edge</div>
      </div>

      <div class="weapon-card" id="weaponCard">
        <div class="wicon" id="wicon">🗡️</div>
        <div class="wmeta">
          <h4 id="wtitle">Rusty Blade <span style="color:#e8b84b">Lv.1</span></h4>
          <p id="wdesc">Short reach • 1 dmg • Slow swing. A humble start.</p>
          <div style="font-size:11px;margin-top:4px;color:#cfe0cf"><span id="wstats">DMG 1 • RNG 26 • SPD 1.0×</span></div>
        </div>
      </div>

      <div style="display:flex;gap:8px">
        <button class="btn btn-accent btn-sm" id="healBtn" style="flex:1">Use Potion (H)</button>
        <button class="btn btn-ghost btn-sm" id="saveBtn" style="flex:1">Rest at Shrine</button>
      </div>
      <div style="font-size:11px;color:#7aa07a;margin-top:8px;text-align:center">Shrines save progress & restore health</div>
    </div>
    <div class="panel-head" style="margin-top:2px"><h3>QUESTS</h3><span class="badge" id="questCount">1/4</span></div>
    <div style="padding:12px;display:grid;gap:8px" id="quests"></div>
  </div>

  <!-- CENTER -->
  <div style="display:grid;gap:16px">
    <div class="canvas-wrap">
      <canvas id="game" width="920" height="520"></canvas>
      <div class="hud-top">
        <div class="hud-chip">📍 <span id="hudLoc">Village Green</span> <span style="opacity:.6">•</span> <b id="hudRoom">1-1</b></div>
        <div class="hud-chip">🎯 <span id="hudObjective">Find the Forest Shrine</span></div>
        <div class="hud-chip" id="hudArrows" style="display:none">🏹 <b id="arrowVal">0</b> arrows</div>
      </div>
      <div class="controls">
        <span class="kbd"><em>WASD</em> Move</span>
        <span class="kbd"><em>SPACE</em> Swing</span>
        <span class="kbd"><em>Q</em> Bow</span>
        <span class="kbd"><em>E</em> Interact</span>
        <span class="kbd"><em>H</em> Potion</span>
      </div>
      <div class="toast" id="toast"></div>
    </div>

    <div class="panel">
      <div class="panel-head"><h3>ADVENTURE LOG</h3><button class="btn btn-ghost btn-sm" id="clearLog">Clear</button></div>
      <div class="log" id="log"></div>
    </div>
  </div>

  <!-- RIGHT -->
  <div class="panel">
    <div class="panel-head"><h3>INVENTORY</h3><span class="badge" id="invCount">2 / 8</span></div>
    <div class="inv-grid" id="inventory"></div>
    <div style="padding:0 12px 12px">
      <div style="display:flex;gap:8px;margin-bottom:10px">
        <button class="btn btn-accent btn-sm" id="shopOpen" style="flex:1">🛒 Open Village Shop</button>
        <button class="btn btn-ghost btn-sm" id="dropBtn">Drop</button>
      </div>
      <div style="background:#0a120a;border:1px solid #1e2e1e;border-radius:12px;padding:10px">
        <div style="font-size:11px;letter-spacing:.12em;font-weight:800;color:#8fb08f">EQUIPPED EFFECTS</div>
        <div id="effects" style="margin-top:8px;display:grid;gap:6px;font-size:12px;color:#cfe0cf"></div>
      </div>
    </div>
    <div class="panel-head"><h3>VILLAGE SHOP PREVIEW</h3><span class="badge">E to open nearby</span></div>
    <div style="padding:12px">
      <div class="shop-grid" id="shopPreview"></div>
    </div>
  </div>
</div>

<!-- Shop Modal -->
<div class="modal" id="shopModal">
  <div class="modal-card">
    <div class="modal-head">
      <h3>🌿 Village Outpost — Item Shop</h3>
      <button class="btn btn-ghost btn-sm" id="shopClose">✕ Close</button>
    </div>
    <div style="padding:16px;display:grid;gap:12px">
      <div style="display:flex;justify-content:space-between;align-items:center;background:#0f1a0f;border:1px solid #2b3a28;border-radius:12px;padding:10px 12px">
        <div style="font-weight:800">Your Coins: <span id="shopCoins" style="color:var(--accent)">45</span> <span class="coin">◉</span></div>
        <div style="font-size:12px;color:#9bb89b">Press <b>E</b> near the stall in Village to trade</div>
      </div>
      <div class="shop-grid" id="shopGrid"></div>
      <div style="font-size:11px;color:#7aa07a;text-align:center">All purchases ping with a chime • Selling is disabled — treasures are meant to be kept</div>
    </div>
  </div>
</div>

<!-- Help -->
<div class="modal" id="helpModal">
  <div class="modal-card">
    <div class="modal-head"><h3>How to Play — 60-Minute Loop</h3><button class="btn btn-ghost btn-sm" id="helpClose">✕</button></div>
    <div style="padding:16px;display:grid;gap:12px;font-size:13px;line-height:1.6;color:#cfe0cf">
      <p><b style="color:var(--accent)">Goal:</b> Grow from 3 ♥ (6 HP) to 5 ♥ (10 HP), master two sword evolutions, and discover the bow at mid-game. Defeat the Ember Golem atop the mountain.</p>
      <p><b>Route:</b> Village (shop + shrine) → <b>Whispering Forest</b> (Iron Edge + Heart #4) → <b>Crystal Cave</b> (Bow + Ember Cleaver) → <b>Mountain Peak</b> (Final Heart + Boss).</p>
      <p><b>Combat:</b> Sword swings have distinct range/speed per level. Bow consumes arrows (find quivers). Enemies drop coins. Touching shrine saves & heals.</p>
      <p><b>Items:</b> Collect 5+ trinkets: Potion, Ring, Boots, Charm, Bomb. Equip effects stack visually on your sprite.</p>
      <p><b>Save:</b> Stand on a teal shrine and press <b>E</b> or click “Rest at Shrine”.</p>
    </div>
  </div>
</div>

<script>
/* ========= Audio via WebAudio ========= */
let ac, soundOn=true;
function ensureAC(){ if(!ac) ac=new (window.AudioContext||window.webkitAudioContext)(); if(ac.state==='suspended') ac.resume(); }
function tone(freq, dur, type='sine', gain=0.18, slideTo){
  if(!soundOn) return;
  ensureAC();
  const o=ac.createOscillator(), g=ac.createGain();
  o.type=type; o.frequency.value=freq; o.connect(g); g.connect(ac.destination);
  g.gain.value=gain;
  g.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime+dur);
  if(slideTo){ o.frequency.exponentialRampToValueAtTime(slideTo, ac.currentTime+dur*0.8); }
  o.start(); o.stop(ac.currentTime+dur);
}
const SFX={
  step:()=>tone(140,0.08,'square',0.06,90),
  swing:()=>{ tone(320,0.12,'square',0.14,150); setTimeout(()=>tone(680,0.08,'sine',0.08),60)},
  hit:()=>tone(120,0.18,'square',0.22,45),
  hurt:()=>{ tone(220,0.22,'sawtooth',0.18,160); setTimeout(()=>tone(90,0.28,'square',0.16),90)},
  pickup:()=>{ tone(520,0.12,'sine',0.18,780); setTimeout(()=>tone(780,0.14,'sine',0.14,1040),90)},
  coin:()=>{ tone(880,0.10,'sine',0.14,1100); setTimeout(()=>tone(1100,0.10,'sine',0.12),80)},
  shop:()=>{ tone(600,0.16,'sine',0.16,900); setTimeout(()=>tone(900,0.18,'sine',0.14,1200),120)},
  heal:()=>{ tone(400,0.2,'sine',0.16,600); setTimeout(()=>tone(600,0.2,'sine',0.14,800),140)},
  shrine:()=>{ tone(300,0.4,'sine',0.14,500); setTimeout(()=>tone(500,0.5,'sine',0.12,700),180)},
  bow:()=>{ tone(180,0.08,'square',0.12,90); setTimeout(()=>tone(900,0.06,'sine',0.10),70)},
  unlock:()=>{ tone(500,0.18,'sine',0.16,750); setTimeout(()=>tone(750,0.18,'sine',0.14),160); setTimeout(()=>tone(1000,0.3,'sine',0.12),320)},
  chest:()=>{ tone(350,0.2,'square',0.14,500); setTimeout(()=>tone(700,0.25,'sine',0.14),140)},
};

/* ========= Game Data ========= */
const WEAPONS=[
  {id:0,name:'Rusty Blade',lv:1, dmg:1, range:26, cooldown:420, color:'#9aa0a6', icon:'🗡️', desc:'Short reach • 1 dmg • Slow swing. A humble start.'},
  {id:1,name:'Iron Edge',lv:2, dmg:2, range:38, cooldown:300, color:'#6fb7ff', icon:'⚔️', desc:'Balanced steel • 2 dmg • Faster arc. Forest-forged.'},
  {id:2,name:'Ember Cleaver',lv:3, dmg:3, range:52, cooldown:210, color:'#ff7a2e', icon:'🔥', desc:'Flame-kissed • 3 dmg + burn • Long, swift cleave.'},
];
const SHOP_ITEMS=[
  {id:'potion', name:'Heart Potion', icon:'🧪', price:18, desc:'Restore 4 HP instantly. Stacks to 3.', effect:'Heal 4 HP', consumable:true},
  {id:'ring', name:'Iron Ring', icon:'💍', price:35, desc:'+1 defense. Reduces damage by 1 (min 1).', effect:'-1 dmg taken'},
  {id:'boots', name:'Swift Boots', icon:'🥾', price:40, desc:'+22% move speed & dash dust.', effect:'+Speed'},
  {id:'charm', name:'Ember Charm', icon:'🔮', price:45, desc:'Fire resist + ember trail. Visual aura.', effect:'Fire aura'},
  {id:'bomb', name:'Bomb Bag', icon:'💣', price:30, desc:'Place with B — 3 dmg AoE. 2 bombs.', effect:'Explosive'},
  {id:'quiver', name:'Quiver', icon:'🎯', price:22, desc:'+12 arrows for your bow.', effect:'+12 arrows'},
];
const CHEST_LOOT=[
  {room:'forest', reward:'weapon1', title:'Iron Edge Discovered!', desc:'Weapon Lv.2 unlocked • Range & speed improved'},
  {room:'cave', reward:'weapon2', title:'Ember Cleaver Awakened!', desc:'Weapon Lv.3 • Fire damage + burn'},
  {room:'forest2', reward:'heart4', title:'Heart Container!', desc:'Max health +1 ♥ (now 4 ♥ / 8 HP)'},
  {room:'mountain', reward:'heart5', title:'Final Heart Container!', desc:'Max health 5 ♥ (10 HP) — Peak reached'},
  {room:'cave2', reward:'bow', title:'Hunter\'s Bow Found!', desc:'Mid-game ranged weapon • Press Q • Uses arrows'},
];
const ROOMS={
  village:{id:'village', name:'Village Green', biome:'village', exits:{north:'forest', east:'cave'}, color:'#1e2f1e', enemies:[], coins:6, chests:[], shrine:true, shop:true},
  forest:{id:'forest', name:'Whispering Forest', biome:'forest', exits:{south:'village', north:'forest2'}, color:'#14301a', enemies:[{type:'slime', hp:3},{type:'slime', hp:3},{type:'brute', hp:5}], coins:8, chests:['forest'], shrine:true},
  forest2:{id:'forest2', name:'Forest Heart', biome:'forest', exits:{south:'forest'}, color:'#0f2816', enemies:[{type:'brute', hp:6}], coins:5, chests:['forest2'], shrine:true},
  cave:{id:'cave', name:'Crystal Cave', biome:'cave', exits:{west:'village', north:'cave2'}, color:'#1a2030', enemies:[{type:'bat', hp:2},{type:'bat', hp:2},{type:'slime', hp:4}], coins:9, chests:[], shrine:false, lockedMsg:'A cold draft blocks the depth... prove forest strength first.'},
  cave2:{id:'cave2', name:'Ember Hollow', biome:'cave', exits:{south:'cave', north:'mountain'}, color:'#2a1a14', enemies:[{type:'brute', hp:7},{type:'bat', hp:3}], coins:7, chests:['cave','cave2'], shrine:true},
  mountain:{id:'mountain', name:'Mountain Peak', biome:'mountain', exits:{south:'cave2'}, color:'#2b2e3a', enemies:[{type:'golem', hp:18}], coins:4, chests:['mountain'], shrine:true, boss:true}
};
const ENEMY_CFG={
  slime:{speed:0.9, dmg:1, color:'#5bd46b', size:22},
  bat:{speed:1.5, dmg:1, color:'#8a7cff', size:18},
  brute:{speed:0.8, dmg:2, color:'#c96a3a', size:26},
  golem:{speed:0.55, dmg:2, color:'#ff7a2e', size:38}
};

/* ========= State ========= */
const state={
  hp:6, maxHp:6, maxHearts:3, coins:45, weapon:0, hasBow:false, arrows:0,
  inventory:[{id:'potion', qty:1},{id:'ring', qty:1}], // start with 2 to show, need 5 distinct soon
  room:'village',
  progress:0.18,
  quests:[
    {id:'q1', text:'Brave the Whispering Forest', done:false},
    {id:'q2', text:'Claim Iron Edge & 4th Heart', done:false},
    {id:'q3', text:'Discover the Bow in Crystal Cave', done:false},
    {id:'q4', text:'Reforge Ember Cleaver & defeat the Peak Golem', done:false},
  ],
  unlocked:{weapon1:false, weapon2:false, bow:false, heart4:false, heart5:false},
  visited:new Set(['village']),
  coinsTaken:new Set(),
  chestsOpened:new Set(),
  deadEnemies:new Set(), // per room index tracking via respawn? simpler
};
let player={x:460,y:300,vx:0,vy:0,dir:0, swinging:false, swingT:0, hurtT:0, speed:2.2};
let enemies=[], coins=[], chests=[], particles=[], arrows=[], bombs=[];
let keys={}, lastStep=0, lastSwing=0;
let saveFlash=0;
let discoveredItems = new Set(state.inventory.map(i=>i.id));

/* ========= DOM ========= */
const canvas=document.getElementById('game'), ctx=canvas.getContext('2d');
const heartsEl=document.getElementById('hearts'), coinVal=document.getElementById('coinVal'), weaponNameEl=document.getElementById('weaponName'), rangedNameEl=document.getElementById('rangedName'), progBar=document.getElementById('progBar'), progLabel=document.getElementById('progLabel'), progHint=document.getElementById('progHint'), wicon=document.getElementById('wicon'), wtitle=document.getElementById('wtitle'), wdesc=document.getElementById('wdesc'), wstats=document.getElementById('wstats'), invEl=document.getElementById('inventory'), effectsEl=document.getElementById('effects'), logEl=document.getElementById('log'), locPill=document.getElementById('locPill'), hudLoc=document.getElementById('hudLoc'), hudRoom=document.getElementById('hudRoom'), hudObjective=document.getElementById('hudObjective'), areaBadge=document.getElementById('areaBadge');
function toast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1800)}
function addLog(msg,hi=false){ const p=document.createElement('p'); p.textContent=msg; if(hi) p.classList.add('hi'); logEl.prepend(p); while(logEl.children.length>18) logEl.removeChild(logEl.lastChild) }

/* ========= Inventory / Shop UI ========= */
function renderHearts(){
  heartsEl.innerHTML='';
  for(let i=0;i<5;i++){
    const c=document.createElement('div'); c.className='heart';
    if(i < state.maxHearts){
      const hpForHeart = state.hp - i*2;
      if(hpForHeart>=2) c.classList.add('full');
      else if(hpForHeart===1) c.classList.add('half');
      else c.classList.add('empty');
      c.textContent = hpForHeart>=1 ? '♥' : '♡';
      if(hpForHeart>=2) c.style.boxShadow='0 2px 10px rgba(255,71,87,.4)';
    } else {
      c.classList.add('empty'); c.textContent='♡'; c.style.opacity=.35; c.style.borderStyle='dashed';
    }
    heartsEl.appendChild(c);
  }
}
function renderStats(){
  coinVal.textContent=state.coins;
  document.getElementById('shopCoins').textContent=state.coins;
  document.getElementById('arrowVal').textContent=state.arrows;
  document.getElementById('hudArrows').style.display = state.hasBow ? 'flex':'none';
  rangedNameEl.textContent = state.hasBow ? `Hunter's Bow • ${state.arrows} arrows` : '— Locked —';
  rangedNameEl.style.color = state.hasBow?'#6bc46b':'#7a8f7a';
  const w=WEAPONS[state.weapon];
  weaponNameEl.textContent=w.name;
  wicon.textContent=w.icon; wicon.style.borderColor = w.id===2?'#ff7a2e': w.id===1?'#6fb7ff':'#3a3520';
  wtitle.innerHTML=`${w.name} <span style="color:${w.id===2?'#ff7a2e':w.id===1?'#6fb7ff':'#e8b84b'}">Lv.${w.lv}</span>`;
  wdesc.textContent=w.desc;
  wstats.textContent=`DMG ${w.dmg} • RNG ${w.range} • SPD ${( (420/w.cooldown).toFixed(2))}×`;
  const totalDistinct = new Set(state.inventory.map(i=>i.id)).size;
  document.getElementById('invCount').textContent = `${state.inventory.length} / 8 • ${totalDistinct} types`;
  document.getElementById('questCount').textContent = `${state.quests.filter(q=>q.done).length}/4`;
  // progress 0-100 based on unlocks
  let prog=0;
  if(state.unlocked.weapon1) prog+=30;
  if(state.unlocked.heart4) prog+=15;
  if(state.unlocked.bow) prog+=20;
  if(state.unlocked.weapon2) prog+=20;
  if(state.unlocked.heart5) prog+=15;
  state.progress= Math.min(100, 18+prog);
  progBar.style.width=state.progress+'%'; progLabel.textContent=Math.round(state.progress)+'%';
  let hint='Explore • Fight • Get stronger';
  if(!state.unlocked.weapon1) hint='North to Whispering Forest — Iron Edge awaits';
  else if(!state.unlocked.bow) hint='Mid-game: seek the Bow in Crystal Cave (East)';
  else if(!state.unlocked.weapon2) hint='Deep in Ember Hollow — reforge the Cleaver';
  else hint='Climb to Mountain Peak for the final heart & boss';
  progHint.textContent=hint;
  hudObjective.textContent=hint;
  // location pill
  const r=ROOMS[state.room];
  locPill.textContent = `${r.name.toUpperCase()} • ${r.biome.toUpperCase()}`;
  hudLoc.textContent=r.name; hudRoom.textContent = ({village:'1-1',forest:'1-2',forest2:'1-3',cave:'2-1',cave2:'2-2',mountain:'3-1'}[state.room]);
  areaBadge.textContent = `Lv.${state.weapon+1} • ${state.maxHearts}♥`;
  renderEffects();
}
function renderEffects(){
  const has = id=> state.inventory.some(i=>i.id===id);
  const lines=[];
  if(has('ring')) lines.push('💍 Iron Ring: -1 damage taken');
  if(has('boots')) lines.push('🥾 Swift Boots: +22% speed & dust trail');
  if(has('charm')) lines.push('🔮 Ember Charm: fire aura & burn bonus');
  if(has('bomb')) lines.push('💣 Bomb Bag: press B to place');
  if(state.hasBow) lines.push('🏹 Bow: Q to shoot (arrows '+state.arrows+')');
  if(lines.length===0) lines.push('<span style="color:#7aa07a">No active charms — visit the shop.</span>');
  effectsEl.innerHTML = lines.map(l=>`<div style="display:flex;gap:8px;align-items:center"><span style="width:6px;height:6px;border-radius:50%;background:var(--accent);display:inline-block"></span>${l}</div>`).join('');
}
function renderInventory(){
  invEl.innerHTML='';
  for(let i=0;i<8;i++){
    const slot=document.createElement('div'); slot.className='slot'+(state.inventory[i]?' has':'');
    if(state.inventory[i]){
      const it=SHOP_ITEMS.find(s=>s.id===state.inventory[i].id);
      slot.innerHTML=`<span>${it?it.icon:'📦'}</span><span class="count">×${state.inventory[i].qty}</span>`;
      slot.title=`${it?it.name:state.inventory[i].id} ×${state.inventory[i].qty}`;
    } else slot.textContent='·';
    slot.onclick=()=>{
      if(state.inventory[i]){
        const it=SHOP_ITEMS.find(s=>s.id===state.inventory[i].id);
        addLog(`Selected ${it?it.name:state.inventory[i].id} — ${it?it.effect:''}`,true);
        SFX.pickup();
      }
    };
    invEl.appendChild(slot);
  }
}
function renderQuests(){
  const qEl=document.getElementById('quests');
  qEl.innerHTML = state.quests.map(q=>`
    <div class="quest ${q.done?'done':''}">
      <div class="dot ${q.done?'done':''}">${q.done?'✓':''}</div>
      <div style="flex:1">
        <div style="font-size:13px;font-weight:700">${q.text}</div>
        <div style="font-size:11px;color:#8fb08f">${q.done?'Completed':'In progress'}</div>
      </div>
    </div>`).join('');
}
function renderShop(){
  const grid=document.getElementById('shopGrid'), preview=document.getElementById('shopPreview');
  const mkCard = it=>`
    <div class="shop-card">
      <div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:20px">${it.icon}</span><span class="price">${it.price} ◉</span></div>
      <h5>${it.name}</h5><p>${it.desc}</p>
      <button class="btn ${state.coins>=it.price?'btn-accent':'btn-ghost'} btn-sm" data-buy="${it.id}" ${state.coins>=it.price?'':'disabled'}>${state.coins>=it.price?'Buy':'Need coins'}</button>
    </div>`;
  grid.innerHTML = SHOP_ITEMS.map(mkCard).join('');
  preview.innerHTML = SHOP_ITEMS.slice(0,4).map(it=>`
    <div style="display:flex;gap:8px;align-items:center;background:#0f1a0f;border:1px solid #1e2e1e;border-radius:10px;padding:8px">
      <span style="font-size:18px">${it.icon}</span><div style="flex:1"><div style="font-size:12px;font-weight:800">${it.name}</div><div style="font-size:11px;color:#8fb08f">${it.price} ◉</div></div>
    </div>`).join('');
  grid.querySelectorAll('[data-buy]').forEach(b=>{
    b.onclick=()=>{
      const id=b.dataset.buy; const it=SHOP_ITEMS.find(s=>s.id===id);
      if(state.coins < it.price){ toast('Not enough coins'); SFX.hit(); return; }
      state.coins-=it.price;
      addInventory(id,1);
      SFX.shop(); burst(player.x, player.y, '#e8b84b',8);
      toast(`Purchased ${it.name}!`); renderAll();
      saveProgress();
    }
  })
}
function addInventory(id, qty=1){
  const ex=state.inventory.find(i=>i.id===id);
  if(ex) ex.qty+=qty; else {
    if(state.inventory.length>=8){ toast('Inventory full!'); return; }
    state.inventory.push({id, qty});
  }
  discoveredItems.add(id);
  if(id==='quiver'){ state.arrows+=12; if(!state.hasBow) { /* quiver still gives arrows but bow needed */ } }
  // passive: if bomb bag first time, keep
  renderAll();
}
function renderAll(){ renderHearts(); renderStats(); renderInventory(); renderQuests(); renderShop(); }

/* ========= Room Loading ========= */
function loadRoom(id){
  const r=ROOMS[id];
  state.room=id; state.visited.add(id);
  // build enemies copies
  enemies = r.enemies.map((e,idx)=>({
    ...e, ...ENEMY_CFG[e.type],
    x: 180 + Math.random()*520, y: 120+ Math.random()*260,
    maxHp:e.hp, hp:e.hp, idx, hurtT:0, dead:false
  }));
  // coins scattered
  coins = Array.from({length:r.coins},(_,i)=>({
    x: 90+ (i*73)%740, y: 90+ ((i*47)%360),
    taken: state.coinsTaken.has(id+'_c'+i), id:id+'_c'+i, bob: Math.random()*6
  }));
  // chests
  chests = (r.chests||[]).map(cid=>{
    const loot = CHEST_LOOT.find(l=>l.room===cid);
    // chest position
    const pos = cid==='forest'?{x:460,y:140}: cid==='forest2'?{x:460,y:120}: cid==='cave'?{x:650,y:160}: cid==='cave2'?{x:460,y:140}: {x:460,y:130};
    return {cid, x:pos.x, y:pos.y, opened: state.chestsOpened.has(cid), loot}
  });
  // reset player pos near entry
  if(id==='village'){ player.x=460; player.y=380; }
  else if(id==='forest'){ player.x=460; player.y=440; }
  else if(id==='forest2'){ player.x=460; player.y=440; }
  else if(id==='cave'){ player.x=120; player.y=260; }
  else if(id==='cave2'){ player.x=460; player.y=440; }
  else if(id==='mountain'){ player.x=460; player.y=420; }
  addLog(`Entered ${r.name} — ${r.biome}`, true);
  // lock check for cave
  if(id==='cave' && !state.unlocked.weapon1){
    // allow entry but depth blocked: show message
  }
  renderAll();
}

/* ========= Input ========= */
window.addEventListener('keydown',e=>{
  keys[e.key.toLowerCase()]=true;
  if(e.code==='Space'){ e.preventDefault(); trySwing(); }
  if(e.key.toLowerCase()==='q'){ tryShoot(); }
  if(e.key.toLowerCase()==='e'){ interact(); }
  if(e.key.toLowerCase()==='h'){ usePotion(); }
  if(e.key.toLowerCase()==='b'){ placeBomb(); }
});
window.addEventListener('keyup',e=> keys[e.key.toLowerCase()]=false);

/* ========= Actions ========= */
function trySwing(){
  const now=performance.now();
  const w=WEAPONS[state.weapon];
  if(now - lastSwing < w.cooldown) return;
  lastSwing=now; player.swinging=true; player.swingT=0;
  SFX.swing();
  // hit detection
  const range=w.range;
  enemies.forEach(en=>{
    if(en.dead) return;
    const dx=en.x-player.x, dy=en.y-player.y, d=Math.hypot(dx,dy);
    const angleOk = Math.atan2(dy,dx);
    // simple distance + front cone (allow 140deg)
    const facing = player.dir; // 0 right, 1 down,2 left,3 up approximated via movement
    // compute facing vector
    let fx=0,fy=0;
    if(facing===0) fx=1; if(facing===2) fx=-1; if(facing===1) fy=1; if(facing===3) fy=-1;
    if(fx===0&&fy===0) fx=1;
    const dot = (dx*fx+dy*fy)/(d||1);
    if(d < range+ en.size && dot > -0.2){
      const dmg = w.dmg + (state.inventory.some(i=>i.id==='charm') && w.id===2 ? 1:0);
      en.hp -= dmg; en.hurtT=12;
      SFX.hit(); burst(en.x,en.y, w.id===2?'#ff6b35':'#ffffff', 10);
      if(en.hp<=0){
        en.dead=true; burst(en.x,en.y,'#e8b84b',14);
        const gain = en.type==='golem'? 25 : en.type==='brute'? 8 : 5;
        state.coins+=gain; SFX.coin(); burst(en.x,en.y-20,'#ffe8a0',6);
        addLog(`Defeated ${en.type}! +${gain} coins`,true);
        // check boss
        if(en.type==='golem'){ state.quests[3].done=true; if(!state.unlocked.heart5) {/* heart in chest still */} toast('Golem felled! Claim the peak chest!'); }
        // if all enemies dead in forest, hint
        if(state.room==='forest' && enemies.every(x=>x.dead)) toast('The shrine chest is now reachable!');
      }
    }
  });
  // hit chest? (close chest automatically via interact, but also allow swing to open? no)
}
function tryShoot(){
  if(!state.hasBow){ toast('Bow not discovered yet — seek Crystal Cave (mid-game)'); SFX.hit(); return; }
  if(state.arrows<=0){ toast('Out of arrows! Buy a Quiver'); SFX.hit(); return; }
  state.arrows--; SFX.bow();
  const ang = [0, Math.PI/2, Math.PI, -Math.PI/2][player.dir] || 0;
  // if no dir, shoot right
  const vx=Math.cos(ang)*7, vy=Math.sin(ang)*7;
  arrows.push({x:player.x, y:player.y-6, vx, vy, life:60});
  renderStats();
}
function interact(){
  const r=ROOMS[state.room];
  // shrine
  const shrine={x:460,y:260};
  if(Math.hypot(player.x-shrine.x, player.y-shrine.y)<70 && r.shrine){
    healFull(); saveProgress(true); SFX.shrine(); burst(shrine.x, shrine.y, '#6bc4ff', 18); toast('Progress saved at shrine — health restored'); saveFlash=30; return;
  }
  // shop
  if(r.shop){
    const shopPos={x:720,y:260};
    if(Math.hypot(player.x-shopPos.x, player.y-shopPos.y)<80){
      document.getElementById('shopModal').classList.add('open'); SFX.shop(); return;
    }
  }
  // chests
  for(const ch of chests){
    if(ch.opened) continue;
    if(Math.hypot(player.x-ch.x, player.y-ch.y)<52){
      openChest(ch); return;
    }
  }
  // exits (doors)
  const exits=r.exits;
  // check proximity to edges for travel
  if(player.y < 60 && exits.north){
    if(exits.north==='cave2' && !state.unlocked.weapon1){ toast('Deep cave sealed by ember — prove yourself in the Forest first'); SFX.hit(); return; }
    if(exits.north==='mountain' && !state.unlocked.bow){ toast('Mountain path needs the Bow — mid-game discovery in Ember Hollow'); SFX.hit(); return; }
    loadRoom(exits.north);
  } else if(player.y > 460 && exits.south){ loadRoom(exits.south); }
  else if(player.x < 80 && exits.west){ loadRoom(exits.west); }
  else if(player.x > 840 && exits.east){
    if(exits.east==='cave' && !state.unlocked.weapon1 && state.visited.has('forest')===false){
      // allow first entry but warn? let entry but cave depth locked
    }
    loadRoom(exits.east);
  } else {
    // hint
    if(r.shop) toast('Visit the stall on the right, or shrine in center (E)');
    else toast('Explore edges to travel — N/S/E/W');
  }
}
function openChest(ch){
  ch.opened=true; state.chestsOpened.add(ch.cid); SFX.chest(); burst(ch.x,ch.y,'#e8b84b',16);
  const loot=ch.loot;
  if(!loot){ toast('Empty chest'); return;}
  if(loot.reward==='weapon1'){ state.weapon=1; state.unlocked.weapon1=true; state.quests[0].done=true; state.quests[1].done=false; // partially
    toast(loot.title); addLog(loot.title+' — '+loot.desc,true); SFX.unlock();
  } else if(loot.reward==='heart4'){ state.maxHearts=4; state.maxHp=8; state.hp=8; state.unlocked.heart4=true; state.quests[1].done=true; toast(loot.title); addLog(loot.title,true); SFX.unlock();
  } else if(loot.reward==='bow'){ state.hasBow=true; state.arrows+=12; state.unlocked.bow=true; state.quests[2].done=true; toast(loot.title); addLog(loot.title+' — Use Q to shoot',true); SFX.unlock();
  } else if(loot.reward==='weapon2'){ state.weapon=2; state.unlocked.weapon2=true; toast(loot.title); addLog(loot.title,true); SFX.unlock();
  } else if(loot.reward==='heart5'){ state.maxHearts=5; state.maxHp=10; state.hp=10; state.unlocked.heart5=true; state.quests[3].done=true; toast('Peak conquered! 5 ♥ achieved'); SFX.unlock();
  }
  // also give coins
  state.coins+=12; SFX.coin();
  renderAll(); saveProgress();
}
function usePotion(){
  const idx=state.inventory.findIndex(i=>i.id==='potion');
  if(idx===-1){ toast('No potion — buy at shop'); SFX.hit(); return; }
  if(state.hp>=state.maxHp){ toast('Health already full'); return; }
  state.hp=Math.min(state.maxHp, state.hp+4);
  state.inventory[idx].qty--; if(state.inventory[idx].qty<=0) state.inventory.splice(idx,1);
  SFX.heal(); burst(player.x,player.y,'#6bc46b',12); toast('Drank potion +4 HP'); renderAll();
}
function healFull(){ state.hp=state.maxHp; renderAll(); }
function placeBomb(){
  if(!state.inventory.some(i=>i.id==='bomb')){ toast('Need Bomb Bag from shop (B)'); return; }
  // consume? let bombs infinite but limited qty
  const b = state.inventory.find(i=>i.id==='bomb');
  if(b.qty<=0){ toast('Out of bombs'); return; }
  b.qty--; if(b.qty<=0 && state.inventory.find(i=>i.id==='bomb')){ /* keep slot removal? */ const idx=state.inventory.findIndex(i=>i.id==='bomb'); state.inventory.splice(idx,1); }
  bombs.push({x:player.x, y:player.y, t:60}); SFX.hit(); toast('Bomb placed!'); renderAll();
}

/* ========= Particles ========= */
function burst(x,y,color,n=8){
  for(let i=0;i<n;i++) particles.push({x,y,vx:(Math.random()-0.5)*6, vy:(Math.random()-0.5)*6 -1, life: 18+Math.random()*12, color, size: 2+Math.random()*3});
}

/* ========= Save ========= */
function saveProgress(isManual=false){
  const data={state:{...state, visited:[...state.visited], coinsTaken:[...state.coinsTaken], chestsOpened:[...state.chestsOpened]}, player:{x:player.x,y:player.y}};
  localStorage.setItem('aurelia_save', JSON.stringify(data));
  if(isManual) addLog('Game saved at shrine.',true);
}
function loadSave(){
  const raw=localStorage.getItem('aurelia_save');
  if(!raw) return false;
  try{
    const d=JSON.parse(raw);
    Object.assign(state, d.state);
    state.visited=new Set(d.state.visited||['village']);
    state.coinsTaken=new Set(d.state.coinsTaken||[]);
    state.chestsOpened=new Set(d.state.chestsOpened||[]);
    if(d.player){ player.x=d.player.x; player.y=d.player.y; }
    // restore maxHp mapping
    state.maxHp = state.maxHearts*2;
    // ensure weapon etc
    loadRoom(state.room);
    addLog('Save restored — welcome back!',true);
    return true;
  }catch{ return false;}
}

/* ========= Loop ========= */
function update(){
  // movement
  let mx=0,my=0;
  if(keys['w']||keys['arrowup']) my=-1, player.dir=3;
  if(keys['s']||keys['arrowdown']) my=1, player.dir=1;
  if(keys['a']||keys['arrowleft']) mx=-1, player.dir=2;
  if(keys['d']||keys['arrowright']) mx=1, player.dir=0;
  if(mx&&my){ mx*=0.707; my*=0.707; }
  const spd = player.speed * (state.inventory.some(i=>i.id==='boots')?1.22:1);
  player.vx=mx*spd; player.vy=my*spd;
  // stamina visual tilt not needed, just move
  let nx=player.x+player.vx*4.2, ny=player.y+player.vy*4.2;
  // clamp room bounds
  nx=Math.max(38, Math.min(882, nx)); ny=Math.max(58, Math.min(472, ny));
  // simple obstacle: central shrine block? allow pass but slow near shrine? ignore
  player.x=nx; player.y=ny;
  if((mx||my) && performance.now()-lastStep>140){ SFX.step(); lastStep=performance.now(); if(state.inventory.some(i=>i.id==='boots') && Math.random()<0.5) burst(player.x, player.y+12, '#9bb89b',2); if(state.inventory.some(i=>i.id==='charm')) burst(player.x, player.y+6, '#ff7a2e',1)}
  // swing timer
  if(player.swinging){ player.swingT+=1; if(player.swingT>14) player.swinging=false; }
  if(player.hurtT>0) player.hurtT--;
  // enemies AI
  enemies.forEach(en=>{
    if(en.dead) return;
    const dx=player.x-en.x, dy=player.y-en.y, d=Math.hypot(dx,dy);
    if(d< 280){
      en.x += (dx/d)*en.speed* (en.type==='golem'?0.9:1);
      en.y += (dy/d)*en.speed;
    } else {
      en.x += (Math.random()-0.5)*0.6; en.y += (Math.random()-0.5)*0.6;
    }
    if(en.hurtT>0) en.hurtT--;
    // collision damage
    if(d < en.size + 14 && player.hurtT<=0){
      let dmg=en.dmg;
      if(state.inventory.some(i=>i.id==='ring')) dmg=Math.max(1, dmg-1);
      if(state.inventory.some(i=>i.id==='charm') && en.type==='golem') dmg=Math.max(1,dmg-1);
      state.hp -= dmg; player.hurtT=50;
      SFX.hurt(); burst(player.x,player.y,'#ff4757',10);
      if(state.hp<=0){ state.hp=0; addLog('You fell... respawning at last shrine',true); SFX.hurt();
        // respawn at village shrine
        state.hp=state.maxHp; loadRoom('village');
      }
      renderHearts();
    }
  });
  // arrows
  arrows.forEach(a=>{ a.x+=a.vx; a.y+=a.vy; a.life--; 
    enemies.forEach(en=>{
      if(en.dead) return;
      if(Math.hypot(a.x-en.x, a.y-en.y)< en.size+6){
        en.hp -= 2; en.hurtT=10; a.life=0; burst(en.x,en.y,'#6bc4ff',10); SFX.hit();
        if(en.hp<=0){ en.dead=true; burst(en.x,en.y,'#e8b84b',14); state.coins+=6; SFX.coin(); }
      }
    });
  });
  arrows=arrows.filter(a=>a.life>0 && a.x>10 && a.x<910 && a.y>10 && a.y<510);
  // bombs
  bombs.forEach(b=>{ b.t--; if(b.t===0){ burst(b.x,b.y,'#ff6b35',18); burst(b.x,b.y,'#1a1a1a',12); SFX.hit();
      enemies.forEach(en=>{ if(!en.dead && Math.hypot(en.x-b.x,en.y-b.y)<70){ en.hp-=3; en.hurtT=12; if(en.hp<=0){en.dead=true; state.coins+=6; SFX.coin();} } });
      if(Math.hypot(player.x-b.x, player.y-b.y)<60 && player.hurtT<=0){ state.hp=Math.max(1,state.hp-1); player.hurtT=40; SFX.hurt(); }
  }});
  bombs=bombs.filter(b=>b.t>0);
  // coins pickup
  coins.forEach(c=>{
    if(c.taken) return;
    c.bob+=0.08;
    if(Math.hypot(player.x-c.x, player.y-c.y)<20){
      c.taken=true; state.coinsTaken.add(c.id); state.coins+=3; SFX.coin(); burst(c.x,c.y,'#ffe8a0',7); renderStats();
    }
  });
  // particles
  particles.forEach(p=>{ p.x+=p.vx; p.y+=p.vy; p.vy+=0.18; p.life--;});
  particles=particles.filter(p=>p.life>0);
  if(saveFlash>0) saveFlash--;
}
function draw(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  const r=ROOMS[state.room];
  // background biome
  ctx.fillStyle=r.color; ctx.fillRect(0,0,canvas.width,canvas.height);
  // grid pattern
  ctx.strokeStyle='rgba(255,255,255,0.04)'; 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(); }
  // vignette
  const g=ctx.createRadialGradient(460,220,80,460,260,520); g.addColorStop(0,'rgba(255,255,255,0.06)'); g.addColorStop(1,'rgba(0,0,0,0.55)'); ctx.fillStyle=g; ctx.fillRect(0,0,canvas.width,canvas.height);
  // walls / bounds
  ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.fillRect(0,0,canvas.width,18); ctx.fillRect(0,canvas.height-18,canvas.width,18); ctx.fillRect(0,0,18,canvas.height); ctx.fillRect(canvas.width-18,0,18,canvas.height);
  // doors indicators
  ctx.fillStyle='#e8b84b'; ctx.font='bold 11px JetBrains Mono'; ctx.textAlign='center';
  if(r.exits.north){ ctx.fillText('▲ '+ROOMS[r.exits.north].name.toUpperCase(),460,32); }
  if(r.exits.south){ ctx.fillText('▼ '+ROOMS[r.exits.south].name.toUpperCase(),460,canvas.height-6); }
  if(r.exits.east){ ctx.save(); ctx.translate(canvas.width-8,260); ctx.rotate(Math.PI/2); ctx.fillText('▶ '+ROOMS[r.exits.east].name.toUpperCase(),0,0); ctx.restore(); }
  if(r.exits.west){ ctx.save(); ctx.translate(8,260); ctx.rotate(-Math.PI/2); ctx.fillText('◀ '+ROOMS[r.exits.west].name.toUpperCase(),0,0); ctx.restore(); }
  // shrine
  if(r.shrine){
    const sx=460,sy=260;
    ctx.save();
    if(saveFlash>0){ ctx.shadowBlur=20; ctx.shadowColor='#6bc4ff'; }
    ctx.fillStyle='#0f2a2a'; ctx.beginPath(); ctx.ellipse(sx,sy+22,46,14,0,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#1e4a4a'; ctx.fillRect(sx-36,sy-8,72,28);
    ctx.fillStyle='#6bc4ff'; ctx.beginPath(); ctx.arc(sx,sy-14,16,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#e8ffff'; ctx.beginPath(); ctx.arc(sx-5,sy-18,4,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#cfe0ff'; ctx.font='800 10px JetBrains Mono'; ctx.textAlign='center'; ctx.fillText('SHRINE • E',sx,sy+48);
    ctx.restore();
  }
  // shop stall in village
  if(r.shop){
    const sx=720,sy=260;
    ctx.fillStyle='#2a1f0f'; ctx.fillRect(sx-48,sy-22,96,56);
    ctx.fillStyle='#e8b84b'; ctx.fillRect(sx-52,sy-28,104,12);
    ctx.fillStyle='#fff'; ctx.font='800 10px Inter'; ctx.textAlign='center'; ctx.fillText('SHOP • E',sx,sy+48);
    ctx.font='16px serif'; ctx.fillText('🛒',sx,sy+6);
  }
  // chests
  chests.forEach(ch=>{
    ctx.save();
    ctx.translate(ch.x,ch.y);
    if(ch.opened){ ctx.fillStyle='#3a2f18'; ctx.fillRect(-18,-12,36,22); ctx.fillStyle='#1a1408'; ctx.fillRect(-14,-8,28,6); }
    else { ctx.fillStyle='#8a5a10'; ctx.fillRect(-20,-14,40,26); ctx.fillStyle='#e8b84b'; ctx.fillRect(-20,-14,40,6); ctx.fillStyle='#fff'; ctx.font='14px serif'; ctx.textAlign='center'; ctx.fillText('▓',0,6); ctx.fillStyle='#ffe8a0'; ctx.font='700 9px JetBrains Mono'; ctx.fillText('E OPEN',0,28); }
    ctx.restore();
  });
  // coins
  coins.forEach(c=>{
    if(c.taken) return;
    const bob=Math.sin(c.bob)*4;
    ctx.save(); ctx.translate(c.x,c.y+bob);
    ctx.fillStyle='#1a1400'; ctx.beginPath(); ctx.ellipse(0,6,10,4,0,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#e8b84b'; ctx.beginPath(); ctx.arc(0,0,9,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle='#fff2a0'; ctx.lineWidth=1.2; ctx.beginPath(); ctx.arc(-2,-2,3,0,Math.PI*2); ctx.stroke();
    ctx.fillStyle='#5a3e00'; ctx.font='800 9px JetBrains Mono'; ctx.textAlign='center'; ctx.fillText('◉',0,3);
    ctx.restore();
  });
  // bombs
  bombs.forEach(b=>{
    ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(b.x,b.y,10,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#ff4757'; ctx.beginPath(); ctx.arc(b.x,b.y-8,3,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#fff'; ctx.font='700 8px JetBrains Mono'; ctx.textAlign='center'; ctx.fillText(Math.ceil(b.t/20),b.x,b.y+4);
  });
  // arrows
  arrows.forEach(a=>{
    ctx.save(); ctx.translate(a.x,a.y); ctx.rotate(Math.atan2(a.vy,a.vx));
    ctx.fillStyle='#e8ecd8'; ctx.fillRect(-10,-2,18,4); ctx.fillStyle='#6bc4ff'; ctx.beginPath(); ctx.moveTo(8,-4); ctx.lineTo(12,0); ctx.lineTo(8,4); ctx.fill();
    ctx.restore();
  });
  // enemies
  enemies.forEach(en=>{
    if(en.dead) return;
    ctx.save(); ctx.translate(en.x,en.y);
    if(en.hurtT>0) ctx.globalAlpha=0.6;
    // shadow
    ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.ellipse(0,en.size*0.6,en.size*0.7, en.size*0.35,0,0,Math.PI*2); ctx.fill();
    // body
    ctx.fillStyle=en.color; ctx.beginPath(); ctx.arc(0,0,en.size,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.arc(4,4,en.size*0.55,0,Math.PI*2); ctx.fill();
    // eyes
    ctx.fillStyle='#0a0a0a'; ctx.beginPath(); ctx.arc(-6,-4,4,0,Math.PI*2); ctx.arc(6,-4,4,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(-5,-6,1.5,0,Math.PI*2); ctx.arc(7,-6,1.5,0,Math.PI*2); ctx.fill();
    // hp bar
    ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(-en.size, -en.size-10, en.size*2, 5);
    ctx.fillStyle='#ff4757'; ctx.fillRect(-en.size, -en.size-10, en.size*2*(en.hp/en.maxHp),5);
    ctx.restore();
  });
  // player
  ctx.save(); ctx.translate(player.x,player.y);
  if(player.hurtT>0 && Math.floor(player.hurtT/4)%2===0) ctx.globalAlpha=0.45;
  // shadow
  ctx.fillStyle='rgba(0,0,0,0.4)'; ctx.beginPath(); ctx.ellipse(0,14,16,7,0,0,Math.PI*2); ctx.fill();
  // boots effect
  const hasBoots=state.inventory.some(i=>i.id==='boots');
  const hasCharm=state.inventory.some(i=>i.id==='charm');
  const hasRing=state.inventory.some(i=>i.id==='ring');
  // body
  ctx.fillStyle= hasCharm ? '#2a1a0a' : '#1e2f1e';
  if(hasRing){ ctx.shadowBlur=10; ctx.shadowColor='#e8b84b'; }
  ctx.beginPath(); ctx.roundRect(-12,-14,24,28,8); ctx.fill(); ctx.shadowBlur=0;
  // head
  ctx.fillStyle='#f0d8b0'; ctx.beginPath(); ctx.arc(0,-18,11,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='#1a1200'; ctx.beginPath(); ctx.arc(-4,-18,2,0,Math.PI*2); ctx.arc(4,-18,2,0,Math.PI*2); ctx.fill();
  // weapon visual
  const w=WEAPONS[state.weapon];
  ctx.save();
  let ang = player.swinging ? -0.9 + (player.swingT/14)*2.2 : 0.35;
  // rotate based on dir
  if(player.dir===2) ang = Math.PI - ang;
  if(player.dir===1) ang = Math.PI/2 + ang*0.6;
  if(player.dir===3) ang = -Math.PI/2 - ang*0.6;
  ctx.rotate(ang);
  ctx.translate(14, -6);
  // blade
  const bladeLen = w.range*0.62;
  ctx.fillStyle=w.color; ctx.fillRect(0,-3, bladeLen,6);
  ctx.fillStyle='#fff'; ctx.fillRect(bladeLen-6,-2,6,4);
  if(w.id===2){ ctx.fillStyle='#ff6b35'; ctx.beginPath(); ctx.arc(bladeLen-2,0,4,0,Math.PI*2); ctx.fill(); }
  // hilt
  ctx.fillStyle='#3a2a0a'; ctx.fillRect(-8,-4,10,8);
  ctx.restore();
  // bow on back if has
  if(state.hasBow){
    ctx.strokeStyle='#6b4a1a'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(10,-8,14, -0.6, 0.6); ctx.stroke();
  }
  // legs with walk cycle
  const walk = (Date.now()/120)% (Math.PI*2);
  const step = (player.vx||player.vy) ? Math.sin(walk)*3 : 0;
  ctx.fillStyle= hasBoots ? '#6bc4ff' : '#2a3a2a';
  ctx.fillRect(-9+step,8,7,10); ctx.fillRect(2-step,8,7,10);
  ctx.restore();
  // swing arc effect
  if(player.swinging){
    ctx.save(); ctx.translate(player.x,player.y);
    ctx.strokeStyle= WEAPONS[state.weapon].id===2 ? 'rgba(255,107,53,0.9)' : 'rgba(255,255,255,0.85)';
    ctx.lineWidth=3; ctx.beginPath();
    const rarc=WEAPONS[state.weapon].range*0.9;
    ctx.arc(0,0, rarc, -0.9, 1.0); ctx.stroke();
    if(WEAPONS[state.weapon].id===2){ ctx.fillStyle='rgba(255,107,53,0.18)'; ctx.beginPath(); ctx.arc(0,0,rarc, -0.9,1.0); ctx.lineTo(0,0); ctx.fill(); }
    ctx.restore();
  }
  // particles
  particles.forEach(p=>{
    ctx.globalAlpha=Math.max(0,p.life/18);
    ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2); ctx.fill();
  });
  ctx.globalAlpha=1;
}
function loop(){ update(); draw(); requestAnimationFrame(loop); }

/* ========= Init ========= */
function init(){
  if(!loadSave()) loadRoom('village');
  // ensure inventory has at least 5 distinct after shop purchases? start with 2, guide player to get 3 more
  renderAll();
  addLog('Awakened in Village Green. The shrine hums. Seek the forest north.',true);
  addLog('Tip: Press E near chests, shrines, and the shop. Space to swing.');
  // continuous coin bob handled in update
  loop();
}
document.getElementById('shopOpen').onclick=()=>document.getElementById('shopModal').classList.add('open');
document.getElementById('shopClose').onclick=()=>document.getElementById('shopModal').classList.remove('open');
document.getElementById('shopModal').addEventListener('click',e=>{ if(e.target.id==='shopModal') e.currentTarget.classList.remove('open')});
document.getElementById('helpBtn').onclick=()=>document.getElementById('helpModal').classList.add('open');
document.getElementById('helpClose').onclick=()=>document.getElementById('helpModal').classList.remove('open');
document.getElementById('helpModal').addEventListener('click',e=>{ if(e.target.id==='helpModal') e.currentTarget.classList.remove('open')});
document.getElementById('saveBtn').onclick=()=>interact();
document.getElementById('healBtn').onclick=usePotion;
document.getElementById('soundBtn').onclick=()=>{
  soundOn=!soundOn;
  document.getElementById('soundBtn').textContent=soundOn?'🔊':'🔇';
  toast(soundOn?'Sound on':'Sound muted');
};
document.getElementById('clearLog').onclick=()=>logEl.innerHTML='';
document.getElementById('dropBtn').onclick=()=>{
  if(state.inventory.length===0) return toast('Nothing to drop');
  const last=state.inventory.pop(); toast(`Dropped ${last.id}`); renderAll();
};
canvas.addEventListener('click',e=>{
  const rect=canvas.getBoundingClientRect();
  const x=(e.clientX-rect.left)/rect.width*canvas.width;
  const y=(e.clientY-rect.top)/rect.height*canvas.height;
  // if click near chest/shrine/shop, interact
  const distShrine=Math.hypot(x-460,y-260);
  if(distShrine<70 && ROOMS[state.room].shrine){ interact(); return; }
  for(const ch of chests){ if(!ch.opened && Math.hypot(x-ch.x,y-ch.y)<42){ openChest(ch); return; }}
  // otherwise move player a bit toward click (quick dash)
  player.x += Math.sign(x-player.x)*18; player.y += Math.sign(y-player.y)*18;
  burst(x,y,'#ffffff',6);
});
// keyboard focus for canvas
window.addEventListener('load', init);
init();
</script>
</body>
</html>