Aethelgard
muse spark 1.1 · composer

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
Human review · Jul 17, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
The reviewer marked this result as broken. It remains in the archive.
- Visual design
- 3 / 5
- UX / feel
- 3 / 5
- Works
- 1 / 5
- Prompt spirit
- 1 / 5
- Delight
- 2 / 5
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 69,992 bytes · SHA-256 dc0e874ee6a2
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AETHERIUM • Resonant Chronicles</title>
<script src="https://cdn.tailwindcss.com/"></script>
<style>
/* ── css: base ── */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800;900&family=JetBrains+Mono:wght@400;700&display=swap');
*{font-family:'Inter',system-ui,sans-serif}
.font-mono{font-family:'JetBrains Mono',monospace !important}
.glass{background:rgba(255,255,255,0.06);backdrop-filter:blur(18px) saturate(1.4);border:1px solid rgba(255,255,255,0.08);box-shadow:0 8px 32px rgba(0,0,0,0.25),inset 0 1px 0 rgba(255,255,255,0.08)}
::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.15);border-radius:999px}
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(15px,-15px)}}
@keyframes floatD{0%,100%{transform:translate(0,0)}50%{transform:translate(-12px,10px)}}
.animate-float{animation:float 12s ease-in-out infinite}
.animate-float-delayed{animation:floatD 14s ease-in-out infinite}
@keyframes bounceSlow{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
.animate-bounce-slow{animation:bounceSlow 2.2s ease-in-out infinite}
@keyframes shake{0%,100%{transform:translate(0)}20%{transform:translate(-6px,2px)}40%{transform:translate(6px,-2px)}60%{transform:translate(-4px,1px)}80%{transform:translate(4px,-1px)}}
.shake{animation:shake 0.35s ease}
@keyframes flash{0%{filter:brightness(2)}100%{filter:none}}
.flash{animation:flash 0.22s ease}
@keyframes capSpin{0%{transform:rotate(0) scale(0.8)}50%{transform:rotate(180deg) scale(1.1)}100%{transform:rotate(360deg) scale(1)}}
.cap-spin{animation:capSpin 0.9s ease-in-out infinite}
.type-Pyro{background:linear-gradient(135deg,#ff5d3b,#ff9a3b);color:white;box-shadow:0 0 12px rgba(255,93,59,0.6)}
.type-Hydro{background:linear-gradient(135deg,#3b82f6,#06b6d4);color:white;box-shadow:0 0 12px rgba(59,130,246,0.6)}
.type-Flora{background:linear-gradient(135deg,#22c55e,#a3e635);color:#052e16;box-shadow:0 0 12px rgba(34,197,94,0.5)}
.type-Volt{background:linear-gradient(135deg,#facc15,#f59e0b);color:#422006;box-shadow:0 0 14px rgba(250,204,21,0.7)}
.type-Zephyr{background:linear-gradient(135deg,#a5b4fc,#e0e7ff);color:#1e1b4b;box-shadow:0 0 12px rgba(165,180,252,0.6)}
.type-Earth{background:linear-gradient(135deg,#a16207,#d6a066);color:white;box-shadow:0 0 12px rgba(161,98,7,0.6)}
.type-Cosmos{background:linear-gradient(135deg,#6b7280,#d1d5db);color:white}
.node{cursor:pointer;transition:all .2s}
.node:hover{filter:brightness(1.4);transform:scale(1.12)}
.node-done{opacity:0.35;pointer-events:none}
.badge-super{color:#22c55e}.badge-not{color:#ef4444}
</style>
</head>
<body>
<div id="app" class="min-h-screen w-full relative overflow-hidden bg-[#0a0a12] text-white selection:bg-violet-500/30">
<div class="fixed inset-0 z-0">
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-violet-900/40 via-[#0a0a12] to-[#0a0a12]"></div>
<div class="absolute inset-0 opacity-[0.03]" style="background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1Ii8+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsdGVyPSJ1cmwoI2EpIi8+PC9zdmc+')"></div>
<div class="absolute w-[600px] h-[600px] rounded-full blur-[120px] opacity-30 bg-violet-600 -top-[200px] -left-[100px] animate-float"></div>
<div class="absolute w-[500px] h-[500px] rounded-full blur-[120px] opacity-20 bg-fuchsia-600 top-[40%] -right-[100px] animate-float-delayed"></div>
</div>
<header class="relative z-20 flex items-center justify-between px-6 md:px-10 py-5 border-b border-white/[0.06] backdrop-blur-xl bg-black/20">
<div class="flex items-center gap-4">
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-fuchsia-500 grid place-items-center font-black tracking-tighter">A</div>
<div><div class="font-bold tracking-[0.2em] text-[13px]">AETHERIUM</div><div class="text-[10px] tracking-[0.3em] opacity-50 -mt-1">RESONANT PROTOCOL</div></div>
</div>
<div class="flex items-center gap-2 md:gap-3">
<div class="glass px-3 py-1.5 rounded-full flex items-center gap-2 text-xs"><span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span><span id="shardDisplay" class="font-mono font-bold">0 ◇</span></div>
<button id="btnDex" class="glass hover:bg-white/10 px-3 md:px-4 py-1.5 rounded-full text-xs tracking-widest transition">DEX</button>
<button id="btnShop" class="glass hover:bg-white/10 px-3 md:px-4 py-1.5 rounded-full text-xs tracking-widest transition">SHOP</button>
<button id="btnSave" class="w-8 h-8 rounded-full glass grid place-items-center hover:bg-white/10 transition">◐</button>
</div>
</header>
<main class="relative z-10">
<section id="screen-menu" class="min-h-[calc(100vh-68px)] flex flex-col items-center justify-center px-6 py-10">
<div class="max-w-[720px] w-full text-center">
<div class="inline-flex glass px-3 py-1 rounded-full text-[10px] tracking-[0.3em] opacity-70 mb-6">CONDUCTOR CLEARANCE LVL_0</div>
<h1 class="text-[clamp(36px,8vw,84px)] font-black leading-[0.85] tracking-[-0.04em]"><span class="bg-gradient-to-br from-white to-white/40 bg-clip-text text-transparent">TUNE THE</span><br><span class="bg-gradient-to-br from-violet-300 to-fuchsia-300 bg-clip-text text-transparent">WILDS</span></h1>
<p class="mt-6 text-sm md:text-[15px] leading-relaxed opacity-60 max-w-[480px] mx-auto">Aethelgard vibrates. You are a Conductor — capture Resonants in Aether Prisms, harmonize elements, and ascend the Apex Pillar.</p>
<div class="mt-10 flex flex-col sm:flex-row gap-3 justify-center">
<button id="btnNew" class="px-8 py-4 rounded-full bg-white text-black font-bold tracking-widest text-sm hover:scale-[1.02] transition">▶ INITIATE LINK</button>
<button id="btnContinue" class="px-8 py-4 rounded-full glass font-bold tracking-widest text-sm hover:bg-white/10 transition">↺ CONTINUE SIGNAL</button>
</div>
<div class="mt-14 grid grid-cols-3 gap-3 text-left">
<div class="glass rounded-2xl p-4"><div class="text-[10px] opacity-50 tracking-widest">ELEMENTS</div><div class="font-bold mt-1">7 Types</div></div>
<div class="glass rounded-2xl p-4"><div class="text-[10px] opacity-50 tracking-widest">RESONANTS</div><div class="font-bold mt-1">18 Forms</div></div>
<div class="glass rounded-2xl p-4"><div class="text-[10px] opacity-50 tracking-widest">ZONES</div><div class="font-bold mt-1">4 Realms</div></div>
</div>
<button id="btnReset" class="mt-10 text-[10px] tracking-widest opacity-30 hover:opacity-70 underline">RESET PROGRESS / WIPE LOCAL PRISM</button>
</div>
</section>
<section id="screen-overworld" class="hidden">
<div class="max-w-[1200px] mx-auto px-6 md:px-10 py-6 md:py-8">
<div class="flex flex-wrap gap-3 items-center justify-between mb-6">
<div class="flex gap-2 flex-wrap" id="zoneTabs"></div>
<div class="flex items-center gap-3">
<div id="partyMini" class="flex gap-2"></div>
<button id="btnHealCamp" class="glass px-4 py-2 rounded-full text-xs">⛨ HEAL CAMP (50◇)</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-6">
<div class="glass rounded-[28px] p-3 md:p-6 relative overflow-hidden min-h-[560px]">
<div id="zoneTitle" class="absolute top-6 left-6 md:left-8 z-10">
<div class="text-[10px] tracking-[0.4em] opacity-60">CURRENT FREQUENCY</div>
<div class="text-2xl font-black tracking-tight mt-1" id="zoneName">Verdant Canopy</div>
<div class="text-xs opacity-60 mt-1" id="zoneDesc">Levels 1-5 • Flora / Zephyr</div>
</div>
<svg id="mapSvg" viewBox="0 0 800 520" class="w-full h-[520px]"></svg>
<div id="nodeTooltip" class="pointer-events-none absolute hidden glass rounded-xl px-3 py-2 text-xs"></div>
</div>
<div class="flex flex-col gap-4">
<div class="glass rounded-[24px] p-5"><div class="text-[10px] tracking-[0.3em] opacity-50 mb-3">ACTIVE HARMONICS</div><div id="partyDetail" class="space-y-3"></div></div>
<div class="glass rounded-[24px] p-5"><div class="text-[10px] tracking-[0.3em] opacity-50 mb-3">CONDUCTOR LOG</div><div id="logBox" class="font-mono text-[11px] leading-relaxed opacity-80 h-[160px] overflow-auto whitespace-pre-wrap"></div></div>
<div class="glass rounded-[24px] p-5"><div class="text-[10px] tracking-[0.3em] opacity-50 mb-2">INVENTORY</div><div id="invQuick" class="flex flex-wrap gap-2"></div></div>
</div>
</div>
</div>
</section>
<section id="screen-battle" class="hidden min-h-[calc(100vh-68px)]">
<div class="max-w-[1100px] mx-auto px-4 md:px-8 py-4 md:py-6">
<div class="glass rounded-[30px] overflow-hidden relative" id="battleCard">
<div id="battleBg" class="absolute inset-0 opacity-60"></div>
<div class="relative p-4 md:p-8 grid grid-cols-2 gap-4">
<div class="order-2 md:order-1 flex flex-col items-start">
<div class="glass rounded-2xl px-4 py-3 w-full max-w-[340px]"><div class="flex justify-between items-center"><div class="font-black tracking-tight" id="enemyName">Pyrowolf</div><div class="text-[10px] tracking-widest opacity-60">Lv <span id="enemyLv">8</span> <span id="enemyType" class="ml-2 px-2 py-0.5 rounded-full text-[9px]"></span></div></div><div class="mt-2 h-2 rounded-full bg-black/40 overflow-hidden"><div id="enemyHp" class="h-full bg-white transition-all duration-500" style="width:100%"></div></div><div class="mt-1 flex justify-between text-[10px] font-mono opacity-60"><span id="enemyHpTxt">40/40</span><span id="enemyStatus"></span></div></div>
<div id="enemySprite" class="mt-6 md:mt-10 w-[180px] md:w-[260px] h-[180px] md:h-[260px] self-center"></div>
</div>
<div class="order-1 md:order-2 flex flex-col items-end">
<div id="playerSprite" class="w-[200px] md:w-[300px] h-[200px] md:h-[300px]"></div>
<div class="glass rounded-2xl px-4 py-3 w-full max-w-[360px] mt-4"><div class="flex justify-between items-center"><div class="font-black tracking-tight" id="playerName">Emberkit</div><div class="text-[10px] tracking-widest opacity-60">Lv <span id="playerLv">5</span> <span id="playerType" class="ml-2 px-2 py-0.5 rounded-full text-[9px]"></span></div></div><div class="mt-2 h-2.5 rounded-full bg-black/40 overflow-hidden"><div id="playerHp" class="h-full bg-emerald-400 transition-all duration-500" style="width:100%"></div></div><div class="mt-1 flex justify-between text-[10px] font-mono opacity-60"><span id="playerHpTxt">40/40</span><span id="playerStatus"></span></div><div class="mt-2 h-1.5 rounded-full bg-black/30 overflow-hidden"><div id="playerXp" class="h-full bg-violet-400 transition-all" style="width:20%"></div></div></div>
</div>
</div>
<div class="relative border-t border-white/10 grid md:grid-cols-[1.1fr_0.9fr] gap-0">
<div class="p-4 md:p-6"><div id="battleLog" class="font-mono text-[12px] leading-6 min-h-[110px] whitespace-pre-wrap"></div></div>
<div class="bg-black/30 backdrop-blur p-3 md:p-4 grid grid-cols-2 gap-2" id="battleActions"></div>
</div>
</div>
<div class="mt-4 flex justify-between items-center text-[11px] opacity-60"><button id="btnFlee" class="glass px-4 py-2 rounded-full">↩ DISENGAGE (Wild only)</button><div id="battleHints" class="tracking-widest">TYPE ADVANTAGE • 1.6x • CRIT 10%</div></div>
</div>
</section>
<section id="screen-dex" class="hidden px-6 md:px-10 py-8 max-w-[1200px] mx-auto"><div class="flex justify-between items-center mb-6"><h2 class="text-3xl font-black tracking-tight">CONDUCTORS DEX</h2><button class="btnClose glass px-4 py-2 rounded-full text-xs">✕ CLOSE</button></div><div id="dexGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div></section>
<section id="screen-shop" class="hidden px-6 md:px-10 py-8 max-w-[900px] mx-auto"><div class="flex justify-between items-center mb-6"><h2 class="text-3xl font-black tracking-tight">CAMP SUPPLY</h2><button class="btnClose glass px-4 py-2 rounded-full text-xs">✕ CLOSE</button></div><div id="shopGrid" class="grid md:grid-cols-2 gap-3"></div></section>
</main>
<div id="modalCapture" class="fixed inset-0 z-50 hidden place-items-center bg-black/70 backdrop-blur"><div class="glass rounded-[28px] p-8 max-w-[380px] w-[90%] text-center"><div id="capAnim" class="w-[120px] h-[120px] mx-auto mb-4 rounded-full grid place-items-center text-5xl">◍</div><div id="capText" class="font-black text-xl">Harmonizing...</div><div class="mt-2 text-xs opacity-60" id="capSub">Aether Prism stabilizing frequency</div></div></div>
<div id="toast" class="fixed bottom-6 left-1/2 -translate-x-1/2 z-[60] pointer-events-none opacity-0 transition glass px-5 py-3 rounded-full text-xs tracking-widest"></div>
</div>
<script>
'use strict';
// ── class: AudioSynth ──
class AudioSynth {
constructor(){ this.ctx=null; }
init(){ if(!this.ctx) this.ctx=new (window.AudioContext||window.webkitAudioContext)(); }
tone(freq, type, dur, vol=0.18){
try{ this.init(); const o=this.ctx.createOscillator(), g=this.ctx.createGain(); o.type=type; o.frequency.value=freq; g.gain.value=vol; g.gain.exponentialRampToValueAtTime(0.0001,this.ctx.currentTime+dur); o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+dur);}catch(e){}
}
uiHover(){ this.tone(660,'square',0.08,0.06); }
uiSelect(){ this.tone(440,'sine',0.12,0.16); setTimeout(()=>this.tone(880,'sine',0.18,0.14),80); }
hitPhys(){ this.tone(120,'square',0.18,0.3); setTimeout(()=>this.tone(60,'sawtooth',0.12,0.22),40); }
hitElem(isSuper){ this.tone(isSuper?620:320,'sawtooth',0.22,0.32); if(isSuper) setTimeout(()=>this.tone(980,'sine',0.3,0.22),70); }
levelUp(){ [261,329,392,523].forEach((f,i)=>setTimeout(()=>this.tone(f,'sine',0.4,0.18), i*90)); }
captureOk(){ [523,659,783,1046].forEach((f,i)=>setTimeout(()=>this.tone(f,'triangle',0.35,0.2), i*110)); }
captureFail(){ this.tone(200,'square',0.3,0.2); setTimeout(()=>this.tone(120,'square',0.4,0.2),120); }
heal(){ this.tone(440,'triangle',0.4,0.15); setTimeout(()=>this.tone(660,'triangle',0.5,0.15),180); }
}
// ── const: synth ──
const synth = new AudioSynth();
// ── const: ELEMENTS ──
const ELEMENTS = ['Pyro','Hydro','Flora','Volt','Zephyr','Earth','Cosmos'];
// ── const: TYPE_COLORS ──
const TYPE_COLORS = { Pyro:['#ff5d3b','#ff9a3b'], Hydro:['#3b82f6','#06b6d4'], Flora:['#22c55e','#a3e635'], Volt:['#facc15','#f59e0b'], Zephyr:['#a5b4fc','#c4b5fd'], Earth:['#a16207','#d6a066'], Cosmos:['#6b7280','#a1a1aa'] };
// ── const: TYPE_CHART ──
const TYPE_CHART = {
Pyro:{ Flora:1.6, Hydro:0.6 },
Hydro:{ Pyro:1.6, Flora:0.6 },
Flora:{ Hydro:1.6, Pyro:0.6 },
Zephyr:{ Earth:1.6, Volt:0.6 },
Earth:{ Volt:1.6, Zephyr:0.6 },
Volt:{ Zephyr:1.6, Earth:0.6 },
Cosmos:{}
};
// ── function: getEffectiveness ──
function getEffectiveness(atkType, defType){
if(!TYPE_CHART[atkType]) return 1;
return TYPE_CHART[atkType][defType] ?? 1;
}
// ── const: MOVE_POOL ──
const MOVE_POOL = {
Ember:{power:40,acc:100,type:'Pyro'}, FlareBite:{power:60,acc:95,type:'Pyro'}, InfernoClaw:{power:85,acc:90,type:'Pyro'}, SolarFlare:{power:110,acc:80,type:'Pyro'},
Splash:{power:40,acc:100,type:'Hydro'}, TidalSmash:{power:60,acc:95,type:'Hydro'}, AbyssalWave:{power:85,acc:90,type:'Hydro'}, Maelstrom:{power:110,acc:80,type:'Hydro'},
VineWhip:{power:40,acc:100,type:'Flora'}, ThornBarrage:{power:60,acc:95,type:'Flora'}, RootCrush:{power:85,acc:90,type:'Flora'}, BloomCannon:{power:110,acc:80,type:'Flora'},
Spark:{power:40,acc:100,type:'Volt'}, VoltSurge:{power:60,acc:95,type:'Volt'}, ThunderFang:{power:85,acc:90,type:'Volt'}, Gigavolt:{power:110,acc:80,type:'Volt'},
gust:{power:40,acc:100,type:'Zephyr'}, CycloneCut:{power:60,acc:95,type:'Zephyr'}, SkyRend:{power:85,acc:90,type:'Zephyr'}, Tempest:{power:110,acc:80,type:'Zephyr'},
PebbleToss:{power:40,acc:100,type:'Earth'}, QuakeStomp:{power:60,acc:95,type:'Earth'}, CragLance:{power:85,acc:90,type:'Earth'}, TerraBreak:{power:110,acc:80,type:'Earth'},
Tackle:{power:35,acc:100,type:'Cosmos'}, Resonate:{power:50,acc:100,type:'Cosmos'}, NullPulse:{power:70,acc:95,type:'Cosmos'}, VoidCollapse:{power:100,acc:85,type:'Cosmos'}
};
// ── const: RESONANTS_DB ──
const RESONANTS_DB = [
// PYRO
{ id:'emberkit', name:'Emberkit', type:'Pyro', tier:1, base:{hp:42,atk:52,def:35,spd:58}, evoTo:'pyrowolf', evoLvl:8, desc:'A skittish cinder cub that sneezes sparks.',
moves:[{id:'Tackle',lvl:1},{id:'Ember',lvl:1},{id:'FlareBite',lvl:6},{id:'Resonate',lvl:10},{id:'InfernoClaw',lvl:14}] },
{ id:'pyrowolf', name:'Pyrowolf', type:'Pyro', tier:2, base:{hp:58,atk:74,def:50,spd:76}, evoTo:'ignisaur', evoLvl:15, desc:'Maned predator, mane burns brighter when hunting.',
moves:[{id:'Ember',lvl:1},{id:'FlareBite',lvl:1},{id:'Resonate',lvl:8},{id:'InfernoClaw',lvl:12},{id:'SolarFlare',lvl:16}] },
{ id:'ignisaur', name:'Ignisaur', type:'Pyro', tier:3, base:{hp:78,atk:102,def:72,spd:88}, evoTo:null, evoLvl:null, desc:'Apex flame sovereign. Its roar ignites the air.',
moves:[{id:'FlareBite',lvl:1},{id:'InfernoClaw',lvl:1},{id:'VoidCollapse',lvl:15},{id:'SolarFlare',lvl:18},{id:'NullPulse',lvl:20}] },
// HYDRO
{ id:'droplet', name:'Droplet', type:'Hydro', tier:1, base:{hp:50,atk:38,def:55,spd:42}, evoTo:'geoxus', evoLvl:8, desc:'Cheerful water droplet that bounces to communicate.',
moves:[{id:'Tackle',lvl:1},{id:'Splash',lvl:1},{id:'TidalSmash',lvl:6},{id:'Resonate',lvl:9},{id:'AbyssalWave',lvl:13}] },
{ id:'geoxus', name:'Geoxus', type:'Hydro', tier:2, base:{hp:68,atk:56,def:78,spd:56}, evoTo:'neptunian', evoLvl:15, desc:'Shell-armored tide guardian.',
moves:[{id:'Splash',lvl:1},{id:'TidalSmash',lvl:1},{id:'Resonate',lvl:8},{id:'AbyssalWave',lvl:12},{id:'Maelstrom',lvl:16}] },
{ id:'neptunian', name:'Neptunian', type:'Hydro', tier:3, base:{hp:92,atk:72,def:102,spd:68}, evoTo:null, evoLvl:null, desc:'Ancient leviathan of the sunken trench.',
moves:[{id:'TidalSmash',lvl:1},{id:'AbyssalWave',lvl:1},{id:'NullPulse',lvl:15},{id:'Maelstrom',lvl:18},{id:'VoidCollapse',lvl:21}] },
// FLORA
{ id:'seedling', name:'Seedling', type:'Flora', tier:1, base:{hp:48,atk:44,def:48,spd:44}, evoTo:'thornback', evoLvl:8, desc:'Rooted sprite always seeking sunlight.',
moves:[{id:'Tackle',lvl:1},{id:'VineWhip',lvl:1},{id:'ThornBarrage',lvl:5},{id:'Resonate',lvl:9},{id:'RootCrush',lvl:13}] },
{ id:'thornback', name:'Thornback', type:'Flora', tier:2, base:{hp:64,atk:66,def:68,spd:58}, evoTo:'bloomgoliath', evoLvl:15, desc:'Vine-wrapped brute, flowers bloom after battles.',
moves:[{id:'VineWhip',lvl:1},{id:'ThornBarrage',lvl:1},{id:'Resonate',lvl:8},{id:'RootCrush',lvl:12},{id:'BloomCannon',lvl:16}] },
{ id:'bloomgoliath', name:'Bloomgoliath', type:'Flora', tier:3, base:{hp:88,atk:88,def:92,spd:66}, evoTo:null, evoLvl:null, desc:'Walking jungle. Birds nest in its canopy.',
moves:[{id:'ThornBarrage',lvl:1},{id:'RootCrush',lvl:1},{id:'VoidCollapse',lvl:15},{id:'BloomCannon',lvl:18},{id:'NullPulse',lvl:20}] },
// VOLT
{ id:'sparky', name:'Sparky', type:'Volt', tier:1, base:{hp:38,atk:48,def:32,spd:72}, evoTo:'boltsaber', evoLvl:8, desc:'Static fur, always makes your hair stand up.',
moves:[{id:'Tackle',lvl:1},{id:'Spark',lvl:1},{id:'VoltSurge',lvl:5},{id:'Resonate',lvl:9},{id:'ThunderFang',lvl:12}] },
{ id:'boltsaber', name:'Boltsaber', type:'Volt', tier:2, base:{hp:52,atk:68,def:46,spd:96}, evoTo:'stormlord', evoLvl:15, desc:'Cheetah-like Volt predator that rides lightning.',
moves:[{id:'Spark',lvl:1},{id:'VoltSurge',lvl:1},{id:'Resonate',lvl:8},{id:'ThunderFang',lvl:12},{id:'Gigavolt',lvl:16}] },
{ id:'stormlord', name:'Stormlord', type:'Volt', tier:3, base:{hp:68,atk:94,def:64,spd:124}, evoTo:null, evoLvl:null, desc:'Storm given form. Crackles like a broken speaker.',
moves:[{id:'VoltSurge',lvl:1},{id:'ThunderFang',lvl:1},{id:'NullPulse',lvl:15},{id:'Gigavolt',lvl:18},{id:'VoidCollapse',lvl:22}] },
// ZEPHYR
{ id:'gusty', name:'Gusty', type:'Zephyr', tier:1, base:{hp:40,atk:42,def:36,spd:66}, evoTo:'slipstream', evoLvl:8, desc:'Playful wind sprite that loves to steal hats.',
moves:[{id:'Tackle',lvl:1},{id:'gust',lvl:1},{id:'CycloneCut',lvl:6},{id:'Resonate',lvl:9},{id:'SkyRend',lvl:13}] },
{ id:'slipstream', name:'Slipstream', type:'Zephyr', tier:2, base:{hp:56,atk:62,def:52,spd:86}, evoTo:'zephyrus', evoLvl:15, desc:'Half-bird, half-gale, all speed.',
moves:[{id:'gust',lvl:1},{id:'CycloneCut',lvl:1},{id:'Resonate',lvl:8},{id:'SkyRend',lvl:12},{id:'Tempest',lvl:16}] },
{ id:'zephyrus', name:'Zephyrus', type:'Zephyr', tier:3, base:{hp:74,atk:82,def:68,spd:116}, evoTo:null, evoLvl:null, desc:'Wind deity. Wings create hurricanes when idle.',
moves:[{id:'CycloneCut',lvl:1},{id:'SkyRend',lvl:1},{id:'VoidCollapse',lvl:15},{id:'Tempest',lvl:18},{id:'NullPulse',lvl:21}] },
// EARTH
{ id:'pebble', name:'Pebble', type:'Earth', tier:1, base:{hp:52,atk:46,def:62,spd:28}, evoTo:'obsidian', evoLvl:8, desc:'Grumpy little rock that rolls to get attention.',
moves:[{id:'Tackle',lvl:1},{id:'PebbleToss',lvl:1},{id:'QuakeStomp',lvl:5},{id:'Resonate',lvl:9},{id:'CragLance',lvl:13}] },
{ id:'obsidian', name:'Obsidian', type:'Earth', tier:2, base:{hp:70,atk:68,def:88,spd:38}, evoTo:'terrasunder', evoLvl:15, desc:'Blade-sharp stone golem, surprisingly gentle.',
moves:[{id:'PebbleToss',lvl:1},{id:'QuakeStomp',lvl:1},{id:'Resonate',lvl:8},{id:'CragLance',lvl:12},{id:'TerraBreak',lvl:16}] },
{ id:'terrasunder', name:'Terrasunder', type:'Earth', tier:3, base:{hp:96,atk:92,def:118,spd:46}, evoTo:null, evoLvl:null, desc:'Mountain heart. Each step causes tremors.',
moves:[{id:'QuakeStomp',lvl:1},{id:'CragLance',lvl:1},{id:'NullPulse',lvl:15},{id:'TerraBreak',lvl:18},{id:'VoidCollapse',lvl:22}] }
];
// ── function: findResonant ──
function findResonant(id){ return RESONANTS_DB.find(r=>r.id===id); }
// ── const: SHOP_CATALOG ──
const SHOP_CATALOG = [
{ id:'prism_basic', name:'Harmonizer Prism', desc:'Standard capture matrix • 1.0x', price:100, icon:'◍' },
{ id:'prism_hyper', name:'Hyper-Conductive Prism', desc:'High fidelity • 1.8x catch', price:350, icon:'⬢' },
{ id:'heal_50', name:'Core Restorer', desc:'Restores 50% HP to active', price:80, icon:'+' },
{ id:'heal_full', name:'Resonance Elixir', desc:'Full heal + cleanse all party', price:200, icon:'✦' },
{ id:'boost_prism', name:'Prism Overcharger', desc:'+25% next capture (stacks)', price:150, icon:'⚡' }
];
// ── function: makeSVGResonant ──
function makeSVGResonant(resId, size=120){
const r=findResonant(resId); if(!r) return '';
const [c1,c2]=TYPE_COLORS[r.type]||['#fff','#aaa'];
const tier=r.tier;
const bg=`<defs><radialGradient id="g${resId}" cx="50%" cy="40%"><stop offset="0%" stop-color="${c2}" stop-opacity="0.9"/><stop offset="100%" stop-color="${c1}" stop-opacity="0.9"/></radialGradient><filter id="glow${resId}"><feGaussianBlur stdDeviation="8" result="coloredBlur"/><feMerge><feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>`;
let shape='';
if(r.type==='Pyro'){
shape=`<g filter="url(#glow${resId})"><ellipse cx="60" cy="78" rx="${28+tier*6}" ry="${18+tier*3}" fill="url(#g${resId})"/><circle cx="60" cy="48" r="${20+tier*4}" fill="url(#g${resId})"/><path d="M42 30 Q60 4 78 30 Q72 18 60 12 Q48 18 42 30" fill="${c2}"/><circle cx="50" cy="45" r="3" fill="white"/><circle cx="70" cy="45" r="3" fill="white"/><path d="M52 60 Q60 68 68 60" stroke="black" stroke-width="1.8" fill="none" stroke-linecap="round"/></g>`;
} else if(r.type==='Hydro'){
shape=`<g filter="url(#glow${resId})"><ellipse cx="60" cy="70" rx="${26+tier*5}" ry="${24+tier*4}" fill="url(#g${resId})"/><circle cx="60" cy="40" r="${18+tier*3}" fill="url(#g${resId})"/><path d="M35 68 Q60 95 85 68" fill="${c1}" opacity="0.6"/><circle cx="51" cy="38" r="3" fill="white"/><circle cx="69" cy="38" r="3" fill="white"/></g>`;
} else if(r.type==='Flora'){
shape=`<g filter="url(#glow${resId})"><ellipse cx="60" cy="76" rx="${26+tier*4}" ry="${20+tier*3}" fill="url(#g${resId})"/><circle cx="60" cy="42" r="${18+tier*4}" fill="url(#g${resId})"/><path d="M60 14 L54 28 L66 28 Z" fill="${c2}"/><path d="M32 58 Q20 42 36 36" stroke="${c2}" stroke-width="3" fill="none" stroke-linecap="round"/><path d="M88 58 Q100 42 84 36" stroke="${c2}" stroke-width="3" fill="none" stroke-linecap="round"/><circle cx="50" cy="40" r="2.5" fill="black"/><circle cx="70" cy="40" r="2.5" fill="black"/></g>`;
} else if(r.type==='Volt'){
shape=`<g filter="url(#glow${resId})"><path d="M60 12 L28 58 L52 58 L44 96 L90 46 L62 46 L76 12 Z" fill="url(#g${resId})" stroke="${c2}" stroke-width="1.5"/><circle cx="50" cy="38" r="3" fill="white"/><circle cx="70" cy="38" r="3" fill="white"/></g>`;
} else if(r.type==='Zephyr'){
shape=`<g filter="url(#glow${resId})"><ellipse cx="60" cy="60" rx="${22+tier*4}" ry="${16+tier*2}" fill="url(#g${resId})"/><path d="M16 56 Q36 44 60 56 Q84 68 104 56" stroke="${c2}" stroke-width="${2+tier}" fill="none" opacity="0.9" stroke-linecap="round"/><path d="M20 70 Q46 62 68 70 Q86 76 102 70" stroke="white" stroke-width="1.2" fill="none" opacity="0.5"/><circle cx="50" cy="56" r="2.8" fill="black"/><circle cx="70" cy="56" r="2.8" fill="black"/></g>`;
} else if(r.type==='Earth'){
shape=`<g filter="url(#glow${resId})"><path d="M30 80 L34 42 L54 28 L78 34 L90 72 L60 88 Z" fill="url(#g${resId})" stroke="${c1}" stroke-width="1.5"/><rect x="46" y="44" width="28" height="16" rx="4" fill="white" opacity="0.9"/><circle cx="53" cy="52" r="3" fill="black"/><circle cx="67" cy="52" r="3" fill="black"/></g>`;
} else {
shape=`<g filter="url(#glow${resId})"><circle cx="60" cy="60" r="${26+tier*4}" fill="url(#g${resId})"/><circle cx="60" cy="60" r="${8+tier*2}" fill="white" opacity="0.8"/></g>`;
}
return `<svg viewBox="0 0 120 120" width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">${bg}${shape}</svg>`;
}
// ── const: ZONES ──
const ZONES = [
{ id:'verdant', name:'Verdant Canopy', lv:[1,5], elems:['Flora','Zephyr'], colorFrom:'#14532d', colorTo:'#052e16', boss:{ name:'Sylph Guardian Liora', dialog:'The forest tests every Conductor. Harmonize or be overgrown.', team:[{id:'thornback',lv:6},{id:'slipstream',lv:6}] } },
{ id:'grotto', name:'Sunken Grotto', lv:[5,10], elems:['Hydro','Earth'], colorFrom:'#0c4a6e', colorTo:'#082f49', boss:{ name:'Tidecaller Morv', dialog:'Depths swallow the unprepared. Show me your current!', team:[{id:'geoxus',lv:10},{id:'obsidian',lv:11}] } },
{ id:'crags', name:'Obsidian Crags', lv:[10,15], elems:['Pyro','Volt'], colorFrom:'#7c2d12', colorTo:'#431407', boss:{ name:'Emberwright Kael', dialog:'Ashes, ashes... will your Resonants burn brighter than mine?', team:[{id:'pyrowolf',lv:13},{id:'boltsaber',lv:14}] } },
{ id:'apex', name:'Apex Pillar', lv:[15,22], elems:['Cosmos','Pyro','Hydro','Flora','Volt','Zephyr','Earth'], colorFrom:'#4c1d95', colorTo:'#1e1b4b', boss:{ name:'High Conductor Veyra', dialog:'You tuned the wilds. Now face the frequency that birthed them.', team:[{id:'ignisaur',lv:17},{id:'neptunian',lv:17},{id:'terrasunder',lv:18}] } }
];
// ── const: TRAINER_NAMES ──
const TRAINER_NAMES = ['Rook','Mira','Joss','Kett','Vale','Sorin','Lyra','Brann','Iris','Dax'];
// ── const: CACHE_ITEMS ──
const CACHE_ITEMS = ['prism_basic','prism_hyper','heal_50','boost_prism'];
// ── function: genZoneNodes ──
function genZoneNodes(zoneIdx){
const zone=ZONES[zoneIdx];
const count=12 + zoneIdx*2;
const nodes=[];
for(let i=0;i<count;i++){
const prog=i/(count-1);
const x=80 + prog*640 + (Math.random()*40-20);
const y=120 + Math.sin(prog*Math.PI*2 + zoneIdx)*90 + (Math.random()*80-40);
let type='wild';
const r=Math.random();
if(i===count-1) type='boss';
else if(r<0.12) type='trainer';
else if(r<0.22) type='cache';
else if(r<0.30) type='heal';
nodes.push({ id:zone.id+'_'+i, x:Math.round(x), y:Math.round(y), type, done:false, prog });
}
return nodes;
}
// ── function: calcStats ──
function calcStats(resId, level){
const base=findResonant(resId).base;
return {
maxHp: Math.floor((base.hp*2*level)/100 + level + 10),
atk: Math.floor((base.atk*2*level)/100 + 5),
def: Math.floor((base.def*2*level)/100 + 5),
spd: Math.floor((base.spd*2*level)/100 + 5)
};
}
// ── function: xpToNext ──
function xpToNext(level){ return Math.floor(100*Math.pow(level,1.35)); }
// ── let: gameState ──
let gameState = null;
// ── let: battleState ──
let battleState = null;
// ── let: currentZoneIdx ──
let currentZoneIdx = 0;
// ── function: createNewState ──
function createNewState(){
const starter = {
id: 'emberkit',
level:5, xp:0, curHp:null,
moves:['Tackle','Ember','FlareBite'].slice(0,2),
status:null
};
// second starter random
const pool=['droplet','seedling','sparky','gusty','pebble'];
const secondId = pool[Math.floor(Math.random()*pool.length)];
const second = { id: secondId, level:3, xp:0, curHp:null, moves:['Tackle', findResonant(secondId).moves[1].id], status:null };
const s1 = calcStats(starter.id, starter.level); starter.curHp=s1.maxHp;
const s2 = calcStats(second.id, second.level); second.curHp=s2.maxHp;
return {
shards: 250,
inventory: { prism_basic:8, prism_hyper:1, heal_50:2, heal_full:0, boost_prism:0, prismBoost:0 },
party: [starter, second],
box: [],
zones: ZONES.map((_,i)=>({ idx:i, nodes: genZoneNodes(i), unlocked: i===0 })),
curZone:0,
seen: new Set([starter.id, secondId]),
caught: new Set([starter.id, secondId]),
log: ['> Link established.', '> Aethelgard frequency synced.', `> Acquired ${findResonant(starter.id).name} Lv5`, `> Acquired ${findResonant(secondId).name} Lv3`],
playTime:0
};
}
// ── function: saveGame ──
function saveGame(){
if(!gameState) return;
const toSave = { ...gameState, seen: Array.from(gameState.seen), caught: Array.from(gameState.caught) };
localStorage.setItem('aetherium_save_v2', JSON.stringify(toSave));
}
// ── function: loadGame ──
function loadGame(){
const raw=localStorage.getItem('aetherium_save_v2');
if(!raw) return null;
try{
const p=JSON.parse(raw);
return { ...p, seen:new Set(p.seen||[]), caught:new Set(p.caught||[]) };
}catch{ return null; }
}
// ── function: toast ──
function toast(msg){
const el=document.getElementById('toast');
el.textContent=msg; el.style.opacity='1'; el.style.transform='translate(-50%, -6px)';
setTimeout(()=>{ el.style.opacity='0'; el.style.transform='translate(-50%, 0px)'; }, 2200);
}
// ── function: pushLog ──
function pushLog(t){
if(!gameState) return;
gameState.log.push('> '+t);
if(gameState.log.length>120) gameState.log.shift();
renderLog();
saveGame();
}
// ── function: renderLog ──
function renderLog(){
const el=document.getElementById('logBox');
if(!el||!gameState) return;
el.textContent=gameState.log.slice(-40).join('\n');
el.scrollTop=el.scrollHeight;
}
// ── function: getMonDataset ──
function getMonDataset(m){
const base=findResonant(m.id);
const st=calcStats(m.id, m.level);
const curMoves=m.moves.map(mid=>({ id:mid, ...MOVE_POOL[mid] })).filter(Boolean);
return { base, stats:st, curMoves, curHp:m.curHp, maxHp:st.maxHp };
}
// ── function: renderShard ──
function renderShard(){ document.getElementById('shardDisplay').textContent = `${gameState?.shards||0} ◇`; }
// ── function: getAvailableMovesForLevel ──
function getAvailableMovesForLevel(resId, level){
return findResonant(resId).moves.filter(m=>m.lvl<=level).sort((a,b)=>b.lvl-a.lvl).slice(0,4).map(m=>m.id);
}
// ── function: refreshPartyMoves ──
function refreshPartyMoves(mon){
const avail=getAvailableMovesForLevel(mon.id, mon.level);
// keep up to 4 most recent
mon.moves=avail;
}
// ── function: renderParty ──
function renderParty(){
const mini=document.getElementById('partyMini');
const det=document.getElementById('partyDetail');
if(!mini||!det||!gameState) return;
mini.innerHTML='';
det.innerHTML='';
gameState.party.forEach((mon,i)=>{
const d=getMonDataset(mon);
const pct=Math.round((d.curHp/d.maxHp)*100);
let col='bg-emerald-400'; if(pct<30) col='bg-red-500'; else if(pct<60) col='bg-yellow-400';
const isActive=i===0;
// mini bubbles
const b=document.createElement('div');
b.className=`w-9 h-9 rounded-full glass grid place-items-center text-[10px] font-bold ${isActive?'ring-2 ring-violet-400':''}`;
b.innerHTML=makeSVGResonant(mon.id,28).replace(/width="\d+" height="\d+"/,'width="28" height="28"');
b.title=`${d.base.name} Lv${mon.level}`;
mini.appendChild(b);
const row=document.createElement('div');
row.className=`glass rounded-xl px-3 py-2.5 flex items-center gap-3 ${isActive?'bg-white/[0.09]':''} cursor-pointer hover:bg-white/10`;
row.innerHTML=`<div class="w-12 h-12">${makeSVGResonant(mon.id,44)}</div>
<div class="flex-1"><div class="flex justify-between items-center"><div class="font-bold text-sm">${d.base.name}<span class="ml-2 text-[10px] opacity-50">Lv${mon.level}</span><span class="ml-1 px-1.5 py-0.5 rounded-full text-[8px] type-${d.base.type}">${d.base.type}</span></div><div class="text-[10px] font-mono opacity-60">${d.curHp}/${d.maxHp}</div></div>
<div class="mt-1.5 h-1.5 rounded-full bg-black/30 overflow-hidden"><div class="h-full ${col} transition-all" style="width:${pct}%"></div></div><div class="mt-1 flex gap-1">${d.curMoves.map(m=>`<span class="text-[8px] px-1.5 py-0.5 rounded-full glass">${m.id}</span>`).join('')}</div></div>`;
row.onclick=()=>{ if(i===0) return; const tmp=gameState.party[0]; gameState.party[0]=gameState.party[i]; gameState.party[i]=tmp; renderParty(); saveGame(); toast('Front Resonant swapped'); };
det.appendChild(row);
});
}
// ── function: renderInventory ──
function renderInventory(){
const el=document.getElementById('invQuick'); if(!el||!gameState) return;
el.innerHTML='';
Object.entries(gameState.inventory).forEach(([k,v])=>{
if(k==='prismBoost') return;
if(!v) return;
const item=SHOP_CATALOG.find(s=>s.id===k); if(!item) return;
const div=document.createElement('div');
div.className='glass px-2.5 py-1.5 rounded-full text-[11px] flex items-center gap-1.5';
div.innerHTML=`<span>${item.icon}</span> ${item.name.split(' ')[0]} x${v}`;
el.appendChild(div);
});
if(gameState.inventory.prismBoost>0){
const d=document.createElement('div'); d.className='px-2.5 py-1.5 rounded-full bg-violet-500/30 border border-violet-400/40 text-[11px]'; d.textContent=`⚡ Boost x${gameState.inventory.prismBoost}`;
el.appendChild(d);
}
}
// ── function: renderZonesTabs ──
function renderZonesTabs(){
const el=document.getElementById('zoneTabs'); el.innerHTML='';
ZONES.forEach((z,i)=>{
const unlocked=gameState?.zones[i]?.unlocked;
const btn=document.createElement('button');
btn.className=`px-4 py-2 rounded-full text-xs tracking-widest transition ${i===currentZoneIdx?'bg-white text-black font-bold':'glass hover:bg-white/10'} ${!unlocked?'opacity-30':''}`;
btn.textContent=`${i+1}. ${z.name}`;
btn.disabled=!unlocked;
btn.onclick=()=>{ currentZoneIdx=i; synth.uiSelect(); renderOverworld(); };
el.appendChild(btn);
});
}
// ── function: renderOverworld ──
function renderOverworld(){
if(!gameState) return;
renderZonesTabs(); renderParty(); renderInventory(); renderShard(); renderLog();
const zone=ZONES[currentZoneIdx];
const zoneState=gameState.zones[currentZoneIdx];
document.getElementById('zoneName').textContent=zone.name;
document.getElementById('zoneDesc').textContent=`Levels ${zone.lv[0]}-${zone.lv[1]} • ${zone.elems.join('/')}`;
document.getElementById('battleBg').style.background=`radial-gradient(ellipse at 50% 0%, ${zone.colorFrom} 0%, ${zone.colorTo} 70%)`;
const svg=document.getElementById('mapSvg');
svg.innerHTML='';
const defs=`<defs><linearGradient id="pathG" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="${zone.colorFrom}"/><stop offset="100%" stop-color="${zone.colorTo}"/></linearGradient></defs>`;
svg.insertAdjacentHTML('afterbegin',defs);
// path line
const sorted=[...zoneState.nodes].sort((a,b)=>a.prog-b.prog);
let d=`M ${sorted[0].x} ${sorted[0].y}`;
sorted.forEach((n,i)=>{ if(i>0) d+=` L ${n.x} ${n.y}`; });
const path=document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('d',d); path.setAttribute('fill','none'); path.setAttribute('stroke','rgba(255,255,255,0.15)'); path.setAttribute('stroke-width','2'); path.setAttribute('stroke-dasharray','8 10');
svg.appendChild(path);
zoneState.nodes.forEach(node=>{
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
g.setAttribute('transform',`translate(${node.x},${node.y})`); g.classList.add('node'); if(node.done) g.classList.add('node-done');
let inner='';
if(node.type==='wild') inner=`<circle r="18" fill="rgba(34,197,94,0.15)" stroke="rgba(34,197,94,0.5)"/><text text-anchor="middle" dy="5" font-size="14">🌿</text>`;
else if(node.type==='trainer') inner=`<circle r="20" fill="rgba(139,92,246,0.2)" stroke="rgba(139,92,246,0.7)"/><text text-anchor="middle" dy="5" font-size="14">◈</text>`;
else if(node.type==='cache') inner=`<circle r="16" fill="rgba(250,204,21,0.15)" stroke="rgba(250,204,21,0.6)"/><text text-anchor="middle" dy="4" font-size="12">◆</text>`;
else if(node.type==='heal') inner=`<circle r="18" fill="rgba(59,130,246,0.18)" stroke="rgba(59,130,246,0.6)"/><text text-anchor="middle" dy="5" font-size="14">+</text>`;
else if(node.type==='boss') inner=`<circle r="26" fill="rgba(239,68,68,0.25)" stroke="rgba(239,68,68,0.9)" stroke-width="2"/><text text-anchor="middle" dy="6" font-size="16">⬢</text>`;
g.innerHTML=inner;
const check=document.createElementNS('http://www.w3.org/2000/svg','circle'); check.setAttribute('r','4'); check.setAttribute('cx','14'); check.setAttribute('cy','-14'); check.setAttribute('fill', node.done ? '#22c55e':'rgba(255,255,255,0.25)');
g.appendChild(check);
g.addEventListener('click',()=>onNodeClick(node));
g.addEventListener('mouseenter',(e)=>{ const tt=document.getElementById('nodeTooltip'); tt.textContent=`${node.type.toUpperCase()} • ${node.done?'Cleared':'Active'}`; tt.style.left=(e.clientX-80)+'px'; tt.style.top=(e.clientY-60)+'px'; tt.classList.remove('hidden'); });
g.addEventListener('mouseleave',()=>{ document.getElementById('nodeTooltip').classList.add('hidden'); });
svg.appendChild(g);
});
}
// ── function: onNodeClick ──
function onNodeClick(node){
if(node.done && node.type!=='heal'){ toast('Node already harmonized'); return; }
synth.uiSelect();
if(node.type==='wild'){ startWildBattle(node); }
else if(node.type==='cache'){ openCache(node); }
else if(node.type==='heal'){ healSpring(node); }
else if(node.type==='trainer'){ startTrainerBattle(node); }
else if(node.type==='boss'){ startBossBattle(node); }
}
// ── function: openCache ──
function openCache(node){
const reward=CACHE_ITEMS[Math.floor(Math.random()*CACHE_ITEMS.length)];
let amt=1; if(reward==='prism_basic') amt=2+Math.floor(Math.random()*3);
gameState.inventory[reward]=(gameState.inventory[reward]||0)+amt;
const extraShards=20+Math.floor(Math.random()*60);
gameState.shards+=extraShards;
node.done=true;
const itemName=SHOP_CATALOG.find(s=>s.id===reward)?.name||reward;
pushLog(`Cache: +${amt} ${itemName} +${extraShards}◇`);
toast(`+${amt} ${itemName} | +${extraShards}◇`);
renderOverworld(); saveGame(); synth.heal();
}
// ── function: healSpring ──
function healSpring(node){
if(gameState.shards<15 && !node.done){
const cost=0;
}
let fullCost=gameState.zones[currentZoneIdx].nodes.filter(n=>n.type==='heal'&&n.done).length ? 0 : 0;
gameState.party.forEach(m=>{ const st=calcStats(m.id,m.level); m.curHp=st.maxHp; m.status=null; });
node.done=false; // reusable
pushLog('Healing Spring: party restored');
toast('Party restored — spring resonance');
renderOverworld(); saveGame(); synth.heal();
}
// ── function: randomWildResonant ──
function randomWildResonant(zoneIdx, boss=false){
const zone=ZONES[zoneIdx];
let pool=RESONANTS_DB.filter(r=>zone.elems.includes(r.type) || Math.random()<0.18);
if(boss) pool=RESONANTS_DB.filter(r=>r.tier>=2);
else pool=pool.filter(r=>r.tier<=2 || Math.random()<0.2);
const pick=pool[Math.floor(Math.random()*pool.length)];
const [min,max]=zone.lv;
const lvl = boss ? max+1 : min + Math.floor(Math.random()*(max-min+2));
return { id:pick.id, level: Math.min(22, Math.max(1,lvl)) };
}
// ── function: startWildBattle ──
function startWildBattle(node){
const wildSpec = randomWildResonant(currentZoneIdx, false);
const wStats=calcStats(wildSpec.id, wildSpec.level);
const wildMon = { id:wildSpec.id, level:wildSpec.level, curHp:wStats.maxHp, maxHp:wStats.maxHp, moves:getAvailableMovesForLevel(wildSpec.id, wildSpec.level), status:null, stats:wStats };
battleState = { mode:'wild', node, playerTeam: gameState.party.map(m=>({...m})), playerIdx:0, wild: wildMon, turn:'player', logLines:[], isOver:false, prismBoost: gameState.inventory.prismBoost||0 };
// sync player cur
battleState.playerTeam.forEach((m,i)=>{ const ds=getMonDataset(gameState.party[i]); m.curHp=ds.curHp; m.maxHp=ds.maxHp; m.moves=ds.curMoves; m.base=findResonant(m.id); });
beginBattleUI(`${findResonant(wildSpec.id).name} emerged from brush!`);
}
// ── function: startTrainerBattle ──
function startTrainerBattle(node){
const name=TRAINER_NAMES[Math.floor(Math.random()*TRAINER_NAMES.length)];
const count=1+Math.floor(Math.random()*2);
const team=[];
for(let i=0;i<count;i++){ const spec=randomWildResonant(currentZoneIdx,false); const st=calcStats(spec.id,spec.level+1); team.push({ id:spec.id, level:spec.level+1, curHp:st.maxHp, maxHp:st.maxHp, moves:getAvailableMovesForLevel(spec.id, spec.level+1), status:null, stats:st, base:findResonant(spec.id) }); }
battleState={ mode:'trainer', name, node, playerTeam: gameState.party.map(m=>({...m})), playerIdx:0, enemyTeam:team, enemyIdx:0, turn:'player', logLines:[], isOver:false };
battleState.playerTeam.forEach((m,i)=>{ const ds=getMonDataset(gameState.party[i]); m.curHp=ds.curHp; m.maxHp=ds.maxHp; m.moves=ds.curMoves; m.base=findResonant(m.id); });
battleState.enemyTeam.forEach(e=> e.base=findResonant(e.id));
beginBattleUI(`Wandering Conductor ${name} challenges you!`);
}
// ── function: startBossBattle ──
function startBossBattle(node){
const zone=ZONES[currentZoneIdx];
const boss=zone.boss;
const team=boss.team.map(t=>{ const st=calcStats(t.id,t.lv); return { id:t.id, level:t.lv, curHp:st.maxHp, maxHp:st.maxHp, moves:getAvailableMovesForLevel(t.id,t.lv), status:null, stats:st, base:findResonant(t.id) }; });
battleState={ mode:'boss', bossDef:boss, node, playerTeam: gameState.party.map(m=>({...m})), playerIdx:0, enemyTeam:team, enemyIdx:0, turn:'player', logLines:[], isOver:false };
battleState.playerTeam.forEach((m,i)=>{ const ds=getMonDataset(gameState.party[i]); m.curHp=ds.curHp; m.maxHp=ds.maxHp; m.moves=ds.curMoves; m.base=findResonant(m.id); });
beginBattleUI(`${boss.name}: "${boss.dialog}"`);
}
// ── function: beginBattleUI ──
function beginBattleUI(firstLine){
document.getElementById('screen-overworld').classList.add('hidden');
document.getElementById('screen-battle').classList.remove('hidden');
document.getElementById('battleLog').textContent='';
logBattle(firstLine);
renderBattle();
}
// ── function: getActivePlayer ──
function getActivePlayer(){ return battleState?.playerTeam[battleState.playerIdx]; }
// ── function: getActiveEnemy ──
function getActiveEnemy(){
if(battleState.mode==='wild') return battleState.wild;
return battleState.enemyTeam[battleState.enemyIdx];
}
// ── function: logBattle ──
function logBattle(t){
battleState.logLines.push(t);
const el=document.getElementById('battleLog');
el.textContent=battleState.logLines.slice(-8).join('\n');
el.scrollTop=el.scrollHeight;
}
// ── function: renderBattle ──
function renderBattle(){
if(!battleState) return;
const p=getActivePlayer(); const e=getActiveEnemy(); if(!p||!e) return;
const pBase=p.base||findResonant(p.id); const eBase=e.base||findResonant(e.id);
document.getElementById('playerName').textContent=pBase.name; document.getElementById('playerLv').textContent=p.level; document.getElementById('playerType').textContent=pBase.type; document.getElementById('playerType').className=`ml-2 px-2 py-0.5 rounded-full text-[9px] type-${pBase.type}`;
document.getElementById('enemyName').textContent=eBase.name; document.getElementById('enemyLv').textContent=e.level; document.getElementById('enemyType').textContent=eBase.type; document.getElementById('enemyType').className=`ml-2 px-2 py-0.5 rounded-full text-[9px] type-${eBase.type}`;
const pp=Math.max(0, Math.round((p.curHp/p.maxHp)*100));
const ep=Math.max(0, Math.round((e.curHp/e.maxHp)*100));
const phpBar=document.getElementById('playerHp'); phpBar.style.width=pp+'%'; phpBar.className='h-full transition-all duration-500 '+(pp<30?'bg-red-500':pp<60?'bg-yellow-400':'bg-emerald-400');
const ehpBar=document.getElementById('enemyHp'); ehpBar.style.width=ep+'%'; ehpBar.className='h-full transition-all duration-500 '+(ep<30?'bg-red-500':ep<60?'bg-yellow-400':'bg-white');
document.getElementById('playerHpTxt').textContent=`${Math.max(0,Math.floor(p.curHp))}/${p.maxHp}`; document.getElementById('enemyHpTxt').textContent=`${Math.max(0,Math.floor(e.curHp))}/${e.maxHp}`;
document.getElementById('playerStatus').textContent=p.status||''; document.getElementById('enemyStatus').textContent=e.status||'';
const xpPct = p.xp!==undefined? Math.min(100, Math.round((p.xp/xpToNext(p.level))*100)) : 0;
document.getElementById('playerXp').style.width=`${xpPct}%`;
document.getElementById('playerSprite').innerHTML=makeSVGResonant(p.id, 280);
document.getElementById('enemySprite').innerHTML=makeSVGResonant(e.id, 260);
document.getElementById('battleBg').style.background=`radial-gradient(ellipse at 50% 0%, ${TYPE_COLORS[eBase.type][0]}33 0%, transparent 70%), radial-gradient(ellipse at 20% 100%, ${TYPE_COLORS[pBase.type][0]}33 0%, transparent 70%)`;
const act=document.getElementById('battleActions'); act.innerHTML='';
if(battleState.turn==='player' && !battleState.isOver){
p.moves.forEach((mv,i)=>{
const mp=MOVE_POOL[mv.id||mv]||mv;
const btn=document.createElement('button');
btn.className=`glass rounded-xl px-3 py-3 text-left hover:bg-white/10 transition border-l-4`;
btn.style.borderLeftColor=TYPE_COLORS[mp.type]?.[0]||'white';
btn.innerHTML=`<div class="flex justify-between"><span class="font-bold text-xs tracking-wide">${mp.id||mv.id}</span><span class="text-[9px] px-1.5 py-0.5 rounded-full type-${mp.type}">${mp.type}</span></div><div class="text-[10px] opacity-60 mt-1">POW ${mp.power} • ACC ${mp.acc}</div>`;
btn.onclick=()=>playerUseMove(i);
act.appendChild(btn);
});
const prismBtn=document.createElement('button'); prismBtn.className='glass rounded-xl px-3 py-3 text-left hover:bg-white/10 col-span-2 md:col-span-1'; prismBtn.innerHTML=`<div class="font-bold text-xs">⬢ PRISM • Capture</div><div class="text-[10px] opacity-60">Basic x${gameState.inventory.prism_basic||0} • Hyper x${gameState.inventory.prism_hyper||0}</div>`; prismBtn.onclick=()=>openPrismChoice();
act.appendChild(prismBtn);
const exchBtn=document.createElement('button'); exchBtn.className='glass rounded-xl px-3 py-3 text-left hover:bg-white/10'; exchBtn.innerHTML=`<div class="font-bold text-xs">⇄ EXCHANGE</div><div class="text-[10px] opacity-60">Swap Resonant</div>`; exchBtn.onclick=()=>openExchange();
act.appendChild(exchBtn);
const invBtn=document.createElement('button'); invBtn.className='glass rounded-xl px-3 py-3 text-left hover:bg-white/10'; invBtn.innerHTML=`<div class="font-bold text-xs">◍ INVENTORY</div><div class="text-[10px] opacity-60">Elixirs • Cleanse</div>`; invBtn.onclick=()=>openBattleInv();
act.appendChild(invBtn);
} else if(battleState.isOver){
const btn=document.createElement('button'); btn.className='glass rounded-xl px-4 py-4 text-center col-span-2 hover:bg-white/10'; btn.textContent='⏎ RETURN TO MAP'; btn.onclick=()=>endBattleReturn();
act.appendChild(btn);
} else {
const d=document.createElement('div'); d.className='col-span-2 text-xs opacity-60 py-6 text-center'; d.textContent='Enemy harmonic calculating...'; act.appendChild(d);
}
}
// ── function: calcDamage ──
function calcDamage(attacker, defender, move){
const atkStat=attacker.stats?.atk || calcStats(attacker.id, attacker.level).atk;
const defStat=defender.stats?.def || calcStats(defender.id, defender.level).def;
const eff=getEffectiveness(move.type, (defender.base||findResonant(defender.id)).type);
const rnd=0.85+Math.random()*0.15;
const crit=Math.random()<0.10 ? 1.5 : 1;
const base=(((2*attacker.level/5+2)*move.power*(atkStat/defStat))/50+2);
const dmg=Math.floor(base*eff*rnd*crit);
return { dmg, eff, crit:crit>1 };
}
// ── function: playerUseMove ──
function playerUseMove(idx){
if(!battleState || battleState.turn!=='player' || battleState.isOver) return;
const p=getActivePlayer(); const e=getActiveEnemy();
const moveId = p.moves[idx].id || p.moves[idx];
const move = MOVE_POOL[moveId];
if(!move) return;
synth.uiSelect();
battleState.turn='busy';
renderBattle();
setTimeout(()=>{
// accuracy
if(Math.random()*100>move.acc){ logBattle(`${p.base.name} used ${move.id}! But it missed.`); synth.hitPhys(); setTimeout(enemyTurn, 700); return; }
const res=calcDamage(p,e,move);
e.curHp=Math.max(0, e.curHp-res.dmg);
let effTxt=''; if(res.eff>1.2) effTxt=' Super effective!'; else if(res.eff<0.9) effTxt=' Not very effective...';
let critTxt=res.crit?' Critical hit!':'';
logBattle(`${p.base.name} used ${move.id}!${critTxt}${effTxt} -${res.dmg}`);
const spr=document.getElementById('enemySprite'); spr.classList.add('flash'); setTimeout(()=>spr.classList.remove('flash'), 240);
if(res.eff>1.1) synth.hitElem(true); else synth.hitElem(false);
if(res.crit){ document.getElementById('battleCard').classList.add('shake'); setTimeout(()=>document.getElementById('battleCard').classList.remove('shake'),400); }
renderBattle();
if(e.curHp<=0){ handleEnemyFaint(); } else { setTimeout(enemyTurn, 800); }
}, 300);
}
// ── function: enemyTurn ──
function enemyTurn(){
if(!battleState||battleState.isOver) return;
const p=getActivePlayer(); const e=getActiveEnemy(); if(!p||!e){ return; }
const move = e.moves[Math.floor(Math.random()*e.moves.length)];
const moveObj = MOVE_POOL[move.id||move]||MOVE_POOL[move];
if(!moveObj){ battleState.turn='player'; renderBattle(); return; }
setTimeout(()=>{
if(Math.random()*100>moveObj.acc){ logBattle(`${e.base.name} used ${moveObj.id}! It missed!`); battleState.turn='player'; renderBattle(); return; }
const res=calcDamage(e,p,moveObj);
p.curHp=Math.max(0, p.curHp-res.dmg);
let txt=''; if(res.eff>1.2) txt=' Super effective!'; else if(res.eff<0.9) txt=' Not very effective...';
if(res.crit) txt=' Critical! '+txt;
logBattle(`${e.base.name} used ${moveObj.id}!${txt} -${res.dmg}`);
const sp=document.getElementById('playerSprite'); sp.classList.add('flash'); setTimeout(()=>sp.classList.remove('flash'),230);
if(res.eff>1.1){ synth.hitElem(true); document.getElementById('battleCard').classList.add('shake'); setTimeout(()=>document.getElementById('battleCard').classList.remove('shake'),380); } else synth.hitPhys();
// sync to gameState party hp (live)
const globalP=gameState.party[battleState.playerIdx]; if(globalP) globalP.curHp=p.curHp;
renderBattle();
if(p.curHp<=0){ handlePlayerFaint(); } else { battleState.turn='player'; renderBattle(); }
}, 500);
}
// ── function: handleEnemyFaint ──
function handleEnemyFaint(){
const e=getActiveEnemy();
logBattle(`${e.base.name} frequency collapsed!`);
const xpGain = Math.floor(e.level*22 + (battleState.mode==='boss'?80:0));
const shardGain = Math.floor(e.level*4 + Math.random()*10 + (battleState.mode==='boss'?120:0));
gameState.shards+=shardGain; renderShard(); pushLog(`+${shardGain}◇ shards from ${e.base.name}`);
// give xp to active
const p=getActivePlayer();
p.xp=(p.xp||0)+xpGain;
logBattle(`${p.base.name} +${xpGain} XP`);
// check level ups
let leveled=false;
while(p.xp>=xpToNext(p.level)){
p.xp-=xpToNext(p.level); p.level++; leveled=true;
const ns=calcStats(p.id,p.level); p.maxHp=ns.maxHp; p.stats=ns; p.curHp=Math.min(p.maxHp, p.curHp+Math.floor(ns.maxHp*0.25));
refreshPartyMoves(p);
logBattle(`${p.base.name} reached Lv${p.level}!`);
synth.levelUp();
// evolution
const curBase=findResonant(p.id);
if(curBase.evoTo && p.level>=curBase.evoLvl){
const old=curBase.name; const nw=findResonant(curBase.evoTo);
p.id=nw.id; p.base=nw; pushLog(`${old} evolved into ${nw.name}!`);
logBattle(`✦ ${old} evolved into ${nw.name}!`);
toast(`Evolution! ${nw.name}`);
gameState.seen.add(nw.id); gameState.caught.add(nw.id);
}
}
// sync back to global
Object.assign(gameState.party[battleState.playerIdx], { curHp:p.curHp, level:p.level, xp:p.xp, id:p.id, moves:p.moves.map(m=>m.id||m), maxHp:p.maxHp });
if(battleState.mode==='wild'){
battleState.isOver=true;
battleState.node.done=true;
logBattle(`Wild area harmonized. +${shardGain}◇`);
pushLog(`Defeated wild ${e.base.name} Lv${e.level}`);
synth.uiSelect();
setTimeout(()=>{ renderBattle(); },300);
} else if(battleState.mode==='trainer' || battleState.mode==='boss'){
battleState.enemyIdx++;
if(battleState.enemyIdx>=battleState.enemyTeam.length){
// win whole battle
battleState.isOver=true;
battleState.node.done=true;
if(battleState.mode==='boss'){
const nextIdx=currentZoneIdx+1;
if(ZONES[nextIdx]){ gameState.zones[nextIdx].unlocked=true; pushLog(`Zone unlocked: ${ZONES[nextIdx].name}`); toast(`Zone Unlocked: ${ZONES[nextIdx].name}`); }
if(currentZoneIdx===ZONES.length-1){ pushLog('AETHERIUM MASTERED! You are Apex Conductor!'); toast('🏆 AETHERIUM COMPLETE'); }
} else {
pushLog(`Conductor ${battleState.name} defeated`);
}
logBattle(`Victory! +${shardGain}◇`);
setTimeout(()=>renderBattle(),300);
} else {
logBattle(`${battleState.mode==='boss'?battleState.bossDef.name:'Foe'} sends ${battleState.enemyTeam[battleState.enemyIdx].base.name}!`);
renderBattle();
setTimeout(()=>{ battleState.turn='player'; renderBattle(); },900);
}
}
saveGame();
if(leveled) renderParty();
}
// ── function: handlePlayerFaint ──
function handlePlayerFaint(){
const p=getActivePlayer();
logBattle(`${p.base.name} lost harmonic!`);
const alive=battleState.playerTeam.findIndex((m,i)=>i!==battleState.playerIdx && m.curHp>0);
if(alive===-1){
// check if any other mon alive
const anyAlive=gameState.party.some((_,i)=>i!==battleState.playerIdx && battleState.playerTeam[i]?.curHp>0);
if(!anyAlive){
logBattle(`All Resonants collapsed. Returning to camp...`);
battleState.isOver=true;
gameState.party.forEach(m=>{ m.curHp=Math.max(1, Math.floor(calcStats(m.id,m.level).maxHp*0.25)); });
saveGame();
setTimeout(()=>{ renderBattle(); },600);
return;
}
}
// auto prompt exchange
logBattle(`Choose next Resonant!`);
battleState.turn='player-exchange';
openExchange(true);
}
// ── function: openExchange ──
function openExchange(forced=false){
const act=document.getElementById('battleActions'); act.innerHTML='';
const title=document.createElement('div'); title.className='col-span-2 text-xs opacity-70 mb-1'; title.textContent=forced?'Your active collapsed - choose replacement:':'Exchange active Resonant:'; act.appendChild(title);
battleState.playerTeam.forEach((m,i)=>{
if(i===battleState.playerIdx && !forced) return;
if(m.curHp<=0) return;
const btn=document.createElement('button');
btn.className=`glass rounded-xl px-3 py-2 flex items-center gap-2 hover:bg-white/10 text-left ${i===battleState.playerIdx?'hidden':''}`;
btn.innerHTML=`<div class="w-8 h-8">${makeSVGResonant(m.id,32)}</div><div><div class="font-bold text-xs">${(m.base||findResonant(m.id)).name} Lv${m.level}</div><div class="text-[10px] opacity-60">${Math.floor(m.curHp)}/${m.maxHp} HP</div></div>`;
btn.onclick=()=>{
if(forced){ gameState.party.forEach((gm,gi)=>{ if(gi===i) gm.curHp=m.curHp; }); battleState.playerIdx=i; logBattle(`Go! ${m.base.name}!`); battleState.turn='player'; renderBattle(); }
else { battleState.playerIdx=i; logBattle(`Exchange! ${m.base.name} enters!`); // enemy gets free hit
renderBattle(); setTimeout(()=>enemyTurn(),700);
}
};
act.appendChild(btn);
});
if(!forced){
const cancel=document.createElement('button'); cancel.className='glass rounded-xl px-3 py-2 text-xs col-span-2'; cancel.textContent='Cancel'; cancel.onclick=()=>renderBattle();
act.appendChild(cancel);
}
}
// ── function: openPrismChoice ──
function openPrismChoice(){
const act=document.getElementById('battleActions'); act.innerHTML='';
[['prism_basic','Basic Prism (1.0x)'],['prism_hyper','Hyper Prism (1.8x)']].forEach(([id,label])=>{
const cnt=gameState.inventory[id]||0;
const btn=document.createElement('button'); btn.className='glass rounded-xl px-4 py-3 text-left hover:bg-white/10';
btn.innerHTML=`<div class="font-bold text-xs">${label}</div><div class="text-[10px] opacity-60">Owned: ${cnt}</div>`;
btn.disabled=cnt<=0 || battleState.mode!=='wild';
if(battleState.mode!=='wild') btn.innerHTML+=`<div class="text-[9px] text-red-400 mt-1">Wild only</div>`;
btn.onclick=()=>attemptCapture(id);
act.appendChild(btn);
});
const cancel=document.createElement('button'); cancel.className='glass rounded-xl px-3 py-3 text-xs'; cancel.textContent='Back'; cancel.onclick=()=>renderBattle(); act.appendChild(cancel);
}
// ── function: attemptCapture ──
function attemptCapture(prismId){
if(battleState.mode!=='wild'){ toast('Can only capture wild Resonants'); return; }
if((gameState.inventory[prismId]||0)<=0){ toast('No prisms'); return; }
gameState.inventory[prismId]--; saveGame();
const enemy=getActiveEnemy();
const pBase=enemy.base; const hpPct=enemy.curHp/enemy.maxHp;
const baseRate=0.35;
const hpMod=1 - hpPct*0.85;
let mod = prismId==='prism_hyper'?1.8:1.0;
mod += (gameState.inventory.prismBoost||0)*0.25;
let chance = (baseRate + hpMod*0.65) * mod;
chance = Math.min(0.92, chance);
// show modal
const modal=document.getElementById('modalCapture'); modal.classList.remove('hidden'); modal.classList.add('grid');
document.getElementById('capAnim').className='w-[120px] h-[120px] mx-auto mb-4 rounded-full grid place-items-center text-5xl cap-spin glass';
document.getElementById('capText').textContent=`Launching ${prismId.includes('hyper')?'Hyper':''} Prism...`;
document.getElementById('capSub').textContent=`${pBase.name} • HP ${Math.round(hpPct*100)}% • Chance ${Math.round(chance*100)}%`;
const success=Math.random()<chance;
setTimeout(()=>{
modal.classList.add('hidden'); modal.classList.remove('grid');
if(success){
synth.captureOk();
// add to party/box
const st=calcStats(enemy.id, enemy.level); const newMon={ id:enemy.id, level:enemy.level, xp:0, curHp:st.maxHp, moves:getAvailableMovesForLevel(enemy.id, enemy.level), status:null };
gameState.seen.add(enemy.id); gameState.caught.add(enemy.id);
if(gameState.party.length<6){ gameState.party.push(newMon); } else { gameState.box.push(newMon); }
battleState.node.done=true; battleState.isOver=true;
logBattle(`Captured ${pBase.name}! Added to ${gameState.party.length<=6?'party':'box'}.`);
pushLog(`Captured ${pBase.name} Lv${enemy.level}`);
toast(`Captured ${pBase.name}!`);
// consume boost
if(gameState.inventory.prismBoost>0){ gameState.inventory.prismBoost=Math.max(0, gameState.inventory.prismBoost-1); }
saveGame(); renderBattle(); renderParty();
} else {
synth.captureFail();
logBattle(`${pBase.name} broke free!`);
toast(`Capture failed • ${Math.round(chance*100)}%`);
battleState.turn='busy';
renderBattle();
setTimeout(()=>enemyTurn(),800);
}
}, 1400);
}
// ── function: openBattleInv ──
function openBattleInv(){
const act=document.getElementById('battleActions'); act.innerHTML='';
const items=[{id:'heal_50',name:'Core Restorer 50%'},{id:'heal_full',name:'Elixir Full Heal'}];
items.forEach(it=>{
const cnt=gameState.inventory[it.id]||0;
const btn=document.createElement('button'); btn.className='glass rounded-xl px-3 py-3 text-left hover:bg-white/10'; btn.innerHTML=`<div class="text-xs font-bold">${it.name}</div><div class="text-[10px] opacity-60">x${cnt}</div>`; btn.disabled=cnt<=0;
btn.onclick=()=>{
if(cnt<=0) return;
gameState.inventory[it.id]--; const p=getActivePlayer();
if(it.id==='heal_50'){ p.curHp=Math.min(p.maxHp, p.curHp+p.maxHp*0.5); gameState.party[battleState.playerIdx].curHp=p.curHp; logBattle(`${p.base.name} restored 50% HP!`); }
else { p.curHp=p.maxHp; p.status=null; gameState.party[battleState.playerIdx].curHp=p.maxHp; logBattle(`${p.base.name} fully restored!`); }
saveGame(); synth.heal(); battleState.turn='busy'; renderBattle(); setTimeout(()=>enemyTurn(),700);
};
act.appendChild(btn);
});
const b=document.createElement('button'); b.className='col-span-2 glass rounded-xl py-2 text-xs'; b.textContent='Back'; b.onclick=()=>renderBattle(); act.appendChild(b);
}
// ── function: endBattleReturn ──
function endBattleReturn(){
document.getElementById('screen-battle').classList.add('hidden');
document.getElementById('screen-overworld').classList.remove('hidden');
battleState=null;
renderOverworld();
}
// ── function: renderDex ──
function renderDex(){
const grid=document.getElementById('dexGrid'); grid.innerHTML='';
RESONANTS_DB.forEach(r=>{
const seen=gameState?.seen?.has(r.id);
const caught=gameState?.caught?.has(r.id);
const card=document.createElement('div');
card.className=`glass rounded-[22px] p-4 ${caught?'':'opacity-60'} hover:bg-white/10 transition`;
card.innerHTML=`<div class="flex gap-4"><div class="w-20 h-20 rounded-xl bg-black/20 grid place-items-center">${seen?makeSVGResonant(r.id,64):'<span class="text-2xl opacity-20">?</span>'}</div><div class="flex-1"><div class="flex items-center gap-2"><div class="font-black">${seen?r.name:'???'}</div><span class="px-2 py-0.5 rounded-full text-[9px] type-${r.type}">${r.type}</span><span class="text-[9px] opacity-50">T${r.tier}</span></div><div class="text-[11px] opacity-60 mt-1">${seen?r.desc:'Resonant not yet synced.'}</div><div class="mt-2 flex gap-1 flex-wrap">${r.base?Object.entries(r.base).map(([k,v])=>`<span class="text-[9px] px-1.5 py-0.5 rounded-full bg-white/10">${k.toUpperCase()} ${v}</span>`).join(''):''}</div><div class="mt-2 text-[10px] font-mono opacity-50">${r.moves.map(m=>`${m.id} Lv${m.lvl}`).join(' • ')}</div></div></div>`;
grid.appendChild(card);
});
}
// ── function: renderShop ──
function renderShop(){
const grid=document.getElementById('shopGrid'); grid.innerHTML='';
SHOP_CATALOG.forEach(it=>{
const own=gameState.inventory[it.id]||0;
const div=document.createElement('div');
div.className='glass rounded-2xl p-4 flex justify-between items-center hover:bg-white/10 transition';
div.innerHTML=`<div><div class="font-bold text-sm flex gap-2 items-center"><span class="w-7 h-7 rounded-full bg-white/10 grid place-items-center">${it.icon}</span>${it.name}</div><div class="text-[11px] opacity-60 mt-1">${it.desc}</div><div class="text-[10px] opacity-40 mt-1">Owned: ${own}</div></div><button class="px-4 py-2 rounded-full bg-white text-black text-xs font-bold" data-buy="${it.id}">${it.price}◇ Buy</button>`;
grid.appendChild(div);
});
grid.querySelectorAll('[data-buy]').forEach(b=>{
b.onclick=()=>{
const id=b.getAttribute('data-buy'); const it=SHOP_CATALOG.find(s=>s.id===id);
if(gameState.shards<it.price){ toast('Insufficient shards'); synth.captureFail(); return; }
gameState.shards-=it.price;
if(id==='boost_prism'){ gameState.inventory.prismBoost=(gameState.inventory.prismBoost||0)+1; }
else { gameState.inventory[id]=(gameState.inventory[id]||0)+1; }
pushLog(`Bought ${it.name}`); toast(`+1 ${it.name}`); renderShop(); renderShard(); renderInventory(); saveGame(); synth.uiSelect();
};
});
}
// ── function: showScreen ──
function showScreen(name){
['menu','overworld','battle','dex','shop'].forEach(s=>{ const el=document.getElementById('screen-'+s); if(el) el.classList.add('hidden'); });
const target=document.getElementById('screen-'+name); if(target) target.classList.remove('hidden');
if(name==='overworld') renderOverworld();
if(name==='dex') renderDex();
if(name==='shop'){ renderShop(); renderShard(); }
}
// ── main ──
(() => {
const $ = (s)=>document.querySelector(s);
// hover sounds
document.addEventListener('mouseover', e=>{ if(e.target.closest('button')) synth.uiHover(); });
function initEvents(){
$('#btnNew').onclick=()=>{
if(confirm('Initialize new link? This creates a new Conductor profile.')){ gameState=createNewState(); currentZoneIdx=0; saveGame(); showScreen('overworld'); synth.uiSelect(); }
};
$('#btnContinue').onclick=()=>{ const loaded=loadGame(); if(!loaded){ toast('No saved signal found'); return; } gameState=loaded; currentZoneIdx=gameState.curZone||0; showScreen('overworld'); synth.uiSelect(); };
$('#btnDex').onclick=()=>{ if(!gameState){ toast('Start a link first'); return; } showScreen('dex'); synth.uiSelect(); };
$('#btnShop').onclick=()=>{ if(!gameState){ toast('Start a link first'); return; } showScreen('shop'); synth.uiSelect(); };
$('#btnSave').onclick=()=>{ if(!gameState){ toast('Nothing to save'); return; } saveGame(); toast('Prism synced to local storage'); synth.uiSelect(); };
$('#btnReset').onclick=()=>{ if(confirm('Wipe all progress? Cannot be undone.')){ localStorage.removeItem('aetherium_save_v2'); gameState=null; showScreen('menu'); toast('Prism wiped'); } };
$('#btnHealCamp').onclick=()=>{
if(!gameState) return;
if(gameState.shards<50){ toast('Need 50◇ for camp heal'); return; }
gameState.shards-=50; gameState.party.forEach(m=>{ const st=calcStats(m.id,m.level); m.curHp=st.maxHp; m.status=null; }); pushLog('Camp healed party -50◇'); toast('Camp heal complete'); renderOverworld(); saveGame(); synth.heal();
};
$('#btnFlee').onclick=()=>{
if(!battleState){ toast('Not in battle'); return; }
if(battleState.mode!=='wild'){ toast('Cannot flee from Conductor battles'); synth.captureFail(); return; }
const p=getActivePlayer(); const e=getActiveEnemy();
const pSpd=p.stats?.spd||calcStats(p.id,p.level).spd; const eSpd=e.stats?.spd||calcStats(e.id,e.level).spd;
if(pSpd>=eSpd || Math.random()<0.7){ toast('Disengaged safely'); synth.uiSelect(); endBattleReturn(); pushLog('Fled wild encounter'); saveGame(); }
else { toast('Cannot evade!'); setTimeout(()=>enemyTurn(),600); }
};
document.querySelectorAll('.btnClose').forEach(b=> b.onclick=()=>{ showScreen('overworld'); synth.uiSelect(); });
}
initEvents();
// auto load if save exists
const auto=loadGame();
if(auto){ $('#btnContinue').classList.remove('opacity-50'); } else { $('#btnContinue').classList.add('opacity-50'); }
showScreen('menu');
// periodic autosave playtime tick
setInterval(()=>{ if(gameState){ gameState.playTime=(gameState.playTime||0)+1; gameState.curZone=currentZoneIdx; saveGame(); } }, 5000);
// keyboard shortcuts for battle: 1-4 moves
document.addEventListener('keydown', e=>{
if(!battleState||battleState.turn!=='player') return;
if(['1','2','3','4'].includes(e.key)){ const idx=parseInt(e.key)-1; if(battleState.playerTeam[battleState.playerIdx]?.moves[idx]) playerUseMove(idx); }
});
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.1","provider":"openrouter","persona":"composer","sessionId":"55e21fb3-2d00-4f8f-a1e5-89bfead1539d","tokensIn":426455,"tokensOut":28967,"tokensTotal":455422,"turns":18,"toolCalls":17,"failedToolCalls":1,"timestamp":"2026-07-17T04:12:52.019Z"} -->