Creature Collection
Grok 4.5 · middle

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 86,264 bytes · SHA-256 06ef59f721c9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Kithbind: Threads of Aevum</title>
<style>
:root{--bg-deep:#0b0e17;--panel:rgba(22,28,48,.82);--panel-border:rgba(140,170,255,.22);--glass:rgba(255,255,255,.06);--text:#e8eefc;--text-dim:#95a0bf;--accent:#7eb6ff;--accent-hot:#ff9e6e;--accent-soft:#9dffe0;--danger:#ff6b7a;--ok:#6dffb0;--warn:#ffd56e;--shadow:0 12px 40px rgba(0,0,0,.45);--radius:16px;--font:"Segoe UI",system-ui,sans-serif;--hp:#6dffb0}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg-deep);color:var(--text);font-family:var(--font);user-select:none;-webkit-tap-highlight-color:transparent}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
#app{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 30%,#1a2240 0%,var(--bg-deep) 70%)}
canvas#game{display:block;image-rendering:pixelated;max-width:100%;max-height:100%;background:#000;border-radius:8px;box-shadow:var(--shadow)}
.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;opacity:0;transition:opacity .35s;z-index:20}
.overlay.active{pointer-events:auto;opacity:1}
.glass{background:var(--panel);backdrop-filter:blur(18px) saturate(1.3);-webkit-backdrop-filter:blur(18px) saturate(1.3);border:1px solid var(--panel-border);border-radius:var(--radius);box-shadow:var(--shadow)}
#titleScreen{flex-direction:column;gap:18px;background:radial-gradient(ellipse at 50% 40%,rgba(80,120,255,.18),transparent 60%),rgba(8,10,20,.92)}
#titleScreen h1{font-size:clamp(2rem,6vw,3.4rem);letter-spacing:.08em;font-weight:300;background:linear-gradient(120deg,#cfe2ff,#9dffe0 40%,#ffc9a0 80%);-webkit-background-clip:text;background-clip:text;color:transparent}
#titleScreen .subtitle{color:var(--text-dim);letter-spacing:.35em;text-transform:uppercase;font-size:.72rem;margin-top:-8px}
.title-thread{width:min(420px,80vw);height:2px;background:linear-gradient(90deg,transparent,var(--accent),var(--accent-soft),transparent);margin:8px 0 20px;opacity:.7}
.title-btns{display:flex;flex-direction:column;gap:12px;width:min(280px,70vw)}
.btn{padding:14px 22px;border-radius:12px;background:linear-gradient(180deg,rgba(255,255,255,.1),rgba(255,255,255,.03));border:1px solid var(--panel-border);font-size:1rem;letter-spacing:.06em;transition:transform .15s,border-color .15s}
.btn:hover,.btn:focus{transform:translateY(-2px);border-color:var(--accent);outline:none}
.btn.primary{background:linear-gradient(180deg,rgba(126,182,255,.35),rgba(126,182,255,.12));border-color:rgba(126,182,255,.5)}
.btn:disabled{opacity:.35;pointer-events:none}.btn.small{padding:8px 14px;font-size:.85rem;border-radius:10px}
#hud{position:absolute;inset:0;pointer-events:none;z-index:10}
#hud .hud-panel{pointer-events:auto;position:absolute}
#zoneLabel{top:12px;left:12px;padding:8px 14px;font-size:.78rem;letter-spacing:.08em;color:var(--text-dim);text-transform:uppercase}
#zoneLabel strong{color:var(--text);display:block;font-size:.95rem;font-weight:600}
#topRight{top:12px;right:56px;display:flex;gap:8px}
#msgLog{position:absolute;bottom:88px;left:50%;transform:translateX(-50%);max-width:min(520px,92vw);text-align:center;pointer-events:none;font-size:.9rem}
#msgLog .line{background:rgba(10,12,22,.72);padding:6px 14px;border-radius:999px;margin:4px 0;animation:fadeLine 3s forwards;display:inline-block}
@keyframes fadeLine{0%{opacity:0;transform:translateY(6px)}12%{opacity:1}80%{opacity:1}100%{opacity:0}}
#touchPad{position:absolute;bottom:16px;left:16px;width:132px;height:132px;pointer-events:auto;display:none}
#touchPad.show{display:block}
#touchPad .pad{position:absolute;width:42px;height:42px;border-radius:10px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.18);display:flex;align-items:center;justify-content:center;color:var(--text-dim)}
#touchPad .u{top:0;left:45px}#touchPad .d{bottom:0;left:45px}#touchPad .l{left:0;top:45px}#touchPad .r{right:0;top:45px}
#actionBtns{position:absolute;bottom:16px;right:16px;display:none;flex-direction:column;gap:8px;pointer-events:auto}
#actionBtns.show{display:flex}
.modal{position:absolute;inset:0;z-index:30;display:none;align-items:center;justify-content:center;background:rgba(6,8,16,.55);backdrop-filter:blur(4px);padding:16px}
.modal.open{display:flex}
.modal-card{width:min(720px,96vw);max-height:min(88vh,820px);overflow:hidden;display:flex;flex-direction:column}
.modal-card header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--panel-border)}
.modal-card header h2{font-size:1.1rem;font-weight:600}.modal-card .body{padding:14px 18px;overflow-y:auto;flex:1}
.tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.tab{padding:6px 12px;border-radius:999px;font-size:.8rem;border:1px solid transparent;color:var(--text-dim)}
.tab.active{background:var(--glass);border-color:var(--panel-border);color:var(--text)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:600px){.grid2{grid-template-columns:1fr}}
.card{background:var(--glass);border:1px solid var(--panel-border);border-radius:12px;padding:10px 12px;cursor:pointer}
.card:hover{border-color:var(--accent)}.card .name{font-weight:600}.card .meta{font-size:.78rem;color:var(--text-dim)}
.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:.68rem;font-weight:600;margin-right:4px}
#dialogue{position:absolute;left:50%;bottom:24px;transform:translateX(-50%);width:min(640px,94vw);z-index:25;display:none;pointer-events:auto}
#dialogue.open{display:block}#dialogue .box{padding:16px 18px 14px;position:relative}
#dialogue .speaker{color:var(--accent-soft);font-size:.78rem;letter-spacing:.1em;text-transform:uppercase;margin-bottom:6px}
#dialogue .text{font-size:1rem;line-height:1.45;min-height:2.9em}#dialogue .cont{position:absolute;right:16px;bottom:10px;font-size:.75rem;color:var(--text-dim);animation:blink 1.2s infinite}
@keyframes blink{50%{opacity:.3}}
#vignette{position:absolute;inset:0;pointer-events:none;z-index:24;opacity:0;transition:opacity .5s;background:radial-gradient(ellipse at center,transparent 40%,rgba(0,0,0,.55)100%)}
#vignette.show{opacity:1}
#vignette.story-blue{background:radial-gradient(ellipse at center,rgba(60,100,200,.15)20%,rgba(10,20,50,.75)100%)}
#vignette.story-ember{background:radial-gradient(ellipse at center,rgba(200,100,40,.18)20%,rgba(30,10,10,.75)100%)}
#vignette.story-green{background:radial-gradient(ellipse at center,rgba(40,160,90,.15)20%,rgba(8,30,18,.75)100%)}
#vignette.story-void{background:radial-gradient(ellipse at center,rgba(80,40,150,.2)20%,rgba(8,4,20,.8)100%)}
#battleUI{position:absolute;inset:0;z-index:15;display:none;pointer-events:none}#battleUI.active{display:block}
#battleUI .battle-msg{position:absolute;top:12px;left:50%;transform:translateX(-50%);background:rgba(8,10,20,.75);padding:8px 16px;border-radius:999px;font-size:.88rem;max-width:90%;text-align:center}
#battlePanel{position:absolute;bottom:0;left:0;right:0;padding:12px;pointer-events:auto;background:linear-gradient(transparent,rgba(6,8,18,.92)30%)}
#battlePanel .inner{max-width:760px;margin:0 auto}
#battlePanel .actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
#battlePanel .move-btn{text-align:left;padding:10px 12px;border-radius:12px;background:var(--panel);border:1px solid var(--panel-border)}
#battlePanel .move-btn:disabled{opacity:.4}#battlePanel .move-btn .row{display:flex;justify-content:space-between;gap:8px}
#battlePanel .menu-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.hp-bar-wrap{height:8px;background:rgba(255,255,255,.1);border-radius:999px;overflow:hidden;margin-top:4px}
.hp-bar{height:100%;background:var(--hp);border-radius:999px;transition:width .4s}.hp-bar.low{background:var(--warn)}.hp-bar.critical{background:var(--danger)}
.creature-hud{background:var(--panel);border:1px solid var(--panel-border);border-radius:12px;padding:8px 12px;min-width:160px}
.creature-hud .n{font-weight:600;font-size:.9rem}.creature-hud .lv{font-size:.75rem;color:var(--text-dim)}
#captureUI{position:absolute;inset:0;z-index:28;display:none;align-items:center;justify-content:center;background:rgba(6,8,18,.7)}
#captureUI.open{display:flex}#captureUI .cap-card{width:min(420px,92vw);padding:22px;text-align:center}
#captureUI .ring{width:180px;height:180px;margin:16px auto;border-radius:50%;border:4px solid rgba(255,255,255,.15);position:relative;overflow:hidden}
#captureUI .needle{position:absolute;left:50%;top:50%;width:4px;height:78px;margin-left:-2px;margin-top:-78px;background:var(--accent);transform-origin:bottom center;border-radius:2px;z-index:2}
#captureUI .zone{position:absolute;inset:0;border-radius:50%}
.float-num{position:absolute;pointer-events:none;font-weight:700;font-size:1.1rem;animation:floatUp .9s ease-out forwards;z-index:40;text-shadow:0 2px 6px #000}
@keyframes floatUp{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-48px)}}
#toast{position:absolute;top:70px;left:50%;transform:translateX(-50%);z-index:50;pointer-events:none}
#toast .t{background:rgba(10,14,28,.9);border:1px solid var(--panel-border);padding:10px 18px;border-radius:12px;margin-bottom:6px;animation:fadeLine 2.8s forwards;font-size:.9rem}
#muteBtn{position:absolute;top:12px;right:12px;z-index:40;width:40px;height:40px;border-radius:50%;background:var(--panel);border:1px solid var(--panel-border);display:flex;align-items:center;justify-content:center;pointer-events:auto}
.bar-label{display:flex;justify-content:space-between;font-size:.72rem;color:var(--text-dim)}
.lore{font-size:.82rem;color:var(--text-dim);line-height:1.4;margin-top:6px}.sep{height:1px;background:var(--panel-border);margin:10px 0}
.party-slot{display:flex;gap:10px;align-items:center;margin-bottom:8px}
.orb{width:42px;height:42px;border-radius:50%;flex-shrink:0;border:2px solid rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;font-size:.7rem;font-weight:700}
.scroll-y{overflow-y:auto;max-height:50vh}.hidden{display:none!important}
.shop-item{display:flex;justify-content:space-between;align-items:center;padding:10px;border-bottom:1px solid var(--panel-border);gap:8px}
</style>
</head>
<body>
<div id="app">
<canvas id="game" width="960" height="540"></canvas>
<div id="vignette"></div>
<div id="hud">
<div id="zoneLabel" class="glass hud-panel"><strong>—</strong><span>Aevum Weave</span></div>
<div id="topRight" class="hud-panel">
<button class="btn small" id="btnMenu" type="button">Menu</button>
<button class="btn small" id="btnParty" type="button">Circle</button>
<button class="btn small" id="btnBag" type="button">Satchel</button>
</div>
<div id="msgLog"></div>
<div id="touchPad">
<div class="pad u" data-dir="up">▲</div><div class="pad d" data-dir="down">▼</div>
<div class="pad l" data-dir="left">◀</div><div class="pad r" data-dir="right">▶</div>
</div>
<div id="actionBtns"><button class="btn small" id="btnInteract" type="button">Bind / Talk</button></div>
</div>
<div id="battleUI">
<div class="battle-msg" id="battleMsg"></div>
<div id="playerHud" class="creature-hud" style="position:absolute;bottom:170px;left:16px"></div>
<div id="enemyHuds" style="position:absolute;top:48px;right:16px;display:flex;flex-direction:column;gap:8px"></div>
<div id="battlePanel"><div class="inner">
<div id="battleMainMenu" class="menu-row">
<button class="btn primary" data-bact="fight" type="button">Channel</button>
<button class="btn" data-bact="switch" type="button">Swap</button>
<button class="btn" data-bact="item" type="button">Satchel</button>
<button class="btn" data-bact="bind" type="button">Bind</button>
<button class="btn" data-bact="flee" type="button">Withdraw</button>
</div>
<div id="battleMoves" class="actions hidden"></div>
<div id="battleSub" class="hidden"></div>
</div></div>
</div>
<div id="captureUI"><div class="cap-card glass">
<h2>Weaving Bind</h2>
<p id="capHint" style="color:var(--text-dim);margin-top:6px;font-size:.85rem">Pulse when the needle enters the bright arc.</p>
<div class="ring"><div class="zone" id="capZone"></div><div class="needle" id="capNeedle"></div></div>
<button class="btn primary" id="capTap" type="button">Pulse</button>
<p id="capResult" style="margin-top:12px;min-height:1.4em"></p>
</div></div>
<div id="dialogue"><div class="box glass"><div class="speaker" id="dlgSpeaker"></div><div class="text" id="dlgText"></div><div class="cont">▾</div></div></div>
<div id="toast"></div>
<button id="muteBtn" type="button" title="Mute">♪</button>
<div id="titleScreen" class="overlay active">
<h1>KITHBIND</h1><div class="subtitle">Threads of Aevum</div><div class="title-thread"></div>
<div class="title-btns">
<button class="btn primary" id="btnNew" type="button">New Binding</button>
<button class="btn" id="btnContinue" type="button" disabled>Continue</button>
</div>
<p style="margin-top:24px;color:var(--text-dim);font-size:.78rem;max-width:360px;text-align:center;line-height:1.5">Weave bonds with wild Kith across the frayed Oases of Aevum. Eight unique bonds. Four Anchors unraveled.</p>
</div>
<div id="starterPick" class="modal"><div class="modal-card glass">
<header><h2>Choose your first Kith</h2></header>
<div class="body"><p style="color:var(--text-dim);font-size:.9rem">Three patterns wait near the first shrine.</p><div id="starterOpts" style="display:grid;gap:10px;margin-top:12px"></div></div>
</div></div>
<div id="menuModal" class="modal"><div class="modal-card glass">
<header><h2 id="menuTitle">Field Index</h2><button class="btn small" id="menuClose" type="button">Close</button></header>
<div class="body" id="menuBody"></div>
</div></div>
<div id="shrineModal" class="modal"><div class="modal-card glass">
<header><h2>Loomshrine</h2><button class="btn small" id="shrineClose" type="button">Leave</button></header>
<div class="body">
<p style="color:var(--text-dim);margin-bottom:10px" id="shrineBlurb">A quiet nexus hums with residual stitch-light.</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px">
<button class="btn primary" id="shrineHeal" type="button">Rest Circle</button>
<button class="btn" id="shrineSave" type="button">Seal Progress</button>
<button class="btn" id="shrineShop" type="button">Thread Market</button>
</div>
<div id="shrineShopPane" class="hidden"></div>
</div>
</div></div>
</div>
<script>
// KITHBIND — full game logic
const TYPES={kindle:{name:'Kindle',hex:'#ff7a4a'},mire:{name:'Mire',hex:'#4a9fff'},briar:{name:'Briar',hex:'#5dce6a'},shard:{name:'Shard',hex:'#c9a06a'},wisp:{name:'Wisp',hex:'#9bb7ff'},radiant:{name:'Radiant',hex:'#ffe66d'},umbral:{name:'Umbral',hex:'#a078ff'}};
function typeMult(a,d){if(!a||!d)return 1;const A='kindle',B='mire',C='briar',D='shard',E='wisp',F='radiant',G='umbral';if(d===G)return a===F?2:.5;if(a===F)return 1.25;if(a===A&&d===B||a===B&&d===C||a===C&&d===A)return 2;if(a===A&&d===C||a===B&&d===A||a===C&&d===B)return .5;if(a===D&&(d===A||d===C))return 2;if((a===A||a===C)&&d===D)return .5;if(a===E&&(d===D||d===B))return 2;if((a===D||a===B)&&d===E)return .5;if(a===E&&d===C)return .5;return 1;}
const AB={
ember_nudge:{name:'Ember Nudge',power:40,acc:100,type:'kindle',pp:30},cinder_lash:{name:'Cinder Lash',power:55,acc:95,type:'kindle',pp:20,effect:{status:'burn',chance:.2}},
pyre_coil:{name:'Pyre Coil',power:80,acc:85,type:'kindle',pp:10,effect:{status:'burn',chance:.35}},flare_burst:{name:'Flare Burst',power:70,acc:100,type:'kindle',pp:12,effect:{recoil:.1}},
hearth_ward:{name:'Hearth Ward',power:0,acc:100,type:'kindle',pp:15,effect:{barrier:true}},sunthread:{name:'Sunthread',power:90,acc:90,type:'kindle',pp:8,effect:{critBoost:1}},
drip_tap:{name:'Drip Tap',power:40,acc:100,type:'mire',pp:30},tide_knit:{name:'Tide Knit',power:55,acc:95,type:'mire',pp:20,effect:{status:'frost',chance:.2}},
undertow:{name:'Undertow',power:75,acc:90,type:'mire',pp:12,effect:{status:'frost',chance:.3}},mist_veil:{name:'Mist Veil',power:0,acc:100,type:'mire',pp:15,effect:{boost:{stat:'def',stages:1}}},
pressure_spool:{name:'Pressure Spool',power:85,acc:85,type:'mire',pp:8,effect:{multi:2}},flood_stitch:{name:'Flood Stitch',power:95,acc:80,type:'mire',pp:6},
leaf_pin:{name:'Leaf Pin',power:40,acc:100,type:'briar',pp:30},thorn_web:{name:'Thorn Web',power:50,acc:95,type:'briar',pp:20,effect:{status:'shock',chance:.15}},
root_surge:{name:'Root Surge',power:70,acc:90,type:'briar',pp:12,effect:{healSelf:.1}},canopy_guard:{name:'Canopy Guard',power:0,acc:100,type:'briar',pp:12,effect:{boost:{stat:'def',stages:2}}},
bloom_burst:{name:'Bloom Burst',power:80,acc:95,type:'briar',pp:10,effect:{healSelf:.05}},wild_trellis:{name:'Wild Trellis',power:90,acc:85,type:'briar',pp:8,effect:{status:'shock',chance:.25}},
pebble_tick:{name:'Pebble Tick',power:40,acc:100,type:'shard',pp:30},fault_line:{name:'Fault Line',power:60,acc:90,type:'shard',pp:18,effect:{boost:{stat:'atk',stages:1}}},
crystal_spear:{name:'Crystal Spear',power:80,acc:90,type:'shard',pp:10,effect:{critBoost:1}},bulwark:{name:'Bulwark',power:0,acc:100,type:'shard',pp:15,effect:{barrier:true,boost:{stat:'def',stages:1}}},
quake_spindle:{name:'Quake Spindle',power:95,acc:80,type:'shard',pp:6},grit_polish:{name:'Grit Polish',power:0,acc:100,type:'shard',pp:20,effect:{boost:{stat:'spd',stages:2}}},
breeze_yawn:{name:'Breeze Yawn',power:40,acc:100,type:'wisp',pp:30},slipstream:{name:'Slipstream',power:50,acc:100,type:'wisp',pp:20,effect:{boost:{stat:'spd',stages:1}}},
cyclone_pin:{name:'Cyclone Pin',power:70,acc:90,type:'wisp',pp:12,effect:{priority:1}},sky_shear:{name:'Sky Shear',power:85,acc:90,type:'wisp',pp:10,effect:{multi:2}},
updraft:{name:'Updraft',power:0,acc:100,type:'wisp',pp:15,effect:{boost:{stat:'spd',stages:2}}},storm_loom:{name:'Storm Loom',power:95,acc:80,type:'wisp',pp:6,effect:{status:'shock',chance:.3}},
glint:{name:'Glint',power:40,acc:100,type:'radiant',pp:30},bright_pin:{name:'Bright Pin',power:60,acc:95,type:'radiant',pp:18,effect:{critBoost:1}},
halo_cut:{name:'Halo Cut',power:75,acc:100,type:'radiant',pp:12},aurora_knit:{name:'Aurora Knit',power:0,acc:100,type:'radiant',pp:10,effect:{healSelf:.35}},
prism_fall:{name:'Prism Fall',power:90,acc:90,type:'radiant',pp:8,effect:{status:'shock',chance:.2}},lumen_crown:{name:'Lumen Crown',power:100,acc:85,type:'radiant',pp:5,effect:{critBoost:2}},
shade_touch:{name:'Shade Touch',power:40,acc:100,type:'umbral',pp:30},umbra_tangle:{name:'Umbra Tangle',power:55,acc:95,type:'umbral',pp:18,effect:{status:'frost',chance:.25}},
void_pinch:{name:'Void Pinch',power:70,acc:90,type:'umbral',pp:12,effect:{drain:.25}},night_shroud:{name:'Night Shroud',power:0,acc:100,type:'umbral',pp:12,effect:{boost:{stat:'def',stages:1},barrier:true}},
eclipse_rind:{name:'Eclipse Rind',power:85,acc:90,type:'umbral',pp:8,effect:{status:'burn',chance:.15}},null_chorus:{name:'Null Chorus',power:100,acc:80,type:'umbral',pp:5,effect:{status:'shock',chance:.35}},
pulse_jab:{name:'Pulse Jab',power:45,acc:100,type:'shard',pp:25,effect:{priority:1}},mend_thread:{name:'Mend Thread',power:0,acc:100,type:'briar',pp:10,effect:{healSelf:.45,regen:3}},
spark_chain:{name:'Spark Chain',power:30,acc:90,type:'kindle',pp:15,effect:{multi:3}},tidal_refresh:{name:'Tidal Refresh',power:0,acc:100,type:'mire',pp:8,effect:{healSelf:.5,clearStatus:true}},
static_knot:{name:'Static Knot',power:50,acc:95,type:'wisp',pp:15,effect:{status:'shock',chance:.4}},mirror_dust:{name:'Mirror Dust',power:0,acc:100,type:'radiant',pp:12,effect:{boost:{stat:'atk',stages:2}}},
gloom_seed:{name:'Gloom Seed',power:45,acc:100,type:'umbral',pp:18,effect:{status:'burn',chance:.3}},tectonic_hum:{name:'Tectonic Hum',power:65,acc:100,type:'shard',pp:12,effect:{boost:{stat:'def',stages:1}}},
sap_ribbon:{name:'Sap Ribbon',power:55,acc:100,type:'briar',pp:15,effect:{drain:.4}},frost_bead:{name:'Frost Bead',power:50,acc:95,type:'mire',pp:18,effect:{status:'frost',chance:.45}},
breeze_ward:{name:'Breeze Ward',power:0,acc:100,type:'wisp',pp:15,effect:{barrier:true}},kindle_waltz:{name:'Kindle Waltz',power:65,acc:95,type:'kindle',pp:12,effect:{boost:{stat:'spd',stages:1}}}
};
const SP={
pipglow:{id:'pipglow',name:'Pipglow',types:['kindle'],lore:'A coal-mote warming cold meadow seams.',base:{hp:42,atk:52,def:38,spd:48},learnset:{1:['ember_nudge','hearth_ward'],5:['cinder_lash'],12:['kindle_waltz'],20:['flare_burst'],32:['pyre_coil'],42:['sunthread']},color:'#ff8a5c',shape:0},
cindrel:{id:'cindrel',name:'Cindrel',types:['kindle'],lore:'Rising form crowned with a revolving cinder ring.',base:{hp:58,atk:72,def:52,spd:64},learnset:{1:['ember_nudge','cinder_lash'],12:['kindle_waltz'],20:['flare_burst'],28:['pyre_coil'],38:['sunthread'],48:['spark_chain']},color:'#ff6a3a',shape:1},
dewlet:{id:'dewlet',name:'Dewlet',types:['mire'],lore:'Condensed continuity with a soft loping outline.',base:{hp:48,atk:40,def:50,spd:42},learnset:{1:['drip_tap','mist_veil'],5:['tide_knit'],12:['frost_bead'],20:['undertow'],32:['pressure_spool'],42:['flood_stitch']},color:'#5ab0ff',shape:0},
rivcale:{id:'rivcale',name:'Rivcale',types:['mire'],lore:'Scaled equalizer of flood and stillness.',base:{hp:70,atk:58,def:72,spd:52},learnset:{1:['drip_tap','tide_knit'],12:['frost_bead'],20:['undertow'],28:['pressure_spool'],38:['flood_stitch'],48:['tidal_refresh']},color:'#3a90ee',shape:1},
sprigbit:{id:'sprigbit',name:'Sprigbit',types:['briar'],lore:'Hopping green-thread knot like a seed.',base:{hp:46,atk:46,def:46,spd:44},learnset:{1:['leaf_pin','canopy_guard'],5:['thorn_web'],12:['root_surge'],20:['sap_ribbon'],32:['bloom_burst'],42:['wild_trellis']},color:'#6ddf78',shape:0},
verdance:{id:'verdance',name:'Verdance',types:['briar'],lore:'Canopy that walks on root-tips.',base:{hp:66,atk:68,def:64,spd:58},learnset:{1:['leaf_pin','thorn_web'],12:['root_surge'],20:['sap_ribbon'],28:['bloom_burst'],38:['wild_trellis'],48:['mend_thread']},color:'#48c45a',shape:1},
motegrub:{id:'motegrub',name:'Motegrub',types:['briar'],lore:'Feeds on lint between dream-grass blades.',base:{hp:50,atk:38,def:42,spd:30},learnset:{1:['leaf_pin'],8:['thorn_web'],16:['sap_ribbon'],28:['root_surge']},color:'#8fbf6a',shape:2},
lintowl:{id:'lintowl',name:'Lintowl',types:['wisp','briar'],lore:'Night-watcher of meadow fluff and loft air.',base:{hp:44,atk:48,def:36,spd:60},learnset:{1:['breeze_yawn','leaf_pin'],10:['slipstream'],18:['cyclone_pin'],30:['sky_shear']},color:'#c5d4a8',shape:3},
ashskip:{id:'ashskip',name:'Ashskip',types:['kindle'],lore:'Skates over cinders without cooling them.',base:{hp:40,atk:55,def:35,spd:62},learnset:{1:['ember_nudge'],8:['cinder_lash'],16:['kindle_waltz'],26:['flare_burst'],38:['pyre_coil']},color:'#d96a40',shape:2},
scorchite:{id:'scorchite',name:'Scorchite',types:['kindle','shard'],lore:'Kiln-born crystal that storms when cracked.',base:{hp:52,atk:68,def:58,spd:40},learnset:{1:['pebble_tick','ember_nudge'],10:['fault_line'],20:['cinder_lash'],32:['crystal_spear'],42:['pyre_coil']},color:'#c06040',shape:4},
brumefin:{id:'brumefin',name:'Brumefin',types:['mire'],lore:'Swims through fog as if it were deep water.',base:{hp:55,atk:42,def:48,spd:50},learnset:{1:['drip_tap'],8:['tide_knit'],16:['frost_bead'],28:['undertow'],40:['flood_stitch']},color:'#6aa8c8',shape:5},
bogmirror:{id:'bogmirror',name:'Bogmirror',types:['mire','umbral'],lore:'Reflects what you fear you left undone.',base:{hp:60,atk:50,def:55,spd:45},learnset:{1:['drip_tap','shade_touch'],12:['umbra_tangle'],22:['undertow'],34:['void_pinch'],45:['eclipse_rind']},color:'#4a7088',shape:4},
gritling:{id:'gritling',name:'Gritling',types:['shard'],lore:'Rolling mountain dust with one patient eye.',base:{hp:48,atk:50,def:60,spd:32},learnset:{1:['pebble_tick'],8:['bulwark'],16:['fault_line'],28:['tectonic_hum'],40:['quake_spindle']},color:'#b89870',shape:2},
spirelux:{id:'spirelux',name:'Spirelux',types:['shard','radiant'],lore:'Ridge crystal that bottled a sunrise forever.',base:{hp:55,atk:62,def:58,spd:48},learnset:{1:['pebble_tick','glint'],12:['bright_pin'],24:['crystal_spear'],36:['prism_fall'],46:['lumen_crown']},color:'#e0c080',shape:1},
fluffzeph:{id:'fluffzeph',name:'Fluffzeph',types:['wisp'],lore:'Buoyant tuft fleeter than the wind it rides.',base:{hp:38,atk:44,def:32,spd:70},learnset:{1:['breeze_yawn'],8:['slipstream'],16:['updraft'],26:['cyclone_pin'],38:['sky_shear']},color:'#d0dcff',shape:3},
galehorn:{id:'galehorn',name:'Galehorn',types:['wisp','shard'],lore:'Hollow horn amplifies altitude into a blade.',base:{hp:58,atk:70,def:50,spd:72},learnset:{1:['breeze_yawn','pebble_tick'],14:['cyclone_pin'],24:['sky_shear'],36:['storm_loom'],46:['quake_spindle']},color:'#a8c0e8',shape:1},
lampkin:{id:'lampkin',name:'Lampkin',types:['radiant'],lore:'A lantern that chose to grow legs.',base:{hp:50,atk:48,def:48,spd:48},learnset:{1:['glint'],8:['bright_pin'],16:['halo_cut'],28:['aurora_knit'],40:['prism_fall']},color:'#ffe88a',shape:0},
solstice:{id:'solstice',name:'Solstice',types:['radiant','kindle'],lore:'Year-turning herald rare at the Vault rim.',base:{hp:68,atk:78,def:62,spd:70},learnset:{1:['glint','ember_nudge'],16:['halo_cut'],28:['prism_fall'],38:['sunthread'],48:['lumen_crown']},color:'#ffd45a',shape:1},
duskmite:{id:'duskmite',name:'Duskmite',types:['umbral'],lore:'Nibbles leftover day into evening husks.',base:{hp:42,atk:50,def:40,spd:55},learnset:{1:['shade_touch'],8:['gloom_seed'],16:['umbra_tangle'],28:['void_pinch'],40:['eclipse_rind']},color:'#7a60b0',shape:2},
veilwyrm:{id:'veilwyrm',name:'Veilwyrm',types:['umbral','wisp'],lore:'Serpentine curtain of nightwind and hush.',base:{hp:62,atk:72,def:55,spd:68},learnset:{1:['shade_touch','breeze_yawn'],14:['umbra_tangle'],26:['void_pinch'],36:['eclipse_rind'],46:['null_chorus']},color:'#5a4890',shape:5},
pebbine:{id:'pebbine',name:'Pebbine',types:['shard','mire'],lore:'River stones stacked into almost-eager form.',base:{hp:54,atk:46,def:62,spd:38},learnset:{1:['pebble_tick','drip_tap'],12:['bulwark'],22:['tide_knit'],34:['tectonic_hum']},color:'#8aa0a8',shape:4},
mosspar:{id:'mosspar',name:'Mosspar',types:['briar','shard'],lore:'Speaks in creaks; listens with lichen ears.',base:{hp:65,atk:52,def:70,spd:28},learnset:{1:['leaf_pin','pebble_tick'],12:['canopy_guard'],22:['fault_line'],36:['bloom_burst']},color:'#6a8f5a',shape:4},
glintlet:{id:'glintlet',name:'Glintlet',types:['radiant','wisp'],lore:'Roof-light sliver that chose roaming.',base:{hp:40,atk:56,def:34,spd:68},learnset:{1:['glint','breeze_yawn'],12:['bright_pin'],22:['slipstream'],34:['halo_cut'],44:['prism_fall']},color:'#f0f0c0',shape:3},
sootling:{id:'sootling',name:'Sootling',types:['kindle','umbral'],lore:'Born where incense forgot its prayer.',base:{hp:46,atk:58,def:40,spd:58},learnset:{1:['ember_nudge','shade_touch'],12:['cinder_lash'],22:['gloom_seed'],34:['flare_burst'],44:['eclipse_rind']},color:'#605050',shape:2},
thornarch:{id:'thornarch',name:'Thornarch',types:['briar','shard'],lore:'Living archway of spines and patience.',base:{hp:78,atk:70,def:88,spd:36},learnset:{1:['leaf_pin','bulwark'],20:['fault_line'],32:['bloom_burst'],42:['wild_trellis'],50:['quake_spindle']},color:'#4a7a40',shape:1},
creekforged:{id:'creekforged',name:'Creekforged',types:['mire','shard'],lore:'Fused by water-song into polished will.',base:{hp:72,atk:64,def:82,spd:48},learnset:{1:['pebble_tick','tide_knit'],20:['undertow'],32:['tectonic_hum'],42:['flood_stitch'],50:['pressure_spool']},color:'#7090a0',shape:1},
nightrill:{id:'nightrill',name:'Nightrill',types:['umbral','kindle'],lore:'A flute for quiet flames.',base:{hp:60,atk:80,def:52,spd:76},learnset:{1:['shade_touch','cinder_lash'],24:['void_pinch'],34:['pyre_coil'],44:['eclipse_rind'],50:['null_chorus']},color:'#4a3040',shape:5},
aureline:{id:'aureline',name:'Aureline',types:['radiant','wisp'],lore:'Corridor of traveling dawn.',base:{hp:58,atk:76,def:50,spd:88},learnset:{1:['glint','slipstream'],24:['halo_cut'],34:['sky_shear'],44:['prism_fall'],50:['lumen_crown']},color:'#fff0a0',shape:5}
};
const RESHAPE={pipglow:{to:'cindrel',level:18},dewlet:{to:'rivcale',level:18},sprigbit:{to:'verdance',level:18},fluffzeph:{to:'galehorn',level:26,item:'zephyr_knot'},lampkin:{to:'solstice',level:30,item:'sol_thread'},duskmite:{to:'veilwyrm',level:28,item:'night_spindle'},mosspar:{to:'thornarch',level:32,item:'root_catalyst'},pebbine:{to:'creekforged',level:30,item:'tide_core'},sootling:{to:'nightrill',level:28,item:'ash_filament'},glintlet:{to:'aureline',level:30,item:'sol_thread'}};
const ITEMS={
bindspool_c:{name:'Common Bindspool',kind:'bind',tier:1,price:30,desc:'Basic bond fiber.'},bindspool_f:{name:'Fine Bindspool',kind:'bind',tier:2,price:90,desc:'Tighter weave.'},
bindspool_m:{name:'Master Bindspool',kind:'bind',tier:3,price:220,desc:'Masterwork fiber.'},mend_salve:{name:'Mend Salve',kind:'heal',heal:40,price:25,desc:'Restores 40 HP.'},
full_reel:{name:'Full Reel',kind:'heal',heal:9999,price:120,desc:'Full heal one Kith.'},status_wash:{name:'Status Wash',kind:'heal',clear:true,price:40,desc:'Clear status.'},
circle_balm:{name:'Circle Balm',kind:'heal',party:true,heal:30,price:80,desc:'Heal all by 30.'},zephyr_knot:{name:'Zephyr Knot',kind:'catalyst',price:150,desc:'Airy reshape.'},
sol_thread:{name:'Sol Thread',kind:'catalyst',price:180,desc:'Luminous reshape.'},night_spindle:{name:'Night Spindle',kind:'catalyst',price:170,desc:'Umbral reshape.'},
root_catalyst:{name:'Root Catalyst',kind:'catalyst',price:160,desc:'Woody reshape.'},tide_core:{name:'Tide Core',kind:'catalyst',price:160,desc:'Creek reshape.'},ash_filament:{name:'Ash Filament',kind:'catalyst',price:160,desc:'Smolder reshape.'}
};
const ZONES=[
{id:0,name:'Dewthread Meadows',bg:['#1a3a28','#0e2218','#152a40'],particle:'pollen',encounterRate:.14,wild:[['motegrub',3,6],['lintowl',3,7],['sprigbit',4,7],['dewlet',3,6],['pipglow',3,6]],shrine:{x:8,y:6},storyNode:{x:14,y:8},boss:null,spawn:{x:5,y:8},mapColor:'#3d8f5a'},
{id:1,name:'Cinderhollow Reach',bg:['#3a1a12','#1a0c0a','#402018'],particle:'embers',encounterRate:.16,wild:[['ashskip',7,12],['scorchite',8,13],['sootling',8,12],['motegrub',6,10],['pipglow',7,11]],shrine:{x:6,y:10},storyNode:{x:15,y:5},boss:{species:'scorchite',level:14,name:'Kindle Anchor: Scoriaform',bonus:'atk'},spawn:{x:3,y:12},mapColor:'#c45a30'},
{id:2,name:'Gloamfen Marsh',bg:['#0e2430','#0a1818','#142838'],particle:'mist',encounterRate:.17,wild:[['brumefin',11,16],['bogmirror',12,17],['duskmite',11,16],['dewlet',10,14],['pebbine',12,16]],shrine:{x:10,y:11},storyNode:{x:4,y:6},boss:{species:'bogmirror',level:20,name:'Mire Anchor: Stillvast',bonus:'def'},spawn:{x:12,y:13},mapColor:'#3a7a8a'},
{id:3,name:'Crystalspine Ridge',bg:['#242838','#14182a','#2a2030'],particle:'glints',encounterRate:.15,wild:[['gritling',15,20],['spirelux',16,21],['mosspar',15,20],['pebbine',14,19],['scorchite',15,19]],shrine:{x:7,y:7},storyNode:{x:16,y:12},boss:{species:'spirelux',level:26,name:'Shard Anchor: Prismarch',bonus:'hp'},spawn:{x:4,y:8},mapColor:'#8a7a60'},
{id:4,name:'Skirlwind Plateau',bg:['#203040','#152030','#283848'],particle:'gust',encounterRate:.16,wild:[['fluffzeph',20,26],['lintowl',19,24],['glintlet',20,25],['galehorn',22,27],['ashskip',18,23]],shrine:{x:12,y:5},storyNode:{x:6,y:10},boss:{species:'galehorn',level:32,name:'Wisp Anchor: Skirllord',bonus:'spd'},spawn:{x:14,y:6},mapColor:'#7aa0c8'},
{id:5,name:'Nightloom Vault',bg:['#12081c','#0a0614','#1a1030'],particle:'void',encounterRate:.18,wild:[['duskmite',25,32],['veilwyrm',26,34],['solstice',27,33],['nightrill',26,32],['lampkin',24,30]],shrine:{x:9,y:9},storyNode:{x:15,y:4},boss:{species:'veilwyrm',level:38,name:'Umbral Anchor: Nightwoven',bonus:'all'},spawn:{x:5,y:12},mapColor:'#6a4aa0'}
];
const STORY={0:[{sp:'Meadow Echo',t:'The first Oasis still smiles, but Anchor roads beyond grow hungry.'},{sp:'Meadow Echo',t:'Bond eight Kith. Unravel four Anchors. The Nightloom will then open its quiet.'}],
1:[{sp:'Scorched Chorus',t:'Cinderhollow was a forge of care. Its Anchor now only remembers heat.'},{sp:'Scorched Chorus',t:'Defeat the Kindle Anchor for permanent ATK in your Circle.'}],
2:[{sp:'Fen Whisper',t:'Slow water remembers. Stillvast nests in reflection.'},{sp:'Fen Whisper',t:'When the Mire Anchor yields, defenses thicken like packed reed.'}],
3:[{sp:'Ridge Memory',t:'Crystalspine stores every snagged sunrise.'},{sp:'Ridge Memory',t:'Break Prismarch — vitality expands across your Circle.'}],
4:[{sp:'High Wind',t:'Up here names peel off. Only speed remains honest.'},{sp:'High Wind',t:'Claim Skirllord and your Circle outpaces storms.'}],
5:[{sp:'Nightloom',t:'Last knots of sewn night. The Fabric watches through you.'},{sp:'Nightloom',t:'Unravel Nightwoven. If eight Kith stand with you, Aevum exhales.'}],
end:[{sp:'Aevum',t:'Anchors quiet. Bonds complete. Freely roam — wild Kith grow denser and prouder.'},{sp:'Aevum',t:'You remain Binder. The Weave remains unfinished by design.'}]};
const TILE=32,MAP_W=20,MAP_H=15;
function makeMap(zid){const g=[];for(let y=0;y<MAP_H;y++){const row=[];for(let x=0;x<MAP_W;x++){let t=0;if(x===0||y===0||x===MAP_W-1||y===MAP_H-1)t=1;else{const n=Math.sin(x*1.7+zid*3)+Math.cos(y*1.3+zid);if(n>1.1)t=1;else if(n>.35)t=2;else if(n<-1.05)t=3;else t=0;}row.push(t);}g.push(row);}
const z=ZONES[zid];const carve=(cx,cy,r=2)=>{for(let y=cy-r;y<=cy+r;y++)for(let x=cx-r;x<=cx+r;x++)if(x>0&&y>0&&x<MAP_W-1&&y<MAP_H-1)g[y][x]=0;};
carve(z.shrine.x,z.shrine.y,2);carve(z.spawn.x,z.spawn.y,1);if(z.storyNode)carve(z.storyNode.x,z.storyNode.y,1);if(z.boss)carve(16,3,2);
let px=z.spawn.x,py=z.spawn.y;while(px!==z.shrine.x||py!==z.shrine.y){g[py][px]=0;if(px<z.shrine.x)px++;else if(px>z.shrine.x)px--;else if(py<z.shrine.y)py++;else py--;}g[z.shrine.y][z.shrine.x]=0;return g;}
const canvas=document.getElementById('game'),ctx=canvas.getContext('2d');
const W=960,H=540,$=id=>document.getElementById(id);
const S={mode:'title',player:{x:5,y:8,px:5,py:8,fromX:5,fromY:8,facing:0,moving:false,moveT:0},zone:0,maps:{},party:[],box:[],
inventory:{bindspool_c:5,bindspool_f:1,bindspool_m:0,mend_salve:3,full_reel:0,status_wash:1,circle_balm:0,zephyr_knot:0,sol_thread:0,night_spindle:0,root_catalyst:0,tide_core:0,ash_filament:0},
currency:120,seen:{},caught:{},flags:{storyStage:0,bossesDefeated:[],storySeen:{},zoneUnlocked:[0,1],postgame:false,uniqueCaught:0,tutBattle:false,tutBind:false},
bonuses:{atk:0,def:0,hp:0,spd:0,all:0},lastShrine:{zone:0,x:8,y:6},battle:null,ambient:[],keys:{},touchDir:null,mute:false,animT:0,encounterCool:0,nextUid:1,returnMode:'overworld'};
function uid(){return S.nextUid++;}
function expToNext(l){return Math.floor(18+l*12+l*l*.9);}
function statsAtLevel(sp,level){const L=Math.max(1,Math.min(50,level|0)),f=1+(L-1)*.032,b=sp.base,bo=S.bonuses,all=bo.all||0;return{maxHp:Math.floor(b.hp*f*2.2+L*1.8+(bo.hp||0)*3+all*2),atk:Math.floor(b.atk*f+L*.55+(bo.atk||0)*2+all),def:Math.floor(b.def*f+L*.5+(bo.def||0)*2+all),spd:Math.floor(b.spd*f+L*.55+(bo.spd||0)*2+all)};}
function movesForLevel(sp,level){const learned=[],ls=sp.learnset||{};Object.keys(ls).map(Number).sort((a,b)=>a-b).forEach(lv=>{if(lv<=level)ls[lv].forEach(m=>{if(!learned.includes(m))learned.push(m);});});return learned.slice(-4).map(id=>({id,pp:AB[id].pp,maxPp:AB[id].pp}));}
function makeKith(sid,level){const sp=SP[sid],st=statsAtLevel(sp,level);return{uid:uid(),speciesId:sid,name:sp.name,level,exp:0,expTo:expToNext(level),hp:st.maxHp,maxHp:st.maxHp,atk:st.atk,def:st.def,spd:st.spd,types:sp.types.slice(),moves:movesForLevel(sp,level),status:null,statusTurns:0,stages:{atk:0,def:0,spd:0},barrier:false,regen:0};}
function refreshStats(k){const sp=SP[k.speciesId],st=statsAtLevel(sp,k.level),ratio=k.maxHp>0?k.hp/k.maxHp:1;k.maxHp=st.maxHp;k.atk=st.atk;k.def=st.def;k.spd=st.spd;k.hp=Math.max(1,Math.min(k.maxHp,Math.round(ratio*k.maxHp)));const want=movesForLevel(sp,k.level),have=new Set(k.moves.map(m=>m.id));for(const m of want)if(!have.has(m.id)){if(k.moves.length<4)k.moves.push({id:m.id,pp:AB[m.id].pp,maxPp:AB[m.id].pp});else k.moves[0]={id:m.id,pp:AB[m.id].pp,maxPp:AB[m.id].pp};toast(k.name+' wove '+AB[m.id].name+'!');}}
function checkReshape(k){const r=RESHAPE[k.speciesId];if(!r||k.level<r.level)return false;if(r.item&&!(S.inventory[r.item]>0))return false;if(r.item)S.inventory[r.item]--;const old=k.name,sp=SP[r.to];k.speciesId=r.to;k.name=sp.name;k.types=sp.types.slice();refreshStats(k);k.hp=k.maxHp;k.moves=movesForLevel(sp,k.level);S.seen[r.to]=S.caught[r.to]=true;recountUnique();toast(old+' reshaped into '+k.name+'!');return true;}
function gainExp(k,amount){if(k.level>=50)return;k.exp+=amount;while(k.exp>=k.expTo&&k.level<50){k.exp-=k.expTo;k.level++;k.expTo=expToNext(k.level);refreshStats(k);k.hp=k.maxHp;toast(k.name+' reached weave-depth '+k.level+'!');checkReshape(k);}}
function stageMul(s){const t=Math.max(-6,Math.min(6,s||0));return t>=0?1+t*.25:1/(1+(-t)*.25);}
function effStat(k,w){let v=k[w];if(w==='atk'&&k.status==='burn')v=Math.floor(v*.5);if(w==='spd'&&k.status==='frost')v=Math.floor(v*.5);return Math.max(1,Math.floor(v*stageMul(k.stages[w]||0)));}
function recountUnique(){S.flags.uniqueCaught=Object.keys(S.caught).filter(k=>S.caught[k]).length;}
const SAVE_KEY='kithbind_aevum_v1';
function serK(k){return{uid:k.uid,speciesId:k.speciesId,name:k.name,level:k.level,exp:k.exp,expTo:k.expTo,hp:k.hp,maxHp:k.maxHp,atk:k.atk,def:k.def,spd:k.spd,types:k.types,moves:k.moves.map(m=>({id:m.id,pp:m.pp,maxPp:m.maxPp})),status:k.status};}
function desK(o){const k=makeKith(o.speciesId,o.level);Object.assign(k,o);k.moves=(o.moves||[]).map(m=>({id:m.id,pp:m.pp,maxPp:m.maxPp||AB[m.id].pp}));k.stages={atk:0,def:0,spd:0};k.barrier=false;k.regen=0;k.statusTurns=0;return k;}
function saveGame(){try{const all=JSON.parse(localStorage.getItem(SAVE_KEY)||'{}');all.slot0={party:S.party.map(serK),box:S.box.map(serK),inventory:S.inventory,currency:S.currency,seen:S.seen,caught:S.caught,flags:S.flags,bonuses:S.bonuses,lastShrine:S.lastShrine,nextUid:S.nextUid,zone:S.zone};localStorage.setItem(SAVE_KEY,JSON.stringify(all));toast('Progress sealed at Loomshrine.');return true;}catch(e){toast('Seal failed.');return false;}}
function loadGame(){try{const d=JSON.parse(localStorage.getItem(SAVE_KEY)||'{}').slot0;if(!d)return false;S.lastShrine=d.lastShrine||{zone:0,x:8,y:6};S.zone=S.lastShrine.zone;S.player.x=S.lastShrine.x;S.player.y=S.lastShrine.y;S.player.px=S.player.fromX=S.player.x;S.player.py=S.player.fromY=S.player.y;S.party=(d.party||[]).map(desK);S.box=(d.box||[]).map(desK);S.inventory=Object.assign({},S.inventory,d.inventory||{});S.currency=d.currency||0;S.seen=d.seen||{};S.caught=d.caught||{};S.flags=Object.assign({},S.flags,d.flags||{});S.bonuses=Object.assign({atk:0,def:0,hp:0,spd:0,all:0},d.bonuses||{});S.nextUid=d.nextUid||100;ensureMaps();recountUnique();return true;}catch(e){return false;}}
function hasSave(){try{return!!JSON.parse(localStorage.getItem(SAVE_KEY)||'{}').slot0;}catch(e){return false;}}
function ensureMaps(){for(let i=0;i<6;i++)if(!S.maps[i])S.maps[i]=makeMap(i);}
function isWalkable(zx,x,y){const m=S.maps[zx];return!!(m&&y>=0&&x>=0&&y<MAP_H&&x<MAP_W&&m[y][x]!==1);}
function toast(msg){const el=document.createElement('div');el.className='t';el.textContent=msg;$('toast').appendChild(el);setTimeout(()=>el.remove(),2800);}
function fieldMsg(msg){const el=document.createElement('div');el.className='line';el.textContent=msg;$('msgLog').appendChild(el);setTimeout(()=>el.remove(),3000);}
function typeBadge(t){const T=TYPES[t];return T?`<span class="badge" style="background:${T.hex}33;color:${T.hex};border:1px solid ${T.hex}66">${T.name}</span>`:'';}
function openModal(id){$(id).classList.add('open');}function closeModal(id){$(id).classList.remove('open');}
let dlgQueue=[],dlgResolve=null;
function showDialogue(lines,vignette,returnMode){return new Promise(res=>{dlgQueue=lines.slice();dlgResolve=res;S.returnMode=returnMode||(S.battle?'battle':'overworld');S.mode='dialogue';$('dialogue').classList.add('open');const v=$('vignette');v.className='';if(vignette)v.classList.add('show',vignette);else v.classList.add('show');nextDialogue();});}
function nextDialogue(){if(!dlgQueue.length){$('dialogue').classList.remove('open');$('vignette').className='';S.mode=S.returnMode||'overworld';const r=dlgResolve;dlgResolve=null;if(r)r();return;}const line=dlgQueue.shift();$('dlgSpeaker').textContent=line.sp||'';$('dlgText').textContent=line.t||'';}
let actx=null,masterGain=null;
function initAudio(){if(actx)return;actx=new(window.AudioContext||window.webkitAudioContext)();masterGain=actx.createGain();masterGain.gain.value=S.mute?0:.12;masterGain.connect(actx.destination);[110,165,220].forEach((f,i)=>{const o=actx.createOscillator(),g=actx.createGain();o.type=i===2?'triangle':'sine';o.frequency.value=f;g.gain.value=.04/(i+1);o.connect(g);g.connect(masterGain);o.start();});}
function setMute(m){S.mute=m;if(masterGain)masterGain.gain.value=m?0:.12;$('muteBtn').textContent=m?'♫':'♪';}
function beep(freq,dur,type='square',vol=.08){if(!actx||S.mute)return;const o=actx.createOscillator(),g=actx.createGain();o.type=type;o.frequency.value=freq;g.gain.value=vol;g.gain.exponentialRampToValueAtTime(.001,actx.currentTime+dur);o.connect(g);g.connect(masterGain);o.start();o.stop(actx.currentTime+dur);}
function sfxHit(s){beep(s?520:240,.08,'square',.1);if(s)setTimeout(()=>beep(780,.1,'square',.08),50);}
function sfxUI(){beep(440,.05,'sine',.06);}function sfxHeal(){beep(520,.08,'sine',.07);setTimeout(()=>beep(660,.1,'sine',.07),70);}
function sfxCapture(ok){if(ok){beep(400,.08,'sine');setTimeout(()=>beep(600,.1,'sine'),80);setTimeout(()=>beep(800,.15,'sine'),160);}else beep(200,.15,'sawtooth',.08);}
function sfxBattleStart(){beep(180,.1);setTimeout(()=>beep(240,.1),80);setTimeout(()=>beep(360,.15),160);}
function fitCanvas(){const app=$('app'),scale=Math.min(app.clientWidth/960,app.clientHeight/540);canvas.style.width=Math.floor(960*scale)+'px';canvas.style.height=Math.floor(540*scale)+'px';}
window.addEventListener('resize',fitCanvas);
function isTouch(){return matchMedia('(pointer:coarse)').matches||'ontouchstart' in window;}
function wait(ms){return new Promise(r=>setTimeout(r,ms));}
function shade(hex,amt){if(!hex||hex[0]!=='#')return hex;let r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);return`rgb(${Math.max(0,Math.min(255,r+amt))},${Math.max(0,Math.min(255,g+amt))},${Math.max(0,Math.min(255,b+amt))})`;}
function canEnterZone(id){if(id<0||id>5)return false;if(id<=1)return true;return S.flags.bossesDefeated.includes(id-1)||S.flags.zoneUnlocked.includes(id);}
function drawKithSprite(x,y,sp,scale=1){ctx.save();ctx.translate(x,y);ctx.scale(scale,scale);ctx.fillStyle=sp.color||'#aaa';ctx.strokeStyle='rgba(0,0,0,.35)';ctx.lineWidth=2;const sh=sp.shape|0;ctx.beginPath();
if(sh===0)ctx.ellipse(0,0,14,12,0,0,Math.PI*2);else if(sh===1){ctx.moveTo(0,-18);ctx.quadraticCurveTo(16,-4,12,14);ctx.lineTo(-12,14);ctx.quadraticCurveTo(-16,-4,0,-18);}else if(sh===2)ctx.ellipse(0,2,16,11,0,0,Math.PI*2);
else if(sh===3){ctx.ellipse(0,0,12,10,0,0,Math.PI*2);ctx.moveTo(-12,0);ctx.quadraticCurveTo(-28,-10,-24,6);ctx.lineTo(-12,2);ctx.moveTo(12,0);ctx.quadraticCurveTo(28,-10,24,6);ctx.lineTo(12,2);}
else if(sh===4){ctx.moveTo(0,-16);ctx.lineTo(14,0);ctx.lineTo(0,16);ctx.lineTo(-14,0);ctx.closePath();}else{ctx.ellipse(-8,4,10,8,-.4,0,Math.PI*2);ctx.ellipse(6,-2,12,7,.3,0,Math.PI*2);}
ctx.fill();ctx.stroke();ctx.fillStyle='#1a1020';ctx.beginPath();ctx.arc(-4,-2,2,0,Math.PI*2);ctx.arc(5,-2,2,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(-3.2,-2.5,.8,0,Math.PI*2);ctx.arc(5.8,-2.5,.8,0,Math.PI*2);ctx.fill();
const t=sp.types[0];if(TYPES[t]){ctx.fillStyle=TYPES[t].hex;ctx.globalAlpha=.55;ctx.beginPath();ctx.arc(0,10,3,0,Math.PI*2);ctx.fill();}ctx.restore();}
function spawnAmbient(zone){if(S.ambient.length>40)return;const kind=ZONES[zone].particle;S.ambient.push({kind,x:Math.random()*W,y:Math.random()*H,vx:(Math.random()-.5)*.4,vy:-.2-Math.random()*.5,life:100+Math.random()*100,r:1+Math.random()*2,a:.3+Math.random()*.5});}
function updateAmbient(){for(const p of S.ambient){p.x+=p.vx;p.y+=p.vy;p.life--;if(p.kind==='embers'){p.vy-=.01;p.vx+=Math.sin(S.animT*.05+p.x)*.01;}if(p.kind==='mist'){p.vx=Math.sin(S.animT*.02+p.y*.01)*.3;p.vy=-.05;}if(p.kind==='gust'){p.vx+=.02;p.vy=Math.sin(p.x*.05)*.2;}if(p.kind==='void'){p.vx=Math.cos(S.animT*.03+p.y)*.4;p.vy=Math.sin(S.animT*.03+p.x)*.4;}if(p.kind==='glints'){p.vy=-.3;p.a=.4+Math.sin(S.animT*.2+p.x)*.4;}}S.ambient=S.ambient.filter(p=>p.life>0&&p.y>-10&&p.x>-10&&p.x<W+10);}
function drawAmbient(){for(const p of S.ambient){ctx.globalAlpha=Math.max(0,p.a*(p.life/120));ctx.fillStyle=p.kind==='embers'?'#ff8a4a':p.kind==='pollen'?'#c8ff9a':p.kind==='mist'?'#a0c8d0':p.kind==='glints'?'#ffe6a0':p.kind==='gust'?'#c0d8ff':'#a080ff';ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.fill();}ctx.globalAlpha=1;}
function camera(){const px=S.player.px*TILE+TILE/2,py=S.player.py*TILE+TILE/2;let cx=px-W/2,cy=py-H/2;const maxX=MAP_W*TILE-W,maxY=MAP_H*TILE-H;if(maxX>0)cx=Math.max(0,Math.min(maxX,cx));else cx=(MAP_W*TILE-W)/2;if(maxY>0)cy=Math.max(0,Math.min(maxY,cy));else cy=(MAP_H*TILE-H)/2;return{cx,cy};}
function drawOverworld(){const z=ZONES[S.zone],cols=z.bg,g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,cols[0]);g.addColorStop(.55,cols[1]);g.addColorStop(1,cols[2]);ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
const cam=camera(),map=S.maps[S.zone];for(let y=0;y<MAP_H;y++)for(let x=0;x<MAP_W;x++){const sx=x*TILE-cam.cx,sy=y*TILE-cam.cy;if(sx<-TILE||sy<-TILE||sx>W||sy>H)continue;const t=map[y][x];
if(t===1){ctx.fillStyle=shade(cols[1],-30);ctx.fillRect(sx,sy,TILE,TILE);ctx.fillStyle=shade(cols[0],-10);ctx.fillRect(sx+2,sy+2,TILE-4,TILE-4);}
else if(t===2){ctx.fillStyle=shade(cols[1],10);ctx.fillRect(sx,sy,TILE,TILE);ctx.fillStyle=shade(cols[0],25);for(let i=0;i<4;i++){const bx=sx+6+i*6,by=sy+TILE-4;ctx.beginPath();ctx.moveTo(bx,by);ctx.lineTo(bx+2,by-10-Math.sin(S.animT*.08+x+i)*2);ctx.lineTo(bx+4,by);ctx.fill();}}
else if(t===3){ctx.fillStyle=shade(cols[2],40);ctx.fillRect(sx,sy,TILE,TILE);ctx.fillStyle='rgba(120,180,220,.25)';ctx.fillRect(sx,sy+Math.sin(S.animT*.05+x)*2,TILE,TILE);}
else{ctx.fillStyle=shade(cols[1],5);ctx.fillRect(sx,sy,TILE,TILE);ctx.strokeStyle='rgba(255,255,255,.03)';ctx.strokeRect(sx+.5,sy+.5,TILE-1,TILE-1);}}
// shrine
const shx=z.shrine.x*TILE-cam.cx+TILE/2,shy=z.shrine.y*TILE-cam.cy+TILE/2;ctx.save();ctx.translate(shx,shy);ctx.fillStyle='rgba(160,200,255,.15)';ctx.beginPath();ctx.arc(0,0,16+Math.sin(S.animT*.08)*2,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#9ec8ff';ctx.lineWidth=2;ctx.beginPath();ctx.arc(0,4,12,Math.PI,0);ctx.stroke();ctx.beginPath();ctx.moveTo(-12,4);ctx.lineTo(-12,14);ctx.moveTo(12,4);ctx.lineTo(12,14);ctx.stroke();ctx.fillStyle='#cfe2ff';ctx.beginPath();ctx.arc(0,0,3,0,Math.PI*2);ctx.fill();ctx.restore();
if(z.storyNode&&!S.flags.storySeen[S.zone]){const sn=z.storyNode,sx=sn.x*TILE-cam.cx+TILE/2,sy=sn.y*TILE-cam.cy+TILE/2;ctx.save();ctx.translate(sx,sy+Math.sin(S.animT*.1)*3);ctx.fillStyle='rgba(160,220,255,.25)';ctx.beginPath();ctx.arc(0,0,14,0,Math.PI*2);ctx.fill();ctx.fillStyle='#b8e0ff';ctx.beginPath();ctx.arc(0,0,6,0,Math.PI*2);ctx.fill();ctx.restore();}
if(z.boss&&!S.flags.bossesDefeated.includes(S.zone)){const bx=16*TILE-cam.cx+TILE/2,by=3*TILE-cam.cy+TILE/2;ctx.fillStyle='rgba(255,100,80,.25)';ctx.beginPath();ctx.arc(bx,by,18+Math.sin(S.animT*.12)*3,0,Math.PI*2);ctx.fill();drawKithSprite(bx,by,SP[z.boss.species],.9);}
// gates
if(S.zone<5){const nx=(MAP_W-2)*TILE-cam.cx+TILE/2,ny=7*TILE-cam.cy+TILE/2;ctx.fillStyle=canEnterZone(S.zone+1)?'rgba(120,255,180,.3)':'rgba(255,80,80,.2)';ctx.beginPath();ctx.arc(nx,ny,10,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.font='10px sans-serif';ctx.textAlign='center';ctx.fillText(canEnterZone(S.zone+1)?'▸':'✕',nx,ny+3);}
if(S.zone>0){const px=1*TILE-cam.cx+TILE/2,py=7*TILE-cam.cy+TILE/2;ctx.fillStyle='rgba(120,180,255,.3)';ctx.beginPath();ctx.arc(px,py,10,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.font='10px sans-serif';ctx.textAlign='center';ctx.fillText('◂',px,py+3);}
// player
const ppx=S.player.px*TILE-cam.cx+TILE/2,ppy=S.player.py*TILE-cam.cy+TILE/2;ctx.save();ctx.translate(ppx,ppy);ctx.fillStyle='#3a4a78';ctx.beginPath();ctx.ellipse(0,6,10,12,0,0,Math.PI*2);ctx.fill();ctx.fillStyle='#e8eefc';ctx.beginPath();ctx.arc(0,-4,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#7eb6ff';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(-6,4);ctx.lineTo(6,8);ctx.stroke();const f=S.player.facing;ctx.fillStyle='#9dffe0';if(f===0)ctx.fillRect(-1,-14,2,4);if(f===1)ctx.fillRect(8,-1,4,2);if(f===2)ctx.fillRect(-1,14,2,4);if(f===3)ctx.fillRect(-12,-1,4,2);ctx.restore();
drawAmbient();const vg=ctx.createRadialGradient(W/2,H/2,H*.2,W/2,H/2,H*.75);vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,'rgba(0,0,0,.35)');ctx.fillStyle=vg;ctx.fillRect(0,0,W,H);}
function tryMove(dx,dy){if(S.mode!=='overworld'||S.player.moving)return;if(!dx&&!dy)return;if(dy<0)S.player.facing=0;else if(dx>0)S.player.facing=1;else if(dy>0)S.player.facing=2;else S.player.facing=3;
const nx=S.player.x+dx,ny=S.player.y+dy;
if(nx===MAP_W-2&&ny===7&&S.zone<5){if(!canEnterZone(S.zone+1)){fieldMsg('The seam is sealed. Defeat the local Anchor first.');return;}enterZone(S.zone+1,'fromWest');return;}
if(nx===1&&ny===7&&S.zone>0){enterZone(S.zone-1,'fromEast');return;}
if(!isWalkable(S.zone,nx,ny))return;S.player.fromX=S.player.x;S.player.fromY=S.player.y;S.player.x=nx;S.player.y=ny;S.player.moving=true;S.player.moveT=0;S.encounterCool=Math.max(0,S.encounterCool-1);}
function enterZone(id,from){S.zone=id;if(!S.flags.zoneUnlocked.includes(id))S.flags.zoneUnlocked.push(id);ensureMaps();if(from==='fromWest'){S.player.x=2;S.player.y=7;}else if(from==='fromEast'){S.player.x=MAP_W-3;S.player.y=7;}else{S.player.x=ZONES[id].spawn.x;S.player.y=ZONES[id].spawn.y;}S.player.px=S.player.fromX=S.player.x;S.player.py=S.player.fromY=S.player.y;S.player.moving=false;updateZoneLabel();fieldMsg('Entered '+ZONES[id].name);beep(330,.08,'sine');}
function updateZoneLabel(){const z=ZONES[S.zone];const anchors=[1,2,3,4].filter(z=>S.flags.bossesDefeated.includes(z)).length;$('zoneLabel').innerHTML=`<strong>${z.name}</strong><span>Oasis ${S.zone+1}/6 · Bonds ${S.flags.uniqueCaught}/8 · Anchors ${anchors}/4</span>`;}
function updateMovement(dt){if(S.mode!=='overworld')return;if(!S.player.moving){let dx=0,dy=0;if(S.keys['arrowup']||S.keys['w'])dy=-1;else if(S.keys['arrowdown']||S.keys['s'])dy=1;else if(S.keys['arrowleft']||S.keys['a'])dx=-1;else if(S.keys['arrowright']||S.keys['d'])dx=1;if(S.touchDir){dx=S.touchDir[0];dy=S.touchDir[1];}if(dx||dy)tryMove(dx,dy);S.player.px=S.player.x;S.player.py=S.player.y;}else{S.player.moveT+=dt*7;const t=Math.min(1,S.player.moveT),e=t*t*(3-2*t);S.player.px=S.player.fromX+(S.player.x-S.player.fromX)*e;S.player.py=S.player.fromY+(S.player.y-S.player.fromY)*e;if(t>=1){S.player.moving=false;S.player.px=S.player.x;S.player.py=S.player.y;onTileLand();}}}
function onTileLand(){const z=ZONES[S.zone],x=S.player.x,y=S.player.y;if(x===z.shrine.x&&y===z.shrine.y){openShrine();return;}if(z.storyNode&&x===z.storyNode.x&&y===z.storyNode.y&&!S.flags.storySeen[S.zone]){triggerStoryNode(S.zone);return;}if(z.boss&&!S.flags.bossesDefeated.includes(S.zone)&&x===16&&y===3){startBossBattle(z.boss);return;}const map=S.maps[S.zone];if(map[y][x]===2&&S.encounterCool<=0){let rate=z.encounterRate+(S.flags.postgame?0.08:0);if(Math.random()<rate)startWildBattle();}}
function triggerStoryNode(zoneId){S.flags.storySeen[zoneId]=true;const lines=STORY[zoneId];if(!lines)return;const vig=['story-green','story-ember','story-blue','story-green','story-blue','story-void'][zoneId];showDialogue(lines,vig);}
function openShrine(){S.mode='shrine';S.lastShrine={zone:S.zone,x:S.player.x,y:S.player.y};$('shrineBlurb').textContent='Loomshrine of '+ZONES[S.zone].name+' — rest, seal, or trade Glintspun.';$('shrineShopPane').classList.add('hidden');openModal('shrineModal');sfxHeal();}
function startWildBattle(){const z=ZONES[S.zone];let table=z.wild.slice();if(S.flags.postgame)table=table.map(e=>[e[0],e[1]+6,e[2]+8]);const n=Math.random()<.18?(Math.random()<.4?3:2):1;const enemies=[];for(let i=0;i<n;i++){const pick=table[Math.floor(Math.random()*table.length)];const lv=pick[1]+Math.floor(Math.random()*(pick[2]-pick[1]+1));const k=makeKith(pick[0],Math.min(50,lv));S.seen[pick[0]]=true;enemies.push(k);}beginBattle(enemies,{wild:true,canFlee:true,canBind:true});}
function startBossBattle(boss){const k=makeKith(boss.species,boss.level);k.name=boss.name;k.maxHp=Math.floor(k.maxHp*1.6);k.hp=k.maxHp;k.atk=Math.floor(k.atk*1.2);k.def=Math.floor(k.def*1.15);S.seen[boss.species]=true;const enemies=[k];if(S.zone>=3)enemies.push(makeKith(ZONES[S.zone].wild[0][0],boss.level-4));beginBattle(enemies,{wild:false,boss:true,bossZone:S.zone,bossBonus:boss.bonus,canFlee:false,canBind:false,title:boss.name});showDialogue([{sp:'Anchor',t:boss.name+' threads itself into presence.'}],'story-ember','battle');}
function beginBattle(enemies,meta){sfxBattleStart();const lead=S.party.find(k=>k.hp>0)||S.party[0];if(!lead){toast('No Kith able to fight!');return;}S.battle={enemies,activeEnemy:0,activePlayer:S.party.indexOf(lead),meta,phase:'intro',busy:false,shake:0,particles:[]};S.mode='battle';$('battleUI').classList.add('active');setBattleMsg(meta.title?meta.title+' appears!':'Wild Kith stir from the weave!');showBattleMain();renderBattleHuds();setTimeout(()=>{if(S.battle){S.battle.phase='choose';setBattleMsg('Choose a channel.');}},600);}
function endBattle(result){const b=S.battle;S.battle=null;$('battleUI').classList.remove('active');$('battleMoves').classList.add('hidden');$('battleSub').classList.add('hidden');$('battleMainMenu').classList.remove('hidden');S.mode='overworld';S.encounterCool=4;
if(result==='win'&&b){const totalLv=b.enemies.reduce((s,e)=>s+e.level,0);for(const k of S.party){if(k.hp<=0)continue;const rel=Math.max(.5,Math.min(1.5,totalLv/Math.max(1,k.level*b.enemies.length)));gainExp(k,Math.floor((12+totalLv*3)*rel/Math.max(1,S.party.filter(p=>p.hp>0).length)));}S.currency+=8+totalLv*2;fieldMsg('Victory. Glintspun +'+(8+totalLv*2));
if(b.meta.boss){const zid=b.meta.bossZone;if(!S.flags.bossesDefeated.includes(zid)){S.flags.bossesDefeated.push(zid);const bonus=b.meta.bossBonus;if(bonus==='all')S.bonuses.all+=2;else S.bonuses[bonus]=(S.bonuses[bonus]||0)+2;for(const k of S.party)refreshStats(k);toast('Anchor unraveled! Permanent bonus gained.');if(zid<5&&!S.flags.zoneUnlocked.includes(zid+1)){S.flags.zoneUnlocked.push(zid+1);toast(ZONES[zid+1].name+' seam unlocked.');}checkVictory();}}
if(!S.flags.tutBattle){S.flags.tutBattle=true;showDialogue([{sp:'Loomshrine',t:'Well woven. Weaken a foe, then use Bind with a Bindspool to capture them — need eight unique bonds.'}],'story-green');}
}else if(result==='lose'){toast('The Circle unravels… waking at last Loomshrine.');for(const k of S.party){k.hp=Math.max(1,Math.floor(k.maxHp*.5));k.status=null;}S.zone=S.lastShrine.zone;S.player.x=S.lastShrine.x;S.player.y=S.lastShrine.y;S.player.px=S.player.fromX=S.player.x;S.player.py=S.player.fromY=S.player.y;updateZoneLabel();}}
function checkVictory(){const unique=S.flags.uniqueCaught,four=[1,2,3,4].filter(z=>S.flags.bossesDefeated.includes(z)).length;if(unique>=8&&four>=4&&!S.flags.postgame){S.flags.postgame=true;showDialogue(STORY.end,'story-void');toast('Post-thread free roam — wild Kith grow fiercer.');}if(S.flags.bossesDefeated.includes(5)&&unique>=8){S.flags.postgame=true;showDialogue(STORY.end,'story-void');}}
function setBattleMsg(t){$('battleMsg').textContent=t||'';}
function activeAlly(){return S.battle?S.party[S.battle.activePlayer]:null;}
function activeFoe(){if(!S.battle)return null;const e=S.battle.enemies;for(let i=0;i<e.length;i++){const idx=(S.battle.activeEnemy+i)%e.length;if(e[idx].hp>0){S.battle.activeEnemy=idx;return e[idx];}}return null;}
function renderBattleHuds(){const ally=activeAlly();if(ally)$('playerHud').innerHTML=hudHtml(ally);const wrap=$('enemyHuds');wrap.innerHTML='';if(!S.battle)return;S.battle.enemies.forEach((e,i)=>{if(e.hp<=0)return;const d=document.createElement('div');d.className='creature-hud';d.innerHTML=hudHtml(e);if(i===S.battle.activeEnemy)d.style.borderColor='var(--accent-hot)';wrap.appendChild(d);});}
function hudHtml(k){const pct=Math.max(0,k.hp/k.maxHp*100),cls=pct<=25?'critical':pct<=50?'low':'';return`<div class="n">${k.name} <span class="lv">D${k.level}${k.status?' · '+k.status:''}</span></div><div class="bar-label"><span>HP</span><span>${Math.max(0,k.hp)}/${k.maxHp}</span></div><div class="hp-bar-wrap"><div class="hp-bar ${cls}" style="width:${pct}%"></div></div><div style="margin-top:4px">${k.types.map(typeBadge).join('')}</div>`;}
function showBattleMain(){$('battleMainMenu').classList.remove('hidden');$('battleMoves').classList.add('hidden');$('battleSub').classList.add('hidden');}
function showMoves(){const ally=activeAlly();if(!ally)return;$('battleMainMenu').classList.add('hidden');$('battleMoves').classList.remove('hidden');$('battleSub').classList.add('hidden');const root=$('battleMoves');root.innerHTML='';ally.moves.forEach((m,idx)=>{const a=AB[m.id];const btn=document.createElement('button');btn.className='move-btn';btn.type='button';btn.disabled=m.pp<=0||S.battle.busy;btn.innerHTML=`<div class="row"><span>${a.name}</span><span>${typeBadge(a.type)}</span></div><div class="row" style="font-size:.75rem;color:var(--text-dim)"><span>Pwr ${a.power||'—'} · Acc ${a.acc}%</span><span>EN ${m.pp}/${m.maxPp}</span></div>`;btn.onclick=()=>chooseMove(idx);root.appendChild(btn);});const back=document.createElement('button');back.className='btn small';back.textContent='Back';back.style.gridColumn='1/-1';back.onclick=showBattleMain;root.appendChild(back);}
function chooseMove(idx){if(!S.battle||S.battle.busy||S.battle.phase!=='choose')return;const ally=activeAlly(),move=ally.moves[idx];if(!move||move.pp<=0)return;const actions=[{side:'p',kith:ally,moveIdx:idx,spd:effStat(ally,'spd')+(AB[move.id].effect&&AB[move.id].effect.priority?1000:0)}];for(const e of S.battle.enemies){if(e.hp<=0)continue;const mi=aiPickMove(e);const mv=e.moves[mi];const pr=(mv&&AB[mv.id].effect&&AB[mv.id].effect.priority)?1000:0;actions.push({side:'e',kith:e,moveIdx:mi,spd:effStat(e,'spd')+pr});}actions.sort((a,b)=>b.spd-a.spd||Math.random()-.5);S.battle.busy=true;S.battle.phase='anim';$('battleMoves').classList.add('hidden');runActionQueue(actions);}
function aiPickMove(e){let best=0,bestScore=-1;const target=activeAlly();e.moves.forEach((m,i)=>{if(m.pp<=0)return;const a=AB[m.id];let sc=a.power||10;if(target)sc*=Math.max(...target.types.map(t=>typeMult(a.type,t)));if(a.effect&&a.effect.healSelf)sc+=20;sc*=.8+Math.random()*.4;if(sc>bestScore){bestScore=sc;best=i;}});return best;}
async function runActionQueue(actions){for(const act of actions){if(!S.battle)return;if(act.kith.hp<=0)continue;if(act.side==='p'&&activeAlly()!==act.kith)continue;if(act.kith.status==='shock'&&Math.random()<.3){setBattleMsg(act.kith.name+' is jolted stiff!');await wait(600);continue;}
if(act.side==='p')await resolveMove(act.kith,activeFoe(),act.moveIdx,true);else await resolveMove(act.kith,activeAlly(),act.moveIdx,false);
if(!activeAlly()||activeAlly().hp<=0){const next=S.party.findIndex(k=>k.hp>0);if(next<0){setBattleMsg('Your Circle falls silent…');await wait(900);endBattle('lose');return;}S.battle.activePlayer=next;setBattleMsg(S.party[next].name+' steps forward!');renderBattleHuds();await wait(500);}
if(!activeFoe()){setBattleMsg('The opposing weave unravels!');await wait(800);endBattle('win');return;}
await tickStatus(act.kith);if(act.side==='p'&&act.kith.hp<=0){const next=S.party.findIndex(k=>k.hp>0);if(next<0){endBattle('lose');return;}S.battle.activePlayer=next;}if(!activeFoe()){endBattle('win');return;}}
if(!S.battle)return;S.battle.busy=false;S.battle.phase='choose';renderBattleHuds();showBattleMain();setBattleMsg('Choose a channel.');}
async function resolveMove(user,target,moveIdx,isPlayer){if(!target||!user||user.hp<=0)return;const m=user.moves[moveIdx];if(!m||m.pp<=0){setBattleMsg(user.name+' has no energy!');await wait(500);return;}m.pp--;const ab=AB[m.id];setBattleMsg(user.name+' channels '+ab.name+'!');await wait(450);if(Math.random()*100>ab.acc){setBattleMsg('The thread misses!');beep(120,.08,'sawtooth',.06);await wait(500);return;}
if(target.barrier&&ab.power>0){target.barrier=false;setBattleMsg(target.name+"'s barrier unspools the hit!");await wait(500);return;}const eff=ab.effect||{};
if(ab.power===0){await applyEffect(user,target,eff);renderBattleHuds();await wait(450);return;}
const hits=eff.multi||1;let total=0;for(let h=0;h<hits;h++){if(target.hp<=0)break;const dmg=calcDamage(user,target,ab);total+=dmg.amount;target.hp=Math.max(0,target.hp-dmg.amount);spawnFloater(isPlayer?W*.7:W*.3,isPlayer?120:H-220,dmg.amount,dmg.crit,dmg.mult);if(S.battle)S.battle.shake=dmg.mult>=2?8:dmg.crit?6:3;sfxHit(dmg.mult>1);if(dmg.mult>1)spawnBurst(isPlayer);renderBattleHuds();let msg=dmg.amount+' warp';if(dmg.crit)msg+=' · CRIT';if(dmg.mult>1.1)msg+=' · Resonant!';if(dmg.mult<.9)msg+=' · Muted…';setBattleMsg(msg);await wait(hits>1?320:500);}
if(eff.drain&&total>0){const heal=Math.floor(total*eff.drain);user.hp=Math.min(user.maxHp,user.hp+heal);setBattleMsg(user.name+' reclaims '+heal+' vitality.');renderBattleHuds();await wait(350);}
if(eff.recoil&&total>0)user.hp=Math.max(0,user.hp-Math.max(1,Math.floor(total*eff.recoil)));
await applyEffect(user,target,eff);renderBattleHuds();}
function calcDamage(user,target,ab){const atk=effStat(user,'atk'),def=effStat(target,'def');let mult=Math.max(...target.types.map(t=>typeMult(ab.type,t)));if(user.types.includes(ab.type))mult*=1.25;let crit=false;const critChance=.08+(ab.effect&&ab.effect.critBoost?ab.effect.critBoost*.1:0);if(Math.random()<critChance){crit=true;mult*=1.5;}const base=.4*user.level*atk/Math.max(1,def)*(ab.power/60)*mult;const amount=Math.max(1,Math.floor(base*(.85+Math.random()*.15)+1));return{amount,mult,crit};}
async function applyEffect(user,target,eff){if(!eff)return;if(eff.barrier){user.barrier=true;setBattleMsg(user.name+' spins a barrier!');await wait(350);}if(eff.healSelf){const h=Math.floor(user.maxHp*eff.healSelf);user.hp=Math.min(user.maxHp,user.hp+h);setBattleMsg(user.name+' restitches '+h+' HP.');sfxHeal();await wait(350);}if(eff.regen)user.regen=eff.regen;if(eff.clearStatus){user.status=null;user.statusTurns=0;}
if(eff.boost){const tgt=user;const stat=eff.boost.stat;tgt.stages[stat]=Math.max(-6,Math.min(6,(tgt.stages[stat]||0)+eff.boost.stages));setBattleMsg(tgt.name+"'s "+stat.toUpperCase()+(eff.boost.stages>0?' rises!':' falls!'));await wait(350);}
if(eff.status&&target.hp>0&&!target.status&&Math.random()<(eff.chance||1)){target.status=eff.status;target.statusTurns=eff.status==='burn'?4:3;setBattleMsg(target.name+' suffers '+eff.status+'!');await wait(350);}}
async function tickStatus(k){if(!k||k.hp<=0)return;if(k.regen>0){k.hp=Math.min(k.maxHp,k.hp+Math.floor(k.maxHp*.08));k.regen--;}if(k.status==='burn'){const d=Math.max(1,Math.floor(k.maxHp*.08));k.hp=Math.max(0,k.hp-d);setBattleMsg(k.name+' is seared by burn ('+d+')');renderBattleHuds();await wait(350);}if(k.status){k.statusTurns=(k.statusTurns||1)-1;if(k.statusTurns<=0){setBattleMsg(k.name+' recovers from '+k.status+'.');k.status=null;await wait(300);}}}
function spawnFloater(x,y,val,crit,mult){const rect=canvas.getBoundingClientRect(),scaleX=rect.width/W,scaleY=rect.height/H;const el=document.createElement('div');el.className='float-num';el.textContent=(crit?'✦ ':'')+val;el.style.left=(rect.left+x*scaleX)+'px';el.style.top=(rect.top+y*scaleY)+'px';el.style.color=crit?'#ffe66d':mult>=2?'#ff9e6e':mult<=.5?'#9bb7ff':'#fff';document.body.appendChild(el);setTimeout(()=>el.remove(),900);}
function spawnBurst(towardEnemy){if(!S.battle)return;for(let i=0;i<12;i++)S.battle.particles.push({x:towardEnemy?W*.72:W*.28,y:towardEnemy?H*.32:H*.55,vx:(Math.random()-.5)*6,vy:(Math.random()-.5)*6,life:20+Math.random()*15,col:towardEnemy?'#ff9e6e':'#7eb6ff',r:2+Math.random()*3});}
function drawBattle(){const b=S.battle,z=ZONES[S.zone],g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,shade(z.bg[0],-10));g.addColorStop(.5,z.bg[1]);g.addColorStop(1,shade(z.bg[2],10));ctx.fillStyle=g;ctx.fillRect(0,0,W,H);ctx.fillStyle='rgba(0,0,0,.25)';ctx.beginPath();ctx.ellipse(W*.28,H*.68,90,28,0,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.ellipse(W*.72,H*.38,80,24,0,0,Math.PI*2);ctx.fill();
let sx=0,sy=0;if(b&&b.shake>0){sx=(Math.random()-.5)*b.shake;sy=(Math.random()-.5)*b.shake;b.shake*=.85;if(b.shake<.5)b.shake=0;}ctx.save();ctx.translate(sx,sy);
if(b){const living=b.enemies.filter(e=>e.hp>0);living.forEach((e,i)=>{const ox=(i-(living.length-1)/2)*70;drawKithSprite(W*.72+ox,H*.32,SP[e.speciesId],1.3+(e===activeFoe()?0.1:0));});const ally=activeAlly();if(ally&&ally.hp>0)drawKithSprite(W*.28,H*.62,SP[ally.speciesId],1.5);
for(const p of b.particles){p.x+=p.vx;p.y+=p.vy;p.life--;ctx.globalAlpha=Math.max(0,p.life/30);ctx.fillStyle=p.col;ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.fill();}b.particles=b.particles.filter(p=>p.life>0);ctx.globalAlpha=1;}ctx.restore();}
// CAPTURE
let capState=null;
function startBindFlow(){if(!S.battle||!S.battle.meta.canBind||S.battle.busy)return;const foe=activeFoe();if(!foe)return;const options=['bindspool_m','bindspool_f','bindspool_c'].filter(id=>S.inventory[id]>0);if(!options.length){toast('No Bindspools left.');return;}S.battle.busy=true;S.battle.phase='bind';$('battleMainMenu').classList.add('hidden');const sub=$('battleSub');sub.classList.remove('hidden');sub.innerHTML='<p style="margin-bottom:8px;font-size:.9rem">Select Bindspool:</p>';
options.forEach(id=>{const it=ITEMS[id],btn=document.createElement('button');btn.className='btn';btn.style.margin='4px';btn.textContent=`${it.name} ×${S.inventory[id]}`;btn.onclick=()=>openCapture(foe,id);sub.appendChild(btn);});
const back=document.createElement('button');back.className='btn small';back.textContent='Back';back.onclick=()=>{S.battle.busy=false;S.battle.phase='choose';showBattleMain();};sub.appendChild(back);}
function openCapture(foe,itemId){S.inventory[itemId]--;const tier=ITEMS[itemId].tier,hpRatio=foe.hp/foe.maxHp;let rate=.28+(1-hpRatio)*.45+(tier-1)*.12-foe.level*.004;rate=Math.max(.05,Math.min(.92,rate));const zoneDeg=20+rate*90;
capState={foe,itemId,rate,zoneDeg,angle:0,speed:3.5+Math.random()*1.5,running:true,pulses:0,maxPulses:3,hits:0};$('capResult').textContent='';$('capHint').textContent='Pulse when the needle is in the bright arc. Need 2 of 3.';$('captureUI').classList.add('open');updateCapVisual();sfxUI();}
function updateCapVisual(){if(!capState)return;$('capNeedle').style.transform=`rotate(${capState.angle}deg)`;const start=270-capState.zoneDeg/2;$('capZone').style.background=`conic-gradient(from ${start}deg, rgba(110,255,180,.45) 0deg, rgba(110,255,180,.45) ${capState.zoneDeg}deg, transparent ${capState.zoneDeg}deg)`;}
function capTick(){if(!capState||!capState.running)return;capState.angle=(capState.angle+capState.speed)%360;updateCapVisual();}
function pulseCapture(){if(!capState||!capState.running)return;const half=capState.zoneDeg/2;const a=capState.angle%360;const dist=Math.min(a,360-a);const hit=dist<=half;capState.pulses++;if(hit){capState.hits++;$('capResult').textContent=`Pulse ${capState.pulses}: Thread holds! (${capState.hits}/${capState.maxPulses})`;beep(600+capState.hits*80,.1,'sine');}else{$('capResult').textContent=`Pulse ${capState.pulses}: Snag lost… (${capState.hits}/${capState.maxPulses})`;beep(180,.1,'sawtooth',.07);}if(capState.pulses>=capState.maxPulses){capState.running=false;finishCapture(capState.hits>=2);}}
function finishCapture(ok){if(!ok&&Math.random()<capState.rate*.25)ok=true;if(ok&&Math.random()>capState.rate+.35)ok=capState.hits>=3;const foe=capState.foe;setTimeout(()=>{$('captureUI').classList.remove('open');if(ok){sfxCapture(true);const k=makeKith(foe.speciesId,foe.level);k.hp=Math.max(1,foe.hp);S.seen[foe.speciesId]=S.caught[foe.speciesId]=true;recountUnique();if(S.party.length<6){S.party.push(k);toast(k.name+' joined your Circle!');}else{S.box.push(k);toast(k.name+' bound into reserve Loom.');}foe.hp=0;setBattleMsg(k.name+' is bound!');if(!S.flags.tutBind){S.flags.tutBind=true;toast('First bond sealed!');}capState=null;setTimeout(()=>{if(!activeFoe())endBattle('win');else{S.battle.busy=false;S.battle.phase='choose';renderBattleHuds();showBattleMain();}},700);}else{sfxCapture(false);setBattleMsg('The bind failed — fiber frayed!');toast('Binding failed.');capState=null;setTimeout(()=>{if(!S.battle)return;S.battle.busy=false;S.battle.phase='choose';showBattleMain();},500);}},500);}
// MENUS
function openFieldMenu(tab){if(S.mode!=='overworld'&&S.mode!=='menu')return;S.mode='menu';openModal('menuModal');renderMenu(tab||'pause');sfxUI();}
function renderMenu(tab){$('menuTitle').textContent={pause:'Field Index',party:'Circle',bag:'Satchel',dex:'Compendium',map:'Oasis Map'}[tab]||'Menu';const body=$('menuBody');let html=`<div class="tabs">
<button class="tab ${tab==='pause'?'active':''}" data-tab="pause">Pause</button><button class="tab ${tab==='party'?'active':''}" data-tab="party">Circle</button>
<button class="tab ${tab==='bag'?'active':''}" data-tab="bag">Satchel</button><button class="tab ${tab==='dex'?'active':''}" data-tab="dex">Compendium</button>
<button class="tab ${tab==='map'?'active':''}" data-tab="map">Map</button></div>`;
if(tab==='pause'){html+=`<p style="color:var(--text-dim);margin-bottom:10px">Glintspun: <strong style="color:var(--accent-soft)">${S.currency}</strong> · Bonds: ${S.flags.uniqueCaught}/8 · Anchors: ${[1,2,3,4].filter(z=>S.flags.bossesDefeated.includes(z)).length}/4</p>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button class="btn" id="pauseSave">Seal Progress</button><button class="btn" id="pauseTitle">Title</button></div>
<div class="sep"></div><p class="lore">WASD/Arrows move · Enter/E interact · 1 Circle · 2 Satchel · Esc menu. Goal: 8 unique Kith + 4 Anchors. Reshape at level (catalyst optional).</p>`;}
else if(tab==='party'){html+='<div class="scroll-y">';S.party.forEach((k,i)=>{const sp=SP[k.speciesId];html+=`<div class="card"><div class="party-slot"><div class="orb" style="background:${sp.color}">${k.level}</div><div style="flex:1"><div class="name">${k.name} <span class="meta">D${k.level}</span></div><div>${k.types.map(typeBadge).join('')}</div>
<div class="hp-bar-wrap"><div class="hp-bar ${k.hp/k.maxHp<=.25?'critical':k.hp/k.maxHp<=.5?'low':''}" style="width:${k.hp/k.maxHp*100}%"></div></div>
<div class="meta">${k.hp}/${k.maxHp} · ATK ${k.atk} DEF ${k.def} SPD ${k.spd}</div></div></div><div class="lore">${sp.lore}</div><div class="meta">Moves: ${k.moves.map(m=>AB[m.id].name).join(', ')}</div>
${i>0?`<button class="btn small" data-lead="${i}">Make Lead</button>`:''}</div>`;});
if(S.box.length){html+=`<div class="sep"></div><p class="meta">Reserve (${S.box.length})</p>`;S.box.forEach((k,i)=>{html+=`<div class="card"><div class="name">${k.name} D${k.level}</div><button class="btn small" data-boxin="${i}" ${S.party.length>=6?'disabled':''}>To Circle</button><button class="btn small" data-boxrel="${i}">Release</button></div>`;});}html+='</div>';}
else if(tab==='bag'){html+=`<p class="meta" style="margin-bottom:8px">Glintspun: ${S.currency}</p><div class="scroll-y">`;for(const id of Object.keys(ITEMS)){const n=S.inventory[id]||0;if(n<=0)continue;const it=ITEMS[id];html+=`<div class="card"><div class="name">${it.name} ×${n}</div><div class="meta">${it.desc}</div>${it.kind==='heal'?`<button class="btn small" data-use="${id}">Use</button>`:''}</div>`;}html+='</div>';}
else if(tab==='dex'){const all=Object.values(SP);const seen=all.filter(s=>S.seen[s.id]).length;html+=`<p class="meta" style="margin-bottom:8px">Observed ${seen}/${all.length} · Bound ${S.flags.uniqueCaught}</p><div class="scroll-y grid2">`;for(const sp of all){const known=!!S.seen[sp.id];html+=`<div class="card" style="opacity:${known?1:.45}"><div class="name">${known?sp.name:'???'}</div><div>${known?sp.types.map(typeBadge).join(''):''}</div><div class="lore">${known?sp.lore:'Unglimpsed pattern.'}</div><div class="meta">${S.caught[sp.id]?'Bound':'Unknown'}</div></div>`;}html+='</div>';}
else if(tab==='map'){html+='<div class="grid2">';ZONES.forEach((z,i)=>{const un=canEnterZone(i)||S.flags.zoneUnlocked.includes(i);html+=`<div class="card" style="border-color:${S.zone===i?'var(--accent)':un?'var(--panel-border)':'#333'}"><div class="name">${un?z.name:'Sealed Oasis'}</div><div class="meta">${S.zone===i?'You are here':''}${S.flags.bossesDefeated.includes(i)?' · Anchor quiet':z.boss?' · Anchor active':''}</div><div style="height:8px;border-radius:4px;background:${un?z.mapColor:'#333'};margin-top:6px"></div></div>`;});html+='</div>';}
body.innerHTML=html;body.querySelectorAll('.tab').forEach(t=>t.onclick=()=>renderMenu(t.dataset.tab));
const ps=body.querySelector('#pauseSave');if(ps)ps.onclick=()=>saveGame();const pt=body.querySelector('#pauseTitle');if(pt)pt.onclick=()=>{closeModal('menuModal');goTitle();};
body.querySelectorAll('[data-lead]').forEach(btn=>btn.onclick=()=>{const f=+btn.dataset.lead;const t=S.party[0];S.party[0]=S.party[f];S.party[f]=t;renderMenu('party');});
body.querySelectorAll('[data-boxin]').forEach(btn=>btn.onclick=()=>{if(S.party.length>=6)return;S.party.push(S.box.splice(+btn.dataset.boxin,1)[0]);renderMenu('party');});
body.querySelectorAll('[data-boxrel]').forEach(btn=>btn.onclick=()=>{const k=S.box.splice(+btn.dataset.boxrel,1)[0];toast(k.name+' returned to the wild.');renderMenu('party');});
body.querySelectorAll('[data-use]').forEach(btn=>btn.onclick=()=>useItemField(btn.dataset.use));}
function useItemField(id){const it=ITEMS[id];if(!it||!(S.inventory[id]>0)||it.kind!=='heal')return;if(it.party){S.inventory[id]--;for(const k of S.party){k.hp=Math.min(k.maxHp,k.hp+(it.heal||0));if(it.clear)k.status=null;}sfxHeal();toast('Circle restored.');renderMenu('bag');return;}
const body=$('menuBody');let html='<p>Use on which Kith?</p>';S.party.forEach((k,i)=>{html+=`<button class="btn" data-ht="${i}" style="margin:4px">${k.name} (${k.hp}/${k.maxHp})</button>`;});html+='<button class="btn small" id="cancelUse">Cancel</button>';body.innerHTML=html;
body.querySelectorAll('[data-ht]').forEach(b=>b.onclick=()=>{const k=S.party[+b.dataset.ht];S.inventory[id]--;if(it.heal)k.hp=Math.min(k.maxHp,k.hp+it.heal);if(it.clear)k.status=null;sfxHeal();toast('Used '+it.name);renderMenu('bag');});
body.querySelector('#cancelUse').onclick=()=>renderMenu('bag');}
function renderShop(){const pane=$('shrineShopPane');pane.classList.remove('hidden');let html=`<p class="meta">Glintspun: ${S.currency}</p>`;const stock=['bindspool_c','bindspool_f','bindspool_m','mend_salve','full_reel','status_wash','circle_balm','zephyr_knot','sol_thread','night_spindle','root_catalyst','tide_core','ash_filament'];
stock.forEach(id=>{const it=ITEMS[id];if(it.kind==='catalyst'&&S.zone<2)return;if(id==='bindspool_m'&&S.zone<3)return;html+=`<div class="shop-item"><div><strong>${it.name}</strong><div class="meta">${it.desc}</div></div><button class="btn small" data-buy="${id}">${it.price}◎</button></div>`;});
pane.innerHTML=html;pane.querySelectorAll('[data-buy]').forEach(b=>b.onclick=()=>{const id=b.dataset.buy,it=ITEMS[id];if(S.currency<it.price){toast('Not enough Glintspun.');return;}S.currency-=it.price;S.inventory[id]=(S.inventory[id]||0)+1;toast('Purchased '+it.name);beep(500,.06,'sine');renderShop();});}
function battleAction(act){if(!S.battle||S.battle.busy||S.battle.phase!=='choose')return;if(act==='fight')showMoves();else if(act==='switch')showSwitch();else if(act==='item')showBattleItems();else if(act==='bind')startBindFlow();else if(act==='flee')tryFlee();}
function showSwitch(){$('battleMainMenu').classList.add('hidden');$('battleMoves').classList.add('hidden');const sub=$('battleSub');sub.classList.remove('hidden');sub.innerHTML='<p class="meta">Swap active Kith:</p>';
S.party.forEach((k,i)=>{if(i===S.battle.activePlayer)return;const btn=document.createElement('button');btn.className='btn';btn.style.margin='4px';btn.disabled=k.hp<=0;btn.textContent=`${k.name} D${k.level} (${k.hp}/${k.maxHp})`;
btn.onclick=async()=>{S.battle.busy=true;S.battle.activePlayer=i;setBattleMsg(k.name+' enters!');showBattleMain();$('battleSub').classList.add('hidden');renderBattleHuds();const actions=[];for(const e of S.battle.enemies){if(e.hp<=0)continue;actions.push({side:'e',kith:e,moveIdx:aiPickMove(e),spd:effStat(e,'spd')});}actions.sort((a,b)=>b.spd-a.spd);await runActionQueue(actions);};sub.appendChild(btn);});
const back=document.createElement('button');back.className='btn small';back.textContent='Back';back.onclick=showBattleMain;sub.appendChild(back);}
function showBattleItems(){$('battleMainMenu').classList.add('hidden');const sub=$('battleSub');sub.classList.remove('hidden');sub.innerHTML='';let any=false;for(const id of Object.keys(ITEMS)){if(ITEMS[id].kind!=='heal'||!(S.inventory[id]>0))continue;any=true;const btn=document.createElement('button');btn.className='btn';btn.style.margin='4px';btn.textContent=`${ITEMS[id].name} ×${S.inventory[id]}`;
btn.onclick=async()=>{const it=ITEMS[id];S.inventory[id]--;const k=activeAlly();if(it.party)for(const p of S.party){p.hp=Math.min(p.maxHp,p.hp+(it.heal||0));if(it.clear)p.status=null;}else{if(it.heal)k.hp=Math.min(k.maxHp,k.hp+it.heal);if(it.clear)k.status=null;}sfxHeal();setBattleMsg('Used '+it.name+'!');renderBattleHuds();S.battle.busy=true;$('battleSub').classList.add('hidden');await wait(400);const actions=[];for(const e of S.battle.enemies){if(e.hp<=0)continue;actions.push({side:'e',kith:e,moveIdx:aiPickMove(e),spd:effStat(e,'spd')});}await runActionQueue(actions);};sub.appendChild(btn);}
if(!any)sub.innerHTML='<p class="meta">No usable salves.</p>';const back=document.createElement('button');back.className='btn small';back.textContent='Back';back.onclick=showBattleMain;sub.appendChild(back);}
async function tryFlee(){if(!S.battle.meta.canFlee){setBattleMsg('Cannot withdraw from an Anchor!');return;}S.battle.busy=true;const ally=activeAlly(),foe=activeFoe();const chance=.4+(effStat(ally,'spd')-effStat(foe,'spd'))*.02;if(Math.random()<Math.max(.2,Math.min(.9,chance))){setBattleMsg('Withdrew successfully.');await wait(500);endBattle('flee');}else{setBattleMsg('Could not withdraw!');await wait(400);const actions=[];for(const e of S.battle.enemies){if(e.hp<=0)continue;actions.push({side:'e',kith:e,moveIdx:aiPickMove(e),spd:effStat(e,'spd')});}await runActionQueue(actions);}}
// INPUT / BOOT
window.addEventListener('keydown',e=>{if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key))e.preventDefault();S.keys[e.key.toLowerCase()]=true;
if(e.key==='Escape'){if(S.mode==='menu'){closeModal('menuModal');S.mode='overworld';}else if(S.mode==='overworld')openFieldMenu('pause');}
if(e.key==='Enter'||e.key===' '){if(S.mode==='dialogue'){nextDialogue();return;}if(S.mode==='overworld')tryInteract();}
if(e.key==='e'||e.key==='E'){if(S.mode==='dialogue')nextDialogue();else if(S.mode==='overworld')tryInteract();}
if(S.mode==='overworld'){if(e.key==='1')openFieldMenu('party');if(e.key==='2')openFieldMenu('bag');}
if(actx&&actx.state==='suspended')actx.resume();});
window.addEventListener('keyup',e=>{S.keys[e.key.toLowerCase()]=false;});
function tryInteract(){const z=ZONES[S.zone],f=S.player.facing,dx=f===1?1:f===3?-1:0,dy=f===2?1:f===0?-1:0,tx=S.player.x+dx,ty=S.player.y+dy;
if((S.player.x===z.shrine.x&&S.player.y===z.shrine.y)||(tx===z.shrine.x&&ty===z.shrine.y)){openShrine();return;}
if(z.storyNode&&!S.flags.storySeen[S.zone]&&((tx===z.storyNode.x&&ty===z.storyNode.y)||(S.player.x===z.storyNode.x&&S.player.y===z.storyNode.y))){triggerStoryNode(S.zone);return;}
if(z.boss&&!S.flags.bossesDefeated.includes(S.zone)&&((tx===16&&ty===3)||(S.player.x===16&&S.player.y===3))){startBossBattle(z.boss);return;}fieldMsg('Nothing to bind here.');}
document.querySelectorAll('#touchPad .pad').forEach(p=>{const map={up:[0,-1],down:[0,1],left:[-1,0],right:[1,0]};const dir=p.dataset.dir;const start=ev=>{ev.preventDefault();S.touchDir=map[dir];};const end=()=>{S.touchDir=null;};p.addEventListener('touchstart',start,{passive:false});p.addEventListener('touchend',end);p.addEventListener('mousedown',start);p.addEventListener('mouseup',end);p.addEventListener('mouseleave',end);});
$('btnInteract').onclick=()=>{if(S.mode==='dialogue')nextDialogue();else tryInteract();};
$('btnMenu').onclick=()=>openFieldMenu('pause');$('btnParty').onclick=()=>openFieldMenu('party');$('btnBag').onclick=()=>openFieldMenu('bag');
$('menuClose').onclick=()=>{closeModal('menuModal');if(S.mode==='menu')S.mode='overworld';};
$('muteBtn').onclick=()=>{initAudio();setMute(!S.mute);};
$('shrineClose').onclick=()=>{closeModal('shrineModal');S.mode='overworld';};
$('shrineHeal').onclick=()=>{for(const k of S.party){k.hp=k.maxHp;k.status=null;k.stages={atk:0,def:0,spd:0};k.moves.forEach(m=>m.pp=m.maxPp);}sfxHeal();toast('Circle fully restitched.');};
$('shrineSave').onclick=()=>saveGame();$('shrineShop').onclick=()=>renderShop();
$('capTap').onclick=()=>pulseCapture();
document.querySelectorAll('#battleMainMenu [data-bact]').forEach(btn=>btn.onclick=()=>battleAction(btn.dataset.bact));
$('dialogue').onclick=()=>{if(S.mode==='dialogue')nextDialogue();};
$('btnNew').onclick=()=>{initAudio();startNewGame();};
$('btnContinue').onclick=()=>{initAudio();if(loadGame()){$('titleScreen').classList.remove('active');S.mode='overworld';updateZoneLabel();fitCanvas();if(isTouch()){$('touchPad').classList.add('show');$('actionBtns').classList.add('show');}toast('Resumed from last sealed shrine.');}};
function goTitle(){S.mode='title';$('titleScreen').classList.add('active');$('battleUI').classList.remove('active');closeModal('menuModal');closeModal('shrineModal');$('btnContinue').disabled=!hasSave();}
function startNewGame(){S.party=[];S.box=[];S.inventory={bindspool_c:5,bindspool_f:1,bindspool_m:0,mend_salve:3,full_reel:0,status_wash:1,circle_balm:0,zephyr_knot:0,sol_thread:0,night_spindle:0,root_catalyst:0,tide_core:0,ash_filament:0};S.currency=120;S.seen={};S.caught={};S.flags={storyStage:0,bossesDefeated:[],storySeen:{},zoneUnlocked:[0,1],postgame:false,uniqueCaught:0,tutBattle:false,tutBind:false};S.bonuses={atk:0,def:0,hp:0,spd:0,all:0};S.maps={};ensureMaps();S.zone=0;S.player.x=5;S.player.y=8;S.player.px=S.player.fromX=5;S.player.py=S.player.fromY=8;S.lastShrine={zone:0,x:8,y:6};S.nextUid=1;$('titleScreen').classList.remove('active');openStarter();}
function openStarter(){S.mode='starter';const opts=[{id:'pipglow',blurb:'Kindle — assertive sparks, swift offense.'},{id:'dewlet',blurb:'Mire — patient pressure, steady defense.'},{id:'sprigbit',blurb:'Briar — balanced growth and resilience.'}];const root=$('starterOpts');root.innerHTML='';opts.forEach(o=>{const sp=SP[o.id],card=document.createElement('div');card.className='card';card.innerHTML=`<div class="name">${sp.name} ${typeBadge(sp.types[0])}</div><div class="lore">${sp.lore}</div><div class="meta">${o.blurb}</div><div class="meta">HP ${sp.base.hp} ATK ${sp.base.atk} DEF ${sp.base.def} SPD ${sp.base.spd}</div>`;card.onclick=()=>pickStarter(o.id);root.appendChild(card);});openModal('starterPick');}
function pickStarter(id){const k=makeKith(id,5);S.party=[k];S.seen[id]=S.caught[id]=true;recountUnique();closeModal('starterPick');S.mode='overworld';updateZoneLabel();fitCanvas();if(isTouch()){$('touchPad').classList.add('show');$('actionBtns').classList.add('show');}
showDialogue([{sp:'Loomshrine',t:'Good. '+k.name+' answers your first pull of the thread.'},{sp:'Loomshrine',t:'Move with WASD or arrows. Tall grass hides wild Kith. The Loomshrine restores and seals progress.'},{sp:'Loomshrine',t:'Step into taller seams for your first clash. Later, Bind weakened foes. East lies Cinderhollow.'},{sp:'Loomshrine',t:'Open the Field Index for Circle, Satchel, Compendium, and Map. Eight bonds. Four Anchors.'}],'story-green').then(()=>fieldMsg('Step into taller grass for a first clash.'));}
let lastTs=performance.now();
function frame(ts){const dt=Math.min(.05,(ts-lastTs)/1000);lastTs=ts;S.animT++;
if(S.mode==='overworld'||S.mode==='shrine'||S.mode==='menu'||(S.mode==='dialogue'&&!S.battle)){if(S.mode==='overworld')updateMovement(dt);if(Math.random()<.3)spawnAmbient(S.zone);updateAmbient();drawOverworld();}
else if(S.mode==='battle'||(S.battle&&S.mode==='dialogue')){drawBattle();}
else if(S.mode==='title'||S.mode==='starter'){const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'#152040');g.addColorStop(1,'#0b0e17');ctx.fillStyle=g;ctx.fillRect(0,0,W,H);if(Math.random()<.4)S.ambient.push({kind:'glints',x:Math.random()*W,y:H+4,vx:(Math.random()-.5)*.5,vy:-.6-Math.random(),life:120,r:1+Math.random()*2,a:.5});updateAmbient();drawAmbient();}
if(capState&&capState.running)capTick();
if(S.battle&&S.mode==='battle'){const bindBtn=document.querySelector('[data-bact="bind"]');if(bindBtn)bindBtn.style.display=S.battle.meta.canBind?'':'none';const fleeBtn=document.querySelector('[data-bact="flee"]');if(fleeBtn)fleeBtn.disabled=!S.battle.meta.canFlee;}
requestAnimationFrame(frame);}
function boot(){fitCanvas();ensureMaps();$('btnContinue').disabled=!hasSave();requestAnimationFrame(frame);}
boot();
window.addEventListener('error',e=>{console.error(e.error||e.message);if(S.mode==='battle'&&S.battle){S.battle.busy=false;S.battle.phase='choose';}});
</script>
</body>
</html>
<!-- agent-meta {"model":"x-ai/grok-4.5","provider":"openrouter","persona":"middle","sessionId":"792f64c2-09a0-4b57-9a15-da2e99bb332a","tokensIn":1917478,"tokensOut":97441,"tokensTotal":2014919,"turns":29,"toolCalls":31,"failedToolCalls":1,"timestamp":"2026-07-12T20:25:15.609Z"} -->