Aethelgard
Qwen 3.6 35b a3b · composer

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 102,044 bytes · SHA-256 e962932653da
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aethelgard: Resonant Chronicles</title>
<script src="https://cdn.tailwindcss.com/"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=Press+Start+2P&display=swap">
<style>
/* ── css: layout ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: 'Inter', sans-serif; background: #0a0a1a; }
#app { width: 100%; height: 100%; position: relative; overflow: hidden; }
.screen { transition: opacity 0.4s ease, transform 0.4s ease; }
.screen.hidden { opacity: 0; pointer-events: none; transform: scale(0.98); }
.screen:not(.hidden) { opacity: 1; pointer-events: auto; transform: scale(1); }
/* ── css: animations ── */
@keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); } 20%, 40%, 60%, 80% { transform: translateX(4px); } }
@keyframes flash { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
@keyframes float { 0%, 100% { transform: translateY(0) rotate(0deg); } 33% { transform: translateY(-6px) rotate(2deg); } 66% { transform: translateY(3px) rotate(-1deg); } }
@keyframes pulse-glow { 0%, 100% { filter: drop-shadow(0 0 4px currentColor); } 50% { filter: drop-shadow(0 0 12px currentColor); } }
@keyframes slide-up { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes particle-float { 0% { transform: translateY(0) scale(1); opacity: 0.8; } 100% { transform: translateY(-60px) scale(0); opacity: 0; } }
@keyframes prism-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.shake { animation: shake 0.5s ease-in-out; }
.flash-overlay { position: absolute; inset: 0; pointer-events: none; z-index: 100; }
.flash-white { animation: flash 0.3s ease; background: white; }
.flash-red { animation: flash 0.4s ease; background: radial-gradient(ellipse at center, rgba(255,0,0,0.4) 0%, transparent 70%); }
.battle-bounce { animation: bounce 1.5s ease-in-out infinite; }
.battle-float { animation: float 3s ease-in-out infinite; }
.menu-btn, .hud-btn, .battle-btn { cursor: pointer; user-select: none; }
.menu-btn:hover { transform: translateY(-2px); }
.menu-btn:active { transform: translateY(0); }
.hud-btn:hover { transform: scale(1.1); }
.battle-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
.battle-btn:active { transform: translateY(0); }
#map-nodes { cursor: default; }
.map-node { cursor: pointer; transition: transform 0.2s ease, filter 0.2s ease; }
.map-node:hover { transform: scale(1.15); filter: brightness(1.3); }
.map-node:active { transform: scale(0.95); }
.map-node.locked { opacity: 0.3; pointer-events: none; filter: grayscale(1); }
.map-node.completed { opacity: 0.5; pointer-events: none; }
.map-node.current { animation: pulse-glow 2s ease-in-out infinite; }
.particle { position: absolute; pointer-events: none; animation: particle-float 1.5s ease-out forwards; }
.hp-bar-green { background: linear-gradient(90deg, #22c55e, #4ade80); }
.hp-bar-yellow { background: linear-gradient(90deg, #eab308, #facc15); }
.hp-bar-red { background: linear-gradient(90deg, #ef4444, #f87171); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 3px; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }
/* ── css: elements ── */
.type-pyro { color: #ef4444; }
.type-hydro { color: #3b82f6; }
.type-flora { color: #22c55e; }
.type-zephyr { color: #06b6d4; }
.type-earth { color: #a855f7; }
.type-volt { color: #eab308; }
.type-cosmos { color: #a78bfa; }
.bg-pyro { background: linear-gradient(135deg, #7f1d1d, #dc2626); }
.bg-hydro { background: linear-gradient(135deg, #1e3a5f, #2563eb); }
.bg-flora { background: linear-gradient(135deg, #14532d, #16a34a); }
.bg-zephyr { background: linear-gradient(135deg, #164e63, #0891b2); }
.bg-earth { background: linear-gradient(135deg, #4a1d8a, #9333ea); }
.bg-volt { background: linear-gradient(135deg, #713f12, #ca8a04); }
.bg-cosmos { background: linear-gradient(135deg, #3b0764, #7c3aed); }
.border-pyro { border-color: rgba(239,68,68,0.5); }
.border-hydro { border-color: rgba(59,130,246,0.5); }
.border-flora { border-color: rgba(34,197,94,0.5); }
.border-zephyr { border-color: rgba(6,182,212,0.5); }
.border-earth { border-color: rgba(168,85,247,0.5); }
.border-volt { border-color: rgba(234,179,8,0.5); }
.border-cosmos { border-color: rgba(167,139,250,0.5); }
.glow-pyro { box-shadow: 0 0 15px rgba(239,68,68,0.4), inset 0 0 15px rgba(239,68,68,0.1); }
.glow-hydro { box-shadow: 0 0 15px rgba(59,130,246,0.4), inset 0 0 15px rgba(59,130,246,0.1); }
.glow-flora { box-shadow: 0 0 15px rgba(34,197,94,0.4), inset 0 0 15px rgba(34,197,94,0.1); }
.glow-zephyr { box-shadow: 0 0 15px rgba(6,182,212,0.4), inset 0 0 15px rgba(6,182,212,0.1); }
.glow-earth { box-shadow: 0 0 15px rgba(168,85,247,0.4), inset 0 0 15px rgba(168,85,247,0.1); }
.glow-volt { box-shadow: 0 0 15px rgba(234,179,8,0.4), inset 0 0 15px rgba(234,179,8,0.1); }
.glow-cosmos { box-shadow: 0 0 15px rgba(167,139,250,0.4), inset 0 0 15px rgba(167,139,250,0.1); }
/* ── css: map ── */
#map-nodes { width: 100%; height: 100%; }
.zone-path { stroke-dasharray: 8 4; animation: dash-flow 2s linear infinite; }
@keyframes dash-flow { to { stroke-dashoffset: -24; } }
.zone-bg { position: absolute; border-radius: 20px; opacity: 0.1; }
.node-label { font-family: 'Inter', sans-serif; font-weight: 600; fill: white; text-anchor: middle; pointer-events: none; }
.node-icon { pointer-events: none; }
</style>
</head>
<body>
<div id="app" class="w-full min-h-screen overflow-hidden">
<!-- Loading Screen -->
<div id="screen-loading" class="screen absolute inset-0 z-50 flex flex-col items-center justify-center bg-slate-900">
<div class="text-4xl font-bold text-violet-400 mb-8 tracking-widest" style="font-family: 'Press Start 2P', monospace;">AETHELGARD</div>
<div class="w-64 h-2 bg-slate-700 rounded-full overflow-hidden">
<div id="loading-bar" class="h-full bg-gradient-to-r from-violet-500 to-fuchsia-500 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div class="mt-4 text-slate-400 text-sm">Initializing Harmonic Matrix...</div>
</div>
<!-- Main Menu -->
<div id="screen-menu" class="screen hidden absolute inset-0 z-40 flex flex-col items-center justify-center" style="background: linear-gradient(135deg, #0f0a1e 0%, #1a1040 50%, #0d1b2a 100%);">
<div class="text-center mb-12">
<h1 class="text-5xl md:text-6xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-violet-400 via-fuchsia-400 to-cyan-400 mb-4" style="font-family: 'Press Start 2P', monospace;">AETHELGARD</h1>
<p class="text-xl text-slate-300 tracking-wider">Resonant Chronicles</p>
</div>
<div class="flex flex-col gap-4 w-72">
<button id="btn-new-game" class="menu-btn px-8 py-4 rounded-xl text-lg font-semibold bg-white/10 backdrop-blur-md border border-white/20 text-white hover:bg-white/20 hover:border-violet-400/50 hover:shadow-lg hover:shadow-violet-500/20 transition-all duration-300">New Game</button>
<button id="btn-continue" class="menu-btn px-8 py-4 rounded-xl text-lg font-semibold bg-white/10 backdrop-blur-md border border-white/20 text-white hover:bg-white/20 hover:border-violet-400/50 hover:shadow-lg hover:shadow-violet-500/20 transition-all duration-300 hidden">Continue</button>
<button id="btn-reset" class="menu-btn px-8 py-4 rounded-xl text-lg font-semibold bg-white/5 backdrop-blur-md border border-red-500/20 text-red-400 hover:bg-red-500/10 hover:border-red-500/40 transition-all duration-300">Reset Progress</button>
</div>
<div class="absolute bottom-6 text-slate-500 text-xs">A World of Elemental Frequencies</div>
</div>
<!-- Overworld Map -->
<div id="screen-overworld" class="screen hidden absolute inset-0 z-30 flex flex-col" style="background: linear-gradient(135deg, #0f0a1e 0%, #1a1040 50%, #0d1b2a 100%);">
<div class="flex items-center justify-between px-4 py-3 bg-black/30 backdrop-blur-md border-b border-white/10">
<div class="flex items-center gap-3">
<div id="overworld-shards" class="text-amber-400 text-sm font-semibold">◆ 0</div>
<div id="overworld-zone" class="text-slate-300 text-sm">Verdant Canopy</div>
</div>
<div class="flex items-center gap-2">
<button id="btn-party" class="hud-btn w-10 h-10 rounded-lg bg-white/10 backdrop-blur border border-white/20 text-white flex items-center justify-center hover:bg-white/20 transition-all">👥</button>
<button id="btn-dex" class="hud-btn w-10 h-10 rounded-lg bg-white/10 backdrop-blur border border-white/20 text-white flex items-center justify-center hover:bg-white/20 transition-all">📖</button>
<button id="btn-shop" class="hud-btn w-10 h-10 rounded-lg bg-white/10 backdrop-blur border border-white/20 text-white flex items-center justify-center hover:bg-white/20 transition-all">🏪</button>
<button id="btn-menu" class="hud-btn w-10 h-10 rounded-lg bg-white/10 backdrop-blur border border-white/20 text-white flex items-center justify-center hover:bg-white/20 transition-all">☰</button>
</div>
</div>
<div id="map-container" class="flex-1 relative overflow-hidden">
<div id="map-nodes" class="absolute inset-0"></div>
<div id="map-overlay" class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div id="map-overlay-content" class="pointer-events-auto"></div>
</div>
</div>
<div id="overworld-info" class="px-4 py-3 bg-black/30 backdrop-blur-md border-t border-white/10 text-slate-400 text-sm text-center min-h-[60px]">Click a node to explore.</div>
</div>
<!-- Battle Screen -->
<div id="screen-battle" class="screen hidden absolute inset-0 z-30 flex flex-col" style="background: linear-gradient(135deg, #0a0a1a 0%, #1a1040 50%, #0a0a2a 100%);">
<div class="flex-1 flex flex-col relative">
<div id="battle-enemy" class="flex flex-col items-center pt-6 px-4">
<div id="enemy-info" class="w-56 mb-3">
<div class="bg-white/10 backdrop-blur-md rounded-xl p-3 border border-white/20">
<div class="flex justify-between items-center mb-1">
<span id="enemy-name" class="text-white font-semibold text-sm">???</span>
<span id="enemy-level" class="text-slate-400 text-xs">Lv.?</span>
</div>
<div class="w-full h-3 bg-slate-700 rounded-full overflow-hidden">
<div id="enemy-hp-bar" class="h-full bg-green-500 rounded-full transition-all duration-500" style="width: 100%"></div>
</div>
<div id="enemy-hp-text" class="text-right text-slate-400 text-xs mt-1">?? / ??</div>
</div>
</div>
<div id="enemy-sprite" class="w-32 h-32 relative"></div>
<div id="enemy-status" class="mt-2 flex gap-1"></div>
</div>
<div class="flex-1 flex items-center justify-center">
<div id="battle-vs" class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-red-500 opacity-0 transition-opacity duration-300">VS</div>
</div>
<div id="battle-player" class="flex flex-col items-center pb-6 px-4">
<div id="player-sprite" class="w-32 h-32 relative mb-3"></div>
<div id="player-info" class="w-56">
<div class="bg-white/10 backdrop-blur-md rounded-xl p-3 border border-white/20">
<div class="flex justify-between items-center mb-1">
<span id="player-name" class="text-white font-semibold text-sm">???</span>
<span id="player-level" class="text-slate-400 text-xs">Lv.?</span>
</div>
<div class="w-full h-3 bg-slate-700 rounded-full overflow-hidden">
<div id="player-hp-bar" class="h-full bg-green-500 rounded-full transition-all duration-500" style="width: 100%"></div>
</div>
<div id="player-hp-text" class="text-right text-slate-400 text-xs mt-1">?? / ??</div>
<div class="w-full h-1.5 bg-slate-700 rounded-full overflow-hidden mt-1">
<div id="player-xp-bar" class="h-full bg-cyan-400 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div id="player-xp-text" class="text-right text-slate-500 text-[10px] mt-0.5">0 / 0 XP</div>
</div>
</div>
<div id="player-status" class="mt-2 flex gap-1"></div>
</div>
</div>
<div id="battle-actions" class="bg-black/40 backdrop-blur-md border-t border-white/10 p-3 min-h-[200px]">
<div id="battle-menu" class="grid grid-cols-2 gap-2">
<button id="action-attack" class="battle-btn px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold hover:bg-violet-500/30 hover:border-violet-400/50 transition-all">⚔ Attack</button>
<button id="action-prism" class="battle-btn px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold hover:bg-cyan-500/30 hover:border-cyan-400/50 transition-all">💎 Prism</button>
<button id="action-exchange" class="battle-btn px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold hover:bg-green-500/30 hover:border-green-400/50 transition-all">🔄 Exchange</button>
<button id="action-inventory" class="battle-btn px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold hover:bg-amber-500/30 hover:border-amber-400/50 transition-all">🎒 Items</button>
</div>
<div id="battle-moves" class="hidden grid grid-cols-2 gap-2"></div>
<div id="battle-prism-select" class="hidden grid grid-cols-2 gap-2">
<button id="prism-basic" class="battle-btn px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold hover:bg-cyan-500/30 transition-all">Harmonizer (×<span id="prism-basic-count">0</span>)</button>
<button id="prism-hyper" class="battle-btn px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold hover:bg-cyan-500/30 transition-all">Hyper (×<span id="prism-hyper-count">0</span>)</button>
<button id="prism-back" class="battle-btn col-span-2 px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-slate-400 font-semibold hover:bg-white/10 transition-all">← Back</button>
</div>
<div id="battle-exchange" class="hidden grid grid-cols-1 gap-2">
<button id="exchange-back" class="battle-btn col-span-2 px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-slate-400 font-semibold hover:bg-white/10 transition-all">← Back</button>
</div>
<div id="battle-inventory" class="hidden grid grid-cols-1 gap-2">
<button id="inventory-back" class="battle-btn col-span-2 px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-slate-400 font-semibold hover:bg-white/10 transition-all">← Back</button>
</div>
<div id="battle-message" class="mt-2 text-center text-slate-300 text-sm min-h-[24px]"></div>
</div>
</div>
<!-- Party Screen -->
<div id="screen-party" class="screen hidden absolute inset-0 z-40 flex flex-col" style="background: rgba(10,10,26,0.95); backdrop-filter: blur(10px);">
<div class="flex items-center justify-between px-4 py-3 border-b border-white/10">
<h2 class="text-xl text-white font-bold">Your Resonants</h2>
<button id="btn-party-close" class="w-10 h-10 rounded-lg bg-white/10 border border-white/20 text-white flex items-center justify-center hover:bg-white/20">✕</button>
</div>
<div id="party-list" class="flex-1 overflow-y-auto p-4 grid grid-cols-1 gap-3"></div>
</div>
<!-- Dex Screen -->
<div id="screen-dex" class="screen hidden absolute inset-0 z-40 flex flex-col" style="background: rgba(10,10,26,0.95); backdrop-filter: blur(10px);">
<div class="flex items-center justify-between px-4 py-3 border-b border-white/10">
<h2 class="text-xl text-white font-bold">Resonant Codex</h2>
<button id="btn-dex-close" class="w-10 h-10 rounded-lg bg-white/10 border border-white/20 text-white flex items-center justify-center hover:bg-white/20">✕</button>
</div>
<div id="dex-list" class="flex-1 overflow-y-auto p-4 grid grid-cols-2 md:grid-cols-3 gap-3"></div>
</div>
<!-- Shop Screen -->
<div id="screen-shop" class="screen hidden absolute inset-0 z-40 flex flex-col" style="background: rgba(10,10,26,0.95); backdrop-filter: blur(10px);">
<div class="flex items-center justify-between px-4 py-3 border-b border-white/10">
<h2 class="text-xl text-white font-bold">Camp Shop</h2>
<div class="text-amber-400 font-semibold">◆ <span id="shop-shards">0</span></div>
<button id="btn-shop-close" class="w-10 h-10 rounded-lg bg-white/10 border border-white/20 text-white flex items-center justify-center hover:bg-white/20">✕</button>
</div>
<div id="shop-list" class="flex-1 overflow-y-auto p-4 grid grid-cols-1 md:grid-cols-2 gap-3"></div>
</div>
<!-- Conductor Menu -->
<div id="screen-conductor-menu" class="screen hidden absolute inset-0 z-40 flex flex-col items-center justify-center" style="background: rgba(10,10,26,0.95); backdrop-filter: blur(10px);">
<h2 class="text-2xl text-white font-bold mb-8">Conductor Menu</h2>
<div class="flex flex-col gap-3 w-64">
<button id="btn-menu-save" class="menu-btn px-6 py-3 rounded-xl text-lg font-semibold bg-white/10 backdrop-blur border border-white/20 text-white hover:bg-white/20 transition-all">Save Game</button>
<button id="btn-menu-leave" class="menu-btn px-6 py-3 rounded-xl text-lg font-semibold bg-white/10 backdrop-blur border border-white/20 text-white hover:bg-white/20 transition-all">Leave</button>
<button id="btn-menu-quit" class="menu-btn px-6 py-3 rounded-xl text-lg font-semibold bg-white/5 border border-red-500/20 text-red-400 hover:bg-red-500/10 transition-all">Quit to Menu</button>
</div>
</div>
<!-- Capture Animation Overlay -->
<div id="capture-overlay" class="hidden absolute inset-0 z-50 flex flex-col items-center justify-center" style="background: rgba(0,0,0,0.8);">
<div id="capture-prism" class="w-24 h-24 relative">
<svg viewBox="0 0 100 100" class="w-full h-full">
<defs>
<linearGradient id="prism-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:1"></stop>
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1"></stop>
</linearGradient>
</defs>
<polygon points="50,5 95,50 50,95 5,50" fill="none" stroke="url(#prism-grad)" stroke-width="3">
<animate attributeName="points" values="50,5 95,50 50,95 5,50;50,15 85,50 50,85 15,50;50,5 95,50 50,95 5,50" dur="1s" repeatCount="indefinite"></animate>
</polygon>
<polygon points="50,20 80,50 50,80 20,50" fill="url(#prism-grad)" opacity="0.3">
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="0.8s" repeatCount="indefinite"></animate>
</polygon>
</svg>
</div>
<div id="capture-text" class="mt-8 text-white text-lg font-semibold text-center"></div>
</div>
<!-- Notification Toast -->
<div id="notification" class="hidden absolute top-20 left-1/2 -translate-x-1/2 z-50 px-6 py-3 rounded-xl bg-white/10 backdrop-blur-md border border-white/20 text-white text-sm font-semibold shadow-xl shadow-black/30 transition-all duration-300"></div>
</div>
<script>
'use strict';
// ── const: TYPE_CHART ──
const TYPE_CHART = {
pyro: { strong: ['flora'], weak: ['hydro'], neutral: ['zephyr', 'earth', 'volt', 'cosmos'] },
hydro: { strong: ['pyro'], weak: ['flora'], neutral: ['zephyr', 'earth', 'volt', 'cosmos'] },
flora: { strong: ['hydro'], weak: ['pyro'], neutral: ['zephyr', 'earth', 'volt', 'cosmos'] },
zephyr: { strong: ['earth'], weak: ['volt'], neutral: ['pyro', 'hydro', 'flora', 'cosmos'] },
earth: { strong: ['volt'], weak: ['zephyr'], neutral: ['pyro', 'hydro', 'flora', 'cosmos'] },
volt: { strong: ['zephyr'], weak: ['earth'], neutral: ['pyro', 'hydro', 'flora', 'cosmos'] },
cosmos: { strong: [], weak: [], neutral: ['pyro', 'hydro', 'flora', 'zephyr', 'earth', 'volt'] }
};
// ── const: TYPE_COLORS ──
const TYPE_COLORS = {
pyro: '#ef4444', hydro: '#3b82f6', flora: '#22c55e',
zephyr: '#06b6d4', earth: '#a855f7', volt: '#eab308', cosmos: '#a78bfa'
};
// ── const: TYPE_BG ──
const TYPE_BG = {
pyro: 'linear-gradient(135deg, #7f1d1d, #dc2626)',
hydro: 'linear-gradient(135deg, #1e3a5f, #2563eb)',
flora: 'linear-gradient(135deg, #14532d, #16a34a)',
zephyr: 'linear-gradient(135deg, #164e63, #0891b2)',
earth: 'linear-gradient(135deg, #4a1d8a, #9333ea)',
volt: 'linear-gradient(135deg, #713f12, #ca8a04)',
cosmos: 'linear-gradient(135deg, #3b0764, #7c3aed)'
};
// ── const: RESONANTS_DB ──
const RESONANTS_DB = [
{ id: 'emberkit', name: 'Emberkit', type: 'pyro', tier: 1, baseHp: 35, baseAtk: 28, baseDef: 20, baseSpd: 30, evolveLevel: 8, spriteParams: { body: '#ef4444', accent: '#f97316', shape: 'round' }, moves: [{ name: 'Scratch', power: 40, accuracy: 100, type: 'pyro', category: 'physical', unlockLevel: 1 }, { name: 'Ember', power: 55, accuracy: 95, type: 'pyro', category: 'special', unlockLevel: 5 }] },
{ id: 'pyrowolf', name: 'Pyrowolf', type: 'pyro', tier: 2, baseHp: 55, baseAtk: 45, baseDef: 35, baseSpd: 42, evolveLevel: 15, spriteParams: { body: '#dc2626', accent: '#fbbf24', shape: 'wolf' }, moves: [{ name: 'Scratch', power: 40, accuracy: 100, type: 'pyro', category: 'physical', unlockLevel: 1 }, { name: 'Ember', power: 55, accuracy: 95, type: 'pyro', category: 'special', unlockLevel: 5 }, { name: 'Flame Burst', power: 75, accuracy: 90, type: 'pyro', category: 'special', unlockLevel: 12 }] },
{ id: 'ignisaur', name: 'Ignisaur', type: 'pyro', tier: 3, baseHp: 80, baseAtk: 68, baseDef: 52, baseSpd: 55, evolveLevel: 99, spriteParams: { body: '#b91c1c', accent: '#f59e0b', shape: 'dino' }, moves: [{ name: 'Scratch', power: 40, accuracy: 100, type: 'pyro', category: 'physical', unlockLevel: 1 }, { name: 'Ember', power: 55, accuracy: 95, type: 'pyro', category: 'special', unlockLevel: 5 }, { name: 'Flame Burst', power: 75, accuracy: 90, type: 'pyro', category: 'special', unlockLevel: 12 }, { name: 'Inferno Rush', power: 100, accuracy: 85, type: 'pyro', category: 'physical', unlockLevel: 20 }] },
{ id: 'droplet', name: 'Droplet', type: 'hydro', tier: 1, baseHp: 40, baseAtk: 22, baseDef: 25, baseSpd: 28, evolveLevel: 8, spriteParams: { body: '#3b82f6', accent: '#60a5fa', shape: 'drop' }, moves: [{ name: 'Splash', power: 35, accuracy: 100, type: 'hydro', category: 'special', unlockLevel: 1 }, { name: 'Aqua Jet', power: 50, accuracy: 95, type: 'hydro', category: 'physical', unlockLevel: 5 }] },
{ id: 'geoxus', name: 'Geoxus', type: 'hydro', tier: 2, baseHp: 60, baseAtk: 40, baseDef: 40, baseSpd: 38, evolveLevel: 15, spriteParams: { body: '#2563eb', accent: '#93c5fd', shape: 'shell' }, moves: [{ name: 'Splash', power: 35, accuracy: 100, type: 'hydro', category: 'special', unlockLevel: 1 }, { name: 'Aqua Jet', power: 50, accuracy: 95, type: 'hydro', category: 'physical', unlockLevel: 5 }, { name: 'Tidal Wave', power: 70, accuracy: 90, type: 'hydro', category: 'special', unlockLevel: 12 }] },
{ id: 'neptunian', name: 'Neptunian', type: 'hydro', tier: 3, baseHp: 85, baseAtk: 62, baseDef: 58, baseSpd: 50, evolveLevel: 99, spriteParams: { body: '#1d4ed8', accent: '#bfdbfe', shape: 'kraken' }, moves: [{ name: 'Splash', power: 35, accuracy: 100, type: 'hydro', category: 'special', unlockLevel: 1 }, { name: 'Aqua Jet', power: 50, accuracy: 95, type: 'hydro', category: 'physical', unlockLevel: 5 }, { name: 'Tidal Wave', power: 70, accuracy: 90, type: 'hydro', category: 'special', unlockLevel: 12 }, { name: 'Mega Tsunami', power: 110, accuracy: 80, type: 'hydro', category: 'special', unlockLevel: 20 }] },
{ id: 'seedling', name: 'Seedling', type: 'flora', tier: 1, baseHp: 38, baseAtk: 25, baseDef: 22, baseSpd: 26, evolveLevel: 8, spriteParams: { body: '#22c55e', accent: '#86efac', shape: 'sprout' }, moves: [{ name: 'Vine Whip', power: 40, accuracy: 100, type: 'flora', category: 'physical', unlockLevel: 1 }, { name: 'Pollen Dust', power: 45, accuracy: 85, type: 'flora', category: 'special', unlockLevel: 5 }] },
{ id: 'thornback', name: 'Thornback', type: 'flora', tier: 2, baseHp: 58, baseAtk: 42, baseDef: 50, baseSpd: 35, evolveLevel: 15, spriteParams: { body: '#16a34a', accent: '#4ade80', shape: 'turtle' }, moves: [{ name: 'Vine Whip', power: 40, accuracy: 100, type: 'flora', category: 'physical', unlockLevel: 1 }, { name: 'Pollen Dust', power: 45, accuracy: 85, type: 'flora', category: 'special', unlockLevel: 5 }, { name: 'Thorn Barrage', power: 70, accuracy: 90, type: 'flora', category: 'physical', unlockLevel: 12 }] },
{ id: 'bloomgoliath', name: 'Bloomgoliath', type: 'flora', tier: 3, baseHp: 82, baseAtk: 65, baseDef: 62, baseSpd: 48, evolveLevel: 99, spriteParams: { body: '#15803d', accent: '#bbf7d0', shape: 'golem' }, moves: [{ name: 'Vine Whip', power: 40, accuracy: 100, type: 'flora', category: 'physical', unlockLevel: 1 }, { name: 'Pollen Dust', power: 45, accuracy: 85, type: 'flora', category: 'special', unlockLevel: 5 }, { name: 'Thorn Barrage', power: 70, accuracy: 90, type: 'flora', category: 'physical', unlockLevel: 12 }, { name: 'World Bloom', power: 100, accuracy: 85, type: 'flora', category: 'special', unlockLevel: 20 }] },
{ id: 'sparky', name: 'Sparky', type: 'volt', tier: 1, baseHp: 32, baseAtk: 30, baseDef: 18, baseSpd: 35, evolveLevel: 8, spriteParams: { body: '#eab308', accent: '#fde047', shape: 'bolt' }, moves: [{ name: 'Thunder Shock', power: 40, accuracy: 100, type: 'volt', category: 'special', unlockLevel: 1 }, { name: 'Quick Zap', power: 50, accuracy: 95, type: 'volt', category: 'physical', unlockLevel: 5 }] },
{ id: 'boltsaber', name: 'Boltsaber', type: 'volt', tier: 2, baseHp: 52, baseAtk: 50, baseDef: 32, baseSpd: 50, evolveLevel: 15, spriteParams: { body: '#ca8a04', accent: '#facc15', shape: 'sword' }, moves: [{ name: 'Thunder Shock', power: 40, accuracy: 100, type: 'volt', category: 'special', unlockLevel: 1 }, { name: 'Quick Zap', power: 50, accuracy: 95, type: 'volt', category: 'physical', unlockLevel: 5 }, { name: 'Volt Chain', power: 75, accuracy: 88, type: 'volt', category: 'special', unlockLevel: 12 }] },
{ id: 'stormlord', name: 'Stormlord', type: 'volt', tier: 3, baseHp: 75, baseAtk: 72, baseDef: 48, baseSpd: 65, evolveLevel: 99, spriteParams: { body: '#a16207', accent: '#fef08a', shape: 'dragon' }, moves: [{ name: 'Thunder Shock', power: 40, accuracy: 100, type: 'volt', category: 'special', unlockLevel: 1 }, { name: 'Quick Zap', power: 50, accuracy: 95, type: 'volt', category: 'physical', unlockLevel: 5 }, { name: 'Volt Chain', power: 75, accuracy: 88, type: 'volt', category: 'special', unlockLevel: 12 }, { name: 'Thunder Dominion', power: 110, accuracy: 82, type: 'volt', category: 'special', unlockLevel: 20 }] },
{ id: 'gusty', name: 'Gusty', type: 'zephyr', tier: 1, baseHp: 33, baseAtk: 26, baseDef: 20, baseSpd: 32, evolveLevel: 8, spriteParams: { body: '#06b6d4', accent: '#67e8f9', shape: 'cloud' }, moves: [{ name: 'Gust', power: 35, accuracy: 100, type: 'zephyr', category: 'special', unlockLevel: 1 }, { name: 'Air Slash', power: 50, accuracy: 95, type: 'zephyr', category: 'physical', unlockLevel: 5 }] },
{ id: 'slipstream', name: 'Slipstream', type: 'zephyr', tier: 2, baseHp: 50, baseAtk: 44, baseDef: 30, baseSpd: 48, evolveLevel: 15, spriteParams: { body: '#0891b2', accent: '#22d3ee', shape: 'tornado' }, moves: [{ name: 'Gust', power: 35, accuracy: 100, type: 'zephyr', category: 'special', unlockLevel: 1 }, { name: 'Air Slash', power: 50, accuracy: 95, type: 'zephyr', category: 'physical', unlockLevel: 5 }, { name: 'Cyclone', power: 75, accuracy: 88, type: 'zephyr', category: 'special', unlockLevel: 12 }] },
{ id: 'zephyrus', name: 'Zephyrus', type: 'zephyr', tier: 3, baseHp: 72, baseAtk: 66, baseDef: 45, baseSpd: 62, evolveLevel: 99, spriteParams: { body: '#0e7490', accent: '#a5f3fc', shape: 'phoenix' }, moves: [{ name: 'Gust', power: 35, accuracy: 100, type: 'zephyr', category: 'special', unlockLevel: 1 }, { name: 'Air Slash', power: 50, accuracy: 95, type: 'zephyr', category: 'physical', unlockLevel: 5 }, { name: 'Cyclone', power: 75, accuracy: 88, type: 'zephyr', category: 'special', unlockLevel: 12 }, { name: 'Tempest Wing', power: 100, accuracy: 85, type: 'zephyr', category: 'physical', unlockLevel: 20 }] },
{ id: 'pebble', name: 'Pebble', type: 'earth', tier: 1, baseHp: 42, baseAtk: 24, baseDef: 28, baseSpd: 22, evolveLevel: 8, spriteParams: { body: '#a855f7', accent: '#c084fc', shape: 'rock' }, moves: [{ name: 'Rock Throw', power: 40, accuracy: 100, type: 'earth', category: 'physical', unlockLevel: 1 }, { name: 'Mud Slide', power: 45, accuracy: 90, type: 'earth', category: 'special', unlockLevel: 5 }] },
{ id: 'obsidian', name: 'Obsidian', type: 'earth', tier: 2, baseHp: 62, baseAtk: 42, baseDef: 55, baseSpd: 30, evolveLevel: 15, spriteParams: { body: '#9333ea', accent: '#d8b4fe', shape: 'golem' }, moves: [{ name: 'Rock Throw', power: 40, accuracy: 100, type: 'earth', category: 'physical', unlockLevel: 1 }, { name: 'Mud Slide', power: 45, accuracy: 90, type: 'earth', category: 'special', unlockLevel: 5 }, { name: 'Quake', power: 80, accuracy: 85, type: 'earth', category: 'physical', unlockLevel: 12 }] },
{ id: 'terrasunder', name: 'Terrasunder', type: 'earth', tier: 3, baseHp: 88, baseAtk: 64, baseDef: 70, baseSpd: 42, evolveLevel: 99, spriteParams: { body: '#7e22ce', accent: '#e9d5ff', shape: 'titan' }, moves: [{ name: 'Rock Throw', power: 40, accuracy: 100, type: 'earth', category: 'physical', unlockLevel: 1 }, { name: 'Mud Slide', power: 45, accuracy: 90, type: 'earth', category: 'special', unlockLevel: 5 }, { name: 'Quake', power: 80, accuracy: 85, type: 'earth', category: 'physical', unlockLevel: 12 }, { name: 'Continental Shift', power: 110, accuracy: 80, type: 'earth', category: 'physical', unlockLevel: 20 }] }
];
// ── const: XP_TABLE ──
const XP_TABLE = [0, 10, 25, 50, 90, 150, 230, 340, 480, 660, 900, 1200, 1600, 2100, 2700, 3500, 4500, 5800, 7500, 9600, 12000];
// ── function: getResonantTemplate ──
function getResonantTemplate(id) {
return RESONANTS_DB.find(function(r) { return r.id === id; });
}
// ── function: createResonant ──
function createResonant(template, level) {
var stats = calculateStats(template, level);
var moves = template.moves.filter(function(m) { return m.unlockLevel <= level; });
return {
id: template.id, name: template.name, type: template.type, tier: template.tier,
level: level, currentHp: stats.hp, maxHp: stats.hp,
atk: stats.atk, def: stats.def, spd: stats.spd,
xp: 0, moves: moves, status: null, statusTurns: 0, isFainted: false,
baseHp: template.baseHp, baseAtk: template.baseAtk, baseDef: template.baseDef, baseSpd: template.baseSpd,
evolveLevel: template.evolveLevel, spriteParams: template.spriteParams
};
}
// ── function: calculateStats ──
function calculateStats(template, level) {
var hp = Math.floor(((2 * template.baseHp + 10 + Math.floor(level / 2)) * level) / 100 + level + 10);
var atk = Math.floor(((2 * template.baseAtk + 5) * level) / 100 + 5);
var def = Math.floor(((2 * template.baseDef + 5) * level) / 100 + 5);
var spd = Math.floor(((2 * template.baseSpd + 5) * level) / 100 + 5);
return { hp: hp, atk: atk, def: def, spd: spd };
}
// ── function: getTypeEffectiveness ──
function getTypeEffectiveness(atkType, defType) {
if (atkType === defType) return 1;
var chart = TYPE_CHART[atkType];
if (!chart) return 1;
if (chart.strong.indexOf(defType) >= 0) return 1.5;
if (chart.weak.indexOf(defType) >= 0) return 0.75;
return 1;
}
// ── function: calcDamage ──
function calcDamage(attacker, defender, move) {
if (move.power <= 0) return { damage: 0, typeMod: 1, isCrit: false };
var level = attacker.level;
var attack = move.category === 'physical' ? attacker.atk : Math.floor(attacker.atk * 0.8);
var defense = move.category === 'physical' ? defender.def : Math.floor(defender.def * 0.8);
var baseDamage = (((2 * level / 5 + 2) * move.power * (attack / Math.max(defense, 1))) / 50 + 2);
var typeMod = getTypeEffectiveness(move.type, defender.type);
var random = 0.85 + Math.random() * 0.15;
var isCrit = Math.random() < 0.1;
var critMod = isCrit ? 1.5 : 1;
return { damage: Math.max(1, Math.floor(baseDamage * typeMod * random * critMod)), typeMod: typeMod, isCrit: isCrit };
}
// ── function: getEvolutionLine ──
function getEvolutionLine(id) {
var entry = getResonantTemplate(id);
return RESONANTS_DB.filter(function(r) { return r.type === entry.type; });
}
// ── function: generateSpriteSVG ──
function generateSpriteSVG(resonant, size) {
size = size || 128;
var p = resonant.spriteParams;
var bodyColor = p.body;
var accentColor = p.accent;
var svg = '<svg viewBox="0 0 128 128" class="w-full h-full battle-bounce">';
svg += '<defs>';
svg += '<radialGradient id="grad-' + resonant.id + '" cx="50%" cy="40%" r="50%">';
svg += '<stop offset="0%" style="stop-color:' + accentColor + ';stop-opacity:0.8"/>';
svg += '<stop offset="100%" style="stop-color:' + bodyColor + ';stop-opacity:1"/>';
svg += '</radialGradient>';
svg += '<filter id="glow-' + resonant.id + '"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>';
svg += '</defs>';
svg += '<g filter="url(#glow-' + resonant.id + ')" class="battle-float">';
if (p.shape === 'round') {
svg += '<circle cx="64" cy="64" r="40" fill="url(#grad-' + resonant.id + ')/>';
svg += '<circle cx="64" cy="64" r="35" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="52" cy="55" rx="6" ry="8" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="76" cy="55" rx="6" ry="8" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="52" cy="56" rx="3" ry="4" fill="#1a1a2e"/>';
svg += '<ellipse cx="76" cy="56" rx="3" ry="4" fill="#1a1a2e"/>';
svg += '<path d="M55 75 Q64 85 73 75" stroke="' + accentColor + '" stroke-width="2.5" fill="none" stroke-linecap="round"/>';
svg += '<circle cx="38" cy="40" r="4" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<circle cx="90" cy="40" r="4" fill="' + accentColor + '" opacity="0.6"/>';
} else if (p.shape === 'wolf') {
svg += '<ellipse cx="64" cy="72" rx="35" ry="28" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="72" rx="30" ry="24" fill="' + bodyColor + '" opacity="0.4"/>';
svg += '<polygon points="40,48 30,20 52,42" fill="' + bodyColor + '"/>';
svg += '<polygon points="88,48 98,20 76,42" fill="' + bodyColor + '"/>';
svg += '<polygon points="42,46 34,26 50,42" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<polygon points="86,46 94,26 78,42" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<ellipse cx="52" cy="62" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="76" cy="62" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<circle cx="52" cy="63" r="2.5" fill="#1a1a2e"/>';
svg += '<circle cx="76" cy="63" r="2.5" fill="#1a1a2e"/>';
svg += '<path d="M56 82 Q64 90 72 82" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<path d="M30 90 Q25 100 20 105" stroke="' + bodyColor + '" stroke-width="4" fill="none" stroke-linecap="round"/>';
} else if (p.shape === 'dino') {
svg += '<ellipse cx="64" cy="75" rx="38" ry="30" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="75" rx="34" ry="26" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="64" cy="50" rx="22" ry="18" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="50" rx="18" ry="14" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="56" cy="46" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="46" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="56" cy="47" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="47" r="2" fill="#1a1a2e"/>';
svg += '<path d="M54 58 Q64 66 74 58" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<polygon points="45,35 40,15 52,32" fill="' + accentColor + '" opacity="0.7"/>';
svg += '<polygon points="83,35 88,15 76,32" fill="' + accentColor + '" opacity="0.7"/>';
svg += '<path d="M100 80 Q110 75 115 85 Q110 95 100 90" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<line x1="40" y1="100" x2="35" y2="115" stroke="' + bodyColor + '" stroke-width="5" stroke-linecap="round"/>';
svg += '<line x1="88" y1="100" x2="93" y2="115" stroke="' + bodyColor + '" stroke-width="5" stroke-linecap="round"/>';
} else if (p.shape === 'drop') {
svg += '<path d="M64 15 Q85 50 85 72 Q85 95 64 95 Q43 95 43 72 Q43 50 64 15Z" fill="url(#grad-' + resonant.id + ')/>';
svg += '<path d="M64 15 Q85 50 85 72 Q85 95 64 95 Q43 95 43 72 Q43 50 64 15Z" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="55" cy="60" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="73" cy="60" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="55" cy="61" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="73" cy="61" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 74 Q64 80 72 74" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<ellipse cx="50" cy="35" rx="3" ry="5" fill="' + accentColor + '" opacity="0.4"/>';
svg += '<ellipse cx="78" cy="35" rx="3" ry="5" fill="' + accentColor + '" opacity="0.4"/>';
} else if (p.shape === 'shell') {
svg += '<ellipse cx="64" cy="75" rx="38" ry="28" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="75" rx="34" ry="24" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<path d="M40 55 Q52 30 64 55 Q76 30 88 55" stroke="' + accentColor + '" stroke-width="2" fill="none" opacity="0.5"/>';
svg += '<path d="M45 65 Q55 45 64 65 Q73 45 83 65" stroke="' + accentColor + '" stroke-width="1.5" fill="none" opacity="0.3"/>';
svg += '<ellipse cx="54" cy="68" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="68" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="69" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="69" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 82 Q64 88 72 82" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<path d="M35 90 Q30 100 25 105" stroke="' + bodyColor + '" stroke-width="3" fill="none" stroke-linecap="round"/>';
svg += '<path d="M93 90 Q98 100 103 105" stroke="' + bodyColor + '" stroke-width="3" fill="none" stroke-linecap="round"/>';
} else if (p.shape === 'kraken') {
svg += '<ellipse cx="64" cy="55" rx="35" ry="30" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="55" rx="30" ry="25" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="52" cy="48" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="76" cy="48" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<circle cx="52" cy="49" r="2.5" fill="#1a1a2e"/>';
svg += '<circle cx="76" cy="49" r="2.5" fill="#1a1a2e"/>';
svg += '<path d="M54 65 Q64 72 74 65" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<path d="M35 75 Q20 85 15 100" stroke="' + bodyColor + '" stroke-width="4" fill="none" stroke-linecap="round"/>';
svg += '<path d="M45 80 Q35 95 30 105" stroke="' + bodyColor + '" stroke-width="3" fill="none" stroke-linecap="round"/>';
svg += '<path d="M93 75 Q108 85 113 100" stroke="' + bodyColor + '" stroke-width="4" fill="none" stroke-linecap="round"/>';
svg += '<path d="M83 80 Q93 95 98 105" stroke="' + bodyColor + '" stroke-width="3" fill="none" stroke-linecap="round"/>';
svg += '<circle cx="48" cy="65" r="2" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<circle cx="80" cy="65" r="2" fill="' + accentColor + '" opacity="0.5"/>';
} else if (p.shape === 'sprout') {
svg += '<circle cx="64" cy="72" r="30" fill="url(#grad-' + resonant.id + ')/>';
svg += '<circle cx="64" cy="72" r="26" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<path d="M64 42 Q60 25 64 15 Q68 25 64 42" fill="' + accentColor + '" opacity="0.7"/>';
svg += '<path d="M64 42 Q50 30 45 35 Q50 40 64 42" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<path d="M64 42 Q78 30 83 35 Q78 40 64 42" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<ellipse cx="54" cy="65" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="65" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="66" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="66" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 80 Q64 86 72 80" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<line x1="55" y1="100" x2="52" y2="112" stroke="' + bodyColor + '" stroke-width="3" stroke-linecap="round"/>';
svg += '<line x1="73" y1="100" x2="76" y2="112" stroke="' + bodyColor + '" stroke-width="3" stroke-linecap="round"/>';
} else if (p.shape === 'turtle') {
svg += '<ellipse cx="64" cy="72" rx="38" ry="28" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="72" rx="34" ry="24" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<path d="M42 58 Q52 40 64 58 Q76 40 86 58" stroke="' + accentColor + '" stroke-width="2" fill="none" opacity="0.6"/>';
svg += '<path d="M48 65 Q56 50 64 65 Q72 50 80 65" stroke="' + accentColor + '" stroke-width="1.5" fill="none" opacity="0.4"/>';
svg += '<ellipse cx="54" cy="68" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="68" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="69" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="69" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 82 Q64 88 72 82" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<circle cx="38" cy="85" r="6" fill="' + bodyColor + '"/>';
svg += '<circle cx="90" cy="85" r="6" fill="' + bodyColor + '"/>';
svg += '<circle cx="38" cy="85" r="3" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<circle cx="90" cy="85" r="3" fill="' + accentColor + '" opacity="0.5"/>';
} else if (p.shape === 'golem') {
svg += '<ellipse cx="64" cy="75" rx="35" ry="32" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="75" rx="30" ry="27" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<polygon points="50,55 55,40 60,55" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<polygon points="68,55 73,40 78,55" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<ellipse cx="54" cy="65" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="65" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="66" r="2.5" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="66" r="2.5" fill="#1a1a2e"/>';
svg += '<path d="M54 82 Q64 90 74 82" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<rect x="35" y="95" width="12" height="15" rx="3" fill="' + bodyColor + '"/>';
svg += '<rect x="81" y="95" width="12" height="15" rx="3" fill="' + bodyColor + '"/>';
} else if (p.shape === 'bolt') {
svg += '<polygon points="64,10 85,55 70,55 80,100 45,60 60,60" fill="url(#grad-' + resonant.id + ')/>';
svg += '<polygon points="64,10 85,55 70,55 80,100 45,60 60,60" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="58" cy="45" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="72" cy="45" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="58" cy="46" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="72" cy="46" r="2" fill="#1a1a2e"/>';
svg += '<path d="M58 58 Q64 62 70 58" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<circle cx="40" cy="30" r="3" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<circle cx="88" cy="30" r="3" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<circle cx="50" cy="110" r="2" fill="' + accentColor + '" opacity="0.4"/>';
svg += '<circle cx="78" cy="110" r="2" fill="' + accentColor + '" opacity="0.4"/>';
} else if (p.shape === 'sword') {
svg += '<ellipse cx="64" cy="70" rx="30" ry="35" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="70" rx="26" ry="30" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<polygon points="64,10 72,35 64,30 56,35" fill="' + accentColor + '" opacity="0.8"/>';
svg += '<ellipse cx="56" cy="60" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="72" cy="60" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="56" cy="61" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="72" cy="61" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 78 Q64 84 72 78" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<line x1="40" y1="100" x2="35" y2="115" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
svg += '<line x1="88" y1="100" x2="93" y2="115" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
} else if (p.shape === 'dragon') {
svg += '<ellipse cx="64" cy="65" rx="30" ry="32" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="65" rx="26" ry="28" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<path d="M35 40 Q20 20 30 10 Q35 25 45 35" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<path d="M93 40 Q108 20 98 10 Q93 25 83 35" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<ellipse cx="54" cy="58" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="58" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="59" r="2.5" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="59" r="2.5" fill="#1a1a2e"/>';
svg += '<path d="M54 76 Q64 84 74 76" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<path d="M38 92 Q30 105 25 110" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
svg += '<path d="M90 92 Q98 105 103 110" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
svg += '<circle cx="48" cy="42" r="2" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<circle cx="80" cy="42" r="2" fill="' + accentColor + '" opacity="0.5"/>';
} else if (p.shape === 'cloud') {
svg += '<ellipse cx="64" cy="70" rx="35" ry="28" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="70" rx="31" ry="24" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<circle cx="45" cy="50" r="12" fill="' + accentColor + '" opacity="0.4"/>';
svg += '<circle cx="83" cy="50" r="12" fill="' + accentColor + '" opacity="0.4"/>';
svg += '<circle cx="64" cy="40" r="14" fill="' + accentColor + '" opacity="0.4"/>';
svg += '<ellipse cx="54" cy="65" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="65" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="66" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="66" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 78 Q64 84 72 78" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<line x1="42" y1="95" x2="38" y2="108" stroke="' + bodyColor + '" stroke-width="3" stroke-linecap="round"/>';
svg += '<line x1="86" y1="95" x2="90" y2="108" stroke="' + bodyColor + '" stroke-width="3" stroke-linecap="round"/>';
} else if (p.shape === 'tornado') {
svg += '<path d="M64 15 Q80 30 75 50 Q70 70 80 90 Q85 100 75 105 Q65 108 60 100 Q55 90 60 75 Q65 60 55 45 Q48 30 64 15Z" fill="url(#grad-' + resonant.id + ')/>';
svg += '<path d="M64 15 Q80 30 75 50 Q70 70 80 90 Q85 100 75 105 Q65 108 60 100 Q55 90 60 75 Q65 60 55 45 Q48 30 64 15Z" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="56" cy="55" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="72" cy="55" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="56" cy="56" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="72" cy="56" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 68 Q64 74 72 68" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<circle cx="40" cy="40" r="3" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<circle cx="88" cy="40" r="3" fill="' + accentColor + '" opacity="0.5"/>';
} else if (p.shape === 'phoenix') {
svg += '<ellipse cx="64" cy="60" rx="28" ry="30" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="60" rx="24" ry="26" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<path d="M36 40 Q20 20 25 10 Q30 25 40 35" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<path d="M92 40 Q108 20 103 10 Q98 25 88 35" fill="' + accentColor + '" opacity="0.6"/>';
svg += '<ellipse cx="54" cy="52" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="52" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="53" r="2.5" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="53" r="2.5" fill="#1a1a2e"/>';
svg += '<path d="M54 70 Q64 78 74 70" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<path d="M38 85 Q30 95 25 105" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
svg += '<path d="M90 85 Q98 95 103 105" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
svg += '<path d="M42 90 Q35 100 30 108" stroke="' + accentColor + '" stroke-width="2" fill="none" opacity="0.5"/>';
svg += '<path d="M86 90 Q93 100 98 108" stroke="' + accentColor + '" stroke-width="2" fill="none" opacity="0.5"/>';
} else if (p.shape === 'rock') {
svg += '<polygon points="64,20 95,50 88,90 40,90 33,50" fill="url(#grad-' + resonant.id + ')/>';
svg += '<polygon points="64,20 95,50 88,90 40,90 33,50" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<ellipse cx="54" cy="55" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="55" rx="4" ry="5" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="56" r="2" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="56" r="2" fill="#1a1a2e"/>';
svg += '<path d="M56 70 Q64 76 72 70" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<line x1="45" y1="90" x2="42" y2="105" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
svg += '<line x1="83" y1="90" x2="86" y2="105" stroke="' + bodyColor + '" stroke-width="4" stroke-linecap="round"/>';
} else if (p.shape === 'titan') {
svg += '<ellipse cx="64" cy="65" rx="32" ry="35" fill="url(#grad-' + resonant.id + ')/>';
svg += '<ellipse cx="64" cy="65" rx="28" ry="30" fill="' + bodyColor + '" opacity="0.3"/>';
svg += '<polygon points="40,35 30,15 50,30" fill="' + bodyColor + '"/>';
svg += '<polygon points="88,35 98,15 78,30" fill="' + bodyColor + '"/>';
svg += '<polygon points="42,33 34,20 48,30" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<polygon points="86,33 94,20 80,30" fill="' + accentColor + '" opacity="0.5"/>';
svg += '<ellipse cx="54" cy="55" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<ellipse cx="74" cy="55" rx="5" ry="6" fill="white" opacity="0.9"/>';
svg += '<circle cx="54" cy="56" r="2.5" fill="#1a1a2e"/>';
svg += '<circle cx="74" cy="56" r="2.5" fill="#1a1a2e"/>';
svg += '<path d="M54 74 Q64 82 74 74" stroke="' + accentColor + '" stroke-width="2" fill="none" stroke-linecap="round"/>';
svg += '<rect x="32" y="95" width="14" height="18" rx="4" fill="' + bodyColor + '"/>';
svg += '<rect x="82" y="95" width="14" height="18" rx="4" fill="' + bodyColor + '"/>';
svg += '<circle cx="48" cy="45" r="2" fill="' + accentColor + '" opacity="0.4"/>';
svg += '<circle cx="80" cy="45" r="2" fill="' + accentColor + '" opacity="0.4"/>';
}
svg += '</g></svg>';
return svg;
}
// ── var: AudioCtx ──
var AudioCtx = window.AudioContext || window.webkitAudioContext;
// ── var: synth ──
var synth = null;
// ── function: initAudio ──
function initAudio() {
if (!synth) {
synth = new AudioCtx();
}
if (synth.state === 'suspended') {
synth.resume();
}
}
// ── function: playTone ──
function playTone(freq, duration, type, volume) {
if (!synth) return;
try {
var osc = synth.createOscillator();
var gain = synth.createGain();
osc.type = type || 'square';
osc.frequency.setValueAtTime(freq, synth.currentTime);
gain.gain.setValueAtTime(volume || 0.1, synth.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, synth.currentTime + duration);
osc.connect(gain);
gain.connect(synth.destination);
osc.start();
osc.stop(synth.currentTime + duration);
} catch(e) {}
}
// ── function: playNoise ──
function playNoise(duration, volume) {
if (!synth) return;
try {
var bufferSize = synth.sampleRate * duration;
var buffer = synth.createBuffer(1, bufferSize, synth.sampleRate);
var data = buffer.getChannelData(0);
for (var i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
var source = synth.createBufferSource();
source.buffer = buffer;
var gain = synth.createGain();
gain.gain.setValueAtTime(volume || 0.05, synth.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, synth.currentTime + duration);
source.connect(gain);
gain.connect(synth.destination);
source.start();
} catch(e) {}
}
// ── function: sfxHover ──
function sfxHover() { playTone(800, 0.05, 'sine', 0.05); }
// ── function: sfxSelect ──
function sfxSelect() { playTone(600, 0.08, 'square', 0.08); playTone(900, 0.08, 'square', 0.06); }
// ── function: sfxHit ──
function sfxHit() { playNoise(0.15, 0.12); playTone(200, 0.1, 'sawtooth', 0.08); }
// ── function: sfxSuperEffective ──
function sfxSuperEffective() { playTone(400, 0.15, 'sawtooth', 0.1); playTone(600, 0.15, 'sawtooth', 0.08); playTone(800, 0.2, 'sawtooth', 0.06); }
// ── function: sfxLevelUp ──
function sfxLevelUp() { playTone(523, 0.15, 'square', 0.08); setTimeout(function(){ playTone(659, 0.15, 'square', 0.08); }, 150); setTimeout(function(){ playTone(784, 0.3, 'square', 0.1); }, 300); }
// ── function: sfxCapture ──
function sfxCapture() { playTone(440, 0.1, 'sine', 0.08); setTimeout(function(){ playTone(554, 0.1, 'sine', 0.08); }, 100); setTimeout(function(){ playTone(659, 0.15, 'sine', 0.08); }, 200); setTimeout(function(){ playTone(880, 0.3, 'sine', 0.1); }, 350); }
// ── function: sfxCaptureFail ──
function sfxCaptureFail() { playTone(400, 0.2, 'sawtooth', 0.08); setTimeout(function(){ playTone(300, 0.3, 'sawtooth', 0.08); }, 200); }
// ── function: sfxEvolve ──
function sfxEvolve() { playTone(330, 0.1, 'sine', 0.08); setTimeout(function(){ playTone(440, 0.1, 'sine', 0.08); }, 100); setTimeout(function(){ playTone(550, 0.1, 'sine', 0.08); }, 200); setTimeout(function(){ playTone(660, 0.1, 'sine', 0.08); }, 300); setTimeout(function(){ playTone(880, 0.4, 'sine', 0.12); }, 400); }
// ── function: sfxHeal ──
function sfxHeal() { playTone(523, 0.1, 'sine', 0.06); setTimeout(function(){ playTone(659, 0.1, 'sine', 0.06); }, 100); setTimeout(function(){ playTone(784, 0.15, 'sine', 0.08); }, 200); }
// ── function: sfxBoss ──
function sfxBoss() { playTone(100, 0.3, 'sawtooth', 0.1); playTone(150, 0.3, 'sawtooth', 0.08); }
// ── var: ZONES ──
var ZONES = [
{
id: 'verdant', name: 'The Verdant Canopy', desc: 'Lush canopy where Flora and Zephyr Resonants dwell.',
bg: 'linear-gradient(135deg, #0a2a1a 0%, #1a4a2a 50%, #0a3a1a 100%)',
minLevel: 1, nodes: [
{ id: 'v1', type: 'wild', label: 'Tall Grass', x: 15, y: 70, encountered: ['seedling', 'gusty'] },
{ id: 'v2', type: 'wild', label: 'Hidden Grove', x: 30, y: 55, encountered: ['seedling', 'gusty', 'sparky'] },
{ id: 'v3', type: 'cache', label: 'Mystery Cache', x: 45, y: 45, reward: 'shards', amount: [5, 15] },
{ id: 'v4', type: 'spring', label: 'Healing Spring', x: 55, y: 55 },
{ id: 'v5', type: 'wild', label: 'Deep Canopy', x: 70, y: 45, encountered: ['seedling', 'gusty', 'sparky'] },
{ id: 'v6', type: 'cache', label: 'Aether Cache', x: 80, y: 35, reward: 'shards', amount: [8, 20] },
{ id: 'v7', type: 'boss', label: 'Canopy Guardian', x: 85, y: 20, bossId: 'boss-verdant', encountered: ['thornback', 'slipstream'] }
]
},
{
id: 'grotto', name: 'The Sunken Grotto', desc: 'Underwater caverns humming with Hydro and Terra energy.',
bg: 'linear-gradient(135deg, #0a1a3a 0%, #1a3a5a 50%, #0a2a4a 100%)',
minLevel: 5, nodes: [
{ id: 'g1', type: 'wild', label: 'Crystal Pool', x: 15, y: 70, encountered: ['droplet', 'pebble'] },
{ id: 'g2', type: 'wild', label: 'Deep Current', x: 30, y: 55, encountered: ['droplet', 'pebble', 'seedling'] },
{ id: 'g3', type: 'cache', label: 'Sunken Chest', x: 45, y: 45, reward: 'shards', amount: [10, 25] },
{ id: 'g4', type: 'spring', label: 'Warm Spring', x: 55, y: 55 },
{ id: 'g5', type: 'wild', label: 'Abyssal Cave', x: 70, y: 45, encountered: ['droplet', 'pebble'] },
{ id: 'g6', type: 'cache', label: 'Pearl Cache', x: 80, y: 35, reward: 'shards', amount: [12, 30] },
{ id: 'g7', type: 'boss', label: 'Grotto Warden', x: 85, y: 20, bossId: 'boss-grotto', encountered: ['geoxus', 'obsidian'] }
]
},
{
id: 'crag', name: 'The Obsidian Crags', desc: 'Volcanic peaks crackling with Pyro and Volt resonance.',
bg: 'linear-gradient(135deg, #2a0a0a 0%, #4a1a1a 50%, #3a0a1a 100%)',
minLevel: 10, nodes: [
{ id: 'c1', type: 'wild', label: 'Magma Vent', x: 15, y: 70, encountered: ['emberkit', 'sparky'] },
{ id: 'c2', type: 'wild', label: 'Ash Trail', x: 30, y: 55, encountered: ['emberkit', 'sparky', 'pebble'] },
{ id: 'c3', type: 'cache', label: 'Volcanic Cache', x: 45, y: 45, reward: 'shards', amount: [15, 35] },
{ id: 'c4', type: 'spring', label: 'Hot Spring', x: 55, y: 55 },
{ id: 'c5', type: 'wild', label: 'Lava Flow', x: 70, y: 45, encountered: ['emberkit', 'sparky'] },
{ id: 'c6', type: 'cache', label: 'Crystal Cache', x: 80, y: 35, reward: 'shards', amount: [18, 40] },
{ id: 'c7', type: 'boss', label: 'Crag Sentinel', x: 85, y: 20, bossId: 'boss-crag', encountered: ['pyrowolf', 'boltsaber'] }
]
},
{
id: 'apex', name: 'The Apex Pillar', desc: 'The final conduit — a gateway to cosmic harmony.',
bg: 'linear-gradient(135deg, #1a0a2a 0%, #2a1a4a 50%, #1a0a3a 100%)',
minLevel: 15, nodes: [
{ id: 'a1', type: 'wild', label: 'Void Edge', x: 15, y: 70, encountered: ['ignisaur', 'neptunian'] },
{ id: 'a2', type: 'wild', label: 'Starfall Path', x: 30, y: 55, encountered: ['bloomgoliath', 'stormlord'] },
{ id: 'a3', type: 'cache', label: 'Cosmic Cache', x: 45, y: 45, reward: 'shards', amount: [25, 50] },
{ id: 'a4', type: 'spring', label: 'Pure Spring', x: 55, y: 55 },
{ id: 'a5', type: 'wild', label: 'Ethereal Rift', x: 70, y: 45, encountered: ['zephyrus', 'terrasunder'] },
{ id: 'a6', type: 'cache', label: 'Final Cache', x: 80, y: 35, reward: 'shards', amount: [30, 60] },
{ id: 'a7', type: 'boss', label: 'Apex Conductor', x: 85, y: 20, bossId: 'boss-apex', encountered: ['ignisaur', 'neptunian', 'stormlord'] }
]
}
];
// ── var: BOSS_DIALOGS ──
var BOSS_DIALOGS = {
'boss-verdant': { name: 'Conductor Sylvara', title: 'Guardian of the Canopy', dialog: ['The Verdant Canopy is under my protection.', 'Your Resonants... their frequencies are intriguing.', 'Prove your worth as a Conductor!'], prize: 50 },
'boss-grotto': { name: 'Conductor Maris', title: 'Warden of the Grotto', dialog: ['The Sunken Grotto yields its secrets to none.', 'Your harmonic devices are impressive.', 'But can you conduct the depths?'], prize: 80 },
'boss-crag': { name: 'Conductor Ignatius', title: 'Sentinel of the Crags', dialog: ['The Obsidian Crags test the resolve of all Conductors.', 'Your Resonants burn bright... for now.', 'Let us see if you can withstand the heat!'], prize: 120 },
'boss-apex': { name: 'Conductor Aethel', title: 'The Final Conductor', dialog: ['You have reached the Apex Pillar.', 'I am the last guardian of Aethelgard.', 'If you defeat me, the world\'s frequencies will be in your hands.', 'Show me the true harmony of a Conductor!'] }
};
// ── var: SHOP_ITEMS ──
var SHOP_ITEMS = [
{ id: 'harmonizer', name: 'Harmonizer Prism', desc: 'Basic Aether Prism. Moderate capture rate.', cost: 30, type: 'prism-basic', qty: 1 },
{ id: 'hyper', name: 'Hyper-Conductive Prism', desc: 'Advanced Aether Prism. High capture rate.', cost: 80, type: 'prism-hyper', qty: 1 },
{ id: 'restorer', name: 'Core Restorer', desc: 'Restores 50% HP to one Resonant.', cost: 20, type: 'vitals-elixir', qty: 1 },
{ id: 'cleanser', name: 'Resonance Cleanser', desc: 'Heals all status conditions for one Resonant.', cost: 25, type: 'status-cleanser', qty: 1 },
{ id: 'prism-booster', name: 'Prism Booster', desc: 'Increases capture rate by 50% for next prism use.', cost: 50, type: 'prism-booster', qty: 1 }
];
// ── var: defaultPlayer ──
var defaultPlayer = function() {
return {
name: 'Conductor',
party: [],
dex: [],
inventory: { 'harmonizer': 3, 'hyper': 0, 'restorer': 2, 'cleanser': 1, 'prism-booster': 0 },
shards: 50,
unlockedZones: ['verdant'],
completedBosses: [],
currentZone: 'verdant',
currentActiveNode: null,
totalBattles: 0,
totalCaptures: 0,
started: false
};
};
// ── var: player ──
var player = defaultPlayer();
// ── function: saveGame ──
function saveGame() {
try {
localStorage.setItem('aethelgard_save', JSON.stringify(player));
showNotification('Game Saved!');
} catch(e) {
showNotification('Save Failed!');
}
}
// ── function: loadGame ──
function loadGame() {
try {
var data = localStorage.getItem('aethelgard_save');
if (data) {
player = JSON.parse(data);
return true;
}
} catch(e) {}
return false;
}
// ── function: hasSave ──
function hasSave() {
try {
return !!localStorage.getItem('aethelgard_save');
} catch(e) { return false; }
}
// ── function: resetProgress ──
function resetProgress() {
localStorage.removeItem('aethelgard_save');
player = defaultPlayer();
showNotification('Progress Reset!');
}
// ── function: addResonantToParty ──
function addResonantToParty(template, level) {
if (player.party.length >= 3) return false;
var r = createResonant(template, level);
player.party.push(r);
if (player.dex.indexOf(template.id) === -1) {
player.dex.push(template.id);
}
return true;
}
// ── function: addResonantToDex ──
function addResonantToDex(template) {
if (player.dex.indexOf(template.id) === -1) {
player.dex.push(template.id);
}
}
// ── function: hasItem ──
function hasItem(itemId) {
return (player.inventory[itemId] || 0) > 0;
}
// ── function: useItem ──
function useItem(itemId) {
if (!hasItem(itemId)) return false;
player.inventory[itemId]--;
return true;
}
// ── function: gainShards ──
function gainShards(amount) {
player.shards += amount;
}
// ── function: canUnlockZone ──
function canUnlockZone(zoneId) {
if (player.unlockedZones.indexOf(zoneId) >= 0) return false;
var zoneIdx = ZONES.findIndex(function(z) { return z.id === zoneId; });
if (zoneIdx <= 0) return false;
var prevZone = ZONES[zoneIdx - 1];
var prevBoss = prevZone.nodes.find(function(n) { return n.type === 'boss'; });
return player.completedBosses.indexOf(prevBoss.bossId) >= 0;
}
// ── function: unlockZone ──
function unlockZone(zoneId) {
if (player.unlockedZones.indexOf(zoneId) >= 0) return false;
player.unlockedZones.push(zoneId);
return true;
}
// ── function: completeBoss ──
function completeBoss(bossId) {
if (player.completedBosses.indexOf(bossId) >= 0) return false;
player.completedBosses.push(bossId);
return true;
}
// ── var: battleState ──
var battleState = null;
// ── function: startBattle ──
function startBattle(wildResonantId, level, isBoss, bossId) {
var template = getResonantTemplate(wildResonantId);
if (!template) return;
var wild = createResonant(template, level);
wild.isBoss = isBoss || false;
wild.bossId = bossId || null;
if (isBoss) {
wild.currentHp = Math.floor(wild.maxHp * 1.2);
wild.maxHp = wild.currentHp;
}
var activeMon = player.party[player.activeIndex];
if (!activeMon) {
showNotification('No Resonants available!');
return;
}
battleState = {
playerMon: activeMon,
enemyMon: wild,
phase: 'menu',
turn: 'player',
isBoss: isBoss,
bossId: bossId,
animating: false,
wildId: wild.id,
wildLevel: level
};
showScreen('screen-battle');
renderBattle();
sfxSelect();
}
// ── function: renderBattle ──
function renderBattle() {
if (!battleState) return;
var pm = battleState.playerMon;
var em = battleState.enemyMon;
document.getElementById('player-name').textContent = pm.name;
document.getElementById('player-level').textContent = 'Lv.' + pm.level;
document.getElementById('enemy-name').textContent = em.name;
document.getElementById('enemy-level').textContent = 'Lv.' + em.level;
var playerHpPct = (pm.currentHp / pm.maxHp) * 100;
var enemyHpPct = (em.currentHp / em.maxHp) * 100;
var pHpBar = document.getElementById('player-hp-bar');
pHpBar.style.width = playerHpPct + '%';
pHpBar.className = 'h-full rounded-full transition-all duration-500 ' + (playerHpPct > 50 ? 'bg-green-500' : playerHpPct > 25 ? 'bg-yellow-500' : 'bg-red-500');
var eHpBar = document.getElementById('enemy-hp-bar');
eHpBar.style.width = enemyHpPct + '%';
eHpBar.className = 'h-full rounded-full transition-all duration-500 ' + (enemyHpPct > 50 ? 'bg-green-500' : enemyHpPct > 25 ? 'bg-yellow-500' : 'bg-red-500');
document.getElementById('player-hp-text').textContent = pm.currentHp + ' / ' + pm.maxHp;
document.getElementById('enemy-hp-text').textContent = em.currentHp + ' / ' + em.maxHp;
var xpNeeded = XP_TABLE[pm.level] || 9999;
var xpPct = (pm.xp / xpNeeded) * 100;
document.getElementById('player-xp-bar').style.width = Math.min(xpPct, 100) + '%';
document.getElementById('player-xp-text').textContent = pm.xp + ' / ' + xpNeeded + ' XP';
document.getElementById('player-sprite').innerHTML = generateSpriteSVG(pm);
document.getElementById('enemy-sprite').innerHTML = generateSpriteSVG(em);
var typeColor = TYPE_COLORS[pm.type] || '#ffffff';
document.getElementById('player-sprite').style.filter = 'drop-shadow(0 0 8px ' + typeColor + ')';
typeColor = TYPE_COLORS[em.type] || '#ffffff';
document.getElementById('enemy-sprite').style.filter = 'drop-shadow(0 0 8px ' + typeColor + ')';
renderStatusEffects();
renderBattleMenu();
document.getElementById('battle-message').textContent = '';
}
// ── function: renderStatusEffects ──
function renderStatusEffects() {
var pStatus = document.getElementById('player-status');
var eStatus = document.getElementById('enemy-status');
pStatus.innerHTML = '';
eStatus.innerHTML = '';
if (battleState.playerMon.status) {
var pTag = document.createElement('span');
pTag.className = 'px-2 py-0.5 rounded text-xs font-bold';
pTag.textContent = battleState.playerMon.status.toUpperCase();
pTag.style.background = TYPE_COLORS.bolt;
pTag.style.color = '#000';
pStatus.appendChild(pTag);
}
if (battleState.enemyMon.status) {
var eTag = document.createElement('span');
eTag.className = 'px-2 py-0.5 rounded text-xs font-bold';
eTag.textContent = battleState.enemyMon.status.toUpperCase();
eTag.style.background = TYPE_COLORS.bolt;
eTag.style.color = '#000';
eStatus.appendChild(eTag);
}
}
// ── function: renderBattleMenu ──
function renderBattleMenu() {
var menu = document.getElementById('battle-menu');
var moves = document.getElementById('battle-moves');
var prism = document.getElementById('battle-prism-select');
var exchange = document.getElementById('battle-exchange');
var inventory = document.getElementById('battle-inventory');
menu.classList.add('hidden');
moves.classList.add('hidden');
prism.classList.add('hidden');
exchange.classList.add('hidden');
inventory.classList.add('hidden');
if (battleState.phase === 'menu') {
menu.classList.remove('hidden');
menu.classList.add('grid');
menu.style.gridTemplateColumns = '1fr 1fr';
} else if (battleState.phase === 'moves') {
moves.classList.remove('hidden');
moves.classList.add('grid');
moves.style.gridTemplateColumns = '1fr 1fr';
moves.innerHTML = '';
battleState.playerMon.moves.forEach(function(move, idx) {
var btn = document.createElement('button');
btn.className = 'battle-btn px-3 py-2 rounded-xl bg-white/10 border border-white/20 text-white text-sm font-semibold hover:bg-violet-500/30 hover:border-violet-400/50 transition-all';
btn.style.borderColor = TYPE_COLORS[move.type] + '60';
btn.innerHTML = '<span style="color:' + TYPE_COLORS[move.type] + '">' + move.name + '</span><br><span class="text-[10px] text-slate-400">PWR ' + move.power + ' | ACC ' + move.accuracy + '%</span>';
btn.addEventListener('click', function() { executeMove(idx); });
btn.addEventListener('mouseenter', sfxHover);
moves.appendChild(btn);
});
var backBtn = document.createElement('button');
backBtn.className = 'battle-btn col-span-2 px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-400 text-sm font-semibold hover:bg-white/10 transition-all';
backBtn.textContent = '← Back';
backBtn.addEventListener('click', function() { battleState.phase = 'menu'; renderBattleMenu(); sfxSelect(); });
backBtn.addEventListener('mouseenter', sfxHover);
moves.appendChild(backBtn);
} else if (battleState.phase === 'prism') {
prism.classList.remove('hidden');
prism.classList.add('grid');
prism.style.gridTemplateColumns = '1fr 1fr';
document.getElementById('prism-basic-count').textContent = player.inventory['harmonizer'] || 0;
document.getElementById('prism-hyper-count').textContent = player.inventory['hyper'] || 0;
document.getElementById('prism-basic').onclick = function() { attemptCapture('harmonizer'); sfxSelect(); };
document.getElementById('prism-hyper').onclick = function() { attemptCapture('hyper'); sfxSelect(); };
document.getElementById('prism-back').onclick = function() { battleState.phase = 'menu'; renderBattleMenu(); sfxSelect(); };
} else if (battleState.phase === 'exchange') {
exchange.classList.remove('hidden');
exchange.classList.add('grid');
exchange.innerHTML = '';
player.party.forEach(function(mon, idx) {
if (idx === player.activeIndex) return;
if (mon.isFainted) return;
var btn = document.createElement('button');
btn.className = 'battle-btn col-span-1 px-3 py-2 rounded-xl bg-white/10 border border-white/20 text-white text-sm font-semibold hover:bg-green-500/30 hover:border-green-400/50 transition-all';
btn.innerHTML = mon.name + ' <span class="text-slate-400">Lv.' + mon.level + '</span>';
btn.addEventListener('click', function() { exchangePokemon(idx); sfxSelect(); });
btn.addEventListener('mouseenter', sfxHover);
exchange.appendChild(btn);
});
var backBtn = document.createElement('button');
backBtn.className = 'battle-btn col-span-2 px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-400 text-sm font-semibold hover:bg-white/10 transition-all';
backBtn.textContent = '← Back';
backBtn.addEventListener('click', function() { battleState.phase = 'menu'; renderBattleMenu(); sfxSelect(); });
backBtn.addEventListener('mouseenter', sfxHover);
exchange.appendChild(backBtn);
} else if (battleState.phase === 'inventory') {
inventory.classList.remove('hidden');
inventory.classList.add('grid');
inventory.innerHTML = '';
var vitals = player.inventory['restorer'] || 0;
var cleanser = player.inventory['cleanser'] || 0;
if (vitals > 0) {
var btn = document.createElement('button');
btn.className = 'battle-btn col-span-1 px-3 py-2 rounded-xl bg-white/10 border border-white/20 text-white text-sm font-semibold hover:bg-amber-500/30 transition-all';
btn.innerHTML = 'Core Restorer (×' + vitals + ')<br><span class="text-[10px] text-slate-400">Heals 50% HP</span>';
btn.addEventListener('click', function() { useItem('restorer'); useItemOnActive(); sfxSelect(); });
btn.addEventListener('mouseenter', sfxHover);
inventory.appendChild(btn);
}
if (cleanser > 0) {
var btn2 = document.createElement('button');
btn2.className = 'battle-btn col-span-1 px-3 py-2 rounded-xl bg-white/10 border border-white/20 text-white text-sm font-semibold hover:bg-amber-500/30 transition-all';
btn2.innerHTML = 'Resonance Cleanser (×' + cleanser + ')<br><span class="text-[10px] text-slate-400">Cures status</span>';
btn2.addEventListener('click', function() { useItem('cleanser'); cureStatus(); sfxSelect(); });
btn2.addEventListener('mouseenter', sfxHover);
inventory.appendChild(btn2);
}
var booster = player.inventory['prism-booster'] || 0;
if (booster > 0) {
var btn3 = document.createElement('button');
btn3.className = 'battle-btn col-span-1 px-3 py-2 rounded-xl bg-white/10 border border-white/20 text-white text-sm font-semibold hover:bg-amber-500/30 transition-all';
btn3.innerHTML = 'Prism Booster (×' + booster + ')<br><span class="text-[10px] text-slate-400">Boost capture rate</span>';
btn3.addEventListener('click', function() { useItem('prism-booster'); battleState.prismBoosted = true; battleState.phase = 'menu'; renderBattleMenu(); sfxSelect(); });
btn3.addEventListener('mouseenter', sfxHover);
inventory.appendChild(btn3);
}
var backBtn = document.createElement('button');
backBtn.className = 'battle-btn col-span-2 px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-400 text-sm font-semibold hover:bg-white/10 transition-all';
backBtn.textContent = '← Back';
backBtn.addEventListener('click', function() { battleState.phase = 'menu'; renderBattleMenu(); sfxSelect(); });
backBtn.addEventListener('mouseenter', sfxHover);
inventory.appendChild(backBtn);
}
}
// ── function: executeMove ──
function executeMove(moveIdx) {
if (battleState.animating || battleState.turn !== 'player') return;
battleState.animating = true;
battleState.phase = 'moves';
var move = battleState.playerMon.moves[moveIdx];
battleState.selectedMove = move;
battleState.selectedMoveIdx = moveIdx;
var result = calcDamage(battleState.playerMon, battleState.enemyMon, move);
battleState.enemyMon.currentHp = Math.max(0, battleState.enemyMon.currentHp - result.damage);
var msg = battleState.playerMon.name + ' used ' + move.name + '!';
if (result.isCrit) { msg += ' Critical hit!'; }
if (result.typeMod > 1) { msg += ' Super effective!'; sfxSuperEffective(); }
else if (result.typeMod < 1) { msg += ' Not very effective...'; }
else { sfxHit(); }
document.getElementById('battle-message').textContent = msg;
shakeElement('enemy-sprite');
flashElement('enemy-sprite', result.typeMod > 1 ? 'flash-red' : 'flash-white');
renderBattle();
setTimeout(function() {
if (battleState.enemyMon.currentHp <= 0) {
enemyFainted();
} else {
battleState.turn = 'enemy';
setTimeout(enemyTurn, 800);
}
}, 1000);
}
// ── function: enemyTurn ──
function enemyTurn() {
if (!battleState || battleState.enemyMon.currentHp <= 0) return;
var em = battleState.enemyMon;
var moves = em.moves;
if (!moves || moves.length === 0) {
battleState.turn = 'player';
battleState.animating = false;
renderBattleMenu();
return;
}
var move = moves[Math.floor(Math.random() * moves.length)];
var result = calcDamage(em, battleState.playerMon, move);
battleState.playerMon.currentHp = Math.max(0, battleState.playerMon.currentHp - result.damage);
var msg = em.name + ' used ' + move.name + '!';
if (result.isCrit) { msg += ' Critical hit!'; }
if (result.typeMod > 1) { msg += ' Super effective!'; sfxSuperEffective(); }
else if (result.typeMod < 1) { msg += ' Not very effective...'; }
else { sfxHit(); }
document.getElementById('battle-message').textContent = msg;
shakeElement('player-sprite');
flashElement('player-sprite', result.typeMod > 1 ? 'flash-red' : 'flash-white');
renderBattle();
setTimeout(function() {
if (battleState.playerMon.currentHp <= 0) {
playerMonFainted();
} else {
battleState.turn = 'player';
battleState.animating = false;
renderBattleMenu();
}
}, 1000);
}
// ── function: enemyFainted ──
function enemyFainted() {
var em = battleState.enemyMon;
var xpGained = Math.floor((em.level * 20) / 5);
battleState.playerMon.xp += xpGained;
document.getElementById('battle-message').textContent = em.name + ' fainted! Gained ' + xpGained + ' XP!';
checkLevelUp();
if (battleState.isBoss) {
completeBoss(battleState.bossId);
var bossData = BOSS_DIALOGS[battleState.bossId];
if (bossData) {
gainShards(bossData.prize);
setTimeout(function() {
document.getElementById('battle-message').textContent = bossData.name + ' has been defeated! +' + bossData.prize + ' Aether Shards!';
setTimeout(function() {
battleState = null;
showScreen('screen-overworld');
updateOverworld();
autoSave();
}, 2000);
}, 1500);
}
} else {
gainShards(Math.floor(em.level * 2) + Math.floor(Math.random() * 5));
setTimeout(function() {
battleState = null;
showScreen('screen-overworld');
updateOverworld();
autoSave();
}, 1500);
}
}
// ── function: playerMonFainted ──
function playerMonFainted() {
battleState.playerMon.isFainted = true;
document.getElementById('battle-message').textContent = battleState.playerMon.name + ' fainted!';
var hasActive = player.party.some(function(mon) { return !mon.isFainted; });
if (!hasActive) {
setTimeout(function() {
document.getElementById('battle-message').textContent = 'All Resonants fainted! Returning to last spring...';
setTimeout(function() {
healParty();
battleState = null;
showScreen('screen-overworld');
updateOverworld();
autoSave();
}, 2000);
}, 1500);
return;
}
setTimeout(function() {
for (var i = 0; i < player.party.length; i++) {
if (!player.party[i].isFainted) {
player.activeIndex = i;
battleState.playerMon = player.party[i];
document.getElementById('battle-message').textContent = player.activeIndex + '! Come out!';
renderBattle();
battleState.turn = 'player';
battleState.animating = false;
renderBattleMenu();
break;
}
}
}, 1500);
}
// ── function: checkLevelUp ──
function checkLevelUp() {
var mon = battleState.playerMon;
var xpNeeded = XP_TABLE[mon.level] || 9999;
while (mon.xp >= xpNeeded && mon.level < 30) {
mon.xp -= xpNeeded;
mon.level++;
var newStats = calculateStats(getResonantTemplate(mon.id), mon.level);
mon.maxHp = newStats.hp;
mon.atk = newStats.atk;
mon.def = newStats.def;
mon.spd = newStats.spd;
mon.currentHp = Math.min(mon.currentHp + 10, mon.maxHp);
var newMoves = getResonantTemplate(mon.id).moves.filter(function(m) { return m.unlockLevel === mon.level; });
newMoves.forEach(function(m) {
if (mon.moves.find(function(existing) { return existing.name === m.name; })) return;
mon.moves.push(m);
});
if (mon.evolveLevel && mon.level >= mon.evolveLevel) {
var newTemplate = RESONANTS_DB.find(function(r) { return r.id === mon.id && r.tier === mon.tier + 1; });
if (newTemplate) {
var oldName = mon.name;
mon.id = newTemplate.id;
mon.name = newTemplate.name;
mon.tier = newTemplate.tier;
mon.baseHp = newTemplate.baseHp;
mon.baseAtk = newTemplate.baseAtk;
mon.baseDef = newTemplate.baseDef;
mon.baseSpd = newTemplate.baseSpd;
mon.evolveLevel = newTemplate.evolveLevel;
mon.spriteParams = newTemplate.spriteParams;
mon.xp = 0;
mon.moves = newTemplate.moves.filter(function(m) { return m.unlockLevel <= mon.level; });
var evolvedStats = calculateStats(newTemplate, mon.level);
mon.maxHp = evolvedStats.hp;
mon.atk = evolvedStats.atk;
mon.def = evolvedStats.def;
mon.spd = evolvedStats.spd;
mon.currentHp = mon.maxHp;
sfxEvolve();
document.getElementById('battle-message').textContent = oldName + ' evolved into ' + mon.name + '!';
return;
}
}
sfxLevelUp();
xpNeeded = XP_TABLE[mon.level] || 9999;
}
}
// ── function: exchangePokemon ──
function exchangePokemon(idx) {
if (battleState.animating) return;
battleState.animating = true;
var oldMon = battleState.playerMon;
battleState.playerMon = player.party[idx];
player.activeIndex = idx;
document.getElementById('battle-message').textContent = 'Swapping with ' + battleState.playerMon.name + '!';
battleState.turn = 'player';
battleState.animating = false;
renderBattle();
}
// ── function: attemptCapture ──
function attemptCapture(prismId) {
if (battleState.animating || !hasItem(prismId)) return;
useItem(prismId);
battleState.animating = true;
battleState.phase = 'menu';
var em = battleState.enemyMon;
var hpRatio = em.currentHp / em.maxHp;
var captureRate = (1 - hpRatio) * 0.6 + 0.1;
if (prismId === 'hyper') captureRate *= 2;
if (battleState.prismBoosted) captureRate *= 1.5;
if (em.isBoss) captureRate *= 0.3;
var overlay = document.getElementById('capture-overlay');
overlay.classList.remove('hidden');
var text = document.getElementById('capture-text');
text.textContent = 'Capturing...';
setTimeout(function() {
if (Math.random() < captureRate) {
sfxCapture();
text.textContent = 'Capture Successful!';
addResonantToDex(getResonantTemplate(em.id));
addResonantToParty(getResonantTemplate(em.id), em.level);
player.totalCaptures++;
setTimeout(function() {
overlay.classList.add('hidden');
battleState = null;
showScreen('screen-overworld');
updateOverworld();
autoSave();
}, 1500);
} else {
sfxCaptureFail();
text.textContent = 'It broke free!';
setTimeout(function() {
overlay.classList.add('hidden');
battleState.animating = false;
battleState.phase = 'menu';
renderBattleMenu();
}, 1500);
}
}, 2000);
}
// ── function: useItemOnActive ──
function useItemOnActive() {
if (!battleState) return;
var mon = battleState.playerMon;
var healAmount = Math.floor(mon.maxHp * 0.5);
mon.currentHp = Math.min(mon.maxHp, mon.currentHp + healAmount);
sfxHeal();
document.getElementById('battle-message').textContent = mon.name + ' restored ' + healAmount + ' HP!';
renderBattle();
setTimeout(function() {
battleState.turn = 'enemy';
setTimeout(enemyTurn, 800);
}, 1000);
}
// ── function: cureStatus ──
function cureStatus() {
if (!battleState) return;
battleState.playerMon.status = null;
battleState.playerMon.statusTurns = 0;
sfxHeal();
document.getElementById('battle-message').textContent = battleState.playerMon.name + ' is cured!';
renderBattle();
setTimeout(function() {
battleState.turn = 'enemy';
setTimeout(enemyTurn, 800);
}, 1000);
}
// ── function: healParty ──
function healParty() {
player.party.forEach(function(mon) {
mon.currentHp = mon.maxHp;
mon.isFainted = false;
mon.status = null;
mon.statusTurns = 0;
});
}
// ── function: showScreen ──
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(function(s) { s.classList.add('hidden'); });
var screen = document.getElementById(screenId);
if (screen) screen.classList.remove('hidden');
}
// ── function: shakeElement ──
function shakeElement(id) {
var el = document.getElementById(id);
if (!el) return;
el.classList.remove('shake');
void el.offsetWidth;
el.classList.add('shake');
setTimeout(function() { el.classList.remove('shake'); }, 500);
}
// ── function: flashElement ──
function flashElement(id, className) {
var el = document.getElementById(id);
if (!el) return;
var flash = document.createElement('div');
flash.className = 'flash-overlay ' + className;
el.appendChild(flash);
setTimeout(function() { if (flash.parentNode) el.removeChild(flash); }, 400);
}
// ── function: showNotification ──
function showNotification(text) {
var notif = document.getElementById('notification');
notif.textContent = text;
notif.classList.remove('hidden');
setTimeout(function() { notif.classList.add('hidden'); }, 2000);
}
// ── function: autoSave ──
function autoSave() {
try { localStorage.setItem('aethelgard_save', JSON.stringify(player)); } catch(e) {}
}
// ── function: updateOverworld ──
function updateOverworld() {
var zone = ZONES.find(function(z) { return z.id === player.currentZone; });
if (!zone) return;
document.getElementById('overworld-shards').textContent = '◆ ' + player.shards;
var typeKey = zone.id === 'verdant' ? 'flora' : zone.id === 'grotto' ? 'hydro' : zone.id === 'crag' ? 'pyro' : 'cosmos';
document.getElementById('overworld-zone').textContent = zone.name;
document.getElementById('overworld-zone').style.color = TYPE_COLORS[typeKey];
renderMapNodes();
document.getElementById('overworld-info').textContent = zone.desc;
}
// ── function: renderMapNodes ──
function renderMapNodes() {
var zone = ZONES.find(function(z) { return z.id === player.currentZone; });
if (!zone) return;
var container = document.getElementById('map-nodes');
container.innerHTML = '';
container.style.background = zone.bg;
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 100 100');
svg.setAttribute('class', 'w-full h-full');
svg.style.position = 'absolute';
svg.style.inset = '0';
var pathD = zone.nodes.map(function(n) { return (n.x === zone.nodes[0].x ? 'M' : 'L') + n.x + ' ' + n.y; }).join(' ');
var pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathEl.setAttribute('d', pathD);
pathEl.setAttribute('fill', 'none');
pathEl.setAttribute('stroke', 'rgba(255,255,255,0.15)');
pathEl.setAttribute('stroke-width', '0.5');
pathEl.setAttribute('class', 'zone-path');
svg.appendChild(pathEl);
zone.nodes.forEach(function(node, idx) {
var isCompleted = (node.type === 'cache' && node.completed) || (node.type === 'boss' && player.completedBosses.indexOf(node.bossId) >= 0);
var isBoss = node.type === 'boss';
var bossDefeated = player.completedBosses.indexOf(node.bossId) >= 0;
var isLocked = isBoss && !bossDefeated;
var isCurrent = idx === 3;
var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('class', 'map-node' + (isCompleted ? ' completed' : isLocked ? ' locked' : isCurrent ? ' current' : ''));
g.setAttribute('data-node-type', node.type);
g.setAttribute('data-node-id', node.id);
g.setAttribute('transform', 'translate(' + node.x + ', ' + node.y + ')');
var color = '#ffffff';
if (node.type === 'wild') color = TYPE_COLORS[zone.id === 'verdant' ? 'flora' : zone.id === 'grotto' ? 'hydro' : zone.id === 'crag' ? 'pyro' : 'cosmos'];
else if (node.type === 'cache') color = '#fbbf24';
else if (node.type === 'spring') color = '#22c55e';
else if (node.type === 'boss') color = '#ef4444';
var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', '0'); circle.setAttribute('cy', '0'); circle.setAttribute('r', '4');
circle.setAttribute('fill', color);
circle.setAttribute('opacity', isCompleted ? '0.3' : isLocked ? '0.2' : '0.8');
circle.setAttribute('stroke', color); circle.setAttribute('stroke-width', '0.5');
g.appendChild(circle);
var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('class', 'node-label'); text.setAttribute('x', '0'); text.setAttribute('y', '8');
text.setAttribute('font-size', '3'); text.textContent = node.label;
g.appendChild(text);
if (isCurrent) {
var glow = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
glow.setAttribute('cx', '0'); glow.setAttribute('cy', '0'); glow.setAttribute('r', '5');
glow.setAttribute('fill', 'none'); glow.setAttribute('stroke', color); glow.setAttribute('stroke-width', '0.3');
glow.setAttribute('opacity', '0.5');
g.appendChild(glow);
}
if (!isCompleted && !isLocked) {
g.addEventListener('click', function() { interactNode(node); });
g.addEventListener('mouseenter', sfxHover);
}
svg.appendChild(g);
});
container.appendChild(svg);
}
// ── function: interactNode ──
function interactNode(node) {
if (node.type === 'wild') {
var wildId = node.encountered[Math.floor(Math.random() * node.encountered.length)];
var zone = ZONES.find(function(z) { return z.id === player.currentZone; });
var minL = zone.minLevel;
var maxL = Math.min(minL + 4, 20);
var level = minL + Math.floor(Math.random() * (maxL - minL + 1));
startBattle(wildId, level, false, null);
} else if (node.type === 'cache') {
if (node.completed) { showNotification('Already opened.'); return; }
var amount = node.amount[0] + Math.floor(Math.random() * (node.amount[1] - node.amount[0]));
gainShards(amount);
showNotification('Found ' + amount + ' Aether Shards!');
node.completed = true;
autoSave();
updateOverworld();
} else if (node.type === 'spring') {
healParty();
sfxHeal();
showNotification('All Resonants fully restored!');
autoSave();
} else if (node.type === 'boss') {
sfxBoss();
var bossData = BOSS_DIALOGS[node.bossId];
if (bossData) {
var dialogHtml = '<div class="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 max-w-md text-center"><h3 class="text-2xl font-bold text-white mb-2">' + bossData.name + '</h3><p class="text-violet-400 mb-4">' + bossData.title + '</p>';
bossData.dialog.forEach(function(line) { dialogHtml += '<p class="text-slate-300 mb-2 italic">"' + line + '"</p>'; });
dialogHtml += '<button id="boss-fight-btn" class="mt-4 px-8 py-3 rounded-xl bg-red-500/30 border border-red-400/50 text-white font-bold hover:bg-red-500/50 transition-all">\u2694 Face the Boss!</button></div>';
document.getElementById('map-overlay-content').innerHTML = dialogHtml;
document.getElementById('map-overlay').classList.remove('pointer-events-none');
document.getElementById('boss-fight-btn').addEventListener('click', function() {
var wildId = node.encountered[Math.floor(Math.random() * node.encountered.length)];
var zone = ZONES.find(function(z) { return z.id === player.currentZone; });
var level = zone.minLevel + 3;
document.getElementById('map-overlay-content').innerHTML = '';
document.getElementById('map-overlay').classList.add('pointer-events-none');
startBattle(wildId, level, true, node.bossId);
});
}
}
}
// ── function: renderPartyScreen ──
function renderPartyScreen() {
var list = document.getElementById('party-list');
list.innerHTML = '';
player.party.forEach(function(mon, idx) {
var card = document.createElement('div');
card.className = 'bg-white/5 backdrop-blur-md rounded-xl p-3 border border-white/10 flex items-center gap-3';
if (idx === player.activeIndex) card.classList.add('border-violet-400/50');
var typeColor = TYPE_COLORS[mon.type] || '#ffffff';
card.innerHTML = '<div class="w-12 h-12 flex-shrink-0">' + generateSpriteSVG(mon, 48) + '</div>';
card.innerHTML += '<div class="flex-1 min-w-0"><div class="flex justify-between items-center"><span class="text-white font-semibold text-sm">' + mon.name + '</span><span class="text-slate-400 text-xs">Lv.' + mon.level + '</span></div>';
card.innerHTML += '<div class="w-full h-2 bg-slate-700 rounded-full overflow-hidden mt-1"><div class="h-full ' + (mon.currentHp / mon.maxHp > 0.5 ? 'bg-green-500' : mon.currentHp / mon.maxHp > 0.25 ? 'bg-yellow-500' : 'bg-red-500') + '" style="width:' + (mon.currentHp / mon.maxHp * 100) + '%"></div></div>';
card.innerHTML += '<div class="text-[10px] text-slate-500 mt-0.5">' + mon.currentHp + '/' + mon.maxHp + ' HP</div></div>';
list.appendChild(card);
});
}
// ── function: renderDexScreen ──
function renderDexScreen() {
var list = document.getElementById('dex-list');
list.innerHTML = '';
player.dex.forEach(function(id) {
var template = getResonantTemplate(id);
if (!template) return;
var card = document.createElement('div');
card.className = 'bg-white/5 backdrop-blur-md rounded-xl p-3 border border-white/10 flex flex-col items-center';
card.innerHTML = '<div class="w-16 h-16">' + generateSpriteSVG(template, 64) + '</div>';
card.innerHTML += '<div class="text-white font-semibold text-xs mt-2">' + template.name + '</div>';
card.innerHTML += '<div class="text-[10px] px-2 py-0.5 rounded-full mt-1" style="background:' + TYPE_BG[template.type] + ';color:white">' + template.type.charAt(0).toUpperCase() + template.type.slice(1) + '</div>';
card.innerHTML += '<div class="text-[9px] text-slate-500 mt-1">Tier ' + template.tier + '</div>';
list.appendChild(card);
});
}
// ── function: renderShopScreen ──
function renderShopScreen() {
document.getElementById('shop-shards').textContent = player.shards;
var list = document.getElementById('shop-list');
list.innerHTML = '';
SHOP_ITEMS.forEach(function(item) {
var card = document.createElement('div');
card.className = 'bg-white/5 backdrop-blur-md rounded-xl p-4 border border-white/10 flex items-center gap-4';
var icon = item.type === 'prism-basic' ? '\uD83D\uDC8E' : item.type === 'prism-hyper' ? '\uD83D\uDC20' : item.type === 'vitals-elixir' ? '\u2764\uFE0F' : item.type === 'status-cleanser' ? '\u2728' : '\u26A1';
card.innerHTML = '<div class="w-12 h-12 rounded-lg flex items-center justify-center text-2xl" style="background:' + TYPE_BG.volt + '">' + icon + '</div>';
card.innerHTML += '<div class="flex-1"><div class="text-white font-semibold text-sm">' + item.name + '</div>';
card.innerHTML += '<div class="text-slate-400 text-xs">' + item.desc + '</div>';
card.innerHTML += '<div class="text-amber-400 text-xs mt-1">\u25C6 ' + item.cost + '</div></div>';
var buyBtn = document.createElement('button');
buyBtn.className = 'px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white text-sm font-semibold hover:bg-amber-500/20 hover:border-amber-400/50 transition-all';
buyBtn.textContent = 'Buy';
buyBtn.addEventListener('click', function() {
if (player.shards >= item.cost) {
player.shards -= item.cost;
player.inventory[item.type] = (player.inventory[item.type] || 0) + item.qty;
sfxSelect();
showNotification('Purchased ' + item.name + '!');
renderShopScreen();
autoSave();
} else {
showNotification('Not enough Aether Shards!');
sfxCaptureFail();
}
});
buyBtn.addEventListener('mouseenter', sfxHover);
card.appendChild(buyBtn);
list.appendChild(card);
});
}
// ── main ──
var gameInitialized = false;
function initGame() {
initAudio();
if (hasSave()) {
loadGame();
document.getElementById('btn-continue').classList.remove('hidden');
} else {
document.getElementById('btn-continue').classList.add('hidden');
}
document.getElementById('btn-new-game').addEventListener('click', function() {
sfxSelect();
startNewGame();
});
document.getElementById('btn-continue').addEventListener('click', function() {
sfxSelect();
loadGame();
showScreen('screen-overworld');
updateOverworld();
});
document.getElementById('btn-reset').addEventListener('click', function() {
sfxSelect();
resetProgress();
document.getElementById('btn-continue').classList.add('hidden');
});
document.getElementById('btn-party').addEventListener('click', function() {
sfxSelect();
showScreen('screen-party');
renderPartyScreen();
});
document.getElementById('btn-party-close').addEventListener('click', function() {
sfxSelect();
showScreen('screen-overworld');
});
document.getElementById('btn-dex').addEventListener('click', function() {
sfxSelect();
showScreen('screen-dex');
renderDexScreen();
});
document.getElementById('btn-dex-close').addEventListener('click', function() {
sfxSelect();
showScreen('screen-overworld');
});
document.getElementById('btn-shop').addEventListener('click', function() {
sfxSelect();
showScreen('screen-shop');
renderShopScreen();
});
document.getElementById('btn-shop-close').addEventListener('click', function() {
sfxSelect();
showScreen('screen-overworld');
});
document.getElementById('btn-menu').addEventListener('click', function() {
sfxSelect();
showScreen('screen-conductor-menu');
});
document.getElementById('btn-menu-save').addEventListener('click', function() {
sfxSelect();
saveGame();
});
document.getElementById('btn-menu-leave').addEventListener('click', function() {
sfxSelect();
showScreen('screen-overworld');
});
document.getElementById('btn-menu-quit').addEventListener('click', function() {
sfxSelect();
saveGame();
showScreen('screen-menu');
});
document.getElementById('action-attack').addEventListener('click', function() {
sfxSelect();
battleState.phase = 'moves';
renderBattleMenu();
});
document.getElementById('action-prism').addEventListener('click', function() {
sfxSelect();
battleState.phase = 'prism';
renderBattleMenu();
});
document.getElementById('action-exchange').addEventListener('click', function() {
sfxSelect();
battleState.phase = 'exchange';
renderBattleMenu();
});
document.getElementById('action-inventory').addEventListener('click', function() {
sfxSelect();
battleState.phase = 'inventory';
renderBattleMenu();
});
startLoadingSequence();
}
function startNewGame() {
player = defaultPlayer();
player.name = 'Conductor';
player.started = true;
var starterChoice = ['seedling', 'droplet', 'emberkit'];
var starterId = starterChoice[Math.floor(Math.random() * starterChoice.length)];
var starterTemplate = getResonantTemplate(starterId);
addResonantToParty(starterTemplate, 5);
addResonantToParty(getResonantTemplate('seedling'), 5);
addResonantToParty(getResonantTemplate('emberkit'), 5);
player.activeIndex = 0;
player.currentZone = 'verdant';
player.unlockedZones = ['verdant'];
saveGame();
showScreen('screen-overworld');
updateOverworld();
showNotification('Welcome, Conductor! Your journey begins!');
}
function startLoadingSequence() {
var bar = document.getElementById('loading-bar');
var progress = 0;
var interval = setInterval(function() {
progress += Math.random() * 15 + 5;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
setTimeout(function() {
showScreen('screen-menu');
}, 300);
}
bar.style.width = progress + '%';
}, 200);
}
document.addEventListener('DOMContentLoaded', function() {
initGame();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"865a4af5-e1f7-496f-b8bc-64e9e9d0d6ee","tokensIn":3320953,"tokensOut":75474,"tokensTotal":3396427,"turns":52,"toolCalls":51,"failedToolCalls":6,"timestamp":"2026-07-21T00:16:26.795Z"} -->