Aethelgard
Qwen 3.6 27b MTP · composer

Reviews & generation details
Reviews
Human review · Jul 16, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
- Visual design
- 3 / 5
- UX / feel
- 3 / 5
- Works
- 4 / 5
- Prompt spirit
- 3 / 5
- Delight
- 2 / 5
boring design with basic fonts; decent creature images; good animations; no sound
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 85,269 bytes · SHA-256 f6b170d14123
<!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>
<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 ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; }
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #0f0a1e 0%, #1a1040 30%, #0d1b2a 70%, #0a0618 100%);
color: #e2e8f0;
}
#game-container { width: 100%; height: 100%; position: relative; overflow: hidden; }
.screen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: none; flex-direction: column; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.4s ease, transform 0.4s ease;
}
.screen.active { display: flex; opacity: 1; }
.glass-card {
background: rgba(255,255,255,0.06);
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px; padding: 24px;
}
/* Element glow colors */
.pyro-glow { box-shadow: 0 0 20px rgba(255,87,34,0.4), inset 0 0 20px rgba(255,87,34,0.1); border-color: rgba(255,87,34,0.5) !important; }
.hydro-glow { box-shadow: 0 0 20px rgba(33,150,243,0.4), inset 0 0 20px rgba(33,150,243,0.1); border-color: rgba(33,150,243,0.5) !important; }
.flora-glow { box-shadow: 0 0 20px rgba(76,175,80,0.4), inset 0 0 20px rgba(76,175,80,0.1); border-color: rgba(76,175,80,0.5) !important; }
.volt-glow { box-shadow: 0 0 20px rgba(255,235,59,0.4), inset 0 0 20px rgba(255,235,59,0.1); border-color: rgba(255,235,59,0.5) !important; }
.zephyr-glow { box-shadow: 0 0 20px rgba(0,230,118,0.4), inset 0 0 20px rgba(0,230,118,0.1); border-color: rgba(0,230,118,0.5) !important; }
.earth-glow { box-shadow: 0 0 20px rgba(141,110,99,0.4), inset 0 0 20px rgba(141,110,99,0.1); border-color: rgba(141,110,99,0.5) !important; }
.cosmos-glow { box-shadow: 0 0 20px rgba(167,139,250,0.4), inset 0 0 20px rgba(167,139,250,0.1); border-color: rgba(167,139,250,0.5) !important; }
.pyro-text { color: #ff5722; }
.hydro-text { color: #2196f3; }
.flora-text { color: #4caf50; }
.volt-text { color: #ebd841; }
.zephyr-text { color: #00e676; }
.earth-text { color: #a1887f; }
.cosmos-text { color: #a78bfa; }
.hidden { display: none !important; }
/* ── css: menu ── */
.menu-content { text-align: center; }
.game-title {
font-family: 'Press Start 2P', monospace;
font-size: clamp(1.5rem, 4vw, 3rem);
background: linear-gradient(90deg, #a78bfa, #60a5fa, #f472b6, #a78bfa);
background-size: 200% auto;
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
animation: shimmer 3s linear infinite;
margin-bottom: 8px; letter-spacing: 4px;
}
.game-subtitle {
font-family: 'Press Start 2P', monospace;
font-size: clamp(0.5rem, 1.5vw, 0.9rem);
color: #94a3b8; margin-bottom: 40px; letter-spacing: 2px;
}
.prism-logo {
width: 80px; height: 80px; margin: 0 auto 40px;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
animation: float 3s ease-in-out infinite;
}
.menu-btn {
display: block; width: 260px; margin: 10px auto; padding: 14px 24px;
border: 1px solid rgba(255,255,255,0.15); border-radius: 12px;
font-family: 'Inter', sans-serif; font-size: 1rem; font-weight: 600;
cursor: pointer; transition: all 0.2s ease; letter-spacing: 1px;
}
.menu-btn.primary { background: linear-gradient(135deg, #7c3aed, #4f46e5); color: white; }
.menu-btn.secondary { background: rgba(255,255,255,0.08); color: #cbd5e1; border-color: rgba(255,255,255,0.2); }
.menu-btn.danger { background: rgba(239,68,68,0.15); color: #fca5a5; border-color: rgba(239,68,68,0.3); }
.menu-btn.small { width: auto; padding: 8px 24px; font-size: 0.85rem; margin-top: 12px; }
.menu-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 20px rgba(124,58,237,0.3); }
.menu-btn:active { transform: scale(0.97); }
@keyframes shimmer { to { background-position: 200% center; } }
@keyframes float { 0%,100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-10px) rotate(3deg); } }
/* ── css: map ── */
.map-header {
width: 100%; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center;
background: rgba(0,0,0,0.3); backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255,255,255,0.08);
}
.map-header h2 { font-family: 'Press Start 2P', monospace; font-size: clamp(0.6rem, 1.5vw, 0.9rem); color: #a78bfa; }
.header-stats { display: flex; gap: 8px; align-items: center; }
.icon-btn {
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
color: #cbd5e1; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 0.8rem;
transition: all 0.2s ease;
}
.icon-btn:hover { background: rgba(255,255,255,0.12); transform: translateY(-1px); }
.map-area {
flex: 1; width: 100%; overflow-y: auto; padding: 24px;
display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.zone-path {
display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; padding: 20px;
position: relative;
}
.map-node {
width: 72px; height: 72px; border-radius: 50%; cursor: pointer;
display: flex; align-items: center; justify-content: center; font-size: 1.8rem;
transition: all 0.3s ease; position: relative;
background: rgba(255,255,255,0.06); border: 2px solid rgba(255,255,255,0.15);
}
.map-node:hover:not(.locked) { transform: scale(1.15); box-shadow: 0 0 24px rgba(124,58,237,0.4); }
.map-node.locked { opacity: 0.3; cursor: not-allowed; filter: grayscale(1); }
.map-node.completed { border-color: #4ade80; background: rgba(74,222,128,0.15); }
.map-node.current { animation: nodePulse 2s ease-in-out infinite; border-color: #a78bfa; }
.map-node .node-label {
position: absolute; bottom: -22px; font-size: 0.6rem; white-space: nowrap; color: #94a3b8;
}
@keyframes nodePulse {
0%,100% { box-shadow: 0 0 10px rgba(167,139,250,0.3); }
50% { box-shadow: 0 0 25px rgba(167,139,250,0.6); }
}
.dialog-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 100;
}
.dialog-box {
background: rgba(20,15,40,0.95); backdrop-filter: blur(20px);
border: 1px solid rgba(255,255,255,0.15); border-radius: 16px; padding: 32px;
max-width: 400px; text-align: center;
}
.dialog-box p { margin-bottom: 16px; font-size: 0.95rem; line-height: 1.6; color: #cbd5e1; }
/* Zone selector */
.zone-selector { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; justify-content: center; }
.zone-tab {
padding: 8px 16px; border-radius: 20px; cursor: pointer; font-size: 0.75rem;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: #94a3b8;
transition: all 0.2s ease; font-weight: 600;
}
.zone-tab.active { background: rgba(124,58,237,0.2); border-color: #7c3aed; color: #c4b5fd; }
.zone-tab.locked { opacity: 0.3; cursor: not-allowed; }
/* ── css: battle ── */
#screen-battle { justify-content: flex-start; }
.battle-scene {
width: 100%; flex: 1; display: flex; padding: 20px; gap: 40px;
align-items: center; justify-content: center; position: relative; overflow: hidden;
}
.enemy-area, .player-area {
display: flex; flex-direction: column; align-items: center; gap: 16px; width: 280px;
}
.resonant-card {
background: rgba(255,255,255,0.06); backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 14px; width: 100%;
}
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card-header span:first-child { font-weight: 600; font-size: 0.9rem; }
.level-badge { background: rgba(124,58,237,0.3); padding: 2px 8px; border-radius: 8px; font-size: 0.7rem; color: #c4b5fd; }
.hp-bar-container { width: 100%; height: 10px; background: rgba(0,0,0,0.4); border-radius: 5px; overflow: hidden; margin-bottom: 4px; }
.hp-bar { height: 100%; border-radius: 5px; transition: width 0.5s ease, background-color 0.3s ease; }
#enemy-hp-text, #player-hp-text { font-size: 0.7rem; color: #94a3b8; text-align: right; display: block; }
.status-effects { display: flex; gap: 4px; margin-top: 6px; }
.status-badge { font-size: 0.65rem; padding: 2px 6px; border-radius: 4px; background: rgba(239,68,68,0.2); color: #fca5a5; }
.resonant-sprite {
width: 120px; height: 120px; display: flex; align-items: center; justify-content: center;
}
.enemy-sprite { animation: enemyIdle 3s ease-in-out infinite; }
.player-sprite { animation: playerIdle 3s ease-in-out infinite; transform: scaleX(-1); }
@keyframes enemyIdle { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
@keyframes playerIdle { 0%,100% { transform: scaleX(-1) translateY(0); } 50% { transform: scaleX(-1) translateY(-6px); } }
.battle-ui { width: 100%; padding: 12px 24px; background: rgba(0,0,0,0.3); backdrop-filter: blur(10px); border-top: 1px solid rgba(255,255,255,0.08); }
.battle-log { height: 60px; overflow-y: auto; margin-bottom: 12px; padding: 8px 12px; background: rgba(0,0,0,0.3); border-radius: 8px; font-size: 0.8rem; color: #94a3b8; line-height: 1.5; }
.battle-log p { margin-bottom: 2px; }
.battle-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.action-btn {
padding: 12px 8px; border-radius: 10px; cursor: pointer; font-size: 0.8rem; font-weight: 600;
transition: all 0.2s ease; text-align: center; border: 1px solid rgba(255,255,255,0.1);
}
.action-btn.primary { background: linear-gradient(135deg, #dc2626, #b91c1c); color: white; }
.action-btn.secondary { background: linear-gradient(135deg, #7c3aed, #6d28d9); color: white; }
.action-btn.tertiary { background: rgba(255,255,255,0.08); color: #cbd5e1; }
.action-btn.quaternary { background: linear-gradient(135deg, #059669, #047857); color: white; }
.action-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,0.3); }
.move-select, .item-select, .exchange-panel { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-top: 8px; }
.move-btn, .prism-btn, .inv-item-btn {
padding: 10px; border-radius: 8px; cursor: pointer; font-size: 0.75rem; text-align: left;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: #cbd5e1;
transition: all 0.2s ease;
}
.move-btn:hover, .prism-btn:hover, .inv-item-btn:hover { background: rgba(255,255,255,0.12); }
.move-btn .move-type { font-size: 0.6rem; opacity: 0.7; display: block; margin-top: 2px; }
.exchange-panel { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.exchange-slot {
padding: 12px; border-radius: 8px; cursor: pointer; background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1); text-align: center; transition: all 0.2s ease;
}
.exchange-slot:hover { background: rgba(124,58,237,0.2); border-color: #7c3aed; }
.exchange-slot.fainted { opacity: 0.4; cursor: not-allowed; }
/* Battle animations */
@keyframes shakeScreen { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-8px)} 75%{transform:translateX(8px)} }
.shake { animation: shakeScreen 0.3s ease-in-out; }
@keyframes flashWhite { 0%{filter:brightness(1)} 50%{filter:brightness(2.5)} 100%{filter:brightness(1)} }
.flash-hit { animation: flashWhite 0.2s ease; }
@keyframes attackLunge { 0%{transform:translateX(0)} 40%{transform:translateX(60px) scaleX(-1)} 100%{transform:translateX(0) scaleX(-1)} }
.attack-anim { animation: attackLunge 0.4s ease; }
@keyframes captureShake { 0%,100%{transform:rotate(0)} 25%{transform:rotate(-15deg)} 75%{transform:rotate(15deg)} }
.capture-shake { animation: captureShake 0.3s ease-in-out; }
/* Overlays */
.overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 200; }
.levelup-box, .evolution-box, .victory-box, .defeat-box { text-align: center; padding: 32px; background: rgba(20,15,40,0.95); backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.15); border-radius: 16px; max-width: 360px; }
.levelup-box h3, .evolution-box h3 { font-family: 'Press Start 2P', monospace; font-size: 0.9rem; color: #fbbf24; margin-bottom: 12px; }
.victory-box h3 { font-family: 'Press Start 2P', monospace; font-size: 1rem; color: #4ade80; margin-bottom: 12px; }
.defeat-box h3 { font-family: 'Press Start 2P', monospace; font-size: 0.9rem; color: #f87171; margin-bottom: 12px; }
.capture-animation { width: 150px; height: 150px; position: relative; }
.capture-prism {
width: 60px; height: 60px; background: linear-gradient(135deg, #a78bfa, #60a5fa);
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
}
/* Shop */
.shop-container, .dex-container, .party-container { width: 90%; max-width: 700px; text-align: center; }
.shop-container h2, .dex-container h2, .party-container h2 { font-family: 'Press Start 2P', monospace; font-size: 1rem; color: #a78bfa; margin-bottom: 16px; }
.shard-display { color: #fbbf24; margin-bottom: 16px; font-weight: 600; }
.shop-grid, .dex-grid, .party-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; max-height: 50vh; overflow-y: auto; padding: 4px; }
.shop-item, .dex-entry, .party-slot {
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 14px; text-align: left;
}
.shop-item h4 { font-size: 0.85rem; margin-bottom: 4px; }
.shop-item p { font-size: 0.7rem; color: #94a3b8; margin-bottom: 8px; }
.buy-btn { padding: 6px 12px; border-radius: 6px; background: rgba(124,58,237,0.3); border: 1px solid #7c3aed; color: #c4b5fd; cursor: pointer; font-size: 0.7rem; }
.buy-btn:hover { background: rgba(124,58,237,0.5); }
.dex-entry { text-align: center; padding: 16px; }
.dex-entry .dex-icon { width: 60px; height: 60px; margin: 0 auto 8px; }
.dex-entry h4 { font-size: 0.8rem; margin-bottom: 2px; }
.dex-entry p { font-size: 0.65rem; color: #94a3b8; }
.dex-undiscovered { opacity: 0.3; filter: blur(1px); }
.party-slot { text-align: center; padding: 12px; cursor: default; }
.party-slot .party-icon { width: 50px; height: 50px; margin: 0 auto 6px; }
.party-slot h4 { font-size: 0.8rem; }
.party-slot p { font-size: 0.65rem; color: #94a3b8; }
.gameover-content { text-align: center; }
.gameover-content h1 { font-family: 'Press Start 2P', monospace; font-size: 1.5rem; margin-bottom: 16px; }
#gameover-title.win { color: #4ade80; }
#gameover-title.lose { color: #f87171; }
/* Particles */
.particle { position: absolute; pointer-events: none; border-radius: 50%; animation: particleFade 1s ease-out forwards; }
@keyframes particleFade { 0%{opacity:1;transform:translateY(0) scale(1)} 100%{opacity:0;transform:translateY(-40px) scale(0.3)} }
/* Responsive */
@media (max-width: 600px) {
.battle-scene { flex-direction: column; gap: 12px; padding: 8px; }
.enemy-area, .player-area { width: 90%; }
.resonant-sprite { width: 80px; height: 80px; }
.battle-actions { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div id="game-container">
<!-- MAIN MENU -->
<div id="screen-menu" class="screen active">
<div class="menu-content">
<h1 class="game-title">AETHELGARD</h1>
<p class="game-subtitle">Resonant Chronicles</p>
<div class="prism-logo"></div>
<button id="btn-new-game" class="menu-btn primary">New Game</button>
<button id="btn-continue" class="menu-btn secondary hidden">Continue</button>
<button id="btn-reset" class="menu-btn danger hidden">Reset Progress</button>
</div>
</div>
<!-- OVERWORLD MAP -->
<div id="screen-map" class="screen">
<div class="map-header">
<h2 id="zone-title">The Verdant Canopy</h2>
<div class="header-stats">
<span id="shard-count">💎 0</span>
<button id="btn-shop" class="icon-btn">🏪 Shop</button>
<button id="btn-dex" class="icon-btn">📖 Dex</button>
<button id="btn-party" class="icon-btn">👥 Party</button>
<button id="btn-menu-back" class="icon-btn">☰ Menu</button>
</div>
</div>
<div id="map-area" class="map-area"></div>
<div id="map-dialog" class="dialog-overlay hidden">
<div class="dialog-box">
<p id="dialog-text"></p>
<button id="dialog-ok" class="menu-btn primary small">OK</button>
</div>
</div>
</div>
<!-- BATTLE SCREEN -->
<div id="screen-battle" class="screen">
<div class="battle-scene">
<div class="enemy-area">
<div class="resonant-card enemy-card">
<div class="card-header">
<span id="enemy-name">Wild Emberkit</span>
<span id="enemy-level" class="level-badge">Lv 3</span>
</div>
<div class="hp-bar-container">
<div id="enemy-hp-bar" class="hp-bar"></div>
</div>
<span id="enemy-hp-text">20/20 HP</span>
</div>
<div id="enemy-sprite" class="resonant-sprite enemy-sprite"></div>
</div>
<div class="player-area">
<div id="player-sprite" class="resonant-sprite player-sprite"></div>
<div class="resonant-card player-card">
<div class="card-header">
<span id="player-name">Emberkit</span>
<span id="player-level" class="level-badge">Lv 3</span>
</div>
<div class="hp-bar-container">
<div id="player-hp-bar" class="hp-bar"></div>
</div>
<span id="player-hp-text">20/20 HP</span>
<div id="player-status-effects" class="status-effects"></div>
</div>
</div>
</div>
<div class="battle-ui">
<div id="battle-log" class="battle-log"></div>
<div id="battle-actions" class="battle-actions">
<button id="btn-attack" class="action-btn primary">⚔️ Attack</button>
<button id="btn-prism" class="action-btn secondary">🔮 Prism</button>
<button id="btn-exchange" class="action-btn tertiary">🔄 Exchange</button>
<button id="btn-inventory" class="action-btn quaternary">🎒 Items</button>
</div>
<div id="move-select" class="move-select hidden"></div>
<div id="prism-select" class="item-select hidden"></div>
<div id="exchange-panel" class="exchange-panel hidden"></div>
<div id="inventory-panel" class="item-select hidden"></div>
</div>
</div>
<!-- SHOP SCREEN -->
<div id="screen-shop" class="screen">
<div class="shop-container">
<h2>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-close" class="menu-btn secondary">Close Shop</button>
</div>
</div>
<!-- DEX SCREEN -->
<div id="screen-dex" class="screen">
<div class="dex-container">
<h2>Conductors Dex</h2>
<p class="dex-info">Discovered: <span id="dex-count">0</span>/18</p>
<div id="dex-grid" class="dex-grid"></div>
<button id="btn-dex-close" class="menu-btn secondary">Close Dex</button>
</div>
</div>
<!-- PARTY SCREEN -->
<div id="screen-party" class="screen">
<div class="party-container">
<h2>Your Resonants</h2>
<div id="party-grid" class="party-grid"></div>
<button id="btn-party-close" class="menu-btn secondary">Close</button>
</div>
</div>
<!-- CAPTURE ANIMATION -->
<div id="capture-overlay" class="overlay hidden">
<div class="capture-animation">
<div id="capture-prism" class="capture-prism"></div>
</div>
</div>
<!-- LEVEL UP OVERLAY -->
<div id="levelup-overlay" class="overlay hidden">
<div class="levelup-box">
<h3 id="levelup-name">Emberkit</h3>
<p id="levelup-text">Reached Level 4!</p>
<button id="btn-levelup-ok" class="menu-btn primary small">OK</button>
</div>
</div>
<!-- EVOLUTION OVERLAY -->
<div id="evolution-overlay" class="overlay hidden">
<div class="evolution-box">
<h3>Evolving...</h3>
<div id="evo-sprite" class="evo-sprite"></div>
<p id="evo-text">Emberkit is evolving!</p>
<button id="btn-evo-ok" class="menu-btn primary small hidden">OK</button>
</div>
</div>
<!-- VICTORY OVERLAY -->
<div id="victory-overlay" class="overlay hidden">
<div class="victory-box">
<h3>Battle Victory!</h3>
<p id="victory-text"></p>
<button id="btn-victory-ok" class="menu-btn primary small">Continue</button>
</div>
</div>
<!-- DEFEAT OVERLAY -->
<div id="defeat-overlay" class="overlay hidden">
<div class="defeat-box">
<h3>All Resonants Fainted!</h3>
<p>You've been returned to the last healing spring.</p>
<button id="btn-defeat-ok" class="menu-btn primary small">Continue</button>
</div>
</div>
<!-- GAME OVER / WIN -->
<div id="screen-gameover" class="screen">
<div class="gameover-content">
<h1 id="gameover-title">Victory!</h1>
<p id="gameover-text"></p>
<button id="btn-gameover-menu" class="menu-btn primary">Return to Menu</button>
</div>
</div>
</div>
<script>
'use strict';
// ── class: SoundManager ──
class SoundManager {
constructor() { this.ctx = null; }
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.enabled !== false) return;
if (!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.15); setTimeout(() => this._tone(900, 0.06, 'square', 0.1), 50); break;
case 'hit': this._noise(0.12, 0.3); break;
case 'hitelem': this._tone(400, 0.15, 'sawtooth', 0.25); setTimeout(() => this._tone(600, 0.1, 'sawtooth', 0.2), 80); break;
case 'supereff': [300,500,700].forEach((f,i) => setTimeout(() => this._tone(f, 0.12, 'square', 0.25), i*60)); break;
case 'levelup': [523,659,784,1047].forEach((f,i) => setTimeout(() => this._tone(f, 0.15, 'sine', 0.2), i*100)); break;
case 'capture': [400,500,600,800,1000].forEach((f,i) => setTimeout(() => this._tone(f, 0.1, 'sine', 0.15), i*80)); break;
case 'victory': [523,659,784,1047,784,1047].forEach((f,i) => setTimeout(() => this._tone(f, 0.12, 'square', 0.15), i*120)); break;
case 'defeat': [400,350,300,250].forEach((f,i) => setTimeout(() => this._tone(f, 0.2, 'sine', 0.15), i*200)); break;
case 'evolve': [400,500,600,700,800,900,1000].forEach((f,i) => setTimeout(() => this._tone(f, 0.08, 'sine', 0.2), i*70)); break;
case 'boss': [200,250,300,250,200].forEach((f,i) => setTimeout(() => this._tone(f, 0.15, 'sawtooth', 0.15), i*150)); break;
}
}
_tone(freq, dur, wave='square', vol=0.1) {
const o = this.ctx.createOscillator(), 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 d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
const src = this.ctx.createBufferSource(), 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: { name:'Pyro', strongAgainst:['flora'], weakAgainst:['hydro'], color:'#ff5722' },
hydro: { name:'Hydro', strongAgainst:['pyro'], weakAgainst:['flora'], color:'#2196f3' },
flora: { name:'Flora', strongAgainst:['hydro'], weakAgainst:['pyro'], color:'#4caf50' },
zephyr: { name:'Zephyr', strongAgainst:['earth'], weakAgainst:['volt'], color:'#00e676' },
earth: { name:'Earth', strongAgainst:['volt'], weakAgainst:['zephyr'], color:'#a1887f' },
volt: { name:'Volt', strongAgainst:['zephyr'], weakAgainst:['earth'], color:'#ebd841' },
cosmos: { name:'Cosmos', strongAgainst:[], weakAgainst:[], color:'#a78bfa' }
};
// ── const: MOVES_DB ──
const MOVES_DB = {
ember_blast:{name:'Ember Blast',power:40,accuracy:100,type:'pyro'},
flame_wheel:{name:'Flame Wheel',power:60,accuracy:95,type:'pyro'},
inferno:{name:'Inferno',power:90,accuracy:85,type:'pyro'},
magma_fist:{name:'Magma Fist',power:75,accuracy:90,type:'pyro'},
water_jet:{name:'Water Jet',power:40,accuracy:100,type:'hydro'},
tidal_wave:{name:'Tidal Wave',power:65,accuracy:90,type:'hydro'},
abyssal_rage:{name:'Abyssal Rage',power:90,accuracy:85,type:'hydro'},
aqua_slash:{name:'Aqua Slash',power:70,accuracy:95,type:'hydro'},
vine_whip:{name:'Vine Whip',power:40,accuracy:100,type:'flora'},
thorn_barrage:{name:'Thorn Barrage',power:60,accuracy:95,type:'flora'},
bloom_storm:{name:'Bloom Storm',power:85,accuracy:85,type:'flora'},
root_crush:{name:'Root Crush',power:70,accuracy:90,type:'flora'},
spark_bolt:{name:'Spark Bolt',power:40,accuracy:100,type:'volt'},
thunder_claw:{name:'Thunder Claw',power:65,accuracy:95,type:'volt'},
lightning_storm:{name:'Lightning Storm',power:90,accuracy:80,type:'volt'},
volt_strike:{name:'Volt Strike',power:75,accuracy:90,type:'volt'},
gust_blast:{name:'Gust Blast',power:40,accuracy:100,type:'zephyr'},
cyclone:{name:'Cyclone',power:65,accuracy:90,type:'zephyr'},
tempest_fury:{name:'Tempest Fury',power:85,accuracy:85,type:'zephyr'},
wind_shear:{name:'Wind Shear',power:70,accuracy:95,type:'zephyr'},
rock_throw:{name:'Rock Throw',power:40,accuracy:100,type:'earth'},
stone_slam:{name:'Stone Slam',power:65,accuracy:90,type:'earth'},
quake_rage:{name:'Quake Rage',power:85,accuracy:85,type:'earth'},
boulder_crush:{name:'Boulder Crush',power:75,accuracy:90,type:'earth'},
tackle:{name:'Tackle',power:30,accuracy:100,type:'cosmos'},
quick_attack:{name:'Quick Attack',power:35,accuracy:100,type:'cosmos'},
body_slam:{name:'Body Slam',power:55,accuracy:95,type:'cosmos'},
headbutt:{name:'Headbutt',power:60,accuracy:90,type:'cosmos'},
heal_wave:{name:'Heal Wave',power:0,accuracy:100,type:'cosmos',effect:'heal30'}
};
// ── const: RESONANTS_DB ──
const RESONANTS_DB = [
{id:1,name:'Emberkit',type:'pyro',baseHp:35,baseAtk:28,baseDef:22,baseSpd:24,evoLevel:8,evoId:2,moves:[{move:'tackle',level:1},{move:'ember_blast',level:3},{move:'flame_wheel',level:7},{move:'body_slam',level:10}]},
{id:2,name:'Pyrowolf',type:'pyro',baseHp:55,baseAtk:42,baseDef:35,baseSpd:38,evoLevel:15,evoId:3,moves:[{move:'tackle',level:1},{move:'ember_blast',level:1},{move:'flame_wheel',level:1},{move:'magma_fist',level:10},{move:'body_slam',level:12}]},
{id:3,name:'Ignisaur',type:'pyro',baseHp:80,baseAtk:62,baseDef:50,baseSpd:55,evoLevel:null,moves:[{move:'ember_blast',level:1},{move:'flame_wheel',level:1},{move:'magma_fist',level:1},{move:'inferno',level:18}]},
{id:4,name:'Droplet',type:'hydro',baseHp:38,baseAtk:25,baseDef:20,baseSpd:26,evoLevel:8,evoId:5,moves:[{move:'tackle',level:1},{move:'water_jet',level:3},{move:'tidal_wave',level:7},{move:'body_slam',level:10}]},
{id:5,name:'Geoxus',type:'hydro',baseHp:58,baseAtk:40,baseDef:36,baseSpd:35,evoLevel:15,evoId:6,moves:[{move:'tackle',level:1},{move:'water_jet',level:1},{move:'tidal_wave',level:1},{move:'aqua_slash',level:10},{move:'body_slam',level:12}]},
{id:6,name:'Neptunian',type:'hydro',baseHp:85,baseAtk:58,baseDef:52,baseSpd:48,evoLevel:null,moves:[{move:'water_jet',level:1},{move:'tidal_wave',level:1},{move:'aqua_slash',level:1},{move:'abyssal_rage',level:18}]},
{id:7,name:'Seedling',type:'flora',baseHp:40,baseAtk:22,baseDef:25,baseSpd:20,evoLevel:8,evoId:8,moves:[{move:'tackle',level:1},{move:'vine_whip',level:3},{move:'thorn_barrage',level:7},{move:'heal_wave',level:10}]},
{id:8,name:'Thornback',type:'flora',baseHp:60,baseAtk:38,baseDef:42,baseSpd:30,evoLevel:15,evoId:9,moves:[{move:'tackle',level:1},{move:'vine_whip',level:1},{move:'thorn_barrage',level:1},{move:'root_crush',level:10},{move:'heal_wave',level:12}]},
{id:9,name:'Bloomgoliath',type:'flora',baseHp:90,baseAtk:55,baseDef:65,baseSpd:35,evoLevel:null,moves:[{move:'vine_whip',level:1},{move:'thorn_barrage',level:1},{move:'root_crush',level:1},{move:'bloom_storm',level:18}]},
{id:10,name:'Sparky',type:'volt',baseHp:32,baseAtk:30,baseDef:18,baseSpd:30,evoLevel:8,evoId:11,moves:[{move:'quick_attack',level:1},{move:'spark_bolt',level:3},{move:'thunder_claw',level:7},{move:'body_slam',level:10}]},
{id:11,name:'Boltsaber',type:'volt',baseHp:52,baseAtk:45,baseDef:30,baseSpd:48,evoLevel:15,evoId:12,moves:[{move:'quick_attack',level:1},{move:'spark_bolt',level:1},{move:'thunder_claw',level:1},{move:'volt_strike',level:10},{move:'body_slam',level:12}]},
{id:12,name:'Stormlord',type:'volt',baseHp:75,baseAtk:65,baseDef:45,baseSpd:68,evoLevel:null,moves:[{move:'spark_bolt',level:1},{move:'thunder_claw',level:1},{move:'volt_strike',level:1},{move:'lightning_storm',level:18}]},
{id:13,name:'Gusty',type:'zephyr',baseHp:30,baseAtk:26,baseDef:20,baseSpd:32,evoLevel:8,evoId:14,moves:[{move:'quick_attack',level:1},{move:'gust_blast',level:3},{move:'cyclone',level:7},{move:'body_slam',level:10}]},
{id:14,name:'Slipstream',type:'zephyr',baseHp:50,baseAtk:42,baseDef:32,baseSpd:52,evoLevel:15,evoId:15,moves:[{move:'quick_attack',level:1},{move:'gust_blast',level:1},{move:'cyclone',level:1},{move:'wind_shear',level:10},{move:'body_slam',level:12}]},
{id:15,name:'Zephyrus',type:'zephyr',baseHp:72,baseAtk:60,baseDef:48,baseSpd:75,evoLevel:null,moves:[{move:'gust_blast',level:1},{move:'cyclone',level:1},{move:'wind_shear',level:1},{move:'tempest_fury',level:18}]},
{id:16,name:'Pebble',type:'earth',baseHp:42,baseAtk:24,baseDef:28,baseSpd:16,evoLevel:8,evoId:17,moves:[{move:'tackle',level:1},{move:'rock_throw',level:3},{move:'stone_slam',level:7},{move:'headbutt',level:10}]},
{id:17,name:'Obsidian',type:'earth',baseHp:65,baseAtk:40,baseDef:48,baseSpd:25,evoLevel:15,evoId:18,moves:[{move:'tackle',level:1},{move:'rock_throw',level:1},{move:'stone_slam',level:1},{move:'boulder_crush',level:10},{move:'headbutt',level:12}]},
{id:18,name:'Terrasunder',type:'earth',baseHp:95,baseAtk:56,baseDef:70,baseSpd:30,evoLevel:null,moves:[{move:'rock_throw',level:1},{move:'stone_slam',level:1},{move:'boulder_crush',level:1},{move:'quake_rage',level:18}]}
];
// ── const: RESONANT_MAP ──
const RESONANT_MAP = Object.fromEntries(RESONANTS_DB.map(r => [r.id, r]));
// ── function: generateSpriteSVG ──
function generateSpriteSVG(resonantId, size = 120) {
const templates = {
1: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff6b35"/><stop offset="100%" stop-color="#ff9800"/></linearGradient></defs><ellipse cx="50" cy="55" rx="28" ry="24" fill="url(#g1)"/><circle cx="50" cy="38" r="18" fill="url(#g1)"/><circle cx="42" cy="35" r="3" fill="#fff"/><circle cx="58" cy="35" r="3" fill="#fff"/><circle cx="42" cy="35" r="1.5" fill="#000"/><circle cx="58" cy="35" r="1.5" fill="#000"/><path d="M46 42 Q50 46 54 42" stroke="#c62828" stroke-width="1.5" fill="none"/><ellipse cx="35" cy="30" rx="5" ry="7" fill="#ff6b35"/><ellipse cx="65" cy="30" rx="5" ry="7" fill="#ff6b35"/><path d="M48 20 Q50 10 52 20" fill="#ff9800"/></svg>`,
2: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#d32f2f"/><stop offset="100%" stop-color="#ff5722"/></linearGradient></defs><ellipse cx="50" cy="58" rx="32" ry="26" fill="url(#g2)"/><circle cx="50" cy="36" r="20" fill="url(#g2)"/><polygon points="40,28 44,18 48,28" fill="#ff9800"/><polygon points="52,28 56,18 60,28" fill="#ff9800"/><circle cx="43" cy="33" r="3.5" fill="#fff"/><circle cx="57" cy="33" r="3.5" fill="#fff"/><circle cx="43" cy="33" r="1.8" fill="#b71c1c"/><circle cx="57" cy="33" r="1.8" fill="#b71c1c"/><path d="M44 42 L46 40 L48 42 L50 40 L52 42 L54 40 L56 42" stroke="#fff" stroke-width="1.5" fill="none"/></svg>`,
3: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g3" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#b71c1c"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient></defs><ellipse cx="50" cy="55" rx="35" ry="30" fill="url(#g3)"/><circle cx="50" cy="32" r="22" fill="url(#g3)"/><path d="M38 16 Q42 4 46 16" fill="#ff9800"/><path d="M54 16 Q58 4 62 16" fill="#ff9800"/><circle cx="42" cy="29" r="4" fill="#fff"/><circle cx="58" cy="29" r="4" fill="#fff"/><circle cx="42" cy="29" r="2" fill="#b71c1c"/><circle cx="58" cy="29" r="2" fill="#b71c1c"/><path d="M30 55 Q20 65 25 75" stroke="#ff6b35" stroke-width="4" fill="none"/><path d="M70 55 Q80 65 75 75" stroke="#ff6b35" stroke-width="4" fill="none"/></svg>`,
4: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g4" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#42a5f5"/><stop offset="100%" stop-color="#1e88e5"/></linearGradient></defs><ellipse cx="50" cy="55" rx="26" ry="22" fill="url(#g4)"/><circle cx="50" cy="38" r="17" fill="url(#g4)"/><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="#0d47a1"/><circle cx="57" cy="35" r="1.5" fill="#0d47a1"/><path d="M46 42 Q50 45 54 42" stroke="#0d47a1" stroke-width="1.5" fill="none"/><ellipse cx="50" cy="80" rx="12" ry="6" fill="#90caf9" opacity="0.6"/></svg>`,
5: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g5" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#1565c0"/><stop offset="100%" stop-color="#42a5f5"/></linearGradient></defs><ellipse cx="50" cy="58" rx="30" ry="25" fill="url(#g5)"/><circle cx="50" cy="36" r="19" fill="url(#g5)"/><path d="M42 22 Q50 12 58 22" fill="#1e88e5"/><circle cx="43" cy="33" r="3.5" fill="#fff"/><circle cx="57" cy="33" r="3.5" fill="#fff"/><circle cx="43" cy="33" r="1.8" fill="#0d47a1"/><circle cx="57" cy="33" r="1.8" fill="#0d47a1"/><path d="M28 60 Q20 70 25 80" stroke="#42a5f5" stroke-width="3" fill="none"/><path d="M72 60 Q80 70 75 80" stroke="#42a5f5" stroke-width="3" fill="none"/></svg>`,
6: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g6" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#0d47a1"/><stop offset="100%" stop-color="#1e88e5"/></linearGradient></defs><ellipse cx="50" cy="55" rx="36" ry="30" fill="url(#g6)"/><circle cx="50" cy="32" r="22" fill="url(#g6)"/><path d="M38 14 Q50 2 62 14" fill="#1565c0"/><circle cx="42" cy="29" r="4" fill="#fff"/><circle cx="58" cy="29" r="4" fill="#fff"/><circle cx="42" cy="29" r="2" fill="#0d47a1"/><circle cx="58" cy="29" r="2" fill="#0d47a1"/><path d="M30 55 Q15 65 20 80" stroke="#42a5f5" stroke-width="5" fill="none"/><path d="M70 55 Q85 65 80 80" stroke="#42a5f5" stroke-width="5" fill="none"/><circle cx="35" cy="50" r="3" fill="#90caf9" opacity="0.5"/><circle cx="65" cy="50" r="3" fill="#90caf9" opacity="0.5"/></svg>`,
7: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g7" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#66bb6a"/><stop offset="100%" stop-color="#43a047"/></linearGradient></defs><ellipse cx="50" cy="58" rx="24" ry="20" fill="url(#g7)"/><circle cx="50" cy="40" r="16" fill="url(#g7)"/><path d="M50 24 Q48 14 50 8 Q52 14 50 24" fill="#8bc34a"/><circle cx="44" cy="37" r="2.5" fill="#fff"/><circle cx="56" cy="37" r="2.5" fill="#fff"/><circle cx="44" cy="37" r="1.2" fill="#1b5e20"/><circle cx="56" cy="37" r="1.2" fill="#1b5e20"/><path d="M46 44 Q50 47 54 44" stroke="#1b5e20" stroke-width="1.5" fill="none"/></svg>`,
8: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g8" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#2e7d32"/><stop offset="100%" stop-color="#4caf50"/></linearGradient></defs><ellipse cx="50" cy="58" rx="28" ry="24" fill="url(#g8)"/><circle cx="50" cy="36" r="18" fill="url(#g8)"/><path d="M44 20 Q50 10 56 20" fill="#66bb6a"/><polygon points="38,30 36,22 40,28" fill="#ffeb3b"/><polygon points="62,30 64,22 60,28" fill="#ffeb3b"/><circle cx="43" cy="34" r="3" fill="#fff"/><circle cx="57" cy="34" r="3" fill="#fff"/><circle cx="43" cy="34" r="1.5" fill="#1b5e20"/><circle cx="57" cy="34" r="1.5" fill="#1b5e20"/></svg>`,
9: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g9" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#1b5e20"/><stop offset="100%" stop-color="#388e3c"/></linearGradient></defs><ellipse cx="50" cy="55" rx="34" ry="28" fill="url(#g9)"/><circle cx="50" cy="32" r="20" fill="url(#g9)"/><path d="M40 16 Q50 4 60 16" fill="#4caf50"/><circle cx="42" cy="29" r="4" fill="#fff"/><circle cx="58" cy="29" r="4" fill="#fff"/><circle cx="42" cy="29" r="2" fill="#1b5e20"/><circle cx="58" cy="29" r="2" fill="#1b5e20"/><path d="M30 55 Q18 60 22 72" stroke="#4caf50" stroke-width="4" fill="none"/><path d="M70 55 Q82 60 78 72" stroke="#4caf50" stroke-width="4" fill="none"/><circle cx="38" cy="48" r="4" fill="#e91e63" opacity="0.7"/><circle cx="62" cy="48" r="4" fill="#ffeb3b" opacity="0.7"/></svg>`,
10: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g10" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#fdd835"/><stop offset="100%" stop-color="#ffb300"/></linearGradient></defs><ellipse cx="50" cy="56" rx="24" ry="22" fill="url(#g10)"/><circle cx="50" cy="38" r="17" fill="url(#g10)"/><polygon points="48,22 50,10 52,22" fill="#ffeb3b"/><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="#f57f17"/><circle cx="57" cy="35" r="1.5" fill="#f57f17"/><path d="M46 42 Q50 45 54 42" stroke="#f57f17" stroke-width="1.5" fill="none"/></svg>`,
11: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g11" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f9a825"/><stop offset="100%" stop-color="#ff8f00"/></linearGradient></defs><ellipse cx="50" cy="58" rx="30" ry="24" fill="url(#g11)"/><circle cx="50" cy="36" r="19" fill="url(#g11)"/><polygon points="44,20 47,8 50,20" fill="#ffeb3b"/><polygon points="50,20 53,8 56,20" fill="#ffeb3b"/><circle cx="43" cy="33" r="3.5" fill="#fff"/><circle cx="57" cy="33" r="3.5" fill="#fff"/><circle cx="43" cy="33" r="1.8" fill="#f57f17"/><circle cx="57" cy="33" r="1.8" fill="#f57f17"/><path d="M28 60 Q20 68 24 78" stroke="#ffb300" stroke-width="3" fill="none"/></svg>`,
12: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g12" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f57f17"/><stop offset="100%" stop-color="#ff8f00"/></linearGradient></defs><ellipse cx="50" cy="55" rx="36" ry="30" fill="url(#g12)"/><circle cx="50" cy="32" r="22" fill="url(#g12)"/><polygon points="40,14 44,2 48,14" fill="#ffeb3b"/><polygon points="52,14 56,2 60,14" fill="#ffeb3b"/><circle cx="42" cy="29" r="4" fill="#fff"/><circle cx="58" cy="29" r="4" fill="#fff"/><circle cx="42" cy="29" r="2" fill="#f57f17"/><circle cx="58" cy="29" r="2" fill="#f57f17"/><path d="M30 55 Q16 62 20 78" stroke="#ffb300" stroke-width="5" fill="none"/><path d="M70 55 Q84 62 80 78" stroke="#ffb300" stroke-width="5" fill="none"/></svg>`,
13: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g13" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#b9f6ca"/><stop offset="100%" stop-color="#00e676"/></linearGradient></defs><ellipse cx="50" cy="58" rx="24" ry="20" fill="url(#g13)"/><circle cx="50" cy="40" r="16" fill="url(#g13)"/><path d="M44 26 Q50 18 56 26" fill="#69f0ae"/><circle cx="44" cy="37" r="2.5" fill="#fff"/><circle cx="56" cy="37" r="2.5" fill="#fff"/><circle cx="44" cy="37" r="1.2" fill="#00c853"/><circle cx="56" cy="37" r="1.2" fill="#00c853"/><path d="M46 44 Q50 47 54 44" stroke="#00c853" stroke-width="1.5" fill="none"/></svg>`,
14: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g14" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00c853"/><stop offset="100%" stop-color="#69f0ae"/></linearGradient></defs><ellipse cx="50" cy="58" rx="28" ry="24" fill="url(#g14)"/><circle cx="50" cy="36" r="18" fill="url(#g14)"/><path d="M42 22 Q50 12 58 22" fill="#b9f6ca"/><circle cx="43" cy="33" r="3" fill="#fff"/><circle cx="57" cy="33" r="3" fill="#fff"/><circle cx="43" cy="33" r="1.5" fill="#00c853"/><circle cx="57" cy="33" r="1.5" fill="#00c853"/><path d="M26 60 Q18 68 22 78" stroke="#00e676" stroke-width="3" fill="none"/></svg>`,
15: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g15" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00c853"/><stop offset="100%" stop-color="#b9f6ca"/></linearGradient></defs><ellipse cx="50" cy="55" rx="34" ry="28" fill="url(#g15)"/><circle cx="50" cy="32" r="20" fill="url(#g15)"/><path d="M40 16 Q50 4 60 16" fill="#69f0ae"/><circle cx="42" cy="29" r="4" fill="#fff"/><circle cx="58" cy="29" r="4" fill="#fff"/><circle cx="42" cy="29" r="2" fill="#00c853"/><circle cx="58" cy="29" r="2" fill="#00c853"/><path d="M30 55 Q16 62 20 78" stroke="#b9f6ca" stroke-width="4" fill="none"/><path d="M70 55 Q84 62 80 78" stroke="#b9f6ca" stroke-width="4" fill="none"/></svg>`,
16: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g16" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#a1887f"/><stop offset="100%" stop-color="#8d6e63"/></linearGradient></defs><ellipse cx="50" cy="58" rx="24" ry="20" fill="url(#g16)"/><circle cx="50" cy="40" r="17" fill="url(#g16)"/><path d="M44 26 Q50 18 56 26" fill="#bcaaa4"/><circle cx="43" cy="37" r="2.5" fill="#fff"/><circle cx="57" cy="37" r="2.5" fill="#fff"/><circle cx="43" cy="37" r="1.2" fill="#4e342e"/><circle cx="57" cy="37" r="1.2" fill="#4e342e"/><path d="M46 44 Q50 47 54 44" stroke="#4e342e" stroke-width="1.5" fill="none"/></svg>`,
17: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g17" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#5d4037"/><stop offset="100%" stop-color="#8d6e63"/></linearGradient></defs><ellipse cx="50" cy="58" rx="30" ry="24" fill="url(#g17)"/><circle cx="50" cy="36" r="19" fill="url(#g17)"/><path d="M42 22 Q50 12 58 22" fill="#a1887f"/><polygon points="38,30 36,22 40,28" fill="#d7ccc8"/><polygon points="62,30 64,22 60,28" fill="#d7ccc8"/><circle cx="43" cy="33" r="3" fill="#fff"/><circle cx="57" cy="33" r="3" fill="#fff"/><circle cx="43" cy="33" r="1.5" fill="#4e342e"/><circle cx="57" cy="33" r="1.5" fill="#4e342e"/></svg>`,
18: `<svg width="${size}" height="${size}" viewBox="0 0 100 100"><defs><linearGradient id="g18" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#3e2723"/><stop offset="100%" stop-color="#5d4037"/></linearGradient></defs><ellipse cx="50" cy="55" rx="36" ry="30" fill="url(#g18)"/><circle cx="50" cy="32" r="22" fill="url(#g18)"/><path d="M40 16 Q50 4 60 16" fill="#795548"/><circle cx="42" cy="29" r="4" fill="#fff"/><circle cx="58" cy="29" r="4" fill="#fff"/><circle cx="42" cy="29" r="2" fill="#3e2723"/><circle cx="58" cy="29" r="2" fill="#3e2723"/><path d="M30 55 Q16 62 20 78" stroke="#8d6e63" stroke-width="5" fill="none"/><path d="M70 55 Q84 62 80 78" stroke="#8d6e63" stroke-width="5" fill="none"/></svg>`
};
return templates[resonantId] || `<svg width="${size}" height="${size}"><circle cx="${size/2}" cy="${size/2}" r="${size/3}" fill="#a78bfa"/></svg>`;
}
// ── function: calcStats ──
function calcStats(template, level) {
const hp = Math.floor((template.baseHp * 2 + level * 0.5) * (1 + level / 100));
const atk = Math.floor(template.baseAtk * (1 + level / 50));
const def = Math.floor(template.baseDef * (1 + level / 50));
const spd = Math.floor(template.baseSpd * (1 + level / 50));
return { hp, maxHp: hp, atk, def, spd };
}
// ── function: calcDamage ──
function calcDamage(attacker, moveKey, defender) {
const move = MOVES_DB[moveKey];
if (!move || move.power === 0) return null;
const typeMod = TYPE_CHART[move.type]?.[defender.type] || 1;
const baseDmg = (((2 * attacker.level / 5 + 2) * move.power * (attacker.atk / defender.def)) / 50 + 2);
const randomFactor = 0.85 + Math.random() * 0.15;
let dmg = Math.max(1, Math.floor(baseDmg * typeMod * randomFactor));
const isCrit = Math.random() < (attacker.critBoost ? 0.3 : 0.1);
if (isCrit) dmg = Math.floor(dmg * 1.5);
return { damage: dmg, typeMod, isCrit };
}
// ── function: xpForLevel ──
function xpForLevel(level) { return Math.floor(20 + level * level * 1.5); }
// ── const: ZONES ──
const ZONES = [
{
id: 'verdant_canopy', name: 'The Verdant Canopy', icon: '🌿', levelRange: [1, 5],
types: ['flora', 'zephyr'],
nodes: [
{ type:'wild', label:'Tall Grass', icon:'🌱', resonants:[7,13] },
{ type:'cache', label:'Mystery Cache', icon:'📦', reward:{shards:25} },
{ type:'wild', label:'Deep Bushes', icon:'🌿', resonants:[7,8,13] },
{ type:'heal', label:'Healing Spring', icon:'💧' },
{ type:'npc', label:'Conductor Mira', icon:'⚔️', name:'Mira', team:[{id:7,level:4},{id:13,level:3}] },
{ type:'wild', label:'Ancient Grove', icon:'🌳', resonants:[8,13] },
{ type:'cache', label:'Hidden Cache', icon:'📦', reward:{shards:35} },
{ type:'boss', label:'Gatekeeper', icon:'👑', name:'Thornwarden', team:[{id:9,level:6}], dialog:'You dare enter the Verdant Canopy? Prove your worth, Conductor!' }
]
},
{
id: 'sunken_grotto', name: 'The Sunken Grotto', icon: '🌊', levelRange: [5, 10],
types: ['hydro', 'earth'],
nodes: [
{ type:'wild', label:'Shallow Pool', icon:'💧', resonants:[4,16] },
{ type:'cache', label:'Sunken Chest', icon:'📦', reward:{shards:40} },
{ type:'npc', label:'Conductor Kai', icon:'⚔️', name:'Kai', team:[{id:5,level:7},{id:16,level:6}] },
{ type:'wild', label:'Deep Caverns', icon:'🕳️', resonants:[4,5,16] },
{ type:'heal', label:'Crystal Spring', icon:'💧' },
{ type:'wild', label:'Underwater Ruins', icon:'🏛️', resonants:[5,17] },
{ type:'cache', label:'Treasure Vault', icon:'📦', reward:{shards:50} },
{ type:'boss', label:'Gatekeeper', icon:'👑', name:'Abyssal Maw', team:[{id:6,level:10},{id:17,level:9}], dialog:'The depths claim all who trespass. Turn back... or be consumed!' }
]
},
{
id: 'obsidian_crags', name: 'The Obsidian Crags', icon: '🔥', levelRange: [10, 15],
types: ['pyro', 'volt'],
nodes: [
{ type:'wild', label:'Lava Fields', icon:'🌋', resonants:[1,10] },
{ type:'npc', label:'Conductor Rex', icon:'⚔️', name:'Rex', team:[{id:2,level:11},{id:11,level:10}] },
{ type:'cache', label:'Magma Cache', icon:'📦', reward:{shards:60} },
{ type:'wild', label:'Volcanic Core', icon:'💥', resonants:[2,3,11] },
{ type:'heal', label:'Hot Springs', icon:'♨️' },
{ type:'wild', label:'Thunder Peak', icon:'⚡', resonants:[3,12] },
{ type:'cache', label:'Sky Cache', icon:'📦', reward:{shards:75} },
{ type:'boss', label:'Gatekeeper', icon:'👑', name:'Inferno Titan', team:[{id:3,level:14},{id:12,level:13}], dialog:'The crags burn with ancient fury! Only the strongest Conductors may pass!' }
]
},
{
id: 'apex_pillar', name: 'The Apex Pillar', icon: '⭐', levelRange: [15, 20],
types: ['pyro','hydro','flora','volt','zephyr','earth'],
nodes: [
{ type:'wild', label:'Cosmic Field', icon:'✨', resonants:[3,6,9,12,15,18] },
{ type:'npc', label:'Conductor Sage', icon:'⚔️', name:'Sage', team:[{id:3,level:15},{id:6,level:15}] },
{ type:'wild', label:'Starfall Arena', icon:'🌟', resonants:[9,12,18] },
{ type:'heal', label:'Eternal Spring', icon:'💧' },
{ type:'npc', label:'Conductor Prime', icon:'⚔️', name:'Prime', team:[{id:6,level:16},{id:9,level:15},{id:12,level:16}] },
{ type:'wild', label:'Final Challenge', icon:'💫', resonants:[3,6,9,12,15,18] },
{ type:'boss', label:'FINAL BOSS', icon:'👑', name:'The Conductor Supreme', team:[{id:3,level:18},{id:6,level:18},{id:9,level:17},{id:12,level:18}], dialog:'I am the apex of all Conductors. If you can defeat my Resonants, Aethelgard will sing your name forever!' }
]
}
];
// ── const: SHOP_ITEMS ──
const SHOP_ITEMS = [
{ id:'prism_basic', name:'Harmonizer Prism', desc:'Basic capture prism. Good catch rate.', price:30, type:'prism', tier:'basic' },
{ id:'prism_hyper', name:'Hyper-Conductive Prism', desc:'Advanced prism with high catch rate.', price:80, type:'prism', tier:'hyper' },
{ id:'elixir_hp', name:'Core Restorer', desc:'Restores 50% HP to active Resonant.', price:25, type:'item', effect:'heal50' },
{ id:'elixir_full', name:'Vital Elixir', desc:'Fully restores HP of active Resonant.', price:60, type:'item', effect:'healfull' },
{ id:'status_cure', name:'Resonance Cleanser', desc:'Heals all status conditions.', price:35, type:'item', effect:'cureall' }
];
// ── function: createPlayerResonant ──
function createPlayerResonant(templateId, level = 1) {
const template = RESONANT_MAP[templateId];
const stats = calcStats(template, level);
return {
id: Date.now() + Math.random(),
templateId,
name: template.name,
type: template.type,
level,
xp: 0,
xpToNext: xpForLevel(level),
hp: stats.hp,
maxHp: stats.maxHp,
atk: stats.atk,
def: stats.def,
spd: stats.spd,
moves: template.moves.filter(m => m.level <= level).map(m => m.move),
statusEffects: [],
critBoost: false
};
}
// ── function: createEnemyResonant ──
function createEnemyResonant(templateId, level) {
const template = RESONANT_MAP[templateId];
const stats = calcStats(template, level);
return {
id: 'enemy_' + Math.random(),
templateId,
name: template.name,
type: template.type,
level,
hp: stats.hp,
maxHp: stats.maxHp,
atk: stats.atk,
def: stats.def,
spd: stats.spd,
moves: template.moves.filter(m => m.level <= level).map(m => m.move),
statusEffects: [],
critBoost: false
};
}
// ── function: getAvailableMoves ──
function getAvailableMoves(resonant) {
const template = RESONANT_MAP[resonant.templateId];
return resonant.moves.map(k => ({ key: k, ...MOVES_DB[k] }));
}
// ── class: GameEngine ──
class GameEngine {
constructor() {
this.sound = new SoundManager();
this.state = null;
this.currentScreen = 'menu';
this.battleState = null;
this.isProcessing = false;
this._prevScreen = 'map';
}
init() {
this.loadGame();
this.setupEventListeners();
if (this.state) {
document.getElementById('btn-continue').classList.remove('hidden');
document.getElementById('btn-reset').classList.remove('hidden');
}
this.showScreen('menu');
}
setupEventListeners() {
const self = this;
document.getElementById('btn-new-game').onclick = () => self.startNewGame();
document.getElementById('btn-continue').onclick = () => self.continueGame();
document.getElementById('btn-reset').onclick = () => { if(confirm('Reset all progress?')) { localStorage.removeItem('aethelgard_save'); location.reload(); } };
document.getElementById('dialog-ok').onclick = () => self.closeDialog();
document.getElementById('btn-shop-close').onclick = () => self.showScreen(self._prevScreen);
document.getElementById('btn-dex-close').onclick = () => self.showScreen(self._prevScreen);
document.getElementById('btn-party-close').onclick = () => self.showScreen(self._prevScreen);
document.getElementById('btn-menu-back').onclick = () => { self._prevScreen = 'menu'; self.showScreen('menu'); };
document.getElementById('btn-shop').onclick = () => { self._prevScreen = 'shop'; self.renderShop(); self.showScreen('shop'); };
document.getElementById('btn-dex').onclick = () => { self._prevScreen = 'dex'; self.renderDex(); self.showScreen('dex'); };
document.getElementById('btn-party').onclick = () => { self._prevScreen = 'party'; self.renderParty(); self.showScreen('party'); };
document.getElementById('btn-attack').onclick = () => self.openMoveSelect();
document.getElementById('btn-prism').onclick = () => self.openPrismSelect();
document.getElementById('btn-exchange').onclick = () => self.openExchangePanel();
document.getElementById('btn-inventory').onclick = () => self.openInventoryPanel();
document.getElementById('btn-levelup-ok').onclick = () => { document.getElementById('levelup-overlay').classList.add('hidden'); };
document.getElementById('btn-evo-ok').onclick = () => { document.getElementById('evolution-overlay').classList.add('hidden'); };
document.getElementById('btn-victory-ok').onclick = () => self.afterVictory();
document.getElementById('btn-defeat-ok').onclick = () => self.afterDefeat();
document.getElementById('btn-gameover-menu').onclick = () => { localStorage.removeItem('aethelgard_save'); location.reload(); };
document.querySelectorAll('.menu-btn, .icon-btn, .action-btn, .buy-btn').forEach(b => {
b.addEventListener('mouseenter', () => self.sound.play('hover'));
});
}
startNewGame() {
this.sound.init();
const starterChoice = prompt("Choose your starter Resonant:\n1. Emberkit (Pyro)\n2. Droplet (Hydro)\n3. Seedling (Flora)", "1");
let starterId;
switch(starterChoice) {
case '1': starterId = 1; break;
case '2': starterId = 4; break;
case '3': starterId = 7; break;
default: starterId = 1;
}
this.state = {
party: [createPlayerResonant(starterId, 1)],
inventory: { prism_basic: 5, prism_hyper: 0, elixir_hp: 3, elixir_full: 1, status_cure: 2 },
shards: 50,
currentZone: 0,
zoneProgress: [0, -1, -1, -1],
discoveredResonants: new Set([starterId]),
bossesDefeated: [],
gameWon: false
};
this.saveGame();
this.showDialog("Welcome to Aethelgard! Your journey as a Conductor begins. Explore the zones, collect Resonants, and become the greatest Conductor!");
this._prevScreen = 'map';
this.renderMap();
this.showScreen('map');
}
continueGame() {
this.sound.init();
this._prevScreen = 'map';
this.renderMap();
this.showScreen('map');
}
saveGame() {
if (!this.state) return;
const s = JSON.parse(JSON.stringify(this.state));
s.discoveredResonants = Array.from(s.discoveredResonants);
localStorage.setItem('aethelgard_save', JSON.stringify(s));
}
loadGame() {
try {
const raw = localStorage.getItem('aethelgard_save');
if (raw) {
const s = JSON.parse(raw);
s.discoveredResonants = new Set(s.discoveredResonants);
this.state = s;
}
} catch(e) {}
}
showScreen(name) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const el = document.getElementById('screen-' + name);
if (el) { el.style.display = 'flex'; requestAnimationFrame(() => el.classList.add('active')); }
this.currentScreen = name;
}
showDialog(text, callback) {
document.getElementById('dialog-text').textContent = text;
const overlay = document.getElementById('map-dialog');
overlay.classList.remove('hidden');
const self = this;
const okBtn = document.getElementById('dialog-ok');
okBtn.onclick = () => { self.closeDialog(); if (callback) callback(); };
}
closeDialog() {
document.getElementById('map-dialog').classList.add('hidden');
}
renderMap() {
if (!this.state) return;
const zoneIdx = this.state.currentZone;
const zone = ZONES[zoneIdx];
document.getElementById('zone-title').textContent = zone.icon + ' ' + zone.name;
document.getElementById('shard-count').textContent = '\uD83D\uDC8E ' + this.state.shards;
const area = document.getElementById('map-area');
area.innerHTML = '';
const tabContainer = document.createElement('div');
tabContainer.className = 'zone-selector';
ZONES.forEach((z, i) => {
const tab = document.createElement('div');
tab.className = 'zone-tab' + (i === zoneIdx ? ' active' : '') + (this.state.zoneProgress[i] < 0 && i > 0 ? ' locked' : '');
tab.textContent = z.icon + ' ' + z.name;
if (this.state.zoneProgress[i] >= 0 && i !== zoneIdx) {
const zi = i;
tab.onclick = () => { this.state.currentZone = zi; this.saveGame(); this.renderMap(); };
}
tabContainer.appendChild(tab);
});
area.appendChild(tabContainer);
const path = document.createElement('div');
path.className = 'zone-path';
zone.nodes.forEach((node, i) => {
const el = document.createElement('div');
el.className = 'map-node';
const progress = this.state.zoneProgress[zoneIdx];
if (i < progress) el.classList.add('completed');
else if (i === progress) el.classList.add('current');
else if (progress < i - 1) el.classList.add('locked');
el.innerHTML = node.icon + '<span class="node-label">' + node.label + '</span>';
if (!el.classList.contains('locked')) {
const ni = i;
el.onclick = () => this.interactNode(zoneIdx, ni);
}
path.appendChild(el);
});
area.appendChild(path);
}
interactNode(zoneIdx, nodeIdx) {
if (nodeIdx !== this.state.zoneProgress[zoneIdx]) return;
const zone = ZONES[zoneIdx];
const node = zone.nodes[nodeIdx];
switch(node.type) {
case 'wild': this.startWildBattle(zoneIdx, node); break;
case 'cache': this.openCache(node); break;
case 'heal': this.healParty(); break;
case 'npc': this.startNPBBattle(zoneIdx, node); break;
case 'boss': this.startBossBattle(zoneIdx, node); break;
}
}
openCache(node) {
const shards = node.reward.shards || 20;
this.state.shards += shards;
this.saveGame();
this.showDialog('You found a Mystery Cache! It contained ' + shards + ' Aether Shards!');
this.advanceNode();
}
healParty() {
this.state.party.forEach(r => { r.hp = r.maxHp; r.statusEffects = []; r.critBoost = false; });
this.saveGame();
this.showDialog('Your Resonants have been fully restored at the Healing Spring!');
this.advanceNode();
}
advanceNode() {
const zoneIdx = this.state.currentZone;
if (this.state.zoneProgress[zoneIdx] < ZONES[zoneIdx].nodes.length - 1) {
this.state.zoneProgress[zoneIdx]++;
}
this.saveGame();
this.renderMap();
}
startWildBattle(zoneIdx, node) {
const zone = ZONES[zoneIdx];
const minLvl = zone.levelRange[0], maxLvl = zone.levelRange[1];
const lvl = minLvl + Math.floor(Math.random() * (maxLvl - minLvl + 1));
const resonantPool = node.resonants || zone.types.map(t => {
return RESONANTS_DB.filter(r => r.type === t && !r.evoLevel).map(r => r.id);
}).flat();
const templateId = resonantPool[Math.floor(Math.random() * resonantPool.length)];
const enemy = createEnemyResonant(templateId, lvl);
this.state.discoveredResonants.add(templateId);
this.saveGame();
this.startBattle(enemy, 'wild', zoneIdx);
}
startNPBBattle(zoneIdx, node) {
const enemies = node.team.map(t => createEnemyResonant(t.id, t.level));
this.state.discoveredResonants.add(enemies[0].templateId);
this.saveGame();
this.startBattle(enemies, 'npc', zoneIdx, node.name || 'Conductor');
}
startBossBattle(zoneIdx, node) {
const enemies = node.team.map(t => createEnemyResonant(t.id, t.level));
enemies.forEach(e => this.state.discoveredResonants.add(e.templateId));
this.saveGame();
this.sound.play('boss');
this.startBattle(enemies, 'boss', zoneIdx, node.name, node.dialog);
}
startBattle(enemyOrEnemies, battleType, zoneIdx, opponentName, dialog) {
const enemies = Array.isArray(enemyOrEnemies) ? enemyOrEnemies : [enemyOrEnemies];
this.battleState = {
type: battleType,
zoneIdx: zoneIdx,
opponentName: opponentName || 'Wild Resonant',
enemies: enemies,
currentEnemyIdx: 0,
playerActiveIdx: 0,
turn: 'player',
log: [],
isCapturing: false
};
if (dialog) {
this.showScreen('battle');
this.renderBattle();
setTimeout(() => {
this.addLog(dialog);
this.showDialog(dialog, () => {});
}, 300);
} else {
this.showScreen('battle');
this.renderBattle();
if (battleType === 'wild') {
this.addLog('A wild ' + enemies[0].name + ' appeared!');
} else {
this.addLog((opponentName || 'The opponent') + ' sent out ' + enemies[0].name + '!');
}
}
}
getActiveEnemy() { return this.battleState.enemies[this.battleState.currentEnemyIdx]; }
getActivePlayer() { return this.state.party[this.battleState.playerActiveIdx]; }
renderBattle() {
const enemy = this.getActiveEnemy();
const player = this.getActivePlayer();
if (!enemy || !player) return;
document.getElementById('enemy-name').textContent = enemy.name;
document.getElementById('enemy-level').textContent = 'Lv ' + enemy.level;
document.getElementById('enemy-hp-text').textContent = Math.max(0,enemy.hp) + '/' + enemy.maxHp + ' HP';
this.updateHPBar('enemy-hp-bar', enemy.hp, enemy.maxHp);
document.getElementById('player-name').textContent = player.name;
document.getElementById('player-level').textContent = 'Lv ' + player.level;
document.getElementById('player-hp-text').textContent = Math.max(0,player.hp) + '/' + player.maxHp + ' HP';
this.updateHPBar('player-hp-bar', player.hp, player.maxHp);
document.getElementById('enemy-sprite').innerHTML = generateSpriteSVG(enemy.templateId);
document.getElementById('player-sprite').innerHTML = generateSpriteSVG(player.templateId);
const statusEl = document.getElementById('player-status-effects');
let statusHTML = (player.statusEffects || []).map(s => '<span class="status-badge">' + s + '</span>').join('');
if (player.critBoost) statusHTML += '<span class="status-badge" style="background:rgba(251,191,36,0.2);color:#fbbf24">Focus</span>';
statusEl.innerHTML = statusHTML;
const logEl = document.getElementById('battle-log');
logEl.innerHTML = this.battleState.log.map(l => '<p>' + l + '</p>').join('');
logEl.scrollTop = logEl.scrollHeight;
document.getElementById('battle-actions').classList.remove('hidden');
document.getElementById('move-select').classList.add('hidden');
document.getElementById('prism-select').classList.add('hidden');
document.getElementById('exchange-panel').classList.add('hidden');
document.getElementById('inventory-panel').classList.add('hidden');
const basicCount = this.state.inventory.prism_basic || 0;
const hyperCount = this.state.inventory.prism_hyper || 0;
document.getElementById('btn-prism').textContent = '\uD83D\uDD2E Prism (' + (basicCount + hyperCount) + ')';
const aliveCount = this.state.party.filter(r => r.hp > 0).length;
document.getElementById('btn-exchange').style.opacity = aliveCount <= 1 ? '0.4' : '1';
}
updateHPBar(barId, hp, maxHp) {
const bar = document.getElementById(barId);
const pct = Math.max(0, (hp / maxHp) * 100);
bar.style.width = pct + '%';
if (pct > 50) bar.style.background = 'linear-gradient(90deg, #4ade80, #22c55e)';
else if (pct > 25) bar.style.background = 'linear-gradient(90deg, #fbbf24, #f59e0b)';
else bar.style.background = 'linear-gradient(90deg, #ef4444, #dc2626)';
}
addLog(text) { this.battleState.log.push(text); if (this.battleState.log.length > 50) this.battleState.log.shift(); }
openMoveSelect() {
const player = this.getActivePlayer();
const moves = getAvailableMoves(player);
const panel = document.getElementById('move-select');
panel.innerHTML = '';
moves.forEach(m => {
const btn = document.createElement('button');
btn.className = 'move-btn';
const typeClass = m.type === 'cosmos' ? '' : (m.type + '-text');
btn.innerHTML = '<strong>' + m.name + '</strong><span class="move-type ' + typeClass + '">' + (ELEMENTS[m.type] ? ELEMENTS[m.type].name : 'Cosmos') + ' | Pwr:' + m.power + (m.effect ? ' | Effect' : '') + '</span>';
const mk = m.key;
btn.onclick = () => this.executePlayerMove(mk);
panel.appendChild(btn);
});
document.getElementById('battle-actions').classList.add('hidden');
panel.classList.remove('hidden');
}
openPrismSelect() {
const panel = document.getElementById('prism-select');
panel.innerHTML = '';
const basicCount = this.state.inventory.prism_basic || 0;
const hyperCount = this.state.inventory.prism_hyper || 0;
if (basicCount > 0) {
const btn = document.createElement('button');
btn.className = 'prism-btn';
btn.innerHTML = '<strong>Harmonizer Prism</strong><span class="move-type">Basic tier | ' + basicCount + ' remaining</span>';
btn.onclick = () => this.attemptCapture('basic');
panel.appendChild(btn);
}
if (hyperCount > 0) {
const btn = document.createElement('button');
btn.className = 'prism-btn';
btn.innerHTML = '<strong>Hyper-Conductive Prism</strong><span class="move-type">Advanced tier | ' + hyperCount + ' remaining</span>';
btn.onclick = () => this.attemptCapture('hyper');
panel.appendChild(btn);
}
if (basicCount === 0 && hyperCount === 0) {
const p = document.createElement('p');
p.style.cssText = 'grid-column:1/-1;text-align:center;color:#94a3b8;padding:20px;';
p.textContent = 'No prisms remaining! Visit the shop.';
panel.appendChild(p);
}
const backBtn = document.createElement('button');
backBtn.className = 'prism-btn';
backBtn.style.gridColumn = '1 / -1';
backBtn.textContent = '\u2190 Back';
backBtn.onclick = () => this.renderBattle();
panel.appendChild(backBtn);
document.getElementById('battle-actions').classList.add('hidden');
panel.classList.remove('hidden');
}
openExchangePanel() {
const panel = document.getElementById('exchange-panel');
panel.innerHTML = '';
this.state.party.forEach((r, i) => {
if (i === this.battleState.playerActiveIdx) return;
const slot = document.createElement('div');
slot.className = 'exchange-slot' + (r.hp <= 0 ? ' fainted' : '');
slot.innerHTML = '<div style="width:40px;height:40px;margin:0 auto 6px">' + generateSpriteSVG(r.templateId, 40) + '</div><strong>' + r.name + '</strong> Lv' + r.level + '<br><span class="' + r.type + '-text" style="font-size:0.7rem">' + (ELEMENTS[r.type] ? ELEMENTS[r.type].name : '') + '</span><br><span style="font-size:0.65rem;color:#94a3b8">HP: ' + Math.max(0,r.hp) + '/' + r.maxHp + '</span>';
if (r.hp > 0) {
const ri = i;
slot.onclick = () => this.exchangeResonant(ri);
}
panel.appendChild(slot);
});
const backBtn = document.createElement('button');
backBtn.className = 'prism-btn';
backBtn.style.gridColumn = '1 / -1';
backBtn.textContent = '\u2190 Back';
backBtn.onclick = () => this.renderBattle();
panel.appendChild(backBtn);
document.getElementById('battle-actions').classList.add('hidden');
panel.classList.remove('hidden');
}
openInventoryPanel() {
const panel = document.getElementById('inventory-panel');
panel.innerHTML = '';
const inv = this.state.inventory;
if ((inv.elixir_hp || 0) > 0) {
const btn = document.createElement('button');
btn.className = 'inv-item-btn';
btn.innerHTML = '<strong>Core Restorer</strong><span class="move-type">Heals 50% HP | ' + inv.elixir_hp + ' remaining</span>';
btn.onclick = () => this.useItem('elixir_hp');
panel.appendChild(btn);
}
if ((inv.elixir_full || 0) > 0) {
const btn = document.createElement('button');
btn.className = 'inv-item-btn';
btn.innerHTML = '<strong>Vital Elixir</strong><span class="move-type">Fully heals HP | ' + inv.elixir_full + ' remaining</span>';
btn.onclick = () => this.useItem('elixir_full');
panel.appendChild(btn);
}
if ((inv.status_cure || 0) > 0) {
const btn = document.createElement('button');
btn.className = 'inv-item-btn';
btn.innerHTML = '<strong>Resonance Cleanser</strong><span class="move-type">Cures all status | ' + inv.status_cure + ' remaining</span>';
btn.onclick = () => this.useItem('status_cure');
panel.appendChild(btn);
}
const backBtn = document.createElement('button');
backBtn.className = 'prism-btn';
backBtn.style.gridColumn = '1 / -1';
backBtn.textContent = '\u2190 Back';
backBtn.onclick = () => this.renderBattle();
panel.appendChild(backBtn);
document.getElementById('battle-actions').classList.add('hidden');
panel.classList.remove('hidden');
}
useItem(itemId) {
const player = this.getActivePlayer();
if (!player || player.hp <= 0) return;
switch(itemId) {
case 'elixir_hp': player.hp = Math.min(player.maxHp, player.hp + Math.floor(player.maxHp * 0.5)); this.addLog(player.name + ' used Core Restorer! HP restored.'); break;
case 'elixir_full': player.hp = player.maxHp; this.addLog(player.name + ' used Vital Elixir! Fully healed!'); break;
case 'status_cure': player.statusEffects = []; player.critBoost = false; this.addLog(player.name + "'s status conditions cleared!"); break;
}
this.state.inventory[itemId]--;
this.saveGame();
this.renderBattle();
setTimeout(() => this.enemyTurn(), 800);
}
async executePlayerMove(moveKey) {
if (this.isProcessing) return;
this.isProcessing = true;
const player = this.getActivePlayer();
const enemy = this.getActiveEnemy();
const move = MOVES_DB[moveKey];
if (move.effect === 'heal30') {
const healAmt = Math.floor(player.maxHp * 0.3);
player.hp = Math.min(player.maxHp, player.hp + healAmt);
this.addLog(player.name + ' used ' + move.name + '! Recovered ' + healAmt + ' HP.');
this.sound.play('levelup');
this.renderBattle();
await this.delay(800);
this.isProcessing = false;
this.enemyTurn();
return;
}
const result = calcDamage(player, moveKey, enemy);
if (!result) {
this.addLog(player.name + ' used ' + move.name + '!');
await this.delay(500);
this.isProcessing = false;
this.enemyTurn();
return;
}
const playerSprite = document.getElementById('player-sprite');
playerSprite.classList.add('attack-anim');
setTimeout(() => playerSprite.classList.remove('attack-anim'), 400);
await this.delay(300);
enemy.hp -= result.damage;
if (result.typeMod >= 2) this.sound.play('supereff');
else if (result.typeMod <= 0.5) this.sound.play('hit');
else this.sound.play('hitelem');
let msg = player.name + ' used ' + move.name + '! Dealt ' + result.damage + ' damage.';
if (result.isCrit) { msg += ' Critical hit!'; document.getElementById('screen-battle').classList.add('shake'); setTimeout(() => document.getElementById('screen-battle').classList.remove('shake'), 300); }
if (result.typeMod >= 2) msg += ' Super effective!';
else if (result.typeMod <= 0.5) msg += ' Not very effective...';
this.addLog(msg);
const enemySprite = document.getElementById('enemy-sprite');
enemySprite.classList.add('flash-hit');
setTimeout(() => enemySprite.classList.remove('flash-hit'), 200);
this.renderBattle();
await this.delay(600);
if (enemy.hp <= 0) {
this.addLog(enemy.name + ' fainted!');
await this.delay(500);
this.handleEnemyFaint();
} else {
this.isProcessing = false;
this.enemyTurn();
}
}
async attemptCapture(tier) {
if (this.isProcessing) return;
this.isProcessing = true;
const enemy = this.getActiveEnemy();
const hpRatio = Math.max(0, enemy.hp / enemy.maxHp);
let catchRate = tier === 'hyper' ? 0.7 : 0.45;
catchRate *= (1 - hpRatio * 0.8);
if (tier === 'basic') this.state.inventory.prism_basic--;
else this.state.inventory.prism_hyper--;
this.saveGame();
const overlay = document.getElementById('capture-overlay');
overlay.classList.remove('hidden');
const prismEl = document.getElementById('capture-prism');
prismEl.style.animation = 'none';
await this.delay(100);
prismEl.style.animation = '';
for (let i = 0; i < 3; i++) {
prismEl.classList.add('capture-shake');
await this.delay(400);
prismEl.classList.remove('capture-shake');
await this.delay(200);
}
const caught = Math.random() < catchRate;
overlay.classList.add('hidden');
if (caught) {
this.sound.play('capture');
this.addLog('Gotcha! ' + enemy.name + ' was captured!');
const newResonant = createPlayerResonant(enemy.templateId, enemy.level);
this.state.party.push(newResonant);
this.state.discoveredResonants.add(enemy.templateId);
this.saveGame();
await this.delay(1000);
this.battleState = null;
this.advanceNode();
this.showScreen('map');
} else {
this.sound.play('hit');
this.addLog(enemy.name + ' broke free!');
await this.delay(800);
this.renderBattle();
this.isProcessing = false;
this.enemyTurn();
}
}
exchangeResonant(idx) {
const oldActive = this.getActivePlayer();
if (oldActive.critBoost) oldActive.critBoost = false;
this.battleState.playerActiveIdx = idx;
const newActive = this.getActivePlayer();
this.addLog('Go, ' + newActive.name + '!');
this.renderBattle();
setTimeout(() => this.enemyTurn(), 800);
}
async enemyTurn() {
if (this.isProcessing) return;
this.isProcessing = true;
const enemy = this.getActiveEnemy();
const player = this.getActivePlayer();
if (!enemy || enemy.hp <= 0) { this.isProcessing = false; return; }
const moves = getAvailableMoves(enemy);
let bestMove = moves[0];
let bestDmg = -1;
for (const mk of moves) {
const m = MOVES_DB[mk.key];
if (m.power > 0) {
const d = calcDamage(enemy, mk.key, player);
if (d && d.damage > bestDmg) { bestDmg = d.damage; bestMove = mk.key; }
}
}
const move = MOVES_DB[bestMove];
this.addLog(enemy.name + ' used ' + move.name + '!');
const enemySprite = document.getElementById('enemy-sprite');
enemySprite.style.transition = 'transform 0.3s';
enemySprite.style.transform = 'translateX(-40px)';
await this.delay(200);
enemySprite.style.transform = '';
if (move.power > 0) {
const result = calcDamage(enemy, bestMove, player);
if (result) {
player.hp -= result.damage;
if (result.typeMod >= 2) this.sound.play('supereff');
else this.sound.play('hit');
let msg = 'Dealt ' + result.damage + ' damage.';
if (result.isCrit) msg += ' Critical hit!';
if (result.typeMod >= 2) msg += ' Super effective!';
this.addLog(msg);
document.getElementById('screen-battle').classList.add('shake');
setTimeout(() => document.getElementById('screen-battle').classList.remove('shake'), 300);
const playerSprite = document.getElementById('player-sprite');
playerSprite.classList.add('flash-hit');
setTimeout(() => playerSprite.classList.remove('flash-hit'), 200);
}
}
this.renderBattle();
await this.delay(600);
if (player.hp <= 0) {
this.addLog(player.name + ' fainted!');
await this.delay(500);
this.handlePlayerFaint();
} else {
this.isProcessing = false;
this.renderBattle();
}
}
handleEnemyFaint() {
const bs = this.battleState;
if (bs.currentEnemyIdx < bs.enemies.length - 1) {
bs.currentEnemyIdx++;
const nextEnemy = bs.enemies[bs.currentEnemyIdx];
this.addLog(bs.opponentName + ' sent out ' + nextEnemy.name + '!');
this.renderBattle();
setTimeout(() => { this.isProcessing = false; }, 500);
} else {
this.handleVictory();
}
}
handlePlayerFaint() {
const alive = this.state.party.filter(r => r.hp > 0);
if (alive.length === 0) {
document.getElementById('defeat-overlay').classList.remove('hidden');
this.sound.play('defeat');
} else {
for (let i = 0; i < this.state.party.length; i++) {
if (this.state.party[i].hp > 0 && i !== this.battleState.playerActiveIdx) {
this.battleState.playerActiveIdx = i;
this.addLog('Go, ' + this.state.party[i].name + '!');
this.renderBattle();
setTimeout(() => { this.isProcessing = false; }, 500);
return;
}
}
}
}
handleVictory() {
const bs = this.battleState;
const player = this.getActivePlayer();
let totalXP = 0;
let shardsReward = 0;
bs.enemies.forEach(e => {
const xpGain = Math.floor(15 + e.level * 3);
totalXP += xpGain;
shardsReward += Math.floor(8 + e.level * 2);
});
if (bs.type === 'boss') {
shardsReward *= 2;
this.state.bossesDefeated.push(bs.zoneIdx);
} else if (bs.type === 'npc') {
shardsReward = Math.floor(shardsReward * 1.5);
}
player.xp += totalXP;
this.state.shards += shardsReward;
const msg = 'Victory! Gained ' + totalXP + ' XP and ' + shardsReward + ' Aether Shards!';
document.getElementById('victory-text').textContent = msg;
document.getElementById('victory-overlay').classList.remove('hidden');
this.sound.play('victory');
while (player.xp >= player.xpToNext) {
player.xp -= player.xpToNext;
player.level++;
player.xpToNext = xpForLevel(player.level);
const newStats = calcStats(RESONANT_MAP[player.templateId], player.level);
player.maxHp = newStats.hp;
player.atk = newStats.atk;
player.def = newStats.def;
player.spd = newStats.spd;
player.hp = Math.min(player.hp + 10, player.maxHp);
const template = RESONANT_MAP[player.templateId];
template.moves.forEach(m => { if (m.level <= player.level && !player.moves.includes(m.move)) player.moves.push(m.move); });
this.addLog(player.name + ' reached Level ' + player.level + '!');
}
const template = RESONANT_MAP[player.templateId];
if (template.evoLevel && player.level >= template.evoLevel) {
setTimeout(() => this.evolveResonant(bs.playerActiveIdx), 1500);
} else {
this.saveGame();
}
if (bs.zoneIdx === 3 && bs.type === 'boss') {
this.state.gameWon = true;
this.saveGame();
}
}
afterVictory() {
document.getElementById('victory-overlay').classList.add('hidden');
if (this.state.gameWon) {
document.getElementById('gameover-title').textContent = '\uD83C\uDFC6 Victory!';
document.getElementById('gameover-title').className = 'win';
document.getElementById('gameover-text').textContent = 'You have defeated the Conductor Supreme and become the greatest Conductor in Aethelgard! Your Resonants sing with joy!';
this.showScreen('gameover');
} else {
this.battleState = null;
this.advanceNode();
this.showScreen('map');
}
}
afterDefeat() {
document.getElementById('defeat-overlay').classList.add('hidden');
this.state.party.forEach(r => { r.hp = Math.floor(r.maxHp * 0.5); r.statusEffects = []; });
this.saveGame();
this.battleState = null;
this.showScreen('map');
}
async evolveResonant(idx) {
const resonant = this.state.party[idx];
const template = RESONANT_MAP[resonant.templateId];
if (!template.evoLevel || resonant.level < template.evoLevel) return;
const evoTemplate = RESONANT_MAP[template.evoId];
document.getElementById('evo-text').textContent = resonant.name + ' is evolving!';
document.getElementById('evo-sprite').innerHTML = generateSpriteSVG(template.evoId, 100);
document.getElementById('evolution-overlay').classList.remove('hidden');
this.sound.play('evolve');
await this.delay(2000);
document.getElementById('evo-text').textContent = resonant.name + ' evolved into ' + evoTemplate.name + '!';
document.getElementById('btn-evo-ok').classList.remove('hidden');
resonant.templateId = template.evoId;
resonant.name = evoTemplate.name;
resonant.type = evoTemplate.type;
const newStats = calcStats(evoTemplate, resonant.level);
resonant.maxHp = newStats.hp;
resonant.atk = newStats.atk;
resonant.def = newStats.def;
resonant.spd = newStats.spd;
resonant.hp = Math.min(resonant.hp + 20, resonant.maxHp);
resonant.moves = evoTemplate.moves.filter(m => m.level <= resonant.level).map(m => m.move);
this.state.discoveredResonants.add(evoTemplate.id);
this.saveGame();
}
renderShop() {
document.getElementById('shop-shards').textContent = this.state.shards;
const grid = document.getElementById('shop-items');
grid.innerHTML = '';
SHOP_ITEMS.forEach(item => {
const el = document.createElement('div');
el.className = 'shop-item';
const canBuy = this.state.shards >= item.price;
el.innerHTML = '<h4>' + item.name + '</h4><p>' + item.desc + '</p><button class="buy-btn" ' + (canBuy ? '' : 'disabled style="opacity:0.4"') + '>💎 ' + item.price + '</button>';
const btn = el.querySelector('.buy-btn');
if (!btn.disabled) {
const iid = item.id;
const iprice = item.price;
btn.onclick = () => {
if (this.state.shards >= iprice) {
this.state.shards -= iprice;
this.state.inventory[iid] = (this.state.inventory[iid] || 0) + 1;
this.saveGame();
this.sound.play('select');
this.renderShop();
}
};
}
grid.appendChild(el);
});
}
renderDex() {
document.getElementById('dex-count').textContent = this.state.discoveredResonants.size;
const grid = document.getElementById('dex-grid');
grid.innerHTML = '';
RESONANTS_DB.forEach(r => {
const discovered = this.state.discoveredResonants.has(r.id);
const el = document.createElement('div');
el.className = 'dex-entry' + (discovered ? '' : ' dex-undiscovered');
el.innerHTML = '<div class="dex-icon">' + generateSpriteSVG(r.id, 60) + '</div><h4>' + (discovered ? r.name : '???') + '</h4><p class="' + r.type + '-text">' + (ELEMENTS[r.type] ? ELEMENTS[r.type].name : '') + ' | Base HP:' + r.baseHp + ' ATK:' + r.baseAtk + '</p>';
grid.appendChild(el);
});
}
renderParty() {
const grid = document.getElementById('party-grid');
grid.innerHTML = '';
this.state.party.forEach((r, i) => {
const el = document.createElement('div');
el.className = 'party-slot';
el.innerHTML = '<div class="party-icon">' + generateSpriteSVG(r.templateId, 50) + '</div><h4>' + r.name + '</h4><p>Lv ' + r.level + ' | HP: ' + Math.max(0,r.hp) + '/' + r.maxHp + '</p><p class="' + r.type + '-text">' + (ELEMENTS[r.type] ? ELEMENTS[r.type].name : '') + '</p>';
grid.appendChild(el);
});
}
delay(ms) { return new Promise(r => setTimeout(r, ms)); }
}
// ── const: TYPE_CHART ──
const TYPE_CHART = {
pyro:{pyro:1,hydro:0.5,flora:2,zephyr:1,earth:1,volt:1,cosmos:1},
hydro:{pyro:2,hydro:1,flora:0.5,zephyr:1,earth:1,volt:1,cosmos:1},
flora:{pyro:0.5,hydro:2,flora:1,zephyr:1,earth:1,volt:1,cosmos:1},
zephyr:{pyro:1,hydro:1,flora:1,zephyr:1,earth:2,volt:0.5,cosmos:1},
earth:{pyro:1,hydro:1,flora:1,zephyr:0.5,earth:1,volt:2,cosmos:1},
volt:{pyro:1,hydro:1,flora:1,zephyr:2,earth:0.5,volt:1,cosmos:1},
cosmos:{pyro:1,hydro:1,flora:1,zephyr:1,earth:1,volt:1,cosmos:1}
};
// ── main ──
const game = new GameEngine();
game.init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"a484a69d-fb7d-4b04-8a6f-2006a32a3742","tokensIn":2327823,"tokensOut":67631,"tokensTotal":2395454,"turns":45,"toolCalls":44,"failedToolCalls":3,"timestamp":"2026-07-16T03:10:32.229Z"} -->