Aethelgard
inkling · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 57,470 bytes · SHA-256 cc7df757e069
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aether Prisms — Resonants of Aethelgard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
:root {
--bg-deep: #0b0c15;
--glass: rgba(255,255,255,0.06);
--glass-border: rgba(255,255,255,0.08);
--text-main: #e8e6f3;
--text-dim: #a09fb8;
--text-muted: #6b6880;
}
* { box-sizing: border-box; }
html, body { background: var(--bg-deep); color: var(--text-main); font-family: 'Inter', sans-serif; overflow-x: hidden; height: 100vh; }
.font-retro { font-family: 'Press Start 2P', cursive; }
.glass-card { background: linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.01) 100%); backdrop-filter: blur(14px) saturate(140%); border: 1px solid var(--glass-border); border-radius: 1.25rem; box-shadow: 0 8px 32px 0 rgba(0,0,0,0.4), inset 0 0 0 1px rgba(255,255,255,0.03); }
.glass-card:hover { border-color: rgba(255,255,255,0.15); box-shadow: 0 12px 40px 0 rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.06) inset; }
.btn-glass { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.75rem; padding: 0.6rem 1.2rem; color: var(--text-main); transition: all .2s ease; backdrop-filter: blur(8px); }
.btn-glass:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.25); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(255,255,255,0.05); }
.glow-pyro { box-shadow: 0 0 20px rgba(255,100,50,0.35), 0 0 60px rgba(255,100,50,0.15); border-color: rgba(255,120,70,0.4) !important; }
.glow-hydro { box-shadow: 0 0 20px rgba(60,150,255,0.35), 0 0 60px rgba(60,150,255,0.15); border-color: rgba(80,170,255,0.4) !important; }
.glow-flora { box-shadow: 0 0 20px rgba(50,220,100,0.35), 0 0 60px rgba(50,220,100,0.15); border-color: rgba(70,240,120,0.4) !important; }
.glow-zephyr { box-shadow: 0 0 20px rgba(130,220,200,0.35), 0 0 60px rgba(130,220,200,0.15); border-color: rgba(150,240,220,0.4) !important; }
.glow-earth { box-shadow: 0 0 20px rgba(180,160,80,0.35), 0 0 60px rgba(180,160,80,0.15); border-color: rgba(200,180,100,0.4) !important; }
.glow-volt { box-shadow: 0 0 20px rgba(60,240,255,0.35), 0 0 60px rgba(60,240,255,0.15); border-color: rgba(80,255,255,0.4) !important; }
.glow-cosmos { box-shadow: 0 0 20px rgba(200,160,255,0.35), 0 0 60px rgba(200,160,255,0.15); border-color: rgba(220,180,255,0.4) !important; }
@keyframes fadeSlideUp { from { opacity:0; transform: translateY(16px); } to { opacity:1; transform: translateY(0); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
@keyframes shake { 0%, 100% { transform: translateX(0); } 10% { transform: translateX(-6px); } 30% { transform: translateX(6px); } 50% { transform: translateX(-4px); } 70% { transform: translateX(4px); } 90% { transform: translateX(-2px); } }
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
.anim-fade-slide { animation: fadeSlideUp 0.5s ease-out forwards; }
.anim-shake { animation: shake 0.35s ease-in-out; }
.anim-float { animation: float 4s ease-in-out infinite; }
.hp-green { background: linear-gradient(90deg, #34d399, #10b981); }
.hp-yellow { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
.hp-red { background: linear-gradient(90deg, #f87171, #ef4444); }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 4px; }
</style>
</head>
<body class="min-h-screen flex flex-col">
<!-- Audio Context -->
<script>
window.addEventListener('click', () => {
if (window.AudioCtx && window.AudioCtx.state === 'suspended') window.AudioCtx.resume();
}, { once: true });
</script>
<div id="app" class="relative w-full h-screen overflow-hidden flex flex-col">
<!-- Background ambient gradients -->
<div class="absolute inset-0 pointer-events-none z-0">
<div class="absolute top-[-10%] left-[-10%] w-[60vw] h-[60vw] rounded-full bg-violet-900/40 blur-[100px] opacity-60"></div>
<div class="absolute bottom-[-10%] right-[-10%] w-[60vw] h-[60vw] rounded-full bg-indigo-900/40 blur-[100px] opacity-60"></div>
</div>
<!-- Screen: MAIN MENU -->
<section id="screen-menu" class="absolute inset-0 z-10 flex flex-col items-center justify-center gap-8 anim-fade-slide">
<div class="text-center space-y-2">
<h1 class="font-retro text-4xl md:text-6xl tracking-tight text-transparent bg-clip-text bg-gradient-to-br from-violet-200 via-cyan-200 to-rose-200 drop-shadow-[0_0_25px_rgba(200,180,255,0.4)]">AETHER PRISMS</h1>
<p class="text-[10px] md:text-xs uppercase tracking-[0.3em] text-violet-300/80">Resonants of Aethelgard</p>
</div>
<div class="glass-card p-8 md:p-10 max-w-md w-[92%] flex flex-col gap-4">
<button onclick="startAdventure()" class="btn-glass font-retro text-sm md:text-base py-4 w-full hover:scale-[1.02]">New Conductor</button>
<button onclick="loadGame()" id="btn-continue" class="btn-glass font-retro text-sm md:text-base py-4 w-full opacity-60" disabled>Continue Journey</button>
<div class="flex gap-3">
<button onclick="openDex()" class="btn-glass flex-1 text-xs uppercase tracking-wide">Dex</button>
<button onclick="resetProgress()" class="btn-glass flex-1 text-xs uppercase tracking-wide text-rose-300 border-rose-500/20 hover:border-rose-400/40">Reset</button>
</div>
</div>
<div class="text-xs text-violet-300/40 max-w-md text-center px-6">A turn-based Resonant collection RPG. Capture spirits, master elements, and become the ultimate Conductor.</div>
</section>
<!-- Screen: OVERWORLD -->
<section id="screen-overworld" class="hidden absolute inset-0 z-10 flex flex-col">
<header class="flex items-center justify-between px-6 py-4 glass-card rounded-none border-x-0 border-t-0 bg-black/20 backdrop-blur-md">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-indigo-600 shadow-lg shadow-violet-900/40 flex items-center justify-center text-xs font-retro">C</div>
<div>
<h2 class="font-retro text-sm leading-tight">Conductor</h2>
<span id="overworld-shards" class="text-[10px] text-amber-300">0 Aether Shards</span>
</div>
</div>
<div class="text-xs text-violet-200/70">Zone <span id="current-zone-display">1</span></div>
</header>
<main class="flex-1 overflow-y-auto p-6 md:p-8">
<div class="max-w-3xl mx-auto space-y-6">
<div class="glass-card p-6">
<h2 id="zone-title" class="font-retro text-lg mb-1 text-violet-200">The Verdant Canopy</h2>
<p id="zone-desc" class="text-xs text-violet-100/60 leading-relaxed">Ancient trees humming with Flora and Zephyr resonance. Begin your training here.</p>
</div>
<div id="node-grid" class="grid grid-cols-2 md:grid-cols-4 gap-4"></div>
</div>
</main>
<footer class="p-4 flex justify-between items-center glass-card rounded-none border-x-0 border-b-0 bg-black/20">
<button onclick="openShop()" class="btn-glass text-xs">Camp Shop</button>
<button onclick="openMenu()" class="btn-glass text-xs">Menu</button>
</footer>
</section>
<!-- Screen: BATTLE -->
<section id="screen-battle" class="hidden absolute inset-0 z-20 flex flex-col bg-gradient-to-b from-[#0f101a] via-[#12122a] to-[#0f101a]">
<!-- Arena top -->
<div class="flex-1 relative overflow-hidden">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(255,255,255,0.03),transparent_70%)]"></div>
<!-- Enemy -->
<div class="absolute top-12 right-8 md:right-16 flex flex-col items-end gap-2">
<div id="enemy-card" class="glass-card p-3 min-w-[140px] text-right glow-pyro">
<div class="font-retro text-xs text-violet-200 mb-1" id="enemy-name">Emberkit</div>
<div class="text-[10px] text-violet-100/50 mb-2">Lv <span id="enemy-lv">3</span> · <span id="enemy-type" class="uppercase tracking-wider">Pyro</span></div>
<div class="w-full h-2 rounded-full bg-black/30 overflow-hidden mb-1">
<div id="enemy-hp-bar" class="h-full w-full hp-green rounded-full transition-all duration-300"></div>
</div>
<div class="text-[9px] text-violet-200/40">HP <span id="enemy-hp-text">30/30</span></div>
</div>
<div id="enemy-sprite" class="w-24 h-24 md:w-32 md:h-32 drop-shadow-[0_0_20px_rgba(255,100,50,0.3)]"></div>
</div>
<!-- Player -->
<div class="absolute bottom-16 left-6 md:left-16 flex flex-col items-start gap-2">
<div id="player-card" class="glass-card p-3 min-w-[140px] glow-hydro">
<div class="font-retro text-xs text-violet-200 mb-1" id="player-name">Droplet</div>
<div class="text-[10px] text-violet-100/50 mb-2">Lv <span id="player-lv">3</span> · <span id="player-type" class="uppercase tracking-wider">Hydro</span></div>
<div class="w-full h-2 rounded-full bg-black/30 overflow-hidden mb-1">
<div id="player-hp-bar" class="h-full w-full hp-green rounded-full transition-all duration-300"></div>
</div>
<div class="text-[9px] text-violet-200/40">HP <span id="player-hp-text">30/30</span></div>
</div>
<div id="player-sprite" class="w-24 h-24 md:w-32 md:h-32 drop-shadow-[0_0_20px_rgba(60,150,255,0.3)]"></div>
</div>
</div>
<!-- Log & Actions -->
<div class="z-10 p-4 md:p-6 space-y-4">
<div id="battle-log" class="glass-card p-4 min-h-[80px] text-sm leading-relaxed text-violet-100/90 shadow-inner">
<span class="text-amber-300 font-retro text-xs">Battle Start!</span><br>
<span id="log-text">A wild Emberkit appears!</span>
</div>
<div id="battle-actions" class="grid grid-cols-2 md:grid-cols-4 gap-3">
<button onclick="battleAction('attack')" class="btn-glass text-xs md:text-sm">Attack</button>
<button onclick="battleAction('prism')" class="btn-glass text-xs md:text-sm">Prism</button>
<button onclick="battleAction('swap')" class="btn-glass text-xs md:text-sm">Exchange</button>
<button onclick="battleAction('item')" class="btn-glass text-xs md:text-sm">Item</button>
</div>
<div id="move-panel" class="hidden grid grid-cols-2 gap-2"></div>
<div id="prism-panel" class="hidden flex gap-2 flex-wrap"></div>
<div id="item-panel" class="hidden grid grid-cols-2 gap-2"></div>
</div>
</section>
<!-- Screen: DEX -->
<section id="screen-dex" class="hidden absolute inset-0 z-30 bg-gradient-to-b from-[#0f101a] via-[#12122a] to-[#0f101a] overflow-y-auto">
<header class="sticky top-0 z-10 glass-card rounded-none border-x-0 border-t-0 bg-black/30 backdrop-blur-lg px-6 py-4 flex items-center justify-between">
<h2 class="font-retro text-xl text-violet-200">Conductor's Dex</h2>
<button onclick="closeScreen('dex')" class="btn-glass text-xs">Close</button>
</header>
<main class="p-6 md:p-10 max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6" id="dex-grid"></main>
</section>
<!-- Screen: SHOP -->
<section id="screen-shop" class="hidden absolute inset-0 z-30 bg-gradient-to-b from-[#0f101a] via-[#12122a] to-[#0f101a] overflow-y-auto">
<header class="sticky top-0 z-10 glass-card rounded-none border-x-0 border-t-0 bg-black/30 backdrop-blur-lg px-6 py-4 flex items-center justify-between">
<h2 class="font-retro text-xl text-violet-200">Camp Shop</h2>
<button onclick="closeScreen('shop')" class="btn-glass text-xs">Leave</button>
</header>
<main class="p-6 md:p-10 max-w-4xl mx-auto space-y-6" id="shop-grid"></main>
</section>
<!-- Screen: MENU OVERLAY -->
<section id="screen-menu-overlay" class="hidden absolute inset-0 z-40 bg-black/60 backdrop-blur-sm flex items-center justify-center">
<div class="glass-card p-8 max-w-sm w-[90%] space-y-4 text-center">
<h3 class="font-retro text-xl text-violet-200 mb-4">Conductor Menu</h3>
<button onclick="saveGame(); showToast('Progress saved.')" class="btn-glass w-full">Save Progress</button>
<button onclick="closeScreen('menu-overlay')" class="btn-glass w-full">Resume</button>
<button onclick="openMenu(); resetProgress()" class="btn-glass w-full text-rose-300">Reset Journey</button>
</div>
</section>
<!-- Toast -->
<div id="toast" class="absolute bottom-6 left-1/2 -translate-x-1/2 z-50 px-4 py-2 rounded-full bg-violet-900/80 text-violet-200 text-xs backdrop-blur-md border border-violet-400/20 opacity-0 transition-opacity duration-300 pointer-events-none shadow-xl shadow-violet-900/40"></div>
</div>
<script>
// ============================================================
// AUDIO SYNTH
// ============================================================
const AudioCtx = (() => { try { return (window.AudioContext || window.webkitAudioContext) ? new (window.AudioContext || window.webkitAudioContext)() : null; } catch(e){ return null; } })();
class Synth {
constructor() { this.ctx = AudioCtx; }
play(type) {
if (!this.ctx) return;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain); gain.connect(this.ctx.destination);
const now = this.ctx.currentTime;
if (type === 'hover') { osc.type = 'sine'; osc.frequency.setValueAtTime(880, now); osc.frequency.exponentialRampToValueAtTime(1100, now + 0.05); gain.gain.setValueAtTime(0.05, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15); osc.start(now); osc.stop(now + 0.15); }
else if (type === 'select') { osc.type = 'triangle'; osc.frequency.setValueAtTime(660, now); osc.frequency.linearRampToValueAtTime(990, now + 0.05); gain.gain.setValueAtTime(0.08, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2); osc.start(now); osc.stop(now + 0.2); }
else if (type === 'hit-phys') { osc.type = 'square'; osc.frequency.setValueAtTime(200, now); osc.frequency.linearRampToValueAtTime(80, now + 0.1); gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.25); osc.start(now); osc.stop(now + 0.25); }
else if (type === 'hit-elem') { osc.type = 'sine'; osc.frequency.setValueAtTime(400, now); osc.frequency.setValueAtTime(600, now + 0.08); osc.frequency.setValueAtTime(800, now + 0.15); gain.gain.setValueAtTime(0.08, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3); osc.start(now); osc.stop(now + 0.3); }
else if (type === 'level') { osc.type = 'triangle'; [880, 1100, 1320, 1760].forEach((f, i) => { osc.frequency.setValueAtTime(f, now + i*0.08); }); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.5); osc.start(now); osc.stop(now + 0.5); }
else if (type === 'capture') { osc.type = 'sine'; [440, 660, 880, 1320].forEach((f, i) => { osc.frequency.setValueAtTime(f, now + i*0.06); }); gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5); osc.start(now); osc.stop(now + 0.5); }
} catch(e) {}
}
}
const synth = new Synth();
// ============================================================
// DATA: RESONANTS (18 full entries)
// ============================================================
const ELEMENTS = {
Pyro: { color: 'text-orange-300', glow: 'glow-pyro', gradient: 'from-orange-500 to-rose-500', bg: 'bg-gradient-to-br from-orange-900/40 to-rose-900/40' },
Hydro: { color: 'text-cyan-300', glow: 'glow-hydro', gradient: 'from-cyan-500 to-blue-500', bg: 'bg-gradient-to-br from-cyan-900/40 to-blue-900/40' },
Flora: { color: 'text-emerald-300', glow: 'glow-flora', gradient: 'from-emerald-400 to-lime-500', bg: 'bg-gradient-to-br from-emerald-900/40 to-lime-900/40' },
Zephyr: { color: 'text-teal-200', glow: 'glow-zephyr', gradient: 'from-teal-300 to-cyan-300', bg: 'bg-gradient-to-br from-teal-900/30 to-cyan-900/30' },
Earth: { color: 'text-amber-200', glow: 'glow-earth', gradient: 'from-amber-400 to-yellow-700', bg: 'bg-gradient-to-br from-amber-900/30 to-yellow-900/30' },
Volt: { color: 'text-sky-300', glow: 'glow-volt', gradient: 'from-sky-300 to-cyan-300', bg: 'bg-gradient-to-br from-sky-900/30 to-cyan-900/30' }
};
const MONSTERS = [
// Pyro family
{ id: 'emberkit', name: 'Emberkit', type: 'Pyro', base: { hp:35, atk:12, def:8, spd:14 }, sprite: { shape:'cat', color:'#fb7185', glow:'#f87171' }, moves: [
{ name: 'Scratch', power: 40, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Ember Spark', power: 45, acc: 100, el: 'Pyro', unlock: 3 },
{ name: 'Flame Bite', power: 65, acc: 90, el: 'Pyro', unlock: 6 },
{ name: 'Inferno Surge', power: 85, acc: 85, el: 'Pyro', unlock: 10 }
], evoLevel: 8, evoTo: 'pyrowolf' },
{ id: 'pyrowolf', name: 'Pyrowolf', type: 'Pyro', base: { hp:65, atk:28, def:14, spd:22 }, sprite: { shape:'wolf', color:'#f97316', glow:'#fb923c' }, moves: [
{ name: 'Claw', power: 45, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Flame Bite', power: 65, acc: 90, el: 'Pyro', unlock: 3 },
{ name: 'Scorching Roar', power: 75, acc: 95, el: 'Pyro', unlock: 7 },
{ name: 'Inferno Surge', power: 85, acc: 85, el: 'Pyro', unlock: 10 }
], evoLevel: 15, evoTo: 'ignisaur' },
{ id: 'ignisaur', name: 'Ignisaur', type: 'Pyro', base: { hp:110, atk:48, def:32, spd:28 }, sprite: { shape:'dragon', color:'#ea580c', glow:'#f97316' }, moves: [
{ name: 'Dragon Claw', power: 80, acc: 95, el: 'Physical', unlock: 1 },
{ name: 'Scorching Roar', power: 75, acc: 95, el: 'Pyro', unlock: 3 },
{ name: 'Solar Flare', power: 100, acc: 85, el: 'Pyro', unlock: 8 },
{ name: 'Inferno Surge', power: 85, acc: 85, el: 'Pyro', unlock: 10 }
], evoLevel: 0, evoTo: null },
// Hydro family
{ id: 'droplet', name: 'Droplet', type: 'Hydro', base: { hp:45, atk:10, def:14, spd:12 }, sprite: { shape:'blob', color:'#38bdf8', glow:'#7dd3fc' }, moves: [
{ name: 'Splash', power: 40, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Water Pulse', power: 50, acc: 100, el: 'Hydro', unlock: 3 },
{ name: 'Aqua Jet', power: 70, acc: 95, el: 'Hydro', unlock: 6 },
{ name: 'Tidal Wave', power: 90, acc: 85, el: 'Hydro', unlock: 10 }
], evoLevel: 8, evoTo: 'geoxus' },
{ id: 'geoxus', name: 'Geoxus', type: 'Hydro', base: { hp:80, atk:22, def:32, spd:18 }, sprite: { shape:'turtle', color:'#0ea5e9', glow:'#38bdf8' }, moves: [
{ name: 'Shell Slam', power: 55, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Aqua Jet', power: 70, acc: 95, el: 'Hydro', unlock: 3 },
{ name: 'Hydro Surge', power: 80, acc: 90, el: 'Hydro', unlock: 7 },
{ name: 'Tidal Wave', power: 90, acc: 85, el: 'Hydro', unlock: 10 }
], evoLevel: 15, evoTo: 'neptunian' },
{ id: 'neptunian', name: 'Neptunian', type: 'Hydro', base: { hp:125, atk:42, def:55, spd:24 }, sprite: { shape:'serpent', color:'#0284c7', glow:'#38bdf8' }, moves: [
{ name: 'Ocean Bite', power: 85, acc: 95, el: 'Physical', unlock: 1 },
{ name: 'Hydro Surge', power: 80, acc: 90, el: 'Hydro', unlock: 3 },
{ name: 'Abyssal Vortex', power: 105, acc: 80, el: 'Hydro', unlock: 8 },
{ name: 'Tidal Wave', power: 90, acc: 85, el: 'Hydro', unlock: 10 }
], evoLevel: 0, evoTo: null },
// Flora family
{ id: 'seedling', name: 'Seedling', type: 'Flora', base: { hp:40, atk:14, def:10, spd:10 }, sprite: { shape:'flower', color:'#4ade80', glow:'#86efac' }, moves: [
{ name: 'Tackle', power: 40, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Leaf Cut', power: 50, acc: 95, el: 'Flora', unlock: 3 },
{ name: 'Thorn Whip', power: 65, acc: 90, el: 'Flora', unlock: 6 },
{ name: 'Bloom Burst', power: 80, acc: 85, el: 'Flora', unlock: 10 }
], evoLevel: 8, evoTo: 'thornback' },
{ id: 'thornback', name: 'Thornback', type: 'Flora', base: { hp:75, atk:30, def:22, spd:16 }, sprite: { shape:'turtle', color:'#22c55e', glow:'#4ade80' }, moves: [
{ name: 'Thorn Slam', power: 55, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Thorn Whip', power: 65, acc: 90, el: 'Flora', unlock: 3 },
{ name: 'Root Crush', power: 75, acc: 85, el: 'Flora', unlock: 7 },
{ name: 'Bloom Burst', power: 80, acc: 85, el: 'Flora', unlock: 10 }
], evoLevel: 15, evoTo: 'bloomgoliath' },
{ id: 'bloomgoliath', name: 'Bloomgoliath', type: 'Flora', base: { hp:120, atk:46, def:42, spd:20 }, sprite: { shape:'golem', color:'#16a34a', glow:'#22c55e' }, moves: [
{ name: 'Earth Slam', power: 80, acc: 95, el: 'Physical', unlock: 1 },
{ name: 'Root Crush', power: 75, acc: 85, el: 'Flora', unlock: 3 },
{ name: 'Photosynthesis', power: 100, acc: 80, el: 'Flora', unlock: 8 },
{ name: 'Bloom Burst', power: 80, acc: 85, el: 'Flora', unlock: 10 }
], evoLevel: 0, evoTo: null },
// Zephyr family
{ id: 'gusty', name: 'Gusty', type: 'Zephyr', base: { hp:42, atk:15, def:9, spd:22 }, sprite: { shape:'bird', color:'#5eead4', glow:'#99f6e4' }, moves: [
{ name: 'Peck', power: 40, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Gust', power: 45, acc: 100, el: 'Zephyr', unlock: 3 },
{ name: 'Air Slash', power: 65, acc: 90, el: 'Zephyr', unlock: 6 },
{ name: 'Cyclone Strike', power: 85, acc: 85, el: 'Zephyr', unlock: 10 }
], evoLevel: 8, evoTo: 'slipstream' },
{ id: 'slipstream', name: 'Slipstream', type: 'Zephyr', base: { hp:70, atk:30, def:18, spd:32 }, sprite: { shape:'bird', color:'#2dd4bf', glow:'#5eead4' }, moves: [
{ name: 'Wing Attack', power: 55, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Air Slash', power: 65, acc: 90, el: 'Zephyr', unlock: 3 },
{ name: 'Aero Blast', power: 80, acc: 85, el: 'Zephyr', unlock: 7 },
{ name: 'Cyclone Strike', power: 85, acc: 85, el: 'Zephyr', unlock: 10 }
], evoLevel: 15, evoTo: 'zephyrus' },
{ id: 'zephyrus', name: 'Zephyrus', type: 'Zephyr', base: { hp:105, atk:48, def:28, spd:42 }, sprite: { shape:'dragon', color:'#14b8a6', glow:'#2dd4bf' }, moves: [
{ name: 'Sky Claw', power: 80, acc: 95, el: 'Physical', unlock: 1 },
{ name: 'Aero Blast', power: 80, acc: 85, el: 'Zephyr', unlock: 3 },
{ name: 'Tempest Surge', power: 105, acc: 80, el: 'Zephyr', unlock: 8 },
{ name: 'Cyclone Strike', power: 85, acc: 85, el: 'Zephyr', unlock: 10 }
], evoLevel: 0, evoTo: null },
// Earth family
{ id: 'pebble', name: 'Pebble', type: 'Earth', base: { hp:50, atk:12, def:18, spd:8 }, sprite: { shape:'rock', color:'#d4a843', glow:'#eab308' }, moves: [
{ name: 'Roll', power: 45, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Mud Slap', power: 50, acc: 95, el: 'Earth', unlock: 3 },
{ name: 'Rock Throw', power: 65, acc: 90, el: 'Earth', unlock: 6 },
{ name: 'Earthquake', power: 85, acc: 80, el: 'Earth', unlock: 10 }
], evoLevel: 8, evoTo: 'obsidian' },
{ id: 'obsidian', name: 'Obsidian', type: 'Earth', base: { hp:95, atk:26, def:42, spd:14 }, sprite: { shape:'golem', color:'#a16207', glow:'#ca8a04' }, moves: [
{ name: 'Rock Slam', power: 60, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Rock Throw', power: 65, acc: 90, el: 'Earth', unlock: 3 },
{ name: 'Seismic Crush', power: 80, acc: 85, el: 'Earth', unlock: 7 },
{ name: 'Earthquake', power: 85, acc: 80, el: 'Earth', unlock: 10 }
], evoLevel: 15, evoTo: 'terrasunder' },
{ id: 'terrasunder', name: 'Terrasunder', type: 'Earth', base: { hp:135, atk:45, def:65, spd:20 }, sprite: { shape:'golem', color:'#854d0e', glow:'#a16207' }, moves: [
{ name: 'Mountain Slam', power: 85, acc: 95, el: 'Physical', unlock: 1 },
{ name: 'Seismic Crush', power: 80, acc: 85, el: 'Earth', unlock: 3 },
{ name: 'Tectonic Rift', power: 105, acc: 80, el: 'Earth', unlock: 8 },
{ name: 'Earthquake', power: 85, acc: 80, el: 'Earth', unlock: 10 }
], evoLevel: 0, evoTo: null },
// Volt family
{ id: 'sparky', name: 'Sparky', type: 'Volt', base: { hp:38, atk:14, def:10, spd:20 }, sprite: { shape:'cat', color:'#38bdf8', glow:'#0ea5e9' }, moves: [
{ name: 'Tackle', power: 40, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Thunder Spark', power: 50, acc: 100, el: 'Volt', unlock: 3 },
{ name: 'Volt Charge', power: 65, acc: 90, el: 'Volt', unlock: 6 },
{ name: 'Lightning Storm', power: 90, acc: 85, el: 'Volt', unlock: 10 }
], evoLevel: 8, evoTo: 'boltsaber' },
{ id: 'boltsaber', name: 'Boltsaber', type: 'Volt', base: { hp:72, atk:32, def:20, spd:28 }, sprite: { shape:'saber', color:'#0284c7', glow:'#38bdf8' }, moves: [
{ name: 'Saber Strike', power: 60, acc: 100, el: 'Physical', unlock: 1 },
{ name: 'Volt Charge', power: 65, acc: 90, el: 'Volt', unlock: 3 },
{ name: 'Electric Surge', power: 80, acc: 85, el: 'Volt', unlock: 7 },
{ name: 'Lightning Storm', power: 90, acc: 85, el: 'Volt', unlock: 10 }
], evoLevel: 15, evoTo: 'stormlord' },
{ id: 'stormlord', name: 'Stormlord', type: 'Volt', base: { hp:115, atk:48, def:32, spd:36 }, sprite: { shape:'dragon', color:'#0369a1', glow:'#0284c7' }, moves: [
{ name: 'Thunder Claw', power: 85, acc: 95, el: 'Physical', unlock: 1 },
{ name: 'Electric Surge', power: 80, acc: 85, el: 'Volt', unlock: 3 },
{ name: 'Thunderstorm', power: 110, acc: 80, el: 'Volt', unlock: 8 },
{ name: 'Lightning Storm', power: 90, acc: 85, el: 'Volt', unlock: 10 }
], evoLevel: 0, evoTo: null }
];
function getMonster(id) { return MONSTERS.find(m => m.id === id); }
function getMonsterSVG(id, size=120) {
const m = getMonster(id);
if (!m) return `<svg width="${size}" height="${size}" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="30" fill="#888"/></svg>`;
const c = m.sprite.color;
const gl = m.sprite.glow;
let path = '';
if (m.sprite.shape === 'cat') path = `<path d="M30 60 Q20 30 40 25 Q50 10 60 25 Q80 30 70 60 L65 80 L35 80 Z" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="45" cy="40" r="3" fill="#fff"/><circle cx="55" cy="40" r="3" fill="#fff"/><path d="M50 52 Q48 58 50 60" stroke="#fff" stroke-width="1.5" fill="none"/>`;
else if (m.sprite.shape === 'wolf') path = `<path d="M25 55 Q15 35 35 30 Q50 15 65 30 Q85 35 75 55 L70 80 L30 80 Z" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="42" cy="45" r="3" fill="#fff"/><circle cx="58" cy="45" r="3" fill="#fff"/><path d="M40 58 L60 58" stroke="#fff" stroke-width="2"/>`;
else if (m.sprite.shape === 'dragon') path = `<path d="M35 70 Q20 50 30 30 Q50 10 70 30 Q80 50 65 70 L60 82 L40 82 Z" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="42" cy="45" r="4" fill="#fff"/><circle cx="58" cy="45" r="4" fill="#fff"/><path d="M48 60 Q55 70 52 75" stroke="#fff" stroke-width="2" fill="none"/>`;
else if (m.sprite.shape === 'blob') path = `<ellipse cx="50" cy="60" rx="28" ry="22" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="42" cy="55" r="4" fill="#fff"/><circle cx="58" cy="55" r="4" fill="#fff"/><path d="M45 65 Q50 72 55 65" stroke="#fff" stroke-width="2" fill="none"/>`;
else if (m.sprite.shape === 'turtle') path = `<ellipse cx="50" cy="55" rx="30" ry="22" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="42" cy="50" r="3" fill="#fff"/><circle cx="58" cy="50" r="3" fill="#fff"/><path d="M45 62 Q50 68 55 62" stroke="#fff" stroke-width="2" fill="none"/>`;
else if (m.sprite.shape === 'serpent') path = `<path d="M30 50 Q40 30 50 30 Q60 30 70 50 T85 60 Q70 80 50 80 T30 60 Z" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="45" cy="45" r="3" fill="#fff"/><circle cx="55" cy="45" r="3" fill="#fff"/>`;
else if (m.sprite.shape === 'flower') path = `<circle cx="50" cy="65" r="20" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="50" cy="45" r="10" fill="#fff" opacity="0.3"/><path d="M50 25 L50 45" stroke="${gl}" stroke-width="3"/>`;
else if (m.sprite.shape === 'golem') path = `<rect x="25" y="40" width="50" height="40" rx="10" fill="${c}" stroke="${gl}" stroke-width="2"/><rect x="35" y="25" width="30" height="15" rx="5" fill="#ddd" opacity="0.2"/><circle cx="42" cy="50" r="3" fill="#fff"/><circle cx="58" cy="50" r="3" fill="#fff"/>`;
else if (m.sprite.shape === 'bird') path = `<path d="M20 50 Q30 20 50 25 Q70 20 80 50 L75 70 L25 70 Z" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="45" cy="45" r="3" fill="#fff"/><circle cx="55" cy="45" r="3" fill="#fff"/><path d="M50 58 L50 65" stroke="#fff" stroke-width="2"/>`;
else if (m.sprite.shape === 'rock') path = `<polygon points="25,70 50,30 75,70 65,85 35,85" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="45" cy="60" r="3" fill="#fff"/><circle cx="55" cy="60" r="3" fill="#fff"/>`;
else if (m.sprite.shape === 'saber') path = `<path d="M30 40 L70 60 L60 75 L40 55 Z" fill="${c}" stroke="${gl}" stroke-width="2"/><circle cx="42" cy="52" r="3" fill="#fff"/><circle cx="58" cy="58" r="3" fill="#fff"/>`;
else path = `<circle cx="50" cy="50" r="30" fill="${c}" stroke="${gl}" stroke-width="2"/>`;
return `<svg width="${size}" height="${size}" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" class="drop-shadow-[0_0_15px_${m.sprite.glow}]">${path}</svg>`;
}
// ============================================================
// GAME DATA: ZONES & NODES
// ============================================================
const ZONES = [
{ id: 'canopy', name: 'Verdant Canopy', desc: 'Ancient trees humming with Flora and Zephyr resonance. Begin your training here.', levels: [1,2,3,4,5], types: ['Flora','Zephyr','Flora','Pyro','Flora'], bossId: 'bloomgoliath' },
{ id: 'grotto', name: 'Sunken Grotto', desc: 'Deep caverns echoing with Hydro and Earth energy. Watch the currents.', levels: [5,6,7,8,9,10], types: ['Hydro','Earth','Hydro','Flora','Earth','Hydro'], bossId: 'terrasunder' },
{ id: 'crags', name: 'Obsidian Crags', desc: 'Volcanic peaks crackling with Pyro and Volt storms. Only the worthy pass.', levels: [10,11,12,13,14,15], types: ['Pyro','Volt','Pyro','Flora','Volt','Pyro'], bossId: 'stormlord' },
{ id: 'apex', name: 'Apex Pillar', desc: 'The final ascension. All elements converge at the summit.', levels: [15,16,17,18,19,20], types: ['Cosmos','Pyro','Hydro','Flora','Zephyr','Earth'], bossId: 'ignisaur' }
];
function getZone(index) { return ZONES[index] || ZONES[0]; }
// ============================================================
// PLAYER STATE
// ============================================================
let PLAYER = {
name: 'Conductor',
shards: 50,
zoneIndex: 0,
zoneProgress: { canopy: 0, grotto: 0, crags: 0, apex: 0 },
completedBosses: {},
team: [], // objects: { id, level, xp, currentHp, maxHp, movesUnlocked }
inventory: { prism: 5, hyperPrism: 2, coreRestorer: 3, elixir: 1 },
dexSeen: new Set(),
unlockedZones: ['canopy']
};
function initPlayer() {
PLAYER.team = [
{ id: 'droplet', level: 3, xp: 0, currentHp: 45, maxHp: 45, movesUnlocked: [1,3,6,10] },
{ id: 'gusty', level: 3, xp: 0, currentHp: 42, maxHp: 42, movesUnlocked: [1,3,6,10] },
];
}
function getCombatStats(monsterId, level) {
const base = getMonster(monsterId).base;
// Simple scaling
const scale = 1 + (level - 1) * 0.15;
return {
hp: Math.round(base.hp * scale),
atk: Math.round(base.atk * scale),
def: Math.round(base.def * scale),
spd: Math.round(base.spd * scale)
};
}
function getTeamStats(id) {
const p = PLAYER.team.find(t => t.id === id);
const m = getMonster(id);
if (!p) return getCombatStats(id, 1);
return getCombatStats(id, p.level);
}
function evolveIfReady(id) {
const p = PLAYER.team.find(t => t.id === id);
if (!p) return false;
const m = getMonster(id);
if (m.evoLevel > 0 && p.level >= m.evoLevel && m.evoTo) {
p.id = m.evoTo;
p.maxHp = getCombatStats(p.id, p.level).hp;
p.currentHp = p.maxHp;
synth.play('level');
showToast(`${m.name} evolved into ${getMonster(p.id).name}!`);
return true;
}
return false;
}
// ============================================================
// SAVE / LOAD
// ============================================================
function saveGame() {
const data = JSON.stringify({ team: PLAYER.team, shards: PLAYER.shards, zoneIndex: PLAYER.zoneIndex, zoneProgress: PLAYER.zoneProgress, completedBosses: PLAYER.completedBosses, unlockedZones: PLAYER.unlockedZones, inventory: PLAYER.inventory, dexSeen: Array.from(PLAYER.dexSeen) });
localStorage.setItem('aether_save', data);
showToast('Progress saved to Aether Core');
document.getElementById('btn-continue').disabled = false;
document.getElementById('btn-continue').classList.remove('opacity-60');
}
function loadGame() {
const raw = localStorage.getItem('aether_save');
if (!raw) { showToast('No saved data found.'); return; }
try {
const data = JSON.parse(raw);
PLAYER.shards = data.shards || 0;
PLAYER.zoneIndex = data.zoneIndex || 0;
PLAYER.zoneProgress = data.zoneProgress || { canopy: 0, grotto: 0, crags: 0, apex: 0 };
PLAYER.completedBosses = data.completedBosses || {};
PLAYER.unlockedZones = data.unlockedZones || ['canopy'];
PLAYER.inventory = data.inventory || { prism: 5, hyperPrism: 2, coreRestorer: 3, elixir: 1 };
PLAYER.dexSeen = new Set(data.dexSeen || []);
PLAYER.team = data.team || [];
startOverworld();
synth.play('select');
} catch (e) { showToast('Corrupted save.'); }
}
function resetProgress() {
if (!confirm('Reset all progress?')) return;
localStorage.removeItem('aether_save');
initPlayer();
PLAYER.shards = 50;
PLAYER.zoneIndex = 0;
PLAYER.zoneProgress = { canopy: 0, grotto: 0, crags: 0, apex: 0 };
PLAYER.completedBosses = {};
PLAYER.unlockedZones = ['canopy'];
PLAYER.inventory = { prism: 5, hyperPrism: 2, coreRestorer: 3, elixir: 1 };
PLAYER.dexSeen = new Set();
document.getElementById('btn-continue').disabled = true;
document.getElementById('btn-continue').classList.add('opacity-60');
showToast('Aether Prisms reset');
openMenu();
}
// ============================================================
// UI & RENDER
// ============================================================
function showScreen(id) {
document.querySelectorAll('#app > section').forEach(s => s.classList.add('hidden'));
const el = document.getElementById('screen-' + id);
if (el) { el.classList.remove('hidden'); el.classList.add('anim-fade-slide'); }
}
function openMenu() { showScreen('menu'); }
function openOverworld() { showScreen('overworld'); renderOverworld(); }
function openBattle() { showScreen('battle'); }
function openDex() { showScreen('dex'); renderDex(); }
function openShop() { showScreen('shop'); renderShop(); }
function closeScreen(suffix) { if (suffix === 'menu-overlay') { document.getElementById('screen-menu-overlay').classList.add('hidden'); return; } if (suffix === 'dex') { openOverworld(); } else if (suffix === 'shop') { openOverworld(); } }
function startAdventure() {
initPlayer();
PLAYER.unlockedZones = ['canopy'];
PLAYER.zoneIndex = 0;
saveGame();
openOverworld();
synth.play('select');
}
function startOverworld() {
openOverworld();
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.remove('opacity-0');
setTimeout(() => t.classList.add('opacity-0'), 1800);
}
// ============================================================
// OVERWORLD RENDER
// ============================================================
function renderOverworld() {
const zone = getZone(PLAYER.zoneIndex);
document.getElementById('zone-title').textContent = zone.name;
document.getElementById('zone-desc').textContent = zone.desc;
document.getElementById('current-zone-display').textContent = PLAYER.zoneIndex + 1;
document.getElementById('overworld-shards').textContent = PLAYER.shards + ' Aether Shards';
const grid = document.getElementById('node-grid');
grid.innerHTML = '';
zone.types.forEach((type, i) => {
const progress = PLAYER.zoneProgress[zone.id] || 0;
const unlocked = i <= progress || i === 0;
const btn = document.createElement('button');
btn.className = `glass-card p-4 flex flex-col gap-2 text-left transition-all hover:scale-[1.03] ${!unlocked ? 'opacity-40 pointer-events-none' : ''}`;
btn.innerHTML = `<div class="flex items-center justify-between"><span class="font-retro text-xs">Node ${i+1}</span><span class="text-[10px] text-violet-200/50">${type}</span></div>
<div class="text-xs text-violet-100/70">${i === zone.types.length - 1 ? 'Zone Gatekeeper' : (i === 0 ? 'Wild Encounter' : 'Encounter / Cache')}</div>`;
btn.onclick = () => visitNode(i, zone);
grid.appendChild(btn);
});
}
function visitNode(index, zone) {
if (!zone) return;
const progress = PLAYER.zoneProgress[zone.id] || 0;
if (index > progress + 1 && index !== progress) {
showToast('Path ahead blocked. Defeat current challenge.');
return;
}
// Update progress
if (index > PLAYER.zoneProgress[zone.id]) {
PLAYER.zoneProgress[zone.id] = index;
}
// Random encounter type based on node index
if (index === zone.types.length - 1) {
// Boss
startBossBattle(zone.bossId, zone);
} else if (index === 0) {
startBattle(zone.types[index], 3 + PLAYER.zoneIndex);
} else {
const roll = Math.random();
if (roll < 0.6) startBattle(zone.types[index], 2 + PLAYER.zoneIndex + Math.floor(index/2));
else if (roll < 0.85) findCache();
else startNpcBattle();
}
saveGame();
}
function findCache() {
const rewards = [
{ name: 'Aether Shard', amount: 10 },
{ name: 'Aether Shard', amount: 15 },
{ name: 'Core Restorer', amount: 1 },
{ name: 'Resonance Elixir', amount: 1 },
];
const r = rewards[Math.floor(Math.random() * rewards.length)];
if (r.name === 'Aether Shard') PLAYER.shards += r.amount;
else if (r.name === 'Core Restorer') PLAYER.inventory.coreRestorer += r.amount;
else if (r.name === 'Resonance Elixir') PLAYER.inventory.elixir += r.amount;
showToast(`Found cache: ${r.amount > 1 ? r.amount + 'x ' : ''}${r.name}`);
synth.play('select');
renderOverworld();
}
// ============================================================
// COMBAT ENGINE
// ============================================================
let BATTLE = { activePlayer: 0, enemyMonster: null, enemyLevel: 1, enemyCurrentHp: 0, enemyMaxHp: 0, turnPhase: 'start', logHistory: [] };
function startBattle(type, level) {
const families = MONSTERS.filter(m => m.type === type);
const seed = families[Math.floor(Math.random() * families.length)];
if (!seed) return;
const id = seed.id;
const stats = getCombatStats(id, level);
BATTLE = {
activePlayer: 0,
enemyMonster: id,
enemyLevel: level,
enemyCurrentHp: stats.hp,
enemyMaxHp: stats.hp,
enemyAtk: stats.atk,
enemyDef: stats.def,
enemySpd: stats.spd,
turnPhase: 'player'
};
document.getElementById('enemy-name').textContent = seed.name;
document.getElementById('enemy-lv').textContent = level;
document.getElementById('enemy-type').textContent = seed.type;
document.getElementById('enemy-hp-text').textContent = stats.hp + '/' + stats.hp;
document.getElementById('enemy-sprite').innerHTML = getMonsterSVG(id, 120);
document.getElementById('battle-log').innerHTML = `<span class="font-retro text-xs text-amber-300">Encounter!</span><br>A wild ${seed.name} appears! <span class="text-xs text-violet-200/50">(Lv ${level})</span>`;
openBattle();
renderPlayerCard();
renderEnemyHp();
synth.play('hover');
}
function startBossBattle(bossId, zone) {
const seed = getMonster(bossId);
if (!seed) return;
const level = zone.levels[zone.levels.length - 1] || 5;
const stats = getCombatStats(bossId, level);
BATTLE = {
activePlayer: 0,
enemyMonster: bossId,
enemyLevel: level,
enemyCurrentHp: stats.hp,
enemyMaxHp: stats.hp,
enemyAtk: stats.atk,
enemyDef: stats.def,
enemySpd: stats.spd,
turnPhase: 'player'
};
document.getElementById('enemy-name').textContent = seed.name;
document.getElementById('enemy-lv').textContent = level;
document.getElementById('enemy-type').textContent = seed.type;
document.getElementById('enemy-hp-text').textContent = stats.hp + '/' + stats.hp;
document.getElementById('enemy-sprite').innerHTML = getMonsterSVG(bossId, 140);
document.getElementById('battle-log').innerHTML = `<span class="font-retro text-xs text-rose-300">Boss Encounter!</span><br>The Gatekeeper <span class="text-violet-200">${seed.name}</span> challenges you!`;
openBattle();
renderPlayerCard();
renderEnemyHp();
synth.play('hover');
}
function startNpcBattle() {
const npc = { name: 'Wandering Conductor', team: ['pyrowolf', 'geoxus', 'slipstream'] };
// Simplified: just a regular wild battle with level scaled
startBattle('Flora', PLAYER.zoneIndex * 3 + 4);
}
function renderPlayerCard() {
const p = PLAYER.team[BATTLE.activePlayer];
if (!p) return;
const m = getMonster(p.id);
const stats = getTeamStats(p.id);
document.getElementById('player-name').textContent = m.name;
document.getElementById('player-lv').textContent = p.level;
document.getElementById('player-type').textContent = m.type;
document.getElementById('player-hp-bar').style.width = (p.currentHp / stats.hp * 100) + '%';
document.getElementById('player-hp-text').textContent = Math.round(p.currentHp) + '/' + stats.hp;
const pct = p.currentHp / stats.hp;
document.getElementById('player-hp-bar').className = 'h-full rounded-full transition-all duration-300 ' + (pct > 0.5 ? 'hp-green' : pct > 0.2 ? 'hp-yellow' : 'hp-red');
document.getElementById('player-sprite').innerHTML = getMonsterSVG(p.id, 120);
}
function renderEnemyHp() {
const stats = getCombatStats(BATTLE.enemyMonster, BATTLE.enemyLevel);
const pct = BATTLE.enemyCurrentHp / stats.hp;
document.getElementById('enemy-hp-bar').style.width = (pct * 100) + '%';
document.getElementById('enemy-hp-bar').className = 'h-full rounded-full transition-all duration-300 ' + (pct > 0.5 ? 'hp-green' : pct > 0.2 ? 'hp-yellow' : 'hp-red');
document.getElementById('enemy-hp-text').textContent = Math.round(BATTLE.enemyCurrentHp) + '/' + stats.hp;
}
function renderMoves() {
const p = PLAYER.team[BATTLE.activePlayer];
if (!p) return;
const panel = document.getElementById('move-panel');
panel.innerHTML = '';
panel.classList.remove('hidden');
document.getElementById('prism-panel').classList.add('hidden');
document.getElementById('item-panel').classList.add('hidden');
const available = (getMonster(p.id).moves || []).filter(mv => (p.movesUnlocked || []).includes(mv.unlock));
available.forEach(mv => {
const btn = document.createElement('button');
btn.className = 'btn-glass text-xs';
btn.textContent = mv.name + ' (' + mv.power + ')';
btn.onclick = () => executeMove(mv);
panel.appendChild(btn);
});
}
function renderPrisms() {
const panel = document.getElementById('prism-panel');
panel.innerHTML = '';
panel.classList.remove('hidden');
document.getElementById('move-panel').classList.add('hidden');
document.getElementById('item-panel').classList.add('hidden');
const tiers = [
{ name: 'Basic', rate: 0.35, count: PLAYER.inventory.prism },
{ name: 'Hyper', rate: 0.55, count: PLAYER.inventory.hyperPrism }
];
tiers.forEach(t => {
if (t.count <= 0) return;
const btn = document.createElement('button');
btn.className = 'btn-glass text-xs';
btn.textContent = `${t.name} Prism (${Math.round(t.rate*100)}%) — ${t.count}`;
btn.onclick = () => attemptCapture(t);
panel.appendChild(btn);
});
}
function renderSwap() {
// Show team members to swap in
const panel = document.getElementById('move-panel');
panel.innerHTML = '';
panel.classList.remove('hidden');
document.getElementById('prism-panel').classList.add('hidden');
document.getElementById('item-panel').classList.add('hidden');
PLAYER.team.forEach((member, i) => {
if (i === BATTLE.activePlayer) return;
const btn = document.createElement('button');
btn.className = 'btn-glass text-xs';
btn.textContent = getMonster(member.id).name + ' Lv' + member.level;
btn.onclick = () => swapActive(i);
panel.appendChild(btn);
});
}
function renderItems() {
const panel = document.getElementById('item-panel');
panel.innerHTML = '';
panel.classList.remove('hidden');
document.getElementById('move-panel').classList.add('hidden');
document.getElementById('prism-panel').classList.add('hidden');
const items = [
{ key: 'coreRestorer', label: 'Core Restorer (+50% HP)', count: PLAYER.inventory.coreRestorer },
{ key: 'elixir', label: 'Resonance Elixir', count: PLAYER.inventory.elixir }
];
items.forEach(it => {
if (it.count <= 0) return;
const btn = document.createElement('button');
btn.className = 'btn-glass text-xs';
btn.textContent = `${it.label} — ${it.count}`;
btn.onclick = () => useItem(it.key);
panel.appendChild(btn);
});
}
function battleAction(action) {
if (BATTLE.turnPhase !== 'player') return;
synth.play('select');
if (action === 'attack') renderMoves();
else if (action === 'prism') renderPrisms();
else if (action === 'swap') renderSwap();
else if (action === 'item') renderItems();
}
function executeMove(move) {
BATTLE.turnPhase = 'resolving';
const p = PLAYER.team[BATTLE.activePlayer];
const pStats = getTeamStats(p.id);
const eStats = { hp: BATTLE.enemyCurrentHp, atk: BATTLE.enemyAtk, def: BATTLE.enemyDef, spd: BATTLE.enemySpd };
let damage = calculateDamage(pStats.atk, eStats.def, move.power, p.id, BATTLE.enemyMonster);
let isCrit = Math.random() < 0.1;
if (isCrit) damage = Math.round(damage * 1.5);
damage = Math.max(1, Math.round(damage));
BATTLE.enemyCurrentHp -= damage;
BATTLE.enemyCurrentHp = Math.max(0, BATTLE.enemyCurrentHp);
let logMsg = `${getMonster(p.id).name} used ${move.name}!`;
if (move.el !== 'Physical') logMsg += ` <span class="text-xs text-violet-300">(${move.el})</span>`;
if (isCrit) {
logMsg += ` <span class="text-rose-300 font-retro text-xs">CRITICAL HIT!</span>`;
synth.play('hit-elem');
} else {
synth.play(move.el === 'Physical' ? 'hit-phys' : 'hit-elem');
}
logMsg += ` It dealt <span class="text-rose-300">${damage}</span> damage.`;
appendLog(logMsg);
renderEnemyHp();
document.getElementById('enemy-card').classList.add('anim-shake');
setTimeout(() => document.getElementById('enemy-card').classList.remove('anim-shake'), 350);
document.getElementById('battle-log').scrollTop = document.getElementById('battle-log').scrollHeight;
// Evolution check
evolveIfReady(p.id);
gainXP(8 + BATTLE.enemyLevel);
if (BATTLE.enemyCurrentHp <= 0) {
appendLog(`<span class="text-emerald-300 font-retro">${getMonster(BATTLE.enemyMonster).name} fainted!</span>`);
setTimeout(endBattle, 1000);
return;
}
setTimeout(enemyTurn, 900);
}
function calculateDamage(atk, def, power, attackerId, defenderId) {
const aMonster = getMonster(attackerId);
const dMonster = getMonster(defenderId);
let modifier = 1;
// Type effectiveness
const typeChart = {
Pyro: { strong: 'Flora', weak: 'Hydro' },
Hydro: { strong: 'Pyro', weak: 'Flora' },
Flora: { strong: 'Hydro', weak: 'Pyro' },
Zephyr: { strong: 'Earth', weak: 'Volt' },
Earth: { strong: 'Volt', weak: 'Zephyr' },
Volt: { strong: 'Zephyr', weak: 'Earth' }
};
// Simple: if attacker element is same as move element, apply to defender
// We'll approximate by using attacker type as proxy for move type in this simple version
// Actually let's just rely on move.el for a simpler approach in full version, but here we'll use attacker id's type for basic effectiveness.
const aType = aMonster.type;
const dType = dMonster.type;
const chart = typeChart[aType];
if (chart) {
if (dType === chart.strong) modifier = 1.6;
else if (dType === chart.weak) modifier = 0.7;
}
const rand = 0.85 + Math.random() * 0.15;
const dmg = (((2 * BATTLE.enemyLevel / 5 + 2) * power * (atk / def)) / 50 + 2) * modifier * rand;
return dmg;
}
function enemyTurn() {
const p = PLAYER.team[BATTLE.activePlayer];
const pStats = getTeamStats(p.id);
// Simple AI: random attack
const damage = Math.round((((2 * BATTLE.enemyLevel / 5 + 2) * 55 * (BATTLE.enemyAtk / pStats.def)) / 50 + 2) * (0.85 + Math.random() * 0.15));
const dmg = Math.max(1, Math.round(damage));
p.currentHp -= dmg;
p.currentHp = Math.max(0, p.currentHp);
appendLog(`<span class="text-violet-200">${getMonster(BATTLE.enemyMonster).name}</span> struck back! <span class="text-rose-300">${dmg}</span> damage.`);
synth.play('hit-phys');
renderPlayerCard();
document.getElementById('player-card').classList.add('anim-shake');
setTimeout(() => document.getElementById('player-card').classList.remove('anim-shake'), 350);
document.getElementById('battle-log').scrollTop = document.getElementById('battle-log').scrollHeight;
if (p.currentHp <= 0) {
appendLog(`<span class="text-violet-400">${getMonster(p.id).name} is unable to continue!</span>`);
setTimeout(() => {
// Heal party slightly and return to overworld (simplified defeat)
PLAYER.team.forEach(t => t.currentHp = Math.round(getTeamStats(t.id).hp * 0.3));
saveGame();
openOverworld();
}, 1000);
return;
}
BATTLE.turnPhase = 'player';
renderPlayerCard();
}
function appendLog(html) {
const log = document.getElementById('battle-log');
const div = document.createElement('div');
div.innerHTML = html;
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
function attemptCapture(tier) {
BATTLE.turnPhase = 'resolving';
const rate = tier.rate;
const hpRatio = BATTLE.enemyCurrentHp / BATTLE.enemyMaxHp;
const successRate = Math.min(0.95, rate + (1 - hpRatio) * 0.3);
tier.count--;
if (PLAYER.inventory.prism !== undefined && tier.name === 'Basic') PLAYER.inventory.prism--;
else if (PLAYER.inventory.hyperPrism !== undefined && tier.name === 'Hyper') PLAYER.inventory.hyperPrism--;
if (Math.random() < successRate) {
PLAYER.team.push({ id: BATTLE.enemyMonster, level: BATTLE.enemyLevel, xp: 0, currentHp: BATTLE.enemyMaxHp, maxHp: BATTLE.enemyMaxHp, movesUnlocked: [1,3,6,10] });
appendLog(`<span class="text-emerald-300 font-retro">Captured!</span> A new <span class="text-violet-200">${getMonster(BATTLE.enemyMonster).name}</span> joined the team!`);
synth.play('capture');
PLAYER.dexSeen.add(BATTLE.enemyMonster);
setTimeout(endBattle, 1200);
} else {
appendLog(`<span class="text-rose-300">Failed!</span> The ${getMonster(BATTLE.enemyMonster).name} broke free.`);
synth.play('hit-phys');
setTimeout(enemyTurn, 800);
}
saveGame();
}
function swapActive(index) {
BATTLE.activePlayer = index;
BATTLE.turnPhase = 'player';
appendLog(`Exchanged! <span class="text-violet-200">${getMonster(PLAYER.team[index].id).name}</span> entered.`);
renderPlayerCard();
renderMoves();
}
function useItem(key) {
BATTLE.turnPhase = 'resolving';
const p = PLAYER.team[BATTLE.activePlayer];
if (key === 'coreRestorer') {
const stats = getTeamStats(p.id);
p.currentHp = Math.min(p.maxHp || stats.hp, p.currentHp + Math.round(stats.hp * 0.5));
PLAYER.inventory.coreRestorer--;
appendLog(`Used Core Restorer! <span class="text-emerald-300">+50% HP</span>`);
synth.play('select');
} else if (key === 'elixir') {
p.currentHp = p.maxHp || getTeamStats(p.id).hp;
PLAYER.inventory.elixir--;
appendLog(`Used Resonance Elixir! <span class="text-violet-300">Status cleared.</span>`);
synth.play('select');
}
renderPlayerCard();
setTimeout(enemyTurn, 600);
saveGame();
}
function gainXP(amount) {
PLAYER.team.forEach(t => {
t.xp += amount;
// Simple level thresholds
const thresholds = [0, 20, 60, 120, 200, 300, 450, 600, 800, 1000, 1300, 1600, 2000, 2400, 2800];
let newLevel = t.level;
for (let i = 1; i < thresholds.length; i++) {
if (t.xp >= thresholds[i]) newLevel = i + 1;
}
if (newLevel > t.level) {
const oldMax = t.maxHp || getTeamStats(t.id).hp;
t.level = newLevel;
const newStats = getTeamStats(t.id);
t.maxHp = newStats.hp;
t.currentHp = Math.round(t.currentHp + (newStats.hp - oldMax));
appendLog(`<span class="text-amber-300 font-retro">Level Up!</span> ${getMonster(t.id).name} is now level ${t.level}!`);
synth.play('level');
}
});
saveGame();
}
function endBattle() {
if (BATTLE.enemyCurrentHp <= 0) {
PLAYER.shards += 15 + BATTLE.enemyLevel * 3;
// Check boss defeat
const zone = getZone(PLAYER.zoneIndex);
if (BATTLE.enemyMonster === zone.bossId) {
PLAYER.completedBosses[zone.id] = true;
const nextIndex = PLAYER.zoneIndex + 1;
if (nextIndex < ZONES.length && !PLAYER.unlockedZones.includes(ZONES[nextIndex].id)) {
PLAYER.unlockedZones.push(ZONES[nextIndex].id);
showToast('Zone unlocked: ' + ZONES[nextIndex].name);
}
}
showToast('Victory! +' + (15 + BATTLE.enemyLevel * 3) + ' shards');
}
saveGame();
openOverworld();
renderOverworld();
}
// ============================================================
// DEX RENDER
// ============================================================
function renderDex() {
const grid = document.getElementById('dex-grid');
grid.innerHTML = '';
MONSTERS.forEach(m => {
const seen = PLAYER.dexSeen.has(m.id);
const card = document.createElement('div');
card.className = 'glass-card p-5 flex gap-4 items-center ' + (seen ? '' : 'opacity-50 grayscale');
const el = ELEMENTS[m.type] || ELEMENTS.Hydro;
card.innerHTML = `<div class="w-16 h-16 shrink-0">${seen ? getMonsterSVG(m.id, 80) : '<svg width="80" height="80" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="20" width="60" height="60" rx="10" fill="#2a2a3a"/></svg>'}</div>
<div class="min-w-0">
<h3 class="font-retro text-sm ${el.color} mb-1">${seen ? m.name : '???'}</h3>
<div class="text-[10px] text-violet-200/50 mb-1">${seen ? m.type + ' · ' + m.id.toUpperCase() : 'Unknown Type'}</div>
<div class="text-[10px] text-violet-200/40">${seen ? 'HP ' + m.base.hp + ' · ATK ' + m.base.atk + ' · DEF ' + m.base.def : 'Data hidden'}</div>
</div>`;
grid.appendChild(card);
});
}
// ============================================================
// SHOP RENDER
// ============================================================
function renderShop() {
const grid = document.getElementById('shop-grid');
grid.innerHTML = `
<div class="glass-card p-6"><h3 class="font-retro text-sm mb-4 text-violet-200">Aether Shards: <span class="text-amber-300">${PLAYER.shards}</span></h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button onclick="buyItem('prism')" class="btn-glass text-left p-4 hover:scale-[1.01]">
<div class="font-retro text-xs text-violet-200">Harmonizer Prism</div>
<div class="text-[10px] text-violet-200/50">Basic capture rate. 25 shards.</div>
</button>
<button onclick="buyItem('hyperPrism')" class="btn-glass text-left p-4 hover:scale-[1.01]">
<div class="font-retro text-xs text-violet-200">Hyper-Conductive Prism</div>
<div class="text-[10px] text-violet-200/50">High capture rate. 70 shards.</div>
</button>
<button onclick="buyItem('coreRestorer')" class="btn-glass text-left p-4 hover:scale-[1.01]">
<div class="font-retro text-xs text-violet-200">Core Restorer</div>
<div class="text-[10px] text-violet-200/50">Restores 50% HP. 25 shards.</div>
</button>
<button onclick="buyItem('elixir')" class="btn-glass text-left p-4 hover:scale-[1.01]">
<div class="font-retro text-xs text-violet-200">Resonance Elixir</div>
<div class="text-[10px] text-violet-200/50">Clears status. 40 shards.</div>
</button>
</div></div>`;
}
function buyItem(key) {
const costs = { prism: 25, hyperPrism: 70, coreRestorer: 25, elixir: 40 };
if (PLAYER.shards < costs[key]) { showToast('Not enough Aether Shards'); return; }
PLAYER.shards -= costs[key];
PLAYER.inventory[key]++;
showToast('Purchase complete');
renderShop();
synth.play('capture');
saveGame();
}
// ============================================================
// EVENT BINDINGS & INIT
// ============================================================
function init() {
initPlayer();
const saved = localStorage.getItem('aether_save');
document.getElementById('btn-continue').disabled = !saved;
if (saved) document.getElementById('btn-continue').classList.remove('opacity-60');
// Add hover sounds to buttons globally
document.querySelectorAll('button').forEach(btn => {
btn.addEventListener('mouseenter', () => synth.play('hover'));
});
}
window.init = init;
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"06d215ba-acb9-4d40-93fc-4e7b8131707e","tokensIn":30444,"tokensOut":20547,"tokensTotal":50991,"turns":2,"toolCalls":1,"failedToolCalls":0,"timestamp":"2026-07-18T22:41:26.869Z"} -->