← Aethelgard results

Aethelgard

GLM 5.3 Flash · full

Initial view of Aethelgard

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 80,392 bytes · SHA-256 2e4dc9deadd3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AETHERGARD — Resonants of Aethelgard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
:root{
  --bg0:#0b0a1a; --bg1:#141228; --glass:rgba(30,27,60,.55); --glass2:rgba(45,40,85,.45);
  --line:rgba(160,150,255,.18); --txt:#e8e6ff; --dim:#9d97c9;
  --pyro:#ff6b35; --hydro:#38bdf8; --flora:#4ade80; --zephyr:#a5f3fc; --earth:#d9a066; --volt:#fde047; --cosmos:#c084fc;
}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent}
html,body{height:100%}
body{
  margin:0; font-family:'Inter',system-ui,sans-serif; color:var(--txt);
  background:
    radial-gradient(1200px 700px at 15% -10%, rgba(124,58,237,.28), transparent 60%),
    radial-gradient(1000px 600px at 110% 20%, rgba(14,165,233,.18), transparent 55%),
    radial-gradient(900px 900px at 50% 120%, rgba(236,72,153,.12), transparent 60%),
    linear-gradient(180deg,var(--bg0),var(--bg1) 60%, #0d0b20);
  background-attachment:fixed; overflow-x:hidden;
}
.font-pixel{font-family:'Press Start 2P',monospace}
#starfield{position:fixed; inset:0; z-index:0; pointer-events:none; opacity:.6}
.glass{
  background:var(--glass); backdrop-filter:blur(14px) saturate(1.3); -webkit-backdrop-filter:blur(14px) saturate(1.3);
  border:1px solid var(--line); border-radius:1rem;
  box-shadow:0 8px 32px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.07);
}
.glass-soft{background:var(--glass2); backdrop-filter:blur(10px); border:1px solid var(--line); border-radius:.85rem}
/* ---------- screens ---------- */
.screen{position:absolute; inset:0; display:flex; flex-direction:column; opacity:0; transform:translateX(40px) scale(.985); pointer-events:none;
  transition:opacity .38s ease, transform .38s cubic-bezier(.22,.9,.3,1.2); overflow-y:auto; overflow-x:hidden; padding:1rem;}
