← Creature Collection results

Creature Collection

muse spark 1.2 contributor · full

Initial view of Creature Collection

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 70,933 bytes · SHA-256 3d9ffb5c913b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title>ATRIUM OF THREADS — Binding Chronicle</title>
<style>
:root{
  --bg:#0a0f1e;
  --panel:rgba(18,26,48,0.72);
  --glass:rgba(255,255,255,0.08);
  --border:rgba(255,255,255,0.12);
  --accent:#7af0ff;
  --accent2:#ff7ad6;
  --accent3:#ffe27a;
  --text:#eaf2ff;
  --muted:#8ea0c2;
  --pyra:#ff6b4a; --aqua:#4ac8ff; --sylv:#4ade7a; --zephyr:#a78bfa; --lithos:#f59e42; --lumen:#ffe27a; --null:#6b7280;
}
*{box-sizing:border-box;margin:0;padding:0;font-family:Inter,system-ui,sans-serif}
body{background:radial-gradient(1200px 800px at 50% -10%, #1a2a5a 0%, #0a0f1e 60%, #050810 100%);color:var(--text);overflow:hidden;height:100vh;height:100dvh;display:flex;flex-direction:column}
button{cursor:pointer;font:inherit}
.glass{background:var(--panel);backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:18px;box-shadow:0 12px 40px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.08)}
/* Title */
#titleScreen{position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:radial-gradient(900px 600px at 50% 30%, rgba(122,240,255,0.18), transparent 70%), radial-gradient(700px 500px at 80% 80%, rgba(255,122,214,0.12), transparent 70%), #050810}
.title-card{width:min(560px,92vw);padding:36px 32px;text-align:center;position:relative;overflow:hidden}
.title-card::before{content:"";position:absolute;inset:-2px;background:conic-gradient(from 0deg, var(--accent), var(--accent2), var(--accent3), var(--accent));opacity:0.12;filter:blur(18px);z-index:-1}
.logo{font-size:42px;font-weight:900;letter-spacing:-0.03em;line-height:0.9}
.logo span{background:linear-gradient(90deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.subtitle{color:var(--muted);font-size:13px;letter-spacing:0.18em;text-transform:uppercase;margin:10px 0 18px}
.lore-intro{font-size:13px;line-height:1.6;color:#b9c8e6;margin-bottom:22px}
.btn-primary{background:linear-gradient(135deg,var(--accent),#5eead4);color:#041018;border:none;padding:14px 22px;border-radius:12px;font-weight:800;letter-spacing:0.04em;box-shadow:0 8px 24px rgba(122,240,255,0.35);transition:transform .12s}
.btn-primary:active{transform:scale(0.97)}
.btn-ghost{background:rgba(255,255,255,0.06);border:1px solid var(--border);color:var(--text);padding:12px 18px;border-radius:12px;font-weight:600}
.title-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
.small{font-size:11px;color:var(--muted);margin-top:14px}
/* Top HUD */
#hud{height:52px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;gap:10px;flex-shrink:0}
.hud-left{display:flex;gap:10px;align-items:center}
.pill{padding:6px 10px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:0.08em;background:rgba(255,255,255,0.07);border:1px solid var(--border)}
.hud-right{display:flex;gap:8px}
.icon-btn{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;background:rgba(255,255,255,0.07);border:1px solid var(--border);color:var(--text);font-size:14px}
.icon-btn.active{background:var(--accent);color:#041018}
/* Main layout */
#main{flex:1;display:grid;grid-template-columns:1fr 320px;gap:14px;padding:0 14px 14px;min-height:0}
@media(max-width:900px){#main{grid-template-columns:1fr;grid-template-rows:1fr auto} #side{order:2}}
#stage{position:relative;border-radius:18px;overflow:hidden;background:#0b1226;border:1px solid var(--border);display:flex;flex-direction:column;min-height:0}
#overworldWrap{position:relative;flex:1;display:flex;align-items:center;justify-content:center;background:var(--zone-bg, linear-gradient(180deg,#0e1a3a,#050810));overflow:hidden}
canvas{image-rendering:pixelated}
#owCanvas{width:100%;height:100%;max-width:720px;max-height:520px;aspect-ratio:4/3;display:block}
#battleWrap{position:absolute;inset:0;display:none;flex-direction:column;background:linear-gradient(180deg, #0f1e3a 0%, #0a0f1e 100%)}
#battleCanvas{flex:1;width:100%}
#battleUI{height:170px;background:rgba(8,12,28,0.92);border-top:1px solid var(--border);display:grid;grid-template-columns:1fr 220px;gap:10px;padding:10px}
.battle-log{font-size:12px;line-height:1.5;color:#cbd8f5;overflow:auto;padding:8px;background:rgba(255,255,255,0.04);border-radius:10px;border:1px solid var(--border)}
.move-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.move-btn{padding:8px 8px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,0.06);color:var(--text);font-size:11px;font-weight:700;text-align:left;line-height:1.2}
.move-btn small{color:var(--muted);font-weight:500}
.move-btn:disabled{opacity:0.4}
.type-badge{font-size:9px;padding:2px 6px;border-radius:999px;font-weight:800;letter-spacing:0.06em;color:#041018}
#side{display:flex;flex-direction:column;gap:12px;min-height:0;overflow:auto}
.card{padding:14px}
.card h3{font-size:11px;letter-spacing:0.14em;text-transform:uppercase;color:var(--muted);margin-bottom:10px}
.zone-list{display:grid;gap:6px}
.zone-item{padding:10px 12px;border-radius:12px;border:1px solid var(--border);background:rgba(255,255,255,0.04);display:flex;justify-content:space-between;align-items:center;font-size:12px}
.zone-item.locked{opacity:0.45}
.zone-item.active{border-color:var(--accent);background:rgba(122,240,255,0.08)}
.party-list{display:grid;gap:8px}
.party-card{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:12px;background:rgba(255,255,255,0.05);border:1px solid var(--border)}
.party-card .hpbar{height:6px;background:rgba(0,0,0,0.4);border-radius:999px;overflow:hidden;flex:1}
.hp-fill{height:100%;background:linear-gradient(90deg,#4ade80,#22c55e);transition:width .4s}
.party-card.active{border-color:var(--accent)}
.controls{display:flex;gap:8px;flex-wrap:wrap}
.ctrl{flex:1;min-width:120px;padding:10px;border-radius:12px;border:1px solid var(--border);background:rgba(255,255,255,0.06);color:var(--text);font-weight:700;font-size:12px;text-align:center}
#dpad{position:absolute;left:12px;bottom:12px;display:grid;grid-template-columns:40px 40px 40px;grid-template-rows:40px 40px;gap:6px;opacity:0.95}
.dbtn{width:40px;height:40px;border-radius:10px;background:rgba(255,255,255,0.08);border:1px solid var(--border);color:var(--text);display:grid;place-items:center;font-weight:800;backdrop-filter:blur(8px)}
@media(min-width:901px){#dpad{display:none}}
/* Modals */
.modal{position:fixed;inset:0;z-index:200;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(3,6,16,0.6);backdrop-filter:blur(8px)}
.modal.open{display:flex}
.modal-card{width:min(760px,96vw);max-height:88vh;overflow:auto}
.tabs{display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap}
.tab{padding:8px 12px;border-radius:999px;border:1px solid var(--border);background:rgba(255,255,255,0.06);font-size:12px;font-weight:700}
.tab.active{background:var(--accent);color:#041018;border-color:transparent}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:600px){.grid2{grid-template-columns:1fr}}
.creature-card{padding:12px;border-radius:14px;background:rgba(255,255,255,0.05);border:1px solid var(--border)}
.creature-card h4{font-size:14px}
.creature-card p{font-size:11px;color:var(--muted);line-height:1.5;margin:6px 0}
.stat-row{display:flex;gap:6px;font-size:11px;margin-top:6px}
.stat{flex:1;text-align:center;padding:6px;background:rgba(0,0,0,0.25);border-radius:8px}
.bag-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.bag-item{padding:10px;border-radius:12px;background:rgba(255,255,255,0.06);border:1px solid var(--border);text-align:center;font-size:11px}
.bag-item b{font-size:13px}
.shake{animation:shake .3s}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
.float-dmg{position:absolute;pointer-events:none;font-weight:900;text-shadow:0 2px 8px rgba(0,0,0,0.6);animation:floatUp .8s forwards}
@keyframes floatUp{0%{transform:translateY(0) scale(1);opacity:1}100%{transform:translateY(-40px) scale(1.1);opacity:0}}
.particle{position:absolute;width:6px;height:6px;border-radius:50%;pointer-events:none}
.vignette{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .4s}
.capture-bar{height:10px;background:rgba(0,0,0,0.4);border-radius:999px;overflow:hidden;margin:8px 0}
.capture-fill{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent2));transition:width .1s}
</style>
</head>
<body>
<div id="titleScreen">
  <div class="title-card glass">
    <div class="logo">ATRIUM <span>OF THREADS</span></div>
    <div class="subtitle">A Binding Chronicle • Original Weave</div>
    <p class="lore-intro">The Atrium shattered into six drifting Atolls, each humming with wild <b>Threadlings</b> — echo-creatures spun from memory, wind and light. As a <b>Binder</b>, you will walk the tiles, weave a team of six, and rekindle the four Anchors before the Veil unravels.</p>
    <div class="title-actions">
      <button class="btn-primary" id="btnNew">NEW BINDING — Choose Starter</button>
      <button class="btn-ghost" id="btnContinue">CONTINUE</button>
    </div>
    <div class="small">WASD / Arrows to move • Touch D-Pad on mobile • No external assets — all original lore & audio</div>
    <div style="margin-top:14px;display:flex;gap:8px;justify-content:center">
      <span class="pill" style="background:var(--pyra);color:#fff">PYRA</span>
      <span class="pill" style="background:var(--aqua);color:#041018">AQUA</span>
      <span class="pill" style="background:var(--sylv);color:#041018">SYLV</span>
      <span class="pill" style="background:var(--zephyr);color:#fff">ZEPHYR</span>
      <span class="pill" style="background:var(--lithos);color:#fff">LITHOS</span>
      <span class="pill" style="background:var(--lumen);color:#041018">LUMEN</span>
      <span class="pill" style="background:var(--null);color:#fff">NULL</span>
    </div>
  </div>
</div>

<div id="hud">
  <div class="hud-left">
    <span class="pill" id="zoneName">DAWN STRAND — Z1</span>
    <span class="pill" id="storyPill">Weave 1/4</span>
    <span class="pill" id="anchorPill">Anchors 0/4</span>
  </div>
  <div class="hud-right">
    <button class="icon-btn" id="btnBag" title="Bag">🎒</button>
    <button class="icon-btn" id="btnTeam" title="Team">◈</button>
    <button class="icon-btn" id="btnDex" title="Codex">📖</button>
    <button class="icon-btn" id="btnMap" title="Map">🗺️</button>
    <button class="icon-btn" id="btnMute" title="Mute">🔊</button>
    <button class="icon-btn" id="btnPause" title="Pause">⏸</button>
  </div>
</div>

<div id="main">
  <div id="stage">
    <div id="overworldWrap">
      <canvas id="owCanvas" width="640" height="480"></canvas>
      <div id="dpad">
        <div></div><button class="dbtn" data-dir="up">▲</button><div></div>
        <button class="dbtn" data-dir="left">◀</button><button class="dbtn" data-dir="down">▼</button><button class="dbtn" data-dir="right">▶</button>
      </div>
      <div class="vignette" id="vignette"></div>
    </div>
    <div id="battleWrap">
      <canvas id="battleCanvas" width="720" height="360"></canvas>
      <div id="battleUI">
        <div class="battle-log" id="battleLog"></div>
        <div>
          <div class="move-grid" id="moveGrid"></div>
          <div style="display:flex;gap:6px;margin-top:6px">
            <button class="btn-ghost" id="btnSwitch" style="flex:1;padding:8px;font-size:11px">Switch</button>
            <button class="btn-ghost" id="btnCapture" style="flex:1;padding:8px;font-size:11px">Bind (Capture)</button>
            <button class="btn-ghost" id="btnFlee" style="flex:1;padding:8px;font-size:11px">Flee</button>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div id="side">
    <div class="card glass">
      <h3>Atolls — 6 Zones</h3>
      <div class="zone-list" id="zoneList"></div>
    </div>
    <div class="card glass">
      <h3>Active Team — up to 6</h3>
      <div class="party-list" id="partyList"></div>
      <div class="controls" style="margin-top:10px">
        <button class="ctrl" id="btnHeal">Rest at Shrine (H)</button>
        <button class="ctrl" id="btnSave">Save</button>
      </div>
    </div>
    <div class="card glass">
      <h3>Field Guide</h3>
      <div style="font-size:11px;color:var(--muted);line-height:1.6">
        • Step on <b style="color:var(--accent)">cyan shrines</b> to heal/save/shop.<br>
        • <b style="color:var(--accent2)">Gold gates</b> hold Guardians. Defeat 4 to win.<br>
        • Wild rate varies by zone. Capture when foe is weak!<br>
        • Type wheel: Pyra→Aqua→Sylv→Pyra, Zephyr beats Pyra/Sylv, Lithos beats Zephyr/Aqua, Lumen > all slightly, Null resists all, weak to Lumen.
      </div>
    </div>
  </div>
</div>

<!-- Modals -->
<div class="modal" id="modalStarter">
  <div class="modal-card glass" style="padding:20px">
    <h2 style="text-align:center">Choose Your First Threadling</h2>
    <p style="text-align:center;color:var(--muted);font-size:12px;margin:6px 0 14px">Each will guide your early weave. You can collect 27 total.</p>
    <div class="grid2" id="starterGrid"></div>
    <div style="text-align:center;margin-top:14px"><button class="btn-ghost" onclick="closeModal('modalStarter')">Cancel</button></div>
  </div>
</div>

<div class="modal" id="modalBag">
  <div class="modal-card glass" style="padding:18px">
    <div style="display:flex;justify-content:space-between;align-items:center"><h2>Satchel</h2><button class="btn-ghost" onclick="closeModal('modalBag')">Close</button></div>
    <div class="tabs"><button class="tab active" data-bag="all">All</button><button class="tab" data-bag="bind">Binders</button><button class="tab" data-bag="heal">Restoratives</button><button class="tab" data-bag="catalyst">Catalysts</button></div>
    <div class="bag-grid" id="bagGrid"></div>
    <div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
      <button class="btn-ghost" id="btnBuyVial">Buy Vial (30c)</button>
      <button class="btn-ghost" id="btnBuyPrism">Buy Prism (90c)</button>
      <button class="btn-ghost" id="btnBuyPotion">Buy Nectar (40c)</button>
      <span class="pill" id="coinPill">Coins: 200</span>
    </div>
  </div>
</div>

<div class="modal" id="modalTeam">
  <div class="modal-card glass" style="padding:18px">
    <div style="display:flex;justify-content:space-between;align-items:center"><h2>Weave Team</h2><button class="btn-ghost" onclick="closeModal('modalTeam')">Close</button></div>
    <div id="teamDetail" style="display:grid;gap:10px;margin-top:10px"></div>
  </div>
</div>

<div class="modal" id="modalDex">
  <div class="modal-card glass" style="padding:18px">
    <div style="display:flex;justify-content:space-between;align-items:center"><h2>Codex — 27 Threadlings</h2><button class="btn-ghost" onclick="closeModal('modalDex')">Close</button></div>
    <div id="dexGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px;margin-top:10px"></div>
  </div>
</div>

<div class="modal" id="modalMap">
  <div class="modal-card glass" style="padding:18px;text-align:center">
    <h2>Atrium Map</h2>
    <p style="color:var(--muted);font-size:12px">Defeat Guardians to unlock the next Atoll. Collect 8 unique Threadlings and 4 Anchors to complete the Weave.</p>
    <div id="mapGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px"></div>
    <button class="btn-ghost" style="margin-top:12px" onclick="closeModal('modalMap')">Close</button>
  </div>
</div>

<div class="modal" id="modalDialog">
  <div class="modal-card glass" style="padding:22px;text-align:center;max-width:520px">
    <h2 id="dialogTitle"></h2>
    <p id="dialogText" style="color:#cbd8f5;font-size:13px;line-height:1.6;margin:10px 0"></p>
    <button class="btn-primary" id="dialogBtn">Continue</button>
  </div>
</div>

<div class="modal" id="modalCapture">
  <div class="modal-card glass" style="padding:18px;text-align:center;max-width:420px">
    <h3>Binding Rite — Tap in rhythm!</h3>
    <p style="font-size:12px;color:var(--muted)">Press SPACE / tap when the needle is in the bright zone. 3 successes to bind.</p>
    <div class="capture-bar"><div class="capture-fill" id="captureFill"></div></div>
    <canvas id="captureCanvas" width="360" height="80" style="width:100%;background:rgba(0,0,0,0.3);border-radius:10px"></canvas>
    <div style="display:flex;gap:8px;justify-content:center;margin-top:10px">
      <button class="btn-primary" id="btnTap">TAP!</button>
      <button class="btn-ghost" onclick="closeModal('modalCapture');cancelCapture()">Cancel</button>
    </div>
    <div id="captureInfo" style="font-size:12px;margin-top:8px;color:var(--accent)"></div>
  </div>
</div>

<div class="modal" id="modalSwitch">
  <div class="modal-card glass" style="padding:18px">
    <h3>Switch Threadling</h3>
    <div id="switchGrid" style="display:grid;gap:8px;margin-top:10px"></div>
    <button class="btn-ghost" style="margin-top:10px" onclick="closeModal('modalSwitch')">Cancel</button>
  </div>
</div>

<script>
// ===== WORLD LORE & TYPES =====
const TYPES = {
  Pyra:{color:'#ff6b4a', label:'PYRA'},
  Aqua:{color:'#4ac8ff', label:'AQUA'},
  Sylv:{color:'#4ade7a', label:'SYLV'},
  Zephyr:{color:'#a78bfa', label:'ZEPHYR'},
  Lithos:{color:'#f59e42', label:'LITHOS'},
  Lumen:{color:'#ffe27a', label:'LUMEN'},
  Null:{color:'#9aa0b2', label:'NULL'}
};
const TYPE_ORDER=['Pyra','Aqua','Sylv','Zephyr','Lithos','Lumen','Null'];
function typeMult(atk, def){
  if(atk==='Lumen' && def!=='Lumen') return 1.25;
  if(atk==='Lumen' && def==='Lumen') return 1;
  if(def==='Null'){
    if(atk==='Lumen') return 2.0;
    return 0.75;
  }
  if(def==='Lumen' && atk==='Null') return 0.5;
  // A>B>C>A
  if(atk==='Pyra' && def==='Aqua') return 2;
  if(atk==='Aqua' && def==='Sylv') return 2;
  if(atk==='Sylv' && def==='Pyra') return 2;
  if(atk==='Aqua' && def==='Pyra') return 0.5;
  if(atk==='Sylv' && def==='Aqua') return 0.5;
  if(atk==='Pyra' && def==='Sylv') return 0.5;
  // D > A & C
  if(atk==='Zephyr' && (def==='Pyra'||def==='Sylv')) return 2;
  if((def==='Zephyr') && (atk==='Pyra'||atk==='Sylv')) return 0.5;
  // E > D & B
  if(atk==='Lithos' && (def==='Zephyr'||def==='Aqua')) return 2;
  if(def==='Lithos' && (atk==='Zephyr'||atk==='Aqua')) return 0.5;
  return 1;
}

// ===== ABILITIES 40+ =====
const ABILITIES = [
  {id:'ember_kiss',name:'Ember Kiss',type:'Pyra',power:40,acc:100,pp:20,effect:'burn'},
  {id:'tidal_slap',name:'Tidal Slap',type:'Aqua',power:45,acc:100,pp:20,effect:'none'},
  {id:'thorn_lash',name:'Thorn Lash',type:'Sylv',power:42,acc:100,pp:20,effect:'none'},
  {id:'gale_cut',name:'Gale Cut',type:'Zephyr',power:38,acc:100,pp:25,effect:'speed_up'},
  {id:'stone_hurl',name:'Stone Hurl',type:'Lithos',power:50,acc:90,pp:15,effect:'none'},
  {id:'lumen_ray',name:'Lumen Ray',type:'Lumen',power:45,acc:100,pp:20,effect:'none'},
  {id:'null_gaze',name:'Null Gaze',type:'Null',power:35,acc:100,pp:25,effect:'frost'},
  {id:'cinder_spout',name:'Cinder Spout',type:'Pyra',power:70,acc:85,pp:12,effect:'burn'},
  {id:'abyss_surge',name:'Abyss Surge',type:'Aqua',power:75,acc:85,pp:12,effect:'none'},
  {id:'canopy_bloom',name:'Canopy Bloom',type:'Sylv',power:0,acc:100,pp:12,effect:'heal50'},
  {id:'zephyr_dance',name:'Zephyr Dance',type:'Zephyr',power:0,acc:100,pp:15,effect:'speed_up2'},
  {id:'quake_step',name:'Quake Step',type:'Lithos',power:60,acc:95,pp:15,effect:'frost'},
  {id:'radiant_veil',name:'Radiant Veil',type:'Lumen',power:0,acc:100,pp:10,effect:'barrier'},
  {id:'void_leech',name:'Void Leech',type:'Null',power:40,acc:100,pp:15,effect:'heal_half_damage'},
  {id:'flare_needle',name:'Flare Needle',type:'Pyra',power:22,acc:95,pp:20,effect:'multi2'},
  {id:'bubble_barrage',name:'Bubble Barrage',type:'Aqua',power:18,acc:95,pp:20,effect:'multi3'},
  {id:'vine_coil',name:'Vine Coil',type:'Sylv',power:30,acc:100,pp:20,effect:'frost'},
  {id:'sky_sprint',name:'Sky Sprint',type:'Zephyr',power:40,acc:100,pp:20,effect:'priority'},
  {id:'crystal_spike',name:'Crystal Spike',type:'Lithos',power:55,acc:90,pp:15,effect:'none'},
  {id:'prism_flash',name:'Prism Flash',type:'Lumen',power:60,acc:95,pp:15,effect:'shock'},
  {id:'hollow_hum',name:'Hollow Hum',type:'Null',power:0,acc:90,pp:15,effect:'shock'},
  {id:'ash_veil',name:'Ash Veil',type:'Pyra',power:0,acc:100,pp:15,effect:'barrier'},
  {id:'spring_renew',name:'Spring Renew',type:'Aqua',power:0,acc:100,pp:10,effect:'regen'},
  {id:'root_mend',name:'Root Mend',type:'Sylv',power:0,acc:100,pp:10,effect:'heal50'},
  {id:'tailwind',name:'Tailwind',type:'Zephyr',power:0,acc:100,pp:12,effect:'speed_up'},
  {id:'iron_bastion',name:'Iron Bastion',type:'Lithos',power:0,acc:100,pp:12,effect:'def_up'},
  {id:'dawn_bless',name:'Dawn Bless',type:'Lumen',power:0,acc:100,pp:10,effect:'heal50'},
  {id:'null_shroud',name:'Null Shroud',type:'Null',power:0,acc:100,pp:10,effect:'barrier'},
  {id:'wildfire',name:'Wildfire',type:'Pyra',power:85,acc:80,pp:8,effect:'burn'},
  {id:'monsoon',name:'Monsoon',type:'Aqua',power:90,acc:80,pp:8,effect:'none'},
  {id:'overgrowth',name:'Overgrowth',type:'Sylv',power:80,acc:85,pp:10,effect:'regen'},
  {id:'cyclone',name:'Cyclone',type:'Zephyr',power:70,acc:90,pp:12,effect:'shock'},
  {id:'titan_slam',name:'Titan Slam',type:'Lithos',power:85,acc:85,pp:10,effect:'none'},
  {id:'nova_burst',name:'Nova Burst',type:'Lumen',power:80,acc:90,pp:10,effect:'none'},
  {id:'entropy_well',name:'Entropy Well',type:'Null',power:70,acc:90,pp:12,effect:'shock'},
  {id:'scorch_mark',name:'Scorch Mark',type:'Pyra',power:45,acc:100,pp:15,effect:'burn'},
  {id:'frost_tide',name:'Frost Tide',type:'Aqua',power:50,acc:95,pp:15,effect:'frost'},
  {id:'spore_cloud',name:'Spore Cloud',type:'Sylv',power:35,acc:95,pp:15,effect:'shock'},
  {id:'aero_needle',name:'Aero Needle',type:'Zephyr',power:25,acc:100,pp:20,effect:'multi2'},
  {id:'shard_volley',name:'Shard Volley',type:'Lithos',power:20,acc:95,pp:20,effect:'multi3'},
  {id:'lucent_pulse',name:'Lucent Pulse',type:'Lumen',power:50,acc:100,pp:15,effect:'none'},
  {id:'wither_touch',name:'Wither Touch',type:'Null',power:45,acc:100,pp:15,effect:'burn'},
];
const ABILITY_MAP = Object.fromEntries(ABILITIES.map(a=>[a.id,a]));

// ===== CREATURES 27 =====
const CREATURES = [
  {id:'embyr',name:'Embyr',types:['Pyra'],base:{hp:44,atk:52,def:38,spd:60},lore:'A cinder cub that naps in cooling ash, humming when warm.',learn:[[1,'ember_kiss'],[5,'flare_needle'],[9,'scorch_mark'],[14,'cinder_spout'],[20,'wildfire']],evo:{to:'inferryx',lvl:16,item:'ember_core'}},
  {id:'nereidrift',name:'Nereidrift',types:['Aqua'],base:{hp:48,atk:46,def:50,spd:52},lore:'Drifts on tide-mirrors, collecting lost reflections.',learn:[[1,'tidal_slap'],[5,'frost_tide'],[9,'bubble_barrage'],[14,'abyss_surge'],[20,'monsoon']],evo:{to:'abyssune',lvl:16}},
  {id:'brambleaf',name:'Brambleaf',types:['Sylv'],base:{hp:50,atk:44,def:54,spd:48},lore:'Its leaf-crown opens only for those who sit quietly.',learn:[[1,'thorn_lash'],[5,'vine_coil'],[9,'canopy_bloom'],[14,'spore_cloud'],[20,'overgrowth']],evo:{to:'canopyra',lvl:16}},
  {id:'zephyrix',name:'Zephyrix',types:['Zephyr'],base:{hp:42,atk:50,def:36,spd:68},lore:'Rides updrafts, leaving faint spiral glyphs.',learn:[[1,'gale_cut'],[5,'sky_sprint'],[9,'tailwind'],[14,'cyclone'],[22,'aero_needle']],evo:{to:'stormwraith',lvl:28,item:'gale_feather'}},
  {id:'golemite',name:'Golemite',types:['Lithos'],base:{hp:56,atk:58,def:62,spd:32},lore:'A pebble that learned to walk to find a better sunspot.',learn:[[1,'stone_hurl'],[6,'iron_bastion'],[10,'crystal_spike'],[16,'quake_step'],[22,'titan_slam']],evo:{to:'titanolith',lvl:28}},
  {id:'luminara',name:'Luminara',types:['Lumen'],base:{hp:46,atk:54,def:42,spd:62},lore:'Glows brighter when it hears laughter.',learn:[[1,'lumen_ray'],[5,'radiant_veil'],[10,'prism_flash'],[16,'lucent_pulse'],[24,'nova_burst']],evo:{to:'solaryth',lvl:32,item:'lumen_core'}},
  {id:'nullwisp',name:'Nullwisp',types:['Null'],base:{hp:45,atk:48,def:48,spd:55},lore:'A quiet hollow that copies the shape of nearby sounds.',learn:[[1,'null_gaze'],[6,'null_shroud'],[11,'void_leech'],[16,'hollow_hum'],[22,'entropy_well']],evo:{to:'nulldrake',lvl:32}},
  {id:'scorchpaw',name:'Scorchpaw',types:['Pyra'],base:{hp:40,atk:60,def:34,spd:66},lore:'Leaves pawprints that smolder for a heartbeat.',learn:[[1,'ember_kiss'],[7,'ash_veil'],[12,'cinder_spout'],[18,'scorch_mark']],evo:{to:'cindralord',lvl:26,item:'ember_core'}},
  {id:'tideling',name:'Tideling',types:['Aqua'],base:{hp:52,atk:42,def:56,spd:46},lore:'Hums with the tide, storing seawater in crystal cheeks.',learn:[[1,'tidal_slap'],[6,'spring_renew'],[11,'abyss_surge']],evo:null},
  {id:'virethorn',name:'Virethorn',types:['Sylv'],base:{hp:54,atk:50,def:58,spd:38},lore:'Vines tighten gently around trusted hands.',learn:[[1,'thorn_lash'],[6,'root_mend'],[12,'overgrowth']],evo:null},
  {id:'galehawk',name:'Galehawk',types:['Zephyr'],base:{hp:43,atk:55,def:38,spd:70},lore:'Circles shrines, guarding them from dust.',learn:[[1,'gale_cut'],[6,'zephyr_dance'],[12,'cyclone']],evo:{to:'aetherion',lvl:30,item:'gale_feather'}},
  {id:'stoneback',name:'Stoneback',types:['Lithos'],base:{hp:60,atk:52,def:68,spd:28},lore:'Moss grows on its back in patterns like maps.',learn:[[1,'stone_hurl'],[6,'iron_bastion'],[13,'titan_slam']],evo:null},
  {id:'glimmerin',name:'Glimmerin',types:['Lumen'],base:{hp:40,atk:58,def:36,spd:72},lore:'Darts like a stray sunbeam through ruins.',learn:[[1,'lumen_ray'],[7,'dawn_bless'],[13,'nova_burst']],evo:null},
  {id:'shadevoid',name:'Shadevoid',types:['Null'],base:{hp:48,atk:50,def:50,spd:50},lore:'Its shadow points the wrong way.',learn:[[1,'null_gaze'],[7,'wither_touch'],[14,'entropy_well']],evo:null},
  {id:'magmire',name:'Magmire',types:['Pyra','Lithos'],base:{hp:58,atk:62,def:60,spd:34},lore:'A living kiln that hardens its own skin.',learn:[[1,'stone_hurl'],[8,'cinder_spout'],[15,'titan_slam']],evo:null},
  {id:'frostglade',name:'Frostglade',types:['Aqua','Sylv'],base:{hp:55,atk:48,def:60,spd:42},lore:'Frost flowers bloom where it rests.',learn:[[1,'frost_tide'],[8,'canopy_bloom'],[15,'overgrowth']],evo:null},
  {id:'tempestral',name:'Tempestral',types:['Zephyr','Lumen'],base:{hp:47,atk:60,def:40,spd:74},lore:'Its wings refract light into brief rainbows.',learn:[[1,'gale_cut'],[8,'prism_flash'],[15,'cyclone']],evo:null},
  {id:'abyssalith',name:'Abyssalith',types:['Null','Lithos'],base:{hp:62,atk:56,def:66,spd:30},lore:'A trench-born monolith that remembers every tide.',learn:[[1,'null_gaze'],[8,'quake_step'],[15,'entropy_well']],evo:null},
  // evolutions
  {id:'inferryx',name:'Inferryx',types:['Pyra'],base:{hp:62,atk:78,def:52,spd:78},lore:'When its mane ignites, ash writes poems.',learn:[[1,'wildfire']],evo:null, isEvo:true},
  {id:'abyssune',name:'Abyssune',types:['Aqua'],base:{hp:68,atk:66,def:70,spd:58},lore:'Sings low notes that make water float.',learn:[[1,'monsoon']],evo:null,isEvo:true},
  {id:'canopyra',name:'Canopyra',types:['Sylv'],base:{hp:70,atk:64,def:74,spd:52},lore:'A walking grove where small lights nest.',learn:[[1,'overgrowth']],evo:null,isEvo:true},
  {id:'stormwraith',name:'Stormwraith',types:['Zephyr'],base:{hp:60,atk:74,def:50,spd:92},lore:'Appears as a gust wearing a crown.',learn:[[1,'cyclone']],evo:null,isEvo:true},
  {id:'titanolith',name:'Titanolith',types:['Lithos'],base:{hp:82,atk:80,def:92,spd:36},lore:'Each step is a quiet earthquake.',learn:[[1,'titan_slam']],evo:null,isEvo:true},
  {id:'solaryth',name:'Solaryth',types:['Lumen'],base:{hp:64,atk:82,def:56,spd:88},lore:'Its halo is warm enough to brew tea.',learn:[[1,'nova_burst']],evo:null,isEvo:true},
  {id:'nulldrake',name:'Nulldrake',types:['Null'],base:{hp:66,atk:76,def:68,spd:72},lore:'Silence follows it like a cape.',learn:[[1,'entropy_well']],evo:null,isEvo:true},
  {id:'cindralord',name:'Cindralord',types:['Pyra','Lithos'],base:{hp:68,atk:84,def:70,spd:60},lore:'Molten veins cool into armor.',learn:[[1,'wildfire']],evo:null,isEvo:true},
  {id:'aetherion',name:'Aetherion',types:['Zephyr','Lumen'],base:{hp:62,atk:80,def:54,spd:96},lore:'Light rides its wind.',learn:[[1,'nova_burst']],evo:null,isEvo:true},
];
const CREATURE_MAP = Object.fromEntries(CREATURES.map(c=>[c.id,c]));

// Zones
const ZONES = [
  {id:0,name:'Dawn Strand',bg:'linear-gradient(180deg,#1a3a6a 0%, #0e2a4a 40%, #f0c98a 100%)',enc:0.08,guardian:'golemite',color:'#7af0ff',particles:'mist'},
  {id:1,name:'Ember Hollows',bg:'linear-gradient(180deg,#3a1a1a 0%, #7a2a14 45%, #1a0a0a 100%)',enc:0.12,guardian:'scorchpaw',color:'#ff6b4a',particles:'ember'},
  {id:2,name:'Azure Expanse',bg:'linear-gradient(180deg,#0a2a5a 0%, #0e4a7a 50%, #0a1a3a 100%)',enc:0.14,guardian:'abyssalith',color:'#4ac8ff',particles:'bubble'},
  {id:3,name:'Skyward Verge',bg:'linear-gradient(180deg,#2a3a7a 0%, #8ea0ff 55%, #dbe6ff 100%)',enc:0.16,guardian:'galehawk',color:'#a78bfa',particles:'feather'},
  {id:4,name:'Obsidian Core',bg:'linear-gradient(180deg,#0a0a14 0%, #2a2a3a 40%, #1a1a2a 100%)',enc:0.18,guardian:'shadevoid',color:'#9aa0b2',particles:'ash'},
  {id:5,name:'Lumen Sanctum',bg:'linear-gradient(180deg,#fff7cc 0%, #ffe27a 30%, #7af0ff 100%)',enc:0.20,guardian:'solaryth',color:'#ffe27a',particles:'spark'},
];

let state = {
  zone:0, x:5, y:5, dir:0, coins:200,
  party:[], box:[], dex:new Set(), seen:new Set(),
  inventory:{vial:6, prism:2, sovereign:1, nectar:4, ether:2, ember_core:1, gale_feather:1, lumen_core:0},
  anchors:0, story:0, unlocked:1, badges:[],
  muted:false, tutorial:true
};

// helpers
function makeMon(id, level=5){
  const base = CREATURE_MAP[id];
  const scale = 1 + (level-1)*0.07;
  const hp = Math.floor(base.base.hp*scale);
  const atk = Math.floor(base.base.atk*scale);
  const def = Math.floor(base.base.def*scale);
  const spd = Math.floor(base.base.spd*scale);
  // learnset
  let moves=[];
  for(const [lvl,ab] of base.learn) if(lvl<=level) moves.push(ab);
  // fill up to 4
  moves = moves.slice(-4);
  while(moves.length<1) moves.push(base.learn[0][1]);
  // ensure PP
  const movesObj = moves.map(id=>({...ABILITY_MAP[id], curPP: ABILITY_MAP[id].pp}));
  return {uid:Math.random().toString(36).slice(2,7), id, name:base.name, types:[...base.types], level, maxHp:hp, hp, atk, def, spd, moves:movesObj, status:null, barrier:false, exp:0, nextExp: level*18}
}
function save(){ localStorage.setItem('atrium_save', JSON.stringify({...state, dex:[...state.dex], seen:[...state.seen]})); }
function load(){ const r=localStorage.getItem('atrium_save'); if(!r) return false; try{const s=JSON.parse(r); state={...state,...s, dex:new Set(s.dex), seen:new Set(s.seen)}; return true;}catch{return false}}

// ===== MAPS =====
const MAP_W=12, MAP_H=9;
function genMap(zone){
  // 0 empty, 1 wall, 2 shrine, 3 gate
  let m = Array.from({length:MAP_H},()=>Array(MAP_W).fill(0));
  // borders
  for(let x=0;x<MAP_W;x++){m[0][x]=1; m[MAP_H-1][x]=1}
  for(let y=0;y<MAP_H;y++){m[y][0]=1; m[y][MAP_W-1]=1}
  // random walls
  for(let i=0;i<10;i++){let x=2+Math.floor(Math.random()*(MAP_W-4)), y=2+Math.floor(Math.random()*(MAP_H-4)); m[y][x]=1; if(Math.random()<0.5) m[y][x+1]=1}
  m[4][6]=2; // shrine center
  m[1][6]=3; // gate north
  // ensure path
  m[5][5]=0; m[5][6]=0; m[4][5]=0;
  return m;
}
let maps = ZONES.map((_,i)=>genMap(i));

// ===== CANVAS OVERWORLD =====
const owCanvas=document.getElementById('owCanvas');
const octx=owCanvas.getContext('2d');
let particles=[];
function spawnParticles(){
  particles=[];
  for(let i=0;i<40;i++) particles.push({x:Math.random()*640,y:Math.random()*480,vx:(Math.random()-0.5)*0.6,vy:Math.random()*0.8+0.2, r: Math.random()*2+1, a: Math.random()*0.6+0.2});
}
spawnParticles();
function drawOverworld(){
  const zone=ZONES[state.zone];
  // bg
  document.getElementById('overworldWrap').style.setProperty('--zone-bg', zone.bg);
  octx.clearRect(0,0,640,480);
  // subtle gradient overlay
  let g=octx.createLinearGradient(0,0,0,480);
  if(state.zone===0){g.addColorStop(0,'#1a3a6a'); g.addColorStop(1,'#f0c98a');}
  else if(state.zone===1){g.addColorStop(0,'#4a1a14'); g.addColorStop(1,'#1a0a0a');}
  else if(state.zone===2){g.addColorStop(0,'#0a2a5a'); g.addColorStop(1,'#0e4a7a');}
  else if(state.zone===3){g.addColorStop(0,'#8ea0ff'); g.addColorStop(1,'#ffffff');}
  else if(state.zone===4){g.addColorStop(0,'#0a0a14'); g.addColorStop(1,'#2a2a3a');}
  else {g.addColorStop(0,'#fff7cc'); g.addColorStop(1,'#7af0ff');}
  octx.fillStyle=g; octx.fillRect(0,0,640,480);
  // particles
  const col= zone.color;
  particles.forEach(p=>{
    p.x+=p.vx; p.y+=p.vy; if(p.y>480){p.y=-4; p.x=Math.random()*640}
    if(p.x<0) p.x=640; if(p.x>640) p.x=0;
    octx.fillStyle=col; octx.globalAlpha=p.a*0.6; octx.beginPath(); octx.arc(p.x,p.y,p.r,0,Math.PI*2); octx.fill();
  }); octx.globalAlpha=1;
  // tiles
  const map=maps[state.zone];
  const tileW= 640/MAP_W, tileH= 480/MAP_H;
  for(let y=0;y<MAP_H;y++) for(let x=0;x<MAP_W;x++){
    const v=map[y][x];
    const px=x*tileW, py=y*tileH;
    if(v===1){
      octx.fillStyle='rgba(0,0,0,0.35)'; octx.fillRect(px+2,py+2,tileW-4,tileH-4);
      octx.strokeStyle='rgba(255,255,255,0.08)'; octx.strokeRect(px+1,py+1,tileW-2,tileH-2);
      // wall texture
      octx.fillStyle='rgba(255,255,255,0.03)'; octx.fillRect(px+6,py+6,tileW-12,4);
    } else if(v===2){
      octx.fillStyle='rgba(122,240,255,0.18)'; octx.fillRect(px+4,py+4,tileW-8,tileH-8);
      octx.fillStyle='#7af0ff'; octx.font='20px sans-serif'; octx.textAlign='center'; octx.fillText('◈',px+tileW/2,py+tileH/2+7);
      octx.fillStyle='rgba(122,240,255,0.9)'; octx.font='8px sans-serif'; octx.fillText('SHRINE',px+tileW/2,py+tileH-6);
    } else if(v===3){
      const locked = state.zone >= state.unlocked && state.zone!==5 ? true : false;
      // gate color
      octx.fillStyle= locked ? 'rgba(255,180,60,0.25)' : 'rgba(120,255,180,0.25)';
      octx.fillRect(px+2,py+2,tileW-4,tileH-4);
      octx.fillStyle= locked ? '#ffb43c' : '#7affb0'; octx.font='18px sans-serif'; octx.textAlign='center'; octx.fillText(locked?'⛩':'✦',px+tileW/2,py+tileH/2+6);
    } else {
      // grass dots
      octx.fillStyle='rgba(255,255,255,0.04)'; if((x+y)%3===0) octx.fillRect(px+tileW/2-1,py+tileH/2-1,2,2);
    }
  }
  // player
  const px= state.x*tileW+tileW/2, py= state.y*tileH+tileH/2;
  octx.shadowColor='rgba(122,240,255,0.8)'; octx.shadowBlur=12;
  octx.fillStyle='#eaf2ff'; octx.beginPath(); octx.arc(px,py,10,0,Math.PI*2); octx.fill(); octx.shadowBlur=0;
  octx.fillStyle='#041018'; octx.font='10px sans-serif'; octx.textAlign='center'; octx.fillText('●',px,py+3);
  // direction indicator
  octx.fillStyle='var(--accent)'; // not needed
  octx.fillStyle='#7af0ff'; octx.beginPath();
  const ang=[-Math.PI/2,0,Math.PI/2,Math.PI][state.dir]||0;
  octx.arc(px+Math.cos(ang)*14, py+Math.sin(ang)*14, 3,0,Math.PI*2); octx.fill();
  requestAnimationFrame(drawOverworld);
}
requestAnimationFrame(drawOverworld);

// movement
function tryMove(dx,dy){
  if(battleActive) return;
  let nx=state.x+dx, ny=state.y+dy;
  if(nx<0||nx>=MAP_W||ny<0||ny>=MAP_H) return;
  const tile=maps[state.zone][ny][nx];
  if(tile===1) { sfx('bump'); return; }
  if(tile===3){
    // gate
    if(state.zone < state.unlocked){
      // go next zone? Actually gate is exit to next zone at top
      if(state.zone < ZONES.length-1){
        state.zone++; state.x=6; state.y=7; save(); updateHUD(); sfx('gate');
        showDialog('Atoll Crossing', `You step through the thread-gate into <b>${ZONES[state.zone].name}</b>. The air changes. Wild Threadlings grow stronger.`);
        if(state.story < state.zone) { state.story = state.zone; triggerStory(); }
        return;
      }
    } else {
      // need guardian
      showDialog('Gate Sealed', `A luminous seal bars the way. Defeat the <b>${CREATURE_MAP[ZONES[state.zone].guardian].name}</b> Guardian at the shrine to break it.`);
      sfx('bump'); return;
    }
  }
  state.x=nx; state.y=ny;
  if(dx===1) state.dir=1; else if(dx===-1) state.dir=3; else if(dy===-1) state.dir=0; else if(dy===1) state.dir=2;
  sfx('step');
  if(tile===2){ onShrine(); }
  else {
    // random encounter
    if(Math.random() < ZONES[state.zone].enc){
      startWildBattle();
    }
  }
  save(); updateHUD();
}
window.addEventListener('keydown',e=>{
  if(e.key==='ArrowUp'||e.key==='w'||e.key==='W') tryMove(0,-1);
  if(e.key==='ArrowDown'||e.key==='s'||e.key==='S') tryMove(0,1);
  if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A') tryMove(-1,0);
  if(e.key==='ArrowRight'||e.key==='d'||e.key==='D') tryMove(1,0);
  if(e.key==='h'||e.key==='H') onShrine();
  if(e.key===' ') { if(captureActive) doTap(); }
});
document.querySelectorAll('.dbtn').forEach(b=> b.addEventListener('click',()=>{
  const d=b.dataset.dir;
  if(d==='up') tryMove(0,-1); if(d==='down') tryMove(0,1); if(d==='left') tryMove(-1,0); if(d==='right') tryMove(1,0);
}));

function onShrine(){
  // heal
  state.party.forEach(p=>{p.hp=p.maxHp; p.status=null; p.moves.forEach(m=>m.curPP=m.pp); p.barrier=false});
  save(); updateHUD(); renderParty();
  showDialog('Shrine of Threads', `The shrine hums and mends your weave. Team healed. <br><br><b>Shop:</b> Vials & Nectars available. <br><b>Guardian:</b> ${state.zone < state.unlocked ? 'Already defeated' : `<button class="btn-primary" onclick="startGuardianBattle();closeModal('modalDialog')" style="margin-top:8px">Challenge Guardian — ${CREATURE_MAP[ZONES[state.zone].guardian].name} Lv${8+state.zone*4}</button>`}`);
  sfx('heal');
  // auto story?
}
function triggerStory(){
  const texts=[
    {t:'First Thread', d:'The Atrium remembers you. Wild Threadlings stir as your first bond forms.'},
    {t:'Ember Hollows Kindled', d:'You broke the first Anchor. The Hollows exhale warmth; deeper Atolls awaken.'},
    {t:'Azure Accord', d:'Second Anchor shattered. Tides recede, revealing the Skyward Verge.'},
    {t:'Veil Thins', d:'Third Anchor falls. Null whispers gather. The Lumen Sanctum opens.'},
  ];
  const idx=Math.min(state.story, texts.length-1);
  showDialog(texts[idx].t, texts[idx].d);
  document.getElementById('vignette').style.background=`radial-gradient(600px 400px at 50% 40%, ${ZONES[state.zone].color}33, transparent 70%)`;
  document.getElementById('vignette').style.opacity=1; setTimeout(()=>document.getElementById('vignette').style.opacity=0,1800);
}

// ===== BATTLE =====
let battleActive=false, battleQueue=[], battleTurn=0, battleFoes=[], battlePlayerIdx=0, isGuardian=false, captureActive=false;
const battleCanvas=document.getElementById('battleCanvas');
const bctx=battleCanvas.getContext('2d');
function startWildBattle(){
  if(state.party.length===0) return;
  // pick foe(s)
  const zonePool = {
    0:['embyr','nereidrift','brambleaf','tideling'],
    1:['scorchpaw','magmire','embyr','golemite'],
    2:['tideling','nereidrift','frostglade','abyssalith'],
    3:['zephyrix','galehawk','tempestral','glimmerin'],
    4:['nullwisp','shadevoid','abyssalith','stoneback'],
    5:['luminara','glimmerin','tempestral','nullwisp','solaryth']
  }[state.zone];
  const count = state.zone>=3 && Math.random()<0.35 ? 2 : 1;
  if(state.zone>=4 && Math.random()<0.15) count===2?null:battleFoes.push(makeMon(zonePool[Math.floor(Math.random()*zonePool.length)], 12+state.zone*3));
  battleFoes=[];
  for(let i=0;i<count;i++) battleFoes.push(makeMon(zonePool[Math.floor(Math.random()*zonePool.length)], 4+state.zone*3+Math.floor(Math.random()*3)));
  battleFoes.forEach(f=>state.seen.add(f.id));
  beginBattle(false);
}
function startGuardianBattle(){
  const gid=ZONES[state.zone].guardian;
  const lvl=8+state.zone*6;
  battleFoes=[makeMon(gid,lvl)];
  // guardian has extra hp
  battleFoes[0].maxHp = Math.floor(battleFoes[0].maxHp*1.4); battleFoes[0].hp=battleFoes[0].maxHp;
  battleFoes[0].isGuardian=true;
  beginBattle(true);
}
function beginBattle(guardian){
  isGuardian=guardian;
  battleActive=true;
  document.getElementById('battleWrap').style.display='flex';
  battlePlayerIdx = state.party.findIndex(p=>p.hp>0);
  if(battlePlayerIdx===-1) battlePlayerIdx=0;
  battleTurn=0;
  buildTurnOrder();
  logBattle(`A wild ${battleFoes.map(f=>f.name+' Lv'+f.level).join(' & ')} appears!`);
  renderBattle(); updateMoveButtons();
  sfx('battle');
}
function buildTurnOrder(){
  // simple SPD order each round
  const activeFoes = battleFoes.filter(f=>f.hp>0);
  const player = state.party[battlePlayerIdx];
  let arr=[{who:'player', spd:player.spd, ref:player}, ...activeFoes.map(f=>({who:'foe', spd:f.spd, ref:f}))];
  // priority moves will be handled per action
  arr.sort((a,b)=>b.spd-a.spd);
  battleQueue=arr;
  battleTurn=0;
}
function logBattle(t){ const el=document.getElementById('battleLog'); el.innerHTML+= `<div>${t}</div>`; el.scrollTop=el.scrollHeight; }
function renderBattle(){
  // draw battle scene
  bctx.clearRect(0,0,720,360);
  // bg gradient
  let g=bctx.createLinearGradient(0,0,0,360); g.addColorStop(0,'#0f1e3a'); g.addColorStop(1,'#0a0f1e'); bctx.fillStyle=g; bctx.fillRect(0,0,720,360);
  // platforms
  bctx.fillStyle='rgba(255,255,255,0.06)'; bctx.beginPath(); bctx.ellipse(180,300,110,22,0,0,Math.PI*2); bctx.fill();
  bctx.beginPath(); bctx.ellipse(540,180,130,26,0,0,Math.PI*2); bctx.fill();
  // player mon
  const p=state.party[battlePlayerIdx];
  if(p){
    drawMon(bctx, 180, 240, p, false);
    // hp bar
    drawHpBar(bctx, 40, 320, 220, p);
  }
  // foes
  battleFoes.forEach((f,i)=>{
    const x= 480 + i*90, y= 130 + i*18;
    drawMon(bctx, x, y, f, true);
    drawHpBar(bctx, x-60, y+50, 120, f);
  });
}
function drawMon(ctx,x,y,mon, isFoe){
  // shadow
  ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(x,y+28,28,8,0,0,Math.PI*2); ctx.fill();
  // body circle with type color
  const col = TYPES[mon.types[0]].color;
  ctx.fillStyle=col; ctx.globalAlpha=0.95;
  ctx.beginPath(); ctx.arc(x,y,26,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1;
  ctx.fillStyle='rgba(255,255,255,0.92)'; ctx.beginPath(); ctx.arc(x,y,18,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='#0a0f1e'; ctx.font='700 11px sans-serif'; ctx.textAlign='center'; ctx.fillText(mon.name.slice(0,6), x, y+4);
  ctx.font='9px sans-serif'; ctx.fillText('Lv'+mon.level, x, y+14);
  // status
  if(mon.status){ ctx.fillStyle= mon.status==='burn'?'#ff6b4a': mon.status==='frost'?'#4ac8ff':'#ffe27a'; ctx.font='8px sans-serif'; ctx.fillText(mon.status.toUpperCase(), x, y-32); }
  if(mon.barrier){ ctx.strokeStyle='#7af0ff'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(x,y,30,0,Math.PI*2); ctx.stroke(); }
  // type badges
  mon.types.forEach((t,i)=>{
    ctx.fillStyle=TYPES[t].color; ctx.fillRect(x-18+i*20, y+32, 16,8);
    ctx.fillStyle='#041018'; ctx.font='6px sans-serif'; ctx.fillText(t.slice(0,2), x-10+i*20, y+38);
  });
}
function drawHpBar(ctx,x,y,w,mon){
  ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.fillRect(x,y,w,10);
  const pct= Math.max(0, mon.hp/mon.maxHp);
  ctx.fillStyle= pct>0.5?'#4ade80': pct>0.2?'#facc15':'#ef4444'; ctx.fillRect(x,y,w*pct,10);
  ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.font='8px sans-serif'; ctx.textAlign='left'; ctx.fillText(`${mon.hp}/${mon.maxHp}`, x+4, y+8);
  ctx.textAlign='right'; ctx.fillText(mon.name, x+w-4, y-4);
}
function updateMoveButtons(){
  const grid=document.getElementById('moveGrid'); grid.innerHTML='';
  const p=state.party[battlePlayerIdx];
  if(!p) return;
  p.moves.forEach((m,idx)=>{
    const btn=document.createElement('button'); btn.className='move-btn';
    btn.innerHTML=`${m.name} <small>${m.type} • PWR ${m.power} • ${m.curPP}/${m.pp}</small><br><span class="type-badge" style="background:${TYPES[m.type].color}">${m.type}</span> ${m.acc}%`;
    btn.disabled=m.curPP<=0;
    btn.onclick=()=> playerUseMove(idx);
    grid.appendChild(btn);
  });
}
function playerUseMove(idx){
  if(!battleActive) return;
  const player=state.party[battlePlayerIdx];
  const move=player.moves[idx];
  if(move.curPP<=0) return;
  move.curPP--;
  // choose target: first alive foe
  const target=battleFoes.find(f=>f.hp>0);
  if(!target) return;
  resolveMove(player,target,move);
  updateMoveButtons(); renderBattle();
  if(checkBattleEnd()) return;
  // foe turn
  setTimeout(foeTurn, 600);
}
function foeTurn(){
  if(!battleActive) return;
  // each alive foe acts
  for(const foe of battleFoes.filter(f=>f.hp>0)){
    if(Math.random()<0.08 && foe.status==='shock'){ logBattle(`${foe.name} is shocked and can't move!`); continue; }
    // pick random move
    const mv = foe.moves[Math.floor(Math.random()*foe.moves.length)];
    if(mv.curPP<=0) continue;
    mv.curPP--;
    const player=state.party[battlePlayerIdx];
    if(player.hp<=0) break;
    // frost halves spd already, but also check barrier
    resolveMove(foe, player, mv);
    if(player.hp<=0){
      // try auto switch
      const next = state.party.findIndex(p=>p.hp>0);
      if(next!==-1){ battlePlayerIdx=next; logBattle(`Go! ${state.party[next].name}!`); }
    }
  }
  renderBattle(); updateMoveButtons();
  applyEndOfTurn();
  checkBattleEnd();
}
function resolveMove(attacker, defender, move){
  // accuracy
  if(Math.random()*100 > move.acc){ logBattle(`${attacker.name} used ${move.name} — missed!`); spawnFloat(defender,'MISS','#fff'); return; }
  // barrier
  if(defender.barrier && move.power>0){ defender.barrier=false; logBattle(`${defender.name}'s barrier deflected ${move.name}!`); spawnFloat(defender,'BLOCK','#7af0ff'); sfx('block'); return; }
  let hits = 1;
  if(move.effect==='multi2') hits=2; if(move.effect==='multi3') hits= 2+Math.floor(Math.random()*2);
  let total=0;
  for(let h=0;h<hits;h++){
    if(move.power===0) break;
    let mult=1;
    defender.types.forEach(dt=> mult *= typeMult(move.type, dt));
    // STAB
    if(attacker.types.includes(move.type)) mult*=1.25;
    // burn halves ATK
    let atkStat = attacker.atk * (attacker.status==='burn'?0.5:1);
    let defStat = defender.def;
    let base = (0.4*attacker.level*atkStat/defStat*move.power/10)+2;
    base *= mult;
    if(Math.random()<0.06){ base*=1.5; spawnFloat(defender,'CRIT!','#ffe27a'); sfx('crit'); }
    base += (Math.random()*4-2);
    base = Math.max(1, Math.floor(base));
    if(mult>=2) sfx('super'); else if(mult<=0.5) sfx('not');
    if(mult>=2) spawnFloat(defender, 'SUPER!', '#4ade80');
    defender.hp = Math.max(0, defender.hp - base);
    total+=base;
    if(hits>1) logBattle(`${attacker.name} hits ${defender.name} with ${move.name} (${h+1}/${hits}) for ${base} (${mult.toFixed(2)}x)`);
  }
  if(move.power>0 && hits===1){
    let mult=1; defender.types.forEach(dt=> mult*=typeMult(move.type,dt)); if(attacker.types.includes(move.type)) mult*=1.25;
    const dmg = total || Math.max(1, Math.floor((0.4*attacker.level*(attacker.status==='burn'?attacker.atk*0.5:attacker.atk)/defender.def*move.power/10+2)*mult));
    // already applied above for single hit, but for total calc we already did
    if(total===0) { // for single hit path where loop did 1
      // already deducted
    }
    logBattle(`${attacker.name} used ${move.name} on ${defender.name} for ${total||'—'} damage!`);
    spawnFloat(defender, `-${total}`, mult>=2?'#4ade80':'#ff6b7a');
    shakeScreen();
  }
  // effects
  if(move.effect==='burn' && Math.random()<0.3){ defender.status='burn'; logBattle(`${defender.name} was burned!`); }
  if(move.effect==='frost' && Math.random()<0.3){ defender.status='frost'; logBattle(`${defender.name} was frosted! SPD halved!`); }
  if(move.effect==='shock' && Math.random()<0.3){ defender.status='shock'; logBattle(`${defender.name} was shocked!`); }
  if(move.effect==='barrier'){ attacker.barrier=true; logBattle(`${attacker.name} raised a barrier!`); }
  if(move.effect==='heal50'){ attacker.hp=Math.min(attacker.maxHp, attacker.hp+Math.floor(attacker.maxHp*0.5)); logBattle(`${attacker.name} healed!`); spawnFloat(attacker,'+HEAL','#7af0ff'); }
  if(move.effect==='heal_half_damage' && total){ attacker.hp=Math.min(attacker.maxHp, attacker.hp+Math.floor(total*0.5)); logBattle(`${attacker.name} leeched health!`); }
  if(move.effect==='regen'){ attacker.status='regen'; logBattle(`${attacker.name} is regenerating!`); }
  if(move.effect==='speed_up'){ attacker.spd = Math.floor(attacker.spd*1.2); logBattle(`${attacker.name}'s speed rose!`); }
  if(move.effect==='speed_up2'){ attacker.spd = Math.floor(attacker.spd*1.5); logBattle(`${attacker.name}'s speed sharply rose!`); }
  if(move.effect==='def_up'){ attacker.def = Math.floor(attacker.def*1.2); logBattle(`${attacker.name}'s defense rose!`); }
  if(move.effect==='priority'){ /* already acted first */ }
  // frost spd halve effect applied in damage calc via status? We'll just keep spd halved visually
  if(defender.status==='frost') defender.spd = Math.floor(CREATURE_MAP[defender.id].base.spd * (1+(defender.level-1)*0.07) *0.5);
}
function applyEndOfTurn(){
  [...state.party, ...battleFoes].forEach(m=>{
    if(m.status==='burn' && m.hp>0){ const d=Math.floor(m.maxHp*0.08); m.hp=Math.max(0,m.hp-d); logBattle(`${m.name} is hurt by burn (-${d})`); }
    if(m.status==='regen' && m.hp>0){ const h=Math.floor(m.maxHp*0.08); m.hp=Math.min(m.maxHp,m.hp+h); logBattle(`${m.name} regenerated (+${h})`); }
  });
}
function checkBattleEnd(){
  const foesAlive = battleFoes.some(f=>f.hp>0);
  const partyAlive = state.party.some(p=>p.hp>0);
  if(!foesAlive){
    // victory
    logBattle(`Victory!`);
    // EXP
    battleFoes.forEach(foe=>{
      state.party.forEach(p=>{
        if(p.hp>0){
          const gain = Math.floor((foe.level*12 + (foe.level - p.level)*2));
          p.exp += Math.max(8,gain);
          logBattle(`${p.name} gained ${gain} EXP`);
          while(p.exp >= p.nextExp && p.level<50){
            p.exp-=p.nextExp; p.level++; p.nextExp=p.level*18;
            // stat growth
            const base=CREATURE_MAP[p.id];
            const scale=1+(p.level-1)*0.07;
            p.maxHp=Math.floor(base.base.hp*scale); p.atk=Math.floor(base.base.atk*scale); p.def=Math.floor(base.base.def*scale); p.spd=Math.floor(base.base.spd*scale);
            p.hp=p.maxHp;
            logBattle(`${p.name} reached Lv${p.level}!`);
            // learn new move?
            const learn = base.learn.find(([lvl])=>lvl===p.level);
            if(learn){ const ab=ABILITY_MAP[learn[1]]; if(p.moves.length<4) p.moves.push({...ab,curPP:ab.pp}); else { p.moves[3]={...ab,curPP:ab.pp}; } logBattle(`${p.name} learned ${ab.name}!`); }
            // evolution check
            if(base.evo && p.level>=base.evo.lvl){
              const needItem=base.evo.item;
              if(!needItem || (state.inventory[needItem]||0)>0){
                if(needItem) state.inventory[needItem]--;
                const evoId=base.evo.to;
                const evoBase=CREATURE_MAP[evoId];
                p.id=evoId; p.name=evoBase.name; p.types=[...evoBase.types];
                const sc=1+(p.level-1)*0.07;
                p.maxHp=Math.floor(evoBase.base.hp*sc); p.atk=Math.floor(evoBase.base.atk*sc); p.def=Math.floor(evoBase.base.def*sc); p.spd=Math.floor(evoBase.base.spd*sc); p.hp=p.maxHp;
                logBattle(`${p.name} transformed into ${evoBase.name}! ✨`);
                state.dex.add(evoId); state.seen.add(evoId);
                sfx('evo');
              }
            }
          }
        }
      });
      state.dex.add(foe.id);
      state.coins+= 12+foe.level*3;
    });
    if(isGuardian){
      state.anchors++;
      state.unlocked = Math.min(ZONES.length, state.unlocked+1);
      state.badges.push(ZONES[state.zone].name);
      logBattle(`Anchor shattered! ${state.anchors}/4`);
      sfx('anchor');
      if(state.anchors>=4){
        showDialog('Weave Complete!', `You have bound 4 Anchors and woven the Atrium whole. The Veil steadies. <br><br>Post-game: wild Threadlings are stronger. Collect all 27! <br>Unique collected: ${state.dex.size}/27`);
      } else {
        showDialog('Anchor Claimed', `The ${ZONES[state.zone].name} Anchor is yours. The gate north opens.`);
      }
    }
    setTimeout(endBattle, 900);
    save(); updateHUD(); renderParty();
    return true;
  }
  if(!partyAlive){
    logBattle(`Your weave faded... You awaken at the shrine.`);
    state.party.forEach(p=>{p.hp=Math.floor(p.maxHp*0.5); p.status=null});
    setTimeout(endBattle, 900);
    state.x=6; state.y=5; save(); updateHUD();
    return true;
  }
  return false;
}
function endBattle(){
  battleActive=false; battleFoes=[]; document.getElementById('battleWrap').style.display='none'; document.getElementById('battleLog').innerHTML='';
}
function shakeScreen(){ const el=document.getElementById('battleCanvas'); el.classList.add('shake'); setTimeout(()=>el.classList.remove('shake'),300); }
function spawnFloat(target, text, color){
  const wrap=document.getElementById('battleWrap');
  const d=document.createElement('div'); d.className='float-dmg'; d.textContent=text; d.style.color=color; d.style.left=(Math.random()*40+30)+'%'; d.style.top='40%'; d.style.fontSize='18px';
  wrap.appendChild(d); setTimeout(()=>d.remove(),800);
}

// Capture
let captureNeedle=0, captureDir=1, captureHits=0, captureTimer=null, captureTarget=null, captureItem=null;
function openCapture(){
  const foe=battleFoes.find(f=>f.hp>0);
  if(!foe){ logBattle('No target!'); return; }
  if(state.inventory.vial<=0 && state.inventory.prism<=0 && state.inventory.sovereign<=0){ logBattle('No binders left! Buy at shrine.'); return; }
  // choose best available
  let item='vial';
  if(state.inventory.sovereign>0) item='sovereign'; else if(state.inventory.prism>0) item='prism';
  captureItem=item; captureTarget=foe;
  const hpPct= foe.hp/foe.maxHp;
  const baseRate = (1 - hpPct)*0.7 + 0.15;
  const itemBonus = item==='vial'?0: item==='prism'?0.25:0.45;
  const rate = Math.min(0.95, baseRate+itemBonus);
  document.getElementById('captureInfo').textContent=`Target HP ${Math.floor(hpPct*100)}% • ${item.toUpperCase()} • Base chance ${(rate*100).toFixed(0)}% • Hit 3 rhythms`;
  document.getElementById('captureFill').style.width='0%';
  captureHits=0; captureNeedle=0; captureDir=1;
  openModal('modalCapture');
  captureActive=true;
  const c=document.getElementById('captureCanvas'), ctx=c.getContext('2d');
  function loop(){
    if(!captureActive) return;
    captureNeedle+= captureDir*0.035;
    if(captureNeedle>1) {captureNeedle=1; captureDir=-1}
    if(captureNeedle<0) {captureNeedle=0; captureDir=1}
    ctx.clearRect(0,0,360,80);
    ctx.fillStyle='rgba(255,255,255,0.08)'; ctx.fillRect(20,30,320,20);
    // sweet spot moves slightly
    const sweet = 0.45 + Math.sin(Date.now()*0.002)*0.1;
    ctx.fillStyle='rgba(122,240,255,0.9)'; ctx.fillRect(20+sweet*320-18,30,36,20);
    ctx.fillStyle='rgba(255,255,255,0.4)'; ctx.fillRect(20+sweet*320-1,30,2,20);
    // needle
    ctx.fillStyle='#ffe27a'; ctx.fillRect(20+captureNeedle*320-2,18,4,44);
    requestAnimationFrame(loop);
  }
  loop();
}
function doTap(){
  if(!captureActive) return;
  const sweet = 0.45 + Math.sin(Date.now()*0.002)*0.1;
  const diff=Math.abs(captureNeedle - sweet);
  if(diff<0.08){
    captureHits++; sfx('tapGood');
    document.getElementById('captureFill').style.width= (captureHits/3*100)+'%';
    spawnFloat({},{},'#7af0ff'); // dummy
    if(captureHits>=3){
      // success calc with HP
      const hpPct=captureTarget.hp/captureTarget.maxHp;
      const chance = (1-hpPct)*0.6 + (captureItem==='prism'?0.2: captureItem==='sovereign'?0.4:0) + 0.2;
      const success = Math.random() < chance || hpPct<0.25;
      finishCapture(success);
    }
  } else {
    sfx('tapBad');
    captureHits=Math.max(0,captureHits-1);
    document.getElementById('captureFill').style.width=(captureHits/3*100)+'%';
  }
}
function finishCapture(success){
  captureActive=false;
  closeModal('modalCapture');
  if(success){
    // consume item
    state.inventory[captureItem]--;
    if(state.party.length<6){
      // clone foe as new party member at same level but healed
      const nm=makeMon(captureTarget.id, captureTarget.level);
      nm.hp=nm.maxHp; state.party.push(nm);
    } else {
      state.box.push(captureTarget.id);
    }
    state.dex.add(captureTarget.id); state.seen.add(captureTarget.id);
    logBattle(`Bound ${captureTarget.name}! It joins your weave.`);
    // remove foe from battle
    captureTarget.hp=0;
    sfx('capture');
    if(checkBattleEnd()) return;
    renderBattle();
  } else {
    logBattle(`Binding failed! ${captureTarget.name} broke free!`);
    sfx('break');
  }
  save(); updateHUD(); renderParty();
}
function cancelCapture(){ captureActive=false; }

// HUD & Lists
function updateHUD(){
  document.getElementById('zoneName').textContent=`${ZONES[state.zone].name.toUpperCase()} — Z${state.zone+1}`;
  document.getElementById('storyPill').textContent=`Weave ${Math.min(4,state.story+1)}/4`;
  document.getElementById('anchorPill').textContent=`Anchors ${state.anchors}/4`;
  document.getElementById('coinPill').textContent=`Coins: ${state.coins}`;
  const zl=document.getElementById('zoneList'); zl.innerHTML='';
  ZONES.forEach((z,i)=>{
    const d=document.createElement('div'); d.className='zone-item'+(i===state.zone?' active':'')+(i>=state.unlocked?' locked':'');
    d.innerHTML=`<span>${i+1}. ${z.name}</span><span>${i<state.unlocked?'✓':'🔒'}</span>`;
    d.onclick=()=>{ if(i<state.unlocked){ state.zone=i; state.x=6; state.y=6; updateHUD(); save(); }};
    zl.appendChild(d);
  });
  // map grid
  const mg=document.getElementById('mapGrid'); if(mg){ mg.innerHTML=''; ZONES.forEach((z,i)=>{
    const c=document.createElement('div'); c.className='glass'; c.style.padding='12px'; c.style.borderColor= i<state.unlocked? 'var(--accent)':'var(--border)';
    c.innerHTML=`<b style="font-size:12px">${z.name}</b><br><span style="font-size:10px;color:var(--muted)">${i<state.unlocked?'Unlocked':'Locked'}</span><br><span style="font-size:11px">${i===state.zone?'● Here':''}</span>`;
    mg.appendChild(c);
  });}
}
function renderParty(){
  const el=document.getElementById('partyList'); el.innerHTML='';
  if(state.party.length===0) el.innerHTML='<div style="font-size:11px;color:var(--muted)">No Threadlings yet. Choose a starter!</div>';
  state.party.forEach((p,i)=>{
    const d=document.createElement('div'); d.className='party-card'+(i===battlePlayerIdx && battleActive?' active':'');
    d.innerHTML=`<div style="width:36px;height:36px;border-radius:10px;background:${TYPES[p.types[0]].color};display:grid;place-items:center;font-weight:800;color:#041018">${p.name[0]}</div>
      <div style="flex:1"><div style="font-size:12px;font-weight:800">${p.name} <span style="font-weight:500;color:var(--muted)">Lv${p.level}</span> <span class="type-badge" style="background:${TYPES[p.types[0]].color}">${p.types[0]}</span></div>
      <div class="hpbar"><div class="hp-fill" style="width:${p.hp/p.maxHp*100}%"></div></div><div style="font-size:10px;color:var(--muted)">${p.hp}/${p.maxHp} HP • ${p.atk} ATK • ${p.spd} SPD</div></div>`;
    el.appendChild(d);
  });
  // team detail modal
  const td=document.getElementById('teamDetail'); if(td){ td.innerHTML=''; state.party.forEach((p,i)=>{
    const c=document.createElement('div'); c.className='creature-card';
    c.innerHTML=`<div style="display:flex;justify-content:space-between;align-items:center"><h4>${p.name} Lv${p.level}</h4><span class="type-badge" style="background:${TYPES[p.types[0]].color}">${p.types.join('/')}</span></div>
      <div class="stat-row"><div class="stat">HP ${p.hp}/${p.maxHp}</div><div class="stat">ATK ${p.atk}</div><div class="stat">DEF ${p.def}</div><div class="stat">SPD ${p.spd}</div></div>
      <div style="font-size:11px;margin-top:6px">${p.moves.map(m=>`${m.name} (${m.curPP}/${m.pp})`).join(' • ')}</div>
      <div style="display:flex;gap:6px;margin-top:8px"><button class="btn-ghost" onclick="healOne(${i})">Nectar</button><button class="btn-ghost" onclick="releaseMon(${i})">Release</button></div>`;
    td.appendChild(c);
  });}
}
function healOne(i){
  if((state.inventory.nectar||0)<=0){ alert('No Nectar!'); return; }
  const p=state.party[i]; p.hp=Math.min(p.maxHp, p.hp+Math.floor(p.maxHp*0.5)); state.inventory.nectar--; save(); renderParty(); updateHUD(); sfx('heal');
}
function releaseMon(i){ if(confirm('Release?')){ state.party.splice(i,1); save(); renderParty(); updateHUD(); }}

// Bag
function renderBag(filter='all'){
  const grid=document.getElementById('bagGrid'); grid.innerHTML='';
  const items=[
    {k:'vial',name:'Thread Vial',desc:'Basic binder',cat:'bind',icon:'🧪'},
    {k:'prism',name:'Prism Cage',desc:'+25% bind',cat:'bind',icon:'🔷'},
    {k:'sovereign',name:'Sovereign Thread',desc:'+45% bind',cat:'bind',icon:'👑'},
    {k:'nectar',name:'Nectar',desc:'Heal 50% HP',cat:'heal',icon:'🍯'},
    {k:'ether',name:'Ether Dew',desc:'Restore 10 PP',cat:'heal',icon:'💧'},
    {k:'ember_core',name:'Ember Core',desc:'Evo catalyst Pyra',cat:'catalyst',icon:'🔥'},
    {k:'gale_feather',name:'Gale Feather',desc:'Evo catalyst Zephyr',cat:'catalyst',icon:'🪶'},
    {k:'lumen_core',name:'Lumen Core',desc:'Evo catalyst Lumen',cat:'catalyst',icon:'☀️'},
  ];
  items.forEach(it=>{
    if(filter!=='all' && it.cat!==filter) return;
    const d=document.createElement('div'); d.className='bag-item';
    d.innerHTML=`<div style="font-size:18px">${it.icon}</div><b>${it.name}</b><br>${it.desc}<br><span style="color:var(--accent);font-weight:800">x${state.inventory[it.k]||0}</span><br><button class="btn-ghost" style="margin-top:6px;padding:4px 8px;font-size:10px" onclick="useBag('${it.k}')">Use</button>`;
    grid.appendChild(d);
  });
}
function useBag(k){
  if(k==='nectar'){
    const idx=state.party.findIndex(p=>p.hp<p.maxHp);
    if(idx===-1) return alert('All healthy');
    if(state.inventory.nectar<=0) return;
    state.inventory.nectar--; state.party[idx].hp=Math.min(state.party[idx].maxHp, state.party[idx].hp+Math.floor(state.party[idx].maxHp*0.5));
  } else if(k==='ether'){
    const p=state.party[0]; if(!p) return; p.moves.forEach(m=>m.curPP=Math.min(m.pp,m.curPP+10)); state.inventory.ether=Math.max(0,(state.inventory.ether||0)-1);
  } else alert('Use binders in battle via Bind button');
  save(); renderBag(document.querySelector('.tab.active')?.dataset.bag||'all'); renderParty(); updateHUD();
}

// Dex
function renderDex(){
  const g=document.getElementById('dexGrid'); g.innerHTML='';
  CREATURES.forEach(c=>{
    const seen=state.seen.has(c.id), owned=state.dex.has(c.id);
    const d=document.createElement('div'); d.className='creature-card'; d.style.opacity= seen?1:0.35;
    d.innerHTML=`<div style="display:flex;justify-content:space-between"><h4>${seen?c.name:'???'}</h4><span class="type-badge" style="background:${TYPES[c.types[0]].color}">${c.types.join('/')}</span></div>
      <p>${seen?c.lore:'Undiscovered threadling.'}</p>
      <div style="font-size:10px;color:var(--muted)">Base HP ${c.base.hp} ATK ${c.base.atk} DEF ${c.base.def} SPD ${c.base.spd}</div>
      <div style="font-size:10px;margin-top:4px">${owned?'● Bound': seen?'○ Seen':'○ Unknown'}</div>`;
    g.appendChild(d);
  });
}

// Modals helpers
function openModal(id){ document.getElementById(id).classList.add('open'); }
function closeModal(id){ document.getElementById(id).classList.remove('open'); }
window.closeModal=closeModal;
window.cancelCapture=cancelCapture;
window.startGuardianBattle=startGuardianBattle;

// Starter
function renderStarters(){
  const grid=document.getElementById('starterGrid'); grid.innerHTML='';
  const starters=['embyr','nereidrift','brambleaf'];
  starters.forEach(id=>{
    const c=CREATURE_MAP[id];
    const d=document.createElement('div'); d.className='creature-card'; d.style.cursor='pointer';
    d.innerHTML=`<h4>${c.name} <span class="type-badge" style="background:${TYPES[c.types[0]].color}">${c.types[0]}</span></h4><p>${c.lore}</p><div style="font-size:11px">HP ${c.base.hp} ATK ${c.base.atk} DEF ${c.base.def} SPD ${c.base.spd}</div><button class="btn-primary" style="margin-top:8px;width:100%">Bind ${c.name}</button>`;
    d.onclick=()=> chooseStarter(id);
    grid.appendChild(d);
  });
}
function chooseStarter(id){
  state.party=[makeMon(id,5), makeMon(['tideling','galehawk'][Math.floor(Math.random()*2)],4)];
  state.dex.add(id); state.seen.add(id);
  state.unlocked=1; state.zone=0; state.x=5; state.y=5;
  closeModal('modalStarter');
  document.getElementById('titleScreen').style.display='none';
  save(); updateHUD(); renderParty();
  showDialog('First Weave', `You bound <b>${CREATURE_MAP[id].name}</b>! Your weave begins at Dawn Strand. Visit the shrine (cyan ◈) to heal and challenge the Guardian.`);
  sfx('capture');
}

// Events
document.getElementById('btnNew').onclick=()=>{ renderStarters(); openModal('modalStarter'); };
document.getElementById('btnContinue').onclick=()=>{
  if(load() && state.party.length>0){ document.getElementById('titleScreen').style.display='none'; updateHUD(); renderParty(); }
  else alert('No save found. Start New Binding.');
};
document.getElementById('btnBag').onclick=()=>{ renderBag(); openModal('modalBag'); };
document.getElementById('btnTeam').onclick=()=>{ renderParty(); openModal('modalTeam'); };
document.getElementById('btnDex').onclick=()=>{ renderDex(); openModal('modalDex'); };
document.getElementById('btnMap').onclick=()=>{ updateHUD(); openModal('modalMap'); };
document.getElementById('btnMute').onclick=(e)=>{
  state.muted=!state.muted; e.currentTarget.textContent= state.muted?'🔇':'🔊';
  if(state.muted) stopAudio(); else startAudio();
};
document.getElementById('btnPause').onclick=()=> showDialog('Paused', 'Take a breath. The Atrium waits.<br><br><button class="btn-ghost" onclick="closeModal(\'modalDialog\')">Resume</button>');
document.getElementById('btnHeal').onclick=onShrine;
document.getElementById('btnSave').onclick=()=>{ save(); showDialog('Saved', 'Your weave is etched at the shrine.'); };
document.getElementById('btnCapture').onclick=openCapture;
document.getElementById('btnFlee').onclick=()=>{ if(Math.random()<0.7){ logBattle('You fled!'); setTimeout(endBattle,400);} else { logBattle('Could not flee!'); setTimeout(foeTurn,400);} };
document.getElementById('btnSwitch').onclick=()=>{
  const g=document.getElementById('switchGrid'); g.innerHTML='';
  state.party.forEach((p,i)=>{
    if(i===battlePlayerIdx || p.hp<=0) return;
    const b=document.createElement('button'); b.className='party-card'; b.style.cursor='pointer'; b.innerHTML=`<b>${p.name} Lv${p.level}</b> ${p.hp}/${p.maxHp} HP`;
    b.onclick=()=>{ battlePlayerIdx=i; closeModal('modalSwitch'); logBattle(`Go, ${p.name}!`); renderBattle(); updateMoveButtons(); setTimeout(foeTurn,500); };
    g.appendChild(b);
  });
  if(g.children.length===0) g.innerHTML='<div style="font-size:12px;color:var(--muted)">No other healthy Threadlings</div>';
  openModal('modalSwitch');
};
document.getElementById('btnTap').onclick=doTap;
document.getElementById('captureCanvas').onclick=doTap;
document.querySelectorAll('.tab').forEach(t=> t.onclick=()=>{
  document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active')); t.classList.add('active'); renderBag(t.dataset.bag);
});
document.getElementById('btnBuyVial').onclick=()=>{ if(state.coins>=30){state.coins-=30; state.inventory.vial++; save(); renderBag(); updateHUD(); sfx('coin')} };
document.getElementById('btnBuyPrism').onclick=()=>{ if(state.coins>=90){state.coins-=90; state.inventory.prism++; save(); renderBag(); updateHUD(); sfx('coin')} };
document.getElementById('btnBuyPotion').onclick=()=>{ if(state.coins>=40){state.coins-=40; state.inventory.nectar++; save(); renderBag(); updateHUD(); sfx('coin')} };
document.getElementById('dialogBtn').onclick=()=> closeModal('modalDialog');
function showDialog(t,html){ document.getElementById('dialogTitle').innerHTML=t; document.getElementById('dialogText').innerHTML=html; openModal('modalDialog'); }

// Audio simple
let audioCtx, osc1, gain1;
function startAudio(){
  if(state.muted) return;
  try{
    audioCtx=new (window.AudioContext||window.webkitAudioContext)();
    gain1=audioCtx.createGain(); gain1.gain.value=0.04; gain1.connect(audioCtx.destination);
    osc1=audioCtx.createOscillator(); osc1.type='sine'; osc1.frequency.value=110; osc1.connect(gain1); osc1.start();
    // gentle LFO
    setInterval(()=>{ if(!audioCtx||state.muted) return; osc1.frequency.linearRampToValueAtTime(110+Math.sin(Date.now()*0.0005)*12, audioCtx.currentTime+0.8); },800);
  }catch{}
}
function stopAudio(){ try{ if(osc1) osc1.stop(); }catch{} }
function sfx(type){
  if(state.muted||!audioCtx) return;
  const o=audioCtx.createOscillator(), g=audioCtx.createGain();
  o.connect(g); g.connect(audioCtx.destination);
  const now=audioCtx.currentTime;
  if(type==='step'){ o.frequency.value=220; g.gain.setValueAtTime(0.08,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.12); o.start(now); o.stop(now+0.12); }
  else if(type==='super'){ o.frequency.value=880; g.gain.setValueAtTime(0.12,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.25); o.start(now); o.stop(now+0.25); }
  else if(type==='crit'){ o.frequency.value=1200; g.gain.setValueAtTime(0.14,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.3); o.start(now); o.stop(now+0.3); }
  else if(type==='heal'){ o.frequency.value=660; g.gain.setValueAtTime(0.1,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.4); o.start(now); o.stop(now+0.4); }
  else if(type==='tapGood'){ o.frequency.value=900; g.gain.setValueAtTime(0.1,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.15); o.start(now); o.stop(now+0.15); }
  else if(type==='tapBad'){ o.frequency.value=160; g.gain.setValueAtTime(0.08,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.15); o.start(now); o.stop(now+0.15); }
  else { o.frequency.value=400; g.gain.setValueAtTime(0.06,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.12); o.start(now); o.stop(now+0.12); }
}
window.addEventListener('click',()=>{ if(!audioCtx) startAudio(); }, {once:true});

// init
let hasSave = load();
updateHUD(); renderParty(); renderBag(); renderDex();
if(hasSave && state.party.length>0){
  document.getElementById('btnContinue').style.outline='2px solid var(--accent)';
  document.getElementById('btnContinue').style.boxShadow='0 0 16px rgba(122,240,255,0.45)';
}
// Always show title on load per spec — do not auto-skip
document.getElementById('titleScreen').style.display='flex';
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"90d1d4e3-ba92-40c6-94c7-c9757783f3a3","tokensIn":1006338,"tokensOut":33927,"tokensTotal":1040265,"cost":0.027336344000000002,"turns":25,"toolCalls":24,"failedToolCalls":0,"timestamp":"2026-08-23T02:45:40.945Z"} -->