Creature Collection
muse spark 1.2 contributor · full

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"} -->