Aethelgard
thinkingcap qwen3.6 27b · 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 73,489 bytes · SHA-256 be03f0151c4e
<!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@300;400;500;600;700&family=Press+Start+2P&display=swap">
<style>
/* ── css: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: 'Inter', sans-serif; }
body { background: linear-gradient(135deg, #0f0a1e 0%, #1a1040 30%, #0d1b2a 70%, #0a0618 100%); color: #e2e8f0; }
.game-screen { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; transition: opacity 0.4s ease, transform 0.4s ease; z-index: 1; }
.game-screen.hidden { opacity: 0; pointer-events: none; transform: scale(0.95); }
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 100; transition: opacity 0.3s ease; backdrop-filter: blur(4px); }
.overlay.hidden { opacity: 0; pointer-events: none; }
.overlay-content { max-width: 420px; padding: 2rem; border-radius: 16px; text-align: center; }
.particle-layer { position: fixed; inset: 0; pointer-events: none; z-index: 50; }
.hidden { display: none !important; }
.text-center { text-align: center; }
.mt-4 { margin-top: 1rem; }
/* Glass Panel */
.glass-panel { background: rgba(30, 27, 56, 0.6); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(139, 92, 246, 0.2); border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05); }
/* Buttons */
.glass-btn { padding: 12px 28px; border-radius: 12px; font-family: 'Inter', sans-serif; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.2s ease; border: 1px solid rgba(139, 92, 246, 0.3); background: rgba(88, 28, 135, 0.4); color: #e2e8f0; backdrop-filter: blur(8px); letter-spacing: 0.5px; }
.glass-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4); border-color: rgba(139, 92, 246, 0.6); }
.glass-btn:active { transform: translateY(0); }
.btn-primary { background: linear-gradient(135deg, rgba(139, 92, 246, 0.6), rgba(76, 29, 149, 0.8)); border-color: rgba(167, 139, 250, 0.5); }
.btn-secondary { background: rgba(51, 65, 85, 0.5); border-color: rgba(100, 116, 139, 0.4); }
.btn-danger { background: rgba(220, 38, 38, 0.4); border-color: rgba(239, 68, 68, 0.5); }
.btn-sm { padding: 6px 14px; font-size: 12px; border-radius: 8px; }
/* Menu */
.menu-content { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 1.5rem; }
.game-title { font-family: 'Press Start 2P', monospace; font-size: clamp(24px, 5vw, 48px); background: linear-gradient(135deg, #a78bfa, #60a5fa, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-shadow: none; filter: drop-shadow(0 0 20px rgba(139, 92, 246, 0.5)); margin-bottom: 0.5rem; }
.game-subtitle { font-size: 18px; color: #a78bfa; letter-spacing: 4px; text-transform: uppercase; opacity: 0.8; }
/* Map */
#screen-map { flex-direction: column; padding: 16px; }
.map-header { width: 100%; display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-shrink: 0; }
.zone-name { font-family: 'Press Start 2P', monospace; font-size: clamp(10px, 2vw, 16px); color: #a78bfa; }
.header-stats { display: flex; gap: 8px; align-items: center; }
.stat-badge { padding: 4px 12px; border-radius: 20px; background: rgba(30, 27, 56, 0.6); backdrop-filter: blur(8px); border: 1px solid rgba(139, 92, 246, 0.2); font-size: 13px; }
.map-area { flex: 1; width: 100%; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
/* Map Nodes */
.zone-section { width: 100%; max-width: 600px; margin-bottom: 12px; }
.zone-label { font-size: 13px; color: #94a3b8; margin-bottom: 8px; padding-left: 8px; text-transform: uppercase; letter-spacing: 2px; }
.node-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.map-node { width: 56px; height: 56px; border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 22px; cursor: pointer; transition: all 0.2s ease; position: relative; background: rgba(30, 27, 56, 0.8); border: 2px solid rgba(100, 116, 139, 0.3); }
.map-node:hover:not(.locked) { transform: scale(1.1); box-shadow: 0 0 20px rgba(139, 92, 246, 0.5); border-color: #a78bfa; }
.map-node.locked { opacity: 0.3; cursor: not-allowed; filter: grayscale(1); }
.map-node.completed { border-color: rgba(74, 222, 128, 0.5); background: rgba(74, 222, 128, 0.1); }
.map-node.boss { width: 64px; height: 64px; border-color: rgba(239, 68, 68, 0.5); background: rgba(239, 68, 68, 0.1); font-size: 26px; }
.map-node.boss.completed { border-color: rgba(74, 222, 128, 0.5); background: rgba(74, 222, 128, 0.15); }
/* Battle */
#screen-battle { flex-direction: column; padding: 16px; gap: 8px; }
.battle-arena { width: 100%; max-width: 700px; display: flex; flex-direction: column; height: 100%; max-height: calc(100vh - 32px); }
.enemy-zone, .player-zone { flex: 1; display: flex; align-items: center; justify-content: center; min-height: 0; }
.resonant-card { width: 100%; max-width: 400px; padding: 16px; border-radius: 16px; }
.sprite-container { width: 80px; height: 80px; margin: 0 auto 8px; display: flex; align-items: center; justify-content: center; }
.sprite-container svg { width: 100%; height: 100%; }
.info-panel { display: flex; gap: 6px; align-items: center; justify-content: center; margin-bottom: 6px; flex-wrap: wrap; }
.resonant-name { font-weight: 700; font-size: 15px; }
.level-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: rgba(139, 92, 246, 0.3); color: #c4b5fd; }
.type-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
.hp-bar-container { width: 100%; margin-top: 4px; }
.hp-bar { height: 10px; background: rgba(0,0,0,0.4); border-radius: 5px; overflow: hidden; }
.hp-fill { height: 100%; border-radius: 5px; transition: width 0.5s ease, background-color 0.3s ease; }
.hp-text { font-size: 11px; color: #94a3b8; text-align: right; margin-top: 2px; }
.status-row { display: flex; gap: 4px; justify-content: center; margin-top: 4px; min-height: 20px; }
.status-badge { font-size: 10px; padding: 2px 6px; border-radius: 8px; background: rgba(251, 191, 36, 0.2); color: #fbbf24; border: 1px solid rgba(251, 191, 36, 0.3); }
/* Battle UI */
.battle-ui { flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; padding-top: 8px; }
.battle-log { max-height: 80px; overflow-y: auto; padding: 12px; font-size: 13px; line-height: 1.5; border-radius: 12px; color: #cbd5e1; }
.battle-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.battle-actions .glass-btn { padding: 10px 8px; font-size: 12px; text-align: center; min-height: 44px; display: flex; align-items: center; justify-content: center; }
/* Shop */
.shop-container, .party-container, .dex-container { width: 90%; max-width: 560px; padding: 24px; max-height: 80vh; overflow-y: auto; }
.panel-title { font-family: 'Press Start 2P', monospace; font-size: 16px; color: #a78bfa; margin-bottom: 16px; text-align: center; }
.shard-display { text-align: center; color: #94a3b8; margin-bottom: 16px; font-size: 14px; }
.shop-grid, .party-grid, .dex-grid { display: flex; flex-direction: column; gap: 8px; }
/* Shop Items */
.shop-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-radius: 12px; background: rgba(30, 27, 56, 0.4); border: 1px solid rgba(100, 116, 139, 0.2); transition: all 0.2s ease; }
.shop-item:hover { border-color: rgba(139, 92, 246, 0.4); background: rgba(88, 28, 135, 0.2); }
.shop-item-info { flex: 1; }
.shop-item-name { font-weight: 600; font-size: 14px; }
.shop-item-desc { font-size: 12px; color: #94a3b8; margin-top: 2px; }
.shop-item-cost { font-size: 13px; color: #fbbf24; white-space: nowrap; margin-left: 12px; }
/* Party Cards */
.party-card { padding: 16px; border-radius: 12px; background: rgba(30, 27, 56, 0.4); border: 1px solid rgba(100, 116, 139, 0.2); display: flex; gap: 12px; align-items: center; }
.party-card-sprite { width: 48px; height: 48px; flex-shrink: 0; }
.party-card-info { flex: 1; }
.party-card-stats { font-size: 11px; color: #94a3b8; margin-top: 4px; }
/* Dex Cards */
.dex-entry { padding: 12px; border-radius: 12px; background: rgba(30, 27, 56, 0.4); border: 1px solid rgba(100, 116, 139, 0.2); display: flex; gap: 10px; align-items: center; }
.dex-entry.discovered { border-color: rgba(139, 92, 246, 0.3); }
.dex-entry.undiscovered { opacity: 0.5; filter: grayscale(0.8); }
.dex-sprite { width: 40px; height: 40px; flex-shrink: 0; }
/* Animations */
@keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-6px)} 75%{transform:translateX(6px)} }
.shake { animation: shake 0.3s ease-in-out; }
@keyframes flash-white { 0%{filter:brightness(1)} 50%{filter:brightness(2.5)} 100%{filter:brightness(1)} }
.flash-hit { animation: flash-white 0.2s ease; }
@keyframes bounce-in { 0%{transform:scale(0);opacity:0} 60%{transform:scale(1.1)} 100%{transform:scale(1);opacity:1} }
.bounce-in { animation: bounce-in 0.5s ease; }
@keyframes float-up { 0%{transform:translateY(0) scale(1);opacity:1} 100%{transform:translateY(-40px) scale(0.5);opacity:0} }
.float-damage { animation: float-up 0.8s ease forwards; position: absolute; font-weight: bold; pointer-events: none; z-index: 60; }
@keyframes pulse-glow { 0%,100%{box-shadow:0 0 5px rgba(139,92,246,0.3)} 50%{box-shadow:0 0 20px rgba(139,92,246,0.6)} }
.pulse-glow { animation: pulse-glow 2s ease-in-out infinite; }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(139, 92, 246, 0.3); border-radius: 3px; }
/* ── css: element-colors ── */
/* Element Type Colors */
.type-pyro { background: rgba(239, 68, 68, 0.2); color: #fca5a5; border-color: rgba(239, 68, 68, 0.4); }
.type-hydro { background: rgba(59, 130, 246, 0.2); color: #93c5fd; border-color: rgba(59, 130, 246, 0.4); }
.type-flora { background: rgba(34, 197, 94, 0.2); color: #86efac; border-color: rgba(34, 197, 94, 0.4); }
.type-zephyr { background: rgba(168, 85, 247, 0.2); color: #d8b4fe; border-color: rgba(168, 85, 247, 0.4); }
.type-earth { background: rgba(234, 179, 8, 0.2); color: #fde047; border-color: rgba(234, 179, 8, 0.4); }
.type-volt { background: rgba(6, 182, 212, 0.2); color: #a5f3fc; border-color: rgba(6, 182, 212, 0.4); }
.type-cosmos { background: rgba(167, 139, 250, 0.2); color: #c4b5fd; border-color: rgba(167, 139, 250, 0.4); }
.glow-pyro { box-shadow: 0 0 15px rgba(239, 68, 68, 0.4); }
.glow-hydro { box-shadow: 0 0 15px rgba(59, 130, 246, 0.4); }
.glow-flora { box-shadow: 0 0 15px rgba(34, 197, 94, 0.4); }
.glow-zephyr { box-shadow: 0 0 15px rgba(168, 85, 247, 0.4); }
.glow-earth { box-shadow: 0 0 15px rgba(234, 179, 8, 0.4); }
.glow-volt { box-shadow: 0 0 15px rgba(6, 182, 212, 0.4); }
.hp-high { background: linear-gradient(90deg, #22c55e, #4ade80); }
.hp-mid { background: linear-gradient(90deg, #eab308, #facc15); }
.hp-low { background: linear-gradient(90deg, #ef4444, #f87171); }
/* Node type colors */
.node-wild { border-color: rgba(34, 197, 94, 0.4); }
.node-cache { border-color: rgba(251, 191, 36, 0.4); background: rgba(251, 191, 36, 0.08); }
.node-conductor { border-color: rgba(239, 68, 68, 0.4); background: rgba(239, 68, 68, 0.08); }
.node-spring { border-color: rgba(59, 130, 246, 0.4); background: rgba(59, 130, 246, 0.08); }
/* Battle move buttons */
.move-btn-pyro { border-color: rgba(239, 68, 68, 0.5) !important; }
.move-btn-hydro { border-color: rgba(59, 130, 246, 0.5) !important; }
.move-btn-flora { border-color: rgba(34, 197, 94, 0.5) !important; }
.move-btn-zephyr { border-color: rgba(168, 85, 247, 0.5) !important; }
.move-btn-earth { border-color: rgba(234, 179, 8, 0.5) !important; }
.move-btn-volt { border-color: rgba(6, 182, 212, 0.5) !important; }
</style>
</head>
<body>
<!-- MAIN MENU -->
<div id="screen-menu" class="game-screen">
<div class="menu-content">
<h1 class="game-title">AETHELGARD</h1>
<p class="game-subtitle">Resonant Chronicles</p>
<button id="btn-new-game" class="glass-btn btn-primary">New Game</button>
<button id="btn-continue" class="glass-btn btn-secondary hidden">Continue</button>
<button id="btn-reset" class="glass-btn btn-danger hidden">Reset Progress</button>
</div>
</div>
<!-- OVERWORLD MAP -->
<div id="screen-map" class="game-screen hidden">
<div class="map-header">
<h2 id="zone-title" class="zone-name"></h2>
<div class="header-stats">
<span id="shard-count" class="stat-badge">💎 0</span>
<button id="btn-shop" class="glass-btn btn-sm">🏪 Shop</button>
<button id="btn-party" class="glass-btn btn-sm">👥 Party</button>
<button id="btn-dex" class="glass-btn btn-sm">📖 Dex</button>
</div>
</div>
<div id="map-container" class="map-area"></div>
</div>
<!-- BATTLE SCREEN -->
<div id="screen-battle" class="game-screen hidden">
<div class="battle-arena">
<div class="enemy-zone">
<div class="resonant-card enemy-card glass-panel">
<div id="enemy-sprite" class="sprite-container"></div>
<div class="info-panel">
<span id="enemy-name" class="resonant-name"></span>
<span id="enemy-level" class="level-badge"></span>
<span id="enemy-type" class="type-badge"></span>
</div>
<div class="hp-bar-container">
<div id="enemy-hp-bar" class="hp-bar"><div id="enemy-hp-fill" class="hp-fill"></div></div>
<span id="enemy-hp-text" class="hp-text"></span>
</div>
</div>
</div>
<div class="player-zone">
<div class="resonant-card player-card glass-panel">
<div id="player-sprite" class="sprite-container"></div>
<div class="info-panel">
<span id="player-res-name" class="resonant-name"></span>
<span id="player-res-level" class="level-badge"></span>
<span id="player-res-type" class="type-badge"></span>
</div>
<div class="hp-bar-container">
<div id="player-hp-bar" class="hp-bar"><div id="player-hp-fill" class="hp-fill"></div></div>
<span id="player-hp-text" class="hp-text"></span>
</div>
<div id="status-effects" class="status-row"></div>
</div>
</div>
<div class="battle-ui">
<div id="battle-log" class="battle-log glass-panel"></div>
<div id="battle-menu" class="battle-actions"></div>
</div>
</div>
</div>
<!-- SHOP SCREEN -->
<div id="screen-shop" class="game-screen hidden">
<div class="shop-container glass-panel">
<h2 class="panel-title">Camp Shop</h2>
<p class="shard-display">💎 Aether Shards: <span id="shop-shards">0</span></p>
<div id="shop-items" class="shop-grid"></div>
<button id="btn-shop-back" class="glass-btn btn-secondary mt-4">← Back to Map</button>
</div>
</div>
<!-- PARTY SCREEN -->
<div id="screen-party" class="game-screen hidden">
<div class="party-container glass-panel">
<h2 class="panel-title">Your Party</h2>
<div id="party-list" class="party-grid"></div>
<button id="btn-party-back" class="glass-btn btn-secondary mt-4">← Back to Map</button>
</div>
</div>
<!-- DEX SCREEN -->
<div id="screen-dex" class="game-screen hidden">
<div class="dex-container glass-panel">
<h2 class="panel-title">Resonant Codex</h2>
<div id="dex-list" class="dex-grid"></div>
<button id="btn-dex-back" class="glass-btn btn-secondary mt-4">← Back to Map</button>
</div>
</div>
<!-- OVERLAYS -->
<div id="capture-overlay" class="overlay hidden"><div class="overlay-content glass-panel text-center"><h2 id="capture-title"></h2><p id="capture-message"></p><button id="btn-capture-ok" class="glass-btn btn-primary mt-4">OK</button></div></div>
<div id="levelup-overlay" class="overlay hidden"><div class="overlay-content glass-panel text-center"><h2 id="levelup-title"></h2><p id="levelup-message"></p><button id="btn-levelup-ok" class="glass-btn btn-primary mt-4">OK</button></div></div>
<div id="evolution-overlay" class="overlay hidden"><div class="overlay-content glass-panel text-center"><h2 id="evo-title"></h2><p id="evo-message"></p><button id="btn-evo-ok" class="glass-btn btn-primary mt-4">OK</button></div></div>
<div id="gameover-overlay" class="overlay hidden"><div class="overlay-content glass-panel text-center"><h2 id="go-title"></h2><p id="go-message"></p><button id="btn-go-ok" class="glass-btn btn-primary mt-4">Continue</button></div></div>
<canvas id="particle-canvas" class="particle-layer"></canvas>
<script>
'use strict';
// ── class: SoundManager ──
class SoundManager {
constructor() {
this.ctx = null;
this.enabled = true;
}
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.ctx.state === 'suspended') this.ctx.resume();
}
play(type) {
if (!this.enabled || !this.ctx) return;
const t = this.ctx.currentTime;
switch(type) {
case 'hover': this._tone(800, 0.05, 'sine', 0.1); break;
case 'select': this._tone(600, 0.08, 'square', 0.12); setTimeout(() => this._tone(900, 0.08, 'square', 0.1), 60); break;
case 'hit-physical': this._noise(0.1, 0.15); this._tone(150, 0.12, 'sawtooth', 0.15); break;
case 'hit-elemental': this._tone(300, 0.15, 'square', 0.18); setTimeout(() => this._tone(450, 0.15, 'square', 0.15), 50); setTimeout(() => this._tone(600, 0.2, 'sine', 0.12), 100); break;
case 'supereffective': this._tone(400, 0.1, 'square', 0.2); setTimeout(() => this._tone(550, 0.1, 'square', 0.18), 60); setTimeout(() => this._tone(700, 0.15, 'sine', 0.15), 120); setTimeout(() => this._tone(900, 0.2, 'sine', 0.12), 180); break;
case 'levelup': [400,500,600,700,800].forEach((f,i) => setTimeout(() => this._tone(f, 0.15, 'sine', 0.15), i*80)); break;
case 'capture': [300,400,500,600,700,800].forEach((f,i) => setTimeout(() => this._tone(f, 0.12, 'sine', 0.12), i*60)); break;
case 'capture-fail': [400,350,300,250].forEach((f,i) => setTimeout(() => this._tone(f, 0.15, 'square', 0.1), i*100)); break;
case 'evolution': [300,400,500,600,700,800,900].forEach((f,i) => setTimeout(() => this._tone(f, 0.2, 'sine', 0.15), i*70)); break;
case 'victory': [523,659,784,1047].forEach((f,i) => setTimeout(() => this._tone(f, 0.2, 'square', 0.12), i*120)); break;
case 'defeat': [400,350,300,200].forEach((f,i) => setTimeout(() => this._tone(f, 0.25, 'sine', 0.12), i*150)); break;
}
}
_tone(freq, dur, wave, vol) {
const o = this.ctx.createOscillator();
const g = this.ctx.createGain();
o.type = wave; o.frequency.value = freq;
g.gain.setValueAtTime(vol, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + dur);
o.connect(g); g.connect(this.ctx.destination);
o.start(); o.stop(this.ctx.currentTime + dur);
}
_noise(dur, vol) {
const buf = this.ctx.createBuffer(1, this.ctx.sampleRate * dur, this.ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
const src = this.ctx.createBufferSource();
const g = this.ctx.createGain();
src.buffer = buf;
g.gain.setValueAtTime(vol, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + dur);
src.connect(g); g.connect(this.ctx.destination);
src.start();
}
}
// ── const: ELEMENTS ──
const ELEMENTS = { pyro:'Pyro', hydro:'Hydro', flora:'Flora', zephyr:'Zephyr', earth:'Earth', volt:'Volt', cosmos:'Cosmos' };
// ── const: TYPE_CHART ──
const TYPE_CHART = { pyro:{strong:['flora'],weak:['hydro']}, hydro:{strong:['pyro'],weak:['flora']}, flora:{strong:['hydro'],weak:['pyro']}, zephyr:{strong:['earth'],weak:['volt']}, earth:{strong:['volt'],weak:['zephyr']}, volt:{strong:['zephyr'],weak:['earth']}, cosmos:{strong:[],weak:[]} };
// ── function: typeMod ──
function typeMod(atkType, defType) { if(TYPE_CHART[atkType] && TYPE_CHART[atkType].strong.includes(defType)) return 2; if(TYPE_CHART[defType] && TYPE_CHART[defType].strong.includes(atkType)) return 0.5; return 1; }
// ── const: MOVES ──
const MOVES = [
{name:'Scratch',power:40,accuracy:100,type:'cosmos'},
{name:'Ember',power:40,accuracy:100,type:'pyro'},
{name:'Water Shot',power:40,accuracy:100,type:'hydro'},
{name:'Vine Whip',power:45,accuracy:100,type:'flora'},
{name:'Gust',power:35,accuracy:100,type:'zephyr'},
{name:'Tackle',power:40,accuracy:100,type:'earth'},
{name:'Spark',power:40,accuracy:100,type:'volt'},
{name:'Flame Burst',power:65,accuracy:95,type:'pyro'},
{name:'Aqua Jet',power:60,accuracy:95,type:'hydro'},
{name:'Razor Leaf',power:60,accuracy:95,type:'flora'},
{name:'Air Slash',power:60,accuracy:95,type:'zephyr'},
{name:'Rock Throw',power:60,accuracy:95,type:'earth'},
{name:'Thunder Shock',power:60,accuracy:95,type:'volt'},
{name:'Inferno',power:90,accuracy:85,type:'pyro'},
{name:'Tidal Wave',power:85,accuracy:85,type:'hydro'},
{name:'Solar Beam',power:85,accuracy:85,type:'flora'},
{name:'Hurricane',power:85,accuracy:85,type:'zephyr'},
{name:'Earthquake',power:90,accuracy:85,type:'earth'},
{name:'Thunderbolt',power:85,accuracy:85,type:'volt'},
{name:'Dragon Claw',power:75,accuracy:100,type:'cosmos'},
{name:'Psybeam',power:65,accuracy:100,type:'cosmos'},
{name:'Quick Attack',power:40,accuracy:100,type:'cosmos'},
{name:'Body Slam',power:70,accuracy:95,type:'cosmos'},
];
// ── const: RESONANTS ──
const RESONANTS = [
// PYRO LINE
{id:1,name:'Emberkit',type:'pyro',baseHp:45,baseAtk:52,baseDef:38,baseSpd:48,evoLevel:8,evoId:2,sprite:{body:'#ef4444',accent:'#f97316',shape:'fox'},moves:[{move:'Scratch',level:1},{move:'Ember',level:1},{move:'Flame Burst',level:5},{move:'Inferno',level:12}]},
{id:2,name:'Pyrowolf',type:'pyro',baseHp:68,baseAtk:70,baseDef:55,baseSpd:62,evoLevel:15,evoId:3,sprite:{body:'#dc2626',accent:'#fb923c',shape:'wolf'},moves:[{move:'Scratch',level:1},{move:'Ember',level:1},{move:'Flame Burst',level:1},{move:'Inferno',level:8}]},
{id:3,name:'Ignisaur',type:'pyro',baseHp:95,baseAtk:92,baseDef:75,baseSpd:80,sprite:{body:'#b91c1c',accent:'#fbbf24',shape:'dragon'},moves:[{move:'Scratch',level:1},{move:'Ember',level:1},{move:'Flame Burst',level:1},{move:'Inferno',level:1}]},
// HYDRO LINE
{id:4,name:'Droplet',type:'hydro',baseHp:50,baseAtk:42,baseDef:45,baseSpd:44,evoLevel:8,evoId:5,sprite:{body:'#3b82f6',accent:'#93c5fd',shape:'blob'},moves:[{move:'Scratch',level:1},{move:'Water Shot',level:1},{move:'Aqua Jet',level:5},{move:'Tidal Wave',level:12}]},
{id:5,name:'Geoxus',type:'hydro',baseHp:74,baseAtk:60,baseDef:62,baseSpd:58,evoLevel:15,evoId:6,sprite:{body:'#2563eb',accent:'#bfdbfe',shape:'serpent'},moves:[{move:'Scratch',level:1},{move:'Water Shot',level:1},{move:'Aqua Jet',level:1},{move:'Tidal Wave',level:8}]},
{id:6,name:'Neptunian',type:'hydro',baseHp:100,baseAtk:82,baseDef:85,baseSpd:72,sprite:{body:'#1d4ed8',accent:'#60a5fa',shape:'leviathan'},moves:[{move:'Scratch',level:1},{move:'Water Shot',level:1},{move:'Aqua Jet',level:1},{move:'Tidal Wave',level:1}]},
// FLORA LINE
{id:7,name:'Seedling',type:'flora',baseHp:52,baseAtk:40,baseDef:48,baseSpd:38,evoLevel:8,evoId:8,sprite:{body:'#22c55e',accent:'#86efac',shape:'plant'},moves:[{move:'Scratch',level:1},{move:'Vine Whip',level:1},{move:'Razor Leaf',level:5},{move:'Solar Beam',level:12}]},
{id:8,name:'Thornback',type:'flora',baseHp:76,baseAtk:58,baseDef:65,baseSpd:50,evoLevel:15,evoId:9,sprite:{body:'#16a34a',accent:'#bbf7d0',shape:'armored'},moves:[{move:'Scratch',level:1},{move:'Vine Whip',level:1},{move:'Razor Leaf',level:1},{move:'Solar Beam',level:8}]},
{id:9,name:'Bloomgoliath',type:'flora',baseHp:105,baseAtk:78,baseDef:92,baseSpd:65,sprite:{body:'#15803d',accent:'#4ade80',shape:'tree'},moves:[{move:'Scratch',level:1},{move:'Vine Whip',level:1},{move:'Razor Leaf',level:1},{move:'Solar Beam',level:1}]},
// ZEPHYR LINE
{id:10,name:'Gusty',type:'zephyr',baseHp:42,baseAtk:48,baseDef:35,baseSpd:56,evoLevel:8,evoId:11,sprite:{body:'#a855f7',accent:'#e9d5ff',shape:'bird'},moves:[{move:'Scratch',level:1},{move:'Gust',level:1},{move:'Air Slash',level:5},{move:'Hurricane',level:12}]},
{id:11,name:'Slipstream',type:'zephyr',baseHp:64,baseAtk:66,baseDef:50,baseSpd:72,evoLevel:15,evoId:12,sprite:{body:'#9333ea',accent:'#c084fc',shape:'falcon'},moves:[{move:'Scratch',level:1},{move:'Gust',level:1},{move:'Air Slash',level:1},{move:'Hurricane',level:8}]},
{id:12,name:'Zephyrus',type:'zephyr',baseHp:90,baseAtk:88,baseDef:68,baseSpd:95,sprite:{body:'#7e22ce',accent:'#a855f7',shape:'phoenix'},moves:[{move:'Scratch',level:1},{move:'Gust',level:1},{move:'Air Slash',level:1},{move:'Hurricane',level:1}]},
// EARTH LINE
{id:13,name:'Pebble',type:'earth',baseHp:55,baseAtk:40,baseDef:52,baseSpd:32,evoLevel:8,evoId:14,sprite:{body:'#ca8a04',accent:'#fde047',shape:'rock'},moves:[{move:'Tackle',level:1},{move:'Rock Throw',level:5},{move:'Earthquake',level:12},{move:'Body Slam',level:8}]},
{id:14,name:'Obsidian',type:'earth',baseHp:78,baseAtk:60,baseDef:72,baseSpd:45,evoLevel:15,evoId:15,sprite:{body:'#a16207',accent:'#facc15',shape:'golem'},moves:[{move:'Tackle',level:1},{move:'Rock Throw',level:1},{move:'Earthquake',level:8},{move:'Body Slam',level:1}]},
{id:15,name:'Terrasunder',type:'earth',baseHp:108,baseAtk:82,baseDef:98,baseSpd:60,sprite:{body:'#854d0e',accent:'#fbbf24',shape:'colossus'},moves:[{move:'Tackle',level:1},{move:'Rock Throw',level:1},{move:'Earthquake',level:1},{move:'Body Slam',level:1}]},
// VOLT LINE
{id:16,name:'Sparky',type:'volt',baseHp:40,baseAtk:50,baseDef:32,baseSpd:58,evoLevel:8,evoId:17,sprite:{body:'#06b6d4',accent:'#a5f3fc',shape:'cat'},moves:[{move:'Scratch',level:1},{move:'Spark',level:1},{move:'Thunder Shock',level:5},{move:'Thunderbolt',level:12}]},
{id:17,name:'Boltsaber',type:'volt',baseHp:62,baseAtk:68,baseDef:48,baseSpd:74,evoLevel:15,evoId:18,sprite:{body:'#0891b2',accent:'#67e8f9',shape:'sabertooth'},moves:[{move:'Scratch',level:1},{move:'Spark',level:1},{move:'Thunder Shock',level:1},{move:'Thunderbolt',level:8}]},
{id:18,name:'Stormlord',type:'volt',baseHp:88,baseAtk:90,baseDef:65,baseSpd:92,sprite:{body:'#0e7490',accent:'#22d3ee',shape:'thunder'},moves:[{move:'Scratch',level:1},{move:'Spark',level:1},{move:'Thunder Shock',level:1},{move:'Thunderbolt',level:1}]},
];
// ── function: getMoveData ──
function getMoveData(name) { return MOVES.find(m => m.name === name); }
// ── function: findResonant ──
function findResonant(id) { return RESONANTS.find(r => r.id === id); }
// ── function: createSpriteSVG ──
function createSpriteSVG(template, size) {
const s = size || 80;
const c = template;
let svg = '';
switch(c.shape) {
case 'fox':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><ellipse cx="50" cy="62" rx="28" ry="24" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="38" r="18" fill="${c.body}"/><polygon points="32,28 22,8 42,26" fill="${c.accent}"/><polygon points="68,28 78,8 58,26" fill="${c.accent}"/><circle cx="43" cy="35" r="3" fill="#fff"/><circle cx="57" cy="35" r="3" fill="#fff"/><circle cx="43" cy="35" r="1.5" fill="#1a1040"/><circle cx="57" cy="35" r="1.5" fill="#1a1040"/><ellipse cx="50" cy="42" rx="3" ry="2" fill="${c.accent}"/><path d="M46,48 Q50,52 54,48" stroke="${c.body}" stroke-width="1.5" fill="none"/></svg>`;
break;
case 'wolf':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><ellipse cx="50" cy="64" rx="32" ry="26" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="36" r="20" fill="${c.body}"/><polygon points="28,24 16,2 40,28" fill="${c.accent}"/><polygon points="72,24 84,2 60,28" fill="${c.accent}"/><circle cx="42" cy="33" r="3.5" fill="#fff"/><circle cx="58" cy="33" r="3.5" fill="#fff"/><circle cx="42" cy="33" r="2" fill="#1a1040"/><circle cx="58" cy="33" r="2" fill="#1a1040"/><path d="M44,44 L50,48 L56,44" stroke="${c.accent}" stroke-width="2" fill="none"/></svg>`;
break;
case 'dragon':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><ellipse cx="50" cy="62" rx="34" ry="28" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="34" r="22" fill="${c.body}"/><polygon points="26,20 10,0 36,24" fill="${c.accent}"/><polygon points="74,20 90,0 64,24" fill="${c.accent}"/><path d="M38,18 Q50,8 62,18" stroke="${c.accent}" stroke-width="3" fill="none"/><circle cx="42" cy="30" r="4" fill="#fbbf24"/><circle cx="58" cy="30" r="4" fill="#fbbf24"/><circle cx="42" cy="30" r="2" fill="#1a1040"/><circle cx="58" cy="30" r="2" fill="#1a1040"/><path d="M44,46 L50,50 L56,46" stroke="${c.accent}" stroke-width="2.5" fill="none"/><ellipse cx="50" cy="78" rx="12" ry="6" fill="${c.accent}" opacity="0.6"/></svg>`;
break;
case 'blob':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="40%" cy="35%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="55" rx="32" ry="30" fill="url(#g${c.body.slice(1)})"/><circle cx="40" cy="48" r="4" fill="#fff"/><circle cx="60" cy="48" r="4" fill="#fff"/><circle cx="40" cy="48" r="2.5" fill="#1a1040"/><circle cx="60" cy="48" r="2.5" fill="#1a1040"/><ellipse cx="50" cy="58" rx="4" ry="3" fill="${c.accent}" opacity="0.7"/><path d="M44,62 Q50,66 56,62" stroke="${c.body}" stroke-width="1.5" fill="none"/></svg>`;
break;
case 'serpent':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><path d="M30,80 Q25,60 40,50 Q55,40 50,30 Q45,20 55,15" stroke="url(#g${c.body.slice(1)})" stroke-width="16" fill="none" stroke-linecap="round"/><circle cx="55" cy="15" r="8" fill="${c.body}"/><circle cx="52" cy="13" r="2.5" fill="#fff"/><circle cx="58" cy="13" r="2.5" fill="#fff"/><circle cx="52" cy="13" r="1.5" fill="#1a1040"/><circle cx="58" cy="13" r="1.5" fill="#1a1040"/></svg>`;
break;
case 'leviathan':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><ellipse cx="50" cy="60" rx="38" ry="28" fill="url(#g${c.body.slice(1)})"/><circle cx="40" cy="35" r="18" fill="${c.body}"/><path d="M22,28 L10,10 L30,26" fill="${c.accent}"/><path d="M58,28 L70,10 L50,26" fill="${c.accent}"/><circle cx="34" cy="32" r="4" fill="#93c5fd"/><circle cx="46" cy="32" r="4" fill="#93c5fd"/><circle cx="34" cy="32" r="2" fill="#1a1040"/><circle cx="46" cy="32" r="2" fill="#1a1040"/><path d="M38,44 Q45,48 52,44" stroke="${c.accent}" stroke-width="2" fill="none"/><ellipse cx="78" cy="60" rx="14" ry="10" fill="${c.body}" opacity="0.7"/></svg>`;
break;
case 'plant':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="50%" cy="40%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="62" rx="24" ry="28" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="38" r="16" fill="${c.body}"/><path d="M50,22 Q42,8 50,4 Q58,8 50,22" fill="${c.accent}"/><path d="M38,28 Q28,16 34,10 Q42,18 38,28" fill="${c.body}" opacity="0.7"/><circle cx="44" cy="35" r="3" fill="#fff"/><circle cx="56" cy="35" r="3" fill="#fff"/><circle cx="44" cy="35" r="1.8" fill="#1a1040"/><circle cx="56" cy="35" r="1.8" fill="#1a1040"/><ellipse cx="50" cy="42" rx="2.5" ry="2" fill="${c.accent}"/></svg>`;
break;
case 'armored':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><ellipse cx="50" cy="62" rx="34" ry="28" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="36" r="20" fill="${c.body}"/><path d="M30,24 L24,6 L38,22" fill="${c.accent}"/><path d="M70,24 L76,6 L62,22" fill="${c.accent}"/><circle cx="42" cy="33" r="3.5" fill="#fff"/><circle cx="58" cy="33" r="3.5" fill="#fff"/><circle cx="42" cy="33" r="2" fill="#1a1040"/><circle cx="58" cy="33" r="2" fill="#1a1040"/><path d="M44,46 L50,50 L56,46" stroke="${c.accent}" stroke-width="2" fill="none"/><line x1="36" y1="58" x2="64" y2="58" stroke="${c.accent}" stroke-width="2" opacity="0.5"/></svg>`;
break;
case 'tree':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="45%" cy="35%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="68" rx="20" ry="18" fill="${c.body}" opacity="0.7"/><circle cx="50" cy="42" r="30" fill="url(#g${c.body.slice(1)})"/><path d="M30,22 Q20,6 35,14" stroke="${c.accent}" stroke-width="4" fill="none"/><path d="M70,22 Q80,6 65,14" stroke="${c.accent}" stroke-width="4" fill="none"/><circle cx="42" cy="38" r="4" fill="#fff"/><circle cx="58" cy="38" r="4" fill="#fff"/><circle cx="42" cy="38" r="2.5" fill="#1a1040"/><circle cx="58" cy="38" r="2.5" fill="#1a1040"/><path d="M44,50 Q50,54 56,50" stroke="${c.accent}" stroke-width="2" fill="none"/></svg>`;
break;
case 'bird':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="45%" cy="35%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="58" rx="22" ry="26" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="34" r="16" fill="${c.body}"/><path d="M34,30 L10,18 L32,28" fill="${c.accent}" opacity="0.7"/><path d="M66,30 L90,18 L68,28" fill="${c.accent}" opacity="0.7"/><circle cx="44" cy="31" r="3" fill="#fff"/><circle cx="56" cy="31" r="3" fill="#fff"/><circle cx="44" cy="31" r="1.8" fill="#1a1040"/><circle cx="56" cy="31" r="1.8" fill="#1a1040"/><polygon points="50,38 47,42 53,42" fill="${c.accent}"/></svg>`;
break;
case 'falcon':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><ellipse cx="50" cy="60" rx="24" ry="28" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="32" r="18" fill="${c.body}"/><path d="M32,26 L4,12 L30,24" fill="${c.accent}" opacity="0.8"/><path d="M68,26 L96,12 L70,24" fill="${c.accent}" opacity="0.8"/><circle cx="43" cy="29" r="3.5" fill="#e9d5ff"/><circle cx="57" cy="29" r="3.5" fill="#e9d5ff"/><circle cx="43" cy="29" r="2" fill="#1a1040"/><circle cx="57" cy="29" r="2" fill="#1a1040"/><polygon points="50,36 46,42 54,42" fill="${c.accent}"/></svg>`;
break;
case 'phoenix':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="45%" cy="30%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="62" rx="28" ry="24" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="34" r="20" fill="${c.body}"/><path d="M30,22 L2,4 L28,26" fill="${c.accent}" opacity="0.9"/><path d="M70,22 L98,4 L72,26" fill="${c.accent}" opacity="0.9"/><circle cx="42" cy="30" r="4" fill="#e9d5ff"/><circle cx="58" cy="30" r="4" fill="#e9d5ff"/><circle cx="42" cy="30" r="2" fill="#1a1040"/><circle cx="58" cy="30" r="2" fill="#1a1040"/><polygon points="50,36 46,42 54,42" fill="${c.accent}"/><path d="M38,78 Q50,92 62,78" stroke="${c.accent}" stroke-width="3" fill="none"/></svg>`;
break;
case 'rock':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="40%" cy="35%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="58" rx="28" ry="30" fill="url(#g${c.body.slice(1)})"/><circle cx="42" cy="48" r="3.5" fill="#fff"/><circle cx="58" cy="48" r="3.5" fill="#fff"/><circle cx="42" cy="48" r="2" fill="#1a1040"/><circle cx="58" cy="48" r="2" fill="#1a1040"/><ellipse cx="50" cy="56" rx="3" ry="2" fill="${c.accent}" opacity="0.7"/><path d="M44,60 Q50,64 56,60" stroke="${c.body}" stroke-width="1.5" fill="none"/></svg>`;
break;
case 'golem':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><rect x="26" y="38" rx="14" ry="14" width="48" height="50" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="30" r="18" fill="${c.body}"/><circle cx="42" cy="27" r="3.5" fill="#fde047"/><circle cx="58" cy="27" r="3.5" fill="#fde047"/><circle cx="42" cy="27" r="2" fill="#1a1040"/><circle cx="58" cy="27" r="2" fill="#1a1040"/><path d="M44,36 Q50,40 56,36" stroke="${c.accent}" stroke-width="2" fill="none"/></svg>`;
break;
case 'colossus':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><rect x="22" y="34" rx="16" ry="16" width="56" height="58" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="26" r="20" fill="${c.body}"/><polygon points="30,14 24,-2 38,12" fill="${c.accent}" opacity="0.7"/><polygon points="70,14 76,-2 62,12" fill="${c.accent}" opacity="0.7"/><circle cx="42" cy="23" r="4" fill="#fde047"/><circle cx="58" cy="23" r="4" fill="#fde047"/><circle cx="42" cy="23" r="2.5" fill="#1a1040"/><circle cx="58" cy="23" r="2.5" fill="#1a1040"/><path d="M44,32 Q50,36 56,32" stroke="${c.accent}" stroke-width="2.5" fill="none"/></svg>`;
break;
case 'cat':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="45%" cy="35%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="62" rx="24" ry="24" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="38" r="18" fill="${c.body}"/><polygon points="34,26 26,6 42,24" fill="${c.accent}"/><polygon points="66,26 74,6 58,24" fill="${c.accent}"/><circle cx="43" cy="35" r="3.5" fill="#a5f3fc"/><circle cx="57" cy="35" r="3.5" fill="#a5f3fc"/><circle cx="43" cy="35" r="2" fill="#1a1040"/><circle cx="57" cy="35" r="2" fill="#1a1040"/><ellipse cx="50" cy="42" rx="2.5" ry="2" fill="${c.accent}"/></svg>`;
break;
case 'sabertooth':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><linearGradient id="g${c.body.slice(1)}" x1="0%" y1="0%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></linearGradient></defs><ellipse cx="50" cy="62" rx="30" ry="26" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="36" r="20" fill="${c.body}"/><polygon points="30,24 20,4 40,26" fill="${c.accent}"/><polygon points="70,24 80,4 60,26" fill="${c.accent}"/><circle cx="42" cy="33" r="4" fill="#a5f3fc"/><circle cx="58" cy="33" r="4" fill="#a5f3fc"/><circle cx="42" cy="33" r="2.5" fill="#1a1040"/><circle cx="58" cy="33" r="2.5" fill="#1a1040"/><path d="M42,46 L46,52 M58,46 L54,52" stroke="${c.accent}" stroke-width="2.5" fill="none"/></svg>`;
break;
case 'thunder':
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><defs><radialGradient id="g${c.body.slice(1)}" cx="45%" cy="30%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.body}"/></radialGradient></defs><ellipse cx="50" cy="62" rx="32" ry="28" fill="url(#g${c.body.slice(1)})"/><circle cx="50" cy="34" r="22" fill="${c.body}"/><polygon points="28,20 14,-2 38,24" fill="${c.accent}" opacity="0.9"/><polygon points="72,20 86,-2 62,24" fill="${c.accent}" opacity="0.9"/><circle cx="42" cy="30" r="4.5" fill="#a5f3fc"/><circle cx="58" cy="30" r="4.5" fill="#a5f3fc"/><circle cx="42" cy="30" r="2.5" fill="#1a1040"/><circle cx="58" cy="30" r="2.5" fill="#1a1040"/><path d="M42,46 L46,54 M58,46 L54,54" stroke="${c.accent}" stroke-width="3" fill="none"/><polygon points="50,78 44,90 56,90" fill="${c.accent}" opacity="0.5"/></svg>`;
break;
default:
svg = `<svg viewBox="0 0 100 100" width="${s}" height="${s}"><circle cx="50" cy="50" r="35" fill="${c.body}"/><circle cx="42" cy="42" r="4" fill="#fff"/><circle cx="58" cy="42" r="4" fill="#fff"/></svg>`;
}
return svg;
}
// ── class: ResonantInstance ──
class ResonantInstance {
constructor(template, level) {
this.id = template.id;
this.name = template.name;
this.type = template.type;
this.level = level || 1;
this.exp = 0;
this.expToNext = this._expNeeded(1);
this.sprite = template.sprite;
this.moves = [];
this.statusEffect = null;
this.maxHp = this._stat(template.baseHp, level);
this.hp = this.maxHp;
this.atk = this._stat(template.baseAtk, level);
this.def = this._stat(template.baseDef, level);
this.spd = this._stat(template.baseSpd, level);
this.template = template;
}
_stat(base, lvl) { return Math.floor((base * (2 + lvl)) / 100) + (this.id <= 6 ? base/5 : base/4) + 5; }
_expNeeded(lvl) { return Math.floor(20 + lvl * 8 + lvl * lvl * 0.5); }
getAvailableMoves() { return this.template.moves.filter(m => m.level <= this.level).map(m => m.move); }
gainExp(amount) {
this.exp += amount;
while (this.exp >= this.expToNext) {
this.exp -= this.expToNext;
this.level++;
this.maxHp = this._stat(this.template.baseHp, this.level);
this.hp = this.maxHp;
this.atk = this._stat(this.template.baseAtk, this.level);
this.def = this._stat(this.template.baseDef, this.level);
this.spd = this._stat(this.template.baseSpd, this.level);
this.expToNext = this._expNeeded(this.level);
}
}
heal() { this.hp = this.maxHp; this.statusEffect = null; }
isAlive() { return this.hp > 0; }
}
// ── const: SHOP_ITEMS ──
const SHOP_ITEMS = [
{id:'prism_basic',name:'Harmonizer Prism',desc:'Basic capture device',cost:25,icon:'💠'},
{id:'prism_hyper',name:'Hyper-Conductive Prism',desc:'High catch rate prism',cost:60,icon:'🔮'},
{id:'elixir_hp',name:'Core Restorer',desc:'Restores 50% HP to active Resonant',cost:20,icon:'❤️🩹'},
{id:'elixir_status',name:'Resonance Elixir',desc:'Heals all status conditions',cost:30,icon:'✨'},
];
// ── const: ZONES ──
const ZONES = [
{id:0,name:'The Verdant Canopy',levelRange:[1,5],types:['flora','zephyr'],nodes:[
{type:'wild',emoji:'🌿'},{type:'wild',emoji:'🌿'},{type:'spring',emoji:'💧'},
{type:'cache',emoji:'📦'},{type:'wild',emoji:'🌿'},{type:'conductor',emoji:'⚔️'},
{type:'wild',emoji:'🌿'},{type:'boss',emoji:'👑'}
]},
{id:1,name:'The Sunken Grotto',levelRange:[5,10],types:['hydro','earth'],nodes:[
{type:'wild',emoji:'🌊'},{type:'cache',emoji:'📦'},{type:'wild',emoji:'🌊'},
{type:'spring',emoji:'💧'},{type:'conductor',emoji:'⚔️'},{type:'wild',emoji:'🌊'},
{type:'wild',emoji:'🌊'},{type:'boss',emoji:'👑'}
]},
{id:2,name:'The Obsidian Crags',levelRange:[10,15],types:['pyro','volt'],nodes:[
{type:'wild',emoji:'🔥'},{type:'wild',emoji:'🔥'},{type:'cache',emoji:'📦'},
{type:'conductor',emoji:'⚔️'},{type:'spring',emoji:'💧'},{type:'wild',emoji:'🔥'},
{type:'wild',emoji:'🔥'},{type:'boss',emoji:'👑'}
]},
{id:3,name:'The Apex Pillar',levelRange:[15,20],types:['pyro','hydro','flora','zephyr','earth','volt'],nodes:[
{type:'wild',emoji:'⚡'},{type:'conductor',emoji:'⚔️'},{type:'wild',emoji:'⚡'},
{type:'cache',emoji:'📦'},{type:'boss',emoji:'👑'}
]},
];
// ── const: BOSS_DATA ──
const BOSS_DATA = [
{name:'Thornback Alpha',templateId:8,level:5,dialog:'The guardian of the canopy blocks your path!'},
{name:'Geoxus Prime',templateId:5,level:10,dialog:'A massive serpent rises from the depths!'},
{name:'Pyrowolf Commander',templateId:2,level:14,dialog:'Flames engulf the crags as the commander appears!'},
{name:'Stormlord Apex',templateId:18,level:17,dialog:'The ultimate Resonant awaits at the pinnacle!'},
];
// ── const: CONDUCTOR_TEAMS ──
const CONDUCTOR_TEAMS = [
[{id:7,level:3},{id:10,level:4}],
[{id:4,level:6},{id:13,level:5}],
[{id:1,level:8},{id:16,level:9}],
];
// ── const: CACHE_REWARDS ──
const CACHE_REWARDS = [
{type:'shards',amount:15,msg:'+15 Aether Shards!'},
{type:'prism_basic',msg:'Found a Harmonizer Prism!'},
{type:'elixir_hp',msg:'Found a Core Restorer!'},
{type:'shards',amount:25,msg:'+25 Aether Shards!'},
];
// ── class: BattleEngine ──
class BattleEngine {
constructor(game) { this.game = game; }
calcDamage(attacker, defender, moveName) {
const move = getMoveData(moveName);
if (!move) return 0;
const level = attacker.level;
const power = move.power;
const atk = attacker.atk;
const def = defender.def;
const typeModVal = typeMod(move.type, defender.type);
const random = 0.85 + Math.random() * 0.15;
let damage = (((2 * level / 5 + 2) * power * (atk / def)) / 50 + 2) * typeModVal * random;
let isCrit = Math.random() < 0.1;
if (isCrit) damage *= 1.5;
return {damage: Math.max(1, Math.floor(damage)), isCrit, typeMod: typeModVal};
}
async executeTurn(playerRes, enemyRes, moveName, onLog, onComplete) {
const result = this.calcDamage(playerRes, enemyRes, moveName);
const move = getMoveData(moveName);
let logMsg = `${playerRes.name} uses ${moveName}!`;
if (result.isCrit) logMsg += ' Critical hit!';
if (result.typeMod > 1) logMsg += ' Super effective!';
else if (result.typeMod < 1) logMsg += ' Not very effective...';
onLog(logMsg);
this.game.sound.play(result.typeMod > 1 ? 'supereffective' : move.type !== 'cosmos' ? 'hit-elemental' : 'hit-physical');
await this._animateHit('enemy', result.damage, onComplete ? null : () => {});
enemyRes.hp = Math.max(0, enemyRes.hp - result.damage);
if (onComplete) onComplete(result);
}
async _animateHit(target, damage, cb) {
const el = document.getElementById(target === 'enemy' ? 'enemy-sprite' : 'player-sprite');
if (el) { el.classList.add('shake', 'flash-hit'); setTimeout(() => el.classList.remove('shake','flash-hit'), 400); }
await this._delay(500);
if (cb) cb();
}
_delay(ms) { return new Promise(r => setTimeout(r, ms)); }
}
// ── class: GameEngine ──
class GameEngine {
constructor() {
this.sound = new SoundManager();
this.battle = new BattleEngine(this);
this.state = null;
this.currentScreen = 'menu';
this.inBattle = false;
this.particles = [];
const c = document.getElementById('particle-canvas');
if (c) { c.width = window.innerWidth; c.height = window.innerHeight; }
}
newGame() {
this.state = { party:[], inventory:{prism_basic:3,prism_hyper:0,elixir_hp:2,elixir_status:1}, shards:50, unlockedZones:[true,false,false,false], completedNodes:[[false,false,false,false,false,false,false,false],[false,false,false,false,false,false,false,false],[false,false,false,false,false,false,false,false],[false,false,false,false,false]], bossesDefeated:[false,false,false,false], discoveredIds:[], currentZone:0 };
this.state.party.push(new ResonantInstance(findResonant(1), 3));
this._save();
this.sound.init();
this.showScreen('map');
this.renderMap();
}
loadGame() {
try { const raw = localStorage.getItem('aethelgard_save'); if (!raw) return false; const data = JSON.parse(raw); this.state = data; this.state.party = data.party.map(p => { const inst = new ResonantInstance(findResonant(p.id), p.level); Object.assign(inst, p); return inst; }); return true; } catch(e) { return false; }
}
_save() { if (!this.state) return; try { localStorage.setItem('aethelgard_save', JSON.stringify(this.state)); } catch(e) {} }
resetProgress() { localStorage.removeItem('aethelgard_save'); this.state = null; document.getElementById('btn-continue').classList.add('hidden'); document.getElementById('btn-reset').classList.add('hidden'); }
showScreen(name) { document.querySelectorAll('.game-screen').forEach(s => s.classList.add('hidden')); const el = document.getElementById('screen-' + name); if (el) { el.classList.remove('hidden'); this.currentScreen = name; } }
getActiveResonant() { return this.state.party[0]; }
renderMap() {
const container = document.getElementById('map-container');
if (!container) return;
container.innerHTML = '';
ZONES.forEach((zone, zi) => {
const section = document.createElement('div'); section.className = 'zone-section';
const label = document.createElement('div'); label.className = 'zone-label';
label.textContent = zone.name + (this.state.unlockedZones[zi] ? '' : ' 🔒'); section.appendChild(label);
const row = document.createElement('div'); row.className = 'node-row';
zone.nodes.forEach((node, ni) => {
const btn = document.createElement('button'); btn.className = 'map-node node-' + node.type; btn.textContent = node.emoji;
if (!this.state.unlockedZones[zi]) btn.classList.add('locked');
else if (this.state.completedNodes[zi][ni]) btn.classList.add('completed');
if (node.type === 'boss') btn.classList.add('boss');
if (this.state.unlockedZones[zi] && !this.state.completedNodes[zi][ni]) {
const _zi = zi, _ni = ni;
btn.addEventListener('click', () => this._handleNode(_zi, _ni));
}
row.appendChild(btn);
});
section.appendChild(row); container.appendChild(section);
});
document.getElementById('zone-title').textContent = ZONES[this.state.currentZone || 0].name;
document.getElementById('shard-count').textContent = '💎 ' + this.state.shards;
}
async _handleNode(zi, ni) {
const zone = ZONES[zi]; const node = zone.nodes[ni];
this.sound.play('select');
this._currentNode = { zi, ni };
switch(node.type) {
case 'wild': await this._startWildBattle(zi); break;
case 'cache': await this._openCache(); break;
case 'conductor': await this._startConductorBattle(zi); break;
case 'spring': await this._healSpring(); break;
case 'boss': await this._startBossBattle(zi); break;
}
}
async _openCache() {
const reward = CACHE_REWARDS[Math.floor(Math.random() * CACHE_REWARDS.length)];
if (reward.type === 'shards') this.state.shards += reward.amount;
else if (this.state.inventory[reward.type] !== undefined) this.state.inventory[reward.type]++;
this._showOverlay('capture', '📦 Mystery Cache!', reward.msg);
const cn = this._currentNode;
if (cn) this.state.completedNodes[cn.zi][cn.ni] = true;
this._save();
}
async _healSpring() {
this.state.party.forEach(r => r.heal());
this._showOverlay('capture', '💧 Healing Spring', 'Your Resonants are fully restored!');
const cn = this._currentNode;
if (cn) this.state.completedNodes[cn.zi][cn.ni] = true;
this._save();
}
_randomWildResonant(zi) {
const zone = ZONES[zi];
const lvl = zone.levelRange[0] + Math.floor(Math.random() * (zone.levelRange[1] - zone.levelRange[0]));
const validIds = RESONANTS.filter(r => zone.types.includes(r.type)).map(r => r.id);
const tid = validIds[Math.floor(Math.random() * validIds.length)];
return new ResonantInstance(findResonant(tid), lvl);
}
async _startWildBattle(zi) { await this._runBattle(this._randomWildResonant(zi), false, zi, false); }
async _startConductorBattle(zi) {
const team = CONDUCTOR_TEAMS[zi] || CONDUCTOR_TEAMS[0];
const pick = team[Math.floor(Math.random() * team.length)];
await this._runBattle(new ResonantInstance(findResonant(pick.id), pick.level), true, zi, false);
}
async _startBossBattle(zi) {
const bossInfo = BOSS_DATA[zi];
const enemy = new ResonantInstance(findResonant(bossInfo.templateId), bossInfo.level);
enemy.name = bossInfo.name;
await this._runBattle(enemy, true, zi, true);
}
async _runBattle(enemy, isConductor, zi, isBoss) {
const playerRes = this.getActiveResonant();
if (!playerRes || !playerRes.isAlive()) {
this._showOverlay('gameover', '💀 Fainted!', 'All your Resonants have fainted! Resting...');
await new Promise(r => setTimeout(r, 1500));
this.state.party.forEach(r => r.heal());
const cn = this._currentNode;
if (cn) this.state.completedNodes[cn.zi][cn.ni] = true;
this._save(); return;
}
this.inBattle = true;
this.battleState = { enemy, isConductor: !!isConductor, zi, isBoss: !!isBoss, turn: 'player', phase: 'menu' };
this.showScreen('battle');
this._renderBattleUI();
await new Promise(r => setTimeout(r, 300));
if (isBoss) { const bossInfo = BOSS_DATA[zi]; this._logBattle(bossInfo.dialog); await new Promise(r => setTimeout(r, 1200)); }
else { this._logBattle('A wild ' + enemy.name + ' appears!'); await new Promise(r => setTimeout(r, 800)); }
this._showBattleMenu();
}
_renderBattleUI() {
const pr = this.getActiveResonant(); const en = this.battleState.enemy;
document.getElementById('player-sprite').innerHTML = createSpriteSVG(pr.sprite, 80);
document.getElementById('enemy-sprite').innerHTML = createSpriteSVG(en.sprite, 80);
document.getElementById('player-res-name').textContent = pr.name;
document.getElementById('player-res-level').textContent = 'Lv.' + pr.level;
const pTypeEl = document.getElementById('player-res-type');
pTypeEl.textContent = ELEMENTS[pr.type]; pTypeEl.className = 'type-badge type-' + pr.type;
this._updateHpBar('player', pr);
document.getElementById('enemy-name').textContent = en.name;
document.getElementById('enemy-level').textContent = 'Lv.' + en.level;
const eTypeEl = document.getElementById('enemy-type');
eTypeEl.textContent = ELEMENTS[en.type]; eTypeEl.className = 'type-badge type-' + en.type;
this._updateHpBar('enemy', en);
}
_updateHpBar(who, res) {
const pct = Math.max(0, (res.hp / res.maxHp) * 100);
const fill = document.getElementById(who === 'player' ? 'player-hp-fill' : 'enemy-hp-fill');
fill.style.width = pct + '%';
fill.className = 'hp-fill ' + (pct > 50 ? 'hp-high' : pct > 20 ? 'hp-mid' : 'hp-low');
document.getElementById(who === 'player' ? 'player-hp-text' : 'enemy-hp-text').textContent = Math.max(0, res.hp) + '/' + res.maxHp;
}
_logBattle(msg) { const log = document.getElementById('battle-log'); if (log) { log.innerHTML += '<div>' + msg + '</div>'; log.scrollTop = log.scrollHeight; } }
_showBattleMenu() {
this.battleState.phase = 'menu';
const menu = document.getElementById('battle-menu'); if (!menu) return;
menu.innerHTML = '';
['Attack','Prisms','Exchange','Items'].forEach(label => {
const btn = document.createElement('button'); btn.className = 'glass-btn'; btn.textContent = label;
btn.addEventListener('click', () => this._onMenuSelect(label));
menu.appendChild(btn);
});
}
_showBattleMoves() {
this.battleState.phase = 'moves';
const pr = this.getActiveResonant(); const moves = pr.getAvailableMoves();
const menu = document.getElementById('battle-menu'); if (!menu) return;
menu.innerHTML = '';
moves.forEach(moveName => {
const moveData = getMoveData(moveName);
const btn = document.createElement('button'); btn.className = 'glass-btn move-btn-' + (moveData ? moveData.type : 'cosmos');
btn.textContent = moveName;
btn.addEventListener('click', () => this._onPlayerAttack(moveName));
menu.appendChild(btn);
});
const backBtn = document.createElement('button'); backBtn.className = 'glass-btn btn-secondary';
backBtn.textContent = '← Back'; backBtn.addEventListener('click', () => this._showBattleMenu());
menu.appendChild(backBtn);
}
_showPrismMenu() {
const menu = document.getElementById('battle-menu'); if (!menu) return;
menu.innerHTML = '';
const inv = this.state.inventory;
[{id:'prism_basic',name:'Harmonizer Prism'},{id:'prism_hyper',name:'Hyper-Conductive Prism'}].forEach(p => {
const btn = document.createElement('button'); btn.className = 'glass-btn';
btn.textContent = p.name + ' (x' + inv[p.id] + ')';
if (inv[p.id] <= 0) { btn.style.opacity = '0.4'; btn.style.pointerEvents = 'none'; }
else { btn.addEventListener('click', () => this._attemptCapture(p.id)); }
menu.appendChild(btn);
});
const backBtn = document.createElement('button'); backBtn.className = 'glass-btn btn-secondary';
backBtn.textContent = '← Back'; backBtn.addEventListener('click', () => this._showBattleMenu());
menu.appendChild(backBtn);
}
_showExchangeMenu() {
const menu = document.getElementById('battle-menu'); if (!menu) return;
menu.innerHTML = '';
this.state.party.forEach((res, i) => {
const btn = document.createElement('button'); btn.className = 'glass-btn';
btn.textContent = (i === 0 ? '▶ ' : '') + res.name + ' Lv.' + res.level;
if (!res.isAlive()) { btn.style.opacity = '0.4'; btn.style.pointerEvents = 'none'; }
else if (i !== 0) { btn.addEventListener('click', () => this._doExchange(i)); }
menu.appendChild(btn);
});
const backBtn = document.createElement('button'); backBtn.className = 'glass-btn btn-secondary';
backBtn.textContent = '← Back'; backBtn.addEventListener('click', () => this._showBattleMenu());
menu.appendChild(backBtn);
}
_showItemMenu() {
const menu = document.getElementById('battle-menu'); if (!menu) return;
menu.innerHTML = '';
const inv = this.state.inventory;
[{id:'elixir_hp',name:'Core Restorer'},{id:'elixir_status',name:'Resonance Elixir'}].forEach(item => {
const btn = document.createElement('button'); btn.className = 'glass-btn';
btn.textContent = item.name + ' (x' + inv[item.id] + ')';
if (inv[item.id] <= 0) { btn.style.opacity = '0.4'; btn.style.pointerEvents = 'none'; }
else { btn.addEventListener('click', () => this._useItem(item.id)); }
menu.appendChild(btn);
});
const backBtn = document.createElement('button'); backBtn.className = 'glass-btn btn-secondary';
backBtn.textContent = '← Back'; backBtn.addEventListener('click', () => this._showBattleMenu());
menu.appendChild(backBtn);
}
_onMenuSelect(label) {
this.sound.play('select');
switch(label) { case 'Attack': this._showBattleMoves(); break; case 'Prisms': this._showPrismMenu(); break; case 'Exchange': this._showExchangeMenu(); break; case 'Items': this._showItemMenu(); break; }
}
async _onPlayerAttack(moveName) {
if (this.battleState.phase !== 'moves') return;
this.battleState.phase = 'attacking';
const pr = this.getActiveResonant(); const en = this.battleState.enemy;
await this.battle.executeTurn(pr, en, moveName, msg => this._logBattle(msg), result => {
this._renderBattleUI();
if (en.hp <= 0) { setTimeout(() => this._onEnemyDefeated(), 600); }
else { setTimeout(() => this._enemyTurn(), 800); }
});
}
async _attemptCapture(prismId) {
const en = this.battleState.enemy;
let rate = prismId === 'prism_hyper' ? 0.55 : 0.35;
if (en.hp / en.maxHp < 0.25) rate += 0.25;
else if (en.hp / en.maxHp < 0.5) rate += 0.1;
rate = Math.min(rate, 0.9);
this.state.inventory[prismId]--;
const prismName = prismId === 'prism_hyper' ? 'Hyper-Conductive Prism' : 'Harmonizer Prism';
this._logBattle('You threw a ' + prismName + '!');
await new Promise(r => setTimeout(r, 1000));
if (Math.random() < rate) {
const captured = new ResonantInstance(findResonant(en.id), en.level);
this.state.party.push(captured);
if (!this.state.discoveredIds.includes(en.id)) this.state.discoveredIds.push(en.id);
this.sound.play('capture');
this._logBattle(en.name + ' was captured!');
await new Promise(r => setTimeout(r, 500));
this.inBattle = false;
const cn = this._currentNode; if (cn) this.state.completedNodes[cn.zi][cn.ni] = true;
this._save(); this.showScreen('map'); this.renderMap();
} else {
this.sound.play('capture-fail');
this._logBattle(en.name + ' broke free!');
await new Promise(r => setTimeout(r, 600));
this._enemyTurn();
}
}
_doExchange(idx) {
const temp = this.state.party[0]; this.state.party[0] = this.state.party[idx]; this.state.party[idx] = temp;
this._logBattle('Switched to ' + this.getActiveResonant().name + '!');
this._renderBattleUI(); setTimeout(() => this._enemyTurn(), 600);
}
_useItem(itemId) {
const pr = this.getActiveResonant();
if (itemId === 'elixir_hp') { pr.hp = Math.min(pr.maxHp, pr.hp + Math.floor(pr.maxHp * 0.5)); this._logBattle('Used Core Restorer! HP restored.'); }
else if (itemId === 'elixir_status') { pr.statusEffect = null; this._logBattle('Used Resonance Elixir! Status cleared.'); }
this.state.inventory[itemId]--; this._renderBattleUI(); setTimeout(() => this._enemyTurn(), 600);
}
async _enemyTurn() {
if (this.battleState.phase === 'done') return;
const pr = this.getActiveResonant(); const en = this.battleState.enemy;
const moves = en.getAvailableMoves();
const moveName = moves[Math.floor(Math.random() * moves.length)];
await this.battle.executeTurn(en, pr, moveName, msg => this._logBattle(msg), result => {
this._renderBattleUI();
if (pr.hp <= 0) { setTimeout(() => this._onPlayerFainted(), 600); }
else { setTimeout(() => this._showBattleMenu(), 500); }
});
}
async _onEnemyDefeated() {
const en = this.battleState.enemy; const pr = this.getActiveResonant();
const shardsGain = Math.floor(8 + en.level * 3 + (this.battleState.isBoss ? 20 : 0));
const expGain = Math.floor(15 + en.level * 4);
this.state.shards += shardsGain; pr.gainExp(expGain);
if (!this.state.discoveredIds.includes(en.id)) this.state.discoveredIds.push(en.id);
this.sound.play('victory');
this._logBattle(en.name + ' was defeated! +' + shardsGain + ' Shards, +' + expGain + ' XP');
await new Promise(r => setTimeout(r, 800));
if (this.battleState.isBoss) {
this.state.bossesDefeated[this.battleState.zi] = true;
const nextZone = this.battleState.zi + 1;
if (nextZone < ZONES.length && !this.state.unlockedZones[nextZone]) {
this.state.unlockedZones[nextZone] = true;
this._logBattle('🎉 ' + ZONES[nextZone].name + ' unlocked!');
}
}
const oldLevel = pr.level - 1; // approximate check
if (pr.template.evoLevel && pr.level >= pr.template.evoLevel) {
await this._evolveResonant(pr, 0);
} else if (pr.exp < expGain) { // leveled up
this.sound.play('levelup');
this._showOverlay('levelup', '⬆️ Level Up!', pr.name + ' reached level ' + pr.level + '!');
}
this.inBattle = false;
const cn = this._currentNode; if (cn) this.state.completedNodes[cn.zi][cn.ni] = true;
this._save(); await new Promise(r => setTimeout(r, 1500));
this.showScreen('map'); this.renderMap();
}
async _onPlayerFainted() {
const pr = this.getActiveResonant();
this.sound.play('defeat'); this._logBattle(pr.name + ' fainted!');
await new Promise(r => setTimeout(r, 800));
const aliveIdx = this.state.party.findIndex((r,i) => i !== 0 && r.isAlive());
if (aliveIdx > 0) {
this._showOverlay('gameover', '💀 Fainted!', pr.name + ' fainted! Switching to ' + this.state.party[aliveIdx].name + '.');
await new Promise(r => setTimeout(r, 1500));
const temp = this.state.party[0]; this.state.party[0] = this.state.party[aliveIdx]; this.state.party[aliveIdx] = temp;
pr.heal(); this.inBattle = false;
const cn = this._currentNode; if (cn) this.state.completedNodes[cn.zi][cn.ni] = true;
this._save(); this.showScreen('map'); this.renderMap();
} else {
this._showOverlay('gameover', '💀 All Fainted!', 'All your Resonants have fainted! Resting...');
await new Promise(r => setTimeout(r, 1500));
this.state.party.forEach(r => r.heal()); this.inBattle = false;
const cn = this._currentNode; if (cn) this.state.completedNodes[cn.zi][cn.ni] = true;
this._save(); this.showScreen('map'); this.renderMap();
}
}
async _evolveResonant(res, idx) {
if (!res.template.evoId) return;
const newTemplate = findResonant(res.template.evoId); if (!newTemplate) return;
this.sound.play('evolution'); this._logBattle(res.name + ' is evolving!');
await new Promise(r => setTimeout(r, 1000));
const evolved = new ResonantInstance(newTemplate, res.level);
evolved.exp = res.exp; evolved.expToNext = res.expToNext;
this.state.party[idx] = evolved;
if (!this.state.discoveredIds.includes(newTemplate.id)) this.state.discoveredIds.push(newTemplate.id);
this._showOverlay('evolution', '✨ Evolution!', res.name + ' evolved into ' + newTemplate.name + '!');
}
_showOverlay(type, title, msg) {
const overlay = document.getElementById(type + '-overlay'); if (!overlay) return;
const tId = type === 'capture' ? 'capture-title' : type === 'levelup' ? 'levelup-title' : type === 'evolution' ? 'evo-title' : 'go-title';
const mId = type === 'capture' ? 'capture-message' : type === 'levelup' ? 'levelup-message' : type === 'evolution' ? 'evo-message' : 'go-message';
const tEl = document.getElementById(tId); const mEl = document.getElementById(mId);
if (tEl) tEl.textContent = title; if (mEl) mEl.textContent = msg;
overlay.classList.remove('hidden');
}
hideOverlay(type) { const overlay = document.getElementById(type + '-overlay'); if (overlay) overlay.classList.add('hidden'); }
renderShop() {
this.showScreen('shop');
document.getElementById('shop-shards').textContent = this.state.shards;
const grid = document.getElementById('shop-items'); grid.innerHTML = '';
SHOP_ITEMS.forEach(item => {
const div = document.createElement('div'); div.className = 'shop-item';
div.innerHTML = '<div class="shop-item-info"><div class="shop-item-name">' + item.icon + ' ' + item.name + '</div><div class="shop-item-desc">' + item.desc + '</div></div><div class="shop-item-cost">💎 ' + item.cost + '</div>';
if (this.state.shards >= item.cost) { div.addEventListener('click', () => this._buyItem(item)); } else { div.style.opacity = '0.5'; }
grid.appendChild(div);
});
}
_buyItem(item) {
if (this.state.shards < item.cost) return;
this.sound.play('select'); this.state.shards -= item.cost;
this.state.inventory[item.id] = (this.state.inventory[item.id] || 0) + 1;
document.getElementById('shop-shards').textContent = this.state.shards;
this.renderShop(); this._save();
}
renderParty() {
this.showScreen('party');
const grid = document.getElementById('party-list'); grid.innerHTML = '';
this.state.party.forEach((res, i) => {
const div = document.createElement('div'); div.className = 'party-card';
div.innerHTML = '<div class="party-card-sprite">' + createSpriteSVG(res.sprite, 48) + '</div><div class="party-card-info"><strong>' + (i===0?'▶ ':'' )+ res.name + '</strong> <span class="type-badge type-' + res.type + '" style="font-size:10px">' + ELEMENTS[res.type] + '</span><br><div class="party-card-stats">Lv.' + res.level + ' | HP:' + res.hp + '/' + res.maxHp + ' ATK:' + res.atk + ' DEF:' + res.def + ' SPD:' + res.spd + '</div></div>';
grid.appendChild(div);
});
}
renderDex() {
this.showScreen('dex');
const grid = document.getElementById('dex-list'); grid.innerHTML = '';
RESONANTS.forEach(res => {
const discovered = this.state.discoveredIds.includes(res.id);
const div = document.createElement('div'); div.className = 'dex-entry ' + (discovered ? 'discovered' : 'undiscovered');
div.innerHTML = '<div class="dex-sprite">' + createSpriteSVG(res.sprite, 40) + '</div><div>' + (discovered ? res.name : '???') + '</div>';
grid.appendChild(div);
});
}
}
// ── main ──
(function() {
const game = new GameEngine();
window.gameRef = game;
// Check for saved game
if (game.loadGame()) {
document.getElementById('btn-continue').classList.remove('hidden');
document.getElementById('btn-reset').classList.remove('hidden');
}
// Menu buttons
document.getElementById('btn-new-game').addEventListener('click', () => { game.sound.init(); game.newGame(); });
document.getElementById('btn-continue').addEventListener('click', () => { game.sound.init(); game.showScreen('map'); game.renderMap(); });
document.getElementById('btn-reset').addEventListener('click', () => { if(confirm('Reset all progress?')) game.resetProgress(); });
// Map buttons
document.getElementById('btn-shop').addEventListener('click', () => { game.sound.play('select'); game.renderShop(); });
document.getElementById('btn-party').addEventListener('click', () => { game.sound.play('select'); game.renderParty(); });
document.getElementById('btn-dex').addEventListener('click', () => { game.sound.play('select'); game.renderDex(); });
// Back buttons
document.getElementById('btn-shop-back').addEventListener('click', () => { game.sound.play('select'); game.showScreen('map'); game.renderMap(); });
document.getElementById('btn-party-back').addEventListener('click', () => { game.sound.play('select'); game.showScreen('map'); game.renderMap(); });
document.getElementById('btn-dex-back').addEventListener('click', () => { game.sound.play('select'); game.showScreen('map'); game.renderMap(); });
// Overlay OK buttons
['capture','levelup','evolution','gameover'].forEach(type => {
const btn = document.getElementById(type === 'capture' ? 'btn-capture-ok' : type === 'levelup' ? 'btn-levelup-ok' : type === 'evolution' ? 'btn-evo-ok' : 'btn-go-ok');
if (btn) btn.addEventListener('click', () => { game.sound.play('select'); game.hideOverlay(type); });
});
// Hover sounds for buttons
document.querySelectorAll('.glass-btn').forEach(btn => {
btn.addEventListener('mouseenter', () => game.sound.play('hover'));
});
// Particle system
const canvas = document.getElementById('particle-canvas');
if (canvas) {
const ctx = canvas.getContext('2d');
function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
class Particle {
constructor(x, y, color) { this.x = x; this.y = y; this.color = color; this.size = Math.random() * 3 + 1; this.speedX = (Math.random() - 0.5) * 2; this.speedY = -Math.random() * 2 - 0.5; this.life = 1; this.decay = Math.random() * 0.02 + 0.01; }
update() { this.x += this.speedX; this.y += this.speedY; this.life -= this.decay; }
draw(ctx) { ctx.globalAlpha = this.life; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; }
}
let particles = [];
function spawnParticles(x, y, color, count) { for (let i = 0; i < count; i++) particles.push(new Particle(x, y, color)); }
// Ambient particles
setInterval(() => {
if (Math.random() < 0.3) {
const colors = ['#a78bfa','#60a5fa','#f472b6','#34d399'];
spawnParticles(Math.random() * canvas.width, canvas.height + 10, colors[Math.floor(Math.random()*colors.length)], 1);
}
}, 200);
function animateParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles = particles.filter(p => p.life > 0);
particles.forEach(p => { p.update(); p.draw(ctx); });
requestAnimationFrame(animateParticles);
}
animateParticles();
// Expose for battle effects
window._spawnBattleParticles = function(x, y, color) { spawnParticles(x, y, color, 15); };
}
})();
</script>
</body>
</html>