Creature Collection
muse spark 1.2 · basic

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 62,023 bytes · SHA-256 d3edefc42b09
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>AETHERMUND - Binding of Veils</title>
<style>
:root{
--bg:#0a0f1c;
--panel:rgba(22,30,50,0.82);
--glass:rgba(255,255,255,0.07);
--border:rgba(255,255,255,0.12);
--accent:#6cf2da;
--accent2:#8b7cff;
--danger:#ff6b8a;
--gold:#ffd166;
--text:#eef2ff;
--muted:#9aa4c2;
}
*{box-sizing:border-box;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif}
html,body{margin:0;height:100%;background:radial-gradient(1200px 600px at 20% -10%, #1a2450 0%, transparent 60%),radial-gradient(900px 500px at 90% 10%, #0e3a3a 0%, transparent 60%), var(--bg);color:var(--text);overflow:hidden}
button{cursor:pointer;font:inherit}
#titleScreen{position:fixed;inset:0;z-index:50;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(800px 600px at 50% 20%,rgba(108,242,218,0.15),transparent 70%),radial-gradient(1000px 700px at 80% 90%,rgba(139,124,255,0.18),transparent 60%),linear-gradient(180deg,#0a0f1c 0%, #0f1830 100%);text-align:center;padding:24px}
.titleLogo{font-size:clamp(32px,7vw,64px);font-weight:900;letter-spacing:0.12em;line-height:0.9}
.titleLogo span{background:linear-gradient(90deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.subtitle{color:var(--muted);letter-spacing:0.3em;text-transform:uppercase;font-size:11px;margin:14px 0 6px}
.lore{max-width:560px;color:#b9c2e2;font-size:13px;line-height:1.6;opacity:0.9}
.titleBtns{display:flex;gap:12px;margin-top:28px;flex-wrap:wrap;justify-content:center}
.btn{padding:12px 22px;border-radius:999px;border:1px solid var(--border);background:rgba(255,255,255,0.08);color:var(--text);backdrop-filter:blur(12px);transition:0.2s;font-weight:700;letter-spacing:0.04em}
.btn.primary{background:linear-gradient(90deg,#6cf2da,#8b7cff);color:#0a0f1c;border:none;box-shadow:0 8px 24px rgba(108,242,218,0.3)}
.btn:hover{transform:translateY(-1px);filter:brightness(1.08)}
.btn:active{transform:translateY(1px)}
.btn.small{padding:8px 14px;font-size:12px}
.topBar{position:fixed;top:0;left:0;right:0;height:54px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;background:rgba(10,15,28,0.7);backdrop-filter:blur(16px);border-bottom:1px solid var(--border);z-index:20}
.topLeft{display:flex;gap:10px;align-items:center}
.badge{padding:6px 10px;border-radius:999px;background:var(--glass);border:1px solid var(--border);font-size:11px;letter-spacing:0.08em;color:var(--muted)}
.zoneName{font-weight:800;letter-spacing:0.06em}
.topRight{display:flex;gap:8px;align-items:center}
.iconBtn{width:36px;height:36px;border-radius:12px;display:grid;place-items:center;background:var(--glass);border:1px solid var(--border);color:var(--text)}
#gameWrap{display:flex;height:100%;padding-top:54px}
#left{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden}
#canvasWrap{flex:1;position:relative;background:#060a14;display:grid;place-items:center;overflow:hidden}
canvas{image-rendering:pixelated;max-width:100%;max-height:100%;}
#overworld{width:640px;height:480px;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,0.5);border:1px solid var(--border)}
#battleCanvas{position:absolute;inset:0;width:100%;height:100%;display:none;background:transparent}
.hudBar{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);display:flex;gap:8px;align-items:center;background:rgba(15,23,42,0.85);border:1px solid var(--border);backdrop-filter:blur(12px);padding:8px 12px;border-radius:999px;z-index:5}
.dpad{display:flex;gap:6px;align-items:center}
.dpad button{width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,0.08);border:1px solid var(--border);color:white}
.partyStrip{display:flex;gap:6px}
.pMini{width:36px;height:36px;border-radius:10px;background:rgba(255,255,255,0.06);border:1px solid var(--border);display:grid;place-items:center;font-size:10px;position:relative;overflow:hidden}
.pMini .hp{position:absolute;bottom:0;left:0;height:4px;background:var(--accent)}
#right{width:340px;background:var(--panel);border-left:1px solid var(--border);backdrop-filter:blur(18px);display:flex;flex-direction:column;overflow:hidden}
.panelHead{padding:16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
.panelHead h3{margin:0;font-size:13px;letter-spacing:0.12em}
.tabs{display:flex;gap:6px;padding:10px;border-bottom:1px solid var(--border);overflow:auto}
.tab{padding:8px 12px;border-radius:999px;background:transparent;border:1px solid transparent;color:var(--muted);font-size:12px;font-weight:700;white-space:nowrap}
.tab.active{background:rgba(108,242,218,0.15);border-color:rgba(108,242,218,0.3);color:var(--accent)}
.panelBody{flex:1;overflow:auto;padding:12px}
.card{background:rgba(255,255,255,0.06);border:1px solid var(--border);border-radius:16px;padding:12px;margin-bottom:10px}
.card h4{margin:0 0 4px;font-size:13px}
.muted{color:var(--muted);font-size:12px;line-height:1.5}
.statRow{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
.stat{font-size:10px;padding:4px 7px;border-radius:999px;background:rgba(0,0,0,0.3);border:1px solid var(--border)}
.type{padding:3px 8px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:0.06em;text-transform:uppercase}
.t-Embervein{background:#ff7a45;color:#1a0a00}
.t-Tideworn{background:#4cc9ff;color:#00141f}
.t-Verdant{background:#52d67a;color:#02140a}
.t-Stonebound{background:#c2b5a3;color:#1a150f}
.t-Galewhisper{background:#a8d8ff;color:#0a1420}
.t-Luminara{background:linear-gradient(90deg,#ffd166,#ff8fab);color:#1a0f00}
.t-Nullshroud{background:#2b2f45;color:#cbd5ff;border:1px solid #5a5f8a}
.battleUI{position:absolute;inset:0;display:none;flex-direction:column;justify-content:flex-end;padding:12px;pointer-events:none}
.battleUI.show{display:flex}
.battleTop{position:absolute;top:12px;left:12px;right:12px;display:flex;justify-content:space-between;gap:8px;pointer-events:none}
.enemyCard,.playerCard{background:rgba(12,18,35,0.9);border:1px solid var(--border);border-radius:14px;padding:10px;min-width:160px;backdrop-filter:blur(10px)}
.hpBar{height:8px;background:rgba(255,255,255,0.12);border-radius:999px;overflow:hidden;margin-top:6px;position:relative}
.hpFill{height:100%;background:linear-gradient(90deg,#6cf2da,#2dd4bf);width:100%;transition:width 0.6s cubic-bezier(.2,.8,.2,1)}
.hpFill.enemy{background:linear-gradient(90deg,#ff6b8a,#ff3b6e)}
.hpText{font-size:10px;color:var(--muted);margin-top:4px;display:flex;justify-content:space-between}
.battleActions{pointer-events:auto;display:grid;grid-template-columns:1fr 1fr;gap:8px;background:rgba(10,15,28,0.92);border:1px solid var(--border);border-radius:18px;padding:12px;backdrop-filter:blur(16px)}
.moveBtn{padding:10px;border-radius:12px;border:1px solid var(--border);background:rgba(255,255,255,0.06);color:var(--text);text-align:left}
.moveBtn:hover{background:rgba(255,255,255,0.1)}
.moveBtn.cant{opacity:0.45;pointer-events:none}
.moveMeta{font-size:10px;color:var(--muted);display:flex;gap:6px;margin-top:4px}
.modal{position:fixed;inset:0;background:rgba(3,7,18,0.7);backdrop-filter:blur(10px);display:none;place-items:center;z-index:30;padding:16px}
.modal.show{display:grid}
.modalBox{width:min(720px,100%);max-height:85vh;overflow:auto;background:rgba(18,24,44,0.96);border:1px solid var(--border);border-radius:20px;padding:18px;box-shadow:0 24px 64px rgba(0,0,0,0.5)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:900px){#right{display:none}#overworld{width:100vw;height:56vh} .topBar{height:48px} .titleLogo{font-size:36px}}
.floating{position:absolute;pointer-events:none;font-weight:900;text-shadow:0 2px 8px rgba(0,0,0,0.8);animation:floatUp 0.9s forwards}
@keyframes floatUp{0%{transform:translateY(0) scale(1);opacity:1}100%{transform:translateY(-60px) scale(1.1);opacity:0}}
.pulse{animation:pulse 0.3s}
@keyframes pulse{0%{transform:scale(1)}50%{transform:scale(1.04)}100%{transform:scale(1)}}
.shake{animation:shake 0.25s}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
.captureBar{height:14px;background:rgba(255,255,255,0.12);border-radius:999px;position:relative;overflow:hidden;border:1px solid var(--border)}
.captureFill{position:absolute;top:0;bottom:0;background:linear-gradient(90deg,#6cf2da,#8b7cff)}
.captureCursor{position:absolute;top:0;bottom:0;width:4px;background:white;box-shadow:0 0 10px white}
.captureZone{position:absolute;top:0;bottom:0;background:rgba(108,242,218,0.35);border:1px solid #6cf2da}
#dialog{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);width:min(720px,92%);background:rgba(15,23,42,0.96);border:1px solid var(--border);border-radius:18px;padding:14px 16px;display:none;z-index:25;backdrop-filter:blur(12px)}
#dialog.show{display:block}
.particles{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.dot{position:absolute;width:4px;height:4px;border-radius:50%;background:var(--accent);opacity:0.6;animation:drift linear infinite}
@keyframes drift{from{transform:translateY(110%)}to{transform:translateY(-10%)}}
</style>
</head>
<body>
<div id="titleScreen">
<div class="subtitle">Aethermund • Binding of Veils</div>
<div class="titleLogo">AETHER<span>MUND</span></div>
<p class="lore">Six veiled zones. Twenty-seven choir-beasts of resonance. Forge your Binding—walk the tile-lit world, heal at Aether Shrines, defeat Anchors, and gather the chorus. An original myth, built from scratch.</p>
<div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;justify-content:center">
<div class="badge">WASD / Arrows • Tap D-Pad</div><div class="badge">Turn SPD Order • 7 Affinities</div><div class="badge">Capture • Evolve</div>
</div>
<div class="titleBtns">
<button class="btn primary" id="newGameBtn">NEW BINDING</button>
<button class="btn" id="continueBtn">CONTINUE</button>
<button class="btn" id="howBtn">HOW TO BIND</button>
</div>
<div style="margin-top:18px" class="muted">Starter Trio — Choose your first echo at shrine.</div>
<div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;justify-content:center" id="starterPreview"></div>
</div>
<div class="topBar">
<div class="topLeft">
<div class="zoneName" id="zoneName">—</div>
<span class="badge" id="progressBadge">0/8 Collected • 0/4 Anchors</span>
<span class="badge" id="posBadge">x0 y0</span>
</div>
<div class="topRight">
<button class="iconBtn" id="soundBtn" title="Sound">♪</button>
<button class="iconBtn" id="bagBtn">◈</button>
<button class="iconBtn" id="teamBtn">⬢</button>
<button class="iconBtn" id="codexBtn">≡</button>
<button class="iconBtn" id="mapBtn">▦</button>
</div>
</div>
<div id="gameWrap">
<div id="left">
<div id="canvasWrap">
<canvas id="overworld" width="640" height="480"></canvas>
<canvas id="battleCanvas" width="640" height="480"></canvas>
<div class="particles" id="particles"></div>
<div class="battleUI" id="battleUI">
<div class="battleTop" id="battleTop"></div>
<div class="battleActions" id="battleActions"></div>
</div>
<div class="hudBar">
<div class="dpad">
<button data-dir="up">▲</button>
<div style="display:flex;flex-direction:column;gap:6px">
<div style="display:flex;gap:6px"><button data-dir="left">◀</button><button data-dir="down">▼</button><button data-dir="right">▶</button></div>
</div>
<button id="interactBtn" style="width:auto;padding:0 10px">Interact</button>
</div>
<div class="partyStrip" id="partyStrip"></div>
</div>
</div>
</div>
<div id="right">
<div class="panelHead"><h3 id="panelTitle">CHORUS DEX</h3><span class="badge" id="panelBadge">ALIVE</span></div>
<div class="tabs">
<button class="tab active" data-tab="dex">Codex</button>
<button class="tab" data-tab="team">Team</button>
<button class="tab" data-tab="bag">Bag</button>
<button class="tab" data-tab="quest">Veils</button>
</div>
<div class="panelBody" id="panelBody"></div>
</div>
</div>
<div id="dialog">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<strong id="dialogSpeaker" style="letter-spacing:0.08em;font-size:12px;color:var(--accent)">Warden</strong>
<button class="btn small" id="dialogNext">Next ▶</button>
</div>
<div id="dialogText" class="muted" style="font-size:14px;color:#e6e9ff"></div>
</div>
<div class="modal" id="bagModal"><div class="modalBox"><div style="display:flex;justify-content:space-between;align-items:center"><h3>Satchel & Shrines</h3><button class="btn small" onclick="closeModal('bagModal')">Close</button></div><div id="bagContent"></div></div></div>
<div class="modal" id="teamModal"><div class="modalBox"><div style="display:flex;justify-content:space-between"><h3>Binding Party (6 max)</h3><button class="btn small" onclick="closeModal('teamModal')">Close</button></div><div id="teamContent"></div></div></div>
<div class="modal" id="codexModal"><div class="modalBox"><div style="display:flex;justify-content:space-between"><h3>Aethermund Codex — 27 Echoes</h3><button class="btn small" onclick="closeModal('codexModal')">Close</button></div><div id="codexContent"></div></div></div>
<div class="modal" id="mapModal"><div class="modalBox"><div style="display:flex;justify-content:space-between"><h3>Veil Map — 6 Zones</h3><button class="btn small" onclick="closeModal('mapModal')">Close</button></div><div id="mapContent"></div></div></div>
<div class="modal" id="captureModal"><div class="modalBox" style="text-align:center"><h3>Rhythm Binding</h3><p class="muted">Press SPACE / TAP when the white needle is inside the teal zone. Timing grants +35% capture!</p><div class="captureBar" id="captureBar" style="height:22px;margin:14px 0"><div class="captureZone" id="captureZone"></div><div class="captureCursor" id="captureCursor"></div></div><div style="display:flex;gap:8px;justify-content:center"><button class="btn primary" id="captureTap">BIND!</button><button class="btn" onclick="resolveCapture(false)">Cancel</button></div><div class="muted" id="captureOdds"></div></div></div>
<div class="modal" id="starterModal"><div class="modalBox" style="text-align:center"><h3>Choose your First Echo</h3><p class="muted">Three nascent resonances await at the First Shrine. Pick one — your binding begins.</p><div id="starterChoices" class="grid2"></div></div></div>
<script>
const TYPES = ["Embervein","Tideworn","Verdant","Stonebound","Galewhisper","Luminara","Nullshroud"];
const TYPE_COLOR = {Embervein:"#ff7a45",Tideworn:"#4cc9ff",Verdant:"#52d67a",Stonebound:"#c2b5a3",Galewhisper:"#a8d8ff",Luminara:"#ffd166",Nullshroud:"#8a90b5"};
function typeMult(att,def){
if(att==="Luminara" && def==="Nullshroud") return 2.0;
if(att==="Luminara") return 1.25;
if(def==="Nullshroud") return 0.65;
if(att==="Embervein" && def==="Tideworn") return 0.5;
if(att==="Tideworn" && def==="Verdant") return 0.5;
if(att==="Verdant" && def==="Embervein") return 0.5;
if(att==="Embervein" && def==="Verdant") return 2;
if(att==="Tideworn" && def==="Embervein") return 2;
if(att==="Verdant" && def==="Tideworn") return 2;
if(att==="Stonebound" && (def==="Embervein"||def==="Verdant")) return 2;
if(att==="Galewhisper" && (def==="Stonebound"||def==="Tideworn")) return 2;
if(att==="Embervein" && def==="Stonebound") return 0.5;
if(att==="Verdant" && def==="Stonebound") return 0.5;
if(att==="Stonebound" && def==="Galewhisper") return 0.5;
if(att==="Tideworn" && def==="Galewhisper") return 0.5;
return 1;
}
const ABILITIES = [
{id:0,name:"Ember Kiss",type:"Embervein",power:40,acc:100,pp:15,effect:"burn"},
{id:1,name:"Tide Slash",type:"Tideworn",power:45,acc:100,pp:15,effect:"none"},
{id:2,name:"Thorn Needle",type:"Verdant",power:35,acc:95,pp:20,effect:"multi"},
{id:3,name:"Quake Fist",type:"Stonebound",power:50,acc:90,pp:15,effect:"none"},
{id:4,name:"Gale Cutter",type:"Galewhisper",power:40,acc:100,pp:15,effect:"speed"},
{id:5,name:"Lumen Flare",type:"Luminara",power:45,acc:100,pp:15,effect:"none"},
{id:6,name:"Null Pulse",type:"Nullshroud",power:40,acc:100,pp:15,effect:"barrier"},
{id:7,name:"Cinder Veil",type:"Embervein",power:55,acc:90,pp:10,effect:"burn"},
{id:8,name:"Frost Brine",type:"Tideworn",power:50,acc:90,pp:10,effect:"frost"},
{id:9,name:"Bloom Mend",type:"Verdant",power:0,acc:100,pp:10,effect:"heal"},
{id:10,name:"Bastion",type:"Stonebound",power:0,acc:100,pp:10,effect:"barrier"},
{id:11,name:"Shock Draft",type:"Galewhisper",power:30,acc:95,pp:15,effect:"shock"},
{id:12,name:"Prism Ray",type:"Luminara",power:60,acc:85,pp:10,effect:"none"},
{id:13,name:"Void Leech",type:"Nullshroud",power:35,acc:100,pp:15,effect:"lifesteal"},
{id:14,name:"Magma Surge",type:"Embervein",power:70,acc:85,pp:5,effect:"burn"},
{id:15,name:"Abyssal Pull",type:"Tideworn",power:60,acc:90,pp:10,effect:"frost"},
{id:16,name:"Canopy Seed",type:"Verdant",power:30,acc:100,pp:15,effect:"regen"},
{id:17,name:"Shard Volley",type:"Stonebound",power:20,acc:95,pp:15,effect:"multi"},
{id:18,name:"Zephyr Step",type:"Galewhisper",power:0,acc:100,pp:15,effect:"speed"},
{id:19,name:"Aether Ward",type:"Luminara",power:0,acc:100,pp:10,effect:"barrier"},
{id:20,name:"Eclipse Mark",type:"Nullshroud",power:55,acc:95,pp:10,effect:"none"},
{id:21,name:"Kindle",type:"Embervein",power:0,acc:100,pp:15,effect:"atkup"},
{id:22,name:"Riptide",type:"Tideworn",power:75,acc:80,pp:5,effect:"none"},
{id:23,name:"Root Grasp",type:"Verdant",power:45,acc:90,pp:15,effect:"frost"},
{id:24,name:"Iron Hurl",type:"Stonebound",power:65,acc:85,pp:10,effect:"none"},
{id:25,name:"Tempest",type:"Galewhisper",power:70,acc:80,pp:5,effect:"shock"},
{id:26,name:"Dawn Crown",type:"Luminara",power:50,acc:100,pp:10,effect:"heal"},
{id:27,name:"Hollow Veil",type:"Nullshroud",power:0,acc:100,pp:10,effect:"regen"},
{id:28,name:"Spark Needle",type:"Embervein",power:25,acc:100,pp:20,effect:"multi"},
{id:29,name:"Brine Bloom",type:"Tideworn",power:40,acc:100,pp:15,effect:"regen"},
{id:30,name:"Petal Storm",type:"Verdant",power:60,acc:90,pp:10,effect:"none"},
{id:31,name:"Granite Roar",type:"Stonebound",power:45,acc:100,pp:15,effect:"atkup"},
{id:32,name:"Feather Dervish",type:"Galewhisper",power:35,acc:100,pp:15,effect:"multi"},
{id:33,name:"Sunlit Edge",type:"Luminara",power:55,acc:95,pp:10,effect:"priority"},
{id:34,name:"Umbral Slash",type:"Nullshroud",power:50,acc:95,pp:15,effect:"priority"},
{id:35,name:"Sear Coil",type:"Embervein",power:35,acc:100,pp:15,effect:"burn"},
{id:36,name:"Glacier Jolt",type:"Tideworn",power:45,acc:90,pp:15,effect:"shock"},
{id:37,name:"Spore Haze",type:"Verdant",power:30,acc:90,pp:15,effect:"shock"},
{id:38,name:"Meteor Shard",type:"Stonebound",power:80,acc:75,pp:5,effect:"none"},
{id:39,name:"Storm Crown",type:"Galewhisper",power:65,acc:90,pp:10,effect:"speed"},
{id:40,name:"Radiant Mend",type:"Luminara",power:0,acc:100,pp:10,effect:"heal"},
{id:41,name:"Null Fracture",type:"Nullshroud",power:70,acc:80,pp:5,effect:"none"},
];
const SPECIES = [
{id:0,name:"Pyreling",type:"Embervein",desc:"A cinder pup that hums when near hearths, ember tucked in its chest.",base:{hp:44,atk:52,def:38,spd:58},learn:[[1,0],[5,21],[9,7],[16,14]],evo:{to:18,lvl:22,item:"Cinder Core"}},
{id:1,name:"Aquillow",type:"Tideworn",desc:"Willow-like driftling that cradles tidewater in leaf cups.",base:{hp:48,atk:48,def:52,spd:46},learn:[[1,1],[7,8],[12,15],[20,22]],evo:{to:19,lvl:24,item:"Brine Pearl"}},
{id:2,name:"Brambleau",type:"Verdant",desc:"Shy bramble sprite that braids sunlight into thorns.",base:{hp:50,atk:46,def:55,spd:42},learn:[[1,2],[8,9],[14,16],[18,30]],evo:{to:20,lvl:23,item:"Verdant Seed"}},
{id:3,name:"Kraglet",type:"Stonebound",desc:"Pebble golem that stacks itself taller each dawn.",base:{hp:55,atk:58,def:62,spd:32},learn:[[1,3],[6,10],[12,24],[18,38]],evo:{to:21,lvl:26,item:"Krag Shard"}},
{id:4,name:"Zephling",type:"Galewhisper",desc:"Kite-thin glider that rides breaths of the world.",base:{hp:42,atk:50,def:36,spd:68},learn:[[1,4],[6,11],[10,18],[16,25]],evo:{to:22,lvl:24,item:"Gale Feather"}},
{id:5,name:"Luxmoth",type:"Luminara",desc:"Moth of woven light, dust trails linger like memory.",base:{hp:46,atk:54,def:40,spd:60},learn:[[1,5],[8,12],[13,26],[17,33]],evo:{to:23,lvl:28,item:"Lumen Prism"}},
{id:6,name:"Umbrae",type:"Nullshroud",desc:"Hollow silhouette that absorbs sound.",base:{hp:52,atk:50,def:60,spd:38},learn:[[1,6],[7,13],[14,20],[19,41]],evo:{to:24,lvl:27,item:"Void Filament"}},
{id:7,name:"Cinderfox",type:"Embervein",desc:"Fox with coal-brush tail leaving warm footfalls.",base:{hp:45,atk:60,def:40,spd:62},learn:[[1,0],[9,35],[12,7],[18,28]],evo:null},
{id:8,name:"Reefscale",type:"Tideworn",desc:"Armored carp that sings through its scales.",base:{hp:58,atk:52,def:58,spd:36},learn:[[1,1],[7,36],[11,29],[16,8]],evo:null},
{id:9,name:"Thornback",type:"Verdant",desc:"Shelled wanderer sprouting living moss.",base:{hp:60,atk:55,def:65,spd:30},learn:[[1,3],[5,2],[13,23],[17,30]],evo:{to:25,lvl:26,item:"Verdant Seed"}},
{id:10,name:"Galeburr",type:"Galewhisper",desc:"Burr that spins forever on wind.",base:{hp:44,atk:48,def:44,spd:66},learn:[[1,4],[6,32],[12,11],[18,39]],evo:null},
{id:11,name:"Prismite",type:"Luminara",desc:"Crystal mite refracting thought into color.",base:{hp:40,atk:62,def:38,spd:64},learn:[[1,5],[9,33],[13,26],[16,40]],evo:null},
{id:12,name:"Hollowshell",type:"Nullshroud",desc:"Empty shell humming with absent tide.",base:{hp:56,atk:48,def:68,spd:28},learn:[[1,6],[8,27],[12,13],[18,20]],evo:null},
{id:13,name:"Magmire",type:"Embervein",desc:"Bog sprite boiling from below.",base:{hp:52,atk:65,def:45,spd:48},learn:[[1,0],[7,7],[14,14],[17,35]],evo:null},
{id:14,name:"Deepcurrent",type:"Tideworn",desc:"Eel of cold currents, eyes like lanterns.",base:{hp:50,atk:56,def:48,spd:54},learn:[[1,8],[10,15],[15,36],[19,22]],evo:null},
{id:15,name:"Canopyr",type:"Verdant",desc:"Canopy dancer braiding vines mid-leap.",base:{hp:48,atk:58,def:50,spd:56},learn:[[1,2],[9,9],[14,37],[18,30]],evo:null},
{id:16,name:"Bouldermaul",type:"Stonebound",desc:"Solitary mauler that carries its hill.",base:{hp:62,atk:70,def:72,spd:26},learn:[[1,3],[8,31],[13,24],[17,17]],evo:null},
{id:17,name:"Stormhawk",type:"Galewhisper",desc:"Hawk that nests in thunderheads.",base:{hp:46,atk:60,def:42,spd:70},learn:[[1,4],[7,11],[12,25],[18,39]],evo:{to:26,lvl:30,item:"Gale Feather"}},
// evos
{id:18,name:"Pyrelord",type:"Embervein",desc:"Crowned flame sovereign, voice crackles.",base:{hp:62,atk:78,def:58,spd:72},learn:[[1,0],[1,14],[22,7],[26,21]],evo:null},
{id:19,name:"Aquillow Monarch",type:"Tideworn",desc:"Monarch whose cape is living water.",base:{hp:68,atk:70,def:74,spd:58},learn:[[1,1],[1,22],[22,15],[28,29]],evo:null},
{id:20,name:"Bramble Titan",type:"Verdant",desc:"Titan that roots villages in bloom.",base:{hp:72,atk:68,def:78,spd:48},learn:[[1,2],[1,30],[22,9],[28,16]],evo:null},
{id:21,name:"Krag Colossus",type:"Stonebound",desc:"Colossus whose steps write canyons.",base:{hp:78,atk:82,def:90,spd:38},learn:[[1,3],[1,38],[24,10],[28,31]],evo:null},
{id:22,name:"Zephyr Sovereign",type:"Galewhisper",desc:"Sovereign of drafts, unseen but felt.",base:{hp:60,atk:72,def:56,spd:88},learn:[[1,4],[1,25],[24,18],[28,39]],evo:null},
{id:23,name:"Lux Ascendant",type:"Luminara",desc:"Ascendant that turned itself into dawn.",base:{hp:64,atk:80,def:62,spd:84},learn:[[1,5],[1,12],[26,40],[28,33]],evo:null},
{id:24,name:"Umbrae Eclipse",type:"Nullshroud",desc:"Eclipse that never ends.",base:{hp:72,atk:70,def:84,spd:52},learn:[[1,6],[1,41],[26,20],[28,27]],evo:null},
{id:25,name:"Thorn Bastion",type:"Verdant",desc:"Walking fortress of thorn and loam.",base:{hp:78,atk:74,def:92,spd:36},learn:[[1,10],[22,23],[26,16],[30,30]],evo:null},
{id:26,name:"Stormhawk Tempest",type:"Galewhisper",desc:"Tempest incarnate, wings storm-lit.",base:{hp:66,atk:84,def:62,spd:92},learn:[[1,25],[24,11],[28,32],[30,39]],evo:null},
];
const ZONES = [
{name:"PALE MEADOW",color:"#6cf2da",bg:["#0d2a2a","#1b4b6b"],rate:0.12,guardian:7},
{name:"AZURE BASIN",color:"#4cc9ff",bg:["#0a1e3a","#0e3a5a"],rate:0.15,guardian:8},
{name:"THORN TANGLE",color:"#52d67a",bg:["#0f2a1a","#1e4a2a"],rate:0.16,guardian:9},
{name:"KARGOS RIP",color:"#c2b5a3",bg:["#2a2218","#4a3a28"],rate:0.14,guardian:16},
{name:"AERIE EXpanse",color:"#a8d8ff",bg:["#0f1e3a","#2a3a6a"],rate:0.18,guardian:17},
{name:"VEIL CORE",color:"#ffd166",bg:["#2a1e3a","#4a2a4a"],rate:0.20,guardian:11},
];
function makeMap(w=16,h=12,zoneIdx){
let m=[];
for(let y=0;y<h;y++){
let row=[];
for(let x=0;x<w;x++){
if(x===0||y===0||x===w-1||y===h-1) row.push(1);
else if(Math.random()<0.08) row.push(1);
else if(Math.random()<0.22) row.push(2);
else row.push(0);
}
m.push(row);
}
m[2][2]=0; m[3][2]=0; m[2][3]=0;
m[h-3][w-3]=4; // guardian portal
m[h-4][w-4]=2;
m[1][w-2]=3; // shrine top right
m[Math.floor(h/2)][Math.floor(w/2)]=3; // central shrine
if(zoneIdx===0) m[2][4]=3;
return m;
}
let maps = ZONES.map((z,i)=>makeMap(16,12,i));
let state = {
zone:0, px:2, py:2, dir:"down",
party:[], box:[], seen:new Set(), caught:new Set(),
inventory:{orb1:8,orb2:2,orb3:1,potion:4,revive:1,"Cinder Core":1,"Brine Pearl":1,"Verdant Seed":1,"Krag Shard":1,"Gale Feather":1,"Lumen Prism":1,"Void Filament":1},
guardians:[false,false,false,false,false,false],
story:0, encCooldown:0,
badges:{collected:0, anchors:0},
selectedMove:0
};
function createCreature(sid,lvl){
const s=SPECIES[sid];
const hp = Math.floor(s.base.hp + lvl*2.6);
const atk = Math.floor(s.base.atk + lvl*1.9);
const def = Math.floor(s.base.def + lvl*1.8);
const spd = Math.floor(s.base.spd + lvl*1.7);
let moves=[];
for(let [lv,ab] of s.learn){ if(lv<=lvl) moves.push(ab); if(moves.length>=4) moves=moves.slice(-4); }
if(moves.length===0) moves=[s.learn[0][1]];
while(moves.length<4 && moves.length < ABILITIES.length) { if(!moves.includes(moves[0])) moves.push(moves[0]); else break; }
moves=moves.slice(0,4);
return {sid,lvl,exp:0,next: lvl*22+30, hp,hpMax:hp, atk,def,spd,type:s.type, moves, status:null, barrier:false, name:s.name};
}
function expNeeded(lvl){return Math.floor(30 + lvl*22 + Math.pow(lvl,2)*1.2);}
function gainExp(cre,amt){
cre.exp+=amt;
let leveled=false;
while(cre.exp>=cre.next && cre.lvl<50){
cre.exp-=cre.next; cre.lvl++; cre.next=expNeeded(cre.lvl);
let s=SPECIES[cre.sid];
cre.hpMax+=3; cre.atk+=2; cre.def+=2; cre.spd+=2; cre.hp=cre.hpMax;
// learn new move check
for(let [lv,ab] of s.learn) if(lv===cre.lvl && !cre.moves.includes(ab)){
if(cre.moves.length<4) cre.moves.push(ab); else cre.moves[Math.floor(Math.random()*4)]=ab;
}
// evo check
if(s.evo && cre.lvl>=s.evo.lvl && state.inventory[s.evo.item]>0){
// auto evolve prompt
}
leveled=true;
}
return leveled;
}
const cvs=document.getElementById('overworld');
const ctx=cvs.getContext('2d');
const bCvs=document.getElementById('battleCanvas');
const bCtx=bCvs.getContext('2d');
const particlesEl=document.getElementById('particles');
function spawnParticles(n=12){
particlesEl.innerHTML="";
for(let i=0;i<n;i++){
let d=document.createElement('div');d.className='dot';
d.style.left=Math.random()*100+"%";d.style.animationDuration=(3+Math.random()*4)+"s";d.style.animationDelay=Math.random()*2+"s";
d.style.background=ZONES[state.zone].color;particlesEl.appendChild(d);
}
}
spawnParticles(14);
function drawOverworld(){
const zone=ZONES[state.zone];
// bg gradient
let g=ctx.createLinearGradient(0,0,0,cvs.height);
g.addColorStop(0,zone.bg[0]); g.addColorStop(1,zone.bg[1]);
ctx.fillStyle=g; ctx.fillRect(0,0,cvs.width,cvs.height);
const cols=16, rows=12, tw=cvs.width/cols, th=cvs.height/rows;
for(let y=0;y<rows;y++) for(let x=0;x<cols;x++){
let t=maps[state.zone][y][x];
if(t===1){ ctx.fillStyle="rgba(10,15,28,0.9)"; ctx.fillRect(x*tw,y*th,tw,th); ctx.strokeStyle="rgba(255,255,255,0.06)"; ctx.strokeRect(x*tw,y*th,tw,th);}
else if(t===2){ ctx.fillStyle=zone.color+"33"; ctx.fillRect(x*tw+2,y*th+2,tw-4,th-4); ctx.fillStyle=zone.color; ctx.font="10px monospace"; ctx.fillText("≋",x*tw+tw/2-5,y*th+th/2+3);}
else if(t===3){ ctx.fillStyle="#ffd166"; ctx.beginPath(); ctx.arc(x*tw+tw/2,y*th+th/2, Math.min(tw,th)/3,0,Math.PI*2); ctx.fill(); ctx.fillStyle="#0a0f1c"; ctx.font="10px monospace"; ctx.fillText("◈",x*tw+tw/2-5,y*th+th/2+4);}
else if(t===4){ ctx.fillStyle=state.guardians[state.zone] ? "rgba(108,242,218,0.3)" : "rgba(255,107,138,0.9)"; ctx.fillRect(x*tw+1,y*th+1,tw-2,th-2); ctx.fillStyle="white"; ctx.font="bold 11px monospace"; ctx.fillText(state.guardians[state.zone]?"✓":"⚑",x*tw+tw/2-6,y*th+th/2+4);}
}
// player
ctx.fillStyle="white"; ctx.strokeStyle="rgba(0,0,0,0.4)"; ctx.lineWidth=2;
let px=state.px*tw+tw/2, py=state.py*th+th/2;
ctx.beginPath(); ctx.arc(px,py,10,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle="#0a0f1c"; ctx.font="bold 8px monospace"; ctx.fillText("YOU",px-10,py-14);
// shadow direction
ctx.fillStyle="rgba(255,255,255,0.85)"; let dx=0,dy=0; if(state.dir==="up")dy=-6; if(state.dir==="down")dy=6; if(state.dir==="left")dx=-6; if(state.dir==="right")dx=6;
ctx.beginPath(); ctx.arc(px+dx,py+dy,3,0,Math.PI*2); ctx.fill();
document.getElementById('zoneName').textContent=ZONES[state.zone].name + (state.guardians[state.zone]?" • Anchored":" • Wild");
document.getElementById('posBadge').textContent=`x${state.px} y${state.py}`;
updateProgress();
}
function updateProgress(){
let collected=state.caught.size;
let anchors=state.guardians.filter(Boolean).length;
document.getElementById('progressBadge').textContent=`${collected}/8 Collected • ${anchors}/4 Anchors`;
if(collected>=8 && anchors>=4) document.getElementById('progressBadge').style.background="linear-gradient(90deg,#6cf2da,#8b7cff)";
}
let battle=null; // {enemies:[], activePlayerIdx, turnOrder, log, captureTarget}
function randSpeciesForZone(zone){
let pool;
if(zone===0) pool=[0,1,2,7,8];
else if(zone===1) pool=[1,8,14,13];
else if(zone===2) pool=[2,9,15,10];
else if(zone===3) pool=[3,16,9,12];
else if(zone===4) pool=[4,10,17,6];
else pool=[5,11,6,12,3,4];
let sid=pool[Math.floor(Math.random()*pool.length)];
let lvl= Math.floor(4+ zone*4 + Math.random()*5);
if(state.story>=3) lvl+=4;
return createCreature(sid,lvl);
}
function startWildBattle(){
let enemies=[];
let count = Math.random()<0.2?2:1;
if(state.zone>=4 && Math.random()<0.15) count=3;
for(let i=0;i<count;i++) enemies.push(randSpeciesForZone(state.zone));
battle={enemies, playerTeam: state.party, active:0, enemiesActive:0, phase:"choose", message:`Wild ${enemies.map(e=>e.name).join(" & ")} appears!`};
showBattle();
}
function startGuardianBattle(){
let gid=ZONES[state.zone].guardian;
let boss=createCreature(gid, 8+state.zone*5);
boss.hpMax=Math.floor(boss.hpMax*1.6); boss.hp=boss.hpMax; boss.atk=Math.floor(boss.atk*1.2);
boss.name="Anchor • "+SPECIES[gid].name;
battle={enemies:[boss], playerTeam:state.party, active:0, phase:"choose", message:`Anchor Guardian ${SPECIES[gid].name} bars the Veil!`};
showBattle();
}
function showBattle(){
document.getElementById('battleCanvas').style.display='block';
document.getElementById('battleUI').classList.add('show');
renderBattle();
drawBattleScene();
}
function hideBattle(){
document.getElementById('battleCanvas').style.display='none';
document.getElementById('battleUI').classList.remove('show');
battle=null;
drawOverworld();
}
function getActivePlayer(){ return state.party[battle.active]; }
function renderBattle(){
if(!battle) return;
const top=document.getElementById('battleTop'); top.innerHTML="";
battle.enemies.forEach((e,i)=>{
let d=document.createElement('div');d.className='enemyCard';
d.innerHTML=`<div style="display:flex;justify-content:space-between;align-items:center"><strong style="font-size:12px">${e.name} Lv${e.lvl}</strong><span class="type t-${e.type}" style="font-size:8px">${e.type}</span></div>
<div class="hpBar"><div class="hpFill enemy" style="width:${Math.max(0,e.hp/e.hpMax*100)}%"></div></div><div class="hpText"><span>${e.hp}/${e.hpMax}</span><span>${e.status||""} ${e.barrier?"⛊":""}</span></div>`;
top.appendChild(d);
});
let p=getActivePlayer();
if(p){
let d=document.createElement('div');d.className='playerCard';
d.innerHTML=`<div style="display:flex;justify-content:space-between"><strong style="font-size:12px">${p.name} Lv${p.lvl}</strong><span class="type t-${p.type}" style="font-size:8px">${p.type}</span></div>
<div class="hpBar"><div class="hpFill" style="width:${Math.max(0,p.hp/p.hpMax*100)}%"></div></div><div class="hpText"><span>${p.hp}/${p.hpMax}</span><span>${p.status||""} ${p.barrier?"⛊":""}</span></div>`;
top.appendChild(d);
}
const acts=document.getElementById('battleActions');
acts.innerHTML="";
if(battle.phase==="choose" && p && p.hp>0){
p.moves.forEach((mid,i)=>{
let ab=ABILITIES[mid];
let btn=document.createElement('button');btn.className='moveBtn';
btn.innerHTML=`<div style="font-weight:800;font-size:12px">${ab.name} <span class="type t-${ab.type}" style="font-size:8px;vertical-align:middle">${ab.type}</span></div><div class="moveMeta"><span>POW ${ab.power||"—"}</span><span>ACC ${ab.acc}</span><span>${ab.effect}</span></div>`;
btn.onclick=()=>playerUseMove(i);
acts.appendChild(btn);
});
let switchBtn=document.createElement('button');switchBtn.className='moveBtn';switchBtn.textContent="Switch Echo";
switchBtn.onclick=()=>openSwitchMenu(); acts.appendChild(switchBtn);
let bagBtn=document.createElement('button');bagBtn.className='moveBtn';bagBtn.innerHTML=`Capture / Bag <div class="moveMeta">Orbs: ${state.inventory.orb1}/${state.inventory.orb2}/${state.inventory.orb3}</div>`;
bagBtn.onclick=()=>openCaptureMenu(); acts.appendChild(bagBtn);
if(battle.enemies.length===1 && !battle.enemies[0].name.includes("Anchor")){ let run=document.createElement('button');run.className='moveBtn';run.textContent="Flee"; run.onclick=()=>{showDialog("You slipped through the veil.","Shade"); hideBattle();}; acts.appendChild(run);}
} else if(battle.phase==="switch"){
state.party.forEach((cre,i)=>{
let b=document.createElement('button');b.className='moveBtn';
b.innerHTML=`${cre.name} Lv${cre.lvl} <span class="muted">${cre.hp}/${cre.hpMax}</span> ${i===battle.active?"(Active)":""}`;
if(cre.hp<=0) b.classList.add('cant');
else b.onclick=()=>{battle.active=i; battle.phase="choose"; renderBattle(); drawBattleScene(); showDialog(`Go, ${cre.name}!`,"Binding"); if(battle.waitEnemy) enemyTurn();}
acts.appendChild(b);
});
let back=document.createElement('button');back.className='moveBtn';back.textContent="Back"; back.onclick=()=>{battle.phase="choose"; renderBattle();}; acts.appendChild(back);
} else {
acts.innerHTML=`<div class="muted" style="grid-column:1/-1;text-align:center;padding:10px">${battle.message}</div>`;
}
}
function drawBattleScene(){
bCtx.clearRect(0,0,bCvs.width,bCvs.height);
// gradient
let g=bCtx.createLinearGradient(0,0,0,bCvs.height);
let z=ZONES[state.zone]; g.addColorStop(0,z.bg[0]); g.addColorStop(1,z.bg[1]);
bCtx.fillStyle=g; bCtx.fillRect(0,0,bCvs.width,bCvs.height);
// platforms
bCtx.fillStyle="rgba(255,255,255,0.08)"; bCtx.beginPath(); bCtx.ellipse(160,220,90,28,0,0,Math.PI*2); bCtx.fill();
bCtx.beginPath(); bCtx.ellipse(480,320,110,34,0,0,Math.PI*2); bCtx.fill();
if(!battle) return;
// enemies
battle.enemies.forEach((e,i)=>{
let x=120+i*90, y=180;
bCtx.fillStyle=TYPE_COLOR[e.type]||"#fff"; bCtx.globalAlpha=0.95;
bCtx.beginPath(); bCtx.arc(x,y,28,0,Math.PI*2); bCtx.fill(); bCtx.globalAlpha=1;
bCtx.fillStyle="rgba(0,0,0,0.7)"; bCtx.font="10px monospace"; bCtx.fillText(e.name.slice(0,8),x-26,y+48);
});
let p=getActivePlayer();
if(p){
bCtx.fillStyle=TYPE_COLOR[p.type]; bCtx.beginPath(); bCtx.arc(480,300,34,0,Math.PI*2); bCtx.fill();
bCtx.fillStyle="white"; bCtx.font="bold 11px monospace"; bCtx.fillText(p.name.slice(0,10),450,360);
}
if(battle.message){ bCtx.fillStyle="rgba(255,255,255,0.9)"; bCtx.font="12px monospace"; bCtx.fillText(battle.message.slice(0,48),20,40); }
}
function calcDamage(attacker,defender,ability){
let ab=ABILITIES[ability];
if(ab.power===0) return 0;
let mult=typeMult(ab.type,defender.type);
// STAB
if(ab.type===attacker.type) mult*=1.25;
if(defender.barrier){ defender.barrier=false; mult=0; battle.message="Barrier absorbed it!"; }
let base = (0.4 * attacker.lvl * attacker.atk / Math.max(1,defender.def) * mult) + (Math.random()*5);
if(Math.random()<0.06){ base*=1.5; battle.message="Critical resonance!"; }
if(mult>=2) { battle.message="Super-effective shimmer!"; shakeScreen(); }
else if(mult<=0.6) battle.message="Veil dulled the blow...";
if(defender.status==="burn") base*=0.9;
return Math.floor(Math.max(1,base));
}
function applyEffect(target,attacker,effect){
if(effect==="burn" && !target.status && Math.random()<0.3) target.status="burn";
if(effect==="frost" && !target.status) target.status="frost";
if(effect==="shock" && Math.random()<0.35) target.status="shock";
if(effect==="barrier") attacker.barrier=true;
if(effect==="regen") attacker.status="regen";
if(effect==="speed") attacker.spd+=4;
if(effect==="atkup") attacker.atk+=5;
if(effect==="heal"){ attacker.hp=Math.min(attacker.hpMax, attacker.hp+Math.floor(attacker.hpMax*0.4)); spawnFloat("+"+Math.floor(attacker.hpMax*0.4),"#6cf2da",480,300); }
if(effect==="lifesteal"){ attacker.hp=Math.min(attacker.hpMax, attacker.hp+12); spawnFloat("+12","#6cf2da",480,300); }
}
function spawnFloat(txt,color,x,y){
let el=document.createElement('div');el.className='floating';el.textContent=txt;el.style.left=x+"px";el.style.top=y+"px";el.style.color=color; document.getElementById('canvasWrap').appendChild(el); setTimeout(()=>el.remove(),900);
}
function shakeScreen(){ document.getElementById('canvasWrap').classList.add('shake'); setTimeout(()=>document.getElementById('canvasWrap').classList.remove('shake'),260); }
function playerUseMove(idx){
let p=getActivePlayer(); if(!p||p.hp<=0) return;
let abId=p.moves[idx]; let ab=ABILITIES[abId];
// speed order: gather all
let combatants=[];
combatants.push({cre:p, isPlayer:true, ability:abId, priority: ab.effect==="priority"?1:0, spd: p.status==="frost"? Math.floor(p.spd/2):p.spd});
battle.enemies.forEach(e=>{
if(e.hp<=0) return;
let eAb = e.moves[Math.floor(Math.random()*e.moves.length)];
let eAbData=ABILITIES[eAb];
combatants.push({cre:e, isPlayer:false, ability:eAb, priority: eAbData.effect==="priority"?1:0, spd: e.status==="frost"? Math.floor(e.spd/2):e.spd});
});
combatants.sort((a,b)=> (b.priority-a.priority) || (b.spd-a.spd) || (Math.random()-0.5));
battle.phase="anim";
renderBattle();
let i=0;
function nextTurn(){
if(i>=combatants.length){ checkBattleEnd(); return; }
let cur=combatants[i++];
if(cur.cre.hp<=0) { nextTurn(); return; }
if(cur.cre.status==="shock" && Math.random()<0.3){ battle.message=`${cur.cre.name} is shocked!`; renderBattle(); setTimeout(nextTurn,700); return; }
if(cur.cre.status==="burn"){ cur.cre.hp=Math.max(0,cur.cre.hp- Math.floor(cur.cre.hpMax*0.06)); spawnFloat("-burn","#ff6b8a", cur.isPlayer?480:160, cur.isPlayer?300:220); }
if(cur.cre.status==="regen"){ cur.cre.hp=Math.min(cur.cre.hpMax, cur.cre.hp+ Math.floor(cur.cre.hpMax*0.06)); spawnFloat("+regen","#52d67a", cur.isPlayer?480:160, cur.isPlayer?300:220); }
let abData=ABILITIES[cur.ability];
let target;
if(cur.isPlayer) target=battle.enemies.find(e=>e.hp>0);
else target=getActivePlayer();
if(!target){ nextTurn(); return; }
if(abData.power===0){
applyEffect(target,cur.cre,abData.effect);
// for heal/barrier effects on self
if(["heal","barrier","regen","speed","atkup"].includes(abData.effect)) applyEffect(cur.cre,cur.cre,abData.effect);
battle.message=`${cur.cre.name} used ${abData.name}!`;
} else {
if(Math.random()*100 > abData.acc){ battle.message=`${cur.cre.name}'s ${abData.name} missed!`; }
else {
let hits = abData.effect==="multi" ? (2+Math.floor(Math.random()*3)) : 1;
let total=0;
for(let h=0;h<hits;h++){ let d=calcDamage(cur.cre,target,cur.ability); target.hp=Math.max(0,target.hp-d); total+=d; if(abData.effect==="burn"||abData.effect==="frost"||abData.effect==="shock") applyEffect(target,cur.cre,abData.effect); }
spawnFloat("-"+total, total>40?"#ffd166":"white", target===p?480:160, target===p?300:220);
if(abData.effect==="lifesteal") applyEffect(target,cur.cre,"lifesteal");
battle.message=`${cur.cre.name} struck with ${abData.name}! ${hits>1?`x${hits}`:""}`;
}
}
renderBattle(); drawBattleScene();
setTimeout(nextTurn,750);
}
nextTurn();
}
function checkBattleEnd(){
// check enemies dead
if(battle.enemies.every(e=>e.hp<=0)){
let expGain= battle.enemies.reduce((s,e)=>s+ e.lvl*14 + 20,0);
battle.enemies.forEach(e=>{ state.seen.add(e.sid); });
getActivePlayer() && gainExp(getActivePlayer(),expGain);
// also give to bench small share
state.party.forEach(c=>{ if(c!==getActivePlayer()) gainExp(c,Math.floor(expGain*0.4));});
showDialog(`Victory! +${expGain} resonance.`, "Chorus");
if(battle.enemies[0].name.includes("Anchor")){
state.guardians[state.zone]=true;
state.story=Math.min(4,state.story+1);
if(state.zone<5) state.zone++;
showDialog(`Anchor shattered! Veil ${state.zone} opens. A boon courses through your binding.`, "Warden");
state.party.forEach(c=>{c.atk+=2;c.def+=2;});
if(state.caught.size>=8 && state.guardians.filter(Boolean).length>=4) showDialog("You have gathered 8 echoes and 4 Anchors — the Core awaits. Free roam empowered!", "Warden");
}
renderPanel(); updateProgress(); save();
setTimeout(()=>{ hideBattle(); checkEvolutions(); },900);
} else if(state.party.every(c=>c.hp<=0)){
showDialog("Your chorus fell silent... returned to last shrine.", "Warden");
// heal at shrine
state.party.forEach(c=>c.hp=c.hpMax);
state.px=2; state.py=2;
hideBattle();
} else {
battle.phase="choose";
battle.message="Your turn...";
renderBattle();
}
}
function checkEvolutions(){
state.party.forEach(c=>{
let s=SPECIES[c.sid];
if(s.evo && c.lvl>=s.evo.lvl && state.inventory[s.evo.item]>0){
if(confirm(`Evolve ${c.name} into ${SPECIES[s.evo.to].name} using ${s.evo.item}?`)){
state.inventory[s.evo.item]--; c.sid=s.evo.to; c.name=SPECIES[s.evo.to].name; let ns=SPECIES[s.evo.to]; c.type=ns.type;
c.hpMax+=10; c.hp=c.hpMax; c.atk+=6;c.def+=6;c.spd+=5;
showDialog(`${c.name} transformed!`, "Chorus"); save();
}
}
});
}
function openSwitchMenu(){ battle.phase="switch"; battle.waitEnemy=false; renderBattle(); }
function openCaptureMenu(){
let target=battle.enemies.find(e=>e.hp>0);
if(!target) return;
let orbs=["orb1","orb2","orb3"]; let available=orbs.filter(k=>state.inventory[k]>0);
if(available.length===0){ showDialog("No binding orbs left. Visit shrine shop.", "Warden"); return; }
// choose best orb automatically or show choice
let chosen=available.includes("orb3")?"orb3":available.includes("orb2")?"orb2":"orb1";
let base = (1 - target.hp/target.hpMax)*0.6 + 0.15;
if(chosen==="orb2") base+=0.18; if(chosen==="orb3") base+=0.32;
base=Math.min(0.92,Math.max(0.05,base));
document.getElementById('captureOdds').textContent=`Orb: ${chosen} • Base ${Math.round(base*100)}% (+35% perfect rhythm)`;
document.getElementById('captureModal').classList.add('show');
battle.capture={target,orb:chosen,base};
startCaptureBar();
}
let capInterval, capPos=0, capDir=1;
function startCaptureBar(){
capPos=0; capDir=1;
let zoneEl=document.getElementById('captureZone');
let start=35+Math.random()*20; zoneEl.style.left=start+"%"; zoneEl.style.width="22%";
capInterval=setInterval(()=>{
capPos+=capDir*3.2;
if(capPos>=100||capPos<=0) capDir*=-1;
document.getElementById('captureCursor').style.left=capPos+"%";
},16);
}
function resolveCapture(fromTap){
clearInterval(capInterval);
document.getElementById('captureModal').classList.remove('show');
if(!battle||!battle.capture) return;
let {target,orb,base}=battle.capture;
let perfect=false;
if(fromTap){
let zoneEl=document.getElementById('captureZone');
let l=parseFloat(zoneEl.style.left), w=parseFloat(zoneEl.style.width);
if(capPos>=l && capPos<=l+w) perfect=true;
}
let chance=base + (perfect?0.35:0);
state.inventory[orb]--;
if(Math.random()<chance){
state.caught.add(target.sid); state.seen.add(target.sid);
// add to party or box
if(state.party.length<6){
let newCre=createCreature(target.sid, target.lvl);
state.party.push(newCre);
showDialog(`Bound ${target.name}! Joined your chorus. (${state.caught.size}/8)`, "Chorus");
} else {
state.box.push(createCreature(target.sid,target.lvl));
showDialog(`Bound ${target.name}! Sent to reserve.`, "Chorus");
}
battle.enemies=battle.enemies.filter(e=>e!==target);
if(battle.enemies.length===0){ checkBattleEnd(); hideBattle(); } else renderBattle();
save(); renderPanel();
} else {
showDialog(`The orb trembled... ${target.name} broke free! ${perfect?"(Perfect timing but still escaped)":""}`, "Shade");
// enemy gets a free hit
setTimeout(()=>{ if(battle) { battle.phase="choose"; renderBattle(); }},600);
}
battle.capture=null;
}
document.getElementById('captureTap').onclick=()=>resolveCapture(true);
// keyboard for capture space
document.addEventListener('keydown',e=>{
if(document.getElementById('captureModal').classList.contains('show') && e.code==="Space"){ e.preventDefault(); resolveCapture(true);}
});
function showDialog(text,speaker="Warden"){
document.getElementById('dialog').classList.add('show');
document.getElementById('dialogText').textContent=text;
document.getElementById('dialogSpeaker').textContent=speaker;
}
document.getElementById('dialogNext').onclick=()=>document.getElementById('dialog').classList.remove('show');
function handleMove(dx,dy){
if(battle) return;
let nx=state.px+dx, ny=state.py+dy;
let map=maps[state.zone];
if(ny<0||ny>=map.length||nx<0||nx>=map[0].length) return;
let tile=map[ny][nx];
if(tile===1) { spawnFloat("blocked","#9aa4c2",state.px*40,state.py*40); return; }
state.px=nx; state.py=ny;
state.dir= dx===1?"right":dx===-1?"left":dy===1?"down":"up";
// tile events
if(tile===3){ // shrine
state.party.forEach(c=>{c.hp=c.hpMax; c.status=null; c.barrier=false;});
showDialog("Aether Shrine: chorus restored. Shop: potions & orbs. Progress saved.", "Shrine");
save(); renderPanel();
if(state.party.length===0){
document.getElementById('starterModal').classList.add('show');
renderStarterChoices();
}
} else if(tile===4){
if(state.guardians[state.zone]){ showDialog("Veil already anchored. The path onward is open.", "Warden"); }
else {
if(state.party.length===0){ showDialog("You need an echo to challenge the Anchor.", "Warden"); return; }
startGuardianBattle();
}
} else if(tile===2){
if(state.encCooldown>0){ state.encCooldown--; }
else if(Math.random()< ZONES[state.zone].rate){
state.encCooldown=2;
if(state.party.length===0){
// give chance to capture without battle? start tutorial capture
startWildBattle();
} else startWildBattle();
}
}
drawOverworld(); renderPanel();
// story nodes
if(state.px===8&&state.py===6&&state.story===0){ state.story=1; showDialog("Act I — The Pale Veil thins. You hear the first chorus beyond grass. Bind 2 echoes to proceed.", "Warden"); }
}
document.addEventListener('keydown',e=>{
if(e.key==="ArrowUp"||e.key==="w"||e.key==="W")handleMove(0,-1);
if(e.key==="ArrowDown"||e.key==="s"||e.key==="S")handleMove(0,1);
if(e.key==="ArrowLeft"||e.key==="a"||e.key==="A")handleMove(-1,0);
if(e.key==="ArrowRight"||e.key==="d"||e.key==="D")handleMove(1,0);
if(e.key==="Enter"||e.key===" ")handleInteract();
});
document.querySelectorAll('[data-dir]').forEach(b=>b.addEventListener('click',()=>{
let d=b.dataset.dir;
if(d==="up")handleMove(0,-1); if(d==="down")handleMove(0,1); if(d==="left")handleMove(-1,0); if(d==="right")handleMove(1,0);
}));
document.getElementById('interactBtn').onclick=handleInteract;
function handleInteract(){
let tile=maps[state.zone][state.py][state.px];
if(tile===3){ showDialog("Shrine menu: Heal • Shop • Save. Bag has orbs.", "Shrine"); document.getElementById('bagModal').classList.add('show'); renderBag(); }
else if(tile===4 && !state.guardians[state.zone]) startGuardianBattle();
else if(tile===2) { if(Math.random()<0.5) startWildBattle(); else showDialog("Tall veil-grass rustles...","Field");}
}
function renderPanel(){
let tab=document.querySelector('.tab.active')?.dataset.tab||"dex";
document.getElementById('panelTitle').textContent= tab==="dex"?"CHORUS DEX":tab==="team"?"BINDING":tab==="bag"?"SATCHEL":"VEILS";
let body=document.getElementById('panelBody');
if(tab==="dex"){
body.innerHTML=SPECIES.map(s=>{
let seen=state.seen.has(s.id), caught=state.caught.has(s.id);
return `<div class="card" style="opacity:${seen?1:0.45}"><div style="display:flex;justify-content:spaceBetween;align-items:center"><h4>${seen?s.name:"???"} <span class="type t-${s.type}" style="font-size:9px">${s.type}</span></h4><span class="badge">${caught?"BOUND":""}${!seen?"UNSEEN":seen&&!caught?"SEEN":""}</span></div><div class="muted">${seen?s.desc:"Veil obscures this echo."}</div><div class="statRow"><span class="stat">HP ${s.base.hp}</span><span class="stat">ATK ${s.base.atk}</span><span class="stat">DEF ${s.base.def}</span><span class="stat">SPD ${s.base.spd}</span></div><div class="muted" style="margin-top:6px">Moves: ${s.learn.map(l=>ABILITIES[l[1]].name).join(", ")}</div>${s.evo?`<div class="muted">Evolves → ${SPECIES[s.evo.to].name} Lv${s.evo.lvl} + ${s.evo.item}</div>`:""}</div>`;
}).join("");
} else if(tab==="team"){
if(state.party.length===0) body.innerHTML=`<div class="card muted">No echoes bound yet. Visit shrine (gold ◈) and pick starter trio.</div>`;
else body.innerHTML=state.party.map((c,i)=>`<div class="card"><div style="display:flex;justify-content:space-between"><h4>${c.name} Lv${c.lvl} <span class="type t-${c.type}" style="font-size:9px">${c.type}</span></h4><span class="badge">${c.hp}/${c.hpMax} HP</span></div><div class="hpBar"><div class="hpFill" style="width:${c.hp/c.hpMax*100}%"></div></div><div class="statRow"><span class="stat">ATK ${c.atk}</span><span class="stat">DEF ${c.def}</span><span class="stat">SPD ${c.spd}</span><span class="stat">${c.status||"clear"}</span></div><div class="muted">Moves: ${c.moves.map(m=>ABILITIES[m].name).join(" • ")}</div><div style="display:flex;gap:6px;margin-top:8px"><button class="btn small" onclick="healOne(${i})">Potion</button><button class="btn small" onclick="tryEvolve(${i})">Evolve</button><button class="btn small" onclick="release(${i})">Release</button></div></div>`).join("");
} else if(tab==="bag"){
body.innerHTML=`
<div class="card"><h4>Orbs</h4><div class="muted">T1 Veil Orb ×${state.inventory.orb1} • T2 Aether Orb ×${state.inventory.orb2} • T3 Crown Orb ×${state.inventory.orb3}</div></div>
<div class="card"><h4>Consumables</h4><div class="muted">Potion ×${state.inventory.potion} (heal 40) • Revive ×${state.inventory.revive}</div><button class="btn small" onclick="buy('potion')">Buy Potion (-1 orb1)</button></div>
<div class="card"><h4>Catalysts</h4><div class="muted">${["Cinder Core","Brine Pearl","Verdant Seed","Krag Shard","Gale Feather","Lumen Prism","Void Filament"].map(k=>`${k} ×${state.inventory[k]||0}`).join(" • ")}</div></div>
<div class="card"><h4>Field</h4><div class="muted">Bound: ${state.caught.size} • Seen: ${state.seen.size} • Anchors: ${state.guardians.filter(Boolean).length}/6</div></div>`;
} else {
body.innerHTML=`
<div class="card"><h4>Act ${Math.min(4,state.story+1)} — ${["The Pale Waking","Basin Hymn","Tangle Verdict","Core Loom"][state.story]||"Free Roam"}</h4><div class="muted">${["Bind your first echo at the shrine. Walk the meadow, learn type edges: Ember > Verdant, Tide > Ember, Verdant > Tide. Stone>Ember/Verdant, Gale>Stone/Tide, Luminara > all, Nullshroud resists.","Azure Basin Anchor awaits. Tidevein beats Ember, Gale beats Tide.","Thorn Bastion guards Verdant. Bring Ember or Stone.","Aerie & Core: Luminara shines vs Nullshroud. Gather 8 echoes and 4 anchors to master the veil."][state.story]||"Post-game: wilds scaled, hunt all 27."}</div></div>
<div class="card"><h4>Type Chart</h4><div class="muted" style="line-height:1.8"><span class="type t-Embervein">Ember</span> > <span class="type t-Verdant">Verdant</span> • <span class="type t-Tideworn">Tide</span> > <span class="type t-Embervein">Ember</span> • <span class="type t-Verdant">Verdant</span> > <span class="type t-Tideworn">Tide</span><br><span class="type t-Stonebound">Stone</span> > Ember & Verdant • <span class="type t-Galewhisper">Gale</span> > Stone & Tide<br><span class="type t-Luminara">Luminara</span> 1.25× vs all, 2× vs Nullshroud • <span class="type t-Nullshroud">Nullshroud</span> resists 0.65×</div></div>
<div class="card"><h4>Progression</h4><div class="muted">Zones: ${ZONES.map((z,i)=>`${z.name} ${state.guardians[i]?"✓":"○"}`).join(" → ")}</div></div>`;
}
renderStrip();
}
function renderStrip(){
let el=document.getElementById('partyStrip'); el.innerHTML="";
state.party.slice(0,6).forEach(c=>{
let d=document.createElement('div');d.className='pMini';
d.innerHTML=`<span style="font-size:9px">${c.name.slice(0,3)}</span><div class="hp" style="width:${c.hp/c.hpMax*100}%"></div>`;
d.title=`${c.name} Lv${c.lvl} ${c.hp}/${c.hpMax}`;
el.appendChild(d);
});
if(state.party.length===0){ let d=document.createElement('div');d.className='pMini';d.textContent="—";el.appendChild(d);}
}
document.querySelectorAll('.tab').forEach(t=>t.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active')); t.classList.add('active'); renderPanel();}));
function healOne(i){ if(state.inventory.potion<=0){showDialog("No potions.","Bag");return;} state.inventory.potion--; state.party[i].hp=Math.min(state.party[i].hpMax, state.party[i].hp+40); renderPanel(); save();}
function release(i){ state.party.splice(i,1); renderPanel(); save();}
function tryEvolve(i){ let c=state.party[i]; let s=SPECIES[c.sid]; if(!s.evo){showDialog("No evolution.", "Chorus");return;} if(c.lvl < s.evo.lvl){showDialog(`Need Lv${s.evo.lvl}.`, "Chorus");return;} if(!state.inventory[s.evo.item]){showDialog(`Need ${s.evo.item}.`,"Chorus");return;} state.inventory[s.evo.item]--; c.sid=s.evo.to; c.name=SPECIES[s.evo.to].name; c.type=SPECIES[s.evo.to].type; c.hpMax+=12;c.hp=c.hpMax; showDialog(`Evolved to ${c.name}!`, "Chorus"); renderPanel(); save();}
function buy(item){ if(state.inventory.orb1<=0){showDialog("Need Veil Orb.","Shop");return;} state.inventory.orb1--; state.inventory.potion++; renderPanel(); save();}
function renderBag(){
let el=document.getElementById('bagContent');
el.innerHTML=`
<div class="grid2">
<div class="card"><h4>Shop at Shrine</h4><button class="btn small" onclick="state.inventory.orb1+=3;renderBag();renderPanel();save()">Buy 3 Veil Orbs (free demo)</button><br><br><button class="btn small" onclick="state.inventory.potion+=2;renderBag();renderPanel()">+2 Potions</button></div>
<div class="card"><h4>Heal</h4><button class="btn small" onclick="state.party.forEach(c=>c.hp=c.hpMax);renderBag();renderPanel();showDialog('Chorus restored.','Shrine')">Full Heal</button></div>
</div>
<div class="card muted">Inventory: Orbs ${state.inventory.orb1}/${state.inventory.orb2}/${state.inventory.orb3} • Potions ${state.inventory.potion} • Catalysts ${Object.entries(state.inventory).filter(([k])=>k.includes(' ')).map(([k,v])=>k+':'+v).join(' ')}</div>
<button class="btn primary" onclick="save();showDialog('Progress saved.','System')">Manual Save</button>
`;
}
function renderStarterChoices(){
let cont=document.getElementById('starterChoices'); cont.innerHTML="";
[0,1,2].forEach(sid=>{
let s=SPECIES[sid];
let card=document.createElement('div');card.className='card';
card.innerHTML=`<h4>${s.name} <span class="type t-${s.type}">${s.type}</span></h4><div class="muted">${s.desc}</div><div class="statRow"><span class="stat">HP ${s.base.hp}</span><span class="stat">ATK ${s.base.atk}</span></div><button class="btn primary" style="margin-top:8px">Choose</button>`;
card.querySelector('button').onclick=()=>{
state.party=[createCreature(sid,5)];
state.seen.add(sid); state.caught.add(sid);
document.getElementById('starterModal').classList.remove('show');
showDialog(`Bound ${s.name}! Walk the grass (≋) to encounter wilds. Press Capture in battle and nail the rhythm!`, "Chorus");
maps.forEach(m=>m[2][2]=0);
save(); renderPanel(); drawOverworld();
};
cont.appendChild(card);
});
}
document.getElementById('starterPreview').innerHTML= [0,1,2].map(i=>`<span class="badge">${SPECIES[i].name} • ${SPECIES[i].type}</span>`).join("");
function closeModal(id){ document.getElementById(id).classList.remove('show'); }
document.getElementById('bagBtn').onclick=()=>{document.getElementById('bagModal').classList.add('show');renderBag();};
document.getElementById('teamBtn').onclick=()=>{document.getElementById('teamModal').classList.add('show');document.getElementById('teamContent').innerHTML=document.getElementById('panelBody').innerHTML;};
document.getElementById('codexBtn').onclick=()=>{document.getElementById('codexModal').classList.add('show');document.getElementById('codexContent').innerHTML=document.getElementById('panelBody').innerHTML;};
document.getElementById('mapBtn').onclick=()=>{
document.getElementById('mapModal').classList.add('show');
let el=document.getElementById('mapContent');
el.innerHTML=`<div class="grid2">${ZONES.map((z,i)=>`<div class="card" style="border-color:${state.zone===i?z.color:''}"><h4>${z.name} ${state.guardians[i]?"✓ Anchored":"○ Veiled"}</h4><div class="muted">Rate ${(z.rate*100).toFixed(0)}% • Guardian ${SPECIES[z.guardian].name}</div><button class="btn small" onclick="state.zone=${i};state.px=2;state.py=2;closeModal('mapModal');drawOverworld();renderPanel();spawnParticles(14)">Travel</button></div>`).join("")}</div>`;
};
function save(){ localStorage.setItem('aethermund_save', JSON.stringify({state:{...state, seen:[...state.seen], caught:[...state.caught]}})); }
function load(){ let raw=localStorage.getItem('aethermund_save'); if(!raw) return false; try{ let o=JSON.parse(raw); state=o.state; state.seen=new Set(o.state.seen); state.caught=new Set(o.state.caught); return true;}catch(e){return false;}}
document.getElementById('newGameBtn').onclick=()=>{
state.zone=0; state.px=2; state.py=2; state.party=[]; state.box=[]; state.seen=new Set(); state.caught=new Set(); state.guardians=[false,false,false,false,false,false]; state.story=0;
state.inventory={orb1:8,orb2:2,orb3:1,potion:4,revive:1,"Cinder Core":1,"Brine Pearl":1,"Verdant Seed":1,"Krag Shard":1,"Gale Feather":1,"Lumen Prism":1,"Void Filament":1};
document.getElementById('titleScreen').style.display='none';
showDialog("Welcome, Binder. Move with WASD, step on gold shrines to heal and choose your first echo. Tall grass hides wild choruses.", "Warden");
drawOverworld(); renderPanel(); spawnParticles(14);
};
document.getElementById('continueBtn').onclick=()=>{
if(load()){ document.getElementById('titleScreen').style.display='none'; drawOverworld(); renderPanel(); showDialog("Veil resumed.","System");}
else showDialog("No save found. Begin New Binding.", "System");
};
document.getElementById('howBtn').onclick=()=>{ showDialog("Move tile by tile. Gold ◈ = shrine (heal/shop/save). Red ⚑ = Anchor boss. ≋ = wild grass. In battle: SPD order, 4 moves, Capture with rhythm tap. Burn halves ATK + DoT, Frost halves SPD, Shock may skip turn, Barrier blocks one hit.", "Guide"); document.getElementById('titleScreen').style.display='none'; drawOverworld(); renderPanel(); };
// audio
let audioOn=false, actx;
document.getElementById('soundBtn').onclick=()=>{
audioOn=!audioOn;
document.getElementById('soundBtn').textContent=audioOn?"♫":"♪";
if(audioOn){ actx=new (window.AudioContext||window.webkitAudioContext)(); let o=actx.createOscillator(), g=actx.createGain(); o.type="sine"; o.frequency.value=110; g.gain.value=0.02; o.connect(g).connect(actx.destination); o.start(); setTimeout(()=>{o.stop();},800); }
};
if(load()){ /* keep for continue */ }
drawOverworld(); renderPanel();
// auto-save at shrines already; manual
setInterval(save,8000);
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"38f8162e-aac2-4551-9c2b-55992213693b","tokensIn":1241,"tokensOut":21255,"tokensTotal":22496,"cost":0.091885,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:26:24.780Z"} -->