.screen.active{opacity:1; transform:none; pointer-events:auto; z-index:2}
.screen.exit-left{transform:translateX(-40px) scale(.985)}
#app{position:relative; width:100%; max-width:1100px; margin:0 auto; height:100dvh; min-height:640px}
@media (min-width:900px){ #app{height:min(94dvh,860px); margin-top:2.5dvh} }
/* ---------- element glows ---------- */
.elg-pyro{--el:var(--pyro)} .elg-hydro{--el:var(--hydro)} .elg-flora{--el:var(--flora)}
.elg-zephyr{--el:var(--zephyr)} .elg-earth{--el:var(--earth)} .elg-volt{--el:var(--volt)} .elg-cosmos{--el:var(--cosmos)}
.el-border{border:1px solid color-mix(in srgb, var(--el) 55%, transparent); box-shadow:0 0 14px color-mix(in srgb, var(--el) 30%, transparent), inset 0 0 18px color-mix(in srgb, var(--el) 8%, transparent)}
.el-text{color:var(--el)}
.el-chip{background:color-mix(in srgb, var(--el) 18%, transparent); border:1px solid color-mix(in srgb, var(--el) 55%, transparent); color:var(--el)}
/* ---------- buttons ---------- */
.btn{position:relative; display:inline-flex; align-items:center; justify-content:center; gap:.5rem; font-weight:600; border-radius:.75rem;
  padding:.7rem 1.15rem; border:1px solid var(--line); background:rgba(60,55,110,.35); color:var(--txt); cursor:pointer;
  transition:transform .12s ease, box-shadow .2s ease, background .2s ease, border-color .2s; user-select:none;}
.btn:hover{transform:translateY(-2px); background:rgba(90,82,160,.45); border-color:rgba(180,170,255,.45); box-shadow:0 6px 20px rgba(120,90,255,.25)}
.btn:active{transform:translateY(0) scale(.97)}
.btn:disabled{opacity:.4; cursor:not-allowed; transform:none; box-shadow:none}
.btn-primary{background:linear-gradient(135deg, rgba(139,92,246,.55), rgba(59,130,246,.45)); border-color:rgba(167,139,250,.6)}
.btn-primary:hover{box-shadow:0 8px 26px rgba(139,92,246,.45)}
.btn-danger{background:rgba(220,38,38,.28); border-color:rgba(248,113,113,.5)}
.btn-danger:hover{background:rgba(220,38,38,.45)}
/* ---------- hp / xp bars ---------- */
.bar{height:12px; border-radius:99px; background:rgba(0,0,0,.45); overflow:hidden; border:1px solid rgba(255,255,255,.12); position:relative}
.bar>i{display:block; height:100%; border-radius:99px; transition:width .55s cubic-bezier(.3,.8,.3,1), background-color .4s;
  background:linear-gradient(90deg,#22c55e,#4ade80); box-shadow:0 0 10px rgba(74,222,128,.6)}
.bar.hp-mid>i{background:linear-gradient(90deg,#eab308,#facc15); box-shadow:0 0 10px rgba(250,204,21,.6)}
.bar.hp-low>i{background:linear-gradient(90deg,#dc2626,#f87171); box-shadow:0 0 12px rgba(248,113,113,.7)}
.bar.xp>i{background:linear-gradient(90deg,#8b5cf6,#38bdf8); box-shadow:0 0 8px rgba(139,92,246,.7)}
.bar.xp{height:7px}
/* ---------- battle ---------- */
.sprite-bob{animation:bob 2.6s ease-in-out infinite}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-9px)}}
.sprite-bob.fast{animation-duration:1.6s}
@keyframes shakeX{0%,100%{transform:translateX(0)}15%{transform:translateX(-14px)}30%{transform:translateX(12px)}45%{transform:translateX(-9px)}60%{transform:translateX(7px)}80%{transform:translateX(-3px)}}
.shake{animation:shakeX .5s ease}
@keyframes hitFlash{0%{filter:brightness(1)}30%{filter:brightness(3) saturate(0)}60%{filter:brightness(1.6)}100%{filter:brightness(1)}}
.hitflash{animation:hitFlash .45s ease}
@keyframes faintDrop{to{transform:translateY(60px) rotate(12deg); opacity:0}}
.faint{animation:faintDrop .7s ease forwards}
@keyframes popIn{from{transform:scale(.5); opacity:0}60%{transform:scale(1.08)}to{transform:scale(1); opacity:1}}
.pop-in{animation:popIn .4s cubic-bezier(.3,1.4,.5,1)}
@keyframes floatUp{from{transform:translateY(0); opacity:1}to{transform:translateY(-46px); opacity:0}}
.float-num{animation:floatUp 1s ease forwards; font-weight:800; text-shadow:0 2px 8px rgba(0,0,0,.7)}
@keyframes pulseGlow{0%,100%{box-shadow:0 0 12px color-mix(in srgb, var(--el) 35%, transparent)}50%{box-shadow:0 0 26px color-mix(in srgb, var(--el) 70%, transparent)}}
.pulse-el{animation:pulseGlow 2s ease-in-out infinite}
/* particles */
.pfield{position:absolute; inset:0; overflow:hidden; pointer-events:none}
.particle{position:absolute; width:7px; height:7px; border-radius:50%; opacity:.75; filter:blur(.5px); animation:drift linear infinite}
@keyframes drift{from{transform:translateY(20px) translateX(0) rotate(0)}to{transform:translateY(-220px) translateX(28px) rotate(220deg); opacity:0}}
/* ---------- map nodes ---------- */
.node{position:absolute; transform:translate(-50%,-50%); width:58px; height:58px; border-radius:50%; display:flex; align-items:center; justify-content:center;
  cursor:pointer; font-size:22px; transition:transform .15s ease, box-shadow .2s; background:var(--glass); border:1px solid var(--line)}
.node:hover{transform:translate(-50%,-50%) scale(1.14)}
.node.done{opacity:.45; filter:grayscale(.8)}
.node.locked{opacity:.35; cursor:not-allowed; filter:grayscale(1)}
.node.current{box-shadow:0 0 0 4px rgba(167,139,250,.35), 0 0 24px rgba(167,139,250,.6); border-color:rgba(196,181,253,.8)}
.node .nlab{position:absolute; top:105%; white-space:nowrap; font-size:.62rem; color:var(--dim); background:rgba(10,8,25,.8); padding:2px 7px; border-radius:6px; border:1px solid var(--line)}
/* ---------- dex / misc ---------- */
.dexcard{cursor:pointer; transition:transform .15s, box-shadow .2s}
.dexcard:hover{transform:translateY(-4px) scale(1.02)}
.dexcard.unknown{filter:grayscale(1) brightness(.55)}
.dexcard.unknown .spr{opacity:.25; filter:blur(3px)}
.toast{position:fixed; left:50%; bottom:26px; transform:translateX(-50%) translateY(20px); opacity:0; z-index:90;
  transition:all .3s ease; padding:.7rem 1.2rem; font-weight:600; max-width:90vw}
.toast.show{opacity:1; transform:translateX(-50%) translateY(0)}
.modal-back{position:fixed; inset:0; background:rgba(5,4,15,.7); backdrop-filter:blur(6px); z-index:80; display:flex; align-items:center; justify-content:center; padding:1rem; opacity:0; pointer-events:none; transition:opacity .25s}
.modal-back.show{opacity:1; pointer-events:auto}
.modal-back .glass{transform:scale(.92); transition:transform .25s}
.modal-back.show .glass{transform:scale(1)}
.spr{display:inline-block}
.scroll-y{overflow-y:auto; scrollbar-width:thin; scrollbar-color:rgba(160,150,255,.35) transparent}
.scroll-y::-webkit-scrollbar{width:8px}
.scroll-y::-webkit-scrollbar-thumb{background:rgba(160,150,255,.3); border-radius:99px}
#battle-log{min-height:86px; max-height:110px}
.logline{animation:popIn .25s ease}
.tabbtn{padding:.5rem .9rem; border-radius:.7rem; font-size:.8rem; font-weight:600; cursor:pointer; border:1px solid transparent; color:var(--dim); transition:all .2s}
.tabbtn.on{background:rgba(139,92,246,.3); border-color:rgba(167,139,250,.55); color:#fff; box-shadow:0 0 14px rgba(139,92,246,.3)}
.movebtn{width:100%; text-align:left; display:flex; justify-content:space-between; align-items:center; gap:.5rem}
.movebtn:hover{border-color:color-mix(in srgb, var(--el) 70%, transparent)}
.title-glow{text-shadow:0 0 24px rgba(167,139,250,.8), 0 0 60px rgba(139,92,246,.4)}
.vignette::after{content:''; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.45));}
</style>
</head>
<body>
<canvas id="starfield"></canvas>
<div id="app">

  <!-- ============ MAIN MENU ============ -->
  <div id="screen-menu" class="screen active items-center justify-center">
    <div class="text-center max-w-lg w-full px-4">
      <div class="mb-2 text-[10px] tracking-[.4em] text-violet-300/80 font-pixel">AETHELGARD</div>
      <h1 class="font-pixel text-3xl sm:text-4xl title-glow text-violet-200 leading-relaxed">AETHER<span class="text-fuchsia-400">PRISM</span></h1>
      <p class="mt-4 text-sm text-[color:var(--dim)] leading-relaxed">Tune the frequencies. Capture the Resonants.<br>Become the Grand Conductor.</p>
      <div class="glass p-6 mt-8 flex flex-col gap-3">
        <button id="btn-continue" class="btn btn-primary text-base">▶ Continue Journey</button>
        <button id="btn-newgame" class="btn text-base">✦ New Journey</button>
        <div class="grid grid-cols-2 gap-3 mt-1">
          <button id="btn-menu-dex" class="btn text-sm">📖 Conductors Dex</button>
          <button id="btn-reset" class="btn btn-danger text-sm">⟲ Reset Progress</button>
        </div>
      </div>
      <p class="mt-4 text-[11px] text-[color:var(--dim)]/70">Progress saves automatically to this device.</p>
    </div>
  </div>

  <!-- ============ OVERWORLD MAP ============ -->
  <div id="screen-map" class="screen">
    <div class="flex items-center justify-between gap-2 flex-wrap mb-3">
      <div class="glass px-4 py-2 flex items-center gap-3">
        <span class="text-lg">💎</span>
        <span class="font-bold text-cyan-300" id="hud-shards">0</span>
        <span class="text-xs text-[color:var(--dim)]">Aether Shards</span>
      </div>
      <div class="flex gap-2 flex-wrap">
        <button class="btn text-xs" data-nav="party">🎒 Party</button>
        <button class="btn text-xs" data-nav="shop">🛒 Camp Shop</button>
        <button class="btn text-xs" data-nav="dex">📖 Dex</button>
        <button class="btn text-xs" data-nav="menu">⏸ Menu</button>
      </div>
    </div>
    <div class="flex gap-2 mb-3 flex-wrap" id="zone-tabs"></div>
    <div class="glass flex-1 min-h-[380px] relative vignette overflow-hidden" id="map-canvas">
      <div class="pfield" id="map-particles"></div>
      <div id="zone-title" class="absolute top-3 left-0 right-0 text-center font-pixel text-[11px] text-violet-200/90 z-10"></div>
      <div id="zone-desc" class="absolute top-9 left-0 right-0 text-center text-[11px] text-[color:var(--dim)] z-10 px-4"></div>
      <div id="node-layer" class="absolute inset-0"></div>
      <svg id="trail-svg" class="absolute inset-0 w-full h-full pointer-events-none" preserveAspectRatio="none"></svg>
    </div>
    <div id="map-party" class="flex gap-2 mt-3 flex-wrap"></div>
  </div>

  <!-- ============ BATTLE ============ -->
  <div id="screen-battle" class="screen">
    <div class="flex-1 flex flex-col gap-3 min-h-0">
      <!-- enemy -->
      <div class="glass elg-pyro p-3 relative overflow-hidden" id="enemy-panel">
        <div class="pfield" id="enemy-particles"></div>
        <div class="flex items-start justify-between gap-3">
          <div class="flex-1 min-w-0">
            <div class="flex items-center gap-2 flex-wrap">
              <span id="enemy-name" class="font-bold"></span>
              <span id="enemy-type" class="text-[10px] px-2 py-.5 rounded-full el-chip"></span>
              <span id="enemy-lv" class="text-xs text-[color:var(--dim)]"></span>
            </div>
            <div class="bar mt-2 w-full max-w-[260px]" id="enemy-hpbar"><i style="width:100%"></i></div>
            <div id="enemy-status" class="text-[10px] mt-1 text-fuchsia-300"></div>
          </div>
          <div id="enemy-sprite" class="spr w-24 h-24 sm:w-32 sm:h-32 relative z-10"></div>
        </div>
      </div>
      <!-- arena message -->
      <div class="glass px-4 py-2 text-sm text-center" id="battle-banner">A wild presence stirs…</div>
      <!-- player -->
      <div class="glass elg-pyro p-3 relative overflow-hidden" id="player-panel">
        <div class="pfield" id="player-particles"></div>
        <div class="flex items-end justify-between gap-3">
          <div id="player-sprite" class="spr w-24 h-24 sm:w-32 sm:h-32 relative z-10"></div>
          <div class="flex-1 min-w-0 text-right">
            <div class="flex items-center justify-end gap-2 flex-wrap">
              <span id="player-name" class="font-bold"></span>
              <span id="player-type" class="text-[10px] px-2 py-.5 rounded-full el-chip"></span>
              <span id="player-lv" class="text-xs text-[color:var(--dim)]"></span>
            </div>
            <div class="bar mt-2 w-full max-w-[260px] ml-auto" id="player-hpbar"><i style="width:100%"></i></div>
            <div class="text-[10px] mt-1 text-[color:var(--dim)]"><span id="player-hptext"></span></div>
            <div class="bar xp mt-1 w-full max-w-[260px] ml-auto" id="player-xpbar"><i style="width:0%"></i></div>
            <div id="player-status" class="text-[10px] mt-1 text-fuchsia-300"></div>
          </div>
        </div>
      </div>
      <!-- actions -->
      <div class="glass p-3" id="battle-actions">
        <div id="action-main" class="grid grid-cols-2 sm:grid-cols-4 gap-2">
          <button class="btn" data-act="attack">⚔ Attack</button>
          <button class="btn" data-act="prism">💠 Prisms</button>
          <button class="btn" data-act="swap">🔄 Exchange</button>
          <button class="btn" data-act="bag">🎒 Inventory</button>
        </div>
        <div id="action-sub" class="hidden"></div>
        <button id="action-back" class="btn text-xs mt-2 hidden">← Back</button>
      </div>
    </div>
  </div>

  <!-- ============ DEX ============ -->
  <div id="screen-dex" class="screen">
    <div class="flex items-center justify-between mb-3">
      <h2 class="font-pixel text-sm text-violet-200">CONDUCTORS DEX</h2>
      <div class="flex gap-2 items-center">
        <span class="text-xs text-[color:var(--dim)]" id="dex-count"></span>
        <button class="btn text-xs" data-nav="back">← Back</button>
      </div>
    </div>
    <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 scroll-y flex-1 pr-1" id="dex-grid"></div>
  </div>

  <!-- ============ SHOP ============ -->
  <div id="screen-shop" class="screen">
    <div class="flex items-center justify-between mb-3">
      <h2 class="font-pixel text-sm text-violet-200">CAMP SHOP</h2>
      <div class="flex gap-2 items-center">
        <span class="glass px-3 py-1 text-sm">💎 <b class="text-cyan-300" id="shop-shards">0</b></span>
        <button class="btn text-xs" data-nav="back">← Back</button>
      </div>
    </div>
    <div class="grid sm:grid-cols-2 gap-3 flex-1 content-start scroll-y pr-1" id="shop-grid"></div>
  </div>

  <!-- ============ PARTY ============ -->
  <div id="screen-party" class="screen">
    <div class="flex items-center justify-between mb-3">
      <h2 class="font-pixel text-sm text-violet-200">YOUR RESONANTS</h2>
      <button class="btn text-xs" data-nav="back">← Back</button>
    </div>
    <div class="grid sm:grid-cols-2 gap-3 flex-1 content-start scroll-y pr-1" id="party-grid"></div>
  </div>
</div>

<div id="toast" class="toast glass"></div>
<div id="modal" class="modal-back"><div class="glass p-6 max-w-md w-full mx-4" id="modal-box"></div></div>

<script>
/* =====================================================================
   AETHERPRISM — single-file monster-collection RPG
   ===================================================================== */
'use strict';

/* ---------------- Synth Manager (Web Audio 8-bit) ---------------- */
class Synth {
  constructor(){ this.ctx=null; this.muted=false; }
  ensure(){ if(!this.ctx){ try{ this.ctx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(this.ctx&&this.ctx.state==='suspended') this.ctx.resume(); return !!this.ctx; }
  tone(freq,dur,type='square',vol=.15,slideTo=null,delay=0){
    if(this.muted||!this.ensure())return;
    const t=this.ctx.currentTime+delay, o=this.ctx.createOscillator(), g=this.ctx.createGain();
    o.type=type; o.frequency.setValueAtTime(freq,t);
    if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(30,slideTo),t+dur);
    g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(.0001,t+dur);
    o.connect(g); g.connect(this.ctx.destination); o.start(t); o.stop(t+dur+.02);
  }
  noise(dur=.15,vol=.12,delay=0){
    if(this.muted||!this.ensure())return;
    const t=this.ctx.currentTime+delay, len=this.ctx.sampleRate*dur, buf=this.ctx.createBuffer(1,len,this.ctx.sampleRate), d=buf.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=(Math.random()*2-1)*(1-i/len);
    const s=this.ctx.createBufferSource(); s.buffer=buf;
    const g=this.ctx.createGain(); g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(.0001,t+dur);
    s.connect(g); g.connect(this.ctx.destination); s.start(t);
  }
  hover(){ this.tone(520,.05,'square',.05); }
  select(){ this.tone(660,.07,'square',.09); this.tone(880,.09,'square',.08,null,.06); }
  back(){ this.tone(440,.08,'square',.08,null,0); this.tone(300,.1,'square',.07,null,.06); }
  hitPhys(){ this.noise(.12,.16); this.tone(160,.1,'sawtooth',.12,80); }
  hitElem(){ this.tone(880,.12,'square',.1,220); this.noise(.15,.1); }
  superEff(){ this.tone(200,.25,'sawtooth',.14,900); this.noise(.3,.16); this.tone(1200,.2,'square',.1,300,.08); }
  weak(){ this.tone(300,.12,'triangle',.09,140); }
  levelUp(){ [523,659,784,1047].forEach((f,i)=>this.tone(f,.14,'square',.1,null,i*.1)); }
  capture(){ [392,523,659,784,1047].forEach((f,i)=>this.tone(f,.16,'triangle',.12,null,i*.11)); }
  fail(){ this.tone(220,.2,'sawtooth',.1,110); }
  heal(){ this.tone(523,.1,'sine',.1); this.tone(784,.14,'sine',.1,null,.09); }
  evolve(){ [262,330,392,523,659,784].forEach((f,i)=>this.tone(f,.18,'square',.1,null,i*.09)); }
  boss(){ this.tone(98,.5,'sawtooth',.14,60); this.tone(147,.5,'sawtooth',.1,90,.1); }
}
const SFX=new Synth();

/* ---------------- Elements ---------------- */
const ELEMENTS={
  pyro:{name:'Pyro', color:'#ff6b35', icon:'🔥', strong:['flora'], weak:['hydro','pyro']},
  hydro:{name:'Hydro', color:'#38bdf8', icon:'💧', strong:['pyro'], weak:['flora','hydro']},
  flora:{name:'Flora', color:'#4ade80', icon:'🌿', strong:['hydro'], weak:['pyro','flora']},
  zephyr:{name:'Zephyr', color:'#a5f3fc', icon:'🌪️', strong:['earth'], weak:['volt']},
  earth:{name:'Earth', color:'#d9a066', icon:'⛰️', strong:['volt'], weak:['zephyr']},
  volt:{name:'Volt', color:'#fde047', icon:'⚡', strong:['zephyr'], weak:['earth']},
  cosmos:{name:'Cosmos', color:'#c084fc', icon:'✦', strong:[], weak:[]},
};
function typeMod(atk,def){
  const a=ELEMENTS[atk]; if(!a) return 1;
  if(a.strong.includes(def)) return 2;
  if(a.weak.includes(def)) return .5;
  return 1;
}

/* ---------------- Move Database ---------------- */
const MOVES={
  ember_spark:{name:'Ember Spark',pow:35,acc:100,el:'pyro',desc:'A flicker of living flame.'},
  flame_fang:{name:'Flame Fang',pow:50,acc:95,el:'pyro',desc:'Bites with superheated jaws.'},
  cinder_storm:{name:'Cinder Storm',pow:70,acc:85,el:'pyro',desc:'A whirlwind of burning ash.'},
  solar_lance:{name:'Solar Lance',pow:90,acc:80,el:'pyro',desc:'A spear of condensed starfire.'},
  bubble_jet:{name:'Bubble Jet',pow:35,acc:100,el:'hydro',desc:'Pressurized water pellets.'},
  tide_crash:{name:'Tide Crash',pow:50,acc:95,el:'hydro',desc:'A slamming wall of water.'},
  abyss_pulse:{name:'Abyss Pulse',pow:70,acc:85,el:'hydro',desc:'A deep-pressure shockwave.'},
  maelstrom:{name:'Maelstrom',pow:90,acc:80,el:'hydro',desc:'A devouring ocean vortex.'},
  leaf_dart:{name:'Leaf Dart',pow:35,acc:100,el:'flora',desc:'Razor leaves on the wind.'},
  vine_lash:{name:'Vine Lash',pow:50,acc:95,el:'flora',desc:'Thorned whips strike out.'},
  spore_burst:{name:'Spore Burst',pow:70,acc:85,el:'flora',desc:'An explosive pollen cloud.'},
  world_bloom:{name:'World Bloom',pow:90,acc:80,el:'flora',desc:'Nature erupts in fury.'},
  gust_cut:{name:'Gust Cut',pow:35,acc:100,el:'zephyr',desc:'A blade of thin air.'},
  wing_slam:{name:'Wing Slam',pow:50,acc:95,el:'zephyr',desc:'A downstroke of pure force.'},
  cyclone:{name:'Cyclone',pow:70,acc:85,el:'zephyr',desc:'A spinning sky funnel.'},
  heaven_gale:{name:'Heaven Gale',pow:90,acc:80,el:'zephyr',desc:'The sky itself attacks.'},
  pebble_shot:{name:'Pebble Shot',pow:35,acc:100,el:'earth',desc:'Fires jagged stones.'},
  boulder_roll:{name:'Boulder Roll',pow:50,acc:95,el:'earth',desc:'A crushing rolling mass.'},
  quake_line:{name:'Quake Line',pow:70,acc:85,el:'earth',desc:'The ground splits open.'},
  tectonix:{name:'Tectonix',pow:90,acc:80,el:'earth',desc:'A continental upheaval.'},
  static_nip:{name:'Static Nip',pow:35,acc:100,el:'volt',desc:'A crackling little bite.'},
  arc_bolt:{name:'Arc Bolt',pow:50,acc:95,el:'volt',desc:'Lightning arcs to the foe.'},
  ion_storm:{name:'Ion Storm',pow:70,acc:85,el:'volt',desc:'A shower of charged plasma.'},
  thunder_verdict:{name:'Thunder Verdict',pow:90,acc:80,el:'volt',desc:'The sky passes judgment.'},
  star_dust:{name:'Star Dust',pow:40,acc:100,el:'cosmos',desc:'Shimmering void motes.'},
  phase_slip:{name:'Phase Slip',pow:55,acc:95,el:'cosmos',desc:'Strikes from between moments.'},
  void_lance:{name:'Void Lance',pow:75,acc:85,el:'cosmos',desc:'A spike of nothingness.'},
  harmonic_nova:{name:'Harmonic Nova',pow:95,acc:75,el:'cosmos',desc:'All frequencies at once.'},
  tackle:{name:'Tackle',pow:30,acc:100,el:'cosmos',desc:'A straightforward body blow.'},
};

/* ---------------- Sprite templates ---------------- */
function grad(id,c1,c2){ return `<defs><linearGradient id="${id}" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c2}"/></linearGradient></defs>`; }
const SPRITE_TEMPLATES={
  quad:(c)=>`${grad('g1',c[0],c[1])}
    <ellipse cx="50" cy="72" rx="26" ry="17" fill="url(#g1)"/>
    <ellipse cx="72" cy="52" rx="16" ry="14" fill="url(#g1)"/>
    <polygon points="60,42 66,24 74,40" fill="url(#g1)"/><polygon points="78,40 86,26 88,44" fill="url(#g1)"/>
    <path d="M26 68 Q8 60 12 44 Q20 54 28 56" fill="url(#g1)"/>
    <rect x="34" y="82" width="7" height="14" rx="3" fill="${c[1]}"/><rect x="58" y="82" width="7" height="14" rx="3" fill="${c[1]}"/>
    <circle cx="68" cy="50" r="3.4" fill="#fff"/><circle cx="69.5" cy="50" r="1.8" fill="#111"/>
    <path d="M78 58 Q86 60 84 66" stroke="${c[2]}" stroke-width="2.5" fill="none" stroke-linecap="round"/>`,
  blob:(c)=>`${grad('g1',c[0],c[1])}
    <path d="M50 20 C78 20 88 46 86 66 C84 84 70 92 50 92 C30 92 16 84 14 66 C12 46 22 20 50 20 Z" fill="url(#g1)"/>
    <circle cx="38" cy="52" r="5" fill="#fff"/><circle cx="62" cy="52" r="5" fill="#fff"/>
    <circle cx="39" cy="53" r="2.6" fill="#111"/><circle cx="61" cy="53" r="2.6" fill="#111"/>
    <path d="M42 68 Q50 76 58 68" stroke="#111" stroke-width="2.5" fill="none" stroke-linecap="round"/>
    <circle cx="26" cy="66" r="6" fill="${c[2]}" opacity=".8"/><circle cx="74" cy="66" r="6" fill="${c[2]}" opacity=".8"/>`,
  serpent:(c)=>`${grad('g1',c[0],c[1])}
    <path d="M20 88 Q34 70 26 54 Q20 40 38 34 Q58 28 62 44 Q66 58 52 62 Q40 66 46 78 Q50 88 66 88" fill="none" stroke="url(#g1)" stroke-width="16" stroke-linecap="round"/>
    <circle cx="66" cy="86" r="11" fill="url(#g1)"/>
    <circle cx="62" cy="84" r="3" fill="#fff"/><circle cx="63" cy="84" r="1.6" fill="#111"/>
    <path d="M74 90 L82 92 L74 94" fill="${c[2]}"/>
    <polygon points="58,74 66,70 64,80" fill="${c[2]}"/>`,
  bird:(c)=>`${grad('g1',c[0],c[1])}
    <ellipse cx="52" cy="58" rx="20" ry="24" fill="url(#g1)"/>
    <circle cx="52" cy="30" r="13" fill="url(#g1)"/>
    <polygon points="40,26 22,20 38,34" fill="${c[2]}"/><polygon points="64,26 82,20 66,34" fill="${c[2]}"/>
    <polygon points="52,32 60,36 52,40" fill="${c[2]}"/>
    <circle cx="47" cy="28" r="3" fill="#fff"/><circle cx="48" cy="28" r="1.6" fill="#111"/>
    <path d="M30 58 Q10 52 8 38 Q24 44 32 50 Z" fill="${c[2]}" opacity=".85"/>
    <path d="M74 58 Q92 52 94 38 Q80 44 72 50 Z" fill="${c[2]}" opacity=".85"/>
    <path d="M46 82 L44 94 M58 82 L60 94" stroke="${c[2]}" stroke-width="3" stroke-linecap="round"/>`,
  golem:(c)=>`${grad('g1',c[0],c[1])}
    <rect x="30" y="42" width="40" height="40" rx="8" fill="url(#g1)"/>
    <rect x="36" y="20" width="28" height="24" rx="6" fill="url(#g1)"/>
    <rect x="14" y="46" width="14" height="26" rx="5" fill="${c[1]}"/><rect x="72" y="46" width="14" height="26" rx="5" fill="${c[1]}"/>
    <rect x="36" y="82" width="11" height="14" rx="4" fill="${c[1]}"/><rect x="53" y="82" width="11" height="14" rx="4" fill="${c[1]}"/>
    <circle cx="44" cy="32" r="3.4" fill="${c[2]}"/><circle cx="56" cy="32" r="3.4" fill="${c[2]}"/>
    <polygon points="50,52 60,62 50,72 40,62" fill="${c[2]}"/>
    <path d="M34 46 L30 30 M66 46 L70 30" stroke="${c[2]}" stroke-width="3" stroke-linecap="round"/>`,
  spark:(c)=>`${grad('g1',c[0],c[1])}
    <polygon points="50,14 62,40 88,44 66,60 74,88 50,72 26,88 34,60 12,44 38,40" fill="url(#g1)" stroke="${c[2]}" stroke-width="2"/>
    <circle cx="44" cy="48" r="4" fill="#fff"/><circle cx="56" cy="48" r="4" fill="#fff"/>
    <circle cx="45" cy="49" r="2" fill="#111"/><circle cx="55" cy="49" r="2" fill="#111"/>
    <path d="M42 60 Q50 66 58 60" stroke="#111" stroke-width="2.5" fill="none" stroke-linecap="round"/>
    <path d="M20 20 L26 28 M84 20 L78 28" stroke="${c[2]}" stroke-width="2.5" stroke-linecap="round"/>`,
};

/* ---------------- Resonant Database (18) ---------------- */
const RESONANTS=[
 {id:'emberkit', name:'Emberkit', el:'pyro', tier:1, evo:'pyrowolf', evoLv:8, base:{hp:44,atk:52,def:40,spd:58}, shape:'quad', colors:['#ff8c42','#c2410c','#fde047'], catch:190,
  learn:[['tackle',1],['ember_spark',1],['flame_fang',6],['cinder_storm',12],['solar_lance',16]]},
 {id:'pyrowolf', name:'Pyrowolf', el:'pyro', tier:2, evo:'ignisaur', evoLv:15, base:{hp:60,atk:72,def:54,spd:74}, shape:'quad', colors:['#f97316','#7c2d12','#fbbf24'], catch:90,
  learn:[['tackle',1],['ember_spark',1],['flame_fang',1],['cinder_storm',12],['solar_lance',17]]},
 {id:'ignisaur', name:'Ignisaur', el:'pyro', tier:3, evo:null, evoLv:null, base:{hp:80,atk:96,def:70,spd:88}, shape:'quad', colors:['#ea580c','#431407','#f97316'], catch:45,
  learn:[['tackle',1],['ember_spark',1],['flame_fang',1],['cinder_storm',1],['solar_lance',18],['harmonic_nova',24]]},
 {id:'droplet', name:'Droplet', el:'hydro', tier:1, evo:'geoxus', evoLv:8, base:{hp:48,atk:44,def:52,spd:50}, shape:'blob', colors:['#7dd3fc','#0369a1','#38bdf8'], catch:190,
  learn:[['tackle',1],['bubble_jet',1],['tide_crash',6],['abyss_pulse',12],['maelstrom',16]]},
 {id:'geoxus', name:'Geoxus', el:'hydro', tier:2, evo:'neptunian', evoLv:15, base:{hp:64,atk:60,def:70,spd:58}, shape:'serpent', colors:['#38bdf8','#075985','#7dd3fc'], catch:90,
  learn:[['tackle',1],['bubble_jet',1],['tide_crash',1],['abyss_pulse',12],['maelstrom',17]]},
 {id:'neptunian', name:'Neptunian', el:'hydro', tier:3, evo:null, evoLv:null, base:{hp:88,atk:78,def:92,spd:70}, shape:'serpent', colors:['#0ea5e9','#082f49','#22d3ee'], catch:45,
  learn:[['tackle',1],['bubble_jet',1],['tide_crash',1],['abyss_pulse',1],['maelstrom',18],['harmonic_nova',24]]},
 {id:'seedling', name:'Seedling', el:'flora', tier:1, evo:'thornback', evoLv:8, base:{hp:50,atk:48,def:50,spd:44}, shape:'blob', colors:['#86efac','#15803d','#4ade80'], catch:190,
  learn:[['tackle',1],['leaf_dart',1],['vine_lash',6],['spore_burst',12],['world_bloom',16]]},
 {id:'thornback', name:'Thornback', el:'flora', tier:2, evo:'bloomgoliath', evoLv:15, base:{hp:68,atk:64,def:66,spd:52}, shape:'golem', colors:['#4ade80','#166534','#a3e635'], catch:90,
  learn:[['tackle',1],['leaf_dart',1],['vine_lash',1],['spore_burst',12],['world_bloom',17]]},
 {id:'bloomgoliath', name:'Bloomgoliath', el:'flora', tier:3, evo:null, evoLv:null, base:{hp:94,atk:82,def:86,spd:60}, shape:'golem', colors:['#22c55e','#14532d','#84cc16'], catch:45,
  learn:[['tackle',1],['leaf_dart',1],['vine_lash',1],['spore_burst',1],['world_bloom',18],['harmonic_nova',24]]},
 {id:'sparky', name:'Sparky', el:'volt', tier:1, evo:'boltsaber', evoLv:8, base:{hp:42,atk:54,def:38,spd:64}, shape:'spark', colors:['#fde047','#a16207','#facc15'], catch:190,
  learn:[['tackle',1],['static_nip',1],['arc_bolt',6],['ion_storm',12],['thunder_verdict',16]]},
 {id:'boltsaber', name:'Boltsaber', el:'volt', tier:2, evo:'stormlord', evoLv:15, base:{hp:58,atk:74,def:52,spd:82}, shape:'bird', colors:['#facc15','#854d0e','#fef08a'], catch:90,
  learn:[['tackle',1],['static_nip',1],['arc_bolt',1],['ion_storm',12],['thunder_verdict',17]]},
 {id:'stormlord', name:'Stormlord', el:'volt', tier:3, evo:null, evoLv:null, base:{hp:78,atk:98,def:68,spd:100}, shape:'bird', colors:['#eab308','#713f12','#fde047'], catch:45,
  learn:[['tackle',1],['static_nip',1],['arc_bolt',1],['ion_storm',1],['thunder_verdict',18],['harmonic_nova',24]]},
 {id:'gusty', name:'Gusty', el:'zephyr', tier:1, evo:'slipstream', evoLv:8, base:{hp:44,atk:46,def:42,spd:62}, shape:'bird', colors:['#bae6fd','#0369a1','#e0f2fe'], catch:190,
  learn:[['tackle',1],['gust_cut',1],['wing_slam',6],['cyclone',12],['heaven_gale',16]]},
 {id:'slipstream', name:'Slipstream', el:'zephyr', tier:2, evo:'zephyrus', evoLv:15, base:{hp:58,atk:64,def:56,spd:80}, shape:'serpent', colors:['#a5f3fc','#0e7490','#cffafe'], catch:90,
  learn:[['tackle',1],['gust_cut',1],['wing_slam',1],['cyclone',12],['heaven_gale',17]]},
 {id:'zephyrus', name:'Zephyrus', el:'zephyr', tier:3, evo:null, evoLv:null, base:{hp:76,atk:84,def:72,spd:98}, shape:'bird', colors:['#67e8f9','#155e75','#ecfeff'], catch:45,
  learn:[['tackle',1],['gust_cut',1],['wing_slam',1],['cyclone',1],['heaven_gale',18],['harmonic_nova',24]]},
 {id:'pebble', name:'Pebble', el:'earth', tier:1, evo:'obsidian', evoLv:8, base:{hp:54,atk:50,def:58,spd:32}, shape:'golem', colors:['#d6b28a','#78350f','#d9a066'], catch:190,
  learn:[['tackle',1],['pebble_shot',1],['boulder_roll',6],['quake_line',12],['tectonix',16]]},
 {id:'obsidian', name:'Obsidian', el:'earth', tier:2, evo:'terrasunder', evoLv:15, base:{hp:70,atk:68,def:78,spd:40}, shape:'golem', colors:['#57534e','#1c1917','#a78bfa'], catch:90,
  learn:[['tackle',1],['pebble_shot',1],['boulder_roll',1],['quake_line',12],['tectonix',17]]},
 {id:'terrasunder', name:'Terrasunder', el:'earth', tier:3, evo:null, evoLv:null, base:{hp:96,atk:88,def:100,spd:48}, shape:'golem', colors:['#44403c','#0c0a09','#f59e0b'], catch:45,
  learn:[['tackle',1],['pebble_shot',1],['boulder_roll',1],['quake_line',1],['tectonix',18],['harmonic_nova',24]]},
];
const RES_BY_ID={}; RESONANTS.forEach(r=>RES_BY_ID[r.id]=r);

function spriteSVG(id,cls=''){
  const r=RES_BY_ID[id]; if(!r) return '';
  const body=SPRITE_TEMPLATES[r.shape](r.colors);
  const aura={pyro:'#ff6b35',hydro:'#38bdf8',flora:'#4ade80',zephyr:'#a5f3fc',earth:'#d9a066',volt:'#fde047',cosmos:'#c084fc'}[r.el];
  return `<svg viewBox="0 0 100 100" class="w-full h-full" xmlns="http://www.w3.org/2000/svg">
    <defs><radialGradient id="aura-${id}" cx=".5" cy=".55" r=".55"><stop offset="0" stop-color="${aura}" stop-opacity=".35"/><stop offset="1" stop-color="${aura}" stop-opacity="0"/></radialGradient></defs>
    <ellipse cx="50" cy="94" rx="30" ry="6" fill="rgba(0,0,0,.4)"/>
    <circle cx="50" cy="55" r="46" fill="url(#aura-${id})"/>
    ${body}</svg>`;
}

/* ---------------- Items ---------------- */
const ITEMS={
  prism:{name:'Harmonizer Prism', icon:'💠', desc:'A basic Aether Prism. Standard harmonic capture.', price:120, consumable:true},
  hyperprism:{name:'Hyper-Conductive Prism', icon:'🔮', desc:'High-fidelity matrix. Greatly boosts capture rate.', price:400, consumable:true},
  elixir:{name:'Core Restorer', icon:'🧪', desc:'Restores 50% of a Resonant\'s max HP.', price:150, consumable:true},
  cleanser:{name:'Resonance Elixir', icon:'✨', desc:'Clears all status conditions.', price:180, consumable:true},
  booster:{name:'Prism Booster', icon:'⚡', desc:'Next Prism throw this battle has +40% capture power.', price:250, consumable:true},
};

/* ---------------- Zones & Node Map ---------------- */
const ZONES=[
 {id:'canopy', name:'The Verdant Canopy', desc:'Sun-dappled frequencies hum through ancient boughs.', el:'flora', lv:[1,5],
  pool:['seedling','seedling','gusty','sparky','droplet'],
  boss:{name:'Sylvan Warden Mira', title:'Warden of the Canopy', sprite:'thornback',
    intro:'Mira: "The Canopy answers only to balanced harmonies, young Conductor. Show me yours!"',
    mid:'Mira: "Your resonance deepens… impressive. But feel the forest\'s true weight!"',
    lose:'Mira: "The boughs bow to you. The Sunken Grotto lies beyond — Brann will test your tide."',
    win:'Mira: "You are not yet ready. Return when your Resonants sing louder."',
    team:[['thornback',8],['gusty',7],['bloomgoliath',10]]}},
 {id:'grotto', name:'The Sunken Grotto', desc:'Dripping echoes carry deep-water wavelengths.', el:'hydro', lv:[5,10],
  pool:['droplet','droplet','pebble','geoxus','seedling'],
  boss:{name:'Tidecaller Brann', title:'Voice of the Deep', sprite:'geoxus',
    intro:'Brann: "Few conductors hear the Grotto\'s low frequencies. Can you feel them, or only fear them?"',
    mid:'Brann: "The pressure rises! Let us see if your harmony holds beneath the weight of the deep!"',
    lose:'Brann: "The tide recedes before your strength. The Obsidian Crags burn ahead — Kael waits there."',
    win:'Brann: "Your song drowns in the dark. Surface, and do not return until you can breathe water."',
    team:[['geoxus',11],['obsidian',10],['neptunian',13]]}},
 {id:'crags', name:'The Obsidian Crags', desc:'Molten veins pulse with violent, crackling energy.', el:'pyro', lv:[10,15],
  pool:['emberkit','sparky','pyrowolf','boltsaber','obsidian'],
  boss:{name:'Cinderfist Kael', title:'The Blazing Fist', sprite:'pyrowolf',
    intro:'Kael: "No tricks. No prisms. Just fire against fire. SHOW ME YOUR RESOLVE!"',
    mid:'Kael: "HAH! Now THIS is a frequency I can respect! BURN BRIGHTER!"',
    lose:'Kael: "Magnificent! The Apex Pillar stands above. Grand Conductor Lyra awaits the worthy."',
    win:'Kael: "Ash. That is all ambition becomes without power. Climb when you have earned the summit."',
    team:[['pyrowolf',14],['ignisaur',15],['stormlord',16]]}},
 {id:'apex', name:'The Apex Pillar', desc:'Where every frequency in Aethelgard converges into one chord.', el:'cosmos', lv:[15,22],
  pool:['boltsaber','slipstream','geoxus','pyrowolf','thornback','obsidian'],
  boss:{name:'Grand Conductor Lyra', title:'Keeper of the Final Chord', sprite:'zephyrus',
    intro:'Lyra: "I have listened to Aethelgard for forty years, waiting for a chord worth conducting. Prove the wait was not wasted."',
    mid:'Lyra: "YES! Do you hear it? The Pillar itself is singing with you! The finale awaits!"',
    lose:'Lyra: "The Final Chord is yours to claim. Aethelgard has a new Grand Conductor. Thank you… for the music."',
    win:'Lyra: "Discordant. Unfinished. The Pillar rejects you — as do I. Listen to the world before you dare conduct it."',
    team:[['zephyrus',18],['terrasunder',18],['ignisaur',19],['neptunian',19],['stormlord',21]]}},
];
const ZONE_BY_ID={}; ZONES.forEach(z=>ZONE_BY_ID[z.id]=z);

function defaultNodes(zoneId){
  const layouts={
    canopy:[[10,72,'grass','Tall Grass'],[26,50,'grass','Tall Grass'],[40,70,'cache','Mystery Cache'],[54,44,'npc','Wandering Conductor'],[68,66,'spring','Healing Spring'],[82,40,'grass','Deep Thicket'],[92,22,'boss','Gatekeeper']],
    grotto:[[8,30,'grass','Fungal Hollow'],[22,55,'cache','Sunken Cache'],[36,32,'grass','Tidepools'],[50,60,'npc','Wandering Conductor'],[64,36,'spring','Thermal Vent'],[78,58,'grass','Black Lagoon'],[92,30,'boss','Gatekeeper']],
    crags:[[8,60,'grass','Ember Fields'],[24,38,'cache','Crag Cache'],[38,62,'grass','Ashen Ridge'],[52,40,'npc','Wandering Conductor'],[66,64,'spring','Magma Spring'],[80,40,'grass','Storm Shelf'],[92,26,'boss','Gatekeeper']],
    apex:[[10,78,'grass','Windward Steps'],[26,58,'cache','Star Cache'],[42,72,'grass','Cloud Terrace'],[58,52,'npc','Elite Conductor'],[74,66,'spring','Pillar Sanctum'],[90,34,'boss','Final Chord']],
  };
  return layouts[zoneId].map((n,i)=>({id:zoneId+'_'+i, x:n[0], y:n[1], type:n[2], label:n[3]}));
}

/* ---------------- NPC trainers for encounter nodes ---------------- */
const NPC_TRAINERS=[
 {name:'Conductor Fen', quip:'"My frequencies are perfectly tuned!"', team:[['seedling',3],['sparky',4]]},
 {name:'Conductor Odessa', quip:'"Feel the rhythm of battle!"', team:[['droplet',4],['gusty',5]]},
 {name:'Conductor Bramble', quip:'"Nature always wins the long game."', team:[['pebble',5],['thornback',6]]},
 {name:'Conductor Volt', quip:'"ZAP! Sorry — force of habit."', team:[['sparky',6],['boltsaber',8]]},
 {name:'Conductor Sela', quip:'"The wind told me I\'d win."', team:[['gusty',7],['slipstream',9]]},
 {name:'Conductor Rook', quip:'"Solid as stone. Stubborn too."', team:[['obsidian',9],['emberkit',8]]},
 {name:'Conductor Vex', quip:'"I conduct chaos, personally."', team:[['pyrowolf',10],['geoxus',11]]},
 {name:'Conductor Halcyon', quip:'"Balance in all frequencies."', team:[['thornback',11],['boltsaber',12],['slipstream',12]]},
 {name:'Elite Conductor Wren', quip:'"The Pillar filters out the unworthy."', team:[['geoxus',14],['pyrowolf',15],['zephyrus',16]]},
 {name:'Elite Conductor Dain', quip:'"Let\'s make some noise."', team:[['terrasunder',15],['ignisaur',16]]},
 {name:'Elite Conductor Sable', quip:'"Silence is also a frequency."', team:[['neptunian',16],['stormlord',17]]},
 {name:'Elite Conductor Mira', quip:'"Rematch? With pleasure."', team:[['bloomgoliath',16],['neptunian',17]]},
];

/* ---------------- Player State ---------------- */
const SAVE_KEY='aetherprism_save_v1';
let P=null; // player profile
let currentZone='canopy';

function newPlayer(){
  return {
    shards:300, items:{prism:5, hyperprism:0, elixir:2, cleanser:1, booster:0},
    party:[], storage:[], zonesUnlocked:['canopy'], clearedBosses:[], completedNodes:[],
    dexSeen:[], dexCaught:[], nextUid:1, battlesWon:0,
  };
}
function saveGame(){ try{ localStorage.setItem(SAVE_KEY, JSON.stringify(P)); }catch(e){} }
function loadGame(){ try{ const s=localStorage.getItem(SAVE_KEY); return s?JSON.parse(s):null; }catch(e){ return null; } }

/* ---------------- Monster factory & math ---------------- */
let UID=1;
function makeMon(id, level){
  const b=RES_BY_ID[id];
  const m={
    uid:P.nextUid++, id, level,
    xp:level*level*level*0 + Math.floor(Math.pow(level,3)*0.6),
    status:null,
    moves:b.learn.filter(l=>l[1]<=level).map(l=>l[0]).slice(-4),
  };
  recalc(m); m.hp=m.maxhp;
  return m;
}
function recalc(m){
  const b=RES_BY_ID[m.id];
  m.maxhp=Math.floor(b.base.hp*2*m.level/100)+m.level+10;
  m.atk=Math.floor(b.base.atk*2*m.level/100)+5;
  m.def=Math.floor(b.base.def*2*m.level/100)+5;
  m.spd=Math.floor(b.base.spd*2*m.level/100)+5;
}
function xpToNext(level){ return Math.pow(level+1,3); }
function healMon(m){ m.hp=m.maxhp; m.status=null; }
function healParty(){ P.party.forEach(healMon); }
function activeParty(){ return P.party.filter(m=>m.active!==false).slice(0,3); }
function ensureActiveDefaults(){ let n=0; P.party.forEach(m=>{ if(m.active!==false && n<3) n++; else if(n>=3) m.active=false; }); }

function gainXP(mon, amount){
  const msgs=[];
  mon.xp+=amount;
  while(mon.xp>=xpToNext(mon.level) && mon.level<50){
    mon.level++;
    const old={atk:mon.atk,def:mon.def,spd:mon.spd,maxhp:mon.maxhp};
    recalc(mon); mon.hp=Math.min(mon.maxhp, mon.hp+(mon.maxhp-old.maxhp));
    SFX.levelUp();
    msgs.push(`${monName(mon)} grew to Level ${mon.level}!`);
    const b=RES_BY_ID[mon.id];
    b.learn.forEach(l=>{ if(l[1]===mon.level && !mon.moves.includes(l[0])){
      if(mon.moves.length>=4) mon.moves.shift();
      mon.moves.push(l[0]); msgs.push(`${monName(mon)} learned ${MOVES[l[0]].name}!`);
    }});
    if(b.evo && mon.level>=b.evoLv){
      const oldName=monName(mon);
      mon.id=b.evo; recalc(mon); mon.hp=mon.maxhp;
      SFX.evolve();
      msgs.push(`✦ ${oldName} resonated and evolved into ${RES_BY_ID[mon.id].name}!`);
      if(!P.dexCaught.includes(mon.id)) P.dexCaught.push(mon.id);
      if(!P.dexSeen.includes(mon.id)) P.dexSeen.push(mon.id);
    }
  }
  return msgs;
}
function monName(m){ return RES_BY_ID[m.id].name; }
function monEl(m){ return RES_BY_ID[m.id].el; }

/* ---------------- UI helpers ---------------- */
const $=s=>document.querySelector(s);
const $$=s=>Array.from(document.querySelectorAll(s));
const wait=ms=>new Promise(r=>setTimeout(r,ms));
const rnd=(a,b)=>a+Math.random()*(b-a);
const ri=(a,b)=>Math.floor(rnd(a,b+1));
const pick=arr=>arr[Math.floor(Math.random()*arr.length)];

let currentScreen='menu';
function showScreen(name){
  SFX.select();
  $$('.screen').forEach(s=>{ s.classList.remove('active'); s.classList.toggle('exit-left', s.id==='screen-'+currentScreen); });
  const el=$('#screen-'+name);
  if(el){ el.classList.remove('exit-left'); el.classList.add('active'); }
  currentScreen=name;
  if(name==='map') renderMap();
  if(name==='dex') renderDex();
  if(name==='shop') renderShop();
  if(name==='party') renderParty();
  if(name==='menu') renderMenu();
}
let toastTimer=null;
function toast(msg){
  const t=$('#toast'); t.textContent=msg; t.classList.add('show');
  clearTimeout(toastTimer); toastTimer=setTimeout(()=>t.classList.remove('show'),2400);
}
function showModal(html, buttons){
  return new Promise(res=>{
    const box=$('#modal-box');
    box.innerHTML=html+`<div class="flex gap-2 justify-end mt-4 flex-wrap" id="modal-btns"></div>`;
    const btns=$('#modal-btns', box)||box.querySelector('#modal-btns');
    buttons.forEach(b=>{
      const el=document.createElement('button');
      el.className='btn '+(b.danger?'btn-danger ':'')+(b.primary?'btn-primary ':'')+'text-sm';
      el.textContent=b.label;
      el.onclick=()=>{ SFX.select(); $('#modal').classList.remove('show'); res(b.value); };
      btns.appendChild(el);
    });
    $('#modal').classList.add('show');
  });
}
function updateHUD(){
  $('#hud-shards').textContent=P.shards;
  const shop=$('#shop-shards'); if(shop) shop.textContent=P.shards;
}
function elementChip(el){ return `<span class="text-[10px] px-2 py-0.5 rounded-full el-chip elg-${el}">${ELEMENTS[el].icon} ${ELEMENTS[el].name}</span>`; }

/* ---------------- Starfield backdrop ---------------- */
(function(){
  const c=$('#starfield'), ctx=c.getContext('2d'); let stars=[];
  function resize(){ c.width=innerWidth; c.height=innerHeight;
    stars=Array.from({length:130},()=>({x:Math.random()*c.width,y:Math.random()*c.height,r:Math.random()*1.6+.3,s:Math.random()*.25+.05,t:Math.random()*6.28}));
  }
  addEventListener('resize',resize); resize();
  (function loop(t){
    ctx.clearRect(0,0,c.width,c.height);
    stars.forEach(s=>{ s.y-=s.s; if(s.y<-4){s.y=c.height+4;s.x=Math.random()*c.width;}
      const a=.35+.35*Math.sin(t/900+s.t);
      ctx.fillStyle=`rgba(190,180,255,${a})`; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,6.28); ctx.fill(); });
    requestAnimationFrame(loop);
  })(0);
})();

/* ---------------- Ambient particles per element ---------------- */
function spawnParticles(containerId, el, count=14){
  const box=document.getElementById(containerId); if(!box) return;
  box.innerHTML='';
  const color=ELEMENTS[el]?ELEMENTS[el].color:'#c084fc';
  for(let i=0;i<count;i++){
    const p=document.createElement('div'); p.className='particle';
    p.style.left=rnd(2,96)+'%'; p.style.bottom=rnd(-10,40)+'%';
    p.style.background=color; p.style.boxShadow=`0 0 8px ${color}`;
    p.style.animationDuration=rnd(3,7)+'s'; p.style.animationDelay=rnd(0,5)+'s';
    if(Math.random()<.4){ p.style.width='4px'; p.style.height='4px'; }
    box.appendChild(p);
  }
}

/* ---------------- Map rendering ---------------- */
function zoneNodeState(zone){
  const nodes=defaultNodes(zone.id);
  return nodes.map(n=>{
    const done=P.completedNodes.includes(n.id);
    const isBoss=n.type==='boss';
    const unlocked=!isBoss || P.zonesUnlocked.includes(zone.id);
    return {...n, done, unlocked};
  });
}
function renderMap(){
  updateHUD();
  const tabs=$('#zone-tabs'); tabs.innerHTML='';
  ZONES.forEach(z=>{
    const unlocked=P.zonesUnlocked.includes(z.id);
    const b=document.createElement('button');
    b.className='tabbtn elg-'+z.el+(currentZone===z.id?' on':'');
    b.innerHTML=`${ELEMENTS[z.el].icon} ${z.name}${unlocked?'':' 🔒'}`;
    b.disabled=!unlocked;
    b.onmouseenter=()=>SFX.hover();
    b.onclick=()=>{ if(!unlocked){toast('Defeat the previous Gatekeeper first!'); SFX.fail(); return;} currentZone=z.id; renderMap(); };
    tabs.appendChild(b);
  });
  const z=ZONE_BY_ID[currentZone];
  $('#zone-title').textContent=z.name.toUpperCase();
  $('#zone-desc').textContent=z.desc+`  ·  Resonant Lv ${z.lv[0]}–${z.lv[1]}`;
  const layer=$('#node-layer'); layer.innerHTML='';
  const svg=$('#trail-svg');
  const nodes=zoneNodeState(z);
  let pts=[];
  nodes.forEach(n=>{
    pts.push([n.x,n.y]);
    const d=document.createElement('div');
    d.className='node'+(n.done?' done':'')+(!n.unlocked?' locked':'');
    const isCurrent=nodes.findIndex(x=>!x.done)===nodes.indexOf(n);
    if(isCurrent && !n.done) d.classList.add('current');
    const icons={grass:'🌿', cache:'🎁', npc:'🧙', spring:'⛲', boss:'👑'};
    d.innerHTML=icons[n.type]+`<span class="nlab">${n.label}</span>`;
    d.style.left=n.x+'%'; d.style.top=n.y+'%';
    if(n.type==='boss') d.classList.add('elg-'+z.el,'pulse-el');
    d.onmouseenter=()=>SFX.hover();
    d.onclick=()=>enterNode(z,n);
    layer.appendChild(d);
  });
  svg.setAttribute('viewBox','0 0 100 100');
  svg.innerHTML=`<polyline points="${pts.map(p=>p.join(',')).join(' ')}" fill="none" stroke="rgba(167,139,250,.35)" stroke-width="0.8" stroke-dasharray="2 2" stroke-linecap="round"/>`;
  spawnParticles('map-particles', z.el, 16);
  // party strip
  const strip=$('#map-party'); strip.innerHTML='';
  P.party.forEach(m=>{
    const b=RES_BY_ID[m.id];
    const d=document.createElement('div');
    d.className='glass-soft elg-'+b.el+' px-3 py-2 flex items-center gap-2 cursor-pointer'+(m.active===false?' opacity-50':'');
    d.innerHTML=`<div class="spr w-9 h-9">${spriteSVG(m.id)}</div>
      <div><div class="text-xs font-bold">${b.name} ${m.active===false?'<span class="text-[9px] text-[color:var(--dim)]">(bench)</span>':''}</div>
      <div class="bar w-20 mt-1 ${m.hp/m.maxhp<.25?'hp-low':m.hp/m.maxhp<.55?'hp-mid':''}"><i style="width:${100*m.hp/m.maxhp}%"></i></div></div>
      <span class="text-[10px] text-[color:var(--dim)]">Lv${m.level}</span>`;
    d.onclick=()=>showScreen('party');
    strip.appendChild(d);
  });
}

/* ---------------- Node interactions ---------------- */
async function enterNode(zone, node){
  if(node.done && node.type!=='spring'){ toast('This node has already been cleared.'); SFX.fail(); return; }
  SFX.select();
  if(node.type==='grass'){ await startBattle({kind:'wild', zone}); }
  else if(node.type==='cache'){ await openCache(zone, node); }
  else if(node.type==='npc'){ const t=pick(NPC_TRAINERS); await startBattle({kind:'trainer', trainer:{...t, team:t.team.map(t2=>makeMon(t2[0],t2[1]))}}); }
  else if(node.type==='spring'){ await useSpring(node); }
  else if(node.type==='boss'){ await bossBattle(zone, node); }
  saveGame(); renderMap();
}
async function openCache(zone, node){
  const shardGain=ri(60,160)+zone.lv[0]*8;
  const itemPool=['prism','prism','elixir','cleanser','hyperprism','booster'];
  const item=pick(itemPool);
  P.shards+=shardGain; P.items[item]=(P.items[item]||0)+1;
  SFX.capture();
  await showModal(`<div class="text-center"><div class="text-5xl mb-3">🎁</div>
    <h3 class="font-pixel text-xs text-amber-300 mb-3">MYSTERY CACHE</h3>
    <p class="text-sm">You found <b class="text-cyan-300">${shardGain} Aether Shards</b><br>and <b>${ITEMS[item].icon} ${ITEMS[item].name}</b>!</p></div>`,
    [{label:'Excellent!', value:1, primary:true}]);
  if(!P.completedNodes.includes(node.id)) P.completedNodes.push(node.id);
}
async function useSpring(node){
  healParty(); SFX.heal();
  await showModal(`<div class="text-center"><div class="text-5xl mb-3">⛲</div>
    <h3 class="font-pixel text-xs text-sky-300 mb-3">HEALING SPRING</h3>
    <p class="text-sm">Pure harmonic waters restore your entire party.<br><b class="text-emerald-300">All Resonants fully healed!</b></p></div>`,
    [{label:'Thank the waters', value:1, primary:true}]);
}

/* =====================================================================
   BATTLE ENGINE
   ===================================================================== */
let B=null;

function battleBanner(txt){ $('#battle-banner').innerHTML=txt; }
function setPanel(elClass, mon){
  const b=RES_BY_ID[mon.id];
  const pct=100*Math.max(0,mon.hp)/mon.maxhp;
  const bar=$(elClass+'-hpbar');
  bar.className='bar mt-2 '+(pct<25?'hp-low':pct<55?'hp-mid':'');
  bar.querySelector('i').style.width=pct+'%';
  $(elClass+'-name').textContent=b.name;
  $(elClass+'-lv').textContent='Lv '+mon.level;
  const chip=$(elClass+'-type');
  chip.className='text-[10px] px-2 py-0.5 rounded-full el-chip elg-'+b.el;
  chip.textContent=ELEMENTS[b.el].icon+' '+ELEMENTS[b.el].name;
  const st=$(elClass+'-status');
  st.textContent=mon.status?({burn:'🔥 BURN',sap:'🌿 SAP',stun:'⚡ STUN'})[mon.status]:'';
  const panel=$(elClass+'-panel');
  panel.className='glass p-3 relative overflow-hidden elg-'+b.el;
}
function renderBattle(){
  setPanel('#enemy', B.enemy);
  setPanel('#player', pMon());
  const pm=pMon();
  $('#player-sprite').innerHTML=spriteSVG(pm.id);
  $('#enemy-sprite').innerHTML=spriteSVG(B.enemy.id);
  $('#player-sprite').firstChild.classList.add('sprite-bob');
  $('#enemy-sprite').firstChild.classList.add('sprite-bob','fast');
  $('#player-hptext').textContent=`${Math.max(0,pm.hp)} / ${pm.maxhp} HP`;
  const xpPct=100*(pm.xp - Math.pow(pm.level,3)*0.6 >=0 ? 0:0);
  const cur=pm.xp, prev=Math.pow(pm.level,3)*0.6, next=xpToNext(pm.level);
  $('#player-xpbar').querySelector('i').style.width=Math.min(100,100*(cur-prev)/(next-prev))+'%';
  spawnParticles('enemy-particles', monEl(B.enemy), 10);
  spawnParticles('player-particles', monEl(pm), 10);
}
function pMon(){ return activeParty()[B.pIdx]; }
function floatNum(side, txt, color){
  const host=$(side==='enemy'?'#enemy-sprite':'#player-sprite');
  const d=document.createElement('div');
  d.className='float-num absolute left-1/2 top-2 -translate-x-1/2 text-lg z-20';
  d.style.color=color||'#fff'; d.textContent=txt;
  host.style.position='relative'; host.appendChild(d);
  setTimeout(()=>d.remove(),1000);
}
function flashAndShake(side, crit){
  const spr=$(side==='enemy'?'#enemy-sprite':'#player-sprite');
  spr.classList.remove('hitflash'); void spr.offsetWidth; spr.classList.add('hitflash');
  if(crit||side==='player'){ const pnl=$(side==='enemy'?'#enemy-panel':'#player-panel');
    pnl.classList.remove('shake'); void pnl.offsetWidth; pnl.classList.add('shake'); }
}
function showMainActions(){ $('#action-main').classList.remove('hidden'); $('#action-sub').classList.add('hidden'); $('#action-back').classList.add('hidden'); }
function showSubActions(html){ $('#action-main').classList.add('hidden'); const sub=$('#action-sub'); sub.classList.remove('hidden'); sub.innerHTML=html; $('#action-back').classList.remove('hidden'); }

async function startBattle(opts){
  B={kind:opts.kind, zone:opts.zone||null, trainer:opts.trainer||null, tIdx:0, pIdx:0, booster:false, over:false, busy:true, node:opts.node||null};
  if(opts.kind==='wild'){
    const z=opts.zone;
    B.enemy=makeMon(pick(z.pool), ri(z.lv[0], z.lv[1]));
    if(!P.dexSeen.includes(B.enemy.id)) P.dexSeen.push(B.enemy.id);
    battleBanner(`A wild <b>${monName(B.enemy)}</b> (Lv ${B.enemy.level}) resonates from the frequencies!`);
  } else {
    B.enemy=B.trainer.team[0];
    if(!P.dexSeen.includes(B.enemy.id)) P.dexSeen.push(B.enemy.id);
    battleBanner(`<b>${B.trainer.name}</b>: ${B.trainer.quip}`);
    if(B.kind==='boss') SFX.boss();
  }
  const first=activeParty().findIndex(m=>m.hp>0);
  if(first<0){ toast('Your whole party has fainted!'); healParty(); }
  B.pIdx=Math.max(0,first);
  showScreen('battle');
  renderBattle();
  await wait(900);
  B.busy=false; showMainActions();
}

/* ---------- damage ---------- */
function calcDamage(att, def, move){
  const m=MOVES[move];
  const eff=typeMod(m.el, monEl(def));
  let atk=att.atk;
  if(att.status==='burn' && m.el!=='cosmos') atk*=0.75;
  const stab = monEl(att)===m.el ? 1.25 : 1;
  const crit=Math.random()<0.10 ? 1.5 : 1;
  const base=(((2*att.level/5+2)*m.pow*(atk/def.def))/50+2);
  const dmg=Math.max(1, Math.floor(base*eff*stab*crit*rnd(0.85,1.00)));
  return {dmg, eff, crit:crit>1};
}
function tryStatus(target, moveEl){
  if(target.status || Math.random()>0.15) return null;
  const map={pyro:'burn', volt:'stun', flora:'sap'};
  const s=map[moveEl]; if(!s) return null;
  target.status=s; return s;
}
async function endOfTurnStatus(mon, side){
  if(mon.status==='burn'||mon.status==='sap'){
    const frac=mon.status==='burn'?16:12;
    const d=Math.max(1,Math.floor(mon.maxhp/frac));
    mon.hp=Math.max(0,mon.hp-d);
    floatNum(side,'-'+d,'#f87171');
    battleBanner(`${monName(mon)} is hurt by ${mon.status==='burn'?'its burn':'the sapping spores'}!`);
    renderBattle(); await wait(650);
    if(mon.hp<=0) await handleFaint(side);
  }
}

async function handleFaint(side){
  const mon = side==='enemy'?B.enemy:pMon();
  const spr=$(side==='enemy'?'#enemy-sprite':'#player-sprite');
  spr.firstChild.classList.add('faint');
  SFX.fail();
  battleBanner(`${monName(mon)} fainted!`);
  await wait(800);
  if(side==='enemy'){
    // reward & next enemy
    const isTrainer=B.kind!=='wild';
    const xp=Math.floor(B.enemy.level*14*(isTrainer?1.4:1));
    const shards=Math.floor(B.enemy.level*(isTrainer?11:7)+ri(0,10));
    P.shards+=shards; P.battlesWon++;
    battleBanner(`${monName(B.enemy)} is defeated! +${xp} XP, +${shards} 💎`);
    const pm=pMon();
    const msgs=gainXP(pm, xp);
    renderBattle(); await wait(700);
    for(const msg of msgs){ battleBanner('✦ '+msg); SFX.levelUp(); await wait(850); renderBattle(); }
    // trainer next mon?
    if(isTrainer){
      B.tIdx++;
      const next=B.trainer.team[B.tIdx];
      if(next){
        if(!P.dexSeen.includes(next.id)) P.dexSeen.push(next.id);
        B.enemy=next;
        if(B.kind==='boss' && B.tIdx===B.trainer.team.length-1){
          const bd=ZONE_BY_ID[B.zone.id].boss;
          battleBanner(`<b>${bd.name}</b>: ${bd.mid}`); SFX.boss();
        } else battleBanner(`<b>${B.trainer.name}</b> sends out <b>${monName(next)}</b>!`);
        renderBattle(); await wait(1000);
        B.busy=false; showMainActions(); return false;
      }
      // trainer defeated
      await trainerDefeated();
      return true;
    }
    await wildDefeated();
    return true;
  } else {
    const next=activeParty().findIndex(m=>m.hp>0);
    if(next<0){ await playerDefeated(); return true; }
    B.pIdx=next;
    battleBanner(`Go, <b>${monName(pMon())}</b>!`);
    renderBattle(); await wait(800);
    B.busy=false; showMainActions(); return false;
  }
}

async function wildDefeated(){
  B.over=true;
  if(B.node && !P.completedNodes.includes(B.node.id)) P.completedNodes.push(B.node.id);
  saveGame();
  await wait(400);
  endBattle(`Victory! The wild resonance fades. You return to the trail.`);
}
async function trainerDefeated(){
  B.over=true;
  if(B.node && !P.completedNodes.includes(B.node.id)) P.completedNodes.push(B.node.id);
  const bonus=ri(120,260);
  P.shards+=bonus;
  SFX.capture();
  if(B.kind==='boss'){
    const bd=ZONE_BY_ID[B.zone.id].boss;
    if(!P.clearedBosses.includes(B.zone.id)) P.clearedBosses.push(B.zone.id);
    const zIdx=ZONES.findIndex(z=>z.id===B.zone.id);
    const nextZone=ZONES[zIdx+1];
    if(nextZone && !P.zonesUnlocked.includes(nextZone.id)) P.zonesUnlocked.push(nextZone.id);
    saveGame();
    const isFinal=B.zone.id==='apex';
    await showModal(`<div class="text-center">
      <div class="text-5xl mb-3">${isFinal?'👑':'🏆'}</div>
      <h3 class="font-pixel text-xs text-amber-300 mb-3">${isFinal?'THE FINAL CHORD RESOLVES':'GATEKEEPER DEFEATED'}</h3>
      <p class="text-sm italic leading-relaxed">"${bd.lose}"</p>
      <p class="text-xs text-cyan-300 mt-3">+${bonus} bonus Aether Shards</p>
      ${isFinal?'<p class="text-xs text-violet-300 mt-3">You are the Grand Conductor of Aethelgard. The world hums your name — keep exploring, catching, and evolving!</p>'
        :`<p class="text-xs text-emerald-300 mt-3">${nextZone?nextZone.name+' is now unlocked!':''}</p>`}</div>`,
      [{label:isFinal?'Encore!':'Onward!', value:1, primary:true}]);
  } else {
    saveGame();
    await showModal(`<div class="text-center"><div class="text-4xl mb-2">🏆</div>
      <h3 class="font-pixel text-xs text-amber-300 mb-2">BATTLE WON</h3>
      <p class="text-sm">${B.trainer.name} is defeated!<br>Bonus: <b class="text-cyan-300">${bonus} Aether Shards</b></p></div>`,
      [{label:'Continue', value:1, primary:true}]);
  }
  endBattle('You return to the trail victorious.');
}
async function playerDefeated(){
  B.over=true;
  battleBanner('All your Resonants have fainted…');
  await wait(800);
  let extra='';
  if(B.kind==='boss'){
    const bd=ZONE_BY_ID[B.zone.id].boss;
    extra=`<p class="text-sm italic leading-relaxed mt-2">"${bd.win}"</p>`;
  }
  await showModal(`<div class="text-center"><div class="text-4xl mb-2">💫</div>
    <h3 class="font-pixel text-xs text-rose-300 mb-2">RESONANCE LOST</h3>
    <p class="text-sm">You retreat to the nearest spring.<br>Your party is restored — but the node remains unclaimed.</p>${extra}</div>`,
    [{label:'Recover', value:1, primary:true}]);
  healParty(); saveGame();
  endBattle('');
}
function endBattle(msg){
  B=null;
  showScreen('map');
  if(msg) toast(msg);
}

/* ---------- player actions ---------- */
async function playerMove(moveId){
  if(B.busy||B.over) return;
  B.busy=true; showMainActions();
  const pm=pMon(), em=B.enemy;
  const mv=MOVES[moveId];
  // stun check
  if(pm.status==='stun' && Math.random()<0.25){
    battleBanner(`${monName(pm)} is stunned and can't move!`); SFX.weak();
    await wait(900);
  } else {
    battleBanner(`${monName(pm)} uses <b class="el-text elg-${mv.el}">${mv.name}</b>!`);
    await wait(500);
    if(Math.random()*100>mv.acc){
      battleBanner(`…but it missed!`); SFX.weak(); await wait(800);
    } else {
      const {dmg,eff,crit}=calcDamage(pm,em,moveId);
      em.hp=Math.max(0,em.hp-dmg);
      if(eff>1){ SFX.superEff(); battleBanner(`It's harmonically SUPER EFFECTIVE!`); }
      else if(eff<1){ SFX.weak(); battleBanner(`It's not very resonant…`); }
      else if(mv.el==='cosmos') SFX.hitElem(); else SFX.hitPhys();
      flashAndShake('enemy',crit);
      floatNum('enemy','-'+dmg, crit?'#fde047':eff>1?'#f87171':'#fff');
      if(crit) battleBanner(`A CRITICAL frequency strike! ${dmg} damage!`);
      renderBattle();
      const st=tryStatus(em, mv.el);
      if(st){ battleBanner(`${monName(em)} is afflicted with ${st.toUpperCase()}!`); renderBattle(); }
      await wait(900);
      if(em.hp<=0){ const done=await handleFaint('enemy'); if(done||!B) return; if(B.over) return; B.busy=false; return; }
    }
  }
  await enemyTurn();
}
async function enemyTurn(){
  if(!B||B.over) return;
  const em=B.enemy, pm=pMon();
  if(em.status==='stun' && Math.random()<0.25){
    battleBanner(`Wild ${monName(em)} is stunned and can't move!`); SFX.weak();
    await wait(900);
  } else {
    const moveId=pick(em.moves);
    const mv=MOVES[moveId];
    battleBanner(`${monName(em)} uses <b class="el-text elg-${mv.el}">${mv.name}</b>!`);
    await wait(500);
    if(Math.random()*100>mv.acc){
      battleBanner(`…but it missed!`); await wait(800);
    } else {
      const {dmg,eff,crit}=calcDamage(em,pm,moveId);
      pm.hp=Math.max(0,pm.hp-dmg);
      if(eff>1){ SFX.superEff(); battleBanner(`${monName(pm)} takes massive resonance damage!`); }
      else if(eff<1){ SFX.weak(); }
      else SFX.hitPhys();
      flashAndShake('player',crit);
      floatNum('player','-'+dmg, crit?'#fde047':'#f87171');
      renderBattle();
      const st=tryStatus(pm, mv.el);
      if(st){ battleBanner(`${monName(pm)} is afflicted with ${st.toUpperCase()}!`); renderBattle(); }
      await wait(900);
      if(pm.hp<=0){ const done=await handleFaint('player'); if(done||!B) return; if(B.over) return; B.busy=false; return; }
    }
  }
  await endOfTurnStatus(em,'enemy'); if(!B||B.over) return;
  await endOfTurnStatus(pm,'player'); if(!B||B.over) return;
  B.busy=false; showMainActions();
  battleBanner('What will <b>'+monName(pMon())+'</b> do?');
}

/* ---------- prisms (capture) ---------- */
function captureChance(mon, prismKey){
  const b=RES_BY_ID[mon.id];
  const hpFactor=(3*mon.maxhp-2*mon.hp)/(3*mon.maxhp);
  const tier=prismKey==='hyperprism'?1.9:1.0;
  const status=mon.status?1.35:1.0;
  const boost=B&&B.booster?1.4:1.0;
  return Math.min(0.95, hpFactor*(b.catch/255)*tier*status*boost);
}
async function throwPrism(prismKey){
  if(B.busy||B.over) return;
  if(B.kind!=='wild'){ toast('You can\'t capture another Conductor\'s Resonant!'); SFX.fail(); return; }
  if((P.items[prismKey]||0)<=0){ toast('None left! Visit the Camp Shop.'); SFX.fail(); return; }
  B.busy=true; showMainActions();
  P.items[prismKey]--;
  const em=B.enemy;
  battleBanner(`You hurl a ${ITEMS[prismKey].name} at ${monName(em)}!`);
  SFX.select();
  await wait(700);
  const chance=captureChance(em,prismKey);
  B.booster=false;
  if(Math.random()<chance){
    SFX.capture();
    battleBanner(`✦ The prism harmonizes! <b>${monName(em)}</b> was captured!`);
    em.status=null;
    P.dexCaught.push(em.id);
    if(P.party.length<6){ P.party.push(em); toast(`${monName(em)} joined your party!`); }
    else { P.storage.push(em); toast(`${monName(em)} was sent to storage (party full).`); }
    if(B.node && !P.completedNodes.includes(B.node.id)) P.completedNodes.push(B.node.id);
    saveGame();
    await wait(1200);
    endBattle(`${monName(em)} now resonates with you!`);
  } else {
    SFX.fail();
    battleBanner(`The prism shatters… ${monName(em)} broke free! (${Math.round(chance*100)}% harmonic match)`);
    await wait(1000);
    await enemyTurn();
  }
}

/* ---------- items in battle ---------- */
async function useBattleItem(key){
  if(B.busy||B.over) return;
  if((P.items[key]||0)<=0){ toast('None left!'); SFX.fail(); return; }
  const pm=pMon();
  if(key==='elixir'){
    if(pm.hp>=pm.maxhp){ toast(pm.name+' is already at full HP!'); SFX.fail(); return; }
    B.busy=true; showMainActions();
    P.items[key]--;
    const heal=Math.floor(pm.maxhp*0.5);
    pm.hp=Math.min(pm.maxhp,pm.hp+heal);
    SFX.heal(); floatNum('player','+'+heal,'#4ade80');
    battleBanner(`${monName(pm)} restores ${heal} HP with the Core Restorer!`);
    renderBattle(); await wait(900);
    await enemyTurn();
  } else if(key==='cleanser'){
    if(!pm.status){ toast('No status to cleanse!'); SFX.fail(); return; }
    B.busy=true; showMainActions();
    P.items[key]--; pm.status=null;
    SFX.heal(); battleBanner(`${monName(pm)}'s resonance is purified!`);
    renderBattle(); await wait(900);
    await enemyTurn();
  } else if(key==='booster'){
    B.busy=true; showMainActions();
    P.items[key]--; B.booster=true;
    SFX.select(); battleBanner('Prism Booster armed! Your next throw is supercharged.');
    await wait(900);
    await enemyTurn();
  } else if(key==='prism'||key==='hyperprism'){
    await throwPrism(key);
  }
}

/* ---------- swap ---------- */
async function swapTo(idx){
  if(B.busy||B.over) return;
  const list=activeParty();
  if(list[idx]===pMon()){ toast('Already in battle!'); return; }
  if(list[idx].hp<=0){ toast('That Resonant has fainted!'); SFX.fail(); return; }
  B.busy=true; showMainActions();
  battleBanner(`You exchange frequencies… Go, <b>${monName(list[idx])}</b>!`);
  SFX.select();
  B.pIdx=idx;
  renderBattle(); await wait(900);
  await enemyTurn();
}

/* ---------- action menus ---------- */
function openMoves(){
  SFX.select();
  const pm=pMon();
  let html='<div class="grid sm:grid-cols-2 gap-2">';
  pm.moves.forEach(mid=>{
    const m=MOVES[mid];
    html+=`<button class="btn movebtn elg-${m.el}" data-move="${mid}">
      <span><b>${m.name}</b><br><span class="text-[10px] text-[color:var(--dim)]">${m.desc}</span></span>
      <span class="text-right text-xs"><span class="el-text elg-${m.el}">${ELEMENTS[m.el].icon} ${ELEMENTS[m.el].name}</span><br>PWR ${m.pow} · ${m.acc}%</span></button>`;
  });
  html+='</div>';
  showSubActions(html);
  $$('#action-sub [data-move]').forEach(b=>b.onclick=()=>playerMove(b.dataset.move));
}
function openPrisms(){
  SFX.select();
  let html='<div class="grid sm:grid-cols-2 gap-2">';
  ['prism','hyperprism'].forEach(k=>{
    html+=`<button class="btn movebtn" data-item="${k}" ${(P.items[k]||0)<=0?'disabled':''}>
      <span><b>${ITEMS[k].icon} ${ITEMS[k].name}</b><br><span class="text-[10px] text-[color:var(--dim)]">Owned: ${P.items[k]||0}</span></span>
      <span class="text-xs text-cyan-300">${B.kind==='wild'?'~'+Math.round(captureChance(B.enemy,k)*100)+'% match':'—'}</span></button>`;
  });
  html+='</div>';
  showSubActions(html);
  $$('#action-sub [data-item]').forEach(b=>b.onclick=()=>throwPrism(b.dataset.item));
}
function openBag(){
  SFX.select();
  const keys=['elixir','cleanser','booster','prism','hyperprism'];
  let html='<div class="grid sm:grid-cols-2 gap-2">';
  keys.forEach(k=>{
    html+=`<button class="btn movebtn" data-item="${k}" ${(P.items[k]||0)<=0?'disabled':''}>
      <span><b>${ITEMS[k].icon} ${ITEMS[k].name}</b><br><span class="text-[10px] text-[color:var(--dim)]">${ITEMS[k].desc}</span></span>
      <span class="text-xs text-cyan-300">×${P.items[k]||0}</span></button>`;
  });
  html+='</div>';
  showSubActions(html);
  $$('#action-sub [data-item]').forEach(b=>b.onclick=()=>useBattleItem(b.dataset.item));
}
function openSwap(){
  SFX.select();
  const list=activeParty();
  let html='<div class="grid sm:grid-cols-3 gap-2">';
  list.forEach((m,i)=>{
    const b=RES_BY_ID[m.id];
    const pct=100*m.hp/m.maxhp;
    html+=`<button class="btn movebtn elg-${b.el}" data-idx="${i}" ${m.hp<=0?'disabled':''}>
      <span class="flex items-center gap-2"><span class="spr w-8 h-8">${spriteSVG(m.id)}</span>
      <span class="text-xs"><b>${b.name}</b><br>Lv ${m.level}</span></span>
      <span class="text-[10px] text-right">${m.hp}/${m.maxhp} HP<br>${m===pMon()?'<b class="text-emerald-300">ACTIVE</b>':m.hp<=0?'FAINTED':''}</span></button>`;
  });
  html+='</div>';
  showSubActions(html);
  $$('#action-sub [data-idx]').forEach(b=>b.onclick=()=>swapTo(+b.dataset.idx));
}

/* ---------- boss battle ---------- */
async function bossBattle(zone, node){
  const bd=zone.boss;
  const trainer={name:bd.name, quip:bd.intro, team:bd.team.map(t=>makeMon(t[0],t[1]))};
  await showModal(`<div class="text-center">
    <div class="text-5xl mb-3">👑</div>
    <h3 class="font-pixel text-xs text-amber-300 mb-2">GATEKEEPER</h3>
    <p class="text-sm italic leading-relaxed">"${bd.intro}"</p>
    <p class="text-xs text-[color:var(--dim)] mt-2">${bd.name} — ${bd.title}<br>Team of ${bd.team.length} · Lv ${bd.team.map(t=>t[1]).join(', ')}</p></div>`,
    [{label:'Accept the Challenge', value:1, primary:true},{label:'Not Yet', value:0}]);
  await startBattle({kind:'boss', zone, node, trainer});
}

/* =====================================================================
   DEX / SHOP / PARTY / MENU
   ===================================================================== */
function renderDex(){
  const grid=$('#dex-grid'); grid.innerHTML='';
  const caught=P.dexCaught.length;
  $('#dex-count').textContent=`${caught} / ${RESONANTS.length} captured · ${P.dexSeen.length} observed`;
  RESONANTS.forEach(r=>{
    const seen=P.dexSeen.includes(r.id), isCaught=P.dexCaught.includes(r.id);
    const card=document.createElement('div');
    card.className='glass p-3 dexcard elg-'+r.el+(seen?'':' unknown');
    card.innerHTML=`
      <div class="flex items-center justify-between mb-1">
        <span class="text-[10px] text-[color:var(--dim)]">#${String(RESONANTS.indexOf(r)+1).padStart(3,'0')}</span>
        <span class="text-xs">${isCaught?'💠':seen?'👁':'?'}</span>
      </div>
      <div class="spr w-20 h-20 mx-auto">${spriteSVG(r.id)}</div>
      <div class="text-center mt-1 text-sm font-bold">${seen?r.name:'???'}</div>
      <div class="text-center mt-1">${seen?elementChip(r.el):''}</div>
      ${seen?`<div class="text-[10px] text-[color:var(--dim)] mt-2 grid grid-cols-4 gap-1 text-center">
        <span>HP ${r.base.hp}</span><span>ATK ${r.base.atk}</span><span>DEF ${r.base.def}</span><span>SPD ${r.base.spd}</span></div>
      <div class="text-[10px] text-[color:var(--dim)] mt-1 text-center">${r.evo?('Evolves → '+RES_BY_ID[r.evo].name+' @ Lv '+r.evoLv):'Final form'}</div>`:''}`;
    card.onclick=()=>{ if(!seen){ SFX.fail(); return; } SFX.select();
      showModal(`<div class="text-center">
        <div class="spr w-32 h-32 mx-auto elg-${r.el}">${spriteSVG(r.id)}</div>
        <h3 class="font-pixel text-sm mt-2 el-text elg-${r.el}">${r.name}</h3>
        <div class="mt-2">${elementChip(r.el)} <span class="text-xs text-[color:var(--dim)]">Tier ${r.tier}</span></div>
        <div class="text-xs mt-3 grid grid-cols-2 gap-1 text-left max-w-[240px] mx-auto">
          <span>❤ HP ${r.base.hp}</span><span>⚔ ATK ${r.base.atk}</span><span>🛡 DEF ${r.base.def}</span><span>💨 SPD ${r.base.spd}</span></div>
        <div class="text-xs mt-3 text-left">
          <div class="font-bold text-violet-300 mb-1">Movepool</div>
          ${r.learn.map(l=>`<div class="flex justify-between text-[11px]"><span>${MOVES[l[0]].name}</span><span class="text-[color:var(--dim)]">Lv${l[1]} · ${MOVES[l[0]].pow}pwr</span></div>`).join('')}
        </div>
        <div class="text-[11px] text-[color:var(--dim)] mt-2">${r.evo?'Evolves into '+RES_BY_ID[r.evo].name+' at Lv '+r.evoLv+'.':'This Resonant has reached its final frequency.'}</div>
      </div>`, [{label:'Close', value:1, primary:true}]);
    };
    grid.appendChild(card);
  });
}

function renderShop(){
  updateHUD();
  const grid=$('#shop-grid'); grid.innerHTML='';
  Object.entries(ITEMS).forEach(([k,it])=>{
    const card=document.createElement('div');
    card.className='glass p-4 flex items-center gap-3';
    card.innerHTML=`<div class="text-3xl">${it.icon}</div>
      <div class="flex-1 min-w-0">
        <div class="font-bold text-sm">${it.name}</div>
        <div class="text-[11px] text-[color:var(--dim)]">${it.desc}</div>
        <div class="text-[11px] mt-1">Owned: <b>${P.items[k]||0}</b></div>
      </div>
      <button class="btn text-xs ${P.shards>=it.price?'btn-primary':''}" data-buy="${k}" ${P.shards<it.price?'disabled':''}>💎 ${it.price}</button>`;
    grid.appendChild(card);
  });
  $$('#shop-grid [data-buy]').forEach(b=>b.onclick=()=>{
    const k=b.dataset.buy, it=ITEMS[k];
    if(P.shards<it.price){ SFX.fail(); return; }
    P.shards-=it.price; P.items[k]=(P.items[k]||0)+1;
    SFX.capture(); toast(`Purchased ${it.name}!`); saveGame(); renderShop();
  });
}

function renderParty(){
  const grid=$('#party-grid'); grid.innerHTML='';
  if(P.party.length===0){ grid.innerHTML='<div class="glass p-6 text-center text-sm text-[color:var(--dim)]">No Resonants yet — venture into the tall grass!</div>'; return; }
  P.party.forEach((m,i)=>{
    const b=RES_BY_ID[m.id];
    const pct=100*m.hp/m.maxhp;
    const next=xpToNext(m.level), prev=Math.pow(m.level,3)*0.6;
    const card=document.createElement('div');
    card.className='glass p-4 elg-'+b.el;
    card.innerHTML=`
      <div class="flex gap-3 items-center">
        <div class="spr w-20 h-20">${spriteSVG(m.id)}</div>
        <div class="flex-1 min-w-0">
          <div class="flex items-center gap-2 flex-wrap">
            <b>${b.name}</b> ${elementChip(b.el)}
            ${m.active===false?'<span class="text-[9px] px-2 py-0.5 rounded-full bg-slate-700/60 text-slate-300">BENCH</span>':'<span class="text-[9px] px-2 py-0.5 rounded-full el-chip elg-'+b.el+'">ACTIVE</span>'}
          </div>
          <div class="text-[11px] text-[color:var(--dim)] mt-1">Lv ${m.level} · ATK ${m.atk} · DEF ${m.def} · SPD ${m.spd}</div>
          <div class="bar mt-2 ${pct<25?'hp-low':pct<55?'hp-mid':''}"><i style="width:${pct}%"></i></div>
          <div class="text-[10px] text-[color:var(--dim)] mt-1">${m.hp}/${m.maxhp} HP</div>
          <div class="bar xp mt-1"><i style="width:${Math.min(100,100*(m.xp-prev)/(next-prev))}%"></i></div>
          <div class="text-[10px] text-[color:var(--dim)] mt-0.5">XP ${m.xp} / ${next}</div>
          ${m.status?`<div class="text-[10px] text-fuchsia-300 mt-1">Status: ${m.status.toUpperCase()}</div>`:''}
        </div>
      </div>
      <div class="text-[11px] mt-2 flex flex-wrap gap-1">
        ${m.moves.map(mid=>`<span class="px-2 py-0.5 rounded-md text-[10px] el-chip elg-${MOVES[mid].el}">${MOVES[mid].name} ${MOVES[mid].pow}p</span>`).join('')}
      </div>
      <div class="flex gap-2 mt-3">
        <button class="btn text-xs flex-1" data-heal="${i}" ${P.items.elixir>0 && m.hp<m.maxhp?'':'disabled'}>🧪 Elixir (${P.items.elixir||0})</button>
        <button class="btn text-xs flex-1" data-toggle="${i}">${m.active===false?'➕ Set Active':'➖ To Bench'}</button>
      </div>`;
    grid.appendChild(card);
  });
  $$('#party-grid [data-heal]').forEach(b=>b.onclick=()=>{
    const m=P.party[+b.dataset.heal];
    if((P.items.elixir||0)<=0||m.hp>=m.maxhp) return;
    P.items.elixir--;
    const heal=Math.floor(m.maxhp*0.5); m.hp=Math.min(m.maxhp,m.hp+heal);
    SFX.heal(); toast(`${monName(m)} restored ${heal} HP!`); saveGame(); renderParty();
  });
  $$('#party-grid [data-toggle]').forEach(b=>b.onclick=()=>{
    const m=P.party[+b.dataset.toggle];
    if(m.active===false){
      const actives=P.party.filter(x=>x.active!==false).length;
      if(actives>=3){ toast('Active party is full (max 3)!'); SFX.fail(); return; }
      m.active=true; SFX.select();
    } else {
      if(P.party.filter(x=>x.active!==false).length<=1){ toast('You need at least one active Resonant!'); SFX.fail(); return; }
      m.active=false; SFX.back();
    }
    saveGame(); renderParty();
  });
}

function renderMenu(){
  $('#btn-continue').style.display=P&&P.party.length?'':'none';
}

/* ---------------- New game / starter ---------------- */
const STARTERS=['emberkit','droplet','seedling','sparky','gusty','pebble'];
async function chooseStarter(){
  return new Promise(res=>{
    const box=$('#modal-box');
    box.innerHTML=`<div class="text-center">
      <h3 class="font-pixel text-xs text-violet-200 mb-2">CHOOSE YOUR FIRST RESONANT</h3>
      <p class="text-xs text-[color:var(--dim)] mb-4">Six frequencies answer your tuning fork. One will become your partner.</p>
      <div class="grid grid-cols-3 gap-2" id="starter-grid"></div></div>`;
    const g=box.querySelector('#starter-grid');
    STARTERS.forEach(id=>{
      const r=RES_BY_ID[id];
      const d=document.createElement('button');
      d.className='glass-soft p-2 elg-'+r.el+' dexcard';
      d.innerHTML=`<div class="spr w-14 h-14 mx-auto">${spriteSVG(id)}</div>
        <div class="text-[11px] font-bold mt-1">${r.name}</div>
        <div class="text-[9px] el-text elg-${r.el}">${ELEMENTS[r.el].icon} ${ELEMENTS[r.el].name}</div>`;
      d.onmouseenter=()=>SFX.hover();
      d.onclick=()=>{ SFX.capture(); $('#modal').classList.remove('show'); res(id); };
      g.appendChild(d);
    });
    $('#modal').classList.add('show');
  });
}
async function newGame(){
  P=newPlayer();
  const starter=await chooseStarter();
  const mon=makeMon(starter,5);
  mon.active=true;
  P.party.push(mon);
  P.dexSeen.push(starter); P.dexCaught.push(starter);
  saveGame();
  toast(`${RES_BY_ID[starter].name} joins your journey!`);
  currentZone='canopy';
  showScreen('map');
}

/* ---------------- Wiring ---------------- */
function wire(){
  $('#btn-newgame').onclick=async()=>{ SFX.select();
    if(loadGame()){ const ok=await showModal('<p class="text-sm text-center">Starting a new journey will <b class="text-rose-300">overwrite your existing save</b>. Continue?</p>',
      [{label:'Overwrite', value:1, danger:true},{label:'Cancel', value:0}]);
      if(!ok) return; }
    newGame();
  };
  $('#btn-continue').onclick=()=>{ SFX.select(); const s=loadGame(); if(s){ P=s; ensureActiveDefaults(); showScreen('map'); } else { toast('No save found — start a new journey!'); SFX.fail(); } };
  $('#btn-menu-dex').onclick=()=>{ if(!P){ P=loadGame()||newPlayer(); } showScreen('dex'); };
  $('#btn-reset').onclick=async()=>{ SFX.select();
    const ok=await showModal('<p class="text-sm text-center">Erase <b class="text-rose-300">all progress</b> permanently?</p>',
      [{label:'Erase Everything', value:1, danger:true},{label:'Keep Progress', value:0}]);
    if(ok){ localStorage.removeItem(SAVE_KEY); P=null; toast('Progress erased.'); renderMenu(); }
  };
  $$('[data-nav]').forEach(b=>b.onclick=()=>{
    const t=b.dataset.nav; SFX.select();
    if(t==='menu'){ showScreen('menu'); return; }
    if(t==='back'){ showScreen(P&&P.party.length?'map':'menu'); return; }
    showScreen(t);
  });
  $('#action-back').onclick=()=>{ SFX.back(); if(B&&!B.busy) showMainActions(); };
  $$('#action-main [data-act]').forEach(b=>{
    b.onmouseenter=()=>SFX.hover();
    b.onclick=()=>{ if(!B||B.busy||B.over) return;
      const a=b.dataset.act;
      if(a==='attack') openMoves();
      else if(a==='prism') openPrisms();
      else if(a==='swap') openSwap();
      else if(a==='bag') openBag();
    };
  });
  // generic hover sound on buttons
  document.addEventListener('mouseover',e=>{ if(e.target.closest('.btn,.tabbtn,.node')) SFX.hover(); }, {passive:true});
  // unlock audio on first interaction
  const unlock=()=>{ SFX.ensure(); document.removeEventListener('pointerdown',unlock); };
  document.addEventListener('pointerdown',unlock);
}

/* ---------------- Boot ---------------- */
function boot(){
  wire();
  P=loadGame();
  renderMenu();
  if(P){ ensureActiveDefaults(); $('#btn-continue').textContent='▶ Continue — Lv '+Math.max(...P.party.map(m=>m.level),1)+' partner'; }
}
boot();
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"d9c0bc50-10c5-4dd3-93f6-99edee702bdf","tokensIn":1656604,"tokensOut":38093,"tokensTotal":1694697,"cost":0.04673786499999999,"turns":37,"toolCalls":37,"failedToolCalls":0,"timestamp":"2026-08-28T04:53:53.445Z"} -->